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 not titleIn == None:
179 self.nameEntry.set_text(titleIn)
180 self.nameEntry.select_region(-1, -1)
182 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
183 self.urlEntry.set_placeholder('Feed URL')
184 self.urlEntry.set_text(urlIn)
185 self.urlEntry.select_region(-1, -1)
186 self.urlEntry.set_activates_default(True)
188 self.table = gtk.Table(3, 2, False)
189 self.table.set_col_spacings(5)
190 label = gtk.Label('Name:')
191 label.set_alignment(1., .5)
192 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
193 self.table.attach(self.nameEntry, 1, 2, 0, 1)
194 label = gtk.Label('URL:')
195 label.set_alignment(1., .5)
196 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
197 self.table.attach(self.urlEntry, 1, 2, 1, 2)
198 selector = self.create_selector(categories, listing)
199 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
200 picker.set_selector(selector)
201 picker.set_title("Select category")
202 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
203 picker.set_name('HildonButton-finger')
204 picker.set_alignment(0,0,1,1)
206 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
208 self.vbox.pack_start(self.table)
213 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
215 def create_selector(self, choices, listing):
216 #self.pickerDialog = hildon.PickerDialog(self.parent)
217 selector = hildon.TouchSelector(text=True)
221 title = listing.getCategoryTitle(item)
222 iter = selector.append_text(str(title))
223 if self.category == item:
224 selector.set_active(0, index)
225 self.map[title] = item
227 selector.connect("changed", self.selection_changed)
228 #self.pickerDialog.set_selector(selector)
231 def selection_changed(self, selector, button):
232 current_selection = selector.get_current_text()
233 if current_selection:
234 self.category = self.map[current_selection]
236 class AddCategoryWizard(gtk.Dialog):
237 def __init__(self, parent, titleIn=None, isEdit=False):
238 gtk.Dialog.__init__(self)
239 self.set_transient_for(parent)
242 self.set_title('Edit Category')
244 self.set_title('Add Category')
247 self.btn_add = self.add_button('Save', 2)
249 self.btn_add = self.add_button('Add', 2)
251 self.set_default_response(2)
253 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
254 self.nameEntry.set_placeholder('Category name')
255 if not titleIn == None:
256 self.nameEntry.set_text(titleIn)
257 self.nameEntry.select_region(-1, -1)
259 self.table = gtk.Table(1, 2, False)
260 self.table.set_col_spacings(5)
261 label = gtk.Label('Name:')
262 label.set_alignment(1., .5)
263 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
264 self.table.attach(self.nameEntry, 1, 2, 0, 1)
265 #label = gtk.Label('URL:')
266 #label.set_alignment(1., .5)
267 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
268 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
269 self.vbox.pack_start(self.table)
274 return self.nameEntry.get_text()
276 class DownloadBar(gtk.ProgressBar):
279 if hasattr (cls, 'class_init_done'):
283 jm.stats_hook_register (cls.update_progress,
284 run_in_main_thread=True)
286 cls.downloadbars = []
287 # Total number of jobs we are monitoring.
289 # Number of jobs complete (of those that we are monitoring).
294 cls.class_init_done = True
296 def __init__(self, parent):
299 gtk.ProgressBar.__init__(self)
301 self.downloadbars.append(weakref.ref (self))
303 self.__class__.update_bars()
307 def downloading(cls):
308 return hasattr (cls, 'jobs_at_start')
311 def update_progress(cls, jm, old_stats, new_stats, updated_feed):
312 if not cls.downloading():
313 cls.jobs_at_start = old_stats['jobs-completed']
315 if not cls.downloadbars:
318 if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
319 del cls.jobs_at_start
320 for ref in cls.downloadbars:
323 # The download bar disappeared.
324 cls.downloadbars.remove (ref)
326 bar.emit("download-done", None)
329 # This should never be called if new_stats['jobs'] is 0, but
331 cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
332 cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
333 cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
334 + new_stats['jobs-queued']) / cls.total
338 for ref in cls.downloadbars:
341 # The download bar disappeared.
342 cls.downloadbars.remove (ref)
344 bar.emit("download-done", updated_feed)
347 def update_bars(cls):
348 # In preparation for i18n/l10n
350 return (a if n == 1 else b)
352 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
354 % (cls.done, cls.total))
356 for ref in cls.downloadbars:
359 # The download bar disappeared.
360 cls.downloadbars.remove (ref)
363 bar.set_fraction(cls.progress)
365 class SortList(hildon.StackableWindow):
366 def __init__(self, parent, listing, feedingit, after_closing, category=None):
367 hildon.StackableWindow.__init__(self)
368 self.set_transient_for(parent)
370 self.isEditingCategories = False
371 self.category = category
372 self.set_title(listing.getCategoryTitle(category))
374 self.isEditingCategories = True
375 self.set_title('Categories')
376 self.listing = listing
377 self.feedingit = feedingit
378 self.after_closing = after_closing
380 self.connect('destroy', lambda w: self.after_closing())
381 self.vbox2 = gtk.VBox(False, 2)
383 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
384 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
385 button.connect("clicked", self.buttonUp)
386 self.vbox2.pack_start(button, expand=False, fill=False)
388 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
389 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
390 button.connect("clicked", self.buttonDown)
391 self.vbox2.pack_start(button, expand=False, fill=False)
393 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
395 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
396 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
397 button.connect("clicked", self.buttonAdd)
398 self.vbox2.pack_start(button, expand=False, fill=False)
400 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
401 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
402 button.connect("clicked", self.buttonEdit)
403 self.vbox2.pack_start(button, expand=False, fill=False)
405 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
406 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
407 button.connect("clicked", self.buttonDelete)
408 self.vbox2.pack_start(button, expand=False, fill=False)
410 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
411 #button.set_label("Done")
412 #button.connect("clicked", self.buttonDone)
413 #self.vbox.pack_start(button)
414 self.hbox2= gtk.HBox(False, 10)
415 self.pannableArea = hildon.PannableArea()
416 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
417 self.treeview = gtk.TreeView(self.treestore)
418 self.hbox2.pack_start(self.pannableArea, expand=True)
420 self.hbox2.pack_end(self.vbox2, expand=False)
421 self.set_default_size(-1, 600)
424 menu = hildon.AppMenu()
425 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
426 button.set_label("Import from OPML")
427 button.connect("clicked", self.feedingit.button_import_clicked)
430 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
431 button.set_label("Export to OPML")
432 button.connect("clicked", self.feedingit.button_export_clicked)
434 self.set_app_menu(menu)
438 #self.connect("destroy", self.buttonDone)
440 def displayFeeds(self):
441 self.treeview.destroy()
442 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
443 self.treeview = gtk.TreeView()
445 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
446 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
448 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
450 self.pannableArea.add(self.treeview)
454 def refreshList(self, selected=None, offset=0):
455 #rect = self.treeview.get_visible_rect()
456 #y = rect.y+rect.height
457 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
458 if self.isEditingCategories:
459 for key in self.listing.getListOfCategories():
460 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
464 for key in self.listing.getListOfFeeds(category=self.category):
465 item = self.treestore.append([self.listing.getFeedTitle(key), key])
468 self.treeview.set_model(self.treestore)
469 if not selected == None:
470 self.treeview.get_selection().select_iter(selectedItem)
471 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
472 self.pannableArea.show_all()
474 def getSelectedItem(self):
475 (model, iter) = self.treeview.get_selection().get_selected()
478 return model.get_value(iter, 1)
480 def findIndex(self, key):
484 for row in self.treestore:
486 return (before, row.iter)
487 if key == list(row)[0]:
491 return (before, None)
493 def buttonUp(self, button):
494 key = self.getSelectedItem()
496 if self.isEditingCategories:
497 self.listing.moveCategoryUp(key)
499 self.listing.moveUp(key)
500 self.refreshList(key, -10)
502 def buttonDown(self, button):
503 key = self.getSelectedItem()
505 if self.isEditingCategories:
506 self.listing.moveCategoryDown(key)
508 self.listing.moveDown(key)
509 self.refreshList(key, 10)
511 def buttonDelete(self, button):
512 key = self.getSelectedItem()
514 message = 'Really remove this feed and its entries?'
515 dlg = hildon.hildon_note_new_confirmation(self, message)
518 if response == gtk.RESPONSE_OK:
519 if self.isEditingCategories:
520 self.listing.removeCategory(key)
522 self.listing.removeFeed(key)
525 def buttonEdit(self, button):
526 key = self.getSelectedItem()
528 if key == 'ArchivedArticles':
529 message = 'Cannot edit the archived articles feed.'
530 hildon.hildon_banner_show_information(self, '', message)
532 if self.isEditingCategories:
534 SortList(self.parent, self.listing, self.feedingit, None, category=key)
537 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
540 (title, url, category) = wizard.getData()
541 if (not title == '') and (not url == ''):
542 self.listing.editFeed(key, title, url, category=category)
546 def buttonDone(self, *args):
549 def buttonAdd(self, button, urlIn="http://"):
550 if self.isEditingCategories:
551 wizard = AddCategoryWizard(self)
554 title = wizard.getData()
555 if (not title == ''):
556 self.listing.addCategory(title)
558 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
561 (title, url, category) = wizard.getData()
562 if (not title == '') and (not url == ''):
563 self.listing.addFeed(title, url, category=category)
568 class DisplayArticle(hildon.StackableWindow):
569 def __init__(self, feed, id, key, config, listing):
570 hildon.StackableWindow.__init__(self)
571 #self.imageDownloader = ImageDownloader()
576 #self.set_title(feed.getTitle(id))
577 self.set_title(self.listing.getFeedTitle(key))
579 self.set_for_removal = False
581 # Init the article display
582 #if self.config.getWebkitSupport():
583 self.view = WebView()
584 #self.view.set_editable(False)
587 # self.view = gtkhtml2.View()
588 # self.document = gtkhtml2.Document()
589 # self.view.set_document(self.document)
590 # self.document.connect("link_clicked", self._signal_link_clicked)
591 self.pannable_article = hildon.PannableArea()
592 self.pannable_article.add(self.view)
593 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
594 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
596 #if self.config.getWebkitSupport():
597 contentLink = self.feed.getContentLink(self.id)
598 self.feed.setEntryRead(self.id)
599 #if key=="ArchivedArticles":
600 self.loadedArticle = False
601 if contentLink.startswith("/home/user/"):
602 self.view.open("file://%s" % contentLink)
603 self.currentUrl = self.feed.getExternalLink(self.id)
605 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
606 self.currentUrl = "%s" % contentLink
607 self.view.connect("motion-notify-event", lambda w,ev: True)
608 self.view.connect('load-started', self.load_started)
609 self.view.connect('load-finished', self.load_finished)
611 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
613 menu = hildon.AppMenu()
614 # Create a button and add it to the menu
615 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
616 button.set_label("Allow horizontal scrolling")
617 button.connect("clicked", self.horiz_scrolling_button)
620 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
621 button.set_label("Open in browser")
622 button.connect("clicked", self.open_in_browser)
625 if key == "ArchivedArticles":
626 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
627 button.set_label("Remove from archived articles")
628 button.connect("clicked", self.remove_archive_button)
630 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631 button.set_label("Add to archived articles")
632 button.connect("clicked", self.archive_button)
635 self.set_app_menu(menu)
638 self.add(self.pannable_article)
640 self.pannable_article.show_all()
642 self.destroyId = self.connect("destroy", self.destroyWindow)
644 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
645 ## Still using an old version of WebKit, so using navigation-requested signal
646 self.view.connect('navigation-requested', self.navigation_requested)
648 self.view.connect("button_press_event", self.button_pressed)
649 self.gestureId = self.view.connect("button_release_event", self.button_released)
651 #def navigation_policy_decision(self, wv, fr, req, action, decision):
652 def navigation_requested(self, wv, fr, req):
653 if self.config.getOpenInExternalBrowser():
654 self.open_in_browser(None, req.get_uri())
659 def load_started(self, *widget):
660 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
662 def load_finished(self, *widget):
663 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
664 frame = self.view.get_main_frame()
665 if self.loadedArticle:
666 self.currentUrl = frame.get_uri()
668 self.loadedArticle = True
670 def button_pressed(self, window, event):
671 #print event.x, event.y
672 self.coords = (event.x, event.y)
674 def button_released(self, window, event):
675 x = self.coords[0] - event.x
676 y = self.coords[1] - event.y
678 if (2*abs(y) < abs(x)):
680 self.emit("article-previous", self.id)
682 self.emit("article-next", self.id)
684 def destroyWindow(self, *args):
685 self.disconnect(self.destroyId)
686 if self.set_for_removal:
687 self.emit("article-deleted", self.id)
689 self.emit("article-closed", self.id)
690 #self.imageDownloader.stopAll()
693 def horiz_scrolling_button(self, *widget):
694 self.pannable_article.disconnect(self.gestureId)
695 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
697 def archive_button(self, *widget):
698 # Call the listing.addArchivedArticle
699 self.listing.addArchivedArticle(self.key, self.id)
701 def remove_archive_button(self, *widget):
702 self.set_for_removal = True
704 def open_in_browser(self, object, link=None):
706 bus = dbus.SessionBus()
707 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
708 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
710 iface.open_new_window(self.currentUrl)
712 iface.open_new_window(link)
714 class DisplayFeed(hildon.StackableWindow):
715 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
716 hildon.StackableWindow.__init__(self)
717 self.listing = listing
719 self.feedTitle = title
720 self.set_title(title)
722 self.current = list()
724 self.updateDbusHandler = updateDbusHandler
726 self.downloadDialog = False
728 #self.listing.setCurrentlyDisplayedFeed(self.key)
732 menu = hildon.AppMenu()
733 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
734 button.set_label("Update feed")
735 button.connect("clicked", self.button_update_clicked)
738 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
739 button.set_label("Mark all as read")
740 button.connect("clicked", self.buttonReadAllClicked)
743 if key=="ArchivedArticles":
744 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
745 button.set_label("Delete read articles")
746 button.connect("clicked", self.buttonPurgeArticles)
749 self.set_app_menu(menu)
752 self.main_vbox = gtk.VBox(False, 0)
753 self.add(self.main_vbox)
755 self.pannableFeed = None
758 if DownloadBar.downloading ():
759 self.show_download_bar ()
761 self.connect('configure-event', self.on_configure_event)
762 self.connect("destroy", self.destroyWindow)
764 def on_configure_event(self, window, event):
765 if getattr(self, 'markup_renderer', None) is None:
768 # Fix up the column width for wrapping the text when the window is
769 # resized (i.e. orientation changed)
770 self.markup_renderer.set_property('wrap-width', event.width-20)
771 it = self.feedItems.get_iter_first()
772 while it is not None:
773 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
774 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
775 it = self.feedItems.iter_next(it)
777 def destroyWindow(self, *args):
778 #self.feed.saveUnread(CONFIGDIR)
779 self.listing.updateUnread(self.key)
780 self.emit("feed-closed", self.key)
782 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
783 #self.listing.closeCurrentlyDisplayedFeed()
785 def fix_title(self, title):
786 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
788 def displayFeed(self):
789 if self.pannableFeed:
790 self.pannableFeed.destroy()
792 self.pannableFeed = hildon.PannableArea()
794 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
796 self.feedItems = gtk.ListStore(str, str)
797 #self.feedList = gtk.TreeView(self.feedItems)
798 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
799 self.feedList.set_rules_hint(True)
801 selection = self.feedList.get_selection()
802 selection.set_mode(gtk.SELECTION_NONE)
803 #selection.connect("changed", lambda w: True)
805 self.feedList.set_model(self.feedItems)
806 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
809 self.feedList.set_hover_selection(False)
810 #self.feedList.set_property('enable-grid-lines', True)
811 #self.feedList.set_property('hildon-mode', 1)
812 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
814 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
816 vbox= gtk.VBox(False, 10)
817 vbox.pack_start(self.feedList)
819 self.pannableFeed.add_with_viewport(vbox)
821 self.markup_renderer = gtk.CellRendererText()
822 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
823 self.markup_renderer.set_property('background', bg_color) #"#333333")
824 (width, height) = self.get_size()
825 self.markup_renderer.set_property('wrap-width', width-20)
826 self.markup_renderer.set_property('ypad', 8)
827 self.markup_renderer.set_property('xpad', 5)
828 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
829 markup=FEED_COLUMN_MARKUP)
830 self.feedList.append_column(markup_column)
832 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
833 hideReadArticles = self.config.getHideReadArticles()
835 articles = self.feed.getIds(onlyUnread=True)
837 articles = self.feed.getIds()
840 self.current = list()
844 isRead = self.feed.isEntryRead(id)
847 if not ( isRead and hideReadArticles ):
848 title = self.fix_title(self.feed.getTitle(id))
849 self.current.append(id)
851 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
853 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
855 self.feedItems.append((markup, id))
858 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
860 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
861 self.feedItems.append((markup, ""))
863 self.main_vbox.pack_start(self.pannableFeed)
867 self.pannableFeed.destroy()
868 #self.remove(self.pannableFeed)
870 def on_feedList_row_activated(self, treeview, path): #, column):
871 selection = self.feedList.get_selection()
872 selection.set_mode(gtk.SELECTION_SINGLE)
873 self.feedList.get_selection().select_path(path)
874 model = treeview.get_model()
875 iter = model.get_iter(path)
876 key = model.get_value(iter, FEED_COLUMN_KEY)
877 # Emulate legacy "button_clicked" call via treeview
878 gobject.idle_add(self.button_clicked, treeview, key)
881 def button_clicked(self, button, index, previous=False, next=False):
882 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
883 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
884 stack = hildon.WindowStack.get_default()
887 stack.pop_and_push(1, newDisp, tmp)
889 gobject.timeout_add(200, self.destroyArticle, tmp)
894 if type(self.disp).__name__ == "DisplayArticle":
895 gobject.timeout_add(200, self.destroyArticle, self.disp)
902 if self.key == "ArchivedArticles":
903 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
904 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
905 self.ids.append(self.disp.connect("article-next", self.nextArticle))
906 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
908 def buttonPurgeArticles(self, *widget):
910 self.feed.purgeReadArticles()
911 #self.feed.saveFeed(CONFIGDIR)
914 def destroyArticle(self, handle):
915 handle.destroyWindow()
917 def mark_item_read(self, key):
918 it = self.feedItems.get_iter_first()
919 while it is not None:
920 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
922 title = self.fix_title(self.feed.getTitle(key))
923 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
924 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
926 it = self.feedItems.iter_next(it)
928 def nextArticle(self, object, index):
929 self.mark_item_read(index)
930 id = self.feed.getNextId(index)
931 while id not in self.current and id != index:
932 id = self.feed.getNextId(id)
934 self.button_clicked(object, id, next=True)
936 def previousArticle(self, object, index):
937 self.mark_item_read(index)
938 id = self.feed.getPreviousId(index)
939 while id not in self.current and id != index:
940 id = self.feed.getPreviousId(id)
942 self.button_clicked(object, id, previous=True)
944 def onArticleClosed(self, object, index):
945 selection = self.feedList.get_selection()
946 selection.set_mode(gtk.SELECTION_NONE)
947 self.mark_item_read(index)
949 def onArticleDeleted(self, object, index):
951 self.feed.removeArticle(index)
952 #self.feed.saveFeed(CONFIGDIR)
955 def button_update_clicked(self, button):
956 self.listing.updateFeed (self.key, priority=-1)
958 def show_download_bar(self):
959 if not type(self.downloadDialog).__name__=="DownloadBar":
960 self.downloadDialog = DownloadBar(self.window)
961 self.downloadDialog.connect("download-done", self.onDownloadDone)
962 self.main_vbox.pack_end(self.downloadDialog,
963 expand=False, fill=False)
966 def onDownloadDone(self, widget, feed):
967 if feed == self.feed or feed is None:
968 self.downloadDialog.destroy()
969 self.downloadDialog = False
970 self.feed = self.listing.getFeed(self.key)
972 self.updateDbusHandler.ArticleCountUpdated()
974 def buttonReadAllClicked(self, button):
976 self.feed.markAllAsRead()
977 it = self.feedItems.get_iter_first()
978 while it is not None:
979 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
980 title = self.fix_title(self.feed.getTitle(k))
981 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
982 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
983 it = self.feedItems.iter_next(it)
985 #for index in self.feed.getIds():
986 # self.feed.setEntryRead(index)
987 # self.mark_item_read(index)
993 self.window = hildon.StackableWindow()
994 self.window.set_title(__appname__)
995 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
996 self.mainVbox = gtk.VBox(False,10)
998 if isfile(CONFIGDIR+"/feeds.db"):
999 self.introLabel = gtk.Label("Loading...")
1001 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1003 self.mainVbox.pack_start(self.introLabel)
1005 self.window.add(self.mainVbox)
1006 self.window.show_all()
1007 self.config = Config(self.window, CONFIGDIR+"config.ini")
1008 gobject.idle_add(self.createWindow)
1010 def createWindow(self):
1013 self.app_lock = get_lock("app_lock")
1014 if self.app_lock == None:
1016 self.stopButton.set_sensitive(True)
1018 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1019 self.stopButton.set_text("Stop update","")
1020 self.stopButton.connect("clicked", self.stop_running_update)
1021 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1022 self.window.show_all()
1023 self.introLabel.set_label("Update in progress, please wait.")
1024 gobject.timeout_add_seconds(3, self.createWindow)
1027 self.stopButton.destroy()
1030 self.listing = Listing(self.config, CONFIGDIR)
1032 self.downloadDialog = False
1034 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1035 self.orientation.set_mode(self.config.getOrientation())
1037 print "Could not start rotation manager"
1039 menu = hildon.AppMenu()
1040 # Create a button and add it to the menu
1041 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1042 button.set_label("Update feeds")
1043 button.connect("clicked", self.button_update_clicked, "All")
1046 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1047 button.set_label("Mark all as read")
1048 button.connect("clicked", self.button_markAll)
1051 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1052 button.set_label("Add new feed")
1053 button.connect("clicked", lambda b: self.addFeed())
1056 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1057 button.set_label("Manage subscriptions")
1058 button.connect("clicked", self.button_organize_clicked)
1061 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1062 button.set_label("Settings")
1063 button.connect("clicked", self.button_preferences_clicked)
1066 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1067 button.set_label("About")
1068 button.connect("clicked", self.button_about_clicked)
1071 self.window.set_app_menu(menu)
1074 #self.feedWindow = hildon.StackableWindow()
1075 #self.articleWindow = hildon.StackableWindow()
1076 self.introLabel.destroy()
1077 self.pannableListing = hildon.PannableArea()
1078 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1079 self.feedList = gtk.TreeView(self.feedItems)
1080 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1081 #self.feedList.set_enable_tree_lines(True)
1082 #self.feedList.set_show_expanders(True)
1083 self.pannableListing.add(self.feedList)
1085 icon_renderer = gtk.CellRendererPixbuf()
1086 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1087 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1089 self.feedList.append_column(icon_column)
1091 markup_renderer = gtk.CellRendererText()
1092 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1093 markup=COLUMN_MARKUP)
1094 self.feedList.append_column(markup_column)
1095 self.mainVbox.pack_start(self.pannableListing)
1096 self.mainVbox.show_all()
1098 self.displayListing()
1099 self.autoupdate = False
1100 self.checkAutoUpdate()
1102 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1103 gobject.idle_add(self.late_init)
1105 def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1106 if (not self.downloadDialog
1107 and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1108 self.updateDbusHandler.UpdateStarted()
1110 self.downloadDialog = DownloadBar(self.window)
1111 self.downloadDialog.connect("download-done", self.onDownloadDone)
1112 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1113 self.mainVbox.show_all()
1115 if self.__dict__.get ('disp', None):
1116 self.disp.show_download_bar ()
1118 def onDownloadDone(self, widget, feed):
1120 self.downloadDialog.destroy()
1121 self.downloadDialog = False
1122 self.displayListing()
1123 self.updateDbusHandler.UpdateFinished()
1124 self.updateDbusHandler.ArticleCountUpdated()
1126 def stop_running_update(self, button):
1127 self.stopButton.set_sensitive(False)
1129 bus=dbus.SessionBus()
1130 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1131 "/org/marcoz/feedingit/update" # Object's path
1133 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1136 def late_init(self):
1137 self.dbusHandler = ServerObject(self)
1138 self.updateDbusHandler = UpdateServerObject(self)
1141 jm.stats_hook_register (self.job_manager_update,
1142 run_in_main_thread=True)
1145 def button_markAll(self, button):
1146 for key in self.listing.getListOfFeeds():
1147 feed = self.listing.getFeed(key)
1148 feed.markAllAsRead()
1149 #for id in feed.getIds():
1150 # feed.setEntryRead(id)
1151 self.listing.updateUnread(key)
1152 self.displayListing()
1154 def button_about_clicked(self, button):
1155 HeAboutDialog.present(self.window, \
1165 def button_export_clicked(self, button):
1166 opml = ExportOpmlData(self.window, self.listing)
1168 def button_import_clicked(self, button):
1169 opml = GetOpmlData(self.window)
1170 feeds = opml.getData()
1171 for (title, url) in feeds:
1172 self.listing.addFeed(title, url)
1173 self.displayListing()
1175 def addFeed(self, urlIn="http://"):
1176 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1179 (title, url, category) = wizard.getData()
1180 if (not title == '') and (not url == ''):
1181 self.listing.addFeed(title, url, category=category)
1183 self.displayListing()
1185 def button_organize_clicked(self, button):
1186 def after_closing():
1187 self.displayListing()
1188 SortList(self.window, self.listing, self, after_closing)
1190 def button_update_clicked(self, button, key):
1191 for k in self.listing.getListOfFeeds():
1192 self.listing.updateFeed (k)
1193 #self.displayListing()
1195 def onDownloadsDone(self, *widget):
1196 self.downloadDialog.destroy()
1197 self.downloadDialog = False
1198 self.displayListing()
1199 self.updateDbusHandler.UpdateFinished()
1200 self.updateDbusHandler.ArticleCountUpdated()
1202 def button_preferences_clicked(self, button):
1203 dialog = self.config.createDialog()
1204 dialog.connect("destroy", self.prefsClosed)
1206 def show_confirmation_note(self, parent, title):
1207 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1209 retcode = gtk.Dialog.run(note)
1212 if retcode == gtk.RESPONSE_OK:
1217 def saveExpandedLines(self):
1218 self.expandedLines = []
1219 model = self.feedList.get_model()
1220 model.foreach(self.checkLine)
1222 def checkLine(self, model, path, iter, data = None):
1223 if self.feedList.row_expanded(path):
1224 self.expandedLines.append(path)
1226 def restoreExpandedLines(self):
1227 model = self.feedList.get_model()
1228 model.foreach(self.restoreLine)
1230 def restoreLine(self, model, path, iter, data = None):
1231 if path in self.expandedLines:
1232 self.feedList.expand_row(path, False)
1234 def displayListing(self):
1235 icon_theme = gtk.icon_theme_get_default()
1236 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1237 gtk.ICON_LOOKUP_USE_BUILTIN)
1239 self.saveExpandedLines()
1241 self.feedItems.clear()
1242 hideReadFeed = self.config.getHideReadFeeds()
1243 order = self.config.getFeedSortOrder()
1245 categories = self.listing.getListOfCategories()
1246 if len(categories) > 1:
1247 showCategories = True
1249 showCategories = False
1251 for categoryId in categories:
1253 title = self.listing.getCategoryTitle(categoryId)
1254 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1256 if showCategories and len(keys)>0:
1257 category = self.feedItems.append(None, (None, title, categoryId))
1258 #print "catID" + str(categoryId) + " " + str(self.category)
1259 if categoryId == self.category:
1261 expandedRow = category
1264 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1265 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1266 updateTime = self.listing.getFeedUpdateTime(key)
1268 updateTime = "Never"
1269 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1271 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1273 markup = FEED_TEMPLATE % (title, subtitle)
1276 icon_filename = self.listing.getFavicon(key)
1277 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1278 LIST_ICON_SIZE, LIST_ICON_SIZE)
1280 pixbuf = default_pixbuf
1283 self.feedItems.append(category, (pixbuf, markup, key))
1285 self.feedItems.append(None, (pixbuf, markup, key))
1288 self.restoreExpandedLines()
1291 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1295 def on_feedList_row_activated(self, treeview, path, column):
1296 model = treeview.get_model()
1297 iter = model.get_iter(path)
1298 key = model.get_value(iter, COLUMN_KEY)
1301 #print "Key: " + str(key)
1303 self.category = catId
1304 if treeview.row_expanded(path):
1305 treeview.collapse_row(path)
1307 # treeview.expand_row(path, True)
1308 #treeview.collapse_all()
1309 #treeview.expand_row(path, False)
1310 #for i in range(len(path)):
1311 # self.feedList.expand_row(path[:i+1], False)
1312 #self.show_confirmation_note(self.window, "Working")
1318 def openFeed(self, key):
1322 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1324 self.feed_lock = get_lock(key)
1325 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1326 self.listing.getFeedTitle(key), key, \
1327 self.config, self.updateDbusHandler)
1328 self.disp.connect("feed-closed", self.onFeedClosed)
1330 def openArticle(self, key, id):
1334 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1336 self.feed_lock = get_lock(key)
1337 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1338 self.listing.getFeedTitle(key), key, \
1339 self.config, self.updateDbusHandler)
1340 self.disp.button_clicked(None, id)
1341 self.disp.connect("feed-closed", self.onFeedClosed)
1344 def onFeedClosed(self, object, key):
1345 #self.listing.saveConfig()
1347 gobject.idle_add(self.onFeedClosedTimeout)
1348 self.displayListing()
1349 #self.updateDbusHandler.ArticleCountUpdated()
1351 def onFeedClosedTimeout(self):
1353 self.updateDbusHandler.ArticleCountUpdated()
1355 def quit(self, *args):
1358 if hasattr (self, 'app_lock'):
1361 # Wait until all slave threads have properly exited before
1362 # terminating the mainloop.
1366 if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1369 gobject.timeout_add(500, self.quit)
1374 self.window.connect("destroy", self.quit)
1377 def prefsClosed(self, *widget):
1379 self.orientation.set_mode(self.config.getOrientation())
1382 self.displayListing()
1383 self.checkAutoUpdate()
1385 def checkAutoUpdate(self, *widget):
1386 interval = int(self.config.getUpdateInterval()*3600000)
1387 if self.config.isAutoUpdateEnabled():
1388 if self.autoupdate == False:
1389 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1390 self.autoupdate = interval
1391 elif not self.autoupdate == interval:
1392 # If auto-update is enabled, but not at the right frequency
1393 gobject.source_remove(self.autoupdateId)
1394 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1395 self.autoupdate = interval
1397 if not self.autoupdate == False:
1398 gobject.source_remove(self.autoupdateId)
1399 self.autoupdate = False
1401 def automaticUpdate(self, *widget):
1402 # Need to check for internet connection
1403 # If no internet connection, try again in 10 minutes:
1404 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1405 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1406 #from time import localtime, strftime
1407 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1409 self.button_update_clicked(None, None)
1412 def stopUpdate(self):
1413 # Not implemented in the app (see update_feeds.py)
1415 JobManager().cancel ()
1419 def getStatus(self):
1421 for key in self.listing.getListOfFeeds():
1422 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1423 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1425 status = "No unread items"
1428 if __name__ == "__main__":
1431 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1432 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1433 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1434 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1435 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1436 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1437 gobject.threads_init()
1438 if not isdir(CONFIGDIR):
1442 print "Error: Can't create configuration directory"
1443 from sys import exit