1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # Copyright (c) 2011 Neal H. Walfield
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # ============================================================================
21 __appname__ = 'FeedingIt'
22 __author__ = 'Yves Marcoz'
24 __description__ = 'A simple RSS Reader for Maemo 5'
25 # ============================================================================
28 from pango import FontDescription
33 from webkit import WebView
38 from os.path import isfile, isdir, exists
39 from os import mkdir, remove, stat
41 from aboutdialog import HeAboutDialog
42 from portrait import FremantleRotation
43 from threading import Thread, activeCount
44 from feedingitdbus import ServerObject
45 from updatedbus import UpdateServerObject, get_lock
46 from config import Config
47 from cgi import escape
50 from rss_sqlite import Listing
51 from opml import GetOpmlData, ExportOpmlData
54 from jobmanager import JobManager
56 from socket import setdefaulttimeout
58 setdefaulttimeout(timeout)
66 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
67 ABOUT_ICON = 'feedingit'
68 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
69 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
70 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
71 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
73 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
74 unread_color = color_style.lookup_color('ActiveTextColor')
75 read_color = color_style.lookup_color('DefaultTextColor')
78 CONFIGDIR="/home/user/.feedingit/"
79 LOCK = CONFIGDIR + "update.lock"
82 from htmlentitydefs import name2codepoint
84 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
86 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
90 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
91 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
92 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
94 # Build the markup template for the Maemo 5 text style
95 head_font = style.get_font_desc('SystemFont')
96 sub_font = style.get_font_desc('SmallSystemFont')
98 #head_color = style.get_color('ButtonTextColor')
99 head_color = style.get_color('DefaultTextColor')
100 sub_color = style.get_color('DefaultTextColor')
101 active_color = style.get_color('ActiveTextColor')
103 bg_color = style.get_color('DefaultBackgroundColor').to_string()
104 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
105 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
106 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
107 bg_color = "#" + c1 + c2 + c3
110 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
111 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
113 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
114 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
116 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
117 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
119 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
120 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
122 FEED_TEMPLATE = '\n'.join((head, normal_sub))
123 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
125 ENTRY_TEMPLATE = entry_head
126 ENTRY_TEMPLATE_UNREAD = entry_active_head
129 # Removes HTML or XML character references and entities from a text string.
131 # @param text The HTML (or XML) source text.
132 # @return The plain text, as a Unicode string, if necessary.
133 # http://effbot.org/zone/re-sub.htm#unescape-html
138 # character reference
140 if text[:3] == "&#x":
141 return unichr(int(text[3:-1], 16))
143 return unichr(int(text[2:-1]))
149 text = unichr(name2codepoint[text[1:-1]])
152 return text # leave as is
153 return sub("&#?\w+;", fixup, text)
156 class AddWidgetWizard(gtk.Dialog):
157 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
158 gtk.Dialog.__init__(self)
159 self.set_transient_for(parent)
161 #self.category = categories[0]
162 self.category = currentCat
165 self.set_title('Edit RSS feed')
167 self.set_title('Add new RSS feed')
170 self.btn_add = self.add_button('Save', 2)
172 self.btn_add = self.add_button('Add', 2)
174 self.set_default_response(2)
176 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
177 self.nameEntry.set_placeholder('Feed name')
178 # If titleIn matches urlIn, there is no title.
179 if not titleIn == None and titleIn != urlIn:
180 self.nameEntry.set_text(titleIn)
181 self.nameEntry.select_region(-1, -1)
183 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
184 self.urlEntry.set_placeholder('Feed URL')
185 self.urlEntry.set_text(urlIn)
186 self.urlEntry.select_region(-1, -1)
187 self.urlEntry.set_activates_default(True)
189 self.table = gtk.Table(3, 2, False)
190 self.table.set_col_spacings(5)
191 label = gtk.Label('Name:')
192 label.set_alignment(1., .5)
193 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
194 self.table.attach(self.nameEntry, 1, 2, 0, 1)
195 label = gtk.Label('URL:')
196 label.set_alignment(1., .5)
197 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
198 self.table.attach(self.urlEntry, 1, 2, 1, 2)
199 selector = self.create_selector(categories, listing)
200 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
201 picker.set_selector(selector)
202 picker.set_title("Select category")
203 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
204 picker.set_name('HildonButton-finger')
205 picker.set_alignment(0,0,1,1)
207 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
209 self.vbox.pack_start(self.table)
214 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
216 def create_selector(self, choices, listing):
217 #self.pickerDialog = hildon.PickerDialog(self.parent)
218 selector = hildon.TouchSelector(text=True)
222 title = listing.getCategoryTitle(item)
223 iter = selector.append_text(str(title))
224 if self.category == item:
225 selector.set_active(0, index)
226 self.map[title] = item
228 selector.connect("changed", self.selection_changed)
229 #self.pickerDialog.set_selector(selector)
232 def selection_changed(self, selector, button):
233 current_selection = selector.get_current_text()
234 if current_selection:
235 self.category = self.map[current_selection]
237 class AddCategoryWizard(gtk.Dialog):
238 def __init__(self, parent, titleIn=None, isEdit=False):
239 gtk.Dialog.__init__(self)
240 self.set_transient_for(parent)
243 self.set_title('Edit Category')
245 self.set_title('Add Category')
248 self.btn_add = self.add_button('Save', 2)
250 self.btn_add = self.add_button('Add', 2)
252 self.set_default_response(2)
254 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
255 self.nameEntry.set_placeholder('Category name')
256 if not titleIn == None:
257 self.nameEntry.set_text(titleIn)
258 self.nameEntry.select_region(-1, -1)
260 self.table = gtk.Table(1, 2, False)
261 self.table.set_col_spacings(5)
262 label = gtk.Label('Name:')
263 label.set_alignment(1., .5)
264 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
265 self.table.attach(self.nameEntry, 1, 2, 0, 1)
266 #label = gtk.Label('URL:')
267 #label.set_alignment(1., .5)
268 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
269 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
270 self.vbox.pack_start(self.table)
275 return self.nameEntry.get_text()
277 class DownloadBar(gtk.ProgressBar):
280 if hasattr (cls, 'class_init_done'):
284 jm.stats_hook_register (cls.update_progress,
285 run_in_main_thread=True)
287 cls.downloadbars = []
288 # Total number of jobs we are monitoring.
290 # Number of jobs complete (of those that we are monitoring).
295 cls.class_init_done = True
297 def __init__(self, parent):
300 gtk.ProgressBar.__init__(self)
302 self.downloadbars.append(weakref.ref (self))
304 self.__class__.update_bars()
308 def downloading(cls):
309 return hasattr (cls, 'jobs_at_start')
312 def update_progress(cls, jm, old_stats, new_stats, updated_feed):
313 if not cls.downloading():
314 cls.jobs_at_start = old_stats['jobs-completed']
316 if not cls.downloadbars:
319 if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
320 del cls.jobs_at_start
321 for ref in cls.downloadbars:
324 # The download bar disappeared.
325 cls.downloadbars.remove (ref)
327 bar.emit("download-done", None)
330 # This should never be called if new_stats['jobs'] is 0, but
332 cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
333 cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
334 cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
335 + new_stats['jobs-queued']) / cls.total
339 for ref in cls.downloadbars:
342 # The download bar disappeared.
343 cls.downloadbars.remove (ref)
345 bar.emit("download-done", updated_feed)
348 def update_bars(cls):
349 # In preparation for i18n/l10n
351 return (a if n == 1 else b)
353 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
355 % (cls.done, cls.total))
357 for ref in cls.downloadbars:
360 # The download bar disappeared.
361 cls.downloadbars.remove (ref)
364 bar.set_fraction(cls.progress)
366 class SortList(hildon.StackableWindow):
367 def __init__(self, parent, listing, feedingit, after_closing, category=None):
368 hildon.StackableWindow.__init__(self)
369 self.set_transient_for(parent)
371 self.isEditingCategories = False
372 self.category = category
373 self.set_title(listing.getCategoryTitle(category))
375 self.isEditingCategories = True
376 self.set_title('Categories')
377 self.listing = listing
378 self.feedingit = feedingit
379 self.after_closing = after_closing
381 self.connect('destroy', lambda w: self.after_closing())
382 self.vbox2 = gtk.VBox(False, 2)
384 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
385 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
386 button.connect("clicked", self.buttonUp)
387 self.vbox2.pack_start(button, expand=False, fill=False)
389 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
390 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
391 button.connect("clicked", self.buttonDown)
392 self.vbox2.pack_start(button, expand=False, fill=False)
394 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
396 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
397 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
398 button.connect("clicked", self.buttonAdd)
399 self.vbox2.pack_start(button, expand=False, fill=False)
401 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
402 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
403 button.connect("clicked", self.buttonEdit)
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_delete', gtk.ICON_SIZE_BUTTON))
408 button.connect("clicked", self.buttonDelete)
409 self.vbox2.pack_start(button, expand=False, fill=False)
411 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
412 #button.set_label("Done")
413 #button.connect("clicked", self.buttonDone)
414 #self.vbox.pack_start(button)
415 self.hbox2= gtk.HBox(False, 10)
416 self.pannableArea = hildon.PannableArea()
417 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
418 self.treeview = gtk.TreeView(self.treestore)
419 self.hbox2.pack_start(self.pannableArea, expand=True)
421 self.hbox2.pack_end(self.vbox2, expand=False)
422 self.set_default_size(-1, 600)
425 menu = hildon.AppMenu()
426 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
427 button.set_label("Import from OPML")
428 button.connect("clicked", self.feedingit.button_import_clicked)
431 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
432 button.set_label("Export to OPML")
433 button.connect("clicked", self.feedingit.button_export_clicked)
435 self.set_app_menu(menu)
439 #self.connect("destroy", self.buttonDone)
441 def displayFeeds(self):
442 self.treeview.destroy()
443 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
444 self.treeview = gtk.TreeView()
446 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
447 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
449 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
451 self.pannableArea.add(self.treeview)
455 def refreshList(self, selected=None, offset=0):
456 #rect = self.treeview.get_visible_rect()
457 #y = rect.y+rect.height
458 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
459 if self.isEditingCategories:
460 for key in self.listing.getListOfCategories():
461 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
465 for key in self.listing.getListOfFeeds(category=self.category):
466 item = self.treestore.append([self.listing.getFeedTitle(key), key])
469 self.treeview.set_model(self.treestore)
470 if not selected == None:
471 self.treeview.get_selection().select_iter(selectedItem)
472 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
473 self.pannableArea.show_all()
475 def getSelectedItem(self):
476 (model, iter) = self.treeview.get_selection().get_selected()
479 return model.get_value(iter, 1)
481 def findIndex(self, key):
485 for row in self.treestore:
487 return (before, row.iter)
488 if key == list(row)[0]:
492 return (before, None)
494 def buttonUp(self, button):
495 key = self.getSelectedItem()
497 if self.isEditingCategories:
498 self.listing.moveCategoryUp(key)
500 self.listing.moveUp(key)
501 self.refreshList(key, -10)
503 def buttonDown(self, button):
504 key = self.getSelectedItem()
506 if self.isEditingCategories:
507 self.listing.moveCategoryDown(key)
509 self.listing.moveDown(key)
510 self.refreshList(key, 10)
512 def buttonDelete(self, button):
513 key = self.getSelectedItem()
515 message = 'Really remove this feed and its entries?'
516 dlg = hildon.hildon_note_new_confirmation(self, message)
519 if response == gtk.RESPONSE_OK:
520 if self.isEditingCategories:
521 self.listing.removeCategory(key)
523 self.listing.removeFeed(key)
526 def buttonEdit(self, button):
527 key = self.getSelectedItem()
529 if key == 'ArchivedArticles':
530 message = 'Cannot edit the archived articles feed.'
531 hildon.hildon_banner_show_information(self, '', message)
533 if self.isEditingCategories:
535 SortList(self.parent, self.listing, self.feedingit, None, category=key)
538 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
541 (title, url, category) = wizard.getData()
543 self.listing.editFeed(key, title, url, category=category)
547 def buttonDone(self, *args):
550 def buttonAdd(self, button, urlIn="http://"):
551 if self.isEditingCategories:
552 wizard = AddCategoryWizard(self)
555 title = wizard.getData()
556 if (not title == ''):
557 self.listing.addCategory(title)
559 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
562 (title, url, category) = wizard.getData()
564 self.listing.addFeed(title, url, category=category)
569 class DisplayArticle(hildon.StackableWindow):
570 def __init__(self, feed, id, key, config, listing):
571 hildon.StackableWindow.__init__(self)
572 #self.imageDownloader = ImageDownloader()
577 #self.set_title(feed.getTitle(id))
578 self.set_title(self.listing.getFeedTitle(key))
580 self.set_for_removal = False
582 # Init the article display
583 #if self.config.getWebkitSupport():
584 self.view = WebView()
585 #self.view.set_editable(False)
588 # self.view = gtkhtml2.View()
589 # self.document = gtkhtml2.Document()
590 # self.view.set_document(self.document)
591 # self.document.connect("link_clicked", self._signal_link_clicked)
592 self.pannable_article = hildon.PannableArea()
593 self.pannable_article.add(self.view)
594 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
595 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
597 #if self.config.getWebkitSupport():
598 contentLink = self.feed.getContentLink(self.id)
599 self.feed.setEntryRead(self.id)
600 #if key=="ArchivedArticles":
601 self.loadedArticle = False
602 if contentLink.startswith("/home/user/"):
603 self.view.open("file://%s" % contentLink)
604 self.currentUrl = self.feed.getExternalLink(self.id)
606 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
607 self.currentUrl = "%s" % contentLink
608 self.view.connect("motion-notify-event", lambda w,ev: True)
609 self.view.connect('load-started', self.load_started)
610 self.view.connect('load-finished', self.load_finished)
612 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
614 menu = hildon.AppMenu()
615 # Create a button and add it to the menu
616 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
617 button.set_label("Allow horizontal scrolling")
618 button.connect("clicked", self.horiz_scrolling_button)
621 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
622 button.set_label("Open in browser")
623 button.connect("clicked", self.open_in_browser)
626 if key == "ArchivedArticles":
627 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
628 button.set_label("Remove from archived articles")
629 button.connect("clicked", self.remove_archive_button)
631 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
632 button.set_label("Add to archived articles")
633 button.connect("clicked", self.archive_button)
636 self.set_app_menu(menu)
639 self.add(self.pannable_article)
641 self.pannable_article.show_all()
643 self.destroyId = self.connect("destroy", self.destroyWindow)
645 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
646 ## Still using an old version of WebKit, so using navigation-requested signal
647 self.view.connect('navigation-requested', self.navigation_requested)
649 self.view.connect("button_press_event", self.button_pressed)
650 self.gestureId = self.view.connect("button_release_event", self.button_released)
652 #def navigation_policy_decision(self, wv, fr, req, action, decision):
653 def navigation_requested(self, wv, fr, req):
654 if self.config.getOpenInExternalBrowser():
655 self.open_in_browser(None, req.get_uri())
660 def load_started(self, *widget):
661 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
663 def load_finished(self, *widget):
664 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
665 frame = self.view.get_main_frame()
666 if self.loadedArticle:
667 self.currentUrl = frame.get_uri()
669 self.loadedArticle = True
671 def button_pressed(self, window, event):
672 #print event.x, event.y
673 self.coords = (event.x, event.y)
675 def button_released(self, window, event):
676 x = self.coords[0] - event.x
677 y = self.coords[1] - event.y
679 if (2*abs(y) < abs(x)):
681 self.emit("article-previous", self.id)
683 self.emit("article-next", self.id)
685 def destroyWindow(self, *args):
686 self.disconnect(self.destroyId)
687 if self.set_for_removal:
688 self.emit("article-deleted", self.id)
690 self.emit("article-closed", self.id)
691 #self.imageDownloader.stopAll()
694 def horiz_scrolling_button(self, *widget):
695 self.pannable_article.disconnect(self.gestureId)
696 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
698 def archive_button(self, *widget):
699 # Call the listing.addArchivedArticle
700 self.listing.addArchivedArticle(self.key, self.id)
702 def remove_archive_button(self, *widget):
703 self.set_for_removal = True
705 def open_in_browser(self, object, link=None):
707 bus = dbus.SessionBus()
708 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
709 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
711 iface.open_new_window(self.currentUrl)
713 iface.open_new_window(link)
715 class DisplayFeed(hildon.StackableWindow):
716 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
717 hildon.StackableWindow.__init__(self)
718 self.listing = listing
720 self.feedTitle = title
721 self.set_title(title)
723 self.current = list()
725 self.updateDbusHandler = updateDbusHandler
727 self.downloadDialog = False
729 #self.listing.setCurrentlyDisplayedFeed(self.key)
733 menu = hildon.AppMenu()
734 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
735 button.set_label("Update feed")
736 button.connect("clicked", self.button_update_clicked)
739 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
740 button.set_label("Mark all as read")
741 button.connect("clicked", self.buttonReadAllClicked)
744 if key=="ArchivedArticles":
745 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
746 button.set_label("Delete read articles")
747 button.connect("clicked", self.buttonPurgeArticles)
750 self.set_app_menu(menu)
753 self.main_vbox = gtk.VBox(False, 0)
754 self.add(self.main_vbox)
756 self.pannableFeed = None
759 if DownloadBar.downloading ():
760 self.show_download_bar ()
762 self.connect('configure-event', self.on_configure_event)
763 self.connect("destroy", self.destroyWindow)
765 def on_configure_event(self, window, event):
766 if getattr(self, 'markup_renderer', None) is None:
769 # Fix up the column width for wrapping the text when the window is
770 # resized (i.e. orientation changed)
771 self.markup_renderer.set_property('wrap-width', event.width-20)
772 it = self.feedItems.get_iter_first()
773 while it is not None:
774 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
775 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
776 it = self.feedItems.iter_next(it)
778 def destroyWindow(self, *args):
779 #self.feed.saveUnread(CONFIGDIR)
780 self.listing.updateUnread(self.key)
781 self.emit("feed-closed", self.key)
783 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
784 #self.listing.closeCurrentlyDisplayedFeed()
786 def fix_title(self, title):
787 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
789 def displayFeed(self):
790 if self.pannableFeed:
791 self.pannableFeed.destroy()
793 self.pannableFeed = hildon.PannableArea()
795 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
797 self.feedItems = gtk.ListStore(str, str)
798 #self.feedList = gtk.TreeView(self.feedItems)
799 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
800 self.feedList.set_rules_hint(True)
802 selection = self.feedList.get_selection()
803 selection.set_mode(gtk.SELECTION_NONE)
804 #selection.connect("changed", lambda w: True)
806 self.feedList.set_model(self.feedItems)
807 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
810 self.feedList.set_hover_selection(False)
811 #self.feedList.set_property('enable-grid-lines', True)
812 #self.feedList.set_property('hildon-mode', 1)
813 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
815 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
817 vbox= gtk.VBox(False, 10)
818 vbox.pack_start(self.feedList)
820 self.pannableFeed.add_with_viewport(vbox)
822 self.markup_renderer = gtk.CellRendererText()
823 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
824 self.markup_renderer.set_property('background', bg_color) #"#333333")
825 (width, height) = self.get_size()
826 self.markup_renderer.set_property('wrap-width', width-20)
827 self.markup_renderer.set_property('ypad', 8)
828 self.markup_renderer.set_property('xpad', 5)
829 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
830 markup=FEED_COLUMN_MARKUP)
831 self.feedList.append_column(markup_column)
833 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
834 hideReadArticles = self.config.getHideReadArticles()
836 articles = self.feed.getIds(onlyUnread=True)
838 articles = self.feed.getIds()
841 self.current = list()
845 isRead = self.feed.isEntryRead(id)
848 if not ( isRead and hideReadArticles ):
849 title = self.fix_title(self.feed.getTitle(id))
850 self.current.append(id)
852 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
854 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
856 self.feedItems.append((markup, id))
859 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
861 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
862 self.feedItems.append((markup, ""))
864 self.main_vbox.pack_start(self.pannableFeed)
868 self.pannableFeed.destroy()
869 #self.remove(self.pannableFeed)
871 def on_feedList_row_activated(self, treeview, path): #, column):
872 selection = self.feedList.get_selection()
873 selection.set_mode(gtk.SELECTION_SINGLE)
874 self.feedList.get_selection().select_path(path)
875 model = treeview.get_model()
876 iter = model.get_iter(path)
877 key = model.get_value(iter, FEED_COLUMN_KEY)
878 # Emulate legacy "button_clicked" call via treeview
879 gobject.idle_add(self.button_clicked, treeview, key)
882 def button_clicked(self, button, index, previous=False, next=False):
883 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
884 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
885 stack = hildon.WindowStack.get_default()
888 stack.pop_and_push(1, newDisp, tmp)
890 gobject.timeout_add(200, self.destroyArticle, tmp)
895 if type(self.disp).__name__ == "DisplayArticle":
896 gobject.timeout_add(200, self.destroyArticle, self.disp)
903 if self.key == "ArchivedArticles":
904 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
905 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
906 self.ids.append(self.disp.connect("article-next", self.nextArticle))
907 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
909 def buttonPurgeArticles(self, *widget):
911 self.feed.purgeReadArticles()
912 #self.feed.saveFeed(CONFIGDIR)
915 def destroyArticle(self, handle):
916 handle.destroyWindow()
918 def mark_item_read(self, key):
919 it = self.feedItems.get_iter_first()
920 while it is not None:
921 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
923 title = self.fix_title(self.feed.getTitle(key))
924 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
925 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
927 it = self.feedItems.iter_next(it)
929 def nextArticle(self, object, index):
930 self.mark_item_read(index)
931 id = self.feed.getNextId(index)
932 while id not in self.current and id != index:
933 id = self.feed.getNextId(id)
935 self.button_clicked(object, id, next=True)
937 def previousArticle(self, object, index):
938 self.mark_item_read(index)
939 id = self.feed.getPreviousId(index)
940 while id not in self.current and id != index:
941 id = self.feed.getPreviousId(id)
943 self.button_clicked(object, id, previous=True)
945 def onArticleClosed(self, object, index):
946 selection = self.feedList.get_selection()
947 selection.set_mode(gtk.SELECTION_NONE)
948 self.mark_item_read(index)
950 def onArticleDeleted(self, object, index):
952 self.feed.removeArticle(index)
953 #self.feed.saveFeed(CONFIGDIR)
956 def button_update_clicked(self, button):
957 self.listing.updateFeed (self.key, priority=-1)
959 def show_download_bar(self):
960 if not type(self.downloadDialog).__name__=="DownloadBar":
961 self.downloadDialog = DownloadBar(self.window)
962 self.downloadDialog.connect("download-done", self.onDownloadDone)
963 self.main_vbox.pack_end(self.downloadDialog,
964 expand=False, fill=False)
967 def onDownloadDone(self, widget, feed):
968 if feed == self.feed or feed is None:
969 self.downloadDialog.destroy()
970 self.downloadDialog = False
971 self.feed = self.listing.getFeed(self.key)
973 self.updateDbusHandler.ArticleCountUpdated()
975 def buttonReadAllClicked(self, button):
977 self.feed.markAllAsRead()
978 it = self.feedItems.get_iter_first()
979 while it is not None:
980 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
981 title = self.fix_title(self.feed.getTitle(k))
982 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
983 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
984 it = self.feedItems.iter_next(it)
986 #for index in self.feed.getIds():
987 # self.feed.setEntryRead(index)
988 # self.mark_item_read(index)
994 self.window = hildon.StackableWindow()
995 self.window.set_title(__appname__)
996 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
997 self.mainVbox = gtk.VBox(False,10)
999 if isfile(CONFIGDIR+"/feeds.db"):
1000 self.introLabel = gtk.Label("Loading...")
1002 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1004 self.mainVbox.pack_start(self.introLabel)
1006 self.window.add(self.mainVbox)
1007 self.window.show_all()
1008 self.config = Config(self.window, CONFIGDIR+"config.ini")
1009 gobject.idle_add(self.createWindow)
1011 def createWindow(self):
1014 self.app_lock = get_lock("app_lock")
1015 if self.app_lock == None:
1017 self.stopButton.set_sensitive(True)
1019 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1020 self.stopButton.set_text("Stop update","")
1021 self.stopButton.connect("clicked", self.stop_running_update)
1022 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
1023 self.window.show_all()
1024 self.introLabel.set_label("Update in progress, please wait.")
1025 gobject.timeout_add_seconds(3, self.createWindow)
1028 self.stopButton.destroy()
1031 self.listing = Listing(self.config, CONFIGDIR)
1033 self.downloadDialog = False
1035 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1036 self.orientation.set_mode(self.config.getOrientation())
1038 print "Could not start rotation manager"
1040 menu = hildon.AppMenu()
1041 # Create a button and add it to the menu
1042 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1043 button.set_label("Update feeds")
1044 button.connect("clicked", self.button_update_clicked, "All")
1047 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1048 button.set_label("Mark all as read")
1049 button.connect("clicked", self.button_markAll)
1052 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1053 button.set_label("Add new feed")
1054 button.connect("clicked", lambda b: self.addFeed())
1057 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1058 button.set_label("Manage subscriptions")
1059 button.connect("clicked", self.button_organize_clicked)
1062 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1063 button.set_label("Settings")
1064 button.connect("clicked", self.button_preferences_clicked)
1067 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1068 button.set_label("About")
1069 button.connect("clicked", self.button_about_clicked)
1072 self.window.set_app_menu(menu)
1075 #self.feedWindow = hildon.StackableWindow()
1076 #self.articleWindow = hildon.StackableWindow()
1077 self.introLabel.destroy()
1078 self.pannableListing = hildon.PannableArea()
1079 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1080 self.feedList = gtk.TreeView(self.feedItems)
1081 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1082 #self.feedList.set_enable_tree_lines(True)
1083 #self.feedList.set_show_expanders(True)
1084 self.pannableListing.add(self.feedList)
1086 icon_renderer = gtk.CellRendererPixbuf()
1087 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1088 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1090 self.feedList.append_column(icon_column)
1092 markup_renderer = gtk.CellRendererText()
1093 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1094 markup=COLUMN_MARKUP)
1095 self.feedList.append_column(markup_column)
1096 self.mainVbox.pack_start(self.pannableListing)
1097 self.mainVbox.show_all()
1099 self.displayListing()
1100 self.autoupdate = False
1101 self.checkAutoUpdate()
1103 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1104 gobject.idle_add(self.late_init)
1106 def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
1107 if (not self.downloadDialog
1108 and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
1109 self.updateDbusHandler.UpdateStarted()
1111 self.downloadDialog = DownloadBar(self.window)
1112 self.downloadDialog.connect("download-done", self.onDownloadDone)
1113 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1114 self.mainVbox.show_all()
1116 if self.__dict__.get ('disp', None):
1117 self.disp.show_download_bar ()
1119 def onDownloadDone(self, widget, feed):
1121 self.downloadDialog.destroy()
1122 self.downloadDialog = False
1123 self.displayListing()
1124 self.updateDbusHandler.UpdateFinished()
1125 self.updateDbusHandler.ArticleCountUpdated()
1127 def stop_running_update(self, button):
1128 self.stopButton.set_sensitive(False)
1130 bus=dbus.SessionBus()
1131 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
1132 "/org/marcoz/feedingit/update" # Object's path
1134 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
1137 def increase_download_parallelism(self):
1138 # The system has been idle for a while. Enable parallel
1140 JobManager().num_threads = 4
1141 gobject.source_remove (self.increase_download_parallelism_id)
1142 del self.increase_download_parallelism_id
1145 def system_inactivity_ind(self, idle):
1146 # The system's idle state changed.
1147 if (self.am_idle and idle) or (not self.am_idle and not idle):
1152 if hasattr (self, 'increase_download_parallelism_id'):
1153 gobject.source_remove (self.increase_download_parallelism_id)
1154 del self.increase_download_parallelism_id
1156 self.increase_download_parallelism_id = \
1157 gobject.timeout_add_seconds(
1158 60, self.increase_download_parallelism)
1161 JobManager().num_threads = 1
1165 def late_init(self):
1166 self.dbusHandler = ServerObject(self)
1167 self.updateDbusHandler = UpdateServerObject(self)
1170 jm.stats_hook_register (self.job_manager_update,
1171 run_in_main_thread=True)
1173 self.am_idle = False
1177 bus = dbus.SystemBus()
1178 proxy = bus.get_object('com.nokia.mce',
1179 '/com/nokia/mce/signal')
1180 iface = dbus.Interface(proxy, 'com.nokia.mce.signal')
1181 iface.connect_to_signal('system_inactivity_ind',
1182 self.system_inactivity_ind)
1184 def button_markAll(self, button):
1185 for key in self.listing.getListOfFeeds():
1186 feed = self.listing.getFeed(key)
1187 feed.markAllAsRead()
1188 #for id in feed.getIds():
1189 # feed.setEntryRead(id)
1190 self.listing.updateUnread(key)
1191 self.displayListing()
1193 def button_about_clicked(self, button):
1194 HeAboutDialog.present(self.window, \
1204 def button_export_clicked(self, button):
1205 opml = ExportOpmlData(self.window, self.listing)
1207 def button_import_clicked(self, button):
1208 opml = GetOpmlData(self.window)
1209 feeds = opml.getData()
1210 for (title, url) in feeds:
1211 self.listing.addFeed(title, url)
1212 self.displayListing()
1214 def addFeed(self, urlIn="http://"):
1215 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1218 (title, url, category) = wizard.getData()
1220 self.listing.addFeed(title, url, category=category)
1222 self.displayListing()
1224 def button_organize_clicked(self, button):
1225 def after_closing():
1226 self.displayListing()
1227 SortList(self.window, self.listing, self, after_closing)
1229 def button_update_clicked(self, button, key):
1230 for k in self.listing.getListOfFeeds():
1231 self.listing.updateFeed (k)
1232 #self.displayListing()
1234 def onDownloadsDone(self, *widget):
1235 self.downloadDialog.destroy()
1236 self.downloadDialog = False
1237 self.displayListing()
1238 self.updateDbusHandler.UpdateFinished()
1239 self.updateDbusHandler.ArticleCountUpdated()
1241 def button_preferences_clicked(self, button):
1242 dialog = self.config.createDialog()
1243 dialog.connect("destroy", self.prefsClosed)
1245 def show_confirmation_note(self, parent, title):
1246 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1248 retcode = gtk.Dialog.run(note)
1251 if retcode == gtk.RESPONSE_OK:
1256 def saveExpandedLines(self):
1257 self.expandedLines = []
1258 model = self.feedList.get_model()
1259 model.foreach(self.checkLine)
1261 def checkLine(self, model, path, iter, data = None):
1262 if self.feedList.row_expanded(path):
1263 self.expandedLines.append(path)
1265 def restoreExpandedLines(self):
1266 model = self.feedList.get_model()
1267 model.foreach(self.restoreLine)
1269 def restoreLine(self, model, path, iter, data = None):
1270 if path in self.expandedLines:
1271 self.feedList.expand_row(path, False)
1273 def displayListing(self):
1274 icon_theme = gtk.icon_theme_get_default()
1275 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1276 gtk.ICON_LOOKUP_USE_BUILTIN)
1278 self.saveExpandedLines()
1280 self.feedItems.clear()
1281 hideReadFeed = self.config.getHideReadFeeds()
1282 order = self.config.getFeedSortOrder()
1284 categories = self.listing.getListOfCategories()
1285 if len(categories) > 1:
1286 showCategories = True
1288 showCategories = False
1290 for categoryId in categories:
1292 title = self.listing.getCategoryTitle(categoryId)
1293 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1295 if showCategories and len(keys)>0:
1296 category = self.feedItems.append(None, (None, title, categoryId))
1297 #print "catID" + str(categoryId) + " " + str(self.category)
1298 if categoryId == self.category:
1300 expandedRow = category
1303 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1304 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1305 updateTime = self.listing.getFeedUpdateTime(key)
1307 updateTime = "Never"
1308 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1310 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1312 markup = FEED_TEMPLATE % (title, subtitle)
1315 icon_filename = self.listing.getFavicon(key)
1316 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1317 LIST_ICON_SIZE, LIST_ICON_SIZE)
1319 pixbuf = default_pixbuf
1322 self.feedItems.append(category, (pixbuf, markup, key))
1324 self.feedItems.append(None, (pixbuf, markup, key))
1327 self.restoreExpandedLines()
1330 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1334 def on_feedList_row_activated(self, treeview, path, column):
1335 model = treeview.get_model()
1336 iter = model.get_iter(path)
1337 key = model.get_value(iter, COLUMN_KEY)
1340 #print "Key: " + str(key)
1342 self.category = catId
1343 if treeview.row_expanded(path):
1344 treeview.collapse_row(path)
1346 # treeview.expand_row(path, True)
1347 #treeview.collapse_all()
1348 #treeview.expand_row(path, False)
1349 #for i in range(len(path)):
1350 # self.feedList.expand_row(path[:i+1], False)
1351 #self.show_confirmation_note(self.window, "Working")
1357 def openFeed(self, key):
1361 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1363 self.feed_lock = get_lock(key)
1364 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1365 self.listing.getFeedTitle(key), key, \
1366 self.config, self.updateDbusHandler)
1367 self.disp.connect("feed-closed", self.onFeedClosed)
1369 def openArticle(self, key, id):
1373 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1375 self.feed_lock = get_lock(key)
1376 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1377 self.listing.getFeedTitle(key), key, \
1378 self.config, self.updateDbusHandler)
1379 self.disp.button_clicked(None, id)
1380 self.disp.connect("feed-closed", self.onFeedClosed)
1383 def onFeedClosed(self, object, key):
1384 #self.listing.saveConfig()
1386 gobject.idle_add(self.onFeedClosedTimeout)
1387 self.displayListing()
1388 #self.updateDbusHandler.ArticleCountUpdated()
1390 def onFeedClosedTimeout(self):
1392 self.updateDbusHandler.ArticleCountUpdated()
1394 def quit(self, *args):
1397 if hasattr (self, 'app_lock'):
1400 # Wait until all slave threads have properly exited before
1401 # terminating the mainloop.
1405 if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
1408 gobject.timeout_add(500, self.quit)
1413 self.window.connect("destroy", self.quit)
1416 def prefsClosed(self, *widget):
1418 self.orientation.set_mode(self.config.getOrientation())
1421 self.displayListing()
1422 self.checkAutoUpdate()
1424 def checkAutoUpdate(self, *widget):
1425 interval = int(self.config.getUpdateInterval()*3600000)
1426 if self.config.isAutoUpdateEnabled():
1427 if self.autoupdate == False:
1428 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1429 self.autoupdate = interval
1430 elif not self.autoupdate == interval:
1431 # If auto-update is enabled, but not at the right frequency
1432 gobject.source_remove(self.autoupdateId)
1433 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1434 self.autoupdate = interval
1436 if not self.autoupdate == False:
1437 gobject.source_remove(self.autoupdateId)
1438 self.autoupdate = False
1440 def automaticUpdate(self, *widget):
1441 # Need to check for internet connection
1442 # If no internet connection, try again in 10 minutes:
1443 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1444 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1445 #from time import localtime, strftime
1446 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1448 self.button_update_clicked(None, None)
1451 def stopUpdate(self):
1452 # Not implemented in the app (see update_feeds.py)
1454 JobManager().cancel ()
1458 def getStatus(self):
1460 for key in self.listing.getListOfFeeds():
1461 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1462 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1464 status = "No unread items"
1467 if __name__ == "__main__":
1470 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1471 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1472 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1473 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1474 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1475 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1476 gobject.threads_init()
1477 if not isdir(CONFIGDIR):
1481 print "Error: Can't create configuration directory"
1482 from sys import exit