1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # ============================================================================
20 __appname__ = 'FeedingIt'
21 __author__ = 'Yves Marcoz'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
27 from pango import FontDescription
32 from webkit import WebView
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from threading import Thread, activeCount
43 from feedingitdbus import ServerObject
44 from updatedbus import UpdateServerObject, get_lock
45 from config import Config
46 from cgi import escape
48 from rss import Listing
49 from opml import GetOpmlData, ExportOpmlData
51 from urllib2 import install_opener, build_opener
53 from socket import setdefaulttimeout
55 setdefaulttimeout(timeout)
61 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
62 ABOUT_ICON = 'feedingit'
63 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
64 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
65 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
66 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
68 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
69 unread_color = color_style.lookup_color('ActiveTextColor')
70 read_color = color_style.lookup_color('DefaultTextColor')
73 CONFIGDIR="/home/user/.feedingit/"
74 LOCK = CONFIGDIR + "update.lock"
77 from htmlentitydefs import name2codepoint
79 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
81 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
85 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
86 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
87 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
89 # Build the markup template for the Maemo 5 text style
90 head_font = style.get_font_desc('SystemFont')
91 sub_font = style.get_font_desc('SmallSystemFont')
93 head_color = style.get_color('ButtonTextColor')
94 sub_color = style.get_color('DefaultTextColor')
95 active_color = style.get_color('ActiveTextColor')
97 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
98 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
100 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
101 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
103 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
104 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
106 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
107 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
109 FEED_TEMPLATE = '\n'.join((head, normal_sub))
110 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
112 ENTRY_TEMPLATE = entry_head
113 ENTRY_TEMPLATE_UNREAD = entry_active_head
116 # Removes HTML or XML character references and entities from a text string.
118 # @param text The HTML (or XML) source text.
119 # @return The plain text, as a Unicode string, if necessary.
120 # http://effbot.org/zone/re-sub.htm#unescape-html
125 # character reference
127 if text[:3] == "&#x":
128 return unichr(int(text[3:-1], 16))
130 return unichr(int(text[2:-1]))
136 text = unichr(name2codepoint[text[1:-1]])
139 return text # leave as is
140 return sub("&#?\w+;", fixup, text)
143 class AddWidgetWizard(hildon.WizardDialog):
145 def __init__(self, parent, urlIn, titleIn=None):
147 self.notebook = gtk.Notebook()
149 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
150 self.nameEntry.set_placeholder("Enter Feed Name")
151 vbox = gtk.VBox(False,10)
152 label = gtk.Label("Enter Feed Name:")
153 vbox.pack_start(label)
154 vbox.pack_start(self.nameEntry)
155 if not titleIn == None:
156 self.nameEntry.set_text(titleIn)
157 self.notebook.append_page(vbox, None)
159 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
160 self.urlEntry.set_placeholder("Enter a URL")
161 self.urlEntry.set_text(urlIn)
162 self.urlEntry.select_region(0,-1)
164 vbox = gtk.VBox(False,10)
165 label = gtk.Label("Enter Feed URL:")
166 vbox.pack_start(label)
167 vbox.pack_start(self.urlEntry)
168 self.notebook.append_page(vbox, None)
170 labelEnd = gtk.Label("Success")
172 self.notebook.append_page(labelEnd, None)
174 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
176 # Set a handler for "switch-page" signal
177 #self.notebook.connect("switch_page", self.on_page_switch, self)
179 # Set a function to decide if user can go to next page
180 self.set_forward_page_func(self.some_page_func)
185 return (self.nameEntry.get_text(), self.urlEntry.get_text())
187 def on_page_switch(self, notebook, page, num, dialog):
190 def some_page_func(self, nb, current, userdata):
191 # Validate data for 1st page
193 return len(self.nameEntry.get_text()) != 0
195 # Check the url is not null, and starts with http
196 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
202 class Download(Thread):
203 def __init__(self, listing, key, config):
204 Thread.__init__(self)
205 self.listing = listing
210 (use_proxy, proxy) = self.config.getProxy()
211 key_lock = get_lock(self.key)
214 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
216 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
220 class DownloadBar(gtk.ProgressBar):
221 def __init__(self, parent, listing, listOfKeys, config, single=False):
223 update_lock = get_lock("update_lock")
224 if update_lock != None:
225 gtk.ProgressBar.__init__(self)
226 self.listOfKeys = listOfKeys[:]
227 self.listing = listing
228 self.total = len(self.listOfKeys)
232 (use_proxy, proxy) = self.config.getProxy()
234 opener = build_opener(proxy)
236 opener = build_opener()
238 opener.addheaders = [('User-agent', USER_AGENT)]
239 install_opener(opener)
242 # In preparation for i18n/l10n
244 return (a if n == 1 else b)
246 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
249 self.set_fraction(self.fraction)
252 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
254 def update_progress_bar(self):
255 #self.progress_bar.pulse()
256 if activeCount() < 4:
257 x = activeCount() - 1
258 k = len(self.listOfKeys)
259 fin = self.total - k - x
260 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
261 #print x, k, fin, fraction
262 self.set_fraction(fraction)
264 if len(self.listOfKeys)>0:
265 self.current = self.current+1
266 key = self.listOfKeys.pop()
267 #if self.single == True:
268 # Check if the feed is being displayed
269 download = Download(self.listing, key, self.config)
272 elif activeCount() > 1:
275 #self.waitingWindow.destroy()
281 self.emit("download-done", "success")
286 class SortList(hildon.StackableWindow):
287 def __init__(self, parent, listing, feedingit, after_closing):
288 hildon.StackableWindow.__init__(self)
289 self.set_transient_for(parent)
290 self.set_title('Subscriptions')
291 self.listing = listing
292 self.feedingit = feedingit
293 self.after_closing = after_closing
294 self.connect('destroy', lambda w: self.after_closing())
295 self.vbox2 = gtk.VBox(False, 2)
297 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
298 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
299 button.connect("clicked", self.buttonUp)
300 self.vbox2.pack_start(button, expand=False, fill=False)
302 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
303 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
304 button.connect("clicked", self.buttonDown)
305 self.vbox2.pack_start(button, expand=False, fill=False)
307 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
309 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
310 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
311 button.connect("clicked", self.buttonAdd)
312 self.vbox2.pack_start(button, expand=False, fill=False)
314 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
315 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
316 button.connect("clicked", self.buttonEdit)
317 self.vbox2.pack_start(button, expand=False, fill=False)
319 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
320 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
321 button.connect("clicked", self.buttonDelete)
322 self.vbox2.pack_start(button, expand=False, fill=False)
324 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
325 #button.set_label("Done")
326 #button.connect("clicked", self.buttonDone)
327 #self.vbox.pack_start(button)
328 self.hbox2= gtk.HBox(False, 10)
329 self.pannableArea = hildon.PannableArea()
330 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
331 self.treeview = gtk.TreeView(self.treestore)
332 self.hbox2.pack_start(self.pannableArea, expand=True)
334 self.hbox2.pack_end(self.vbox2, expand=False)
335 self.set_default_size(-1, 600)
338 menu = hildon.AppMenu()
339 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
340 button.set_label("Import from OPML")
341 button.connect("clicked", self.feedingit.button_import_clicked)
344 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
345 button.set_label("Export to OPML")
346 button.connect("clicked", self.feedingit.button_export_clicked)
348 self.set_app_menu(menu)
352 #self.connect("destroy", self.buttonDone)
354 def displayFeeds(self):
355 self.treeview.destroy()
356 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
357 self.treeview = gtk.TreeView()
359 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
360 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
362 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
364 self.pannableArea.add(self.treeview)
368 def refreshList(self, selected=None, offset=0):
369 #rect = self.treeview.get_visible_rect()
370 #y = rect.y+rect.height
371 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
372 for key in self.listing.getListOfFeeds():
373 item = self.treestore.append([self.listing.getFeedTitle(key), key])
376 self.treeview.set_model(self.treestore)
377 if not selected == None:
378 self.treeview.get_selection().select_iter(selectedItem)
379 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
380 self.pannableArea.show_all()
382 def getSelectedItem(self):
383 (model, iter) = self.treeview.get_selection().get_selected()
386 return model.get_value(iter, 1)
388 def findIndex(self, key):
392 for row in self.treestore:
394 return (before, row.iter)
395 if key == list(row)[0]:
399 return (before, None)
401 def buttonUp(self, button):
402 key = self.getSelectedItem()
404 self.listing.moveUp(key)
405 self.refreshList(key, -10)
407 def buttonDown(self, button):
408 key = self.getSelectedItem()
410 self.listing.moveDown(key)
411 self.refreshList(key, 10)
413 def buttonDelete(self, button):
414 key = self.getSelectedItem()
416 if key == 'ArchivedArticles':
417 message = 'Cannot remove the archived articles feed.'
418 hildon.hildon_banner_show_information(self, '', message)
419 elif key is not None:
420 message = 'Really remove this feed and its entries?'
421 dlg = hildon.hildon_note_new_confirmation(self, message)
424 if response == gtk.RESPONSE_OK:
425 self.listing.removeFeed(key)
428 def buttonEdit(self, button):
429 key = self.getSelectedItem()
431 if key == 'ArchivedArticles':
432 message = 'Cannot edit the archived articles feed.'
433 hildon.hildon_banner_show_information(self, '', message)
437 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
440 (title, url) = wizard.getData()
441 if (not title == '') and (not url == ''):
442 self.listing.editFeed(key, title, url)
446 def buttonDone(self, *args):
449 def buttonAdd(self, button, urlIn="http://"):
450 wizard = AddWidgetWizard(self, urlIn)
453 (title, url) = wizard.getData()
454 if (not title == '') and (not url == ''):
455 self.listing.addFeed(title, url)
460 class DisplayArticle(hildon.StackableWindow):
461 def __init__(self, feed, id, key, config, listing):
462 hildon.StackableWindow.__init__(self)
463 #self.imageDownloader = ImageDownloader()
468 #self.set_title(feed.getTitle(id))
469 self.set_title(self.listing.getFeedTitle(key))
471 self.set_for_removal = False
473 # Init the article display
474 #if self.config.getWebkitSupport():
475 self.view = WebView()
476 #self.view.set_editable(False)
479 # self.view = gtkhtml2.View()
480 # self.document = gtkhtml2.Document()
481 # self.view.set_document(self.document)
482 # self.document.connect("link_clicked", self._signal_link_clicked)
483 self.pannable_article = hildon.PannableArea()
484 self.pannable_article.add(self.view)
485 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
486 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
488 #if self.config.getWebkitSupport():
489 contentLink = self.feed.getContentLink(self.id)
490 self.feed.setEntryRead(self.id)
491 #if key=="ArchivedArticles":
492 if contentLink.startswith("/home/user/"):
493 self.view.open("file://" + contentLink)
495 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
496 self.view.connect("motion-notify-event", lambda w,ev: True)
497 self.view.connect('load-started', self.load_started)
498 self.view.connect('load-finished', self.load_finished)
501 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
502 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
504 # if not key == "ArchivedArticles":
505 # Do not download images if the feed is "Archived Articles"
506 # self.document.connect("request-url", self._signal_request_url)
508 # self.document.clear()
509 # self.document.open_stream("text/html")
510 # self.document.write_stream(self.text)
511 # self.document.close_stream()
513 menu = hildon.AppMenu()
514 # Create a button and add it to the menu
515 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
516 button.set_label("Allow horizontal scrolling")
517 button.connect("clicked", self.horiz_scrolling_button)
520 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
521 button.set_label("Open in browser")
522 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
525 if key == "ArchivedArticles":
526 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
527 button.set_label("Remove from archived articles")
528 button.connect("clicked", self.remove_archive_button)
530 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
531 button.set_label("Add to archived articles")
532 button.connect("clicked", self.archive_button)
535 self.set_app_menu(menu)
538 #self.event_box = gtk.EventBox()
539 #self.event_box.add(self.pannable_article)
540 self.add(self.pannable_article)
543 self.pannable_article.show_all()
545 self.destroyId = self.connect("destroy", self.destroyWindow)
547 self.view.connect("button_press_event", self.button_pressed)
548 self.gestureId = self.view.connect("button_release_event", self.button_released)
549 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
551 def load_started(self, *widget):
552 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
554 def load_finished(self, *widget):
555 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
557 def button_pressed(self, window, event):
558 #print event.x, event.y
559 self.coords = (event.x, event.y)
561 def button_released(self, window, event):
562 x = self.coords[0] - event.x
563 y = self.coords[1] - event.y
565 if (2*abs(y) < abs(x)):
567 self.emit("article-previous", self.id)
569 self.emit("article-next", self.id)
573 #def gesture(self, widget, direction, startx, starty):
574 # if (direction == 3):
575 # self.emit("article-next", self.index)
576 # if (direction == 2):
577 # self.emit("article-previous", self.index)
578 #print startx, starty
579 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
581 def destroyWindow(self, *args):
582 self.disconnect(self.destroyId)
583 if self.set_for_removal:
584 self.emit("article-deleted", self.id)
586 self.emit("article-closed", self.id)
587 #self.imageDownloader.stopAll()
590 def horiz_scrolling_button(self, *widget):
591 self.pannable_article.disconnect(self.gestureId)
592 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
594 def archive_button(self, *widget):
595 # Call the listing.addArchivedArticle
596 self.listing.addArchivedArticle(self.key, self.id)
598 def remove_archive_button(self, *widget):
599 self.set_for_removal = True
601 #def reloadArticle(self, *widget):
602 # if threading.activeCount() > 1:
603 # Image thread are still running, come back in a bit
606 # for (stream, imageThread) in self.images:
608 # stream.write(imageThread.data)
613 def _signal_link_clicked(self, object, link):
615 bus = dbus.SessionBus()
616 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
617 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
618 iface.open_new_window(link)
620 #def _signal_request_url(self, object, url, stream):
622 # self.imageDownloader.queueImage(url, stream)
623 #imageThread = GetImage(url)
625 #self.images.append((stream, imageThread))
628 class DisplayFeed(hildon.StackableWindow):
629 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
630 hildon.StackableWindow.__init__(self)
631 self.listing = listing
633 self.feedTitle = title
634 self.set_title(title)
637 self.updateDbusHandler = updateDbusHandler
639 self.downloadDialog = False
641 #self.listing.setCurrentlyDisplayedFeed(self.key)
645 menu = hildon.AppMenu()
646 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
647 button.set_label("Update feed")
648 button.connect("clicked", self.button_update_clicked)
651 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
652 button.set_label("Mark all as read")
653 button.connect("clicked", self.buttonReadAllClicked)
656 if key=="ArchivedArticles":
657 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
658 button.set_label("Delete read articles")
659 button.connect("clicked", self.buttonPurgeArticles)
662 self.set_app_menu(menu)
667 self.connect('configure-event', self.on_configure_event)
668 self.connect("destroy", self.destroyWindow)
670 def on_configure_event(self, window, event):
671 if getattr(self, 'markup_renderer', None) is None:
674 # Fix up the column width for wrapping the text when the window is
675 # resized (i.e. orientation changed)
676 self.markup_renderer.set_property('wrap-width', event.width-20)
677 it = self.feedItems.get_iter_first()
678 while it is not None:
679 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
680 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
681 it = self.feedItems.iter_next(it)
683 def destroyWindow(self, *args):
684 #self.feed.saveUnread(CONFIGDIR)
685 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
686 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
687 self.emit("feed-closed", self.key)
689 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
690 #self.listing.closeCurrentlyDisplayedFeed()
692 def fix_title(self, title):
693 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
695 def displayFeed(self):
696 self.pannableFeed = hildon.PannableArea()
698 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
700 self.feedItems = gtk.ListStore(str, str)
701 #self.feedList = gtk.TreeView(self.feedItems)
702 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
703 selection = self.feedList.get_selection()
704 selection.set_mode(gtk.SELECTION_NONE)
705 #selection.connect("changed", lambda w: True)
707 self.feedList.set_model(self.feedItems)
708 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
711 self.feedList.set_hover_selection(False)
712 #self.feedList.set_property('enable-grid-lines', True)
713 #self.feedList.set_property('hildon-mode', 1)
714 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
716 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
718 vbox= gtk.VBox(False, 10)
719 vbox.pack_start(self.feedList)
721 self.pannableFeed.add_with_viewport(vbox)
723 self.markup_renderer = gtk.CellRendererText()
724 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
725 self.markup_renderer.set_property('background', "#333333")
726 (width, height) = self.get_size()
727 self.markup_renderer.set_property('wrap-width', width-20)
728 self.markup_renderer.set_property('ypad', 5)
729 self.markup_renderer.set_property('xpad', 5)
730 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
731 markup=FEED_COLUMN_MARKUP)
732 self.feedList.append_column(markup_column)
734 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
735 hideReadArticles = self.config.getHideReadArticles()
737 for id in self.feed.getIds():
740 isRead = self.feed.isEntryRead(id)
743 if not ( isRead and hideReadArticles ):
744 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
745 #title = self.feed.getTitle(id)
746 title = self.fix_title(self.feed.getTitle(id))
748 #if self.feed.isEntryRead(id):
750 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
752 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
754 self.feedItems.append((markup, id))
757 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
759 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
760 self.feedItems.append((markup, ""))
762 self.add(self.pannableFeed)
766 self.pannableFeed.destroy()
767 #self.remove(self.pannableFeed)
769 def on_feedList_row_activated(self, treeview, path): #, column):
770 selection = self.feedList.get_selection()
771 selection.set_mode(gtk.SELECTION_SINGLE)
772 self.feedList.get_selection().select_path(path)
773 model = treeview.get_model()
774 iter = model.get_iter(path)
775 key = model.get_value(iter, FEED_COLUMN_KEY)
776 # Emulate legacy "button_clicked" call via treeview
777 gobject.idle_add(self.button_clicked, treeview, key)
780 def button_clicked(self, button, index, previous=False, next=False):
781 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
782 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
783 stack = hildon.WindowStack.get_default()
786 stack.pop_and_push(1, newDisp, tmp)
788 gobject.timeout_add(200, self.destroyArticle, tmp)
793 if type(self.disp).__name__ == "DisplayArticle":
794 gobject.timeout_add(200, self.destroyArticle, self.disp)
801 if self.key == "ArchivedArticles":
802 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
803 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
804 self.ids.append(self.disp.connect("article-next", self.nextArticle))
805 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
807 def buttonPurgeArticles(self, *widget):
809 self.feed.purgeReadArticles()
810 self.feed.saveUnread(CONFIGDIR)
811 self.feed.saveFeed(CONFIGDIR)
814 def destroyArticle(self, handle):
815 handle.destroyWindow()
817 def mark_item_read(self, key):
818 it = self.feedItems.get_iter_first()
819 while it is not None:
820 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
822 title = self.fix_title(self.feed.getTitle(key))
823 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
824 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
826 it = self.feedItems.iter_next(it)
828 def nextArticle(self, object, index):
829 self.mark_item_read(index)
830 id = self.feed.getNextId(index)
831 if self.config.getHideReadArticles():
834 isRead = self.feed.isEntryRead(id)
837 while isRead and id != index:
838 id = self.feed.getNextId(id)
841 isRead = self.feed.isEntryRead(id)
845 self.button_clicked(object, id, next=True)
847 def previousArticle(self, object, index):
848 self.mark_item_read(index)
849 id = self.feed.getPreviousId(index)
850 if self.config.getHideReadArticles():
853 isRead = self.feed.isEntryRead(id)
856 while isRead and id != index:
857 id = self.feed.getPreviousId(id)
860 isRead = self.feed.isEntryRead(id)
864 self.button_clicked(object, id, previous=True)
866 def onArticleClosed(self, object, index):
867 selection = self.feedList.get_selection()
868 selection.set_mode(gtk.SELECTION_NONE)
869 self.mark_item_read(index)
871 def onArticleDeleted(self, object, index):
873 self.feed.removeArticle(index)
874 self.feed.saveUnread(CONFIGDIR)
875 self.feed.saveFeed(CONFIGDIR)
878 def button_update_clicked(self, button):
879 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
880 if not type(self.downloadDialog).__name__=="DownloadBar":
881 self.pannableFeed.destroy()
882 self.vbox = gtk.VBox(False, 10)
883 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
884 self.downloadDialog.connect("download-done", self.onDownloadsDone)
885 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
889 def onDownloadsDone(self, *widget):
891 self.feed = self.listing.getFeed(self.key)
893 self.updateDbusHandler.ArticleCountUpdated()
895 def buttonReadAllClicked(self, button):
896 for index in self.feed.getIds():
897 self.feed.setEntryRead(index)
898 self.mark_item_read(index)
904 self.window = hildon.StackableWindow()
905 self.window.set_title(__appname__)
906 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
907 self.mainVbox = gtk.VBox(False,10)
909 self.introLabel = gtk.Label("Loading...")
912 self.mainVbox.pack_start(self.introLabel)
914 self.window.add(self.mainVbox)
915 self.window.show_all()
916 self.config = Config(self.window, CONFIGDIR+"config.ini")
917 gobject.idle_add(self.createWindow)
919 def createWindow(self):
920 self.app_lock = get_lock("app_lock")
921 if self.app_lock == None:
922 self.introLabel.set_label("Update in progress, please wait.")
923 gobject.timeout_add_seconds(3, self.createWindow)
925 self.listing = Listing(CONFIGDIR)
927 self.downloadDialog = False
929 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
930 self.orientation.set_mode(self.config.getOrientation())
932 print "Could not start rotation manager"
934 menu = hildon.AppMenu()
935 # Create a button and add it to the menu
936 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
937 button.set_label("Update feeds")
938 button.connect("clicked", self.button_update_clicked, "All")
941 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
942 button.set_label("Mark all as read")
943 button.connect("clicked", self.button_markAll)
946 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
947 button.set_label("Manage subscriptions")
948 button.connect("clicked", self.button_organize_clicked)
951 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
952 button.set_label("Settings")
953 button.connect("clicked", self.button_preferences_clicked)
956 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
957 button.set_label("About")
958 button.connect("clicked", self.button_about_clicked)
961 self.window.set_app_menu(menu)
964 #self.feedWindow = hildon.StackableWindow()
965 #self.articleWindow = hildon.StackableWindow()
966 self.introLabel.destroy()
967 self.pannableListing = hildon.PannableArea()
968 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
969 self.feedList = gtk.TreeView(self.feedItems)
970 self.feedList.connect('row-activated', self.on_feedList_row_activated)
971 self.pannableListing.add(self.feedList)
973 icon_renderer = gtk.CellRendererPixbuf()
974 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
975 icon_column = gtk.TreeViewColumn('', icon_renderer, \
977 self.feedList.append_column(icon_column)
979 markup_renderer = gtk.CellRendererText()
980 markup_column = gtk.TreeViewColumn('', markup_renderer, \
981 markup=COLUMN_MARKUP)
982 self.feedList.append_column(markup_column)
983 self.mainVbox.pack_start(self.pannableListing)
984 self.mainVbox.show_all()
986 self.displayListing()
987 self.autoupdate = False
988 self.checkAutoUpdate()
989 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
990 gobject.idle_add(self.enableDbus)
992 def enableDbus(self):
993 self.dbusHandler = ServerObject(self)
994 self.updateDbusHandler = UpdateServerObject(self)
996 def button_markAll(self, button):
997 for key in self.listing.getListOfFeeds():
998 feed = self.listing.getFeed(key)
999 for id in feed.getIds():
1000 feed.setEntryRead(id)
1001 feed.saveUnread(CONFIGDIR)
1002 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
1003 self.displayListing()
1005 def button_about_clicked(self, button):
1006 HeAboutDialog.present(self.window, \
1016 def button_export_clicked(self, button):
1017 opml = ExportOpmlData(self.window, self.listing)
1019 def button_import_clicked(self, button):
1020 opml = GetOpmlData(self.window)
1021 feeds = opml.getData()
1022 for (title, url) in feeds:
1023 self.listing.addFeed(title, url)
1024 self.displayListing()
1026 def addFeed(self, urlIn="http://"):
1027 wizard = AddWidgetWizard(self.window, urlIn)
1030 (title, url) = wizard.getData()
1031 if (not title == '') and (not url == ''):
1032 self.listing.addFeed(title, url)
1034 self.displayListing()
1036 def button_organize_clicked(self, button):
1037 def after_closing():
1038 self.listing.saveConfig()
1039 self.displayListing()
1040 SortList(self.window, self.listing, self, after_closing)
1042 def button_update_clicked(self, button, key):
1043 if not type(self.downloadDialog).__name__=="DownloadBar":
1044 self.updateDbusHandler.UpdateStarted()
1045 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1046 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1047 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1048 self.mainVbox.show_all()
1049 #self.displayListing()
1051 def onDownloadsDone(self, *widget):
1052 self.downloadDialog.destroy()
1053 self.downloadDialog = False
1054 self.displayListing()
1055 self.updateDbusHandler.UpdateFinished()
1056 self.updateDbusHandler.ArticleCountUpdated()
1058 def button_preferences_clicked(self, button):
1059 dialog = self.config.createDialog()
1060 dialog.connect("destroy", self.prefsClosed)
1062 def show_confirmation_note(self, parent, title):
1063 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1065 retcode = gtk.Dialog.run(note)
1068 if retcode == gtk.RESPONSE_OK:
1073 def displayListing(self):
1074 icon_theme = gtk.icon_theme_get_default()
1075 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1076 gtk.ICON_LOOKUP_USE_BUILTIN)
1078 self.feedItems.clear()
1079 for key in self.listing.getListOfFeeds():
1080 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1081 if unreadItems > 0 or not self.config.getHideReadFeeds():
1082 title = self.listing.getFeedTitle(key)
1083 updateTime = self.listing.getFeedUpdateTime(key)
1085 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1088 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1090 markup = FEED_TEMPLATE % (title, subtitle)
1093 icon_filename = self.listing.getFavicon(key)
1094 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1095 LIST_ICON_SIZE, LIST_ICON_SIZE)
1097 pixbuf = default_pixbuf
1099 self.feedItems.append((pixbuf, markup, key))
1101 def on_feedList_row_activated(self, treeview, path, column):
1102 model = treeview.get_model()
1103 iter = model.get_iter(path)
1104 key = model.get_value(iter, COLUMN_KEY)
1107 def openFeed(self, key):
1111 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1112 self.feed_lock = get_lock(key)
1113 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1114 self.listing.getFeedTitle(key), key, \
1115 self.config, self.updateDbusHandler)
1116 self.disp.connect("feed-closed", self.onFeedClosed)
1119 def onFeedClosed(self, object, key):
1120 #self.listing.saveConfig()
1122 gobject.idle_add(self.onFeedClosedTimeout)
1123 self.displayListing()
1124 #self.updateDbusHandler.ArticleCountUpdated()
1126 def onFeedClosedTimeout(self):
1127 self.listing.saveConfig()
1129 self.updateDbusHandler.ArticleCountUpdated()
1132 self.window.connect("destroy", gtk.main_quit)
1134 self.listing.saveConfig()
1137 def prefsClosed(self, *widget):
1139 self.orientation.set_mode(self.config.getOrientation())
1142 self.displayListing()
1143 self.checkAutoUpdate()
1145 def checkAutoUpdate(self, *widget):
1146 interval = int(self.config.getUpdateInterval()*3600000)
1147 if self.config.isAutoUpdateEnabled():
1148 if self.autoupdate == False:
1149 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1150 self.autoupdate = interval
1151 elif not self.autoupdate == interval:
1152 # If auto-update is enabled, but not at the right frequency
1153 gobject.source_remove(self.autoupdateId)
1154 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1155 self.autoupdate = interval
1157 if not self.autoupdate == False:
1158 gobject.source_remove(self.autoupdateId)
1159 self.autoupdate = False
1161 def automaticUpdate(self, *widget):
1162 # Need to check for internet connection
1163 # If no internet connection, try again in 10 minutes:
1164 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1165 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1166 #from time import localtime, strftime
1167 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1169 self.button_update_clicked(None, None)
1172 def stopUpdate(self):
1173 # Not implemented in the app (see update_feeds.py)
1175 self.downloadDialog.listOfKeys = []
1179 def getStatus(self):
1181 for key in self.listing.getListOfFeeds():
1182 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1183 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1185 status = "No unread items"
1188 if __name__ == "__main__":
1189 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1190 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1191 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1192 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1193 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1194 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1195 gobject.threads_init()
1196 if not isdir(CONFIGDIR):
1200 print "Error: Can't create configuration directory"
1201 from sys import exit