1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # ============================================================================
20 __appname__ = 'FeedingIt'
21 __author__ = 'Yves Marcoz'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
27 from pango import FontDescription
32 from webkit import WebView
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from threading import Thread, activeCount
43 from feedingitdbus import ServerObject
44 from updatedbus import UpdateServerObject, get_lock
45 from config import Config
46 from cgi import escape
48 from rss_sqlite import Listing
49 from opml import GetOpmlData, ExportOpmlData
51 from urllib2 import install_opener, build_opener
53 from socket import setdefaulttimeout
55 setdefaulttimeout(timeout)
63 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
64 ABOUT_ICON = 'feedingit'
65 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
66 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
67 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
68 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
70 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
71 unread_color = color_style.lookup_color('ActiveTextColor')
72 read_color = color_style.lookup_color('DefaultTextColor')
75 CONFIGDIR="/home/user/.feedingit/"
76 LOCK = CONFIGDIR + "update.lock"
79 from htmlentitydefs import name2codepoint
81 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
83 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
87 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
88 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
89 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
91 # Build the markup template for the Maemo 5 text style
92 head_font = style.get_font_desc('SystemFont')
93 sub_font = style.get_font_desc('SmallSystemFont')
95 #head_color = style.get_color('ButtonTextColor')
96 head_color = style.get_color('DefaultTextColor')
97 sub_color = style.get_color('DefaultTextColor')
98 active_color = style.get_color('ActiveTextColor')
100 bg_color = style.get_color('DefaultBackgroundColor').to_string()
101 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
102 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
103 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
104 bg_color = "#" + c1 + c2 + c3
107 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
108 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
110 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
111 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
113 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
114 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
116 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
117 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
119 FEED_TEMPLATE = '\n'.join((head, normal_sub))
120 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
122 ENTRY_TEMPLATE = entry_head
123 ENTRY_TEMPLATE_UNREAD = entry_active_head
126 # Removes HTML or XML character references and entities from a text string.
128 # @param text The HTML (or XML) source text.
129 # @return The plain text, as a Unicode string, if necessary.
130 # http://effbot.org/zone/re-sub.htm#unescape-html
135 # character reference
137 if text[:3] == "&#x":
138 return unichr(int(text[3:-1], 16))
140 return unichr(int(text[2:-1]))
146 text = unichr(name2codepoint[text[1:-1]])
149 return text # leave as is
150 return sub("&#?\w+;", fixup, text)
153 class AddWidgetWizard(gtk.Dialog):
154 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
155 gtk.Dialog.__init__(self)
156 self.set_transient_for(parent)
158 #self.category = categories[0]
159 self.category = currentCat
162 self.set_title('Edit RSS feed')
164 self.set_title('Add new RSS feed')
167 self.btn_add = self.add_button('Save', 2)
169 self.btn_add = self.add_button('Add', 2)
171 self.set_default_response(2)
173 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
174 self.nameEntry.set_placeholder('Feed name')
175 if not titleIn == None:
176 self.nameEntry.set_text(titleIn)
177 self.nameEntry.select_region(-1, -1)
179 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
180 self.urlEntry.set_placeholder('Feed URL')
181 self.urlEntry.set_text(urlIn)
182 self.urlEntry.select_region(-1, -1)
183 self.urlEntry.set_activates_default(True)
185 self.table = gtk.Table(3, 2, False)
186 self.table.set_col_spacings(5)
187 label = gtk.Label('Name:')
188 label.set_alignment(1., .5)
189 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
190 self.table.attach(self.nameEntry, 1, 2, 0, 1)
191 label = gtk.Label('URL:')
192 label.set_alignment(1., .5)
193 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
194 self.table.attach(self.urlEntry, 1, 2, 1, 2)
195 selector = self.create_selector(categories, listing)
196 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
197 picker.set_selector(selector)
198 picker.set_title("Select category")
199 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
200 picker.set_name('HildonButton-finger')
201 picker.set_alignment(0,0,1,1)
203 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
205 self.vbox.pack_start(self.table)
210 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
212 def create_selector(self, choices, listing):
213 #self.pickerDialog = hildon.PickerDialog(self.parent)
214 selector = hildon.TouchSelector(text=True)
218 title = listing.getCategoryTitle(item)
219 iter = selector.append_text(str(title))
220 if self.category == item:
221 selector.set_active(0, index)
222 self.map[title] = item
224 selector.connect("changed", self.selection_changed)
225 #self.pickerDialog.set_selector(selector)
228 def selection_changed(self, selector, button):
229 current_selection = selector.get_current_text()
230 if current_selection:
231 self.category = self.map[current_selection]
233 class AddCategoryWizard(gtk.Dialog):
234 def __init__(self, parent, titleIn=None, isEdit=False):
235 gtk.Dialog.__init__(self)
236 self.set_transient_for(parent)
239 self.set_title('Edit Category')
241 self.set_title('Add Category')
244 self.btn_add = self.add_button('Save', 2)
246 self.btn_add = self.add_button('Add', 2)
248 self.set_default_response(2)
250 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
251 self.nameEntry.set_placeholder('Category name')
252 if not titleIn == None:
253 self.nameEntry.set_text(titleIn)
254 self.nameEntry.select_region(-1, -1)
256 self.table = gtk.Table(1, 2, False)
257 self.table.set_col_spacings(5)
258 label = gtk.Label('Name:')
259 label.set_alignment(1., .5)
260 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
261 self.table.attach(self.nameEntry, 1, 2, 0, 1)
262 #label = gtk.Label('URL:')
263 #label.set_alignment(1., .5)
264 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
265 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
266 self.vbox.pack_start(self.table)
271 return self.nameEntry.get_text()
273 class Download(Thread):
274 def __init__(self, listing, key, config):
275 Thread.__init__(self)
276 self.listing = listing
281 (use_proxy, proxy) = self.config.getProxy()
282 key_lock = get_lock(self.key)
285 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
287 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
291 class DownloadBar(gtk.ProgressBar):
292 def __init__(self, parent, listing, listOfKeys, config, single=False):
294 update_lock = get_lock("update_lock")
295 if update_lock != None:
296 gtk.ProgressBar.__init__(self)
297 self.listOfKeys = listOfKeys[:]
298 self.listing = listing
299 self.total = len(self.listOfKeys)
303 (use_proxy, proxy) = self.config.getProxy()
305 opener = build_opener(proxy)
307 opener = build_opener()
309 opener.addheaders = [('User-agent', USER_AGENT)]
310 install_opener(opener)
313 # In preparation for i18n/l10n
315 return (a if n == 1 else b)
317 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
320 self.set_fraction(self.fraction)
323 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
325 def update_progress_bar(self):
326 #self.progress_bar.pulse()
327 if activeCount() < 4:
328 x = activeCount() - 1
329 k = len(self.listOfKeys)
330 fin = self.total - k - x
331 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
332 #print x, k, fin, fraction
333 self.set_fraction(fraction)
335 if len(self.listOfKeys)>0:
336 self.current = self.current+1
337 key = self.listOfKeys.pop()
338 #if self.single == True:
339 # Check if the feed is being displayed
340 download = Download(self.listing, key, self.config)
343 elif activeCount() > 1:
346 #self.waitingWindow.destroy()
352 self.emit("download-done", "success")
357 class SortList(hildon.StackableWindow):
358 def __init__(self, parent, listing, feedingit, after_closing, category=None):
359 hildon.StackableWindow.__init__(self)
360 self.set_transient_for(parent)
362 self.isEditingCategories = False
363 self.category = category
364 self.set_title(listing.getCategoryTitle(category))
366 self.isEditingCategories = True
367 self.set_title('Categories')
368 self.listing = listing
369 self.feedingit = feedingit
370 self.after_closing = after_closing
372 self.connect('destroy', lambda w: self.after_closing())
373 self.vbox2 = gtk.VBox(False, 2)
375 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
376 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
377 button.connect("clicked", self.buttonUp)
378 self.vbox2.pack_start(button, expand=False, fill=False)
380 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
381 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
382 button.connect("clicked", self.buttonDown)
383 self.vbox2.pack_start(button, expand=False, fill=False)
385 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
387 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
388 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
389 button.connect("clicked", self.buttonAdd)
390 self.vbox2.pack_start(button, expand=False, fill=False)
392 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
393 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
394 button.connect("clicked", self.buttonEdit)
395 self.vbox2.pack_start(button, expand=False, fill=False)
397 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
398 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
399 button.connect("clicked", self.buttonDelete)
400 self.vbox2.pack_start(button, expand=False, fill=False)
402 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
403 #button.set_label("Done")
404 #button.connect("clicked", self.buttonDone)
405 #self.vbox.pack_start(button)
406 self.hbox2= gtk.HBox(False, 10)
407 self.pannableArea = hildon.PannableArea()
408 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
409 self.treeview = gtk.TreeView(self.treestore)
410 self.hbox2.pack_start(self.pannableArea, expand=True)
412 self.hbox2.pack_end(self.vbox2, expand=False)
413 self.set_default_size(-1, 600)
416 menu = hildon.AppMenu()
417 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
418 button.set_label("Import from OPML")
419 button.connect("clicked", self.feedingit.button_import_clicked)
422 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
423 button.set_label("Export to OPML")
424 button.connect("clicked", self.feedingit.button_export_clicked)
426 self.set_app_menu(menu)
430 #self.connect("destroy", self.buttonDone)
432 def displayFeeds(self):
433 self.treeview.destroy()
434 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
435 self.treeview = gtk.TreeView()
437 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
438 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
440 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
442 self.pannableArea.add(self.treeview)
446 def refreshList(self, selected=None, offset=0):
447 #rect = self.treeview.get_visible_rect()
448 #y = rect.y+rect.height
449 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
450 if self.isEditingCategories:
451 for key in self.listing.getListOfCategories():
452 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
456 for key in self.listing.getListOfFeeds(category=self.category):
457 item = self.treestore.append([self.listing.getFeedTitle(key), key])
460 self.treeview.set_model(self.treestore)
461 if not selected == None:
462 self.treeview.get_selection().select_iter(selectedItem)
463 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
464 self.pannableArea.show_all()
466 def getSelectedItem(self):
467 (model, iter) = self.treeview.get_selection().get_selected()
470 return model.get_value(iter, 1)
472 def findIndex(self, key):
476 for row in self.treestore:
478 return (before, row.iter)
479 if key == list(row)[0]:
483 return (before, None)
485 def buttonUp(self, button):
486 key = self.getSelectedItem()
488 if self.isEditingCategories:
489 self.listing.moveCategoryUp(key)
491 self.listing.moveUp(key)
492 self.refreshList(key, -10)
494 def buttonDown(self, button):
495 key = self.getSelectedItem()
497 if self.isEditingCategories:
498 self.listing.moveCategoryDown(key)
500 self.listing.moveDown(key)
501 self.refreshList(key, 10)
503 def buttonDelete(self, button):
504 key = self.getSelectedItem()
506 message = 'Really remove this feed and its entries?'
507 dlg = hildon.hildon_note_new_confirmation(self, message)
510 if response == gtk.RESPONSE_OK:
511 if self.isEditingCategories:
512 self.listing.removeCategory(key)
514 self.listing.removeFeed(key)
517 def buttonEdit(self, button):
518 key = self.getSelectedItem()
520 if key == 'ArchivedArticles':
521 message = 'Cannot edit the archived articles feed.'
522 hildon.hildon_banner_show_information(self, '', message)
524 if self.isEditingCategories:
526 SortList(self.parent, self.listing, self.feedingit, None, category=key)
529 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
532 (title, url, category) = wizard.getData()
533 if (not title == '') and (not url == ''):
534 self.listing.editFeed(key, title, url, category=category)
538 def buttonDone(self, *args):
541 def buttonAdd(self, button, urlIn="http://"):
542 if self.isEditingCategories:
543 wizard = AddCategoryWizard(self)
546 title = wizard.getData()
547 if (not title == ''):
548 self.listing.addCategory(title)
550 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
553 (title, url, category) = wizard.getData()
554 if (not title == '') and (not url == ''):
555 self.listing.addFeed(title, url, category=category)
560 class DisplayArticle(hildon.StackableWindow):
561 def __init__(self, feed, id, key, config, listing):
562 hildon.StackableWindow.__init__(self)
563 #self.imageDownloader = ImageDownloader()
568 #self.set_title(feed.getTitle(id))
569 self.set_title(self.listing.getFeedTitle(key))
571 self.set_for_removal = False
573 # Init the article display
574 #if self.config.getWebkitSupport():
575 self.view = WebView()
576 #self.view.set_editable(False)
579 # self.view = gtkhtml2.View()
580 # self.document = gtkhtml2.Document()
581 # self.view.set_document(self.document)
582 # self.document.connect("link_clicked", self._signal_link_clicked)
583 self.pannable_article = hildon.PannableArea()
584 self.pannable_article.add(self.view)
585 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
586 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
588 #if self.config.getWebkitSupport():
589 contentLink = self.feed.getContentLink(self.id)
590 self.feed.setEntryRead(self.id)
591 #if key=="ArchivedArticles":
592 self.loadedArticle = False
593 if contentLink.startswith("/home/user/"):
594 self.view.open("file://%s" % contentLink)
595 self.currentUrl = self.feed.getExternalLink(self.id)
597 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
598 self.currentUrl = "%s" % contentLink
599 self.view.connect("motion-notify-event", lambda w,ev: True)
600 self.view.connect('load-started', self.load_started)
601 self.view.connect('load-finished', self.load_finished)
603 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
605 menu = hildon.AppMenu()
606 # Create a button and add it to the menu
607 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
608 button.set_label("Allow horizontal scrolling")
609 button.connect("clicked", self.horiz_scrolling_button)
612 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
613 button.set_label("Open in browser")
614 button.connect("clicked", self.open_in_browser)
617 if key == "ArchivedArticles":
618 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
619 button.set_label("Remove from archived articles")
620 button.connect("clicked", self.remove_archive_button)
622 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
623 button.set_label("Add to archived articles")
624 button.connect("clicked", self.archive_button)
627 self.set_app_menu(menu)
630 self.add(self.pannable_article)
632 self.pannable_article.show_all()
634 self.destroyId = self.connect("destroy", self.destroyWindow)
636 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
637 ## Still using an old version of WebKit, so using navigation-requested signal
638 self.view.connect('navigation-requested', self.navigation_requested)
640 self.view.connect("button_press_event", self.button_pressed)
641 self.gestureId = self.view.connect("button_release_event", self.button_released)
643 #def navigation_policy_decision(self, wv, fr, req, action, decision):
644 def navigation_requested(self, wv, fr, req):
645 if self.config.getOpenInExternalBrowser():
646 self.open_in_browser(None, req.get_uri())
651 def load_started(self, *widget):
652 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
654 def load_finished(self, *widget):
655 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
656 frame = self.view.get_main_frame()
657 if self.loadedArticle:
658 self.currentUrl = frame.get_uri()
660 self.loadedArticle = True
662 def button_pressed(self, window, event):
663 #print event.x, event.y
664 self.coords = (event.x, event.y)
666 def button_released(self, window, event):
667 x = self.coords[0] - event.x
668 y = self.coords[1] - event.y
670 if (2*abs(y) < abs(x)):
672 self.emit("article-previous", self.id)
674 self.emit("article-next", self.id)
676 def destroyWindow(self, *args):
677 self.disconnect(self.destroyId)
678 if self.set_for_removal:
679 self.emit("article-deleted", self.id)
681 self.emit("article-closed", self.id)
682 #self.imageDownloader.stopAll()
685 def horiz_scrolling_button(self, *widget):
686 self.pannable_article.disconnect(self.gestureId)
687 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
689 def archive_button(self, *widget):
690 # Call the listing.addArchivedArticle
691 self.listing.addArchivedArticle(self.key, self.id)
693 def remove_archive_button(self, *widget):
694 self.set_for_removal = True
696 def open_in_browser(self, object, link=None):
698 bus = dbus.SessionBus()
699 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
700 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
702 iface.open_new_window(self.currentUrl)
704 iface.open_new_window(link)
706 class DisplayFeed(hildon.StackableWindow):
707 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
708 hildon.StackableWindow.__init__(self)
709 self.listing = listing
711 self.feedTitle = title
712 self.set_title(title)
714 self.current = list()
716 self.updateDbusHandler = updateDbusHandler
718 self.downloadDialog = False
720 #self.listing.setCurrentlyDisplayedFeed(self.key)
724 menu = hildon.AppMenu()
725 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
726 button.set_label("Update feed")
727 button.connect("clicked", self.button_update_clicked)
730 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
731 button.set_label("Mark all as read")
732 button.connect("clicked", self.buttonReadAllClicked)
735 if key=="ArchivedArticles":
736 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
737 button.set_label("Delete read articles")
738 button.connect("clicked", self.buttonPurgeArticles)
741 self.set_app_menu(menu)
746 self.connect('configure-event', self.on_configure_event)
747 self.connect("destroy", self.destroyWindow)
749 def on_configure_event(self, window, event):
750 if getattr(self, 'markup_renderer', None) is None:
753 # Fix up the column width for wrapping the text when the window is
754 # resized (i.e. orientation changed)
755 self.markup_renderer.set_property('wrap-width', event.width-20)
756 it = self.feedItems.get_iter_first()
757 while it is not None:
758 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
759 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
760 it = self.feedItems.iter_next(it)
762 def destroyWindow(self, *args):
763 #self.feed.saveUnread(CONFIGDIR)
764 self.listing.updateUnread(self.key)
765 self.emit("feed-closed", self.key)
767 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
768 #self.listing.closeCurrentlyDisplayedFeed()
770 def fix_title(self, title):
771 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
773 def displayFeed(self):
774 self.pannableFeed = hildon.PannableArea()
776 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
778 self.feedItems = gtk.ListStore(str, str)
779 #self.feedList = gtk.TreeView(self.feedItems)
780 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
781 self.feedList.set_rules_hint(True)
783 selection = self.feedList.get_selection()
784 selection.set_mode(gtk.SELECTION_NONE)
785 #selection.connect("changed", lambda w: True)
787 self.feedList.set_model(self.feedItems)
788 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
791 self.feedList.set_hover_selection(False)
792 #self.feedList.set_property('enable-grid-lines', True)
793 #self.feedList.set_property('hildon-mode', 1)
794 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
796 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
798 vbox= gtk.VBox(False, 10)
799 vbox.pack_start(self.feedList)
801 self.pannableFeed.add_with_viewport(vbox)
803 self.markup_renderer = gtk.CellRendererText()
804 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
805 self.markup_renderer.set_property('background', bg_color) #"#333333")
806 (width, height) = self.get_size()
807 self.markup_renderer.set_property('wrap-width', width-20)
808 self.markup_renderer.set_property('ypad', 8)
809 self.markup_renderer.set_property('xpad', 5)
810 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
811 markup=FEED_COLUMN_MARKUP)
812 self.feedList.append_column(markup_column)
814 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
815 hideReadArticles = self.config.getHideReadArticles()
817 articles = self.feed.getIds(onlyUnread=True)
819 articles = self.feed.getIds()
822 self.current = list()
826 isRead = self.feed.isEntryRead(id)
829 if not ( isRead and hideReadArticles ):
830 title = self.fix_title(self.feed.getTitle(id))
831 self.current.append(id)
833 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
835 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
837 self.feedItems.append((markup, id))
840 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
842 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
843 self.feedItems.append((markup, ""))
845 self.add(self.pannableFeed)
849 self.pannableFeed.destroy()
850 #self.remove(self.pannableFeed)
852 def on_feedList_row_activated(self, treeview, path): #, column):
853 selection = self.feedList.get_selection()
854 selection.set_mode(gtk.SELECTION_SINGLE)
855 self.feedList.get_selection().select_path(path)
856 model = treeview.get_model()
857 iter = model.get_iter(path)
858 key = model.get_value(iter, FEED_COLUMN_KEY)
859 # Emulate legacy "button_clicked" call via treeview
860 gobject.idle_add(self.button_clicked, treeview, key)
863 def button_clicked(self, button, index, previous=False, next=False):
864 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
865 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
866 stack = hildon.WindowStack.get_default()
869 stack.pop_and_push(1, newDisp, tmp)
871 gobject.timeout_add(200, self.destroyArticle, tmp)
876 if type(self.disp).__name__ == "DisplayArticle":
877 gobject.timeout_add(200, self.destroyArticle, self.disp)
884 if self.key == "ArchivedArticles":
885 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
886 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
887 self.ids.append(self.disp.connect("article-next", self.nextArticle))
888 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
890 def buttonPurgeArticles(self, *widget):
892 self.feed.purgeReadArticles()
893 #self.feed.saveFeed(CONFIGDIR)
896 def destroyArticle(self, handle):
897 handle.destroyWindow()
899 def mark_item_read(self, key):
900 it = self.feedItems.get_iter_first()
901 while it is not None:
902 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
904 title = self.fix_title(self.feed.getTitle(key))
905 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
906 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
908 it = self.feedItems.iter_next(it)
910 def nextArticle(self, object, index):
911 self.mark_item_read(index)
912 id = self.feed.getNextId(index)
913 while id not in self.current and id != index:
914 id = self.feed.getNextId(id)
916 self.button_clicked(object, id, next=True)
918 def previousArticle(self, object, index):
919 self.mark_item_read(index)
920 id = self.feed.getPreviousId(index)
921 while id not in self.current and id != index:
922 id = self.feed.getPreviousId(id)
924 self.button_clicked(object, id, previous=True)
926 def onArticleClosed(self, object, index):
927 selection = self.feedList.get_selection()
928 selection.set_mode(gtk.SELECTION_NONE)
929 self.mark_item_read(index)
931 def onArticleDeleted(self, object, index):
933 self.feed.removeArticle(index)
934 #self.feed.saveFeed(CONFIGDIR)
937 def button_update_clicked(self, button):
938 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
939 if not type(self.downloadDialog).__name__=="DownloadBar":
940 self.pannableFeed.destroy()
941 self.vbox = gtk.VBox(False, 10)
942 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
943 self.downloadDialog.connect("download-done", self.onDownloadsDone)
944 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
948 def onDownloadsDone(self, *widget):
950 self.feed = self.listing.getFeed(self.key)
952 self.updateDbusHandler.ArticleCountUpdated()
954 def buttonReadAllClicked(self, button):
956 self.feed.markAllAsRead()
957 it = self.feedItems.get_iter_first()
958 while it is not None:
959 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
960 title = self.fix_title(self.feed.getTitle(k))
961 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
962 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
963 it = self.feedItems.iter_next(it)
965 #for index in self.feed.getIds():
966 # self.feed.setEntryRead(index)
967 # self.mark_item_read(index)
973 self.window = hildon.StackableWindow()
974 self.window.set_title(__appname__)
975 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
976 self.mainVbox = gtk.VBox(False,10)
978 if isfile(CONFIGDIR+"/feeds.db"):
979 self.introLabel = gtk.Label("Loading...")
981 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
983 self.mainVbox.pack_start(self.introLabel)
985 self.window.add(self.mainVbox)
986 self.window.show_all()
987 self.config = Config(self.window, CONFIGDIR+"config.ini")
988 gobject.idle_add(self.createWindow)
990 def createWindow(self):
993 self.app_lock = get_lock("app_lock")
994 if self.app_lock == None:
996 self.stopButton.set_sensitive(True)
998 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
999 self.stopButton.set_text("Stop update","")
1000 self.stopButton.connect("clicked", self.stop_running_update)
1001 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1002 self.window.show_all()
1003 self.introLabel.set_label("Update in progress, please wait.")
1004 gobject.timeout_add_seconds(3, self.createWindow)
1007 self.stopButton.destroy()
1010 self.listing = Listing(CONFIGDIR)
1012 self.downloadDialog = False
1014 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1015 self.orientation.set_mode(self.config.getOrientation())
1017 print "Could not start rotation manager"
1019 menu = hildon.AppMenu()
1020 # Create a button and add it to the menu
1021 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1022 button.set_label("Update feeds")
1023 button.connect("clicked", self.button_update_clicked, "All")
1026 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1027 button.set_label("Mark all as read")
1028 button.connect("clicked", self.button_markAll)
1031 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1032 button.set_label("Add new feed")
1033 button.connect("clicked", lambda b: self.addFeed())
1036 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1037 button.set_label("Manage subscriptions")
1038 button.connect("clicked", self.button_organize_clicked)
1041 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1042 button.set_label("Settings")
1043 button.connect("clicked", self.button_preferences_clicked)
1046 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1047 button.set_label("About")
1048 button.connect("clicked", self.button_about_clicked)
1051 self.window.set_app_menu(menu)
1054 #self.feedWindow = hildon.StackableWindow()
1055 #self.articleWindow = hildon.StackableWindow()
1056 self.introLabel.destroy()
1057 self.pannableListing = hildon.PannableArea()
1058 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1059 self.feedList = gtk.TreeView(self.feedItems)
1060 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1061 #self.feedList.set_enable_tree_lines(True)
1062 #self.feedList.set_show_expanders(True)
1063 self.pannableListing.add(self.feedList)
1065 icon_renderer = gtk.CellRendererPixbuf()
1066 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1067 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1069 self.feedList.append_column(icon_column)
1071 markup_renderer = gtk.CellRendererText()
1072 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1073 markup=COLUMN_MARKUP)
1074 self.feedList.append_column(markup_column)
1075 self.mainVbox.pack_start(self.pannableListing)
1076 self.mainVbox.show_all()
1078 self.displayListing()
1079 self.autoupdate = False
1080 self.checkAutoUpdate()
1082 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1083 gobject.idle_add(self.enableDbus)
1085 def stop_running_update(self, button):
1086 self.stopButton.set_sensitive(False)
1088 bus=dbus.SessionBus()
1089 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1090 "/org/marcoz/feedingit/update" # Object's path
1092 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1095 def enableDbus(self):
1096 self.dbusHandler = ServerObject(self)
1097 self.updateDbusHandler = UpdateServerObject(self)
1099 def button_markAll(self, button):
1100 for key in self.listing.getListOfFeeds():
1101 feed = self.listing.getFeed(key)
1102 feed.markAllAsRead()
1103 #for id in feed.getIds():
1104 # feed.setEntryRead(id)
1105 self.listing.updateUnread(key)
1106 self.displayListing()
1108 def button_about_clicked(self, button):
1109 HeAboutDialog.present(self.window, \
1119 def button_export_clicked(self, button):
1120 opml = ExportOpmlData(self.window, self.listing)
1122 def button_import_clicked(self, button):
1123 opml = GetOpmlData(self.window)
1124 feeds = opml.getData()
1125 for (title, url) in feeds:
1126 self.listing.addFeed(title, url)
1127 self.displayListing()
1129 def addFeed(self, urlIn="http://"):
1130 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1133 (title, url, category) = wizard.getData()
1134 if (not title == '') and (not url == ''):
1135 self.listing.addFeed(title, url, category=category)
1137 self.displayListing()
1139 def button_organize_clicked(self, button):
1140 def after_closing():
1141 self.displayListing()
1142 SortList(self.window, self.listing, self, after_closing)
1144 def button_update_clicked(self, button, key):
1145 if not type(self.downloadDialog).__name__=="DownloadBar":
1146 self.updateDbusHandler.UpdateStarted()
1147 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1148 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1149 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1150 self.mainVbox.show_all()
1151 #self.displayListing()
1153 def onDownloadsDone(self, *widget):
1154 self.downloadDialog.destroy()
1155 self.downloadDialog = False
1156 self.displayListing()
1157 self.updateDbusHandler.UpdateFinished()
1158 self.updateDbusHandler.ArticleCountUpdated()
1160 def button_preferences_clicked(self, button):
1161 dialog = self.config.createDialog()
1162 dialog.connect("destroy", self.prefsClosed)
1164 def show_confirmation_note(self, parent, title):
1165 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1167 retcode = gtk.Dialog.run(note)
1170 if retcode == gtk.RESPONSE_OK:
1175 def saveExpandedLines(self):
1176 self.expandedLines = []
1177 model = self.feedList.get_model()
1178 model.foreach(self.checkLine)
1180 def checkLine(self, model, path, iter, data = None):
1181 if self.feedList.row_expanded(path):
1182 self.expandedLines.append(path)
1184 def restoreExpandedLines(self):
1185 model = self.feedList.get_model()
1186 model.foreach(self.restoreLine)
1188 def restoreLine(self, model, path, iter, data = None):
1189 if path in self.expandedLines:
1190 self.feedList.expand_row(path, False)
1192 def displayListing(self):
1193 icon_theme = gtk.icon_theme_get_default()
1194 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1195 gtk.ICON_LOOKUP_USE_BUILTIN)
1197 self.saveExpandedLines()
1199 self.feedItems.clear()
1200 hideReadFeed = self.config.getHideReadFeeds()
1201 order = self.config.getFeedSortOrder()
1203 categories = self.listing.getListOfCategories()
1204 if len(categories) > 1:
1205 showCategories = True
1207 showCategories = False
1209 for categoryId in categories:
1211 title = self.listing.getCategoryTitle(categoryId)
1212 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1214 if showCategories and len(keys)>0:
1215 category = self.feedItems.append(None, (None, title, categoryId))
1216 #print "catID" + str(categoryId) + " " + str(self.category)
1217 if categoryId == self.category:
1219 expandedRow = category
1222 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1223 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1224 updateTime = self.listing.getFeedUpdateTime(key)
1226 updateTime = "Never"
1227 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1229 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1231 markup = FEED_TEMPLATE % (title, subtitle)
1234 icon_filename = self.listing.getFavicon(key)
1235 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1236 LIST_ICON_SIZE, LIST_ICON_SIZE)
1238 pixbuf = default_pixbuf
1241 self.feedItems.append(category, (pixbuf, markup, key))
1243 self.feedItems.append(None, (pixbuf, markup, key))
1246 self.restoreExpandedLines()
1249 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1253 def on_feedList_row_activated(self, treeview, path, column):
1254 model = treeview.get_model()
1255 iter = model.get_iter(path)
1256 key = model.get_value(iter, COLUMN_KEY)
1259 #print "Key: " + str(key)
1261 self.category = catId
1262 if treeview.row_expanded(path):
1263 treeview.collapse_row(path)
1265 # treeview.expand_row(path, True)
1266 #treeview.collapse_all()
1267 #treeview.expand_row(path, False)
1268 #for i in range(len(path)):
1269 # self.feedList.expand_row(path[:i+1], False)
1270 #self.show_confirmation_note(self.window, "Working")
1276 def openFeed(self, key):
1280 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1282 self.feed_lock = get_lock(key)
1283 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1284 self.listing.getFeedTitle(key), key, \
1285 self.config, self.updateDbusHandler)
1286 self.disp.connect("feed-closed", self.onFeedClosed)
1288 def openArticle(self, key, id):
1292 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1294 self.feed_lock = get_lock(key)
1295 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1296 self.listing.getFeedTitle(key), key, \
1297 self.config, self.updateDbusHandler)
1298 self.disp.button_clicked(None, id)
1299 self.disp.connect("feed-closed", self.onFeedClosed)
1302 def onFeedClosed(self, object, key):
1303 #self.listing.saveConfig()
1305 gobject.idle_add(self.onFeedClosedTimeout)
1306 self.displayListing()
1307 #self.updateDbusHandler.ArticleCountUpdated()
1309 def onFeedClosedTimeout(self):
1311 self.updateDbusHandler.ArticleCountUpdated()
1314 self.window.connect("destroy", gtk.main_quit)
1318 def prefsClosed(self, *widget):
1320 self.orientation.set_mode(self.config.getOrientation())
1323 self.displayListing()
1324 self.checkAutoUpdate()
1326 def checkAutoUpdate(self, *widget):
1327 interval = int(self.config.getUpdateInterval()*3600000)
1328 if self.config.isAutoUpdateEnabled():
1329 if self.autoupdate == False:
1330 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1331 self.autoupdate = interval
1332 elif not self.autoupdate == interval:
1333 # If auto-update is enabled, but not at the right frequency
1334 gobject.source_remove(self.autoupdateId)
1335 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1336 self.autoupdate = interval
1338 if not self.autoupdate == False:
1339 gobject.source_remove(self.autoupdateId)
1340 self.autoupdate = False
1342 def automaticUpdate(self, *widget):
1343 # Need to check for internet connection
1344 # If no internet connection, try again in 10 minutes:
1345 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1346 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1347 #from time import localtime, strftime
1348 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1350 self.button_update_clicked(None, None)
1353 def stopUpdate(self):
1354 # Not implemented in the app (see update_feeds.py)
1356 self.downloadDialog.listOfKeys = []
1360 def getStatus(self):
1362 for key in self.listing.getListOfFeeds():
1363 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1364 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1366 status = "No unread items"
1369 if __name__ == "__main__":
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 print "Error: Can't create configuration directory"
1382 from sys import exit