0.6.1-1 Added dbus locking mechanism, and widget changes
[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 # Name        : FeedingIt.py
21 # Author      : Yves Marcoz
22 # Version     : 0.6.0
23 # Description : Simple RSS Reader
24 # ============================================================================
25
26 import gtk
27 from pango import FontDescription
28 import hildon
29 #import gtkhtml2
30 #try:
31 from webkit import WebView
32 #    has_webkit=True
33 #except:
34 #    import gtkhtml2
35 #    has_webkit=False
36 from os.path import isfile, isdir, exists
37 from os import mkdir, remove, stat
38 import gobject
39 from portrait import FremantleRotation
40 from threading import Thread, activeCount
41 from feedingitdbus import ServerObject
42 from updatedbus import UpdateServerObject, get_lock
43 from config import Config
44 from cgi import escape
45
46 from rss import Listing
47 from opml import GetOpmlData, ExportOpmlData
48
49 from socket import setdefaulttimeout
50 timeout = 5
51 setdefaulttimeout(timeout)
52 del timeout
53
54 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
55 unread_color = color_style.lookup_color('ActiveTextColor')
56 read_color = color_style.lookup_color('DefaultTextColor')
57 del color_style
58
59 CONFIGDIR="/home/user/.feedingit/"
60 LOCK = CONFIGDIR + "update.lock"
61
62 from re import sub
63 from htmlentitydefs import name2codepoint
64
65 ##
66 # Removes HTML or XML character references and entities from a text string.
67 #
68 # @param text The HTML (or XML) source text.
69 # @return The plain text, as a Unicode string, if necessary.
70 # http://effbot.org/zone/re-sub.htm#unescape-html
71 def unescape(text):
72     def fixup(m):
73         text = m.group(0)
74         if text[:2] == "&#":
75             # character reference
76             try:
77                 if text[:3] == "&#x":
78                     return unichr(int(text[3:-1], 16))
79                 else:
80                     return unichr(int(text[2:-1]))
81             except ValueError:
82                 pass
83         else:
84             # named entity
85             try:
86                 text = unichr(name2codepoint[text[1:-1]])
87             except KeyError:
88                 pass
89         return text # leave as is
90     return sub("&#?\w+;", fixup, text)
91
92
93 class AddWidgetWizard(hildon.WizardDialog):
94     
95     def __init__(self, parent, urlIn, titleIn=None):
96         # Create a Notebook
97         self.notebook = gtk.Notebook()
98
99         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
100         self.nameEntry.set_placeholder("Enter Feed Name")
101         vbox = gtk.VBox(False,10)
102         label = gtk.Label("Enter Feed Name:")
103         vbox.pack_start(label)
104         vbox.pack_start(self.nameEntry)
105         if not titleIn == None:
106             self.nameEntry.set_text(titleIn)
107         self.notebook.append_page(vbox, None)
108         
109         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
110         self.urlEntry.set_placeholder("Enter a URL")
111         self.urlEntry.set_text(urlIn)
112         self.urlEntry.select_region(0,-1)
113         
114         vbox = gtk.VBox(False,10)
115         label = gtk.Label("Enter Feed URL:")
116         vbox.pack_start(label)
117         vbox.pack_start(self.urlEntry)
118         self.notebook.append_page(vbox, None)
119
120         labelEnd = gtk.Label("Success")
121         
122         self.notebook.append_page(labelEnd, None)      
123
124         hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
125    
126         # Set a handler for "switch-page" signal
127         #self.notebook.connect("switch_page", self.on_page_switch, self)
128    
129         # Set a function to decide if user can go to next page
130         self.set_forward_page_func(self.some_page_func)
131    
132         self.show_all()
133         
134     def getData(self):
135         return (self.nameEntry.get_text(), self.urlEntry.get_text())
136         
137     def on_page_switch(self, notebook, page, num, dialog):
138         return True
139    
140     def some_page_func(self, nb, current, userdata):
141         # Validate data for 1st page
142         if current == 0:
143             return len(self.nameEntry.get_text()) != 0
144         elif current == 1:
145             # Check the url is not null, and starts with http
146             return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
147         elif current != 2:
148             return False
149         else:
150             return True
151         
152 class Download(Thread):
153     def __init__(self, listing, key, config):
154         Thread.__init__(self)
155         self.listing = listing
156         self.key = key
157         self.config = config
158         
159     def run (self):
160         (use_proxy, proxy) = self.config.getProxy()
161         key_lock = get_lock(self.key)
162         if key_lock != None:
163             if use_proxy:
164                 from urllib2 import install_opener, build_opener
165                 install_opener(build_opener(proxy))
166                 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
167             else:
168                 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
169         del key_lock
170
171         
172 class DownloadBar(gtk.ProgressBar):
173     def __init__(self, parent, listing, listOfKeys, config, single=False):
174         
175         update_lock = get_lock("update_lock")
176         if update_lock != None:
177             gtk.ProgressBar.__init__(self)
178             self.listOfKeys = listOfKeys[:]
179             self.listing = listing
180             self.total = len(self.listOfKeys)
181             self.config = config
182             self.current = 0
183             self.single = single
184
185             if self.total>0:
186                 self.set_text("Updating...")
187                 self.fraction = 0
188                 self.set_fraction(self.fraction)
189                 self.show_all()
190                 # Create a timeout
191                 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
192
193     def update_progress_bar(self):
194         #self.progress_bar.pulse()
195         if activeCount() < 4:
196             x = activeCount() - 1
197             k = len(self.listOfKeys)
198             fin = self.total - k - x
199             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
200             #print x, k, fin, fraction
201             self.set_fraction(fraction)
202
203             if len(self.listOfKeys)>0:
204                 self.current = self.current+1
205                 key = self.listOfKeys.pop()
206                 #if self.single == True:
207                     # Check if the feed is being displayed
208                 download = Download(self.listing, key, self.config)
209                 download.start()
210                 return True
211             elif activeCount() > 1:
212                 return True
213             else:
214                 #self.waitingWindow.destroy()
215                 #self.destroy()
216                 try:
217                     del self.update_lock
218                 except:
219                     pass
220                 self.emit("download-done", "success")
221                 return False 
222         return True
223     
224     
225 class SortList(gtk.Dialog):
226     def __init__(self, parent, listing):
227         gtk.Dialog.__init__(self, "Organizer",  parent)
228         self.listing = listing
229         
230         self.vbox2 = gtk.VBox(False, 10)
231         
232         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
233         button.set_label("Move Up")
234         button.connect("clicked", self.buttonUp)
235         self.vbox2.pack_start(button, expand=False, fill=False)
236         
237         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
238         button.set_label("Move Down")
239         button.connect("clicked", self.buttonDown)
240         self.vbox2.pack_start(button, expand=False, fill=False)
241
242         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
243         button.set_label("Add Feed")
244         button.connect("clicked", self.buttonAdd)
245         self.vbox2.pack_start(button, expand=False, fill=False)
246
247         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
248         button.set_label("Edit Feed")
249         button.connect("clicked", self.buttonEdit)
250         self.vbox2.pack_start(button, expand=False, fill=False)
251         
252         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
253         button.set_label("Delete")
254         button.connect("clicked", self.buttonDelete)
255         self.vbox2.pack_start(button, expand=False, fill=False)
256         
257         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
258         #button.set_label("Done")
259         #button.connect("clicked", self.buttonDone)
260         #self.vbox.pack_start(button)
261         self.hbox2= gtk.HBox(False, 10)
262         self.pannableArea = hildon.PannableArea()
263         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
264         self.treeview = gtk.TreeView(self.treestore)
265         self.hbox2.pack_start(self.pannableArea, expand=True)
266         self.displayFeeds()
267         self.hbox2.pack_end(self.vbox2, expand=False)
268         self.set_default_size(-1, 600)
269         self.vbox.pack_start(self.hbox2)
270         
271         self.show_all()
272         #self.connect("destroy", self.buttonDone)
273         
274     def displayFeeds(self):
275         self.treeview.destroy()
276         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
277         self.treeview = gtk.TreeView()
278         
279         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
280         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
281         self.refreshList()
282         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
283
284         self.pannableArea.add(self.treeview)
285
286         #self.show_all()
287
288     def refreshList(self, selected=None, offset=0):
289         #rect = self.treeview.get_visible_rect()
290         #y = rect.y+rect.height
291         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
292         for key in self.listing.getListOfFeeds():
293             item = self.treestore.append([self.listing.getFeedTitle(key), key])
294             if key == selected:
295                 selectedItem = item
296         self.treeview.set_model(self.treestore)
297         if not selected == None:
298             self.treeview.get_selection().select_iter(selectedItem)
299             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
300         self.pannableArea.show_all()
301
302     def getSelectedItem(self):
303         (model, iter) = self.treeview.get_selection().get_selected()
304         if not iter:
305             return None
306         return model.get_value(iter, 1)
307
308     def findIndex(self, key):
309         after = None
310         before = None
311         found = False
312         for row in self.treestore:
313             if found:
314                 return (before, row.iter)
315             if key == list(row)[0]:
316                 found = True
317             else:
318                 before = row.iter
319         return (before, None)
320
321     def buttonUp(self, button):
322         key  = self.getSelectedItem()
323         if not key == None:
324             self.listing.moveUp(key)
325             self.refreshList(key, -10)
326
327     def buttonDown(self, button):
328         key = self.getSelectedItem()
329         if not key == None:
330             self.listing.moveDown(key)
331             self.refreshList(key, 10)
332
333     def buttonDelete(self, button):
334         key = self.getSelectedItem()
335         if not key == None:
336             self.listing.removeFeed(key)
337         self.refreshList()
338
339     def buttonEdit(self, button):
340         key = self.getSelectedItem()
341         if not key == None:
342             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
343             ret = wizard.run()
344             if ret == 2:
345                 (title, url) = wizard.getData()
346                 if (not title == '') and (not url == ''):
347                     self.listing.editFeed(key, title, url)
348             wizard.destroy()
349         self.refreshList()
350
351     def buttonDone(self, *args):
352         self.destroy()
353         
354     def buttonAdd(self, button, urlIn="http://"):
355         wizard = AddWidgetWizard(self, urlIn)
356         ret = wizard.run()
357         if ret == 2:
358             (title, url) = wizard.getData()
359             if (not title == '') and (not url == ''): 
360                self.listing.addFeed(title, url)
361         wizard.destroy()
362         self.refreshList()
363                
364
365 class DisplayArticle(hildon.StackableWindow):
366     def __init__(self, feed, id, key, config, listing):
367         hildon.StackableWindow.__init__(self)
368         #self.imageDownloader = ImageDownloader()
369         self.feed = feed
370         self.listing=listing
371         self.key = key
372         self.id = id
373         #self.set_title(feed.getTitle(id))
374         self.set_title(self.listing.getFeedTitle(key))
375         self.config = config
376         
377         # Init the article display
378         #if self.config.getWebkitSupport():
379         self.view = WebView()
380             #self.view.set_editable(False)
381         #else:
382         #    import gtkhtml2
383         #    self.view = gtkhtml2.View()
384         #    self.document = gtkhtml2.Document()
385         #    self.view.set_document(self.document)
386         #    self.document.connect("link_clicked", self._signal_link_clicked)
387         self.pannable_article = hildon.PannableArea()
388         self.pannable_article.add(self.view)
389         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
390         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
391
392         #if self.config.getWebkitSupport():
393         contentLink = self.feed.getContentLink(self.id)
394         self.feed.setEntryRead(self.id)
395         #if key=="ArchivedArticles":
396         self.view.open("file://" + contentLink)
397         self.view.connect("motion-notify-event", lambda w,ev: True)
398         self.view.connect('load-started', self.load_started)
399         self.view.connect('load-finished', self.load_finished)
400
401         #else:
402         #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
403         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
404         #else:
405         #    if not key == "ArchivedArticles":
406                 # Do not download images if the feed is "Archived Articles"
407         #        self.document.connect("request-url", self._signal_request_url)
408             
409         #    self.document.clear()
410         #    self.document.open_stream("text/html")
411         #    self.document.write_stream(self.text)
412         #    self.document.close_stream()
413         
414         menu = hildon.AppMenu()
415         # Create a button and add it to the menu
416         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
417         button.set_label("Allow Horizontal Scrolling")
418         button.connect("clicked", self.horiz_scrolling_button)
419         menu.append(button)
420         
421         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
422         button.set_label("Open in Browser")
423         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
424         menu.append(button)
425         
426         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
427         button.set_label("Add to Archived Articles")
428         button.connect("clicked", self.archive_button)
429         menu.append(button)
430         
431         self.set_app_menu(menu)
432         menu.show_all()
433         
434         #self.event_box = gtk.EventBox()
435         #self.event_box.add(self.pannable_article)
436         self.add(self.pannable_article)
437         
438         
439         self.pannable_article.show_all()
440
441         self.destroyId = self.connect("destroy", self.destroyWindow)
442         
443         self.view.connect("button_press_event", self.button_pressed)
444         self.gestureId = self.view.connect("button_release_event", self.button_released)
445         #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
446
447     def load_started(self, *widget):
448         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
449         
450     def load_finished(self, *widget):
451         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
452
453     def button_pressed(self, window, event):
454         #print event.x, event.y
455         self.coords = (event.x, event.y)
456         
457     def button_released(self, window, event):
458         x = self.coords[0] - event.x
459         y = self.coords[1] - event.y
460         
461         if (2*abs(y) < abs(x)):
462             if (x > 15):
463                 self.emit("article-previous", self.id)
464             elif (x<-15):
465                 self.emit("article-next", self.id)   
466         #print x, y
467         #print "Released"
468
469     #def gesture(self, widget, direction, startx, starty):
470     #    if (direction == 3):
471     #        self.emit("article-next", self.index)
472     #    if (direction == 2):
473     #        self.emit("article-previous", self.index)
474         #print startx, starty
475         #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
476
477     def destroyWindow(self, *args):
478         self.disconnect(self.destroyId)
479         self.emit("article-closed", self.id)
480         #self.imageDownloader.stopAll()
481         self.destroy()
482         
483     def horiz_scrolling_button(self, *widget):
484         self.pannable_article.disconnect(self.gestureId)
485         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
486         
487     def archive_button(self, *widget):
488         # Call the listing.addArchivedArticle
489         self.listing.addArchivedArticle(self.key, self.id)
490         
491     #def reloadArticle(self, *widget):
492     #    if threading.activeCount() > 1:
493             # Image thread are still running, come back in a bit
494     #        return True
495     #    else:
496     #        for (stream, imageThread) in self.images:
497     #            imageThread.join()
498     #            stream.write(imageThread.data)
499     #            stream.close()
500     #        return False
501     #    self.show_all()
502
503     def _signal_link_clicked(self, object, link):
504         import dbus
505         bus = dbus.SessionBus()
506         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
507         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
508         iface.open_new_window(link)
509
510     #def _signal_request_url(self, object, url, stream):
511         #print url
512     #    self.imageDownloader.queueImage(url, stream)
513         #imageThread = GetImage(url)
514         #imageThread.start()
515         #self.images.append((stream, imageThread))
516
517
518 class DisplayFeed(hildon.StackableWindow):
519     def __init__(self, listing, feed, title, key, config):
520         hildon.StackableWindow.__init__(self)
521         self.listing = listing
522         self.feed = feed
523         self.feedTitle = title
524         self.set_title(title)
525         self.key=key
526         self.config = config
527         
528         self.downloadDialog = False
529         
530         self.listing.setCurrentlyDisplayedFeed(self.key)
531         
532         self.disp = False
533         
534         menu = hildon.AppMenu()
535         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
536         button.set_label("Update Feed")
537         button.connect("clicked", self.button_update_clicked)
538         menu.append(button)
539         
540         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
541         button.set_label("Mark All As Read")
542         button.connect("clicked", self.buttonReadAllClicked)
543         menu.append(button)
544         self.set_app_menu(menu)
545         menu.show_all()
546         
547         self.displayFeed()
548         
549         self.connect("destroy", self.destroyWindow)
550         
551     def destroyWindow(self, *args):
552         self.feed.saveUnread(CONFIGDIR)
553         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
554         self.emit("feed-closed", self.key)
555         self.destroy()
556         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
557         self.listing.closeCurrentlyDisplayedFeed()
558
559     def displayFeed(self):
560         self.vboxFeed = gtk.VBox(False, 10)
561         self.pannableFeed = hildon.PannableArea()
562         self.pannableFeed.add_with_viewport(self.vboxFeed)
563         self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
564         self.buttons = {}
565         for id in self.feed.getIds():
566             title = self.feed.getTitle(id)
567             esc_title = unescape(title)
568             #title.replace("<em>","").replace("</em>","").replace("&amp;","&").replace("&mdash;", "-").replace("&#8217;", "'")
569             button = gtk.Button(esc_title)
570             button.set_alignment(0,0)
571             label = button.child
572             if self.feed.isEntryRead(id):
573                 #label.modify_font(FontDescription("sans 16"))
574                 label.modify_font(FontDescription(self.config.getReadFont()))
575                 label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
576             else:
577                 #print self.listing.getFont() + " bold"
578                 label.modify_font(FontDescription(self.config.getUnreadFont()))
579                 label.modify_fg(gtk.STATE_NORMAL, unread_color)
580             label.set_line_wrap(True)
581             
582             label.set_size_request(self.get_size()[0]-50, -1)
583             button.connect("clicked", self.button_clicked, id)
584             self.buttons[id] = button
585             
586             self.vboxFeed.pack_start(button, expand=False)
587
588         self.add(self.pannableFeed)
589         self.show_all()
590         
591     def clear(self):
592         self.pannableFeed.destroy()
593         #self.remove(self.pannableFeed)
594
595     def button_clicked(self, button, index, previous=False, next=False):
596         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
597         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
598         stack = hildon.WindowStack.get_default()
599         if previous:
600             tmp = stack.peek()
601             stack.pop_and_push(1, newDisp, tmp)
602             newDisp.show()
603             gobject.timeout_add(200, self.destroyArticle, tmp)
604             #print "previous"
605             self.disp = newDisp
606         elif next:
607             newDisp.show_all()
608             if type(self.disp).__name__ == "DisplayArticle":
609                 gobject.timeout_add(200, self.destroyArticle, self.disp)
610             self.disp = newDisp
611         else:
612             self.disp = newDisp
613             self.disp.show_all()
614         
615         self.ids = []
616         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
617         self.ids.append(self.disp.connect("article-next", self.nextArticle))
618         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
619
620     def destroyArticle(self, handle):
621         handle.destroyWindow()
622
623     def nextArticle(self, object, index):
624         label = self.buttons[index].child
625         label.modify_font(FontDescription(self.config.getReadFont()))
626         label.modify_fg(gtk.STATE_NORMAL, read_color) #  gtk.gdk.color_parse("white"))
627         id = self.feed.getNextId(index)
628         self.button_clicked(object, id, next=True)
629
630     def previousArticle(self, object, index):
631         label = self.buttons[index].child
632         label.modify_font(FontDescription(self.config.getReadFont()))
633         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
634         id = self.feed.getPreviousId(index)
635         self.button_clicked(object, id, previous=True)
636
637     def onArticleClosed(self, object, index):
638         label = self.buttons[index].child
639         label.modify_font(FontDescription(self.config.getReadFont()))
640         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
641         self.buttons[index].show()
642
643     def button_update_clicked(self, button):
644         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
645         if not type(self.downloadDialog).__name__=="DownloadBar":
646             self.pannableFeed.destroy()
647             self.vbox = gtk.VBox(False, 10)
648             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
649             self.downloadDialog.connect("download-done", self.onDownloadsDone)
650             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
651             self.add(self.vbox)
652             self.show_all()
653             
654     def onDownloadsDone(self, *widget):
655         self.vbox.destroy()
656         self.feed = self.listing.getFeed(self.key)
657         self.displayFeed()
658         self.updateDbusHandler.ArticleCountUpdated()
659         
660     def buttonReadAllClicked(self, button):
661         for index in self.feed.getIds():
662             self.feed.setEntryRead(index)
663             label = self.buttons[index].child
664             label.modify_font(FontDescription(self.config.getReadFont()))
665             label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
666             self.buttons[index].show()
667
668
669 class FeedingIt:
670     def __init__(self):
671         # Init the windows
672         self.window = hildon.StackableWindow()
673         self.window.set_title("FeedingIt")
674         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
675         self.mainVbox = gtk.VBox(False,10)
676         self.pannableListing = gtk.Label("Loading...")
677         self.mainVbox.pack_start(self.pannableListing)
678         self.window.add(self.mainVbox)
679         self.window.show_all()
680         self.config = Config(self.window, CONFIGDIR+"config.ini")
681         gobject.idle_add(self.createWindow)
682         
683     def createWindow(self):
684         self.app_lock = get_lock("app_lock")
685         if self.app_lock == None:
686             self.pannableListing.set_label("Update in progress, please wait.")
687             gobject.timeout_add_seconds(3, self.createWindow)
688             return False
689         self.listing = Listing(CONFIGDIR)
690         
691         self.downloadDialog = False
692         self.orientation = FremantleRotation("FeedingIt", main_window=self.window, app=self)
693         self.orientation.set_mode(self.config.getOrientation())
694         
695         menu = hildon.AppMenu()
696         # Create a button and add it to the menu
697         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
698         button.set_label("Update All Feeds")
699         button.connect("clicked", self.button_update_clicked, "All")
700         menu.append(button)
701         
702         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
703         button.set_label("Mark All As Read")
704         button.connect("clicked", self.button_markAll)
705         menu.append(button)
706         
707         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
708         button.set_label("Organize Feeds")
709         button.connect("clicked", self.button_organize_clicked)
710         menu.append(button)
711
712         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
713         button.set_label("Preferences")
714         button.connect("clicked", self.button_preferences_clicked)
715         menu.append(button)
716        
717         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
718         button.set_label("Import Feeds")
719         button.connect("clicked", self.button_import_clicked)
720         menu.append(button)
721         
722         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
723         button.set_label("Export Feeds")
724         button.connect("clicked", self.button_export_clicked)
725         menu.append(button)
726         
727         self.window.set_app_menu(menu)
728         menu.show_all()
729         
730         self.feedWindow = hildon.StackableWindow()
731         self.articleWindow = hildon.StackableWindow()
732
733         self.displayListing()
734         self.autoupdate = False
735         self.checkAutoUpdate()
736         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
737         gobject.idle_add(self.enableDbus)
738         
739     def enableDbus(self):
740         self.dbusHandler = ServerObject(self)
741         self.updateDbusHandler = UpdateServerObject(self)
742
743     def button_markAll(self, button):
744         for key in self.listing.getListOfFeeds():
745             feed = self.listing.getFeed(key)
746             for id in feed.getIds():
747                 feed.setEntryRead(id)
748             feed.saveUnread(CONFIGDIR)
749             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
750         self.refreshList()
751
752     def button_export_clicked(self, button):
753         opml = ExportOpmlData(self.window, self.listing)
754         
755     def button_import_clicked(self, button):
756         opml = GetOpmlData(self.window)
757         feeds = opml.getData()
758         for (title, url) in feeds:
759             self.listing.addFeed(title, url)
760         self.displayListing()
761
762     def addFeed(self, urlIn="http://"):
763         wizard = AddWidgetWizard(self.window, urlIn)
764         ret = wizard.run()
765         if ret == 2:
766             (title, url) = wizard.getData()
767             if (not title == '') and (not url == ''): 
768                self.listing.addFeed(title, url)
769         wizard.destroy()
770         self.displayListing()
771
772     def button_organize_clicked(self, button):
773         org = SortList(self.window, self.listing)
774         org.run()
775         org.destroy()
776         self.listing.saveConfig()
777         self.displayListing()
778         
779     def button_update_clicked(self, button, key):
780         if not type(self.downloadDialog).__name__=="DownloadBar":
781             self.updateDbusHandler.UpdateStarted()
782             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
783             self.downloadDialog.connect("download-done", self.onDownloadsDone)
784             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
785             self.mainVbox.show_all()
786         #self.displayListing()
787
788     def onDownloadsDone(self, *widget):
789         self.downloadDialog.destroy()
790         self.downloadDialog = False
791         #self.displayListing()
792         self.refreshList()
793         self.updateDbusHandler.UpdateFinished()
794         self.updateDbusHandler.ArticleCountUpdated()
795
796     def button_preferences_clicked(self, button):
797         dialog = self.config.createDialog()
798         dialog.connect("destroy", self.prefsClosed)
799
800     def show_confirmation_note(self, parent, title):
801         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
802
803         retcode = gtk.Dialog.run(note)
804         note.destroy()
805         
806         if retcode == gtk.RESPONSE_OK:
807             return True
808         else:
809             return False
810         
811     def displayListing(self):
812         try:
813             self.mainVbox.remove(self.pannableListing)
814         except:
815             pass
816         self.vboxListing = gtk.VBox(False,10)
817         self.pannableListing = hildon.PannableArea()
818         self.pannableListing.add_with_viewport(self.vboxListing)
819
820         self.buttons = {}
821         list = self.listing.getListOfFeeds()[:]
822         #list.reverse()
823         for key in list:
824             #button = gtk.Button(item)
825             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
826             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
827                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
828             button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
829                             + str(unreadItems) + " Unread Items")
830             button.set_alignment(0,0,1,1)
831             button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
832             self.vboxListing.pack_start(button, expand=False)
833             self.buttons[key] = button
834      
835         self.mainVbox.pack_start(self.pannableListing)
836         self.window.show_all()
837         gobject.idle_add(self.refreshList)
838
839     def refreshList(self):
840         for key in self.listing.getListOfFeeds():
841             if self.buttons.has_key(key):
842                 button = self.buttons[key]
843                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
844                 button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
845                             + str(unreadItems) + " Unread Items")
846                 label = button.child.child.get_children()[0].get_children()[1]
847                 if unreadItems == 0:
848                     label.modify_fg(gtk.STATE_NORMAL, read_color)
849                 else:
850                     label.modify_fg(gtk.STATE_NORMAL, unread_color)
851             else:
852                 self.displayListing()
853                 break
854
855     def buttonFeedClicked(widget, button, self, window, key):
856         try:
857             self.feed_lock
858         except:
859             # If feed_lock doesn't exist, we can open the feed, else we do nothing
860             self.feed_lock = get_lock(key)
861             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key, self.config)
862             self.disp.connect("feed-closed", self.onFeedClosed)
863
864     def onFeedClosed(self, object, key):
865         self.listing.saveConfig()
866         del self.feed_lock
867         self.refreshList()
868         self.updateDbusHandler.ArticleCountUpdated()
869      
870     def run(self):
871         self.window.connect("destroy", gtk.main_quit)
872         gtk.main()
873         self.listing.saveConfig()
874         del self.app_lock
875
876     def prefsClosed(self, *widget):
877         self.orientation.set_mode(self.config.getOrientation())
878         self.checkAutoUpdate()
879
880     def checkAutoUpdate(self, *widget):
881         interval = int(self.config.getUpdateInterval()*3600000)
882         if self.config.isAutoUpdateEnabled():
883             if self.autoupdate == False:
884                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
885                 self.autoupdate = interval
886             elif not self.autoupdate == interval:
887                 # If auto-update is enabled, but not at the right frequency
888                 gobject.source_remove(self.autoupdateId)
889                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
890                 self.autoupdate = interval
891         else:
892             if not self.autoupdate == False:
893                 gobject.source_remove(self.autoupdateId)
894                 self.autoupdate = False
895
896     def automaticUpdate(self, *widget):
897         # Need to check for internet connection
898         # If no internet connection, try again in 10 minutes:
899         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
900         self.button_update_clicked(None, None)
901         return True
902     
903     def stopUpdate(self):
904         # Not implemented in the app (see update_feeds.py)
905         try:
906             self.downloadDialog.listOfKeys = []
907         except:
908             pass
909     
910     def getStatus(self):
911         status = ""
912         for key in self.listing.getListOfFeeds():
913             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
914                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
915         if status == "":
916             status = "No unread items"
917         return status
918
919 if __name__ == "__main__":
920     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
921     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
922     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
923     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
924     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
925     gobject.threads_init()
926     if not isdir(CONFIGDIR):
927         try:
928             mkdir(CONFIGDIR)
929         except:
930             print "Error: Can't create configuration directory"
931             from sys import exit
932             exit(1)
933     app = FeedingIt()
934     app.run()