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'
23 __version__ = '0.9.1~woodchuck'
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, environ
41 from aboutdialog import HeAboutDialog
42 from portrait import FremantleRotation
43 from threading import Thread, activeCount
44 from feedingitdbus import ServerObject
45 from config import Config
46 from cgi import escape
51 logger = logging.getLogger(__name__)
53 from rss_sqlite import Listing
54 from opml import GetOpmlData, ExportOpmlData
58 from socket import setdefaulttimeout
60 setdefaulttimeout(timeout)
68 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
69 ABOUT_ICON = 'feedingit'
70 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
71 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
72 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
73 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
75 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
76 unread_color = color_style.lookup_color('ActiveTextColor')
77 read_color = color_style.lookup_color('DefaultTextColor')
80 CONFIGDIR="/home/user/.feedingit/"
81 LOCK = CONFIGDIR + "update.lock"
84 from htmlentitydefs import name2codepoint
86 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
88 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
92 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
93 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
94 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
96 # Build the markup template for the Maemo 5 text style
97 head_font = style.get_font_desc('SystemFont')
98 sub_font = style.get_font_desc('SmallSystemFont')
100 #head_color = style.get_color('ButtonTextColor')
101 head_color = style.get_color('DefaultTextColor')
102 sub_color = style.get_color('DefaultTextColor')
103 active_color = style.get_color('ActiveTextColor')
105 bg_color = style.get_color('DefaultBackgroundColor').to_string()
106 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
107 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
108 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
109 bg_color = "#" + c1 + c2 + c3
112 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
113 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
115 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
116 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
118 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
119 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
121 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
122 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
124 FEED_TEMPLATE = '\n'.join((head, normal_sub))
125 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
127 ENTRY_TEMPLATE = entry_head
128 ENTRY_TEMPLATE_UNREAD = entry_active_head
130 notification_iface = None
133 global notification_iface
135 bus = dbus.SessionBus()
136 proxy = bus.get_object('org.freedesktop.Notifications',
137 '/org/freedesktop/Notifications')
139 = dbus.Interface(proxy, 'org.freedesktop.Notifications')
142 notification_iface.SystemNoteInfoprint("FeedingIt: " + message)
144 if notification_iface is None:
149 except dbus.DBusException:
150 # Rebind the name and try again.
155 # Removes HTML or XML character references and entities from a text string.
157 # @param text The HTML (or XML) source text.
158 # @return The plain text, as a Unicode string, if necessary.
159 # http://effbot.org/zone/re-sub.htm#unescape-html
164 # character reference
166 if text[:3] == "&#x":
167 return unichr(int(text[3:-1], 16))
169 return unichr(int(text[2:-1]))
175 text = unichr(name2codepoint[text[1:-1]])
178 return text # leave as is
179 return sub("&#?\w+;", fixup, text)
182 class AddWidgetWizard(gtk.Dialog):
183 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
184 gtk.Dialog.__init__(self)
185 self.set_transient_for(parent)
187 #self.category = categories[0]
188 self.category = currentCat
191 self.set_title('Edit RSS feed')
193 self.set_title('Add new RSS feed')
196 self.btn_add = self.add_button('Save', 2)
198 self.btn_add = self.add_button('Add', 2)
200 self.set_default_response(2)
202 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
203 self.nameEntry.set_placeholder('Feed name')
204 # If titleIn matches urlIn, there is no title.
205 if not titleIn == None and titleIn != urlIn:
206 self.nameEntry.set_text(titleIn)
207 self.nameEntry.select_region(-1, -1)
209 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
210 self.urlEntry.set_placeholder('Feed URL')
211 self.urlEntry.set_text(urlIn)
212 self.urlEntry.select_region(-1, -1)
213 self.urlEntry.set_activates_default(True)
215 self.table = gtk.Table(3, 2, False)
216 self.table.set_col_spacings(5)
217 label = gtk.Label('Name:')
218 label.set_alignment(1., .5)
219 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
220 self.table.attach(self.nameEntry, 1, 2, 0, 1)
221 label = gtk.Label('URL:')
222 label.set_alignment(1., .5)
223 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
224 self.table.attach(self.urlEntry, 1, 2, 1, 2)
225 selector = self.create_selector(categories, listing)
226 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
227 picker.set_selector(selector)
228 picker.set_title("Select category")
229 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
230 picker.set_name('HildonButton-finger')
231 picker.set_alignment(0,0,1,1)
233 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
235 self.vbox.pack_start(self.table)
240 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
242 def create_selector(self, choices, listing):
243 #self.pickerDialog = hildon.PickerDialog(self.parent)
244 selector = hildon.TouchSelector(text=True)
248 title = listing.getCategoryTitle(item)
249 iter = selector.append_text(str(title))
250 if self.category == item:
251 selector.set_active(0, index)
252 self.map[title] = item
254 selector.connect("changed", self.selection_changed)
255 #self.pickerDialog.set_selector(selector)
258 def selection_changed(self, selector, button):
259 current_selection = selector.get_current_text()
260 if current_selection:
261 self.category = self.map[current_selection]
263 class AddCategoryWizard(gtk.Dialog):
264 def __init__(self, parent, titleIn=None, isEdit=False):
265 gtk.Dialog.__init__(self)
266 self.set_transient_for(parent)
269 self.set_title('Edit Category')
271 self.set_title('Add Category')
274 self.btn_add = self.add_button('Save', 2)
276 self.btn_add = self.add_button('Add', 2)
278 self.set_default_response(2)
280 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
281 self.nameEntry.set_placeholder('Category name')
282 if not titleIn == None:
283 self.nameEntry.set_text(titleIn)
284 self.nameEntry.select_region(-1, -1)
286 self.table = gtk.Table(1, 2, False)
287 self.table.set_col_spacings(5)
288 label = gtk.Label('Name:')
289 label.set_alignment(1., .5)
290 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
291 self.table.attach(self.nameEntry, 1, 2, 0, 1)
292 #label = gtk.Label('URL:')
293 #label.set_alignment(1., .5)
294 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
295 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
296 self.vbox.pack_start(self.table)
301 return self.nameEntry.get_text()
303 class DownloadBar(gtk.ProgressBar):
306 if hasattr (cls, 'class_init_done'):
309 cls.downloadbars = []
310 # Total number of jobs we are monitoring.
312 # Number of jobs complete (of those that we are monitoring).
317 cls.class_init_done = True
319 bus = dbus.SessionBus()
320 bus.add_signal_receiver(handler_function=cls.update_progress,
322 signal_name='UpdateProgress',
323 dbus_interface='org.marcoz.feedingit',
324 path='/org/marcoz/feedingit/update')
326 def __init__(self, parent):
329 gtk.ProgressBar.__init__(self)
331 self.downloadbars.append(weakref.ref (self))
333 self.__class__.update_bars()
337 def downloading(cls):
339 return cls.done != cls.total
342 def update_progress(cls, percent_complete,
343 completed, in_progress, queued,
344 bytes_downloaded, bytes_updated, bytes_per_second,
346 if not cls.downloadbars:
349 cls.total = completed + in_progress + queued
351 cls.progress = percent_complete / 100.
352 if cls.progress < 0: cls.progress = 0
353 if cls.progress > 1: cls.progress = 1
356 for ref in cls.downloadbars:
359 # The download bar disappeared.
360 cls.downloadbars.remove (ref)
362 bar.emit("download-done", feed_updated)
364 if in_progress == 0 and queued == 0:
365 for ref in cls.downloadbars:
368 # The download bar disappeared.
369 cls.downloadbars.remove (ref)
371 bar.emit("download-done", None)
377 def update_bars(cls):
378 # In preparation for i18n/l10n
380 return (a if n == 1 else b)
382 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
384 % (cls.done, cls.total))
386 for ref in cls.downloadbars:
389 # The download bar disappeared.
390 cls.downloadbars.remove (ref)
393 bar.set_fraction(cls.progress)
395 class SortList(hildon.StackableWindow):
396 def __init__(self, parent, listing, feedingit, after_closing, category=None):
397 hildon.StackableWindow.__init__(self)
398 self.set_transient_for(parent)
400 self.isEditingCategories = False
401 self.category = category
402 self.set_title(listing.getCategoryTitle(category))
404 self.isEditingCategories = True
405 self.set_title('Categories')
406 self.listing = listing
407 self.feedingit = feedingit
408 self.after_closing = after_closing
410 self.connect('destroy', lambda w: self.after_closing())
411 self.vbox2 = gtk.VBox(False, 2)
413 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
414 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
415 button.connect("clicked", self.buttonUp)
416 self.vbox2.pack_start(button, expand=False, fill=False)
418 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
419 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
420 button.connect("clicked", self.buttonDown)
421 self.vbox2.pack_start(button, expand=False, fill=False)
423 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
425 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
426 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
427 button.connect("clicked", self.buttonAdd)
428 self.vbox2.pack_start(button, expand=False, fill=False)
430 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
431 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
432 button.connect("clicked", self.buttonEdit)
433 self.vbox2.pack_start(button, expand=False, fill=False)
435 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
436 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
437 button.connect("clicked", self.buttonDelete)
438 self.vbox2.pack_start(button, expand=False, fill=False)
440 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
441 #button.set_label("Done")
442 #button.connect("clicked", self.buttonDone)
443 #self.vbox.pack_start(button)
444 self.hbox2= gtk.HBox(False, 10)
445 self.pannableArea = hildon.PannableArea()
446 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
447 self.treeview = gtk.TreeView(self.treestore)
448 self.hbox2.pack_start(self.pannableArea, expand=True)
450 self.hbox2.pack_end(self.vbox2, expand=False)
451 self.set_default_size(-1, 600)
454 menu = hildon.AppMenu()
455 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
456 button.set_label("Import from OPML")
457 button.connect("clicked", self.feedingit.button_import_clicked)
460 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
461 button.set_label("Export to OPML")
462 button.connect("clicked", self.feedingit.button_export_clicked)
464 self.set_app_menu(menu)
468 #self.connect("destroy", self.buttonDone)
470 def displayFeeds(self):
471 self.treeview.destroy()
472 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
473 self.treeview = gtk.TreeView()
475 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
476 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
478 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
480 self.pannableArea.add(self.treeview)
484 def refreshList(self, selected=None, offset=0):
485 #rect = self.treeview.get_visible_rect()
486 #y = rect.y+rect.height
487 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
488 if self.isEditingCategories:
489 for key in self.listing.getListOfCategories():
490 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
494 for key in self.listing.getListOfFeeds(category=self.category):
495 item = self.treestore.append([self.listing.getFeedTitle(key), key])
498 self.treeview.set_model(self.treestore)
499 if not selected == None:
500 self.treeview.get_selection().select_iter(selectedItem)
501 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
502 self.pannableArea.show_all()
504 def getSelectedItem(self):
505 (model, iter) = self.treeview.get_selection().get_selected()
508 return model.get_value(iter, 1)
510 def findIndex(self, key):
514 for row in self.treestore:
516 return (before, row.iter)
517 if key == list(row)[0]:
521 return (before, None)
523 def buttonUp(self, button):
524 key = self.getSelectedItem()
526 if self.isEditingCategories:
527 self.listing.moveCategoryUp(key)
529 self.listing.moveUp(key)
530 self.refreshList(key, -10)
532 def buttonDown(self, button):
533 key = self.getSelectedItem()
535 if self.isEditingCategories:
536 self.listing.moveCategoryDown(key)
538 self.listing.moveDown(key)
539 self.refreshList(key, 10)
541 def buttonDelete(self, button):
542 key = self.getSelectedItem()
544 message = 'Really remove this feed and its entries?'
545 dlg = hildon.hildon_note_new_confirmation(self, message)
548 if response == gtk.RESPONSE_OK:
549 if self.isEditingCategories:
550 self.listing.removeCategory(key)
552 self.listing.removeFeed(key)
555 def buttonEdit(self, button):
556 key = self.getSelectedItem()
558 if key == 'ArchivedArticles':
559 message = 'Cannot edit the archived articles feed.'
560 hildon.hildon_banner_show_information(self, '', message)
562 if self.isEditingCategories:
564 SortList(self.parent, self.listing, self.feedingit, None, category=key)
567 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
570 (title, url, category) = wizard.getData()
572 self.listing.editFeed(key, title, url, category=category)
576 def buttonDone(self, *args):
579 def buttonAdd(self, button, urlIn="http://"):
580 if self.isEditingCategories:
581 wizard = AddCategoryWizard(self)
584 title = wizard.getData()
585 if (not title == ''):
586 self.listing.addCategory(title)
588 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
591 (title, url, category) = wizard.getData()
593 self.listing.addFeed(title, url, category=category)
598 class DisplayArticle(hildon.StackableWindow):
599 def __init__(self, feed, id, key, config, listing):
600 hildon.StackableWindow.__init__(self)
601 #self.imageDownloader = ImageDownloader()
606 #self.set_title(feed.getTitle(id))
607 self.set_title(self.listing.getFeedTitle(key))
609 self.set_for_removal = False
611 # Init the article display
612 #if self.config.getWebkitSupport():
613 self.view = WebView()
614 #self.view.set_editable(False)
617 # self.view = gtkhtml2.View()
618 # self.document = gtkhtml2.Document()
619 # self.view.set_document(self.document)
620 # self.document.connect("link_clicked", self._signal_link_clicked)
621 self.pannable_article = hildon.PannableArea()
622 self.pannable_article.add(self.view)
623 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
624 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
626 #if self.config.getWebkitSupport():
627 contentLink = self.feed.getContentLink(self.id)
628 self.feed.setEntryRead(self.id)
629 #if key=="ArchivedArticles":
630 self.loadedArticle = False
631 if contentLink.startswith("/home/user/"):
632 self.view.open("file://%s" % contentLink)
633 self.currentUrl = self.feed.getExternalLink(self.id)
635 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
636 self.currentUrl = "%s" % contentLink
637 self.view.connect("motion-notify-event", lambda w,ev: True)
638 self.view.connect('load-started', self.load_started)
639 self.view.connect('load-finished', self.load_finished)
641 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
643 menu = hildon.AppMenu()
644 # Create a button and add it to the menu
645 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
646 button.set_label("Allow horizontal scrolling")
647 button.connect("clicked", self.horiz_scrolling_button)
650 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
651 button.set_label("Open in browser")
652 button.connect("clicked", self.open_in_browser)
655 if key == "ArchivedArticles":
656 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
657 button.set_label("Remove from archived articles")
658 button.connect("clicked", self.remove_archive_button)
660 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
661 button.set_label("Add to archived articles")
662 button.connect("clicked", self.archive_button)
665 self.set_app_menu(menu)
668 self.add(self.pannable_article)
670 self.pannable_article.show_all()
672 self.destroyId = self.connect("destroy", self.destroyWindow)
674 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
675 ## Still using an old version of WebKit, so using navigation-requested signal
676 self.view.connect('navigation-requested', self.navigation_requested)
678 self.view.connect("button_press_event", self.button_pressed)
679 self.gestureId = self.view.connect("button_release_event", self.button_released)
681 #def navigation_policy_decision(self, wv, fr, req, action, decision):
682 def navigation_requested(self, wv, fr, req):
683 if self.config.getOpenInExternalBrowser():
684 self.open_in_browser(None, req.get_uri())
689 def load_started(self, *widget):
690 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
692 def load_finished(self, *widget):
693 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
694 frame = self.view.get_main_frame()
695 if self.loadedArticle:
696 self.currentUrl = frame.get_uri()
698 self.loadedArticle = True
700 def button_pressed(self, window, event):
701 #print event.x, event.y
702 self.coords = (event.x, event.y)
704 def button_released(self, window, event):
705 x = self.coords[0] - event.x
706 y = self.coords[1] - event.y
708 if (2*abs(y) < abs(x)):
710 self.emit("article-previous", self.id)
712 self.emit("article-next", self.id)
714 def destroyWindow(self, *args):
715 self.disconnect(self.destroyId)
716 if self.set_for_removal:
717 self.emit("article-deleted", self.id)
719 self.emit("article-closed", self.id)
720 #self.imageDownloader.stopAll()
723 def horiz_scrolling_button(self, *widget):
724 self.pannable_article.disconnect(self.gestureId)
725 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
727 def archive_button(self, *widget):
728 # Call the listing.addArchivedArticle
729 self.listing.addArchivedArticle(self.key, self.id)
731 def remove_archive_button(self, *widget):
732 self.set_for_removal = True
734 def open_in_browser(self, object, link=None):
736 link = self.currentUrl
738 bus = dbus.SessionBus()
739 b_proxy = bus.get_object("com.nokia.osso_browser",
740 "/com/nokia/osso_browser/request")
741 b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
743 notify("Opening %s" % link)
745 # We open the link asynchronously: if the web browser is not
746 # already running, this can take a while.
749 Something went wrong opening the URL.
752 notify("Error opening %s: %s" % (link, str(exception)))
755 b_iface.open_new_window(link,
756 reply_handler=lambda *args: None,
757 error_handler=error_handler())
759 class DisplayFeed(hildon.StackableWindow):
760 def __init__(self, listing, feed, title, key, config):
761 hildon.StackableWindow.__init__(self)
762 self.listing = listing
764 self.feedTitle = title
765 self.set_title(title)
767 self.current = list()
770 self.downloadDialog = False
772 #self.listing.setCurrentlyDisplayedFeed(self.key)
776 menu = hildon.AppMenu()
777 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
778 button.set_label("Update feed")
779 button.connect("clicked", self.button_update_clicked)
782 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
783 button.set_label("Mark all as read")
784 button.connect("clicked", self.buttonReadAllClicked)
787 if key=="ArchivedArticles":
788 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
789 button.set_label("Delete read articles")
790 button.connect("clicked", self.buttonPurgeArticles)
793 self.set_app_menu(menu)
796 self.main_vbox = gtk.VBox(False, 0)
797 self.add(self.main_vbox)
799 self.pannableFeed = None
802 if DownloadBar.downloading ():
803 self.show_download_bar ()
805 self.connect('configure-event', self.on_configure_event)
806 self.connect("destroy", self.destroyWindow)
808 def on_configure_event(self, window, event):
809 if getattr(self, 'markup_renderer', None) is None:
812 # Fix up the column width for wrapping the text when the window is
813 # resized (i.e. orientation changed)
814 self.markup_renderer.set_property('wrap-width', event.width-20)
815 it = self.feedItems.get_iter_first()
816 while it is not None:
817 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
818 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
819 it = self.feedItems.iter_next(it)
821 def destroyWindow(self, *args):
822 #self.feed.saveUnread(CONFIGDIR)
823 self.listing.updateUnread(self.key)
824 self.emit("feed-closed", self.key)
826 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
827 #self.listing.closeCurrentlyDisplayedFeed()
829 def fix_title(self, title):
830 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
832 def displayFeed(self):
833 if self.pannableFeed:
834 self.pannableFeed.destroy()
836 self.pannableFeed = hildon.PannableArea()
838 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
840 self.feedItems = gtk.ListStore(str, str)
841 #self.feedList = gtk.TreeView(self.feedItems)
842 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
843 self.feedList.set_rules_hint(True)
845 selection = self.feedList.get_selection()
846 selection.set_mode(gtk.SELECTION_NONE)
847 #selection.connect("changed", lambda w: True)
849 self.feedList.set_model(self.feedItems)
850 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
853 self.feedList.set_hover_selection(False)
854 #self.feedList.set_property('enable-grid-lines', True)
855 #self.feedList.set_property('hildon-mode', 1)
856 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
858 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
860 vbox= gtk.VBox(False, 10)
861 vbox.pack_start(self.feedList)
863 self.pannableFeed.add_with_viewport(vbox)
865 self.markup_renderer = gtk.CellRendererText()
866 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
867 self.markup_renderer.set_property('background', bg_color) #"#333333")
868 (width, height) = self.get_size()
869 self.markup_renderer.set_property('wrap-width', width-20)
870 self.markup_renderer.set_property('ypad', 8)
871 self.markup_renderer.set_property('xpad', 5)
872 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
873 markup=FEED_COLUMN_MARKUP)
874 self.feedList.append_column(markup_column)
876 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
877 hideReadArticles = self.config.getHideReadArticles()
879 articles = self.feed.getIds(onlyUnread=True)
881 articles = self.feed.getIds()
884 self.current = list()
888 isRead = self.feed.isEntryRead(id)
891 if not ( isRead and hideReadArticles ):
892 title = self.fix_title(self.feed.getTitle(id))
893 self.current.append(id)
895 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
897 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
899 self.feedItems.append((markup, id))
902 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
904 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
905 self.feedItems.append((markup, ""))
907 self.main_vbox.pack_start(self.pannableFeed)
911 self.pannableFeed.destroy()
912 #self.remove(self.pannableFeed)
914 def on_feedList_row_activated(self, treeview, path): #, column):
915 selection = self.feedList.get_selection()
916 selection.set_mode(gtk.SELECTION_SINGLE)
917 self.feedList.get_selection().select_path(path)
918 model = treeview.get_model()
919 iter = model.get_iter(path)
920 key = model.get_value(iter, FEED_COLUMN_KEY)
921 # Emulate legacy "button_clicked" call via treeview
922 gobject.idle_add(self.button_clicked, treeview, key)
925 def button_clicked(self, button, index, previous=False, next=False):
926 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
927 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
928 stack = hildon.WindowStack.get_default()
931 stack.pop_and_push(1, newDisp, tmp)
933 gobject.timeout_add(200, self.destroyArticle, tmp)
938 if type(self.disp).__name__ == "DisplayArticle":
939 gobject.timeout_add(200, self.destroyArticle, self.disp)
946 if self.key == "ArchivedArticles":
947 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
948 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
949 self.ids.append(self.disp.connect("article-next", self.nextArticle))
950 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
952 def buttonPurgeArticles(self, *widget):
954 self.feed.purgeReadArticles()
955 #self.feed.saveFeed(CONFIGDIR)
958 def destroyArticle(self, handle):
959 handle.destroyWindow()
961 def mark_item_read(self, key):
962 it = self.feedItems.get_iter_first()
963 while it is not None:
964 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
966 title = self.fix_title(self.feed.getTitle(key))
967 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
968 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
970 it = self.feedItems.iter_next(it)
972 def nextArticle(self, object, index):
973 self.mark_item_read(index)
974 id = self.feed.getNextId(index)
975 while id not in self.current and id != index:
976 id = self.feed.getNextId(id)
978 self.button_clicked(object, id, next=True)
980 def previousArticle(self, object, index):
981 self.mark_item_read(index)
982 id = self.feed.getPreviousId(index)
983 while id not in self.current and id != index:
984 id = self.feed.getPreviousId(id)
986 self.button_clicked(object, id, previous=True)
988 def onArticleClosed(self, object, index):
989 selection = self.feedList.get_selection()
990 selection.set_mode(gtk.SELECTION_NONE)
991 self.mark_item_read(index)
993 def onArticleDeleted(self, object, index):
995 self.feed.removeArticle(index)
996 #self.feed.saveFeed(CONFIGDIR)
1000 def do_update_feed(self):
1001 self.listing.updateFeed (self.key, priority=-1)
1003 def button_update_clicked(self, button):
1004 gobject.idle_add(self.do_update_feed)
1006 def show_download_bar(self):
1007 if not type(self.downloadDialog).__name__=="DownloadBar":
1008 self.downloadDialog = DownloadBar(self.window)
1009 self.downloadDialog.connect("download-done", self.onDownloadDone)
1010 self.main_vbox.pack_end(self.downloadDialog,
1011 expand=False, fill=False)
1014 def onDownloadDone(self, widget, feed):
1015 if feed == self.feed:
1016 self.feed = self.listing.getFeed(self.key)
1020 self.downloadDialog.destroy()
1021 self.downloadDialog = False
1023 def buttonReadAllClicked(self, button):
1025 self.feed.markAllAsRead()
1026 it = self.feedItems.get_iter_first()
1027 while it is not None:
1028 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1029 title = self.fix_title(self.feed.getTitle(k))
1030 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1031 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1032 it = self.feedItems.iter_next(it)
1034 #for index in self.feed.getIds():
1035 # self.feed.setEntryRead(index)
1036 # self.mark_item_read(index)
1042 self.window = hildon.StackableWindow()
1043 self.window.set_title(__appname__)
1044 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1045 self.mainVbox = gtk.VBox(False,10)
1047 if isfile(CONFIGDIR+"/feeds.db"):
1048 self.introLabel = gtk.Label("Loading...")
1050 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1052 self.mainVbox.pack_start(self.introLabel)
1054 self.window.add(self.mainVbox)
1055 self.window.show_all()
1056 self.config = Config(self.window, CONFIGDIR+"config.ini")
1057 gobject.idle_add(self.createWindow)
1059 def createWindow(self):
1061 self.listing = Listing(self.config, CONFIGDIR)
1063 self.downloadDialog = False
1065 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1066 self.orientation.set_mode(self.config.getOrientation())
1067 except Exception, e:
1068 logger.warn("Could not start rotation manager: %s" % str(e))
1070 menu = hildon.AppMenu()
1071 # Create a button and add it to the menu
1072 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1073 button.set_label("Update feeds")
1074 button.connect("clicked", self.button_update_clicked, "All")
1077 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1078 button.set_label("Mark all as read")
1079 button.connect("clicked", self.button_markAll)
1082 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1083 button.set_label("Add new feed")
1084 button.connect("clicked", lambda b: self.addFeed())
1087 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1088 button.set_label("Manage subscriptions")
1089 button.connect("clicked", self.button_organize_clicked)
1092 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1093 button.set_label("Settings")
1094 button.connect("clicked", self.button_preferences_clicked)
1097 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1098 button.set_label("About")
1099 button.connect("clicked", self.button_about_clicked)
1102 self.window.set_app_menu(menu)
1105 #self.feedWindow = hildon.StackableWindow()
1106 #self.articleWindow = hildon.StackableWindow()
1107 self.introLabel.destroy()
1108 self.pannableListing = hildon.PannableArea()
1109 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1110 self.feedList = gtk.TreeView(self.feedItems)
1111 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1112 #self.feedList.set_enable_tree_lines(True)
1113 #self.feedList.set_show_expanders(True)
1114 self.pannableListing.add(self.feedList)
1116 icon_renderer = gtk.CellRendererPixbuf()
1117 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1118 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1120 self.feedList.append_column(icon_column)
1122 markup_renderer = gtk.CellRendererText()
1123 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1124 markup=COLUMN_MARKUP)
1125 self.feedList.append_column(markup_column)
1126 self.mainVbox.pack_start(self.pannableListing)
1127 self.mainVbox.show_all()
1129 self.displayListing()
1130 self.autoupdate = False
1131 self.checkAutoUpdate()
1133 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1134 gobject.idle_add(self.late_init)
1136 def update_progress(self, percent_complete,
1137 completed, in_progress, queued,
1138 bytes_downloaded, bytes_updated, bytes_per_second,
1140 if (in_progress or queued) and not self.downloadDialog:
1141 self.downloadDialog = DownloadBar(self.window)
1142 self.downloadDialog.connect("download-done", self.onDownloadDone)
1143 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1144 self.mainVbox.show_all()
1146 if self.__dict__.get ('disp', None):
1147 self.disp.show_download_bar ()
1149 def onDownloadDone(self, widget, feed):
1151 self.downloadDialog.destroy()
1152 self.downloadDialog = False
1153 self.displayListing()
1155 def late_init(self):
1156 self.dbusHandler = ServerObject(self)
1157 bus = dbus.SessionBus()
1158 bus.add_signal_receiver(handler_function=self.update_progress,
1160 signal_name='UpdateProgress',
1161 dbus_interface='org.marcoz.feedingit',
1162 path='/org/marcoz/feedingit/update')
1164 def button_markAll(self, button):
1165 for key in self.listing.getListOfFeeds():
1166 feed = self.listing.getFeed(key)
1167 feed.markAllAsRead()
1168 #for id in feed.getIds():
1169 # feed.setEntryRead(id)
1170 self.listing.updateUnread(key)
1171 self.displayListing()
1173 def button_about_clicked(self, button):
1174 HeAboutDialog.present(self.window, \
1184 def button_export_clicked(self, button):
1185 opml = ExportOpmlData(self.window, self.listing)
1187 def button_import_clicked(self, button):
1188 opml = GetOpmlData(self.window)
1189 feeds = opml.getData()
1190 for (title, url) in feeds:
1191 self.listing.addFeed(title, url)
1192 self.displayListing()
1194 def addFeed(self, urlIn="http://"):
1195 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1198 (title, url, category) = wizard.getData()
1200 self.listing.addFeed(title, url, category=category)
1202 self.displayListing()
1204 def button_organize_clicked(self, button):
1205 def after_closing():
1206 self.displayListing()
1207 SortList(self.window, self.listing, self, after_closing)
1209 def do_update_feeds(self):
1210 for k in self.listing.getListOfFeeds():
1211 self.listing.updateFeed (k)
1213 def button_update_clicked(self, button, key):
1214 gobject.idle_add(self.do_update_feeds)
1216 def onDownloadsDone(self, *widget):
1217 self.downloadDialog.destroy()
1218 self.downloadDialog = False
1219 self.displayListing()
1221 def button_preferences_clicked(self, button):
1222 dialog = self.config.createDialog()
1223 dialog.connect("destroy", self.prefsClosed)
1225 def show_confirmation_note(self, parent, title):
1226 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1228 retcode = gtk.Dialog.run(note)
1231 if retcode == gtk.RESPONSE_OK:
1236 def saveExpandedLines(self):
1237 self.expandedLines = []
1238 model = self.feedList.get_model()
1239 model.foreach(self.checkLine)
1241 def checkLine(self, model, path, iter, data = None):
1242 if self.feedList.row_expanded(path):
1243 self.expandedLines.append(path)
1245 def restoreExpandedLines(self):
1246 model = self.feedList.get_model()
1247 model.foreach(self.restoreLine)
1249 def restoreLine(self, model, path, iter, data = None):
1250 if path in self.expandedLines:
1251 self.feedList.expand_row(path, False)
1253 def displayListing(self):
1254 icon_theme = gtk.icon_theme_get_default()
1255 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1256 gtk.ICON_LOOKUP_USE_BUILTIN)
1258 self.saveExpandedLines()
1260 self.feedItems.clear()
1261 hideReadFeed = self.config.getHideReadFeeds()
1262 order = self.config.getFeedSortOrder()
1264 categories = self.listing.getListOfCategories()
1265 if len(categories) > 1:
1266 showCategories = True
1268 showCategories = False
1270 for categoryId in categories:
1272 title = self.listing.getCategoryTitle(categoryId)
1273 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1275 if showCategories and len(keys)>0:
1276 category = self.feedItems.append(None, (None, title, categoryId))
1277 #print "catID" + str(categoryId) + " " + str(self.category)
1278 if categoryId == self.category:
1280 expandedRow = category
1283 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1284 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1285 updateTime = self.listing.getFeedUpdateTime(key)
1287 updateTime = "Never"
1288 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1290 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1292 markup = FEED_TEMPLATE % (title, subtitle)
1295 icon_filename = self.listing.getFavicon(key)
1296 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1297 LIST_ICON_SIZE, LIST_ICON_SIZE)
1299 pixbuf = default_pixbuf
1302 self.feedItems.append(category, (pixbuf, markup, key))
1304 self.feedItems.append(None, (pixbuf, markup, key))
1307 self.restoreExpandedLines()
1310 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1314 def on_feedList_row_activated(self, treeview, path, column):
1315 model = treeview.get_model()
1316 iter = model.get_iter(path)
1317 key = model.get_value(iter, COLUMN_KEY)
1320 #print "Key: " + str(key)
1322 self.category = catId
1323 if treeview.row_expanded(path):
1324 treeview.collapse_row(path)
1326 # treeview.expand_row(path, True)
1327 #treeview.collapse_all()
1328 #treeview.expand_row(path, False)
1329 #for i in range(len(path)):
1330 # self.feedList.expand_row(path[:i+1], False)
1331 #self.show_confirmation_note(self.window, "Working")
1337 def openFeed(self, key):
1339 self.disp = DisplayFeed(
1340 self.listing, self.listing.getFeed(key),
1341 self.listing.getFeedTitle(key), key,
1343 self.disp.connect("feed-closed", self.onFeedClosed)
1345 def openArticle(self, key, id):
1348 self.disp.button_clicked(None, id)
1350 def onFeedClosed(self, object, key):
1351 self.displayListing()
1353 def quit(self, *args):
1358 self.window.connect("destroy", self.quit)
1361 def prefsClosed(self, *widget):
1363 self.orientation.set_mode(self.config.getOrientation())
1366 self.displayListing()
1367 self.checkAutoUpdate()
1369 def checkAutoUpdate(self, *widget):
1370 interval = int(self.config.getUpdateInterval()*3600000)
1371 if self.config.isAutoUpdateEnabled():
1372 if self.autoupdate == False:
1373 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1374 self.autoupdate = interval
1375 elif not self.autoupdate == interval:
1376 # If auto-update is enabled, but not at the right frequency
1377 gobject.source_remove(self.autoupdateId)
1378 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1379 self.autoupdate = interval
1381 if not self.autoupdate == False:
1382 gobject.source_remove(self.autoupdateId)
1383 self.autoupdate = False
1385 def automaticUpdate(self, *widget):
1386 # Need to check for internet connection
1387 # If no internet connection, try again in 10 minutes:
1388 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1389 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1390 #from time import localtime, strftime
1391 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1393 self.button_update_clicked(None, None)
1396 def getStatus(self):
1398 for key in self.listing.getListOfFeeds():
1399 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1400 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1402 status = "No unread items"
1405 if __name__ == "__main__":
1407 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1409 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1410 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1411 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1412 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1413 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1414 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1415 gobject.threads_init()
1416 if not isdir(CONFIGDIR):
1420 logger.error("Error: Can't create configuration directory")
1421 from sys import exit