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
131 # Removes HTML or XML character references and entities from a text string.
133 # @param text The HTML (or XML) source text.
134 # @return The plain text, as a Unicode string, if necessary.
135 # http://effbot.org/zone/re-sub.htm#unescape-html
140 # character reference
142 if text[:3] == "&#x":
143 return unichr(int(text[3:-1], 16))
145 return unichr(int(text[2:-1]))
151 text = unichr(name2codepoint[text[1:-1]])
154 return text # leave as is
155 return sub("&#?\w+;", fixup, text)
158 class AddWidgetWizard(gtk.Dialog):
159 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
160 gtk.Dialog.__init__(self)
161 self.set_transient_for(parent)
163 #self.category = categories[0]
164 self.category = currentCat
167 self.set_title('Edit RSS feed')
169 self.set_title('Add new RSS feed')
172 self.btn_add = self.add_button('Save', 2)
174 self.btn_add = self.add_button('Add', 2)
176 self.set_default_response(2)
178 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
179 self.nameEntry.set_placeholder('Feed name')
180 # If titleIn matches urlIn, there is no title.
181 if not titleIn == None and titleIn != urlIn:
182 self.nameEntry.set_text(titleIn)
183 self.nameEntry.select_region(-1, -1)
185 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
186 self.urlEntry.set_placeholder('Feed URL')
187 self.urlEntry.set_text(urlIn)
188 self.urlEntry.select_region(-1, -1)
189 self.urlEntry.set_activates_default(True)
191 self.table = gtk.Table(3, 2, False)
192 self.table.set_col_spacings(5)
193 label = gtk.Label('Name:')
194 label.set_alignment(1., .5)
195 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
196 self.table.attach(self.nameEntry, 1, 2, 0, 1)
197 label = gtk.Label('URL:')
198 label.set_alignment(1., .5)
199 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
200 self.table.attach(self.urlEntry, 1, 2, 1, 2)
201 selector = self.create_selector(categories, listing)
202 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
203 picker.set_selector(selector)
204 picker.set_title("Select category")
205 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
206 picker.set_name('HildonButton-finger')
207 picker.set_alignment(0,0,1,1)
209 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
211 self.vbox.pack_start(self.table)
216 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
218 def create_selector(self, choices, listing):
219 #self.pickerDialog = hildon.PickerDialog(self.parent)
220 selector = hildon.TouchSelector(text=True)
224 title = listing.getCategoryTitle(item)
225 iter = selector.append_text(str(title))
226 if self.category == item:
227 selector.set_active(0, index)
228 self.map[title] = item
230 selector.connect("changed", self.selection_changed)
231 #self.pickerDialog.set_selector(selector)
234 def selection_changed(self, selector, button):
235 current_selection = selector.get_current_text()
236 if current_selection:
237 self.category = self.map[current_selection]
239 class AddCategoryWizard(gtk.Dialog):
240 def __init__(self, parent, titleIn=None, isEdit=False):
241 gtk.Dialog.__init__(self)
242 self.set_transient_for(parent)
245 self.set_title('Edit Category')
247 self.set_title('Add Category')
250 self.btn_add = self.add_button('Save', 2)
252 self.btn_add = self.add_button('Add', 2)
254 self.set_default_response(2)
256 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
257 self.nameEntry.set_placeholder('Category name')
258 if not titleIn == None:
259 self.nameEntry.set_text(titleIn)
260 self.nameEntry.select_region(-1, -1)
262 self.table = gtk.Table(1, 2, False)
263 self.table.set_col_spacings(5)
264 label = gtk.Label('Name:')
265 label.set_alignment(1., .5)
266 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
267 self.table.attach(self.nameEntry, 1, 2, 0, 1)
268 #label = gtk.Label('URL:')
269 #label.set_alignment(1., .5)
270 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
271 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
272 self.vbox.pack_start(self.table)
277 return self.nameEntry.get_text()
279 class DownloadBar(gtk.ProgressBar):
282 if hasattr (cls, 'class_init_done'):
285 cls.downloadbars = []
286 # Total number of jobs we are monitoring.
288 # Number of jobs complete (of those that we are monitoring).
293 cls.class_init_done = True
295 bus = dbus.SessionBus()
296 bus.add_signal_receiver(handler_function=cls.update_progress,
298 signal_name='UpdateProgress',
299 dbus_interface='org.marcoz.feedingit',
300 path='/org/marcoz/feedingit/update')
302 def __init__(self, parent):
305 gtk.ProgressBar.__init__(self)
307 self.downloadbars.append(weakref.ref (self))
309 self.__class__.update_bars()
313 def downloading(cls):
315 return cls.done != cls.total
318 def update_progress(cls, percent_complete,
319 completed, in_progress, queued,
320 bytes_downloaded, bytes_updated, bytes_per_second,
322 if not cls.downloadbars:
325 cls.total = completed + in_progress + queued
327 cls.progress = percent_complete / 100.
328 if cls.progress < 0: cls.progress = 0
329 if cls.progress > 1: cls.progress = 1
332 for ref in cls.downloadbars:
335 # The download bar disappeared.
336 cls.downloadbars.remove (ref)
338 bar.emit("download-done", feed_updated)
340 if in_progress == 0 and queued == 0:
341 for ref in cls.downloadbars:
344 # The download bar disappeared.
345 cls.downloadbars.remove (ref)
347 bar.emit("download-done", None)
353 def update_bars(cls):
354 # In preparation for i18n/l10n
356 return (a if n == 1 else b)
358 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
360 % (cls.done, cls.total))
362 for ref in cls.downloadbars:
365 # The download bar disappeared.
366 cls.downloadbars.remove (ref)
369 bar.set_fraction(cls.progress)
371 class SortList(hildon.StackableWindow):
372 def __init__(self, parent, listing, feedingit, after_closing, category=None):
373 hildon.StackableWindow.__init__(self)
374 self.set_transient_for(parent)
376 self.isEditingCategories = False
377 self.category = category
378 self.set_title(listing.getCategoryTitle(category))
380 self.isEditingCategories = True
381 self.set_title('Categories')
382 self.listing = listing
383 self.feedingit = feedingit
384 self.after_closing = after_closing
386 self.connect('destroy', lambda w: self.after_closing())
387 self.vbox2 = gtk.VBox(False, 2)
389 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
390 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
391 button.connect("clicked", self.buttonUp)
392 self.vbox2.pack_start(button, expand=False, fill=False)
394 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
395 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
396 button.connect("clicked", self.buttonDown)
397 self.vbox2.pack_start(button, expand=False, fill=False)
399 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
401 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
402 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
403 button.connect("clicked", self.buttonAdd)
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_information', gtk.ICON_SIZE_BUTTON))
408 button.connect("clicked", self.buttonEdit)
409 self.vbox2.pack_start(button, expand=False, fill=False)
411 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
412 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
413 button.connect("clicked", self.buttonDelete)
414 self.vbox2.pack_start(button, expand=False, fill=False)
416 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
417 #button.set_label("Done")
418 #button.connect("clicked", self.buttonDone)
419 #self.vbox.pack_start(button)
420 self.hbox2= gtk.HBox(False, 10)
421 self.pannableArea = hildon.PannableArea()
422 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
423 self.treeview = gtk.TreeView(self.treestore)
424 self.hbox2.pack_start(self.pannableArea, expand=True)
426 self.hbox2.pack_end(self.vbox2, expand=False)
427 self.set_default_size(-1, 600)
430 menu = hildon.AppMenu()
431 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
432 button.set_label("Import from OPML")
433 button.connect("clicked", self.feedingit.button_import_clicked)
436 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
437 button.set_label("Export to OPML")
438 button.connect("clicked", self.feedingit.button_export_clicked)
440 self.set_app_menu(menu)
444 #self.connect("destroy", self.buttonDone)
446 def displayFeeds(self):
447 self.treeview.destroy()
448 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
449 self.treeview = gtk.TreeView()
451 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
452 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
454 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
456 self.pannableArea.add(self.treeview)
460 def refreshList(self, selected=None, offset=0):
461 #rect = self.treeview.get_visible_rect()
462 #y = rect.y+rect.height
463 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
464 if self.isEditingCategories:
465 for key in self.listing.getListOfCategories():
466 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
470 for key in self.listing.getListOfFeeds(category=self.category):
471 item = self.treestore.append([self.listing.getFeedTitle(key), key])
474 self.treeview.set_model(self.treestore)
475 if not selected == None:
476 self.treeview.get_selection().select_iter(selectedItem)
477 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
478 self.pannableArea.show_all()
480 def getSelectedItem(self):
481 (model, iter) = self.treeview.get_selection().get_selected()
484 return model.get_value(iter, 1)
486 def findIndex(self, key):
490 for row in self.treestore:
492 return (before, row.iter)
493 if key == list(row)[0]:
497 return (before, None)
499 def buttonUp(self, button):
500 key = self.getSelectedItem()
502 if self.isEditingCategories:
503 self.listing.moveCategoryUp(key)
505 self.listing.moveUp(key)
506 self.refreshList(key, -10)
508 def buttonDown(self, button):
509 key = self.getSelectedItem()
511 if self.isEditingCategories:
512 self.listing.moveCategoryDown(key)
514 self.listing.moveDown(key)
515 self.refreshList(key, 10)
517 def buttonDelete(self, button):
518 key = self.getSelectedItem()
520 message = 'Really remove this feed and its entries?'
521 dlg = hildon.hildon_note_new_confirmation(self, message)
524 if response == gtk.RESPONSE_OK:
525 if self.isEditingCategories:
526 self.listing.removeCategory(key)
528 self.listing.removeFeed(key)
531 def buttonEdit(self, button):
532 key = self.getSelectedItem()
534 if key == 'ArchivedArticles':
535 message = 'Cannot edit the archived articles feed.'
536 hildon.hildon_banner_show_information(self, '', message)
538 if self.isEditingCategories:
540 SortList(self.parent, self.listing, self.feedingit, None, category=key)
543 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
546 (title, url, category) = wizard.getData()
548 self.listing.editFeed(key, title, url, category=category)
552 def buttonDone(self, *args):
555 def buttonAdd(self, button, urlIn="http://"):
556 if self.isEditingCategories:
557 wizard = AddCategoryWizard(self)
560 title = wizard.getData()
561 if (not title == ''):
562 self.listing.addCategory(title)
564 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
567 (title, url, category) = wizard.getData()
569 self.listing.addFeed(title, url, category=category)
574 class DisplayArticle(hildon.StackableWindow):
575 def __init__(self, feed, id, key, config, listing):
576 hildon.StackableWindow.__init__(self)
577 #self.imageDownloader = ImageDownloader()
582 #self.set_title(feed.getTitle(id))
583 self.set_title(self.listing.getFeedTitle(key))
585 self.set_for_removal = False
587 # Init the article display
588 #if self.config.getWebkitSupport():
589 self.view = WebView()
590 #self.view.set_editable(False)
593 # self.view = gtkhtml2.View()
594 # self.document = gtkhtml2.Document()
595 # self.view.set_document(self.document)
596 # self.document.connect("link_clicked", self._signal_link_clicked)
597 self.pannable_article = hildon.PannableArea()
598 self.pannable_article.add(self.view)
599 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
600 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
602 #if self.config.getWebkitSupport():
603 contentLink = self.feed.getContentLink(self.id)
604 self.feed.setEntryRead(self.id)
605 #if key=="ArchivedArticles":
606 self.loadedArticle = False
607 if contentLink.startswith("/home/user/"):
608 self.view.open("file://%s" % contentLink)
609 self.currentUrl = self.feed.getExternalLink(self.id)
611 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
612 self.currentUrl = "%s" % contentLink
613 self.view.connect("motion-notify-event", lambda w,ev: True)
614 self.view.connect('load-started', self.load_started)
615 self.view.connect('load-finished', self.load_finished)
617 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
619 menu = hildon.AppMenu()
620 # Create a button and add it to the menu
621 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
622 button.set_label("Allow horizontal scrolling")
623 button.connect("clicked", self.horiz_scrolling_button)
626 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
627 button.set_label("Open in browser")
628 button.connect("clicked", self.open_in_browser)
631 if key == "ArchivedArticles":
632 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
633 button.set_label("Remove from archived articles")
634 button.connect("clicked", self.remove_archive_button)
636 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
637 button.set_label("Add to archived articles")
638 button.connect("clicked", self.archive_button)
641 self.set_app_menu(menu)
644 self.add(self.pannable_article)
646 self.pannable_article.show_all()
648 self.destroyId = self.connect("destroy", self.destroyWindow)
650 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
651 ## Still using an old version of WebKit, so using navigation-requested signal
652 self.view.connect('navigation-requested', self.navigation_requested)
654 self.view.connect("button_press_event", self.button_pressed)
655 self.gestureId = self.view.connect("button_release_event", self.button_released)
657 #def navigation_policy_decision(self, wv, fr, req, action, decision):
658 def navigation_requested(self, wv, fr, req):
659 if self.config.getOpenInExternalBrowser():
660 self.open_in_browser(None, req.get_uri())
665 def load_started(self, *widget):
666 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
668 def load_finished(self, *widget):
669 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
670 frame = self.view.get_main_frame()
671 if self.loadedArticle:
672 self.currentUrl = frame.get_uri()
674 self.loadedArticle = True
676 def button_pressed(self, window, event):
677 #print event.x, event.y
678 self.coords = (event.x, event.y)
680 def button_released(self, window, event):
681 x = self.coords[0] - event.x
682 y = self.coords[1] - event.y
684 if (2*abs(y) < abs(x)):
686 self.emit("article-previous", self.id)
688 self.emit("article-next", self.id)
690 def destroyWindow(self, *args):
691 self.disconnect(self.destroyId)
692 if self.set_for_removal:
693 self.emit("article-deleted", self.id)
695 self.emit("article-closed", self.id)
696 #self.imageDownloader.stopAll()
699 def horiz_scrolling_button(self, *widget):
700 self.pannable_article.disconnect(self.gestureId)
701 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
703 def archive_button(self, *widget):
704 # Call the listing.addArchivedArticle
705 self.listing.addArchivedArticle(self.key, self.id)
707 def remove_archive_button(self, *widget):
708 self.set_for_removal = True
710 def open_in_browser(self, object, link=None):
712 bus = dbus.SessionBus()
713 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
714 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
716 iface.open_new_window(self.currentUrl)
718 iface.open_new_window(link)
720 class DisplayFeed(hildon.StackableWindow):
721 def __init__(self, listing, feed, title, key, config):
722 hildon.StackableWindow.__init__(self)
723 self.listing = listing
725 self.feedTitle = title
726 self.set_title(title)
728 self.current = list()
731 self.downloadDialog = False
733 #self.listing.setCurrentlyDisplayedFeed(self.key)
737 menu = hildon.AppMenu()
738 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
739 button.set_label("Update feed")
740 button.connect("clicked", self.button_update_clicked)
743 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
744 button.set_label("Mark all as read")
745 button.connect("clicked", self.buttonReadAllClicked)
748 if key=="ArchivedArticles":
749 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
750 button.set_label("Delete read articles")
751 button.connect("clicked", self.buttonPurgeArticles)
754 self.set_app_menu(menu)
757 self.main_vbox = gtk.VBox(False, 0)
758 self.add(self.main_vbox)
760 self.pannableFeed = None
763 if DownloadBar.downloading ():
764 self.show_download_bar ()
766 self.connect('configure-event', self.on_configure_event)
767 self.connect("destroy", self.destroyWindow)
769 def on_configure_event(self, window, event):
770 if getattr(self, 'markup_renderer', None) is None:
773 # Fix up the column width for wrapping the text when the window is
774 # resized (i.e. orientation changed)
775 self.markup_renderer.set_property('wrap-width', event.width-20)
776 it = self.feedItems.get_iter_first()
777 while it is not None:
778 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
779 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
780 it = self.feedItems.iter_next(it)
782 def destroyWindow(self, *args):
783 #self.feed.saveUnread(CONFIGDIR)
784 self.listing.updateUnread(self.key)
785 self.emit("feed-closed", self.key)
787 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
788 #self.listing.closeCurrentlyDisplayedFeed()
790 def fix_title(self, title):
791 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
793 def displayFeed(self):
794 if self.pannableFeed:
795 self.pannableFeed.destroy()
797 self.pannableFeed = hildon.PannableArea()
799 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
801 self.feedItems = gtk.ListStore(str, str)
802 #self.feedList = gtk.TreeView(self.feedItems)
803 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
804 self.feedList.set_rules_hint(True)
806 selection = self.feedList.get_selection()
807 selection.set_mode(gtk.SELECTION_NONE)
808 #selection.connect("changed", lambda w: True)
810 self.feedList.set_model(self.feedItems)
811 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
814 self.feedList.set_hover_selection(False)
815 #self.feedList.set_property('enable-grid-lines', True)
816 #self.feedList.set_property('hildon-mode', 1)
817 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
819 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
821 vbox= gtk.VBox(False, 10)
822 vbox.pack_start(self.feedList)
824 self.pannableFeed.add_with_viewport(vbox)
826 self.markup_renderer = gtk.CellRendererText()
827 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
828 self.markup_renderer.set_property('background', bg_color) #"#333333")
829 (width, height) = self.get_size()
830 self.markup_renderer.set_property('wrap-width', width-20)
831 self.markup_renderer.set_property('ypad', 8)
832 self.markup_renderer.set_property('xpad', 5)
833 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
834 markup=FEED_COLUMN_MARKUP)
835 self.feedList.append_column(markup_column)
837 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
838 hideReadArticles = self.config.getHideReadArticles()
840 articles = self.feed.getIds(onlyUnread=True)
842 articles = self.feed.getIds()
845 self.current = list()
849 isRead = self.feed.isEntryRead(id)
852 if not ( isRead and hideReadArticles ):
853 title = self.fix_title(self.feed.getTitle(id))
854 self.current.append(id)
856 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
858 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
860 self.feedItems.append((markup, id))
863 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
865 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
866 self.feedItems.append((markup, ""))
868 self.main_vbox.pack_start(self.pannableFeed)
872 self.pannableFeed.destroy()
873 #self.remove(self.pannableFeed)
875 def on_feedList_row_activated(self, treeview, path): #, column):
876 selection = self.feedList.get_selection()
877 selection.set_mode(gtk.SELECTION_SINGLE)
878 self.feedList.get_selection().select_path(path)
879 model = treeview.get_model()
880 iter = model.get_iter(path)
881 key = model.get_value(iter, FEED_COLUMN_KEY)
882 # Emulate legacy "button_clicked" call via treeview
883 gobject.idle_add(self.button_clicked, treeview, key)
886 def button_clicked(self, button, index, previous=False, next=False):
887 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
888 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
889 stack = hildon.WindowStack.get_default()
892 stack.pop_and_push(1, newDisp, tmp)
894 gobject.timeout_add(200, self.destroyArticle, tmp)
899 if type(self.disp).__name__ == "DisplayArticle":
900 gobject.timeout_add(200, self.destroyArticle, self.disp)
907 if self.key == "ArchivedArticles":
908 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
909 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
910 self.ids.append(self.disp.connect("article-next", self.nextArticle))
911 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
913 def buttonPurgeArticles(self, *widget):
915 self.feed.purgeReadArticles()
916 #self.feed.saveFeed(CONFIGDIR)
919 def destroyArticle(self, handle):
920 handle.destroyWindow()
922 def mark_item_read(self, key):
923 it = self.feedItems.get_iter_first()
924 while it is not None:
925 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
927 title = self.fix_title(self.feed.getTitle(key))
928 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
929 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
931 it = self.feedItems.iter_next(it)
933 def nextArticle(self, object, index):
934 self.mark_item_read(index)
935 id = self.feed.getNextId(index)
936 while id not in self.current and id != index:
937 id = self.feed.getNextId(id)
939 self.button_clicked(object, id, next=True)
941 def previousArticle(self, object, index):
942 self.mark_item_read(index)
943 id = self.feed.getPreviousId(index)
944 while id not in self.current and id != index:
945 id = self.feed.getPreviousId(id)
947 self.button_clicked(object, id, previous=True)
949 def onArticleClosed(self, object, index):
950 selection = self.feedList.get_selection()
951 selection.set_mode(gtk.SELECTION_NONE)
952 self.mark_item_read(index)
954 def onArticleDeleted(self, object, index):
956 self.feed.removeArticle(index)
957 #self.feed.saveFeed(CONFIGDIR)
961 def do_update_feed(self):
962 self.listing.updateFeed (self.key, priority=-1)
964 def button_update_clicked(self, button):
965 gobject.idle_add(self.do_update_feed)
967 def show_download_bar(self):
968 if not type(self.downloadDialog).__name__=="DownloadBar":
969 self.downloadDialog = DownloadBar(self.window)
970 self.downloadDialog.connect("download-done", self.onDownloadDone)
971 self.main_vbox.pack_end(self.downloadDialog,
972 expand=False, fill=False)
975 def onDownloadDone(self, widget, feed):
976 if feed == self.feed:
977 self.feed = self.listing.getFeed(self.key)
981 self.downloadDialog.destroy()
982 self.downloadDialog = False
984 def buttonReadAllClicked(self, button):
986 self.feed.markAllAsRead()
987 it = self.feedItems.get_iter_first()
988 while it is not None:
989 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
990 title = self.fix_title(self.feed.getTitle(k))
991 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
992 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
993 it = self.feedItems.iter_next(it)
995 #for index in self.feed.getIds():
996 # self.feed.setEntryRead(index)
997 # self.mark_item_read(index)
1003 self.window = hildon.StackableWindow()
1004 self.window.set_title(__appname__)
1005 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1006 self.mainVbox = gtk.VBox(False,10)
1008 if isfile(CONFIGDIR+"/feeds.db"):
1009 self.introLabel = gtk.Label("Loading...")
1011 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1013 self.mainVbox.pack_start(self.introLabel)
1015 self.window.add(self.mainVbox)
1016 self.window.show_all()
1017 self.config = Config(self.window, CONFIGDIR+"config.ini")
1018 gobject.idle_add(self.createWindow)
1020 def createWindow(self):
1022 self.listing = Listing(self.config, CONFIGDIR)
1024 self.downloadDialog = False
1026 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1027 self.orientation.set_mode(self.config.getOrientation())
1028 except Exception, e:
1029 logger.warn("Could not start rotation manager: %s" % str(e))
1031 menu = hildon.AppMenu()
1032 # Create a button and add it to the menu
1033 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1034 button.set_label("Update feeds")
1035 button.connect("clicked", self.button_update_clicked, "All")
1038 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1039 button.set_label("Mark all as read")
1040 button.connect("clicked", self.button_markAll)
1043 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1044 button.set_label("Add new feed")
1045 button.connect("clicked", lambda b: self.addFeed())
1048 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1049 button.set_label("Manage subscriptions")
1050 button.connect("clicked", self.button_organize_clicked)
1053 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1054 button.set_label("Settings")
1055 button.connect("clicked", self.button_preferences_clicked)
1058 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1059 button.set_label("About")
1060 button.connect("clicked", self.button_about_clicked)
1063 self.window.set_app_menu(menu)
1066 #self.feedWindow = hildon.StackableWindow()
1067 #self.articleWindow = hildon.StackableWindow()
1068 self.introLabel.destroy()
1069 self.pannableListing = hildon.PannableArea()
1070 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1071 self.feedList = gtk.TreeView(self.feedItems)
1072 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1073 #self.feedList.set_enable_tree_lines(True)
1074 #self.feedList.set_show_expanders(True)
1075 self.pannableListing.add(self.feedList)
1077 icon_renderer = gtk.CellRendererPixbuf()
1078 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1079 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1081 self.feedList.append_column(icon_column)
1083 markup_renderer = gtk.CellRendererText()
1084 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1085 markup=COLUMN_MARKUP)
1086 self.feedList.append_column(markup_column)
1087 self.mainVbox.pack_start(self.pannableListing)
1088 self.mainVbox.show_all()
1090 self.displayListing()
1091 self.autoupdate = False
1092 self.checkAutoUpdate()
1094 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1095 gobject.idle_add(self.late_init)
1097 def update_progress(self, percent_complete,
1098 completed, in_progress, queued,
1099 bytes_downloaded, bytes_updated, bytes_per_second,
1101 if (in_progress or queued) and not self.downloadDialog:
1102 self.downloadDialog = DownloadBar(self.window)
1103 self.downloadDialog.connect("download-done", self.onDownloadDone)
1104 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1105 self.mainVbox.show_all()
1107 if self.__dict__.get ('disp', None):
1108 self.disp.show_download_bar ()
1110 def onDownloadDone(self, widget, feed):
1112 self.downloadDialog.destroy()
1113 self.downloadDialog = False
1114 self.displayListing()
1116 def late_init(self):
1117 self.dbusHandler = ServerObject(self)
1118 bus = dbus.SessionBus()
1119 bus.add_signal_receiver(handler_function=self.update_progress,
1121 signal_name='UpdateProgress',
1122 dbus_interface='org.marcoz.feedingit',
1123 path='/org/marcoz/feedingit/update')
1125 def button_markAll(self, button):
1126 for key in self.listing.getListOfFeeds():
1127 feed = self.listing.getFeed(key)
1128 feed.markAllAsRead()
1129 #for id in feed.getIds():
1130 # feed.setEntryRead(id)
1131 self.listing.updateUnread(key)
1132 self.displayListing()
1134 def button_about_clicked(self, button):
1135 HeAboutDialog.present(self.window, \
1145 def button_export_clicked(self, button):
1146 opml = ExportOpmlData(self.window, self.listing)
1148 def button_import_clicked(self, button):
1149 opml = GetOpmlData(self.window)
1150 feeds = opml.getData()
1151 for (title, url) in feeds:
1152 self.listing.addFeed(title, url)
1153 self.displayListing()
1155 def addFeed(self, urlIn="http://"):
1156 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1159 (title, url, category) = wizard.getData()
1161 self.listing.addFeed(title, url, category=category)
1163 self.displayListing()
1165 def button_organize_clicked(self, button):
1166 def after_closing():
1167 self.displayListing()
1168 SortList(self.window, self.listing, self, after_closing)
1170 def do_update_feeds(self):
1171 for k in self.listing.getListOfFeeds():
1172 self.listing.updateFeed (k)
1174 def button_update_clicked(self, button, key):
1175 gobject.idle_add(self.do_update_feeds)
1177 def onDownloadsDone(self, *widget):
1178 self.downloadDialog.destroy()
1179 self.downloadDialog = False
1180 self.displayListing()
1182 def button_preferences_clicked(self, button):
1183 dialog = self.config.createDialog()
1184 dialog.connect("destroy", self.prefsClosed)
1186 def show_confirmation_note(self, parent, title):
1187 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1189 retcode = gtk.Dialog.run(note)
1192 if retcode == gtk.RESPONSE_OK:
1197 def saveExpandedLines(self):
1198 self.expandedLines = []
1199 model = self.feedList.get_model()
1200 model.foreach(self.checkLine)
1202 def checkLine(self, model, path, iter, data = None):
1203 if self.feedList.row_expanded(path):
1204 self.expandedLines.append(path)
1206 def restoreExpandedLines(self):
1207 model = self.feedList.get_model()
1208 model.foreach(self.restoreLine)
1210 def restoreLine(self, model, path, iter, data = None):
1211 if path in self.expandedLines:
1212 self.feedList.expand_row(path, False)
1214 def displayListing(self):
1215 icon_theme = gtk.icon_theme_get_default()
1216 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1217 gtk.ICON_LOOKUP_USE_BUILTIN)
1219 self.saveExpandedLines()
1221 self.feedItems.clear()
1222 hideReadFeed = self.config.getHideReadFeeds()
1223 order = self.config.getFeedSortOrder()
1225 categories = self.listing.getListOfCategories()
1226 if len(categories) > 1:
1227 showCategories = True
1229 showCategories = False
1231 for categoryId in categories:
1233 title = self.listing.getCategoryTitle(categoryId)
1234 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1236 if showCategories and len(keys)>0:
1237 category = self.feedItems.append(None, (None, title, categoryId))
1238 #print "catID" + str(categoryId) + " " + str(self.category)
1239 if categoryId == self.category:
1241 expandedRow = category
1244 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1245 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1246 updateTime = self.listing.getFeedUpdateTime(key)
1248 updateTime = "Never"
1249 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1251 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1253 markup = FEED_TEMPLATE % (title, subtitle)
1256 icon_filename = self.listing.getFavicon(key)
1257 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1258 LIST_ICON_SIZE, LIST_ICON_SIZE)
1260 pixbuf = default_pixbuf
1263 self.feedItems.append(category, (pixbuf, markup, key))
1265 self.feedItems.append(None, (pixbuf, markup, key))
1268 self.restoreExpandedLines()
1271 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1275 def on_feedList_row_activated(self, treeview, path, column):
1276 model = treeview.get_model()
1277 iter = model.get_iter(path)
1278 key = model.get_value(iter, COLUMN_KEY)
1281 #print "Key: " + str(key)
1283 self.category = catId
1284 if treeview.row_expanded(path):
1285 treeview.collapse_row(path)
1287 # treeview.expand_row(path, True)
1288 #treeview.collapse_all()
1289 #treeview.expand_row(path, False)
1290 #for i in range(len(path)):
1291 # self.feedList.expand_row(path[:i+1], False)
1292 #self.show_confirmation_note(self.window, "Working")
1298 def openFeed(self, key):
1300 self.disp = DisplayFeed(
1301 self.listing, self.listing.getFeed(key),
1302 self.listing.getFeedTitle(key), key,
1304 self.disp.connect("feed-closed", self.onFeedClosed)
1306 def openArticle(self, key, id):
1309 self.disp.button_clicked(None, id)
1311 def onFeedClosed(self, object, key):
1312 self.displayListing()
1314 def quit(self, *args):
1319 self.window.connect("destroy", self.quit)
1322 def prefsClosed(self, *widget):
1324 self.orientation.set_mode(self.config.getOrientation())
1327 self.displayListing()
1328 self.checkAutoUpdate()
1330 def checkAutoUpdate(self, *widget):
1331 interval = int(self.config.getUpdateInterval()*3600000)
1332 if self.config.isAutoUpdateEnabled():
1333 if self.autoupdate == False:
1334 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1335 self.autoupdate = interval
1336 elif not self.autoupdate == interval:
1337 # If auto-update is enabled, but not at the right frequency
1338 gobject.source_remove(self.autoupdateId)
1339 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1340 self.autoupdate = interval
1342 if not self.autoupdate == False:
1343 gobject.source_remove(self.autoupdateId)
1344 self.autoupdate = False
1346 def automaticUpdate(self, *widget):
1347 # Need to check for internet connection
1348 # If no internet connection, try again in 10 minutes:
1349 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1350 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1351 #from time import localtime, strftime
1352 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1354 self.button_update_clicked(None, None)
1357 def getStatus(self):
1359 for key in self.listing.getListOfFeeds():
1360 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1361 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1363 status = "No unread items"
1366 if __name__ == "__main__":
1368 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1370 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1371 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1372 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1373 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1374 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1375 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1376 gobject.threads_init()
1377 if not isdir(CONFIGDIR):
1381 logger.error("Error: Can't create configuration directory")
1382 from sys import exit