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