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 updatedbus import UpdateServerObject, get_lock
46 from config import Config
47 from cgi import escape
51 from rss_sqlite import Listing
52 from opml import GetOpmlData, ExportOpmlData
55 from jobmanager import JobManager
57 from socket import setdefaulttimeout
59 setdefaulttimeout(timeout)
67 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
68 ABOUT_ICON = 'feedingit'
69 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
70 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
71 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
72 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
74 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
75 unread_color = color_style.lookup_color('ActiveTextColor')
76 read_color = color_style.lookup_color('DefaultTextColor')
79 CONFIGDIR="/home/user/.feedingit/"
80 LOCK = CONFIGDIR + "update.lock"
83 from htmlentitydefs import name2codepoint
85 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
87 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
91 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
92 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
93 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
95 # Build the markup template for the Maemo 5 text style
96 head_font = style.get_font_desc('SystemFont')
97 sub_font = style.get_font_desc('SmallSystemFont')
99 #head_color = style.get_color('ButtonTextColor')
100 head_color = style.get_color('DefaultTextColor')
101 sub_color = style.get_color('DefaultTextColor')
102 active_color = style.get_color('ActiveTextColor')
104 bg_color = style.get_color('DefaultBackgroundColor').to_string()
105 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
106 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
107 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
108 bg_color = "#" + c1 + c2 + c3
111 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
112 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
114 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
115 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
117 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
118 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
120 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
121 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
123 FEED_TEMPLATE = '\n'.join((head, normal_sub))
124 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
126 ENTRY_TEMPLATE = entry_head
127 ENTRY_TEMPLATE_UNREAD = entry_active_head
130 # Removes HTML or XML character references and entities from a text string.
132 # @param text The HTML (or XML) source text.
133 # @return The plain text, as a Unicode string, if necessary.
134 # http://effbot.org/zone/re-sub.htm#unescape-html
139 # character reference
141 if text[:3] == "&#x":
142 return unichr(int(text[3:-1], 16))
144 return unichr(int(text[2:-1]))
150 text = unichr(name2codepoint[text[1:-1]])
153 return text # leave as is
154 return sub("&#?\w+;", fixup, text)
157 class AddWidgetWizard(gtk.Dialog):
158 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
159 gtk.Dialog.__init__(self)
160 self.set_transient_for(parent)
162 #self.category = categories[0]
163 self.category = currentCat
166 self.set_title('Edit RSS feed')
168 self.set_title('Add new RSS feed')
171 self.btn_add = self.add_button('Save', 2)
173 self.btn_add = self.add_button('Add', 2)
175 self.set_default_response(2)
177 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
178 self.nameEntry.set_placeholder('Feed name')
179 # If titleIn matches urlIn, there is no title.
180 if not titleIn == None and titleIn != urlIn:
181 self.nameEntry.set_text(titleIn)
182 self.nameEntry.select_region(-1, -1)
184 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
185 self.urlEntry.set_placeholder('Feed URL')
186 self.urlEntry.set_text(urlIn)
187 self.urlEntry.select_region(-1, -1)
188 self.urlEntry.set_activates_default(True)
190 self.table = gtk.Table(3, 2, False)
191 self.table.set_col_spacings(5)
192 label = gtk.Label('Name:')
193 label.set_alignment(1., .5)
194 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
195 self.table.attach(self.nameEntry, 1, 2, 0, 1)
196 label = gtk.Label('URL:')
197 label.set_alignment(1., .5)
198 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
199 self.table.attach(self.urlEntry, 1, 2, 1, 2)
200 selector = self.create_selector(categories, listing)
201 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
202 picker.set_selector(selector)
203 picker.set_title("Select category")
204 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
205 picker.set_name('HildonButton-finger')
206 picker.set_alignment(0,0,1,1)
208 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
210 self.vbox.pack_start(self.table)
215 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
217 def create_selector(self, choices, listing):
218 #self.pickerDialog = hildon.PickerDialog(self.parent)
219 selector = hildon.TouchSelector(text=True)
223 title = listing.getCategoryTitle(item)
224 iter = selector.append_text(str(title))
225 if self.category == item:
226 selector.set_active(0, index)
227 self.map[title] = item
229 selector.connect("changed", self.selection_changed)
230 #self.pickerDialog.set_selector(selector)
233 def selection_changed(self, selector, button):
234 current_selection = selector.get_current_text()
235 if current_selection:
236 self.category = self.map[current_selection]
238 class AddCategoryWizard(gtk.Dialog):
239 def __init__(self, parent, titleIn=None, isEdit=False):
240 gtk.Dialog.__init__(self)
241 self.set_transient_for(parent)
244 self.set_title('Edit Category')
246 self.set_title('Add Category')
249 self.btn_add = self.add_button('Save', 2)
251 self.btn_add = self.add_button('Add', 2)
253 self.set_default_response(2)
255 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
256 self.nameEntry.set_placeholder('Category name')
257 if not titleIn == None:
258 self.nameEntry.set_text(titleIn)
259 self.nameEntry.select_region(-1, -1)
261 self.table = gtk.Table(1, 2, False)
262 self.table.set_col_spacings(5)
263 label = gtk.Label('Name:')
264 label.set_alignment(1., .5)
265 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
266 self.table.attach(self.nameEntry, 1, 2, 0, 1)
267 #label = gtk.Label('URL:')
268 #label.set_alignment(1., .5)
269 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
270 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
271 self.vbox.pack_start(self.table)
276 return self.nameEntry.get_text()
278 class DownloadBar(gtk.ProgressBar):
281 if hasattr (cls, 'class_init_done'):
285 jm.stats_hook_register (cls.update_progress,
286 run_in_main_thread=True)
288 cls.downloadbars = []
289 # Total number of jobs we are monitoring.
291 # Number of jobs complete (of those that we are monitoring).
296 cls.class_init_done = True
298 def __init__(self, parent):
301 gtk.ProgressBar.__init__(self)
303 self.downloadbars.append(weakref.ref (self))
305 self.__class__.update_bars()
309 def downloading(cls):
310 return hasattr (cls, 'jobs_at_start')
313 def update_progress(cls, jm, old_stats, new_stats, updated_feed):
314 if not cls.downloading():
315 cls.jobs_at_start = old_stats['jobs-completed']
317 if not cls.downloadbars:
320 if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
321 del cls.jobs_at_start
322 for ref in cls.downloadbars:
325 # The download bar disappeared.
326 cls.downloadbars.remove (ref)
328 bar.emit("download-done", None)
331 # This should never be called if new_stats['jobs'] is 0, but
333 cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
334 cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
335 cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
336 + new_stats['jobs-queued']) / cls.total
340 for ref in cls.downloadbars:
343 # The download bar disappeared.
344 cls.downloadbars.remove (ref)
346 bar.emit("download-done", updated_feed)
349 def update_bars(cls):
350 # In preparation for i18n/l10n
352 return (a if n == 1 else b)
354 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
356 % (cls.done, cls.total))
358 for ref in cls.downloadbars:
361 # The download bar disappeared.
362 cls.downloadbars.remove (ref)
365 bar.set_fraction(cls.progress)
367 class SortList(hildon.StackableWindow):
368 def __init__(self, parent, listing, feedingit, after_closing, category=None):
369 hildon.StackableWindow.__init__(self)
370 self.set_transient_for(parent)
372 self.isEditingCategories = False
373 self.category = category
374 self.set_title(listing.getCategoryTitle(category))
376 self.isEditingCategories = True
377 self.set_title('Categories')
378 self.listing = listing
379 self.feedingit = feedingit
380 self.after_closing = after_closing
382 self.connect('destroy', lambda w: self.after_closing())
383 self.vbox2 = gtk.VBox(False, 2)
385 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
386 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
387 button.connect("clicked", self.buttonUp)
388 self.vbox2.pack_start(button, expand=False, fill=False)
390 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
391 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
392 button.connect("clicked", self.buttonDown)
393 self.vbox2.pack_start(button, expand=False, fill=False)
395 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
397 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
398 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
399 button.connect("clicked", self.buttonAdd)
400 self.vbox2.pack_start(button, expand=False, fill=False)
402 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
403 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
404 button.connect("clicked", self.buttonEdit)
405 self.vbox2.pack_start(button, expand=False, fill=False)
407 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
408 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
409 button.connect("clicked", self.buttonDelete)
410 self.vbox2.pack_start(button, expand=False, fill=False)
412 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
413 #button.set_label("Done")
414 #button.connect("clicked", self.buttonDone)
415 #self.vbox.pack_start(button)
416 self.hbox2= gtk.HBox(False, 10)
417 self.pannableArea = hildon.PannableArea()
418 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
419 self.treeview = gtk.TreeView(self.treestore)
420 self.hbox2.pack_start(self.pannableArea, expand=True)
422 self.hbox2.pack_end(self.vbox2, expand=False)
423 self.set_default_size(-1, 600)
426 menu = hildon.AppMenu()
427 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
428 button.set_label("Import from OPML")
429 button.connect("clicked", self.feedingit.button_import_clicked)
432 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
433 button.set_label("Export to OPML")
434 button.connect("clicked", self.feedingit.button_export_clicked)
436 self.set_app_menu(menu)
440 #self.connect("destroy", self.buttonDone)
442 def displayFeeds(self):
443 self.treeview.destroy()
444 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
445 self.treeview = gtk.TreeView()
447 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
448 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
450 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
452 self.pannableArea.add(self.treeview)
456 def refreshList(self, selected=None, offset=0):
457 #rect = self.treeview.get_visible_rect()
458 #y = rect.y+rect.height
459 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
460 if self.isEditingCategories:
461 for key in self.listing.getListOfCategories():
462 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
466 for key in self.listing.getListOfFeeds(category=self.category):
467 item = self.treestore.append([self.listing.getFeedTitle(key), key])
470 self.treeview.set_model(self.treestore)
471 if not selected == None:
472 self.treeview.get_selection().select_iter(selectedItem)
473 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
474 self.pannableArea.show_all()
476 def getSelectedItem(self):
477 (model, iter) = self.treeview.get_selection().get_selected()
480 return model.get_value(iter, 1)
482 def findIndex(self, key):
486 for row in self.treestore:
488 return (before, row.iter)
489 if key == list(row)[0]:
493 return (before, None)
495 def buttonUp(self, button):
496 key = self.getSelectedItem()
498 if self.isEditingCategories:
499 self.listing.moveCategoryUp(key)
501 self.listing.moveUp(key)
502 self.refreshList(key, -10)
504 def buttonDown(self, button):
505 key = self.getSelectedItem()
507 if self.isEditingCategories:
508 self.listing.moveCategoryDown(key)
510 self.listing.moveDown(key)
511 self.refreshList(key, 10)
513 def buttonDelete(self, button):
514 key = self.getSelectedItem()
516 message = 'Really remove this feed and its entries?'
517 dlg = hildon.hildon_note_new_confirmation(self, message)
520 if response == gtk.RESPONSE_OK:
521 if self.isEditingCategories:
522 self.listing.removeCategory(key)
524 self.listing.removeFeed(key)
527 def buttonEdit(self, button):
528 key = self.getSelectedItem()
530 if key == 'ArchivedArticles':
531 message = 'Cannot edit the archived articles feed.'
532 hildon.hildon_banner_show_information(self, '', message)
534 if self.isEditingCategories:
536 SortList(self.parent, self.listing, self.feedingit, None, category=key)
539 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
542 (title, url, category) = wizard.getData()
544 self.listing.editFeed(key, title, url, category=category)
548 def buttonDone(self, *args):
551 def buttonAdd(self, button, urlIn="http://"):
552 if self.isEditingCategories:
553 wizard = AddCategoryWizard(self)
556 title = wizard.getData()
557 if (not title == ''):
558 self.listing.addCategory(title)
560 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
563 (title, url, category) = wizard.getData()
565 self.listing.addFeed(title, url, category=category)
570 class DisplayArticle(hildon.StackableWindow):
571 def __init__(self, feed, id, key, config, listing):
572 hildon.StackableWindow.__init__(self)
573 #self.imageDownloader = ImageDownloader()
578 #self.set_title(feed.getTitle(id))
579 self.set_title(self.listing.getFeedTitle(key))
581 self.set_for_removal = False
583 # Init the article display
584 #if self.config.getWebkitSupport():
585 self.view = WebView()
586 #self.view.set_editable(False)
589 # self.view = gtkhtml2.View()
590 # self.document = gtkhtml2.Document()
591 # self.view.set_document(self.document)
592 # self.document.connect("link_clicked", self._signal_link_clicked)
593 self.pannable_article = hildon.PannableArea()
594 self.pannable_article.add(self.view)
595 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
596 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
598 #if self.config.getWebkitSupport():
599 contentLink = self.feed.getContentLink(self.id)
600 self.feed.setEntryRead(self.id)
601 #if key=="ArchivedArticles":
602 self.loadedArticle = False
603 if contentLink.startswith("/home/user/"):
604 self.view.open("file://%s" % contentLink)
605 self.currentUrl = self.feed.getExternalLink(self.id)
607 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
608 self.currentUrl = "%s" % contentLink
609 self.view.connect("motion-notify-event", lambda w,ev: True)
610 self.view.connect('load-started', self.load_started)
611 self.view.connect('load-finished', self.load_finished)
613 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
615 menu = hildon.AppMenu()
616 # Create a button and add it to the menu
617 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
618 button.set_label("Allow horizontal scrolling")
619 button.connect("clicked", self.horiz_scrolling_button)
622 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
623 button.set_label("Open in browser")
624 button.connect("clicked", self.open_in_browser)
627 if key == "ArchivedArticles":
628 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
629 button.set_label("Remove from archived articles")
630 button.connect("clicked", self.remove_archive_button)
632 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
633 button.set_label("Add to archived articles")
634 button.connect("clicked", self.archive_button)
637 self.set_app_menu(menu)
640 self.add(self.pannable_article)
642 self.pannable_article.show_all()
644 self.destroyId = self.connect("destroy", self.destroyWindow)
646 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
647 ## Still using an old version of WebKit, so using navigation-requested signal
648 self.view.connect('navigation-requested', self.navigation_requested)
650 self.view.connect("button_press_event", self.button_pressed)
651 self.gestureId = self.view.connect("button_release_event", self.button_released)
653 #def navigation_policy_decision(self, wv, fr, req, action, decision):
654 def navigation_requested(self, wv, fr, req):
655 if self.config.getOpenInExternalBrowser():
656 self.open_in_browser(None, req.get_uri())
661 def load_started(self, *widget):
662 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
664 def load_finished(self, *widget):
665 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
666 frame = self.view.get_main_frame()
667 if self.loadedArticle:
668 self.currentUrl = frame.get_uri()
670 self.loadedArticle = True
672 def button_pressed(self, window, event):
673 #print event.x, event.y
674 self.coords = (event.x, event.y)
676 def button_released(self, window, event):
677 x = self.coords[0] - event.x
678 y = self.coords[1] - event.y
680 if (2*abs(y) < abs(x)):
682 self.emit("article-previous", self.id)
684 self.emit("article-next", self.id)
686 def destroyWindow(self, *args):
687 self.disconnect(self.destroyId)
688 if self.set_for_removal:
689 self.emit("article-deleted", self.id)
691 self.emit("article-closed", self.id)
692 #self.imageDownloader.stopAll()
695 def horiz_scrolling_button(self, *widget):
696 self.pannable_article.disconnect(self.gestureId)
697 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
699 def archive_button(self, *widget):
700 # Call the listing.addArchivedArticle
701 self.listing.addArchivedArticle(self.key, self.id)
703 def remove_archive_button(self, *widget):
704 self.set_for_removal = True
706 def open_in_browser(self, object, link=None):
708 bus = dbus.SessionBus()
709 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
710 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
712 iface.open_new_window(self.currentUrl)
714 iface.open_new_window(link)
716 class DisplayFeed(hildon.StackableWindow):
717 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
718 hildon.StackableWindow.__init__(self)
719 self.listing = listing
721 self.feedTitle = title
722 self.set_title(title)
724 self.current = list()
726 self.updateDbusHandler = updateDbusHandler
728 self.downloadDialog = False
730 #self.listing.setCurrentlyDisplayedFeed(self.key)
734 menu = hildon.AppMenu()
735 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
736 button.set_label("Update feed")
737 button.connect("clicked", self.button_update_clicked)
740 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
741 button.set_label("Mark all as read")
742 button.connect("clicked", self.buttonReadAllClicked)
745 if key=="ArchivedArticles":
746 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
747 button.set_label("Delete read articles")
748 button.connect("clicked", self.buttonPurgeArticles)
751 self.set_app_menu(menu)
754 self.main_vbox = gtk.VBox(False, 0)
755 self.add(self.main_vbox)
757 self.pannableFeed = None
760 if DownloadBar.downloading ():
761 self.show_download_bar ()
763 self.connect('configure-event', self.on_configure_event)
764 self.connect("destroy", self.destroyWindow)
766 def on_configure_event(self, window, event):
767 if getattr(self, 'markup_renderer', None) is None:
770 # Fix up the column width for wrapping the text when the window is
771 # resized (i.e. orientation changed)
772 self.markup_renderer.set_property('wrap-width', event.width-20)
773 it = self.feedItems.get_iter_first()
774 while it is not None:
775 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
776 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
777 it = self.feedItems.iter_next(it)
779 def destroyWindow(self, *args):
780 #self.feed.saveUnread(CONFIGDIR)
781 self.listing.updateUnread(self.key)
782 self.emit("feed-closed", self.key)
784 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
785 #self.listing.closeCurrentlyDisplayedFeed()
787 def fix_title(self, title):
788 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
790 def displayFeed(self):
791 if self.pannableFeed:
792 self.pannableFeed.destroy()
794 self.pannableFeed = hildon.PannableArea()
796 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
798 self.feedItems = gtk.ListStore(str, str)
799 #self.feedList = gtk.TreeView(self.feedItems)
800 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
801 self.feedList.set_rules_hint(True)
803 selection = self.feedList.get_selection()
804 selection.set_mode(gtk.SELECTION_NONE)
805 #selection.connect("changed", lambda w: True)
807 self.feedList.set_model(self.feedItems)
808 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
811 self.feedList.set_hover_selection(False)
812 #self.feedList.set_property('enable-grid-lines', True)
813 #self.feedList.set_property('hildon-mode', 1)
814 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
816 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
818 vbox= gtk.VBox(False, 10)
819 vbox.pack_start(self.feedList)
821 self.pannableFeed.add_with_viewport(vbox)
823 self.markup_renderer = gtk.CellRendererText()
824 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
825 self.markup_renderer.set_property('background', bg_color) #"#333333")
826 (width, height) = self.get_size()
827 self.markup_renderer.set_property('wrap-width', width-20)
828 self.markup_renderer.set_property('ypad', 8)
829 self.markup_renderer.set_property('xpad', 5)
830 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
831 markup=FEED_COLUMN_MARKUP)
832 self.feedList.append_column(markup_column)
834 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
835 hideReadArticles = self.config.getHideReadArticles()
837 articles = self.feed.getIds(onlyUnread=True)
839 articles = self.feed.getIds()
842 self.current = list()
846 isRead = self.feed.isEntryRead(id)
849 if not ( isRead and hideReadArticles ):
850 title = self.fix_title(self.feed.getTitle(id))
851 self.current.append(id)
853 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
855 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
857 self.feedItems.append((markup, id))
860 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
862 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
863 self.feedItems.append((markup, ""))
865 self.main_vbox.pack_start(self.pannableFeed)
869 self.pannableFeed.destroy()
870 #self.remove(self.pannableFeed)
872 def on_feedList_row_activated(self, treeview, path): #, column):
873 selection = self.feedList.get_selection()
874 selection.set_mode(gtk.SELECTION_SINGLE)
875 self.feedList.get_selection().select_path(path)
876 model = treeview.get_model()
877 iter = model.get_iter(path)
878 key = model.get_value(iter, FEED_COLUMN_KEY)
879 # Emulate legacy "button_clicked" call via treeview
880 gobject.idle_add(self.button_clicked, treeview, key)
883 def button_clicked(self, button, index, previous=False, next=False):
884 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
885 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
886 stack = hildon.WindowStack.get_default()
889 stack.pop_and_push(1, newDisp, tmp)
891 gobject.timeout_add(200, self.destroyArticle, tmp)
896 if type(self.disp).__name__ == "DisplayArticle":
897 gobject.timeout_add(200, self.destroyArticle, self.disp)
904 if self.key == "ArchivedArticles":
905 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
906 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
907 self.ids.append(self.disp.connect("article-next", self.nextArticle))
908 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
910 def buttonPurgeArticles(self, *widget):
912 self.feed.purgeReadArticles()
913 #self.feed.saveFeed(CONFIGDIR)
916 def destroyArticle(self, handle):
917 handle.destroyWindow()
919 def mark_item_read(self, key):
920 it = self.feedItems.get_iter_first()
921 while it is not None:
922 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
924 title = self.fix_title(self.feed.getTitle(key))
925 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
926 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
928 it = self.feedItems.iter_next(it)
930 def nextArticle(self, object, index):
931 self.mark_item_read(index)
932 id = self.feed.getNextId(index)
933 while id not in self.current and id != index:
934 id = self.feed.getNextId(id)
936 self.button_clicked(object, id, next=True)
938 def previousArticle(self, object, index):
939 self.mark_item_read(index)
940 id = self.feed.getPreviousId(index)
941 while id not in self.current and id != index:
942 id = self.feed.getPreviousId(id)
944 self.button_clicked(object, id, previous=True)
946 def onArticleClosed(self, object, index):
947 selection = self.feedList.get_selection()
948 selection.set_mode(gtk.SELECTION_NONE)
949 self.mark_item_read(index)
951 def onArticleDeleted(self, object, index):
953 self.feed.removeArticle(index)
954 #self.feed.saveFeed(CONFIGDIR)
957 def button_update_clicked(self, button):
958 self.listing.updateFeed (self.key, priority=-1)
960 def show_download_bar(self):
961 if not type(self.downloadDialog).__name__=="DownloadBar":
962 self.downloadDialog = DownloadBar(self.window)
963 self.downloadDialog.connect("download-done", self.onDownloadDone)
964 self.main_vbox.pack_end(self.downloadDialog,
965 expand=False, fill=False)
968 def onDownloadDone(self, widget, feed):
969 if feed == self.feed or feed is None:
970 self.downloadDialog.destroy()
971 self.downloadDialog = False
972 self.feed = self.listing.getFeed(self.key)
974 self.updateDbusHandler.ArticleCountUpdated()
976 def buttonReadAllClicked(self, button):
978 self.feed.markAllAsRead()
979 it = self.feedItems.get_iter_first()
980 while it is not None:
981 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
982 title = self.fix_title(self.feed.getTitle(k))
983 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
984 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
985 it = self.feedItems.iter_next(it)
987 #for index in self.feed.getIds():
988 # self.feed.setEntryRead(index)
989 # self.mark_item_read(index)
995 self.window = hildon.StackableWindow()
996 self.window.set_title(__appname__)
997 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
998 self.mainVbox = gtk.VBox(False,10)
1000 if isfile(CONFIGDIR+"/feeds.db"):
1001 self.introLabel = gtk.Label("Loading...")
1003 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1005 self.mainVbox.pack_start(self.introLabel)
1007 self.window.add(self.mainVbox)
1008 self.window.show_all()
1009 self.config = Config(self.window, CONFIGDIR+"config.ini")
1010 gobject.idle_add(self.createWindow)
1012 # This is set to try when the user interacts with the program.
1013 # If, after an update is complete, we discover that the
1014 # environment variable DBUS_STARTED_ADDRESS is set and
1015 # self.had_interaction is False, we quit.
1016 self.had_interaction = False
1018 def createWindow(self):
1021 self.app_lock = get_lock("app_lock")
1022 if self.app_lock == None:
1024 self.stopButton.set_sensitive(True)
1026 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1027 self.stopButton.set_text("Stop update","")
1028 self.stopButton.connect("clicked", self.stop_running_update)
1029 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1030 self.window.show_all()
1031 self.introLabel.set_label("Update in progress, please wait.")
1032 gobject.timeout_add_seconds(3, self.createWindow)
1035 self.stopButton.destroy()
1038 self.listing = Listing(self.config, CONFIGDIR)
1040 self.downloadDialog = False
1042 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1043 self.orientation.set_mode(self.config.getOrientation())
1045 print "Could not start rotation manager"
1047 menu = hildon.AppMenu()
1048 # Create a button and add it to the menu
1049 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1050 button.set_label("Update feeds")
1051 button.connect("clicked", self.button_update_clicked, "All")
1054 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1055 button.set_label("Mark all as read")
1056 button.connect("clicked", self.button_markAll)
1059 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1060 button.set_label("Add new feed")
1061 button.connect("clicked", lambda b: self.addFeed())
1064 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1065 button.set_label("Manage subscriptions")
1066 button.connect("clicked", self.button_organize_clicked)
1069 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1070 button.set_label("Settings")
1071 button.connect("clicked", self.button_preferences_clicked)
1074 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1075 button.set_label("About")
1076 button.connect("clicked", self.button_about_clicked)
1079 self.window.set_app_menu(menu)
1082 #self.feedWindow = hildon.StackableWindow()
1083 #self.articleWindow = hildon.StackableWindow()
1084 self.introLabel.destroy()
1085 self.pannableListing = hildon.PannableArea()
1086 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1087 self.feedList = gtk.TreeView(self.feedItems)
1088 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1089 #self.feedList.set_enable_tree_lines(True)
1090 #self.feedList.set_show_expanders(True)
1091 self.pannableListing.add(self.feedList)
1093 icon_renderer = gtk.CellRendererPixbuf()
1094 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1095 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1097 self.feedList.append_column(icon_column)
1099 markup_renderer = gtk.CellRendererText()
1100 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1101 markup=COLUMN_MARKUP)
1102 self.feedList.append_column(markup_column)
1103 self.mainVbox.pack_start(self.pannableListing)
1104 self.mainVbox.show_all()
1106 self.displayListing()
1107 self.autoupdate = False
1108 self.checkAutoUpdate()
1110 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1111 gobject.idle_add(self.late_init)
1113 def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1114 if (not self.downloadDialog
1115 and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1116 self.updateDbusHandler.UpdateStarted()
1118 self.downloadDialog = DownloadBar(self.window)
1119 self.downloadDialog.connect("download-done", self.onDownloadDone)
1120 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1121 self.mainVbox.show_all()
1123 if self.__dict__.get ('disp', None):
1124 self.disp.show_download_bar ()
1126 def onDownloadDone(self, widget, feed):
1128 self.downloadDialog.destroy()
1129 self.downloadDialog = False
1130 self.displayListing()
1131 self.updateDbusHandler.UpdateFinished()
1132 self.updateDbusHandler.ArticleCountUpdated()
1134 if not self.had_interaction and 'DBUS_STARTER_ADDRESS' in environ:
1135 print "Update complete. No interaction, started by dbus: quitting."
1137 def stop_running_update(self, button):
1138 self.stopButton.set_sensitive(False)
1140 bus=dbus.SessionBus()
1141 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1142 "/org/marcoz/feedingit/update" # Object's path
1144 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1147 def increase_download_parallelism(self):
1148 # The system has been idle for a while. Enable parallel
1150 JobManager().num_threads = 4
1151 gobject.source_remove (self.increase_download_parallelism_id)
1152 del self.increase_download_parallelism_id
1155 def system_inactivity_ind(self, idle):
1156 # The system's idle state changed.
1157 if (self.am_idle and idle) or (not self.am_idle and not idle):
1162 if hasattr (self, 'increase_download_parallelism_id'):
1163 gobject.source_remove (self.increase_download_parallelism_id)
1164 del self.increase_download_parallelism_id
1166 self.increase_download_parallelism_id = \
1167 gobject.timeout_add_seconds(
1168 60, self.increase_download_parallelism)
1171 JobManager().num_threads = 1
1175 def late_init(self):
1176 self.dbusHandler = ServerObject(self)
1177 self.updateDbusHandler = UpdateServerObject(self)
1180 jm.stats_hook_register (self.job_manager_update,
1181 run_in_main_thread=True)
1183 self.am_idle = False
1187 bus = dbus.SystemBus()
1188 proxy = bus.get_object('com.nokia.mce',
1189 '/com/nokia/mce/signal')
1190 iface = dbus.Interface(proxy, 'com.nokia.mce.signal')
1191 iface.connect_to_signal('system_inactivity_ind',
1192 self.system_inactivity_ind)
1194 def button_markAll(self, button):
1195 self.had_interaction = True
1196 for key in self.listing.getListOfFeeds():
1197 feed = self.listing.getFeed(key)
1198 feed.markAllAsRead()
1199 #for id in feed.getIds():
1200 # feed.setEntryRead(id)
1201 self.listing.updateUnread(key)
1202 self.displayListing()
1204 def button_about_clicked(self, button):
1205 self.had_interaction = True
1206 HeAboutDialog.present(self.window, \
1216 def button_export_clicked(self, button):
1217 self.had_interaction = True
1218 opml = ExportOpmlData(self.window, self.listing)
1220 def button_import_clicked(self, button):
1221 self.had_interaction = True
1222 opml = GetOpmlData(self.window)
1223 feeds = opml.getData()
1224 for (title, url) in feeds:
1225 self.listing.addFeed(title, url)
1226 self.displayListing()
1228 def addFeed(self, urlIn="http://"):
1229 self.had_interaction = True
1230 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1233 (title, url, category) = wizard.getData()
1235 self.listing.addFeed(title, url, category=category)
1237 self.displayListing()
1239 def button_organize_clicked(self, button):
1240 self.had_interaction = True
1241 def after_closing():
1242 self.displayListing()
1243 SortList(self.window, self.listing, self, after_closing)
1245 def button_update_clicked(self, button, key):
1246 self.had_interaction = True
1247 for k in self.listing.getListOfFeeds():
1248 self.listing.updateFeed (k)
1249 #self.displayListing()
1251 def onDownloadsDone(self, *widget):
1252 self.downloadDialog.destroy()
1253 self.downloadDialog = False
1254 self.displayListing()
1255 self.updateDbusHandler.UpdateFinished()
1256 self.updateDbusHandler.ArticleCountUpdated()
1258 def button_preferences_clicked(self, button):
1259 self.had_interaction = True
1260 dialog = self.config.createDialog()
1261 dialog.connect("destroy", self.prefsClosed)
1263 def show_confirmation_note(self, parent, title):
1264 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1266 retcode = gtk.Dialog.run(note)
1269 if retcode == gtk.RESPONSE_OK:
1274 def saveExpandedLines(self):
1275 self.expandedLines = []
1276 model = self.feedList.get_model()
1277 model.foreach(self.checkLine)
1279 def checkLine(self, model, path, iter, data = None):
1280 if self.feedList.row_expanded(path):
1281 self.expandedLines.append(path)
1283 def restoreExpandedLines(self):
1284 model = self.feedList.get_model()
1285 model.foreach(self.restoreLine)
1287 def restoreLine(self, model, path, iter, data = None):
1288 if path in self.expandedLines:
1289 self.feedList.expand_row(path, False)
1291 def displayListing(self):
1292 icon_theme = gtk.icon_theme_get_default()
1293 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1294 gtk.ICON_LOOKUP_USE_BUILTIN)
1296 self.saveExpandedLines()
1298 self.feedItems.clear()
1299 hideReadFeed = self.config.getHideReadFeeds()
1300 order = self.config.getFeedSortOrder()
1302 categories = self.listing.getListOfCategories()
1303 if len(categories) > 1:
1304 showCategories = True
1306 showCategories = False
1308 for categoryId in categories:
1310 title = self.listing.getCategoryTitle(categoryId)
1311 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1313 if showCategories and len(keys)>0:
1314 category = self.feedItems.append(None, (None, title, categoryId))
1315 #print "catID" + str(categoryId) + " " + str(self.category)
1316 if categoryId == self.category:
1318 expandedRow = category
1321 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1322 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1323 updateTime = self.listing.getFeedUpdateTime(key)
1325 updateTime = "Never"
1326 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1328 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1330 markup = FEED_TEMPLATE % (title, subtitle)
1333 icon_filename = self.listing.getFavicon(key)
1334 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1335 LIST_ICON_SIZE, LIST_ICON_SIZE)
1337 pixbuf = default_pixbuf
1340 self.feedItems.append(category, (pixbuf, markup, key))
1342 self.feedItems.append(None, (pixbuf, markup, key))
1345 self.restoreExpandedLines()
1348 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1352 def on_feedList_row_activated(self, treeview, path, column):
1353 self.had_interaction = True
1354 model = treeview.get_model()
1355 iter = model.get_iter(path)
1356 key = model.get_value(iter, COLUMN_KEY)
1359 #print "Key: " + str(key)
1361 self.category = catId
1362 if treeview.row_expanded(path):
1363 treeview.collapse_row(path)
1365 # treeview.expand_row(path, True)
1366 #treeview.collapse_all()
1367 #treeview.expand_row(path, False)
1368 #for i in range(len(path)):
1369 # self.feedList.expand_row(path[:i+1], False)
1370 #self.show_confirmation_note(self.window, "Working")
1376 def openFeed(self, key):
1380 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1382 self.feed_lock = get_lock(key)
1383 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1384 self.listing.getFeedTitle(key), key, \
1385 self.config, self.updateDbusHandler)
1386 self.disp.connect("feed-closed", self.onFeedClosed)
1388 def openArticle(self, key, id):
1392 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1394 self.feed_lock = get_lock(key)
1395 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1396 self.listing.getFeedTitle(key), key, \
1397 self.config, self.updateDbusHandler)
1398 self.disp.button_clicked(None, id)
1399 self.disp.connect("feed-closed", self.onFeedClosed)
1402 def onFeedClosed(self, object, key):
1403 #self.listing.saveConfig()
1405 gobject.idle_add(self.onFeedClosedTimeout)
1406 self.displayListing()
1407 #self.updateDbusHandler.ArticleCountUpdated()
1409 def onFeedClosedTimeout(self):
1411 self.updateDbusHandler.ArticleCountUpdated()
1413 def quit(self, *args):
1416 if hasattr (self, 'app_lock'):
1419 # Wait until all slave threads have properly exited before
1420 # terminating the mainloop.
1424 if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1427 gobject.timeout_add(500, self.quit)
1432 self.window.connect("destroy", self.quit)
1435 def prefsClosed(self, *widget):
1437 self.orientation.set_mode(self.config.getOrientation())
1440 self.displayListing()
1441 self.checkAutoUpdate()
1443 def checkAutoUpdate(self, *widget):
1444 interval = int(self.config.getUpdateInterval()*3600000)
1445 if self.config.isAutoUpdateEnabled():
1446 if self.autoupdate == False:
1447 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1448 self.autoupdate = interval
1449 elif not self.autoupdate == interval:
1450 # If auto-update is enabled, but not at the right frequency
1451 gobject.source_remove(self.autoupdateId)
1452 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1453 self.autoupdate = interval
1455 if not self.autoupdate == False:
1456 gobject.source_remove(self.autoupdateId)
1457 self.autoupdate = False
1459 def automaticUpdate(self, *widget):
1460 # Need to check for internet connection
1461 # If no internet connection, try again in 10 minutes:
1462 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1463 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1464 #from time import localtime, strftime
1465 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1467 self.button_update_clicked(None, None)
1470 def stopUpdate(self):
1471 # Not implemented in the app (see update_feeds.py)
1473 JobManager().cancel ()
1477 def getStatus(self):
1479 for key in self.listing.getListOfFeeds():
1480 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1481 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1483 status = "No unread items"
1486 if __name__ == "__main__":
1488 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1490 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1491 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1492 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1493 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1494 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1495 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1496 gobject.threads_init()
1497 if not isdir(CONFIGDIR):
1501 print "Error: Can't create configuration directory"
1502 from sys import exit