Added a stop button in the application shown if a background update is running when...
[feedingit] / src / FeedingIt.py
1 #!/usr/bin/env python2.5
2
3
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.
9 #
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.
14 #
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/>.
17 #
18
19 # ============================================================================
20 __appname__ = 'FeedingIt'
21 __author__  = 'Yves Marcoz'
22 __version__ = '0.7.0'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
25
26 import gtk
27 from pango import FontDescription
28 import pango
29 import hildon
30 #import gtkhtml2
31 #try:
32 from webkit import WebView
33 #    has_webkit=True
34 #except:
35 #    import gtkhtml2
36 #    has_webkit=False
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
39 import gobject
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
47
48 from rss import Listing
49 from opml import GetOpmlData, ExportOpmlData
50
51 from urllib2 import install_opener, build_opener
52
53 from socket import setdefaulttimeout
54 timeout = 5
55 setdefaulttimeout(timeout)
56 del timeout
57
58 import xml.sax
59
60 LIST_ICON_SIZE = 32
61 LIST_ICON_BORDER = 10
62
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
69
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')
73 del color_style
74
75 CONFIGDIR="/home/user/.feedingit/"
76 LOCK = CONFIGDIR + "update.lock"
77
78 from re import sub
79 from htmlentitydefs import name2codepoint
80
81 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
82
83 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
84
85 import style
86
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>'
90
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')
94
95 head_color = style.get_color('ButtonTextColor')
96 sub_color = style.get_color('DefaultTextColor')
97 active_color = style.get_color('ActiveTextColor')
98
99 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
100 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
101
102 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
103 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
104
105 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
106 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
107
108 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
109 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
110
111 FEED_TEMPLATE = '\n'.join((head, normal_sub))
112 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
113
114 ENTRY_TEMPLATE = entry_head
115 ENTRY_TEMPLATE_UNREAD = entry_active_head
116
117 ##
118 # Removes HTML or XML character references and entities from a text string.
119 #
120 # @param text The HTML (or XML) source text.
121 # @return The plain text, as a Unicode string, if necessary.
122 # http://effbot.org/zone/re-sub.htm#unescape-html
123 def unescape(text):
124     def fixup(m):
125         text = m.group(0)
126         if text[:2] == "&#":
127             # character reference
128             try:
129                 if text[:3] == "&#x":
130                     return unichr(int(text[3:-1], 16))
131                 else:
132                     return unichr(int(text[2:-1]))
133             except ValueError:
134                 pass
135         else:
136             # named entity
137             try:
138                 text = unichr(name2codepoint[text[1:-1]])
139             except KeyError:
140                 pass
141         return text # leave as is
142     return sub("&#?\w+;", fixup, text)
143
144
145 class AddWidgetWizard(gtk.Dialog):
146     def __init__(self, parent, urlIn, titleIn=None, isEdit=False):
147         gtk.Dialog.__init__(self)
148         self.set_transient_for(parent)
149
150         if isEdit:
151             self.set_title('Edit RSS feed')
152         else:
153             self.set_title('Add new RSS feed')
154
155         if isEdit:
156             self.btn_add = self.add_button('Save', 2)
157         else:
158             self.btn_add = self.add_button('Add', 2)
159
160         self.set_default_response(2)
161
162         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
163         self.nameEntry.set_placeholder('Feed name')
164         if not titleIn == None:
165             self.nameEntry.set_text(titleIn)
166             self.nameEntry.select_region(-1, -1)
167
168         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
169         self.urlEntry.set_placeholder('Feed URL')
170         self.urlEntry.set_text(urlIn)
171         self.urlEntry.select_region(-1, -1)
172         self.urlEntry.set_activates_default(True)
173
174         self.table = gtk.Table(2, 2, False)
175         self.table.set_col_spacings(5)
176         label = gtk.Label('Name:')
177         label.set_alignment(1., .5)
178         self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
179         self.table.attach(self.nameEntry, 1, 2, 0, 1)
180         label = gtk.Label('URL:')
181         label.set_alignment(1., .5)
182         self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
183         self.table.attach(self.urlEntry, 1, 2, 1, 2)
184         self.vbox.pack_start(self.table)
185
186         self.show_all()
187
188     def getData(self):
189         return (self.nameEntry.get_text(), self.urlEntry.get_text())
190
191     def some_page_func(self, nb, current, userdata):
192         # Validate data for 1st page
193         if current == 0:
194             return len(self.nameEntry.get_text()) != 0
195         elif current == 1:
196             # Check the url is not null, and starts with http
197             return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
198         elif current != 2:
199             return False
200         else:
201             return True
202         
203 class Download(Thread):
204     def __init__(self, listing, key, config):
205         Thread.__init__(self)
206         self.listing = listing
207         self.key = key
208         self.config = config
209         
210     def run (self):
211         (use_proxy, proxy) = self.config.getProxy()
212         key_lock = get_lock(self.key)
213         if key_lock != None:
214             if use_proxy:
215                 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
216             else:
217                 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
218         del key_lock
219
220         
221 class DownloadBar(gtk.ProgressBar):
222     def __init__(self, parent, listing, listOfKeys, config, single=False):
223         
224         update_lock = get_lock("update_lock")
225         if update_lock != None:
226             gtk.ProgressBar.__init__(self)
227             self.listOfKeys = listOfKeys[:]
228             self.listing = listing
229             self.total = len(self.listOfKeys)
230             self.config = config
231             self.current = 0
232             self.single = single
233             (use_proxy, proxy) = self.config.getProxy()
234             if use_proxy:
235                 opener = build_opener(proxy)
236             else:
237                 opener = build_opener()
238
239             opener.addheaders = [('User-agent', USER_AGENT)]
240             install_opener(opener)
241
242             if self.total>0:
243                 # In preparation for i18n/l10n
244                 def N_(a, b, n):
245                     return (a if n == 1 else b)
246
247                 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
248
249                 self.fraction = 0
250                 self.set_fraction(self.fraction)
251                 self.show_all()
252                 # Create a timeout
253                 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
254
255     def update_progress_bar(self):
256         #self.progress_bar.pulse()
257         if activeCount() < 4:
258             x = activeCount() - 1
259             k = len(self.listOfKeys)
260             fin = self.total - k - x
261             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
262             #print x, k, fin, fraction
263             self.set_fraction(fraction)
264
265             if len(self.listOfKeys)>0:
266                 self.current = self.current+1
267                 key = self.listOfKeys.pop()
268                 #if self.single == True:
269                     # Check if the feed is being displayed
270                 download = Download(self.listing, key, self.config)
271                 download.start()
272                 return True
273             elif activeCount() > 1:
274                 return True
275             else:
276                 #self.waitingWindow.destroy()
277                 #self.destroy()
278                 try:
279                     del self.update_lock
280                 except:
281                     pass
282                 self.emit("download-done", "success")
283                 return False 
284         return True
285     
286     
287 class SortList(hildon.StackableWindow):
288     def __init__(self, parent, listing, feedingit, after_closing):
289         hildon.StackableWindow.__init__(self)
290         self.set_transient_for(parent)
291         self.set_title('Subscriptions')
292         self.listing = listing
293         self.feedingit = feedingit
294         self.after_closing = after_closing
295         self.connect('destroy', lambda w: self.after_closing())
296         self.vbox2 = gtk.VBox(False, 2)
297
298         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
299         button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
300         button.connect("clicked", self.buttonUp)
301         self.vbox2.pack_start(button, expand=False, fill=False)
302
303         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
304         button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
305         button.connect("clicked", self.buttonDown)
306         self.vbox2.pack_start(button, expand=False, fill=False)
307
308         self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
309
310         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
311         button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
312         button.connect("clicked", self.buttonAdd)
313         self.vbox2.pack_start(button, expand=False, fill=False)
314
315         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
316         button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
317         button.connect("clicked", self.buttonEdit)
318         self.vbox2.pack_start(button, expand=False, fill=False)
319
320         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
321         button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
322         button.connect("clicked", self.buttonDelete)
323         self.vbox2.pack_start(button, expand=False, fill=False)
324
325         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
326         #button.set_label("Done")
327         #button.connect("clicked", self.buttonDone)
328         #self.vbox.pack_start(button)
329         self.hbox2= gtk.HBox(False, 10)
330         self.pannableArea = hildon.PannableArea()
331         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
332         self.treeview = gtk.TreeView(self.treestore)
333         self.hbox2.pack_start(self.pannableArea, expand=True)
334         self.displayFeeds()
335         self.hbox2.pack_end(self.vbox2, expand=False)
336         self.set_default_size(-1, 600)
337         self.add(self.hbox2)
338
339         menu = hildon.AppMenu()
340         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
341         button.set_label("Import from OPML")
342         button.connect("clicked", self.feedingit.button_import_clicked)
343         menu.append(button)
344
345         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
346         button.set_label("Export to OPML")
347         button.connect("clicked", self.feedingit.button_export_clicked)
348         menu.append(button)
349         self.set_app_menu(menu)
350         menu.show_all()
351         
352         self.show_all()
353         #self.connect("destroy", self.buttonDone)
354         
355     def displayFeeds(self):
356         self.treeview.destroy()
357         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
358         self.treeview = gtk.TreeView()
359         
360         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
361         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
362         self.refreshList()
363         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
364
365         self.pannableArea.add(self.treeview)
366
367         #self.show_all()
368
369     def refreshList(self, selected=None, offset=0):
370         #rect = self.treeview.get_visible_rect()
371         #y = rect.y+rect.height
372         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
373         for key in self.listing.getListOfFeeds():
374             item = self.treestore.append([self.listing.getFeedTitle(key), key])
375             if key == selected:
376                 selectedItem = item
377         self.treeview.set_model(self.treestore)
378         if not selected == None:
379             self.treeview.get_selection().select_iter(selectedItem)
380             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
381         self.pannableArea.show_all()
382
383     def getSelectedItem(self):
384         (model, iter) = self.treeview.get_selection().get_selected()
385         if not iter:
386             return None
387         return model.get_value(iter, 1)
388
389     def findIndex(self, key):
390         after = None
391         before = None
392         found = False
393         for row in self.treestore:
394             if found:
395                 return (before, row.iter)
396             if key == list(row)[0]:
397                 found = True
398             else:
399                 before = row.iter
400         return (before, None)
401
402     def buttonUp(self, button):
403         key  = self.getSelectedItem()
404         if not key == None:
405             self.listing.moveUp(key)
406             self.refreshList(key, -10)
407
408     def buttonDown(self, button):
409         key = self.getSelectedItem()
410         if not key == None:
411             self.listing.moveDown(key)
412             self.refreshList(key, 10)
413
414     def buttonDelete(self, button):
415         key = self.getSelectedItem()
416
417         message = 'Really remove this feed and its entries?'
418         dlg = hildon.hildon_note_new_confirmation(self, message)
419         response = dlg.run()
420         dlg.destroy()
421         if response == gtk.RESPONSE_OK:
422             self.listing.removeFeed(key)
423             self.refreshList()
424
425     def buttonEdit(self, button):
426         key = self.getSelectedItem()
427
428         if key == 'ArchivedArticles':
429             message = 'Cannot edit the archived articles feed.'
430             hildon.hildon_banner_show_information(self, '', message)
431             return
432
433         if key is not None:
434             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True)
435             ret = wizard.run()
436             if ret == 2:
437                 (title, url) = wizard.getData()
438                 if (not title == '') and (not url == ''):
439                     self.listing.editFeed(key, title, url)
440                     self.refreshList()
441             wizard.destroy()
442
443     def buttonDone(self, *args):
444         self.destroy()
445         
446     def buttonAdd(self, button, urlIn="http://"):
447         wizard = AddWidgetWizard(self, urlIn)
448         ret = wizard.run()
449         if ret == 2:
450             (title, url) = wizard.getData()
451             if (not title == '') and (not url == ''): 
452                self.listing.addFeed(title, url)
453         wizard.destroy()
454         self.refreshList()
455                
456
457 class DisplayArticle(hildon.StackableWindow):
458     def __init__(self, feed, id, key, config, listing):
459         hildon.StackableWindow.__init__(self)
460         #self.imageDownloader = ImageDownloader()
461         self.feed = feed
462         self.listing=listing
463         self.key = key
464         self.id = id
465         #self.set_title(feed.getTitle(id))
466         self.set_title(self.listing.getFeedTitle(key))
467         self.config = config
468         self.set_for_removal = False
469         
470         # Init the article display
471         #if self.config.getWebkitSupport():
472         self.view = WebView()
473             #self.view.set_editable(False)
474         #else:
475         #    import gtkhtml2
476         #    self.view = gtkhtml2.View()
477         #    self.document = gtkhtml2.Document()
478         #    self.view.set_document(self.document)
479         #    self.document.connect("link_clicked", self._signal_link_clicked)
480         self.pannable_article = hildon.PannableArea()
481         self.pannable_article.add(self.view)
482         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
483         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
484
485         #if self.config.getWebkitSupport():
486         contentLink = self.feed.getContentLink(self.id)
487         self.feed.setEntryRead(self.id)
488         #if key=="ArchivedArticles":
489         self.loadedArticle = False
490         if contentLink.startswith("/home/user/"):
491             self.view.open("file://%s" % contentLink)
492             self.currentUrl = self.feed.getExternalLink(self.id)
493         else:
494             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
495             self.currentUrl = "%s" % contentLink
496         self.view.connect("motion-notify-event", lambda w,ev: True)
497         self.view.connect('load-started', self.load_started)
498         self.view.connect('load-finished', self.load_finished)
499
500         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
501         
502         menu = hildon.AppMenu()
503         # Create a button and add it to the menu
504         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
505         button.set_label("Allow horizontal scrolling")
506         button.connect("clicked", self.horiz_scrolling_button)
507         menu.append(button)
508         
509         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
510         button.set_label("Open in browser")
511         button.connect("clicked", self.open_in_browser)
512         menu.append(button)
513         
514         if key == "ArchivedArticles":
515             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
516             button.set_label("Remove from archived articles")
517             button.connect("clicked", self.remove_archive_button)
518         else:
519             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
520             button.set_label("Add to archived articles")
521             button.connect("clicked", self.archive_button)
522         menu.append(button)
523         
524         self.set_app_menu(menu)
525         menu.show_all()
526         
527         self.add(self.pannable_article)
528         
529         self.pannable_article.show_all()
530
531         self.destroyId = self.connect("destroy", self.destroyWindow)
532         
533         #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
534         ## Still using an old version of WebKit, so using navigation-requested signal
535         self.view.connect('navigation-requested', self.navigation_requested)
536         
537         self.view.connect("button_press_event", self.button_pressed)
538         self.gestureId = self.view.connect("button_release_event", self.button_released)
539
540     #def navigation_policy_decision(self, wv, fr, req, action, decision):
541     def navigation_requested(self, wv, fr, req):
542         if self.config.getOpenInExternalBrowser():
543             self.open_in_browser(None, req.get_uri())
544             return True
545         else:
546             return False
547
548     def load_started(self, *widget):
549         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
550         
551     def load_finished(self, *widget):
552         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
553         frame = self.view.get_main_frame()
554         if self.loadedArticle:
555             self.currentUrl = frame.get_uri()
556         else:
557             self.loadedArticle = True
558
559     def button_pressed(self, window, event):
560         #print event.x, event.y
561         self.coords = (event.x, event.y)
562         
563     def button_released(self, window, event):
564         x = self.coords[0] - event.x
565         y = self.coords[1] - event.y
566         
567         if (2*abs(y) < abs(x)):
568             if (x > 15):
569                 self.emit("article-previous", self.id)
570             elif (x<-15):
571                 self.emit("article-next", self.id)   
572
573     def destroyWindow(self, *args):
574         self.disconnect(self.destroyId)
575         if self.set_for_removal:
576             self.emit("article-deleted", self.id)
577         else:
578             self.emit("article-closed", self.id)
579         #self.imageDownloader.stopAll()
580         self.destroy()
581         
582     def horiz_scrolling_button(self, *widget):
583         self.pannable_article.disconnect(self.gestureId)
584         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
585         
586     def archive_button(self, *widget):
587         # Call the listing.addArchivedArticle
588         self.listing.addArchivedArticle(self.key, self.id)
589         
590     def remove_archive_button(self, *widget):
591         self.set_for_removal = True
592
593     def open_in_browser(self, object, link=None):
594         import dbus
595         bus = dbus.SessionBus()
596         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
597         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
598         if link == None:
599             iface.open_new_window(self.currentUrl)
600         else:
601             iface.open_new_window(link)
602
603 class DisplayFeed(hildon.StackableWindow):
604     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
605         hildon.StackableWindow.__init__(self)
606         self.listing = listing
607         self.feed = feed
608         self.feedTitle = title
609         self.set_title(title)
610         self.key=key
611         self.config = config
612         self.updateDbusHandler = updateDbusHandler
613         
614         self.downloadDialog = False
615         
616         #self.listing.setCurrentlyDisplayedFeed(self.key)
617         
618         self.disp = False
619         
620         menu = hildon.AppMenu()
621         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
622         button.set_label("Update feed")
623         button.connect("clicked", self.button_update_clicked)
624         menu.append(button)
625         
626         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
627         button.set_label("Mark all as read")
628         button.connect("clicked", self.buttonReadAllClicked)
629         menu.append(button)
630         
631         if key=="ArchivedArticles":
632             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
633             button.set_label("Delete read articles")
634             button.connect("clicked", self.buttonPurgeArticles)
635             menu.append(button)
636         
637         self.set_app_menu(menu)
638         menu.show_all()
639         
640         self.displayFeed()
641         
642         self.connect('configure-event', self.on_configure_event)
643         self.connect("destroy", self.destroyWindow)
644
645     def on_configure_event(self, window, event):
646         if getattr(self, 'markup_renderer', None) is None:
647             return
648
649         # Fix up the column width for wrapping the text when the window is
650         # resized (i.e. orientation changed)
651         self.markup_renderer.set_property('wrap-width', event.width-20)  
652         it = self.feedItems.get_iter_first()
653         while it is not None:
654             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
655             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
656             it = self.feedItems.iter_next(it)
657
658     def destroyWindow(self, *args):
659         #self.feed.saveUnread(CONFIGDIR)
660         gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
661         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
662         self.emit("feed-closed", self.key)
663         self.destroy()
664         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
665         #self.listing.closeCurrentlyDisplayedFeed()
666
667     def fix_title(self, title):
668         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
669
670     def displayFeed(self):
671         self.pannableFeed = hildon.PannableArea()
672
673         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
674
675         self.feedItems = gtk.ListStore(str, str)
676         #self.feedList = gtk.TreeView(self.feedItems)
677         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
678         selection = self.feedList.get_selection()
679         selection.set_mode(gtk.SELECTION_NONE)
680         #selection.connect("changed", lambda w: True)
681         
682         self.feedList.set_model(self.feedItems)
683         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
684
685         
686         self.feedList.set_hover_selection(False)
687         #self.feedList.set_property('enable-grid-lines', True)
688         #self.feedList.set_property('hildon-mode', 1)
689         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
690         
691         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
692
693         vbox= gtk.VBox(False, 10)
694         vbox.pack_start(self.feedList)
695         
696         self.pannableFeed.add_with_viewport(vbox)
697
698         self.markup_renderer = gtk.CellRendererText()
699         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
700         self.markup_renderer.set_property('background', "#333333")
701         (width, height) = self.get_size()
702         self.markup_renderer.set_property('wrap-width', width-20)
703         self.markup_renderer.set_property('ypad', 5)
704         self.markup_renderer.set_property('xpad', 5)
705         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
706                 markup=FEED_COLUMN_MARKUP)
707         self.feedList.append_column(markup_column)
708
709         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
710         hideReadArticles = self.config.getHideReadArticles()
711         hasArticle = False
712         for id in self.feed.getIds():
713             isRead = False
714             try:
715                 isRead = self.feed.isEntryRead(id)
716             except:
717                 pass
718             if not ( isRead and hideReadArticles ):
719             #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
720                 #title = self.feed.getTitle(id)
721                 title = self.fix_title(self.feed.getTitle(id))
722     
723                 #if self.feed.isEntryRead(id):
724                 if isRead:
725                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
726                 else:
727                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
728     
729                 self.feedItems.append((markup, id))
730                 hasArticle = True
731         if hasArticle:
732             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
733         else:
734             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
735             self.feedItems.append((markup, ""))
736
737         self.add(self.pannableFeed)
738         self.show_all()
739
740     def clear(self):
741         self.pannableFeed.destroy()
742         #self.remove(self.pannableFeed)
743
744     def on_feedList_row_activated(self, treeview, path): #, column):
745         selection = self.feedList.get_selection()
746         selection.set_mode(gtk.SELECTION_SINGLE)
747         self.feedList.get_selection().select_path(path)
748         model = treeview.get_model()
749         iter = model.get_iter(path)
750         key = model.get_value(iter, FEED_COLUMN_KEY)
751         # Emulate legacy "button_clicked" call via treeview
752         gobject.idle_add(self.button_clicked, treeview, key)
753         #return True
754
755     def button_clicked(self, button, index, previous=False, next=False):
756         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
757         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
758         stack = hildon.WindowStack.get_default()
759         if previous:
760             tmp = stack.peek()
761             stack.pop_and_push(1, newDisp, tmp)
762             newDisp.show()
763             gobject.timeout_add(200, self.destroyArticle, tmp)
764             #print "previous"
765             self.disp = newDisp
766         elif next:
767             newDisp.show_all()
768             if type(self.disp).__name__ == "DisplayArticle":
769                 gobject.timeout_add(200, self.destroyArticle, self.disp)
770             self.disp = newDisp
771         else:
772             self.disp = newDisp
773             self.disp.show_all()
774         
775         self.ids = []
776         if self.key == "ArchivedArticles":
777             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
778         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
779         self.ids.append(self.disp.connect("article-next", self.nextArticle))
780         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
781
782     def buttonPurgeArticles(self, *widget):
783         self.clear()
784         self.feed.purgeReadArticles()
785         self.feed.saveUnread(CONFIGDIR)
786         self.feed.saveFeed(CONFIGDIR)
787         self.displayFeed()
788
789     def destroyArticle(self, handle):
790         handle.destroyWindow()
791
792     def mark_item_read(self, key):
793         it = self.feedItems.get_iter_first()
794         while it is not None:
795             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
796             if k == key:
797                 title = self.fix_title(self.feed.getTitle(key))
798                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
799                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
800                 break
801             it = self.feedItems.iter_next(it)
802
803     def nextArticle(self, object, index):
804         self.mark_item_read(index)
805         id = self.feed.getNextId(index)
806         if self.config.getHideReadArticles():
807             isRead = False
808             try:
809                 isRead = self.feed.isEntryRead(id)
810             except:
811                 pass
812             while isRead and id != index:
813                 id = self.feed.getNextId(id)
814                 isRead = False
815                 try:
816                        isRead = self.feed.isEntryRead(id)
817                 except:
818                        pass
819         if id != index:
820             self.button_clicked(object, id, next=True)
821
822     def previousArticle(self, object, index):
823         self.mark_item_read(index)
824         id = self.feed.getPreviousId(index)
825         if self.config.getHideReadArticles():
826             isRead = False
827             try:
828                 isRead = self.feed.isEntryRead(id)
829             except:
830                 pass
831             while isRead and id != index:
832                 id = self.feed.getPreviousId(id)
833                 isRead = False
834                 try:
835                        isRead = self.feed.isEntryRead(id)
836                 except:
837                        pass
838         if id != index:
839             self.button_clicked(object, id, previous=True)
840
841     def onArticleClosed(self, object, index):
842         selection = self.feedList.get_selection()
843         selection.set_mode(gtk.SELECTION_NONE)
844         self.mark_item_read(index)
845
846     def onArticleDeleted(self, object, index):
847         self.clear()
848         self.feed.removeArticle(index)
849         self.feed.saveUnread(CONFIGDIR)
850         self.feed.saveFeed(CONFIGDIR)
851         self.displayFeed()
852
853     def button_update_clicked(self, button):
854         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
855         if not type(self.downloadDialog).__name__=="DownloadBar":
856             self.pannableFeed.destroy()
857             self.vbox = gtk.VBox(False, 10)
858             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
859             self.downloadDialog.connect("download-done", self.onDownloadsDone)
860             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
861             self.add(self.vbox)
862             self.show_all()
863             
864     def onDownloadsDone(self, *widget):
865         self.vbox.destroy()
866         self.feed = self.listing.getFeed(self.key)
867         self.displayFeed()
868         self.updateDbusHandler.ArticleCountUpdated()
869         
870     def buttonReadAllClicked(self, button):
871         for index in self.feed.getIds():
872             self.feed.setEntryRead(index)
873             self.mark_item_read(index)
874
875
876 class FeedingIt:
877     def __init__(self):
878         # Init the windows
879         self.window = hildon.StackableWindow()
880         self.window.set_title(__appname__)
881         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
882         self.mainVbox = gtk.VBox(False,10)
883         
884         self.introLabel = gtk.Label("Loading...")
885
886         
887         self.mainVbox.pack_start(self.introLabel)
888
889         self.window.add(self.mainVbox)
890         self.window.show_all()
891         self.config = Config(self.window, CONFIGDIR+"config.ini")
892         gobject.idle_add(self.createWindow)
893         
894     def createWindow(self):
895         self.app_lock = get_lock("app_lock")
896         if self.app_lock == None:
897             try:
898                 self.stopButton.set_sensitive(True)
899             except:
900                 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
901                 self.stopButton.set_text("Stop update","")
902                 self.stopButton.connect("clicked", self.stop_running_update)
903                 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
904                 self.window.show_all()
905             self.introLabel.set_label("Update in progress, please wait.")
906             gobject.timeout_add_seconds(3, self.createWindow)
907             return False
908         try:
909             self.stopButton.destroy()
910         except:
911             pass
912         self.listing = Listing(CONFIGDIR)
913         
914         self.downloadDialog = False
915         try:
916             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
917             self.orientation.set_mode(self.config.getOrientation())
918         except:
919             print "Could not start rotation manager"
920         
921         menu = hildon.AppMenu()
922         # Create a button and add it to the menu
923         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
924         button.set_label("Update feeds")
925         button.connect("clicked", self.button_update_clicked, "All")
926         menu.append(button)
927         
928         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
929         button.set_label("Mark all as read")
930         button.connect("clicked", self.button_markAll)
931         menu.append(button)
932
933         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
934         button.set_label("Add new feed")
935         button.connect("clicked", lambda b: self.addFeed())
936         menu.append(button)
937
938         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
939         button.set_label("Manage subscriptions")
940         button.connect("clicked", self.button_organize_clicked)
941         menu.append(button)
942
943         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
944         button.set_label("Settings")
945         button.connect("clicked", self.button_preferences_clicked)
946         menu.append(button)
947        
948         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
949         button.set_label("About")
950         button.connect("clicked", self.button_about_clicked)
951         menu.append(button)
952         
953         self.window.set_app_menu(menu)
954         menu.show_all()
955         
956         #self.feedWindow = hildon.StackableWindow()
957         #self.articleWindow = hildon.StackableWindow()
958         self.introLabel.destroy()
959         self.pannableListing = hildon.PannableArea()
960         self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
961         self.feedList = gtk.TreeView(self.feedItems)
962         self.feedList.connect('row-activated', self.on_feedList_row_activated)
963         self.pannableListing.add(self.feedList)
964
965         icon_renderer = gtk.CellRendererPixbuf()
966         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
967         icon_column = gtk.TreeViewColumn('', icon_renderer, \
968                 pixbuf=COLUMN_ICON)
969         self.feedList.append_column(icon_column)
970
971         markup_renderer = gtk.CellRendererText()
972         markup_column = gtk.TreeViewColumn('', markup_renderer, \
973                 markup=COLUMN_MARKUP)
974         self.feedList.append_column(markup_column)
975         self.mainVbox.pack_start(self.pannableListing)
976         self.mainVbox.show_all()
977
978         self.displayListing()
979         self.autoupdate = False
980         self.checkAutoUpdate()
981         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
982         gobject.idle_add(self.enableDbus)
983         
984     def stop_running_update(self, button):
985         self.stopButton.set_sensitive(False)
986         import dbus
987         bus=dbus.SessionBus()
988         remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
989                                "/org/marcoz/feedingit/update" # Object's path
990                               )
991         iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
992         iface.StopUpdate()
993     
994     def enableDbus(self):
995         self.dbusHandler = ServerObject(self)
996         self.updateDbusHandler = UpdateServerObject(self)
997
998     def button_markAll(self, button):
999         for key in self.listing.getListOfFeeds():
1000             feed = self.listing.getFeed(key)
1001             for id in feed.getIds():
1002                 feed.setEntryRead(id)
1003             feed.saveUnread(CONFIGDIR)
1004             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
1005         self.displayListing()
1006
1007     def button_about_clicked(self, button):
1008         HeAboutDialog.present(self.window, \
1009                 __appname__, \
1010                 ABOUT_ICON, \
1011                 __version__, \
1012                 __description__, \
1013                 ABOUT_COPYRIGHT, \
1014                 ABOUT_WEBSITE, \
1015                 ABOUT_BUGTRACKER, \
1016                 ABOUT_DONATE)
1017
1018     def button_export_clicked(self, button):
1019         opml = ExportOpmlData(self.window, self.listing)
1020         
1021     def button_import_clicked(self, button):
1022         opml = GetOpmlData(self.window)
1023         feeds = opml.getData()
1024         for (title, url) in feeds:
1025             self.listing.addFeed(title, url)
1026         self.displayListing()
1027
1028     def addFeed(self, urlIn="http://"):
1029         wizard = AddWidgetWizard(self.window, urlIn)
1030         ret = wizard.run()
1031         if ret == 2:
1032             (title, url) = wizard.getData()
1033             if (not title == '') and (not url == ''): 
1034                self.listing.addFeed(title, url)
1035         wizard.destroy()
1036         self.displayListing()
1037
1038     def button_organize_clicked(self, button):
1039         def after_closing():
1040             self.listing.saveConfig()
1041             self.displayListing()
1042         SortList(self.window, self.listing, self, after_closing)
1043
1044     def button_update_clicked(self, button, key):
1045         if not type(self.downloadDialog).__name__=="DownloadBar":
1046             self.updateDbusHandler.UpdateStarted()
1047             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1048             self.downloadDialog.connect("download-done", self.onDownloadsDone)
1049             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1050             self.mainVbox.show_all()
1051         #self.displayListing()
1052
1053     def onDownloadsDone(self, *widget):
1054         self.downloadDialog.destroy()
1055         self.downloadDialog = False
1056         self.displayListing()
1057         self.updateDbusHandler.UpdateFinished()
1058         self.updateDbusHandler.ArticleCountUpdated()
1059
1060     def button_preferences_clicked(self, button):
1061         dialog = self.config.createDialog()
1062         dialog.connect("destroy", self.prefsClosed)
1063
1064     def show_confirmation_note(self, parent, title):
1065         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1066
1067         retcode = gtk.Dialog.run(note)
1068         note.destroy()
1069         
1070         if retcode == gtk.RESPONSE_OK:
1071             return True
1072         else:
1073             return False
1074         
1075     def displayListing(self):
1076         icon_theme = gtk.icon_theme_get_default()
1077         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1078                 gtk.ICON_LOOKUP_USE_BUILTIN)
1079
1080         self.feedItems.clear()
1081         feedInfo = {}
1082         count = 0
1083         for key in self.listing.getListOfFeeds():
1084             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1085             if unreadItems > 0 or not self.config.getHideReadFeeds():
1086                 count=count+1
1087                 title = self.listing.getFeedTitle(key)
1088                 updateTime = self.listing.getFeedUpdateTime(key)
1089                 updateStamp = self.listing.getFeedUpdateStamp(key)
1090                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1091                 feedInfo[key] = [count, unreadItems, updateStamp, title, subtitle, updateTime];
1092
1093         order = self.config.getFeedSortOrder();
1094         if   order == "Most unread":
1095             keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
1096         elif order == "Least unread":
1097             keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
1098         elif order == "Most recent":
1099             keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
1100         elif order == "Least recent":
1101             keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
1102         else: # order == "Manual" or invalid value...
1103             keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
1104         
1105         for key in keyorder:
1106             unreadItems = feedInfo[key][1]
1107             title = xml.sax.saxutils.escape(feedInfo[key][3])
1108             subtitle = feedInfo[key][4]
1109             updateTime = feedInfo[key][5]
1110             if unreadItems:
1111                 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1112             else:
1113                 markup = FEED_TEMPLATE % (title, subtitle)
1114     
1115             try:
1116                 icon_filename = self.listing.getFavicon(key)
1117                 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1118                                                LIST_ICON_SIZE, LIST_ICON_SIZE)
1119             except:
1120                 pixbuf = default_pixbuf
1121     
1122             self.feedItems.append((pixbuf, markup, key))
1123
1124     def on_feedList_row_activated(self, treeview, path, column):
1125         model = treeview.get_model()
1126         iter = model.get_iter(path)
1127         key = model.get_value(iter, COLUMN_KEY)
1128         self.openFeed(key)
1129             
1130     def openFeed(self, key):
1131         try:
1132             self.feed_lock
1133         except:
1134             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1135             if key != None:
1136                 self.feed_lock = get_lock(key)
1137                 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1138                         self.listing.getFeedTitle(key), key, \
1139                         self.config, self.updateDbusHandler)
1140                 self.disp.connect("feed-closed", self.onFeedClosed)
1141         
1142
1143     def onFeedClosed(self, object, key):
1144         #self.listing.saveConfig()
1145         #del self.feed_lock
1146         gobject.idle_add(self.onFeedClosedTimeout)
1147         self.displayListing()
1148         #self.updateDbusHandler.ArticleCountUpdated()
1149         
1150     def onFeedClosedTimeout(self):
1151         self.listing.saveConfig()
1152         del self.feed_lock
1153         self.updateDbusHandler.ArticleCountUpdated()
1154      
1155     def run(self):
1156         self.window.connect("destroy", gtk.main_quit)
1157         gtk.main()
1158         self.listing.saveConfig()
1159         del self.app_lock
1160
1161     def prefsClosed(self, *widget):
1162         try:
1163             self.orientation.set_mode(self.config.getOrientation())
1164         except:
1165             pass
1166         self.displayListing()
1167         self.checkAutoUpdate()
1168
1169     def checkAutoUpdate(self, *widget):
1170         interval = int(self.config.getUpdateInterval()*3600000)
1171         if self.config.isAutoUpdateEnabled():
1172             if self.autoupdate == False:
1173                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1174                 self.autoupdate = interval
1175             elif not self.autoupdate == interval:
1176                 # If auto-update is enabled, but not at the right frequency
1177                 gobject.source_remove(self.autoupdateId)
1178                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1179                 self.autoupdate = interval
1180         else:
1181             if not self.autoupdate == False:
1182                 gobject.source_remove(self.autoupdateId)
1183                 self.autoupdate = False
1184
1185     def automaticUpdate(self, *widget):
1186         # Need to check for internet connection
1187         # If no internet connection, try again in 10 minutes:
1188         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1189         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1190         #from time import localtime, strftime
1191         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1192         #file.close()
1193         self.button_update_clicked(None, None)
1194         return True
1195     
1196     def stopUpdate(self):
1197         # Not implemented in the app (see update_feeds.py)
1198         try:
1199             self.downloadDialog.listOfKeys = []
1200         except:
1201             pass
1202     
1203     def getStatus(self):
1204         status = ""
1205         for key in self.listing.getListOfFeeds():
1206             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1207                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1208         if status == "":
1209             status = "No unread items"
1210         return status
1211
1212 if __name__ == "__main__":
1213     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1214     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1215     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1216     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1217     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1218     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1219     gobject.threads_init()
1220     if not isdir(CONFIGDIR):
1221         try:
1222             mkdir(CONFIGDIR)
1223         except:
1224             print "Error: Can't create configuration directory"
1225             from sys import exit
1226             exit(1)
1227     app = FeedingIt()
1228     app.run()