ce0369c7d568dbb0fa1f801e98956a236441d961
[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         self.view.connect('load-started', self.load_started)
408         self.view.connect('load-finished', self.load_finished)
409
410         #else:
411         #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
412         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
413         #else:
414         #    if not key == "ArchivedArticles":
415                 # Do not download images if the feed is "Archived Articles"
416         #        self.document.connect("request-url", self._signal_request_url)
417             
418         #    self.document.clear()
419         #    self.document.open_stream("text/html")
420         #    self.document.write_stream(self.text)
421         #    self.document.close_stream()
422         
423         menu = hildon.AppMenu()
424         # Create a button and add it to the menu
425         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
426         button.set_label("Allow Horizontal Scrolling")
427         button.connect("clicked", self.horiz_scrolling_button)
428         menu.append(button)
429         
430         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
431         button.set_label("Open in Browser")
432         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
433         menu.append(button)
434         
435         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
436         button.set_label("Add to Archived Articles")
437         button.connect("clicked", self.archive_button)
438         menu.append(button)
439         
440         self.set_app_menu(menu)
441         menu.show_all()
442         
443         #self.event_box = gtk.EventBox()
444         #self.event_box.add(self.pannable_article)
445         self.add(self.pannable_article)
446         
447         
448         self.pannable_article.show_all()
449
450         self.destroyId = self.connect("destroy", self.destroyWindow)
451         
452         self.view.connect("button_press_event", self.button_pressed)
453         self.gestureId = self.view.connect("button_release_event", self.button_released)
454         #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
455
456     def load_started(self, *widget):
457         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
458         
459     def load_finished(self, *widget):
460         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
461
462     def button_pressed(self, window, event):
463         #print event.x, event.y
464         self.coords = (event.x, event.y)
465         
466     def button_released(self, window, event):
467         x = self.coords[0] - event.x
468         y = self.coords[1] - event.y
469         
470         if (2*abs(y) < abs(x)):
471             if (x > 15):
472                 self.emit("article-previous", self.id)
473             elif (x<-15):
474                 self.emit("article-next", self.id)   
475         #print x, y
476         #print "Released"
477
478     #def gesture(self, widget, direction, startx, starty):
479     #    if (direction == 3):
480     #        self.emit("article-next", self.index)
481     #    if (direction == 2):
482     #        self.emit("article-previous", self.index)
483         #print startx, starty
484         #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
485
486     def destroyWindow(self, *args):
487         self.disconnect(self.destroyId)
488         self.emit("article-closed", self.id)
489         #self.imageDownloader.stopAll()
490         self.destroy()
491         
492     def horiz_scrolling_button(self, *widget):
493         self.pannable_article.disconnect(self.gestureId)
494         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
495         
496     def archive_button(self, *widget):
497         # Call the listing.addArchivedArticle
498         self.listing.addArchivedArticle(self.key, self.id)
499         
500     #def reloadArticle(self, *widget):
501     #    if threading.activeCount() > 1:
502             # Image thread are still running, come back in a bit
503     #        return True
504     #    else:
505     #        for (stream, imageThread) in self.images:
506     #            imageThread.join()
507     #            stream.write(imageThread.data)
508     #            stream.close()
509     #        return False
510     #    self.show_all()
511
512     def _signal_link_clicked(self, object, link):
513         bus = dbus.SessionBus()
514         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
515         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
516         iface.open_new_window(link)
517
518     #def _signal_request_url(self, object, url, stream):
519         #print url
520     #    self.imageDownloader.queueImage(url, stream)
521         #imageThread = GetImage(url)
522         #imageThread.start()
523         #self.images.append((stream, imageThread))
524
525
526 class DisplayFeed(hildon.StackableWindow):
527     def __init__(self, listing, feed, title, key, config):
528         hildon.StackableWindow.__init__(self)
529         self.listing = listing
530         self.feed = feed
531         self.feedTitle = title
532         self.set_title(title)
533         self.key=key
534         self.config = config
535         
536         self.downloadDialog = False
537         
538         self.listing.setCurrentlyDisplayedFeed(self.key)
539         
540         self.disp = False
541         
542         menu = hildon.AppMenu()
543         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
544         button.set_label("Update Feed")
545         button.connect("clicked", self.button_update_clicked)
546         menu.append(button)
547         
548         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
549         button.set_label("Mark All As Read")
550         button.connect("clicked", self.buttonReadAllClicked)
551         menu.append(button)
552         self.set_app_menu(menu)
553         menu.show_all()
554         
555         self.displayFeed()
556         
557         self.connect("destroy", self.destroyWindow)
558         
559     def destroyWindow(self, *args):
560         self.feed.saveUnread(CONFIGDIR)
561         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
562         self.emit("feed-closed", self.key)
563         self.destroy()
564         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
565         self.listing.closeCurrentlyDisplayedFeed()
566
567     def displayFeed(self):
568         self.vboxFeed = gtk.VBox(False, 10)
569         self.pannableFeed = hildon.PannableArea()
570         self.pannableFeed.add_with_viewport(self.vboxFeed)
571         self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
572         self.buttons = {}
573         for id in self.feed.getIds():
574             title = self.feed.getTitle(id)
575             esc_title = title.replace("<em>","").replace("</em>","").replace("&amp;","&").replace("&mdash;", "-").replace("&#8217;", "'")
576             button = gtk.Button(esc_title)
577             button.set_alignment(0,0)
578             label = button.child
579             if self.feed.isEntryRead(id):
580                 #label.modify_font(pango.FontDescription("sans 16"))
581                 label.modify_font(pango.FontDescription(self.config.getReadFont()))
582                 label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
583             else:
584                 #print self.listing.getFont() + " bold"
585                 label.modify_font(pango.FontDescription(self.config.getUnreadFont()))
586                 label.modify_fg(gtk.STATE_NORMAL, unread_color)
587             label.set_line_wrap(True)
588             
589             label.set_size_request(self.get_size()[0]-50, -1)
590             button.connect("clicked", self.button_clicked, id)
591             self.buttons[id] = button
592             
593             self.vboxFeed.pack_start(button, expand=False)
594
595         self.add(self.pannableFeed)
596         self.show_all()
597         
598     def clear(self):
599         self.pannableFeed.destroy()
600         #self.remove(self.pannableFeed)
601
602     def button_clicked(self, button, index, previous=False, next=False):
603         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
604         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
605         stack = hildon.WindowStack.get_default()
606         if previous:
607             tmp = stack.peek()
608             stack.pop_and_push(1, newDisp, tmp)
609             newDisp.show()
610             gobject.timeout_add(200, self.destroyArticle, tmp)
611             #print "previous"
612             self.disp = newDisp
613         elif next:
614             newDisp.show_all()
615             if type(self.disp).__name__ == "DisplayArticle":
616                 gobject.timeout_add(200, self.destroyArticle, self.disp)
617             self.disp = newDisp
618         else:
619             self.disp = newDisp
620             self.disp.show_all()
621         
622         self.ids = []
623         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
624         self.ids.append(self.disp.connect("article-next", self.nextArticle))
625         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
626
627     def destroyArticle(self, handle):
628         handle.destroyWindow()
629
630     def nextArticle(self, object, index):
631         label = self.buttons[index].child
632         label.modify_font(pango.FontDescription(self.config.getReadFont()))
633         label.modify_fg(gtk.STATE_NORMAL, read_color) #  gtk.gdk.color_parse("white"))
634         id = self.feed.getNextId(index)
635         self.button_clicked(object, id, next=True)
636
637     def previousArticle(self, object, index):
638         label = self.buttons[index].child
639         label.modify_font(pango.FontDescription(self.config.getReadFont()))
640         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
641         id = self.feed.getPreviousId(index)
642         self.button_clicked(object, id, previous=True)
643
644     def onArticleClosed(self, object, index):
645         label = self.buttons[index].child
646         label.modify_font(pango.FontDescription(self.config.getReadFont()))
647         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
648         self.buttons[index].show()
649
650     def button_update_clicked(self, button):
651         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
652         if not type(self.downloadDialog).__name__=="DownloadBar":
653             self.pannableFeed.destroy()
654             self.vbox = gtk.VBox(False, 10)
655             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
656             self.downloadDialog.connect("download-done", self.onDownloadsDone)
657             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
658             self.add(self.vbox)
659             self.show_all()
660             
661     def onDownloadsDone(self, *widget):
662         self.vbox.destroy()
663         self.feed = self.listing.getFeed(self.key)
664         self.displayFeed()
665         
666     def buttonReadAllClicked(self, button):
667         for index in self.feed.getIds():
668             self.feed.setEntryRead(index)
669             label = self.buttons[index].child
670             label.modify_font(pango.FontDescription(self.config.getReadFont()))
671             label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
672             self.buttons[index].show()
673
674
675 class FeedingIt:
676     def __init__(self):
677         # Init the windows
678         self.window = hildon.StackableWindow()
679         self.window.set_title("FeedingIt")
680         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
681         self.mainVbox = gtk.VBox(False,10)
682         self.pannableListing = gtk.Label("Loading...")
683         self.mainVbox.pack_start(self.pannableListing)
684         self.window.add(self.mainVbox)
685         self.window.show_all()
686         self.config = Config(self.window, CONFIGDIR+"config.ini")
687         gobject.idle_add(self.createWindow)
688         
689     def createWindow(self):
690         self.listing = Listing(CONFIGDIR)
691         
692         self.downloadDialog = False
693         self.orientation = FremantleRotation("FeedingIt", main_window=self.window, app=self)
694         self.orientation.set_mode(self.config.getOrientation())
695         
696         menu = hildon.AppMenu()
697         # Create a button and add it to the menu
698         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
699         button.set_label("Update All Feeds")
700         button.connect("clicked", self.button_update_clicked, "All")
701         menu.append(button)
702         
703         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
704         button.set_label("Mark All As Read")
705         button.connect("clicked", self.button_markAll)
706         menu.append(button)
707         
708         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
709         button.set_label("Organize Feeds")
710         button.connect("clicked", self.button_organize_clicked)
711         menu.append(button)
712
713         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
714         button.set_label("Preferences")
715         button.connect("clicked", self.button_preferences_clicked)
716         menu.append(button)
717        
718         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
719         button.set_label("Import Feeds")
720         button.connect("clicked", self.button_import_clicked)
721         menu.append(button)
722         
723         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
724         button.set_label("Export Feeds")
725         button.connect("clicked", self.button_export_clicked)
726         menu.append(button)
727         
728         self.window.set_app_menu(menu)
729         menu.show_all()
730         
731         self.feedWindow = hildon.StackableWindow()
732         self.articleWindow = hildon.StackableWindow()
733
734         self.displayListing()
735         self.autoupdate = False
736         self.checkAutoUpdate()
737         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
738         gobject.idle_add(self.enableDbus)
739         
740     def enableDbus(self):
741         self.dbusHandler = ServerObject(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.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
782             self.downloadDialog.connect("download-done", self.onDownloadsDone)
783             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
784             self.mainVbox.show_all()
785         #self.displayListing()
786
787     def onDownloadsDone(self, *widget):
788         self.downloadDialog.destroy()
789         self.downloadDialog = False
790         #self.displayListing()
791         self.refreshList()
792         self.dbusHandler.ArticleCountUpdated()
793
794     def button_preferences_clicked(self, button):
795         dialog = self.config.createDialog()
796         dialog.connect("destroy", self.prefsClosed)
797
798     def show_confirmation_note(self, parent, title):
799         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
800
801         retcode = gtk.Dialog.run(note)
802         note.destroy()
803         
804         if retcode == gtk.RESPONSE_OK:
805             return True
806         else:
807             return False
808         
809     def displayListing(self):
810         try:
811             self.mainVbox.remove(self.pannableListing)
812         except:
813             pass
814         self.vboxListing = gtk.VBox(False,10)
815         self.pannableListing = hildon.PannableArea()
816         self.pannableListing.add_with_viewport(self.vboxListing)
817
818         self.buttons = {}
819         list = self.listing.getListOfFeeds()[:]
820         #list.reverse()
821         for key in list:
822             #button = gtk.Button(item)
823             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
824             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
825                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
826             button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
827                             + str(unreadItems) + " Unread Items")
828             button.set_alignment(0,0,1,1)
829             button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
830             self.vboxListing.pack_start(button, expand=False)
831             self.buttons[key] = button
832      
833         self.mainVbox.pack_start(self.pannableListing)
834         self.window.show_all()
835         gobject.idle_add(self.refreshList)
836
837     def refreshList(self):
838         for key in self.listing.getListOfFeeds():
839             if self.buttons.has_key(key):
840                 button = self.buttons[key]
841                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
842                 button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
843                             + str(unreadItems) + " Unread Items")
844                 label = button.child.child.get_children()[0].get_children()[1]
845                 if unreadItems == 0:
846                     label.modify_fg(gtk.STATE_NORMAL, read_color)
847                 else:
848                     label.modify_fg(gtk.STATE_NORMAL, unread_color)
849             else:
850                 self.displayListing()
851                 break
852
853     def buttonFeedClicked(widget, button, self, window, key):
854         self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key, self.config)
855         self.disp.connect("feed-closed", self.onFeedClosed)
856
857     def onFeedClosed(self, object, key):
858         self.listing.saveConfig()
859         self.refreshList()
860         self.dbusHandler.ArticleCountUpdated()
861      
862     def run(self):
863         self.window.connect("destroy", gtk.main_quit)
864         gtk.main()
865         self.listing.saveConfig()
866
867     def prefsClosed(self, *widget):
868         self.orientation.set_mode(self.config.getOrientation())
869         self.checkAutoUpdate()
870
871     def checkAutoUpdate(self, *widget):
872         interval = int(self.config.getUpdateInterval()*3600000)
873         if self.config.isAutoUpdateEnabled():
874             if self.autoupdate == False:
875                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
876                 self.autoupdate = interval
877             elif not self.autoupdate == interval:
878                 # If auto-update is enabled, but not at the right frequency
879                 gobject.source_remove(self.autoupdateId)
880                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
881                 self.autoupdate = interval
882         else:
883             if not self.autoupdate == False:
884                 gobject.source_remove(self.autoupdateId)
885                 self.autoupdate = False
886
887     def automaticUpdate(self, *widget):
888         # Need to check for internet connection
889         # If no internet connection, try again in 10 minutes:
890         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
891         self.button_update_clicked(None, None)
892         return True
893     
894     def getStatus(self):
895         status = ""
896         for key in self.listing.getListOfFeeds():
897             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
898                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
899         if status == "":
900             status = "No unread items"
901         return status
902
903 if __name__ == "__main__":
904     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
905     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
906     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
907     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
908     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
909     gobject.threads_init()
910     if not isdir(CONFIGDIR):
911         try:
912             mkdir(CONFIGDIR)
913         except:
914             print "Error: Can't create configuration directory"
915             sys.exit(1)
916     app = FeedingIt()
917     app.run()