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