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