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