Version 0.1.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.1.3
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 DisplayArticle(hildon.StackableWindow):
160     def __init__(self, title, text, index):
161         hildon.StackableWindow.__init__(self)
162         self.index = index
163         self.text = text
164         self.set_title(title)
165
166         # Init the article display    
167         self.view = gtkhtml2.View()
168         self.pannable_article = hildon.PannableArea()
169         self.pannable_article.add(self.view)
170         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
171         self.document = gtkhtml2.Document()
172         self.view.set_document(self.document)
173         
174         self.document.clear()
175         self.document.open_stream("text/html")
176         self.document.write_stream(self.text)
177         self.document.close_stream()
178         
179         menu = hildon.AppMenu()
180         # Create a button and add it to the menu
181         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
182         button.set_label("Display Images")
183         button.connect("clicked", self.reloadArticle)
184         menu.append(button)
185         self.set_app_menu(menu)
186         menu.show_all()
187         
188         self.add(self.pannable_article)
189         
190         self.show_all()
191         
192         self.document.connect("link_clicked", self._signal_link_clicked)
193         self.document.connect("request-url", self._signal_request_url)
194         self.connect("destroy", self.destroyWindow)
195         self.timeout_handler_id = gobject.timeout_add(200, self.reloadArticle)
196         
197     def destroyWindow(self, *args):
198         self.emit("article-closed", self.index)
199         self.destroy()
200         
201     def reloadArticle(self, *widget):
202         self.document.open_stream("text/html")
203         self.document.write_stream(self.text)
204         self.document.close_stream()
205
206     def _signal_link_clicked(self, object, link):
207         bus = dbus.SystemBus()
208         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
209         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
210         #webbrowser.open(link)
211         iface.open_new_window(link)
212
213     def _signal_request_url(self, object, url, stream):
214         f = urllib2.urlopen(url)
215         stream.write(f.read())
216         stream.close()
217
218
219 class DisplayFeed(hildon.StackableWindow):
220     def __init__(self, listing, feed, title, key):
221         hildon.StackableWindow.__init__(self)
222         self.listing = listing
223         self.feed = feed
224         self.feedTitle = title
225         self.set_title(title)
226         self.key=key
227         
228         menu = hildon.AppMenu()
229         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
230         button.set_label("Update Feed")
231         button.connect("clicked", self.button_update_clicked)
232         menu.append(button)
233         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
234         button.set_label("Mark All As Read")
235         button.connect("clicked", self.buttonReadAllClicked)
236         menu.append(button)
237         self.set_app_menu(menu)
238         menu.show_all()
239         
240         self.displayFeed()
241         
242         self.connect("destroy", self.destroyWindow)
243         
244     def destroyWindow(self, *args):
245         self.feed.saveFeed()
246         self.emit("feed-closed", self.key)
247         self.destroy()
248
249     def displayFeed(self):
250         self.vboxFeed = gtk.VBox(False, 10)
251         self.pannableFeed = hildon.PannableArea()
252         self.pannableFeed.add_with_viewport(self.vboxFeed)
253         self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
254         self.buttons = []
255         for index in range(self.feed.getNumberOfEntries()):
256             button = gtk.Button(self.feed.getTitle(index))
257             button.set_alignment(0,0)
258             label = button.child
259             if self.feed.isEntryRead(index):
260                 label.modify_font(pango.FontDescription("sans 16"))
261             else:
262                 label.modify_font(pango.FontDescription("sans bold 16"))
263             label.set_line_wrap(True)
264             
265             label.set_size_request(self.get_size()[0]-50, -1)
266             button.connect("clicked", self.button_clicked, index)
267             self.buttons.append(button)
268             
269             self.vboxFeed.pack_start(button, expand=False)           
270             index=index+1
271
272         self.add(self.pannableFeed)
273         self.show_all()
274         
275     def clear(self):
276         self.remove(self.pannableFeed)
277         
278     def button_clicked(self, button, index):
279         disp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), index)
280         disp.connect("article-closed", self.onArticleClosed)
281         
282     def onArticleClosed(self, object, index):
283         label = self.buttons[index].child
284         label.modify_font(pango.FontDescription("sans 16"))
285         self.buttons[index].show()
286
287     def button_update_clicked(self, button):
288         disp = DownloadDialog(self, self.listing, [self.key,] )       
289         #self.feed.updateFeed()
290         self.clear()
291         self.displayFeed()
292         
293     def buttonReadAllClicked(self, button):
294         for index in range(self.feed.getNumberOfEntries()):
295             self.feed.setEntryRead(index)
296             label = self.buttons[index].child
297             label.modify_font(pango.FontDescription("sans 16"))
298             self.buttons[index].show()
299
300
301 class FeedingIt:
302     def __init__(self):
303         self.listing = Listing()
304         
305         # Init the windows
306         self.window = hildon.StackableWindow()
307         self.window.set_title("FeedingIt")
308         FremantleRotation("FeedingIt", main_window=self.window)
309         menu = hildon.AppMenu()
310         # Create a button and add it to the menu
311         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
312         button.set_label("Update All Feeds")
313         button.connect("clicked", self.button_update_clicked, "All")
314         menu.append(button)
315         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
316         button.set_label("Add Feed")
317         button.connect("clicked", self.button_add_clicked)
318         menu.append(button)
319         
320         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
321         button.set_label("Delete Feed")
322         button.connect("clicked", self.button_delete_clicked)
323         menu.append(button)
324         
325         self.window.set_app_menu(menu)
326         menu.show_all()
327         
328         self.feedWindow = hildon.StackableWindow()
329         self.articleWindow = hildon.StackableWindow()
330
331         self.displayListing() 
332         
333     def button_add_clicked(self, button, urlIn="http://"):
334         wizard = AddWidgetWizard(self.window, urlIn)
335         ret = wizard.run()
336         if ret == 2:
337             (title, url) = wizard.getData()
338             if (not title == '') and (not url == ''): 
339                self.listing.addFeed(title, url)
340         wizard.destroy()
341         self.displayListing()
342         
343     def button_update_clicked(self, button, key):
344         disp = DownloadDialog(self.window, self.listing, self.listing.getListOfFeeds() )           
345         self.displayListing()
346
347     def button_delete_clicked(self, button):
348         self.pickerDialog = hildon.PickerDialog(self.window)
349         #HildonPickerDialog
350         self.pickerDialog.set_selector(self.create_selector())
351         self.pickerDialog.show_all()
352         
353     def create_selector(self):
354         selector = hildon.TouchSelector(text=True)
355         # Selection multiple
356         #selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_MULTIPLE)
357         self.mapping = {}
358         selector.connect("changed", self.selection_changed)
359
360         for key in self.listing.getListOfFeeds():
361             title=self.listing.getFeedTitle(key)
362             selector.append_text(title)
363             self.mapping[title]=key
364
365         return selector
366
367     def selection_changed(self, widget, data):
368         current_selection = widget.get_current_text()
369         #print 'Current selection: %s' % current_selection
370         #print "To Delete: %s" % self.mapping[current_selection]
371         self.pickerDialog.destroy()
372         if self.show_confirmation_note(self.window, current_selection):
373             self.listing.removeFeed(self.mapping[current_selection])
374             
375         del self.mapping
376         self.displayListing()
377
378     def show_confirmation_note(self, parent, title):
379         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
380
381         retcode = gtk.Dialog.run(note)
382         note.destroy()
383         
384         if retcode == gtk.RESPONSE_OK:
385             return True
386         else:
387             return False
388         
389     def displayListing(self):
390         try:
391             self.window.remove(self.pannableListing)
392         except:
393             pass
394         self.vboxListing = gtk.VBox(False,10)
395         self.pannableListing = hildon.PannableArea()
396         self.pannableListing.add_with_viewport(self.vboxListing)
397
398         self.buttons = {}
399         for key in self.listing.getListOfFeeds():
400             #button = gtk.Button(item)
401             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
402                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
403             button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
404                             + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
405             button.set_alignment(0,0,1,1)
406             button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
407             self.vboxListing.pack_start(button, expand=False)
408             self.buttons[key] = button
409         self.window.add(self.pannableListing)
410         self.window.show_all()
411     
412     def buttonFeedClicked(widget, button, self, window, key):
413         disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key)
414         disp.connect("feed-closed", self.onFeedClosed)
415         
416     def onFeedClosed(self, object, key):
417         self.buttons[key].set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key) + " / " 
418                             + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items")
419         self.buttons[key].show()
420      
421     def run(self):
422         self.window.connect("destroy", gtk.main_quit)
423         gtk.main()
424
425
426 if __name__ == "__main__":
427     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
428     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
429     gobject.threads_init()
430     if not isdir(CONFIGDIR):
431         try:
432             mkdir(CONFIGDIR)
433         except:
434             print "Error: Can't create configuration directory"
435             sys.exit(1)
436     app = FeedingIt()
437     dbusHandler = ServerObject(app)
438     app.run()