1 #!/usr/bin/env python2.5
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.
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.
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/>.
19 # ============================================================================
21 # Author : Yves Marcoz
23 # Description : Simple RSS Reader
24 # ============================================================================
34 from os.path import isfile, isdir
39 from portrait import FremantleRotation
42 from feedingitdbus import ServerObject
46 class AddWidgetWizard(hildon.WizardDialog):
48 def __init__(self, parent, urlIn):
50 self.notebook = gtk.Notebook()
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)
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)
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)
71 labelEnd = gtk.Label("Success")
73 self.notebook.append_page(labelEnd, None)
75 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
77 # Set a handler for "switch-page" signal
78 #self.notebook.connect("switch_page", self.on_page_switch, self)
80 # Set a function to decide if user can go to next page
81 self.set_forward_page_func(self.some_page_func)
86 return (self.nameEntry.get_text(), self.urlEntry.get_text())
88 def on_page_switch(self, notebook, page, num, dialog):
91 def some_page_func(self, nb, current, userdata):
92 # Validate data for 1st page
94 return len(self.nameEntry.get_text()) != 0
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")) )
103 class GetImage(threading.Thread):
104 def __init__(self, url):
105 threading.Thread.__init__(self)
109 f = urllib2.urlopen(self.url)
114 class Download(threading.Thread):
115 def __init__(self, listing, key):
116 threading.Thread.__init__(self)
117 self.listing = listing
121 self.listing.updateFeed(self.key)
124 class DownloadDialog():
125 def __init__(self, parent, listing, listOfKeys):
126 self.listOfKeys = listOfKeys[:]
127 self.listing = listing
128 self.total = len(self.listOfKeys)
132 self.progress = gtk.ProgressBar()
133 self.waitingWindow = hildon.Note("cancel", parent, "Downloading",
134 progressbar=self.progress)
135 self.progress.set_text("Downloading")
137 self.progress.set_fraction(self.fraction)
139 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
140 self.waitingWindow.show_all()
141 response = self.waitingWindow.run()
143 while threading.activeCount() > 1:
144 # Wait for current downloads to finish
146 self.waitingWindow.destroy()
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)
158 if len(self.listOfKeys)>0:
159 self.current = self.current+1
160 key = self.listOfKeys.pop()
161 download = Download(self.listing, key)
164 elif threading.activeCount() > 1:
167 self.waitingWindow.destroy()
172 class SortList(gtk.Dialog):
173 def __init__(self, parent, listing):
174 gtk.Dialog.__init__(self, "Organizer", parent)
175 self.listing = listing
177 self.vbox2 = gtk.VBox(False, 10)
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)
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)
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)
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)
204 self.hbox2.pack_end(self.vbox2, expand=False)
205 self.set_default_size(-1, 600)
206 self.vbox.pack_start(self.hbox2)
209 #self.connect("destroy", self.buttonDone)
211 def displayFeeds(self):
212 self.treeview.destroy()
213 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
214 self.treeview = gtk.TreeView()
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)
219 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
221 self.pannableArea.add(self.treeview)
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])
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()
242 def getSelectedItem(self):
243 (model, iter) = self.treeview.get_selection().get_selected()
246 return model.get_value(iter, 1)
248 def findIndex(self, key):
252 for row in self.treestore:
254 return (before, row.iter)
255 if key == list(row)[0]:
259 return (before, None)
261 def buttonUp(self, button):
262 key = self.getSelectedItem()
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)
270 #self.treeview.scroll_to_point(-1, y)
271 #self.pannableArea.set_placement(placement)
273 def buttonDown(self, button):
274 key = self.getSelectedItem()
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)
283 #placement = self.pannableArea.get_placement()
284 #self.displayFeeds(key)
285 #self.pannableArea.set_placement(placement)
287 def buttonDelete(self, button):
288 key = self.getSelectedItem()
290 self.listing.removeFeed(key)
293 def buttonDone(self, *args):
297 class DisplayArticle(hildon.StackableWindow):
298 def __init__(self, title, text, index):
299 hildon.StackableWindow.__init__(self)
302 self.set_title(title)
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()
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)
327 self.set_app_menu(menu)
330 self.add(self.pannable_article)
334 self.destroyId = self.connect("destroy", self.destroyWindow)
335 self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
337 def gesture(self, widget, direction, startx, starty):
339 self.emit("article-next", self.index)
341 self.emit("article-previous", self.index)
342 self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
344 def destroyWindow(self, *args):
345 self.emit("article-closed", self.index)
348 def reloadArticle(self, *widget):
349 if threading.activeCount() > 1:
350 # Image thread are still running, come back in a bit
353 for (stream, imageThread) in self.images:
355 stream.write(imageThread.data)
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)
366 def _signal_request_url(self, object, url, stream):
367 imageThread = GetImage(url)
369 self.images.append((stream, imageThread))
372 class DisplayFeed(hildon.StackableWindow):
373 def __init__(self, listing, feed, title, key):
374 hildon.StackableWindow.__init__(self)
375 self.listing = listing
377 self.feedTitle = title
378 self.set_title(title)
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)
386 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
387 button.set_label("Mark All As Read")
388 button.connect("clicked", self.buttonReadAllClicked)
390 self.set_app_menu(menu)
395 self.connect("destroy", self.destroyWindow)
397 def destroyWindow(self, *args):
398 self.emit("feed-closed", self.key)
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)
408 for index in range(self.feed.getNumberOfEntries()):
409 button = gtk.Button(self.feed.getTitle(index))
410 button.set_alignment(0,0)
412 if self.feed.isEntryRead(index):
413 label.modify_font(pango.FontDescription("sans 16"))
415 label.modify_font(pango.FontDescription("sans bold 16"))
416 label.set_line_wrap(True)
418 label.set_size_request(self.get_size()[0]-50, -1)
419 button.connect("clicked", self.button_clicked, index)
420 self.buttons.append(button)
422 self.vboxFeed.pack_start(button, expand=False)
425 self.add(self.pannableFeed)
429 self.remove(self.pannableFeed)
431 def button_clicked(self, button, index):
432 self.disp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), index)
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))
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)
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)
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()
455 def button_update_clicked(self, button):
456 disp = DownloadDialog(self, self.listing, [self.key,] )
457 #self.feed.updateFeed()
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()
471 self.listing = Listing()
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")
483 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
484 button.set_label("Add Feed")
485 button.connect("clicked", self.button_add_clicked)
488 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
489 button.set_label("Delete Feed")
490 button.connect("clicked", self.button_delete_clicked)
493 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
494 button.set_label("Organize Feeds")
495 button.connect("clicked", self.button_organize_clicked)
498 self.window.set_app_menu(menu)
501 self.feedWindow = hildon.StackableWindow()
502 self.articleWindow = hildon.StackableWindow()
504 self.displayListing()
506 def button_organize_clicked(self, button):
507 org = SortList(self.window, self.listing)
510 self.listing.saveConfig()
511 self.displayListing()
513 def button_add_clicked(self, button, urlIn="http://"):
514 wizard = AddWidgetWizard(self.window, urlIn)
517 (title, url) = wizard.getData()
518 if (not title == '') and (not url == ''):
519 self.listing.addFeed(title, url)
521 self.displayListing()
523 def button_update_clicked(self, button, key):
524 disp = DownloadDialog(self.window, self.listing, self.listing.getListOfFeeds() )
525 self.displayListing()
527 def button_delete_clicked(self, button):
528 self.pickerDialog = hildon.PickerDialog(self.window)
530 self.pickerDialog.set_selector(self.create_selector())
531 self.pickerDialog.show_all()
533 def create_selector(self):
534 selector = hildon.TouchSelector(text=True)
536 #selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_MULTIPLE)
538 selector.connect("changed", self.selection_changed)
540 for key in self.listing.getListOfFeeds():
541 title=self.listing.getFeedTitle(key)
542 selector.append_text(title)
543 self.mapping[title]=key
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()
556 self.displayListing()
558 def show_confirmation_note(self, parent, title):
559 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
561 retcode = gtk.Dialog.run(note)
564 if retcode == gtk.RESPONSE_OK:
569 def displayListing(self):
571 self.window.remove(self.pannableListing)
574 self.vboxListing = gtk.VBox(False,10)
575 self.pannableListing = hildon.PannableArea()
576 self.pannableListing.add_with_viewport(self.vboxListing)
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()
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)
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()
602 self.window.connect("destroy", gtk.main_quit)
604 self.listing.saveConfig()
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):
617 print "Error: Can't create configuration directory"
620 dbusHandler = ServerObject(app)