1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # Copyright (c) 2011 Neal H. Walfield
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # ============================================================================
21 __appname__ = 'FeedingIt'
22 __author__ = 'Yves Marcoz'
24 __description__ = 'A simple RSS Reader for Maemo 5'
25 # ============================================================================
28 from pango import FontDescription
33 from webkit import WebView
38 from os.path import isfile, isdir, exists
39 from os import mkdir, remove, stat
41 from aboutdialog import HeAboutDialog
42 from portrait import FremantleRotation
43 from threading import Thread, activeCount
44 from feedingitdbus import ServerObject
45 from updatedbus import UpdateServerObject, get_lock
46 from config import Config
47 from cgi import escape
50 from rss_sqlite import Listing
51 from opml import GetOpmlData, ExportOpmlData
54 from jobmanager import JobManager
56 from socket import setdefaulttimeout
58 setdefaulttimeout(timeout)
66 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
67 ABOUT_ICON = 'feedingit'
68 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
69 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
70 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
71 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
73 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
74 unread_color = color_style.lookup_color('ActiveTextColor')
75 read_color = color_style.lookup_color('DefaultTextColor')
78 CONFIGDIR="/home/user/.feedingit/"
79 LOCK = CONFIGDIR + "update.lock"
82 from htmlentitydefs import name2codepoint
84 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
86 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
90 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
91 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
92 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
94 # Build the markup template for the Maemo 5 text style
95 head_font = style.get_font_desc('SystemFont')
96 sub_font = style.get_font_desc('SmallSystemFont')
98 #head_color = style.get_color('ButtonTextColor')
99 head_color = style.get_color('DefaultTextColor')
100 sub_color = style.get_color('DefaultTextColor')
101 active_color = style.get_color('ActiveTextColor')
103 bg_color = style.get_color('DefaultBackgroundColor').to_string()
104 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
105 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
106 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
107 bg_color = "#" + c1 + c2 + c3
110 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
111 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
113 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
114 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
116 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
117 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
119 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
120 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
122 FEED_TEMPLATE = '\n'.join((head, normal_sub))
123 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
125 ENTRY_TEMPLATE = entry_head
126 ENTRY_TEMPLATE_UNREAD = entry_active_head
129 # Removes HTML or XML character references and entities from a text string.
131 # @param text The HTML (or XML) source text.
132 # @return The plain text, as a Unicode string, if necessary.
133 # http://effbot.org/zone/re-sub.htm#unescape-html
138 # character reference
140 if text[:3] == "&#x":
141 return unichr(int(text[3:-1], 16))
143 return unichr(int(text[2:-1]))
149 text = unichr(name2codepoint[text[1:-1]])
152 return text # leave as is
153 return sub("&#?\w+;", fixup, text)
156 class AddWidgetWizard(gtk.Dialog):
157 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
158 gtk.Dialog.__init__(self)
159 self.set_transient_for(parent)
161 #self.category = categories[0]
162 self.category = currentCat
165 self.set_title('Edit RSS feed')
167 self.set_title('Add new RSS feed')
170 self.btn_add = self.add_button('Save', 2)
172 self.btn_add = self.add_button('Add', 2)
174 self.set_default_response(2)
176 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
177 self.nameEntry.set_placeholder('Feed name')
178 # If titleIn matches urlIn, there is no title.
179 if not titleIn == None and titleIn != urlIn:
180 self.nameEntry.set_text(titleIn)
181 self.nameEntry.select_region(-1, -1)
183 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
184 self.urlEntry.set_placeholder('Feed URL')
185 self.urlEntry.set_text(urlIn)
186 self.urlEntry.select_region(-1, -1)
187 self.urlEntry.set_activates_default(True)
189 self.table = gtk.Table(3, 2, False)
190 self.table.set_col_spacings(5)
191 label = gtk.Label('Name:')
192 label.set_alignment(1., .5)
193 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
194 self.table.attach(self.nameEntry, 1, 2, 0, 1)
195 label = gtk.Label('URL:')
196 label.set_alignment(1., .5)
197 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
198 self.table.attach(self.urlEntry, 1, 2, 1, 2)
199 selector = self.create_selector(categories, listing)
200 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
201 picker.set_selector(selector)
202 picker.set_title("Select category")
203 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
204 picker.set_name('HildonButton-finger')
205 picker.set_alignment(0,0,1,1)
207 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
209 self.vbox.pack_start(self.table)
214 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
216 def create_selector(self, choices, listing):
217 #self.pickerDialog = hildon.PickerDialog(self.parent)
218 selector = hildon.TouchSelector(text=True)
222 title = listing.getCategoryTitle(item)
223 iter = selector.append_text(str(title))
224 if self.category == item:
225 selector.set_active(0, index)
226 self.map[title] = item
228 selector.connect("changed", self.selection_changed)
229 #self.pickerDialog.set_selector(selector)
232 def selection_changed(self, selector, button):
233 current_selection = selector.get_current_text()
234 if current_selection:
235 self.category = self.map[current_selection]
237 class AddCategoryWizard(gtk.Dialog):
238 def __init__(self, parent, titleIn=None, isEdit=False):
239 gtk.Dialog.__init__(self)
240 self.set_transient_for(parent)
243 self.set_title('Edit Category')
245 self.set_title('Add Category')
248 self.btn_add = self.add_button('Save', 2)
250 self.btn_add = self.add_button('Add', 2)
252 self.set_default_response(2)
254 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
255 self.nameEntry.set_placeholder('Category name')
256 if not titleIn == None:
257 self.nameEntry.set_text(titleIn)
258 self.nameEntry.select_region(-1, -1)
260 self.table = gtk.Table(1, 2, False)
261 self.table.set_col_spacings(5)
262 label = gtk.Label('Name:')
263 label.set_alignment(1., .5)
264 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
265 self.table.attach(self.nameEntry, 1, 2, 0, 1)
266 #label = gtk.Label('URL:')
267 #label.set_alignment(1., .5)
268 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
269 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
270 self.vbox.pack_start(self.table)
275 return self.nameEntry.get_text()
277 class DownloadBar(gtk.ProgressBar):
280 if hasattr (cls, 'class_init_done'):
284 jm.stats_hook_register (cls.update_progress,
285 run_in_main_thread=True)
287 cls.downloadbars = []
288 # Total number of jobs we are monitoring.
290 # Number of jobs complete (of those that we are monitoring).
295 cls.class_init_done = True
297 def __init__(self, parent):
300 gtk.ProgressBar.__init__(self)
302 self.downloadbars.append(weakref.ref (self))
304 self.__class__.update_bars()
308 def downloading(cls):
309 return hasattr (cls, 'jobs_at_start')
312 def update_progress(cls, jm, old_stats, new_stats, updated_feed):
313 if not cls.downloading():
314 cls.jobs_at_start = old_stats['jobs-completed']
316 if not cls.downloadbars:
319 if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
320 del cls.jobs_at_start
321 for ref in cls.downloadbars:
324 # The download bar disappeared.
325 cls.downloadbars.remove (ref)
327 bar.emit("download-done", None)
330 # This should never be called if new_stats['jobs'] is 0, but
332 cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
333 cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
334 cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
335 + new_stats['jobs-queued']) / cls.total
339 for ref in cls.downloadbars:
342 # The download bar disappeared.
343 cls.downloadbars.remove (ref)
345 bar.emit("download-done", updated_feed)
348 def update_bars(cls):
349 # In preparation for i18n/l10n
351 return (a if n == 1 else b)
353 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
355 % (cls.done, cls.total))
357 for ref in cls.downloadbars:
360 # The download bar disappeared.
361 cls.downloadbars.remove (ref)
364 bar.set_fraction(cls.progress)
366 class SortList(hildon.StackableWindow):
367 def __init__(self, parent, listing, feedingit, after_closing, category=None):
368 hildon.StackableWindow.__init__(self)
369 self.set_transient_for(parent)
371 self.isEditingCategories = False
372 self.category = category
373 self.set_title(listing.getCategoryTitle(category))
375 self.isEditingCategories = True
376 self.set_title('Categories')
377 self.listing = listing
378 self.feedingit = feedingit
379 self.after_closing = after_closing
381 self.connect('destroy', lambda w: self.after_closing())
382 self.vbox2 = gtk.VBox(False, 2)
384 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
385 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
386 button.connect("clicked", self.buttonUp)
387 self.vbox2.pack_start(button, expand=False, fill=False)
389 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
390 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
391 button.connect("clicked", self.buttonDown)
392 self.vbox2.pack_start(button, expand=False, fill=False)
394 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
396 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
397 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
398 button.connect("clicked", self.buttonAdd)
399 self.vbox2.pack_start(button, expand=False, fill=False)
401 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
402 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
403 button.connect("clicked", self.buttonEdit)
404 self.vbox2.pack_start(button, expand=False, fill=False)
406 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
407 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
408 button.connect("clicked", self.buttonDelete)
409 self.vbox2.pack_start(button, expand=False, fill=False)
411 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
412 #button.set_label("Done")
413 #button.connect("clicked", self.buttonDone)
414 #self.vbox.pack_start(button)
415 self.hbox2= gtk.HBox(False, 10)
416 self.pannableArea = hildon.PannableArea()
417 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
418 self.treeview = gtk.TreeView(self.treestore)
419 self.hbox2.pack_start(self.pannableArea, expand=True)
421 self.hbox2.pack_end(self.vbox2, expand=False)
422 self.set_default_size(-1, 600)
425 menu = hildon.AppMenu()
426 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
427 button.set_label("Import from OPML")
428 button.connect("clicked", self.feedingit.button_import_clicked)
431 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
432 button.set_label("Export to OPML")
433 button.connect("clicked", self.feedingit.button_export_clicked)
435 self.set_app_menu(menu)
439 #self.connect("destroy", self.buttonDone)
441 def displayFeeds(self):
442 self.treeview.destroy()
443 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
444 self.treeview = gtk.TreeView()
446 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
447 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
449 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
451 self.pannableArea.add(self.treeview)
455 def refreshList(self, selected=None, offset=0):
456 #rect = self.treeview.get_visible_rect()
457 #y = rect.y+rect.height
458 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
459 if self.isEditingCategories:
460 for key in self.listing.getListOfCategories():
461 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
465 for key in self.listing.getListOfFeeds(category=self.category):
466 item = self.treestore.append([self.listing.getFeedTitle(key), key])
469 self.treeview.set_model(self.treestore)
470 if not selected == None:
471 self.treeview.get_selection().select_iter(selectedItem)
472 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
473 self.pannableArea.show_all()
475 def getSelectedItem(self):
476 (model, iter) = self.treeview.get_selection().get_selected()
479 return model.get_value(iter, 1)
481 def findIndex(self, key):
485 for row in self.treestore:
487 return (before, row.iter)
488 if key == list(row)[0]:
492 return (before, None)
494 def buttonUp(self, button):
495 key = self.getSelectedItem()
497 if self.isEditingCategories:
498 self.listing.moveCategoryUp(key)
500 self.listing.moveUp(key)
501 self.refreshList(key, -10)
503 def buttonDown(self, button):
504 key = self.getSelectedItem()
506 if self.isEditingCategories:
507 self.listing.moveCategoryDown(key)
509 self.listing.moveDown(key)
510 self.refreshList(key, 10)
512 def buttonDelete(self, button):
513 key = self.getSelectedItem()
515 message = 'Really remove this feed and its entries?'
516 dlg = hildon.hildon_note_new_confirmation(self, message)
519 if response == gtk.RESPONSE_OK:
520 if self.isEditingCategories:
521 self.listing.removeCategory(key)
523 self.listing.removeFeed(key)
526 def buttonEdit(self, button):
527 key = self.getSelectedItem()
529 if key == 'ArchivedArticles':
530 message = 'Cannot edit the archived articles feed.'
531 hildon.hildon_banner_show_information(self, '', message)
533 if self.isEditingCategories:
535 SortList(self.parent, self.listing, self.feedingit, None, category=key)
538 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
541 (title, url, category) = wizard.getData()
543 self.listing.editFeed(key, title, url, category=category)
547 def buttonDone(self, *args):
550 def buttonAdd(self, button, urlIn="http://"):
551 if self.isEditingCategories:
552 wizard = AddCategoryWizard(self)
555 title = wizard.getData()
556 if (not title == ''):
557 self.listing.addCategory(title)
559 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
562 (title, url, category) = wizard.getData()
564 self.listing.addFeed(title, url, category=category)
569 class DisplayArticle(hildon.StackableWindow):
570 def __init__(self, feed, id, key, config, listing):
571 hildon.StackableWindow.__init__(self)
572 #self.imageDownloader = ImageDownloader()
577 #self.set_title(feed.getTitle(id))
578 self.set_title(self.listing.getFeedTitle(key))
580 self.set_for_removal = False
582 # Init the article display
583 #if self.config.getWebkitSupport():
584 self.view = WebView()
585 #self.view.set_editable(False)
588 # self.view = gtkhtml2.View()
589 # self.document = gtkhtml2.Document()
590 # self.view.set_document(self.document)
591 # self.document.connect("link_clicked", self._signal_link_clicked)
592 self.pannable_article = hildon.PannableArea()
593 self.pannable_article.add(self.view)
594 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
595 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
597 #if self.config.getWebkitSupport():
598 contentLink = self.feed.getContentLink(self.id)
599 self.feed.setEntryRead(self.id)
600 #if key=="ArchivedArticles":
601 self.loadedArticle = False
602 if contentLink.startswith("/home/user/"):
603 self.view.open("file://%s" % contentLink)
604 self.currentUrl = self.feed.getExternalLink(self.id)
606 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
607 self.currentUrl = "%s" % contentLink
608 self.view.connect("motion-notify-event", lambda w,ev: True)
609 self.view.connect('load-started', self.load_started)
610 self.view.connect('load-finished', self.load_finished)
612 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
614 menu = hildon.AppMenu()
615 # Create a button and add it to the menu
616 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
617 button.set_label("Allow horizontal scrolling")
618 button.connect("clicked", self.horiz_scrolling_button)
621 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
622 button.set_label("Open in browser")
623 button.connect("clicked", self.open_in_browser)
626 if key == "ArchivedArticles":
627 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
628 button.set_label("Remove from archived articles")
629 button.connect("clicked", self.remove_archive_button)
631 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
632 button.set_label("Add to archived articles")
633 button.connect("clicked", self.archive_button)
636 self.set_app_menu(menu)
639 self.add(self.pannable_article)
641 self.pannable_article.show_all()
643 self.destroyId = self.connect("destroy", self.destroyWindow)
645 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
646 ## Still using an old version of WebKit, so using navigation-requested signal
647 self.view.connect('navigation-requested', self.navigation_requested)
649 self.view.connect("button_press_event", self.button_pressed)
650 self.gestureId = self.view.connect("button_release_event", self.button_released)
652 #def navigation_policy_decision(self, wv, fr, req, action, decision):
653 def navigation_requested(self, wv, fr, req):
654 if self.config.getOpenInExternalBrowser():
655 self.open_in_browser(None, req.get_uri())
660 def load_started(self, *widget):
661 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
663 def load_finished(self, *widget):
664 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
665 frame = self.view.get_main_frame()
666 if self.loadedArticle:
667 self.currentUrl = frame.get_uri()
669 self.loadedArticle = True
671 def button_pressed(self, window, event):
672 #print event.x, event.y
673 self.coords = (event.x, event.y)
675 def button_released(self, window, event):
676 x = self.coords[0] - event.x
677 y = self.coords[1] - event.y
679 if (2*abs(y) < abs(x)):
681 self.emit("article-previous", self.id)
683 self.emit("article-next", self.id)
685 def destroyWindow(self, *args):
686 self.disconnect(self.destroyId)
687 if self.set_for_removal:
688 self.emit("article-deleted", self.id)
690 self.emit("article-closed", self.id)
691 #self.imageDownloader.stopAll()
694 def horiz_scrolling_button(self, *widget):
695 self.pannable_article.disconnect(self.gestureId)
696 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
698 def archive_button(self, *widget):
699 # Call the listing.addArchivedArticle
700 self.listing.addArchivedArticle(self.key, self.id)
702 def remove_archive_button(self, *widget):
703 self.set_for_removal = True
705 def open_in_browser(self, object, link=None):
707 bus = dbus.SessionBus()
708 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
709 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
711 iface.open_new_window(self.currentUrl)
713 iface.open_new_window(link)
715 class DisplayFeed(hildon.StackableWindow):
716 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
717 hildon.StackableWindow.__init__(self)
718 self.listing = listing
720 self.feedTitle = title
721 self.set_title(title)
723 self.current = list()
725 self.updateDbusHandler = updateDbusHandler
727 self.downloadDialog = False
729 #self.listing.setCurrentlyDisplayedFeed(self.key)
733 menu = hildon.AppMenu()
734 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
735 button.set_label("Update feed")
736 button.connect("clicked", self.button_update_clicked)
739 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
740 button.set_label("Mark all as read")
741 button.connect("clicked", self.buttonReadAllClicked)
744 if key=="ArchivedArticles":
745 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
746 button.set_label("Delete read articles")
747 button.connect("clicked", self.buttonPurgeArticles)
750 self.set_app_menu(menu)
753 self.main_vbox = gtk.VBox(False, 0)
754 self.add(self.main_vbox)
756 self.pannableFeed = None
759 if DownloadBar.downloading ():
760 self.show_download_bar ()
762 self.connect('configure-event', self.on_configure_event)
763 self.connect("destroy", self.destroyWindow)
765 def on_configure_event(self, window, event):
766 if getattr(self, 'markup_renderer', None) is None:
769 # Fix up the column width for wrapping the text when the window is
770 # resized (i.e. orientation changed)
771 self.markup_renderer.set_property('wrap-width', event.width-20)
772 it = self.feedItems.get_iter_first()
773 while it is not None:
774 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
775 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
776 it = self.feedItems.iter_next(it)
778 def destroyWindow(self, *args):
779 #self.feed.saveUnread(CONFIGDIR)
780 self.listing.updateUnread(self.key)
781 self.emit("feed-closed", self.key)
783 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
784 #self.listing.closeCurrentlyDisplayedFeed()
786 def fix_title(self, title):
787 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
789 def displayFeed(self):
790 if self.pannableFeed:
791 self.pannableFeed.destroy()
793 self.pannableFeed = hildon.PannableArea()
795 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
797 self.feedItems = gtk.ListStore(str, str)
798 #self.feedList = gtk.TreeView(self.feedItems)
799 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
800 self.feedList.set_rules_hint(True)
802 selection = self.feedList.get_selection()
803 selection.set_mode(gtk.SELECTION_NONE)
804 #selection.connect("changed", lambda w: True)
806 self.feedList.set_model(self.feedItems)
807 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
810 self.feedList.set_hover_selection(False)
811 #self.feedList.set_property('enable-grid-lines', True)
812 #self.feedList.set_property('hildon-mode', 1)
813 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
815 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
817 vbox= gtk.VBox(False, 10)
818 vbox.pack_start(self.feedList)
820 self.pannableFeed.add_with_viewport(vbox)
822 self.markup_renderer = gtk.CellRendererText()
823 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
824 self.markup_renderer.set_property('background', bg_color) #"#333333")
825 (width, height) = self.get_size()
826 self.markup_renderer.set_property('wrap-width', width-20)
827 self.markup_renderer.set_property('ypad', 8)
828 self.markup_renderer.set_property('xpad', 5)
829 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
830 markup=FEED_COLUMN_MARKUP)
831 self.feedList.append_column(markup_column)
833 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
834 hideReadArticles = self.config.getHideReadArticles()
836 articles = self.feed.getIds(onlyUnread=True)
838 articles = self.feed.getIds()
841 self.current = list()
845 isRead = self.feed.isEntryRead(id)
848 if not ( isRead and hideReadArticles ):
849 title = self.fix_title(self.feed.getTitle(id))
850 self.current.append(id)
852 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
854 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
856 self.feedItems.append((markup, id))
859 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
861 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
862 self.feedItems.append((markup, ""))
864 self.main_vbox.pack_start(self.pannableFeed)
868 self.pannableFeed.destroy()
869 #self.remove(self.pannableFeed)
871 def on_feedList_row_activated(self, treeview, path): #, column):
872 selection = self.feedList.get_selection()
873 selection.set_mode(gtk.SELECTION_SINGLE)
874 self.feedList.get_selection().select_path(path)
875 model = treeview.get_model()
876 iter = model.get_iter(path)
877 key = model.get_value(iter, FEED_COLUMN_KEY)
878 # Emulate legacy "button_clicked" call via treeview
879 gobject.idle_add(self.button_clicked, treeview, key)
882 def button_clicked(self, button, index, previous=False, next=False):
883 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
884 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
885 stack = hildon.WindowStack.get_default()
888 stack.pop_and_push(1, newDisp, tmp)
890 gobject.timeout_add(200, self.destroyArticle, tmp)
895 if type(self.disp).__name__ == "DisplayArticle":
896 gobject.timeout_add(200, self.destroyArticle, self.disp)
903 if self.key == "ArchivedArticles":
904 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
905 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
906 self.ids.append(self.disp.connect("article-next", self.nextArticle))
907 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
909 def buttonPurgeArticles(self, *widget):
911 self.feed.purgeReadArticles()
912 #self.feed.saveFeed(CONFIGDIR)
915 def destroyArticle(self, handle):
916 handle.destroyWindow()
918 def mark_item_read(self, key):
919 it = self.feedItems.get_iter_first()
920 while it is not None:
921 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
923 title = self.fix_title(self.feed.getTitle(key))
924 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
925 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
927 it = self.feedItems.iter_next(it)
929 def nextArticle(self, object, index):
930 self.mark_item_read(index)
931 id = self.feed.getNextId(index)
932 while id not in self.current and id != index:
933 id = self.feed.getNextId(id)
935 self.button_clicked(object, id, next=True)
937 def previousArticle(self, object, index):
938 self.mark_item_read(index)
939 id = self.feed.getPreviousId(index)
940 while id not in self.current and id != index:
941 id = self.feed.getPreviousId(id)
943 self.button_clicked(object, id, previous=True)
945 def onArticleClosed(self, object, index):
946 selection = self.feedList.get_selection()
947 selection.set_mode(gtk.SELECTION_NONE)
948 self.mark_item_read(index)
950 def onArticleDeleted(self, object, index):
952 self.feed.removeArticle(index)
953 #self.feed.saveFeed(CONFIGDIR)
956 def button_update_clicked(self, button):
957 self.listing.updateFeed (self.key, priority=-1)
959 def show_download_bar(self):
960 if not type(self.downloadDialog).__name__=="DownloadBar":
961 self.downloadDialog = DownloadBar(self.window)
962 self.downloadDialog.connect("download-done", self.onDownloadDone)
963 self.main_vbox.pack_end(self.downloadDialog,
964 expand=False, fill=False)
967 def onDownloadDone(self, widget, feed):
968 if feed == self.feed or feed is None:
969 self.downloadDialog.destroy()
970 self.downloadDialog = False
971 self.feed = self.listing.getFeed(self.key)
973 self.updateDbusHandler.ArticleCountUpdated()
975 def buttonReadAllClicked(self, button):
977 self.feed.markAllAsRead()
978 it = self.feedItems.get_iter_first()
979 while it is not None:
980 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
981 title = self.fix_title(self.feed.getTitle(k))
982 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
983 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
984 it = self.feedItems.iter_next(it)
986 #for index in self.feed.getIds():
987 # self.feed.setEntryRead(index)
988 # self.mark_item_read(index)
994 self.window = hildon.StackableWindow()
995 self.window.set_title(__appname__)
996 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
997 self.mainVbox = gtk.VBox(False,10)
999 if isfile(CONFIGDIR+"/feeds.db"):
1000 self.introLabel = gtk.Label("Loading...")
1002 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1004 self.mainVbox.pack_start(self.introLabel)
1006 self.window.add(self.mainVbox)
1007 self.window.show_all()
1008 self.config = Config(self.window, CONFIGDIR+"config.ini")
1009 gobject.idle_add(self.createWindow)
1011 def createWindow(self):
1014 self.app_lock = get_lock("app_lock")
1015 if self.app_lock == None:
1017 self.stopButton.set_sensitive(True)
1019 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1020 self.stopButton.set_text("Stop update","")
1021 self.stopButton.connect("clicked", self.stop_running_update)
1022 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1023 self.window.show_all()
1024 self.introLabel.set_label("Update in progress, please wait.")
1025 gobject.timeout_add_seconds(3, self.createWindow)
1028 self.stopButton.destroy()
1031 self.listing = Listing(self.config, CONFIGDIR)
1033 self.downloadDialog = False
1035 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1036 self.orientation.set_mode(self.config.getOrientation())
1038 print "Could not start rotation manager"
1040 menu = hildon.AppMenu()
1041 # Create a button and add it to the menu
1042 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1043 button.set_label("Update feeds")
1044 button.connect("clicked", self.button_update_clicked, "All")
1047 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1048 button.set_label("Mark all as read")
1049 button.connect("clicked", self.button_markAll)
1052 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1053 button.set_label("Add new feed")
1054 button.connect("clicked", lambda b: self.addFeed())
1057 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1058 button.set_label("Manage subscriptions")
1059 button.connect("clicked", self.button_organize_clicked)
1062 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1063 button.set_label("Settings")
1064 button.connect("clicked", self.button_preferences_clicked)
1067 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1068 button.set_label("About")
1069 button.connect("clicked", self.button_about_clicked)
1072 self.window.set_app_menu(menu)
1075 #self.feedWindow = hildon.StackableWindow()
1076 #self.articleWindow = hildon.StackableWindow()
1077 self.introLabel.destroy()
1078 self.pannableListing = hildon.PannableArea()
1079 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1080 self.feedList = gtk.TreeView(self.feedItems)
1081 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1082 #self.feedList.set_enable_tree_lines(True)
1083 #self.feedList.set_show_expanders(True)
1084 self.pannableListing.add(self.feedList)
1086 icon_renderer = gtk.CellRendererPixbuf()
1087 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1088 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1090 self.feedList.append_column(icon_column)
1092 markup_renderer = gtk.CellRendererText()
1093 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1094 markup=COLUMN_MARKUP)
1095 self.feedList.append_column(markup_column)
1096 self.mainVbox.pack_start(self.pannableListing)
1097 self.mainVbox.show_all()
1099 self.displayListing()
1100 self.autoupdate = False
1101 self.checkAutoUpdate()
1103 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1104 gobject.idle_add(self.late_init)
1106 def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1107 if (not self.downloadDialog
1108 and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1109 self.updateDbusHandler.UpdateStarted()
1111 self.downloadDialog = DownloadBar(self.window)
1112 self.downloadDialog.connect("download-done", self.onDownloadDone)
1113 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1114 self.mainVbox.show_all()
1116 if self.__dict__.get ('disp', None):
1117 self.disp.show_download_bar ()
1119 def onDownloadDone(self, widget, feed):
1121 self.downloadDialog.destroy()
1122 self.downloadDialog = False
1123 self.displayListing()
1124 self.updateDbusHandler.UpdateFinished()
1125 self.updateDbusHandler.ArticleCountUpdated()
1127 def stop_running_update(self, button):
1128 self.stopButton.set_sensitive(False)
1130 bus=dbus.SessionBus()
1131 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1132 "/org/marcoz/feedingit/update" # Object's path
1134 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1137 def late_init(self):
1138 self.dbusHandler = ServerObject(self)
1139 self.updateDbusHandler = UpdateServerObject(self)
1142 jm.stats_hook_register (self.job_manager_update,
1143 run_in_main_thread=True)
1146 def button_markAll(self, button):
1147 for key in self.listing.getListOfFeeds():
1148 feed = self.listing.getFeed(key)
1149 feed.markAllAsRead()
1150 #for id in feed.getIds():
1151 # feed.setEntryRead(id)
1152 self.listing.updateUnread(key)
1153 self.displayListing()
1155 def button_about_clicked(self, button):
1156 HeAboutDialog.present(self.window, \
1166 def button_export_clicked(self, button):
1167 opml = ExportOpmlData(self.window, self.listing)
1169 def button_import_clicked(self, button):
1170 opml = GetOpmlData(self.window)
1171 feeds = opml.getData()
1172 for (title, url) in feeds:
1173 self.listing.addFeed(title, url)
1174 self.displayListing()
1176 def addFeed(self, urlIn="http://"):
1177 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1180 (title, url, category) = wizard.getData()
1182 self.listing.addFeed(title, url, category=category)
1184 self.displayListing()
1186 def button_organize_clicked(self, button):
1187 def after_closing():
1188 self.displayListing()
1189 SortList(self.window, self.listing, self, after_closing)
1191 def button_update_clicked(self, button, key):
1192 for k in self.listing.getListOfFeeds():
1193 self.listing.updateFeed (k)
1194 #self.displayListing()
1196 def onDownloadsDone(self, *widget):
1197 self.downloadDialog.destroy()
1198 self.downloadDialog = False
1199 self.displayListing()
1200 self.updateDbusHandler.UpdateFinished()
1201 self.updateDbusHandler.ArticleCountUpdated()
1203 def button_preferences_clicked(self, button):
1204 dialog = self.config.createDialog()
1205 dialog.connect("destroy", self.prefsClosed)
1207 def show_confirmation_note(self, parent, title):
1208 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1210 retcode = gtk.Dialog.run(note)
1213 if retcode == gtk.RESPONSE_OK:
1218 def saveExpandedLines(self):
1219 self.expandedLines = []
1220 model = self.feedList.get_model()
1221 model.foreach(self.checkLine)
1223 def checkLine(self, model, path, iter, data = None):
1224 if self.feedList.row_expanded(path):
1225 self.expandedLines.append(path)
1227 def restoreExpandedLines(self):
1228 model = self.feedList.get_model()
1229 model.foreach(self.restoreLine)
1231 def restoreLine(self, model, path, iter, data = None):
1232 if path in self.expandedLines:
1233 self.feedList.expand_row(path, False)
1235 def displayListing(self):
1236 icon_theme = gtk.icon_theme_get_default()
1237 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1238 gtk.ICON_LOOKUP_USE_BUILTIN)
1240 self.saveExpandedLines()
1242 self.feedItems.clear()
1243 hideReadFeed = self.config.getHideReadFeeds()
1244 order = self.config.getFeedSortOrder()
1246 categories = self.listing.getListOfCategories()
1247 if len(categories) > 1:
1248 showCategories = True
1250 showCategories = False
1252 for categoryId in categories:
1254 title = self.listing.getCategoryTitle(categoryId)
1255 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1257 if showCategories and len(keys)>0:
1258 category = self.feedItems.append(None, (None, title, categoryId))
1259 #print "catID" + str(categoryId) + " " + str(self.category)
1260 if categoryId == self.category:
1262 expandedRow = category
1265 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1266 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1267 updateTime = self.listing.getFeedUpdateTime(key)
1269 updateTime = "Never"
1270 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1272 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1274 markup = FEED_TEMPLATE % (title, subtitle)
1277 icon_filename = self.listing.getFavicon(key)
1278 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1279 LIST_ICON_SIZE, LIST_ICON_SIZE)
1281 pixbuf = default_pixbuf
1284 self.feedItems.append(category, (pixbuf, markup, key))
1286 self.feedItems.append(None, (pixbuf, markup, key))
1289 self.restoreExpandedLines()
1292 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1296 def on_feedList_row_activated(self, treeview, path, column):
1297 model = treeview.get_model()
1298 iter = model.get_iter(path)
1299 key = model.get_value(iter, COLUMN_KEY)
1302 #print "Key: " + str(key)
1304 self.category = catId
1305 if treeview.row_expanded(path):
1306 treeview.collapse_row(path)
1308 # treeview.expand_row(path, True)
1309 #treeview.collapse_all()
1310 #treeview.expand_row(path, False)
1311 #for i in range(len(path)):
1312 # self.feedList.expand_row(path[:i+1], False)
1313 #self.show_confirmation_note(self.window, "Working")
1319 def openFeed(self, key):
1323 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1325 self.feed_lock = get_lock(key)
1326 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1327 self.listing.getFeedTitle(key), key, \
1328 self.config, self.updateDbusHandler)
1329 self.disp.connect("feed-closed", self.onFeedClosed)
1331 def openArticle(self, key, id):
1335 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1337 self.feed_lock = get_lock(key)
1338 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1339 self.listing.getFeedTitle(key), key, \
1340 self.config, self.updateDbusHandler)
1341 self.disp.button_clicked(None, id)
1342 self.disp.connect("feed-closed", self.onFeedClosed)
1345 def onFeedClosed(self, object, key):
1346 #self.listing.saveConfig()
1348 gobject.idle_add(self.onFeedClosedTimeout)
1349 self.displayListing()
1350 #self.updateDbusHandler.ArticleCountUpdated()
1352 def onFeedClosedTimeout(self):
1354 self.updateDbusHandler.ArticleCountUpdated()
1356 def quit(self, *args):
1359 if hasattr (self, 'app_lock'):
1362 # Wait until all slave threads have properly exited before
1363 # terminating the mainloop.
1367 if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1370 gobject.timeout_add(500, self.quit)
1375 self.window.connect("destroy", self.quit)
1378 def prefsClosed(self, *widget):
1380 self.orientation.set_mode(self.config.getOrientation())
1383 self.displayListing()
1384 self.checkAutoUpdate()
1386 def checkAutoUpdate(self, *widget):
1387 interval = int(self.config.getUpdateInterval()*3600000)
1388 if self.config.isAutoUpdateEnabled():
1389 if self.autoupdate == False:
1390 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1391 self.autoupdate = interval
1392 elif not self.autoupdate == interval:
1393 # If auto-update is enabled, but not at the right frequency
1394 gobject.source_remove(self.autoupdateId)
1395 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1396 self.autoupdate = interval
1398 if not self.autoupdate == False:
1399 gobject.source_remove(self.autoupdateId)
1400 self.autoupdate = False
1402 def automaticUpdate(self, *widget):
1403 # Need to check for internet connection
1404 # If no internet connection, try again in 10 minutes:
1405 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1406 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1407 #from time import localtime, strftime
1408 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1410 self.button_update_clicked(None, None)
1413 def stopUpdate(self):
1414 # Not implemented in the app (see update_feeds.py)
1416 JobManager().cancel ()
1420 def getStatus(self):
1422 for key in self.listing.getListOfFeeds():
1423 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1424 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1426 status = "No unread items"
1429 if __name__ == "__main__":
1432 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1433 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1434 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1435 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1436 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1437 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1438 gobject.threads_init()
1439 if not isdir(CONFIGDIR):
1443 print "Error: Can't create configuration directory"
1444 from sys import exit