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_sqlite import Listing
49 from opml import GetOpmlData, ExportOpmlData
51 from urllib2 import install_opener, build_opener
53 from socket import setdefaulttimeout
55 setdefaulttimeout(timeout)
63 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
64 ABOUT_ICON = 'feedingit'
65 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
66 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
67 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
68 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
70 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
71 unread_color = color_style.lookup_color('ActiveTextColor')
72 read_color = color_style.lookup_color('DefaultTextColor')
75 CONFIGDIR="/home/user/.feedingit/"
76 LOCK = CONFIGDIR + "update.lock"
79 from htmlentitydefs import name2codepoint
81 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
83 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
87 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
88 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
89 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
91 # Build the markup template for the Maemo 5 text style
92 head_font = style.get_font_desc('SystemFont')
93 sub_font = style.get_font_desc('SmallSystemFont')
95 #head_color = style.get_color('ButtonTextColor')
96 head_color = style.get_color('DefaultTextColor')
97 sub_color = style.get_color('DefaultTextColor')
98 active_color = style.get_color('ActiveTextColor')
100 bg_color = style.get_color('DefaultBackgroundColor').to_string()
101 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
102 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
103 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
104 bg_color = "#" + c1 + c2 + c3
107 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
108 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
110 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
111 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
113 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
114 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
116 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
117 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
119 FEED_TEMPLATE = '\n'.join((head, normal_sub))
120 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
122 ENTRY_TEMPLATE = entry_head
123 ENTRY_TEMPLATE_UNREAD = entry_active_head
126 # Removes HTML or XML character references and entities from a text string.
128 # @param text The HTML (or XML) source text.
129 # @return The plain text, as a Unicode string, if necessary.
130 # http://effbot.org/zone/re-sub.htm#unescape-html
135 # character reference
137 if text[:3] == "&#x":
138 return unichr(int(text[3:-1], 16))
140 return unichr(int(text[2:-1]))
146 text = unichr(name2codepoint[text[1:-1]])
149 return text # leave as is
150 return sub("&#?\w+;", fixup, text)
153 class AddWidgetWizard(gtk.Dialog):
154 def __init__(self, parent, urlIn, titleIn=None, isEdit=False):
155 gtk.Dialog.__init__(self)
156 self.set_transient_for(parent)
159 self.set_title('Edit RSS feed')
161 self.set_title('Add new RSS feed')
164 self.btn_add = self.add_button('Save', 2)
166 self.btn_add = self.add_button('Add', 2)
168 self.set_default_response(2)
170 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
171 self.nameEntry.set_placeholder('Feed name')
172 if not titleIn == None:
173 self.nameEntry.set_text(titleIn)
174 self.nameEntry.select_region(-1, -1)
176 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
177 self.urlEntry.set_placeholder('Feed URL')
178 self.urlEntry.set_text(urlIn)
179 self.urlEntry.select_region(-1, -1)
180 self.urlEntry.set_activates_default(True)
182 self.table = gtk.Table(2, 2, False)
183 self.table.set_col_spacings(5)
184 label = gtk.Label('Name:')
185 label.set_alignment(1., .5)
186 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
187 self.table.attach(self.nameEntry, 1, 2, 0, 1)
188 label = gtk.Label('URL:')
189 label.set_alignment(1., .5)
190 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
191 self.table.attach(self.urlEntry, 1, 2, 1, 2)
192 self.vbox.pack_start(self.table)
197 return (self.nameEntry.get_text(), self.urlEntry.get_text())
199 def some_page_func(self, nb, current, userdata):
200 # Validate data for 1st page
202 return len(self.nameEntry.get_text()) != 0
204 # Check the url is not null, and starts with http
205 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
211 class Download(Thread):
212 def __init__(self, listing, key, config):
213 Thread.__init__(self)
214 self.listing = listing
219 (use_proxy, proxy) = self.config.getProxy()
220 key_lock = get_lock(self.key)
223 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
225 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
229 class DownloadBar(gtk.ProgressBar):
230 def __init__(self, parent, listing, listOfKeys, config, single=False):
232 update_lock = get_lock("update_lock")
233 if update_lock != None:
234 gtk.ProgressBar.__init__(self)
235 self.listOfKeys = listOfKeys[:]
236 self.listing = listing
237 self.total = len(self.listOfKeys)
241 (use_proxy, proxy) = self.config.getProxy()
243 opener = build_opener(proxy)
245 opener = build_opener()
247 opener.addheaders = [('User-agent', USER_AGENT)]
248 install_opener(opener)
251 # In preparation for i18n/l10n
253 return (a if n == 1 else b)
255 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
258 self.set_fraction(self.fraction)
261 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
263 def update_progress_bar(self):
264 #self.progress_bar.pulse()
265 if activeCount() < 4:
266 x = activeCount() - 1
267 k = len(self.listOfKeys)
268 fin = self.total - k - x
269 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
270 #print x, k, fin, fraction
271 self.set_fraction(fraction)
273 if len(self.listOfKeys)>0:
274 self.current = self.current+1
275 key = self.listOfKeys.pop()
276 #if self.single == True:
277 # Check if the feed is being displayed
278 download = Download(self.listing, key, self.config)
281 elif activeCount() > 1:
284 #self.waitingWindow.destroy()
290 self.emit("download-done", "success")
295 class SortList(hildon.StackableWindow):
296 def __init__(self, parent, listing, feedingit, after_closing):
297 hildon.StackableWindow.__init__(self)
298 self.set_transient_for(parent)
299 self.set_title('Subscriptions')
300 self.listing = listing
301 self.feedingit = feedingit
302 self.after_closing = after_closing
303 self.connect('destroy', lambda w: self.after_closing())
304 self.vbox2 = gtk.VBox(False, 2)
306 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
307 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
308 button.connect("clicked", self.buttonUp)
309 self.vbox2.pack_start(button, expand=False, fill=False)
311 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
312 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
313 button.connect("clicked", self.buttonDown)
314 self.vbox2.pack_start(button, expand=False, fill=False)
316 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
318 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
319 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
320 button.connect("clicked", self.buttonAdd)
321 self.vbox2.pack_start(button, expand=False, fill=False)
323 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
324 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
325 button.connect("clicked", self.buttonEdit)
326 self.vbox2.pack_start(button, expand=False, fill=False)
328 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
329 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
330 button.connect("clicked", self.buttonDelete)
331 self.vbox2.pack_start(button, expand=False, fill=False)
333 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
334 #button.set_label("Done")
335 #button.connect("clicked", self.buttonDone)
336 #self.vbox.pack_start(button)
337 self.hbox2= gtk.HBox(False, 10)
338 self.pannableArea = hildon.PannableArea()
339 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
340 self.treeview = gtk.TreeView(self.treestore)
341 self.hbox2.pack_start(self.pannableArea, expand=True)
343 self.hbox2.pack_end(self.vbox2, expand=False)
344 self.set_default_size(-1, 600)
347 menu = hildon.AppMenu()
348 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
349 button.set_label("Import from OPML")
350 button.connect("clicked", self.feedingit.button_import_clicked)
353 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
354 button.set_label("Export to OPML")
355 button.connect("clicked", self.feedingit.button_export_clicked)
357 self.set_app_menu(menu)
361 #self.connect("destroy", self.buttonDone)
363 def displayFeeds(self):
364 self.treeview.destroy()
365 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
366 self.treeview = gtk.TreeView()
368 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
369 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
371 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
373 self.pannableArea.add(self.treeview)
377 def refreshList(self, selected=None, offset=0):
378 #rect = self.treeview.get_visible_rect()
379 #y = rect.y+rect.height
380 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
381 for key in self.listing.getListOfFeeds():
382 item = self.treestore.append([self.listing.getFeedTitle(key), key])
385 self.treeview.set_model(self.treestore)
386 if not selected == None:
387 self.treeview.get_selection().select_iter(selectedItem)
388 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
389 self.pannableArea.show_all()
391 def getSelectedItem(self):
392 (model, iter) = self.treeview.get_selection().get_selected()
395 return model.get_value(iter, 1)
397 def findIndex(self, key):
401 for row in self.treestore:
403 return (before, row.iter)
404 if key == list(row)[0]:
408 return (before, None)
410 def buttonUp(self, button):
411 key = self.getSelectedItem()
413 self.listing.moveUp(key)
414 self.refreshList(key, -10)
416 def buttonDown(self, button):
417 key = self.getSelectedItem()
419 self.listing.moveDown(key)
420 self.refreshList(key, 10)
422 def buttonDelete(self, button):
423 key = self.getSelectedItem()
425 message = 'Really remove this feed and its entries?'
426 dlg = hildon.hildon_note_new_confirmation(self, message)
429 if response == gtk.RESPONSE_OK:
430 self.listing.removeFeed(key)
433 def buttonEdit(self, button):
434 key = self.getSelectedItem()
436 if key == 'ArchivedArticles':
437 message = 'Cannot edit the archived articles feed.'
438 hildon.hildon_banner_show_information(self, '', message)
442 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True)
445 (title, url) = wizard.getData()
446 if (not title == '') and (not url == ''):
447 self.listing.editFeed(key, title, url)
451 def buttonDone(self, *args):
454 def buttonAdd(self, button, urlIn="http://"):
455 wizard = AddWidgetWizard(self, urlIn)
458 (title, url) = wizard.getData()
459 if (not title == '') and (not url == ''):
460 self.listing.addFeed(title, url)
465 class DisplayArticle(hildon.StackableWindow):
466 def __init__(self, feed, id, key, config, listing):
467 hildon.StackableWindow.__init__(self)
468 #self.imageDownloader = ImageDownloader()
473 #self.set_title(feed.getTitle(id))
474 self.set_title(self.listing.getFeedTitle(key))
476 self.set_for_removal = False
478 # Init the article display
479 #if self.config.getWebkitSupport():
480 self.view = WebView()
481 #self.view.set_editable(False)
484 # self.view = gtkhtml2.View()
485 # self.document = gtkhtml2.Document()
486 # self.view.set_document(self.document)
487 # self.document.connect("link_clicked", self._signal_link_clicked)
488 self.pannable_article = hildon.PannableArea()
489 self.pannable_article.add(self.view)
490 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
491 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
493 #if self.config.getWebkitSupport():
494 contentLink = self.feed.getContentLink(self.id)
495 self.feed.setEntryRead(self.id)
496 #if key=="ArchivedArticles":
497 self.loadedArticle = False
498 if contentLink.startswith("/home/user/"):
499 self.view.open("file://%s" % contentLink)
500 self.currentUrl = self.feed.getExternalLink(self.id)
502 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
503 self.currentUrl = "%s" % contentLink
504 self.view.connect("motion-notify-event", lambda w,ev: True)
505 self.view.connect('load-started', self.load_started)
506 self.view.connect('load-finished', self.load_finished)
508 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
510 menu = hildon.AppMenu()
511 # Create a button and add it to the menu
512 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
513 button.set_label("Allow horizontal scrolling")
514 button.connect("clicked", self.horiz_scrolling_button)
517 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
518 button.set_label("Open in browser")
519 button.connect("clicked", self.open_in_browser)
522 if key == "ArchivedArticles":
523 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
524 button.set_label("Remove from archived articles")
525 button.connect("clicked", self.remove_archive_button)
527 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
528 button.set_label("Add to archived articles")
529 button.connect("clicked", self.archive_button)
532 self.set_app_menu(menu)
535 self.add(self.pannable_article)
537 self.pannable_article.show_all()
539 self.destroyId = self.connect("destroy", self.destroyWindow)
541 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
542 ## Still using an old version of WebKit, so using navigation-requested signal
543 self.view.connect('navigation-requested', self.navigation_requested)
545 self.view.connect("button_press_event", self.button_pressed)
546 self.gestureId = self.view.connect("button_release_event", self.button_released)
548 #def navigation_policy_decision(self, wv, fr, req, action, decision):
549 def navigation_requested(self, wv, fr, req):
550 if self.config.getOpenInExternalBrowser():
551 self.open_in_browser(None, req.get_uri())
556 def load_started(self, *widget):
557 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
559 def load_finished(self, *widget):
560 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
561 frame = self.view.get_main_frame()
562 if self.loadedArticle:
563 self.currentUrl = frame.get_uri()
565 self.loadedArticle = True
567 def button_pressed(self, window, event):
568 #print event.x, event.y
569 self.coords = (event.x, event.y)
571 def button_released(self, window, event):
572 x = self.coords[0] - event.x
573 y = self.coords[1] - event.y
575 if (2*abs(y) < abs(x)):
577 self.emit("article-previous", self.id)
579 self.emit("article-next", self.id)
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 open_in_browser(self, object, link=None):
603 bus = dbus.SessionBus()
604 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
605 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
607 iface.open_new_window(self.currentUrl)
609 iface.open_new_window(link)
611 class DisplayFeed(hildon.StackableWindow):
612 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
613 hildon.StackableWindow.__init__(self)
614 self.listing = listing
616 self.feedTitle = title
617 self.set_title(title)
619 self.current = list()
621 self.updateDbusHandler = updateDbusHandler
623 self.downloadDialog = False
625 #self.listing.setCurrentlyDisplayedFeed(self.key)
629 menu = hildon.AppMenu()
630 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631 button.set_label("Update feed")
632 button.connect("clicked", self.button_update_clicked)
635 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
636 button.set_label("Mark all as read")
637 button.connect("clicked", self.buttonReadAllClicked)
640 if key=="ArchivedArticles":
641 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
642 button.set_label("Delete read articles")
643 button.connect("clicked", self.buttonPurgeArticles)
646 self.set_app_menu(menu)
651 self.connect('configure-event', self.on_configure_event)
652 self.connect("destroy", self.destroyWindow)
654 def on_configure_event(self, window, event):
655 if getattr(self, 'markup_renderer', None) is None:
658 # Fix up the column width for wrapping the text when the window is
659 # resized (i.e. orientation changed)
660 self.markup_renderer.set_property('wrap-width', event.width-20)
661 it = self.feedItems.get_iter_first()
662 while it is not None:
663 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
664 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
665 it = self.feedItems.iter_next(it)
667 def destroyWindow(self, *args):
668 #self.feed.saveUnread(CONFIGDIR)
669 self.listing.updateUnread(self.key)
670 self.emit("feed-closed", self.key)
672 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
673 #self.listing.closeCurrentlyDisplayedFeed()
675 def fix_title(self, title):
676 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
678 def displayFeed(self):
679 self.pannableFeed = hildon.PannableArea()
681 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
683 self.feedItems = gtk.ListStore(str, str)
684 #self.feedList = gtk.TreeView(self.feedItems)
685 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
686 self.feedList.set_rules_hint(True)
688 selection = self.feedList.get_selection()
689 selection.set_mode(gtk.SELECTION_NONE)
690 #selection.connect("changed", lambda w: True)
692 self.feedList.set_model(self.feedItems)
693 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
696 self.feedList.set_hover_selection(False)
697 #self.feedList.set_property('enable-grid-lines', True)
698 #self.feedList.set_property('hildon-mode', 1)
699 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
701 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
703 vbox= gtk.VBox(False, 10)
704 vbox.pack_start(self.feedList)
706 self.pannableFeed.add_with_viewport(vbox)
708 self.markup_renderer = gtk.CellRendererText()
709 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
710 self.markup_renderer.set_property('background', bg_color) #"#333333")
711 (width, height) = self.get_size()
712 self.markup_renderer.set_property('wrap-width', width-20)
713 self.markup_renderer.set_property('ypad', 8)
714 self.markup_renderer.set_property('xpad', 5)
715 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
716 markup=FEED_COLUMN_MARKUP)
717 self.feedList.append_column(markup_column)
719 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
720 hideReadArticles = self.config.getHideReadArticles()
722 articles = self.feed.getIds(onlyUnread=True)
724 articles = self.feed.getIds()
727 self.current = list()
731 isRead = self.feed.isEntryRead(id)
734 if not ( isRead and hideReadArticles ):
735 title = self.fix_title(self.feed.getTitle(id))
736 self.current.append(id)
738 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
740 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
742 self.feedItems.append((markup, id))
745 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
747 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
748 self.feedItems.append((markup, ""))
750 self.add(self.pannableFeed)
754 self.pannableFeed.destroy()
755 #self.remove(self.pannableFeed)
757 def on_feedList_row_activated(self, treeview, path): #, column):
758 selection = self.feedList.get_selection()
759 selection.set_mode(gtk.SELECTION_SINGLE)
760 self.feedList.get_selection().select_path(path)
761 model = treeview.get_model()
762 iter = model.get_iter(path)
763 key = model.get_value(iter, FEED_COLUMN_KEY)
764 # Emulate legacy "button_clicked" call via treeview
765 gobject.idle_add(self.button_clicked, treeview, key)
768 def button_clicked(self, button, index, previous=False, next=False):
769 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
770 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
771 stack = hildon.WindowStack.get_default()
774 stack.pop_and_push(1, newDisp, tmp)
776 gobject.timeout_add(200, self.destroyArticle, tmp)
781 if type(self.disp).__name__ == "DisplayArticle":
782 gobject.timeout_add(200, self.destroyArticle, self.disp)
789 if self.key == "ArchivedArticles":
790 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
791 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
792 self.ids.append(self.disp.connect("article-next", self.nextArticle))
793 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
795 def buttonPurgeArticles(self, *widget):
797 self.feed.purgeReadArticles()
798 #self.feed.saveFeed(CONFIGDIR)
801 def destroyArticle(self, handle):
802 handle.destroyWindow()
804 def mark_item_read(self, key):
805 it = self.feedItems.get_iter_first()
806 while it is not None:
807 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
809 title = self.fix_title(self.feed.getTitle(key))
810 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
811 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
813 it = self.feedItems.iter_next(it)
815 def nextArticle(self, object, index):
816 self.mark_item_read(index)
817 id = self.feed.getNextId(index)
818 while id not in self.current and id != index:
819 id = self.feed.getNextId(id)
821 self.button_clicked(object, id, next=True)
823 def previousArticle(self, object, index):
824 self.mark_item_read(index)
825 id = self.feed.getPreviousId(index)
826 while id not in self.current and id != index:
827 id = self.feed.getPreviousId(id)
829 self.button_clicked(object, id, previous=True)
831 def onArticleClosed(self, object, index):
832 selection = self.feedList.get_selection()
833 selection.set_mode(gtk.SELECTION_NONE)
834 self.mark_item_read(index)
836 def onArticleDeleted(self, object, index):
838 self.feed.removeArticle(index)
839 #self.feed.saveFeed(CONFIGDIR)
842 def button_update_clicked(self, button):
843 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
844 if not type(self.downloadDialog).__name__=="DownloadBar":
845 self.pannableFeed.destroy()
846 self.vbox = gtk.VBox(False, 10)
847 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
848 self.downloadDialog.connect("download-done", self.onDownloadsDone)
849 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
853 def onDownloadsDone(self, *widget):
855 self.feed = self.listing.getFeed(self.key)
857 self.updateDbusHandler.ArticleCountUpdated()
859 def buttonReadAllClicked(self, button):
861 self.feed.markAllAsRead()
862 it = self.feedItems.get_iter_first()
863 while it is not None:
864 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
865 title = self.fix_title(self.feed.getTitle(k))
866 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
867 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
868 it = self.feedItems.iter_next(it)
870 #for index in self.feed.getIds():
871 # self.feed.setEntryRead(index)
872 # self.mark_item_read(index)
878 self.window = hildon.StackableWindow()
879 self.window.set_title(__appname__)
880 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
881 self.mainVbox = gtk.VBox(False,10)
883 if isfile(CONFIGDIR+"/feeds.db"):
884 self.introLabel = gtk.Label("Loading...")
886 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
888 self.mainVbox.pack_start(self.introLabel)
890 self.window.add(self.mainVbox)
891 self.window.show_all()
892 self.config = Config(self.window, CONFIGDIR+"config.ini")
893 gobject.idle_add(self.createWindow)
895 def createWindow(self):
896 self.app_lock = get_lock("app_lock")
897 if self.app_lock == None:
899 self.stopButton.set_sensitive(True)
901 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
902 self.stopButton.set_text("Stop update","")
903 self.stopButton.connect("clicked", self.stop_running_update)
904 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
905 self.window.show_all()
906 self.introLabel.set_label("Update in progress, please wait.")
907 gobject.timeout_add_seconds(3, self.createWindow)
910 self.stopButton.destroy()
913 self.listing = Listing(CONFIGDIR)
915 self.downloadDialog = False
917 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
918 self.orientation.set_mode(self.config.getOrientation())
920 print "Could not start rotation manager"
922 menu = hildon.AppMenu()
923 # Create a button and add it to the menu
924 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925 button.set_label("Update feeds")
926 button.connect("clicked", self.button_update_clicked, "All")
929 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930 button.set_label("Mark all as read")
931 button.connect("clicked", self.button_markAll)
934 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
935 button.set_label("Add new feed")
936 button.connect("clicked", lambda b: self.addFeed())
939 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
940 button.set_label("Manage subscriptions")
941 button.connect("clicked", self.button_organize_clicked)
944 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
945 button.set_label("Settings")
946 button.connect("clicked", self.button_preferences_clicked)
949 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
950 button.set_label("About")
951 button.connect("clicked", self.button_about_clicked)
954 self.window.set_app_menu(menu)
957 #self.feedWindow = hildon.StackableWindow()
958 #self.articleWindow = hildon.StackableWindow()
959 self.introLabel.destroy()
960 self.pannableListing = hildon.PannableArea()
961 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
962 self.feedList = gtk.TreeView(self.feedItems)
963 self.feedList.connect('row-activated', self.on_feedList_row_activated)
964 self.pannableListing.add(self.feedList)
966 icon_renderer = gtk.CellRendererPixbuf()
967 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
968 icon_column = gtk.TreeViewColumn('', icon_renderer, \
970 self.feedList.append_column(icon_column)
972 markup_renderer = gtk.CellRendererText()
973 markup_column = gtk.TreeViewColumn('', markup_renderer, \
974 markup=COLUMN_MARKUP)
975 self.feedList.append_column(markup_column)
976 self.mainVbox.pack_start(self.pannableListing)
977 self.mainVbox.show_all()
979 self.displayListing()
980 self.autoupdate = False
981 self.checkAutoUpdate()
982 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
983 gobject.idle_add(self.enableDbus)
985 def stop_running_update(self, button):
986 self.stopButton.set_sensitive(False)
988 bus=dbus.SessionBus()
989 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
990 "/org/marcoz/feedingit/update" # Object's path
992 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
995 def enableDbus(self):
996 self.dbusHandler = ServerObject(self)
997 self.updateDbusHandler = UpdateServerObject(self)
999 def button_markAll(self, button):
1000 for key in self.listing.getListOfFeeds():
1001 feed = self.listing.getFeed(key)
1002 feed.markAllAsRead()
1003 #for id in feed.getIds():
1004 # feed.setEntryRead(id)
1005 self.listing.updateUnread(key)
1006 self.displayListing()
1008 def button_about_clicked(self, button):
1009 HeAboutDialog.present(self.window, \
1019 def button_export_clicked(self, button):
1020 opml = ExportOpmlData(self.window, self.listing)
1022 def button_import_clicked(self, button):
1023 opml = GetOpmlData(self.window)
1024 feeds = opml.getData()
1025 for (title, url) in feeds:
1026 self.listing.addFeed(title, url)
1027 self.displayListing()
1029 def addFeed(self, urlIn="http://"):
1030 wizard = AddWidgetWizard(self.window, urlIn)
1033 (title, url) = wizard.getData()
1034 if (not title == '') and (not url == ''):
1035 self.listing.addFeed(title, url)
1037 self.displayListing()
1039 def button_organize_clicked(self, button):
1040 def after_closing():
1041 self.displayListing()
1042 SortList(self.window, self.listing, self, after_closing)
1044 def button_update_clicked(self, button, key):
1045 if not type(self.downloadDialog).__name__=="DownloadBar":
1046 self.updateDbusHandler.UpdateStarted()
1047 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1048 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1049 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1050 self.mainVbox.show_all()
1051 #self.displayListing()
1053 def onDownloadsDone(self, *widget):
1054 self.downloadDialog.destroy()
1055 self.downloadDialog = False
1056 self.displayListing()
1057 self.updateDbusHandler.UpdateFinished()
1058 self.updateDbusHandler.ArticleCountUpdated()
1060 def button_preferences_clicked(self, button):
1061 dialog = self.config.createDialog()
1062 dialog.connect("destroy", self.prefsClosed)
1064 def show_confirmation_note(self, parent, title):
1065 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1067 retcode = gtk.Dialog.run(note)
1070 if retcode == gtk.RESPONSE_OK:
1075 def displayListing(self):
1076 icon_theme = gtk.icon_theme_get_default()
1077 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1078 gtk.ICON_LOOKUP_USE_BUILTIN)
1080 self.feedItems.clear()
1081 hideReadFeed = self.config.getHideReadFeeds()
1082 order = self.config.getFeedSortOrder()
1083 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed)
1086 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1087 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1088 updateTime = self.listing.getFeedUpdateTime(key)
1090 updateTime = "Never"
1091 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1093 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1095 markup = FEED_TEMPLATE % (title, subtitle)
1098 icon_filename = self.listing.getFavicon(key)
1099 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1100 LIST_ICON_SIZE, LIST_ICON_SIZE)
1102 pixbuf = default_pixbuf
1104 self.feedItems.append((pixbuf, markup, key))
1106 def on_feedList_row_activated(self, treeview, path, column):
1107 model = treeview.get_model()
1108 iter = model.get_iter(path)
1109 key = model.get_value(iter, COLUMN_KEY)
1112 def openFeed(self, key):
1116 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1118 self.feed_lock = get_lock(key)
1119 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1120 self.listing.getFeedTitle(key), key, \
1121 self.config, self.updateDbusHandler)
1122 self.disp.connect("feed-closed", self.onFeedClosed)
1124 def openArticle(self, key, id):
1128 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1130 self.feed_lock = get_lock(key)
1131 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1132 self.listing.getFeedTitle(key), key, \
1133 self.config, self.updateDbusHandler)
1134 self.disp.button_clicked(None, id)
1135 self.disp.connect("feed-closed", self.onFeedClosed)
1138 def onFeedClosed(self, object, key):
1139 #self.listing.saveConfig()
1141 gobject.idle_add(self.onFeedClosedTimeout)
1142 self.displayListing()
1143 #self.updateDbusHandler.ArticleCountUpdated()
1145 def onFeedClosedTimeout(self):
1147 self.updateDbusHandler.ArticleCountUpdated()
1150 self.window.connect("destroy", gtk.main_quit)
1154 def prefsClosed(self, *widget):
1156 self.orientation.set_mode(self.config.getOrientation())
1159 self.displayListing()
1160 self.checkAutoUpdate()
1162 def checkAutoUpdate(self, *widget):
1163 interval = int(self.config.getUpdateInterval()*3600000)
1164 if self.config.isAutoUpdateEnabled():
1165 if self.autoupdate == False:
1166 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1167 self.autoupdate = interval
1168 elif not self.autoupdate == interval:
1169 # If auto-update is enabled, but not at the right frequency
1170 gobject.source_remove(self.autoupdateId)
1171 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1172 self.autoupdate = interval
1174 if not self.autoupdate == False:
1175 gobject.source_remove(self.autoupdateId)
1176 self.autoupdate = False
1178 def automaticUpdate(self, *widget):
1179 # Need to check for internet connection
1180 # If no internet connection, try again in 10 minutes:
1181 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1182 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1183 #from time import localtime, strftime
1184 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1186 self.button_update_clicked(None, None)
1189 def stopUpdate(self):
1190 # Not implemented in the app (see update_feeds.py)
1192 self.downloadDialog.listOfKeys = []
1196 def getStatus(self):
1198 for key in self.listing.getListOfFeeds():
1199 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1200 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1202 status = "No unread items"
1205 if __name__ == "__main__":
1206 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1207 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1208 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1209 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1210 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1211 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1212 gobject.threads_init()
1213 if not isdir(CONFIGDIR):
1217 print "Error: Can't create configuration directory"
1218 from sys import exit