Starting opml support
[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.2.2
23 # Description : Simple RSS Reader
24 # ============================================================================
25
26 import gtk
27 import feedparser
28 import pango
29 import hildon
30 import gtkhtml2
31 import time
32 import dbus
33 import pickle
34 from os.path import isfile, isdir
35 from os import mkdir
36 import sys   
37 import urllib2
38 import gobject
39 from portrait import FremantleRotation
40 import threading
41 import thread
42 from feedingitdbus import ServerObject
43
44 from rss import *
45    
46 class AddWidgetWizard(hildon.WizardDialog):
47     
48     def __init__(self, parent, urlIn):
49         # Create a Notebook
50         self.notebook = gtk.Notebook()
51
52         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
53         self.nameEntry.set_placeholder("Enter Feed Name")
54         vbox = gtk.VBox(False,10)
55         label = gtk.Label("Enter Feed Name:")
56         vbox.pack_start(label)
57         vbox.pack_start(self.nameEntry)
58         self.notebook.append_page(vbox, None)
59         
60         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
61         self.urlEntry.set_placeholder("Enter a URL")
62         self.urlEntry.set_text(urlIn)
63         self.urlEntry.select_region(0,-1)
64         
65         vbox = gtk.VBox(False,10)
66         label = gtk.Label("Enter Feed URL:")
67         vbox.pack_start(label)
68         vbox.pack_start(self.urlEntry)
69         self.notebook.append_page(vbox, None)
70
71         labelEnd = gtk.Label("Success")
72         
73         self.notebook.append_page(labelEnd, None)      
74
75         hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
76    
77         # Set a handler for "switch-page" signal
78         #self.notebook.connect("switch_page", self.on_page_switch, self)
79    
80         # Set a function to decide if user can go to next page
81         self.set_forward_page_func(self.some_page_func)
82    
83         self.show_all()
84         
85     def getData(self):
86         return (self.nameEntry.get_text(), self.urlEntry.get_text())
87         
88     def on_page_switch(self, notebook, page, num, dialog):
89         return True
90    
91     def some_page_func(self, nb, current, userdata):
92         # Validate data for 1st page
93         if current == 0:
94             return len(self.nameEntry.get_text()) != 0
95         elif current == 1:
96             # Check the url is not null, and starts with http
97             return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
98         elif current != 2:
99             return False
100         else:
101             return True
102
103 class GetImage(threading.Thread):
104     def __init__(self, url):
105         threading.Thread.__init__(self)
106         self.url = url
107     
108     def run(self):
109         f = urllib2.urlopen(self.url)
110         self.data = f.read()
111         f.close()
112
113         
114 class Download(threading.Thread):
115     def __init__(self, listing, key):
116         threading.Thread.__init__(self)
117         self.listing = listing
118         self.key = key
119         
120     def run (self):
121         self.listing.updateFeed(self.key)
122
123         
124 class DownloadDialog():
125     def __init__(self, parent, listing, listOfKeys):
126         self.listOfKeys = listOfKeys[:]
127         self.listing = listing
128         self.total = len(self.listOfKeys)
129         self.current = 0            
130         
131         if self.total>0:
132             self.progress = gtk.ProgressBar()
133             self.waitingWindow = hildon.Note("cancel", parent, "Downloading",
134                                  progressbar=self.progress)
135             self.progress.set_text("Downloading")
136             self.fraction = 0
137             self.progress.set_fraction(self.fraction)
138             # Create a timeout
139             self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
140             self.waitingWindow.show_all()
141             response = self.waitingWindow.run()
142             self.listOfKeys = []
143             while threading.activeCount() > 1:
144                 # Wait for current downloads to finish
145                 time.sleep(0.5)
146             self.waitingWindow.destroy()
147         
148     def update_progress_bar(self):
149         #self.progress_bar.pulse()
150         if threading.activeCount() < 4:
151             x = threading.activeCount() - 1
152             k = len(self.listOfKeys)
153             fin = self.total - k - x
154             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
155             #print x, k, fin, fraction
156             self.progress.set_fraction(fraction)
157             
158             if len(self.listOfKeys)>0:
159                 self.current = self.current+1
160                 key = self.listOfKeys.pop()
161                 download = Download(self.listing, key)
162                 download.start()
163                 return True
164             elif threading.activeCount() > 1:
165                 return True
166             else:
167                 self.waitingWindow.destroy()
168                 return False 
169         return True
170     
171     
172 class SortList(gtk.Dialog):
173     def __init__(self, parent, listing):
174         gtk.Dialog.__init__(self, "Organizer",  parent)
175         self.listing = listing
176         
177         self.vbox2 = gtk.VBox(False, 10)
178         
179         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
180         button.set_label("Move Up")
181         button.connect("clicked", self.buttonUp)
182         self.vbox2.pack_start(button, expand=False, fill=False)
183         
184         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
185         button.set_label("Move Down")
186         button.connect("clicked", self.buttonDown)
187         self.vbox2.pack_start(button, expand=False, fill=False)
188         
189         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
190         button.set_label("Delete")
191         button.connect("clicked", self.buttonDelete)
192         self.vbox2.pack_start(button, expand=False, fill=False)
193         
194         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
195         #button.set_label("Done")
196         #button.connect("clicked", self.buttonDone)
197         #self.vbox.pack_start(button)
198         self.hbox2= gtk.HBox(False, 10)
199         self.pannableArea = hildon.PannableArea()
200         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
201         self.treeview = gtk.TreeView(self.treestore)
202         self.hbox2.pack_start(self.pannableArea, expand=True)
203         self.displayFeeds()
204         self.hbox2.pack_end(self.vbox2, expand=False)
205         self.set_default_size(-1, 600)
206         self.vbox.pack_start(self.hbox2)
207         
208         self.show_all()
209         #self.connect("destroy", self.buttonDone)
210         
211     def displayFeeds(self):
212         self.treeview.destroy()
213         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
214         self.treeview = gtk.TreeView()
215         
216         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
217         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
218         self.refreshList()
219         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
220
221         self.pannableArea.add(self.treeview)
222
223         #self.show_all()
224
225     def refreshList(self, selected=None, offset=0):
226         #x = self.treeview.get_visible_rect().x
227         rect = self.treeview.get_visible_rect()
228         y = rect.y+rect.height
229         #self.pannableArea.jump_to(-1, 0)
230         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
231         for key in self.listing.getListOfFeeds():
232             item = self.treestore.append([self.listing.getFeedTitle(key), key])
233             if key == selected:
234                 selectedItem = item
235         self.treeview.set_model(self.treestore)
236         if not selected == None:
237             self.treeview.get_selection().select_iter(selectedItem)
238             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
239             #self.pannableArea.jump_to(-1, y+offset)
240         self.pannableArea.show_all()
241
242     def getSelectedItem(self):
243         (model, iter) = self.treeview.get_selection().get_selected()
244         if not iter:
245             return None
246         return model.get_value(iter, 1)
247
248     def findIndex(self, key):
249         after = None
250         before = None
251         found = False
252         for row in self.treestore:
253             if found:
254                 return (before, row.iter)
255             if key == list(row)[0]:
256                 found = True
257             else:
258                 before = row.iter
259         return (before, None)
260
261     def buttonUp(self, button):
262         key  = self.getSelectedItem()
263         if not key == None:
264             self.listing.moveUp(key)
265             self.refreshList(key, -10)
266         #placement = self.pannableArea.get_placement()
267         #placement = self.treeview.get_visible_rect().y
268         #self.displayFeeds(key)
269         
270         #self.treeview.scroll_to_point(-1, y)
271         #self.pannableArea.set_placement(placement)
272
273     def buttonDown(self, button):
274         key = self.getSelectedItem()
275         if not key == None:
276             self.listing.moveDown(key)
277             #(before, after) = self.findIndex(key)
278             #self.treestore.move_after(iter, after)
279             #self.treeview.set_model(self.treestore)
280             #self.treeview.show_all()
281             self.refreshList(key, 10)
282             
283         #placement = self.pannableArea.get_placement()
284         #self.displayFeeds(key)
285         #self.pannableArea.set_placement(placement)
286
287     def buttonDelete(self, button):
288         key = self.getSelectedItem()
289         if not key == None:
290             self.listing.removeFeed(key)
291         self.refreshList()
292
293     def buttonDone(self, *args):
294         self.destroy()
295                
296
297 class DisplayArticle(hildon.StackableWindow):
298     def __init__(self, title, text, index):
299         hildon.StackableWindow.__init__(self)
300         self.index = index
301         self.text = text
302         self.set_title(title)
303         self.images = []
304         
305         # Init the article display    
306         self.view = gtkhtml2.View()
307         self.pannable_article = hildon.PannableArea()
308         self.pannable_article.add(self.view)
309         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
310         self.pannable_article.connect('horizontal-movement', self.gesture)
311         self.document = gtkhtml2.Document()
312         self.view.set_document(self.document)
313         self.document.connect("link_clicked", self._signal_link_clicked)
314         self.document.connect("request-url", self._signal_request_url)
315         self.document.clear()
316         self.document.open_stream("text/html")
317         self.document.write_stream(self.text)
318         self.document.close_stream()
319         
320         menu = hildon.AppMenu()
321         # Create a button and add it to the menu
322         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
323         button.set_label("Display Images")
324         button.connect("clicked", self.reloadArticle)
325         
326         menu.append(button)
327         self.set_app_menu(menu)
328         menu.show_all()
329         
330         self.add(self.pannable_article)
331         
332         self.show_all()
333         
334         self.destroyId = self.connect("destroy", self.destroyWindow)
335         self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
336         
337     def gesture(self, widget, direction, startx, starty):
338         if (direction == 3):
339             self.emit("article-next", self.index)
340         if (direction == 2):
341             self.emit("article-previous", self.index)
342         self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
343
344     def destroyWindow(self, *args):
345         self.emit("article-closed", self.index)
346         self.destroy()
347         
348     def reloadArticle(self, *widget):
349         if threading.activeCount() > 1:
350             # Image thread are still running, come back in a bit
351             return True
352         else:
353             for (stream, imageThread) in self.images:
354                 imageThread.join()
355                 stream.write(imageThread.data)
356                 stream.close()
357             return False
358         self.show_all()
359
360     def _signal_link_clicked(self, object, link):
361         bus = dbus.SystemBus()
362         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
363         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
364         iface.open_new_window(link)
365
366     def _signal_request_url(self, object, url, stream):
367         imageThread = GetImage(url)
368         imageThread.start()
369         self.images.append((stream, imageThread))
370
371
372 class DisplayFeed(hildon.StackableWindow):
373     def __init__(self, listing, feed, title, key):
374         hildon.StackableWindow.__init__(self)
375         self.listing = listing
376         self.feed = feed
377         self.feedTitle = title
378         self.set_title(title)
379         self.key=key
380         
381         menu = hildon.AppMenu()
382         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
383         button.set_label("Update Feed")
384         button.connect("clicked", self.button_update_clicked)
385         menu.append(button)
386         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
387         button.set_label("Mark All As Read")
388         button.connect("clicked", self.buttonReadAllClicked)
389         menu.append(button)
390         self.set_app_menu(menu)
391         menu.show_all()
392         
393         self.displayFeed()
394         
395         self.connect("destroy", self.destroyWindow)
396         
397     def destroyWindow(self, *args):
398         self.emit("feed-closed", self.key)
399         self.destroy()
400         self.feed.saveFeed()
401
402     def displayFeed(self):
403         self.vboxFeed = gtk.VBox(False, 10)
404         self.pannableFeed = hildon.PannableArea()
405         self.pannableFeed.add_with_viewport(self.vboxFeed)
406         self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
407         self.buttons = []
408         for index in range(self.feed.getNumberOfEntries()):
409             button = gtk.Button(self.feed.getTitle(index))
410             button.set_alignment(0,0)
411             label = button.child
412             if self.feed.isEntryRead(index):
413                 label.modify_font(pango.FontDescription("sans 16"))
414             else:
415                 label.modify_font(pango.FontDescription("sans bold 16"))
416             label.set_line_wrap(True)
417             
418             label.set_size_request(self.get_size()[0]-50, -1)
419             button.connect("clicked", self.button_clicked, index)
420             self.buttons.append(button)
421             
422             self.vboxFeed.pack_start(button, expand=False)           
423             index=index+1
424
425         self.add(self.pannableFeed)
426         self.show_all()
427         
428     def clear(self):
429         self.remove(self.pannableFeed)
430         
431     def button_clicked(self, button, index):
432         self.disp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), index)
433         self.ids = []
434         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
435         self.ids.append(self.disp.connect("article-next", self.nextArticle))
436         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
437
438     def nextArticle(self, object, index):
439         label = self.buttons[index].child
440         label.modify_font(pango.FontDescription("sans 16"))
441         index = (index+1) % self.feed.getNumberOfEntries()
442         self.button_clicked(object, index)
443
444     def previousArticle(self, object, index):
445         label = self.buttons[index].child
446         label.modify_font(pango.FontDescription("sans 16"))
447         index = (index-1) % self.feed.getNumberOfEntries()
448         self.button_clicked(object, index)
449
450     def onArticleClosed(self, object, index):
451         label = self.buttons[index].child
452         label.modify_font(pango.FontDescription("sans 16"))
453         self.buttons[index].show()
454
455     def button_update_clicked(self, button):
456         disp = DownloadDialog(self, self.listing, [self.key,] )       
457         #self.feed.updateFeed()
458         self.clear()
459         self.displayFeed()
460         
461     def buttonReadAllClicked(self, button):
462         for index in range(self.feed.getNumberOfEntries()):
463             self.feed.setEntryRead(index)
464             label = self.buttons[index].child
465             label.modify_font(pango.FontDescription("sans 16"))
466             self.buttons[index].show()
467
468
469 class FeedingIt:
470     def __init__(self):
471         self.listing = Listing()
472         
473         # Init the windows
474         self.window = hildon.StackableWindow()
475         self.window.set_title("FeedingIt")
476         FremantleRotation("FeedingIt", main_window=self.window)
477         menu = hildon.AppMenu()
478         # Create a button and add it to the menu
479         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
480         button.set_label("Update All Feeds")
481         button.connect("clicked", self.button_update_clicked, "All")
482         menu.append(button)
483         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
484         button.set_label("Add Feed")
485         button.connect("clicked", self.button_add_clicked)
486         menu.append(button)
487         
488         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
489         button.set_label("Delete Feed")
490         button.connect("clicked", self.button_delete_clicked)
491         menu.append(button)
492         
493         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
494         button.set_label("Organize Feeds")
495         button.connect("clicked", self.button_organize_clicked)
496         menu.append(button)
497         
498         self.window.set_app_menu(menu)
499         menu.show_all()
500         
501         self.feedWindow = hildon.StackableWindow()
502         self.articleWindow = hildon.StackableWindow()
503
504         self.displayListing() 
505         
506     def button_organize_clicked(self, button):
507         org = SortList(self.window, self.listing)
508         org.run()
509         org.destroy()
510         self.listing.saveConfig()
511         self.displayListing()
512         
513     def button_add_clicked(self, button, urlIn="http://"):
514         wizard = AddWidgetWizard(self.window, urlIn)
515         ret = wizard.run()
516         if ret == 2:
517             (title, url) = wizard.getData()
518             if (not title == '') and (not url == ''): 
519                self.listing.addFeed(title, url)
520         wizard.destroy()
521         self.displayListing()
522         
523     def button_update_clicked(self, button, key):
524         disp = DownloadDialog(self.window, self.listing, self.listing.getListOfFeeds() )           
525         self.displayListing()
526
527     def button_delete_clicked(self, button):
528         self.pickerDialog = hildon.PickerDialog(self.window)
529         #HildonPickerDialog
530         self.pickerDialog.set_selector(self.create_selector())
531         self.pickerDialog.show_all()
532         
533     def create_selector(self):
534         selector = hildon.TouchSelector(text=True)
535         # Selection multiple
536         #selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_MULTIPLE)
537         self.mapping = {}
538         selector.connect("changed", self.selection_changed)
539
540         for key in self.listing.getListOfFeeds():
541             title=self.listing.getFeedTitle(key)
542             selector.append_text(title)
543             self.mapping[title]=key
544
545         return selector
546
547     def selection_changed(self, widget, data):
548         current_selection = widget.get_current_text()
549         #print 'Current selection: %s' % current_selection
550         #print "To Delete: %s" % self.mapping[current_selection]
551         self.pickerDialog.destroy()
552         if self.show_confirmation_note(self.window, current_selection):
553             self.listing.removeFeed(self.mapping[current_selection])
554             self.listing.saveConfig()
555         del self.mapping
556         self.displayListing()
557
558     def show_confirmation_note(self, parent, title):
559         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
560
561         retcode = gtk.Dialog.run(note)
562         note.destroy()
563         
564         if retcode == gtk.RESPONSE_OK:
565             return True
566         else:
567             return False
568         
569     def displayListing(self):
570         try:
571             self.window.remove(self.pannableListing)
572         except:
573             pass
574         self.vboxListing = gtk.VBox(False,10)
575         self.pannableListing = hildon.PannableArea()
576         self.pannableListing.add_with_viewport(self.vboxListing)
577
578         self.buttons = {}
579         for key in self.listing.getListOfFeeds():
580             #button = gtk.Button(item)
581             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
582                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
583             button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
584                             + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
585             button.set_alignment(0,0,1,1)
586             button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
587             self.vboxListing.pack_start(button, expand=False)
588             self.buttons[key] = button
589         self.window.add(self.pannableListing)
590         self.window.show_all()
591
592     def buttonFeedClicked(widget, button, self, window, key):
593         disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key)
594         disp.connect("feed-closed", self.onFeedClosed)
595
596     def onFeedClosed(self, object, key):
597         self.buttons[key].set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
598                             + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
599         self.buttons[key].show()
600      
601     def run(self):
602         self.window.connect("destroy", gtk.main_quit)
603         gtk.main()
604         self.listing.saveConfig()
605
606
607 if __name__ == "__main__":
608     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
609     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
610     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
611     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
612     gobject.threads_init()
613     if not isdir(CONFIGDIR):
614         try:
615             mkdir(CONFIGDIR)
616         except:
617             print "Error: Can't create configuration directory"
618             sys.exit(1)
619     app = FeedingIt()
620     dbusHandler = ServerObject(app)
621     app.run()