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
45 from opml import GetOpmlData, ExportOpmlData
47 class AddWidgetWizard(hildon.WizardDialog):
49 def __init__(self, parent, urlIn):
51 self.notebook = gtk.Notebook()
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)
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)
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)
72 labelEnd = gtk.Label("Success")
74 self.notebook.append_page(labelEnd, None)
76 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
78 # Set a handler for "switch-page" signal
79 #self.notebook.connect("switch_page", self.on_page_switch, self)
81 # Set a function to decide if user can go to next page
82 self.set_forward_page_func(self.some_page_func)
87 return (self.nameEntry.get_text(), self.urlEntry.get_text())
89 def on_page_switch(self, notebook, page, num, dialog):
92 def some_page_func(self, nb, current, userdata):
93 # Validate data for 1st page
95 return len(self.nameEntry.get_text()) != 0
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")) )
104 class GetImage(threading.Thread):
105 def __init__(self, url):
106 threading.Thread.__init__(self)
110 f = urllib2.urlopen(self.url)
115 class Download(threading.Thread):
116 def __init__(self, listing, key):
117 threading.Thread.__init__(self)
118 self.listing = listing
122 self.listing.updateFeed(self.key)
125 class DownloadDialog():
126 def __init__(self, parent, listing, listOfKeys):
127 self.listOfKeys = listOfKeys[:]
128 self.listing = listing
129 self.total = len(self.listOfKeys)
133 self.progress = gtk.ProgressBar()
134 self.waitingWindow = hildon.Note("cancel", parent, "Downloading",
135 progressbar=self.progress)
136 self.progress.set_text("Downloading")
138 self.progress.set_fraction(self.fraction)
140 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
141 self.waitingWindow.show_all()
142 response = self.waitingWindow.run()
144 while threading.activeCount() > 1:
145 # Wait for current downloads to finish
147 self.waitingWindow.destroy()
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)
159 if len(self.listOfKeys)>0:
160 self.current = self.current+1
161 key = self.listOfKeys.pop()
162 download = Download(self.listing, key)
165 elif threading.activeCount() > 1:
168 self.waitingWindow.destroy()
173 class SortList(gtk.Dialog):
174 def __init__(self, parent, listing):
175 gtk.Dialog.__init__(self, "Organizer", parent)
176 self.listing = listing
178 self.vbox2 = gtk.VBox(False, 10)
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)
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)
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)
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)
205 self.hbox2.pack_end(self.vbox2, expand=False)
206 self.set_default_size(-1, 600)
207 self.vbox.pack_start(self.hbox2)
210 #self.connect("destroy", self.buttonDone)
212 def displayFeeds(self):
213 self.treeview.destroy()
214 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
215 self.treeview = gtk.TreeView()
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)
220 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
222 self.pannableArea.add(self.treeview)
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])
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()
240 def getSelectedItem(self):
241 (model, iter) = self.treeview.get_selection().get_selected()
244 return model.get_value(iter, 1)
246 def findIndex(self, key):
250 for row in self.treestore:
252 return (before, row.iter)
253 if key == list(row)[0]:
257 return (before, None)
259 def buttonUp(self, button):
260 key = self.getSelectedItem()
262 self.listing.moveUp(key)
263 self.refreshList(key, -10)
265 def buttonDown(self, button):
266 key = self.getSelectedItem()
268 self.listing.moveDown(key)
269 self.refreshList(key, 10)
271 def buttonDelete(self, button):
272 key = self.getSelectedItem()
274 self.listing.removeFeed(key)
277 def buttonDone(self, *args):
281 class DisplayArticle(hildon.StackableWindow):
282 def __init__(self, title, text, index):
283 hildon.StackableWindow.__init__(self)
286 self.set_title(title)
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()
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)
311 self.set_app_menu(menu)
314 self.add(self.pannable_article)
318 self.destroyId = self.connect("destroy", self.destroyWindow)
319 self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
321 def gesture(self, widget, direction, startx, starty):
323 self.emit("article-next", self.index)
325 self.emit("article-previous", self.index)
326 self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
328 def destroyWindow(self, *args):
329 self.emit("article-closed", self.index)
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)
336 def reloadArticle(self, *widget):
337 if threading.activeCount() > 1:
338 # Image thread are still running, come back in a bit
341 for (stream, imageThread) in self.images:
343 stream.write(imageThread.data)
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)
354 def _signal_request_url(self, object, url, stream):
355 imageThread = GetImage(url)
357 self.images.append((stream, imageThread))
360 class DisplayFeed(hildon.StackableWindow):
361 def __init__(self, listing, feed, title, key):
362 hildon.StackableWindow.__init__(self)
363 self.listing = listing
365 self.feedTitle = title
366 self.set_title(title)
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)
374 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
375 button.set_label("Mark All As Read")
376 button.connect("clicked", self.buttonReadAllClicked)
378 self.set_app_menu(menu)
383 self.connect("destroy", self.destroyWindow)
385 def destroyWindow(self, *args):
386 self.emit("feed-closed", self.key)
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)
396 for index in range(self.feed.getNumberOfEntries()):
397 button = gtk.Button(self.feed.getTitle(index))
398 button.set_alignment(0,0)
400 if self.feed.isEntryRead(index):
401 #label.modify_font(pango.FontDescription("sans 16"))
402 label.modify_font(pango.FontDescription(self.listing.getReadFont()))
404 #print self.listing.getFont() + " bold"
405 label.modify_font(pango.FontDescription(self.listing.getUnreadFont()))
406 #label.modify_font(pango.FontDescription("sans bold 23"))
408 label.set_line_wrap(True)
410 label.set_size_request(self.get_size()[0]-50, -1)
411 button.connect("clicked", self.button_clicked, index)
412 self.buttons.append(button)
414 self.vboxFeed.pack_start(button, expand=False)
417 self.add(self.pannableFeed)
421 self.remove(self.pannableFeed)
423 def button_clicked(self, button, index):
424 self.disp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), index)
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))
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)
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)
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()
447 def button_update_clicked(self, button):
448 disp = DownloadDialog(self, self.listing, [self.key,] )
449 #self.feed.updateFeed()
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()
463 self.listing = Listing()
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")
475 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
476 button.set_label("Add Feed")
477 button.connect("clicked", self.button_add_clicked)
480 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
481 button.set_label("Organize Feeds")
482 button.connect("clicked", self.button_organize_clicked)
485 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
486 button.set_label("Listing Font Size")
487 button.connect("clicked", self.button_font_clicked)
490 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
491 button.set_label("Import Feeds")
492 button.connect("clicked", self.button_import_clicked)
495 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
496 button.set_label("Export Feeds")
497 button.connect("clicked", self.button_export_clicked)
500 self.window.set_app_menu(menu)
503 self.feedWindow = hildon.StackableWindow()
504 self.articleWindow = hildon.StackableWindow()
506 self.displayListing()
508 def button_export_clicked(self, button):
509 opml = ExportOpmlData(self.window, self.listing)
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()
518 def button_organize_clicked(self, button):
519 org = SortList(self.window, self.listing)
522 self.listing.saveConfig()
523 self.displayListing()
525 def button_add_clicked(self, button, urlIn="http://"):
526 wizard = AddWidgetWizard(self.window, urlIn)
529 (title, url) = wizard.getData()
530 if (not title == '') and (not url == ''):
531 self.listing.addFeed(title, url)
533 self.displayListing()
535 def button_update_clicked(self, button, key):
536 disp = DownloadDialog(self.window, self.listing, self.listing.getListOfFeeds() )
537 self.displayListing()
539 def button_font_clicked(self, button):
540 self.pickerDialog = hildon.PickerDialog(self.window)
542 selector = self.create_selector()
543 self.pickerDialog.set_selector(selector)
544 self.pickerDialog.show_all()
546 def create_selector(self):
547 selector = hildon.TouchSelector(text=True)
549 #selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_MULTIPLE)
552 current_size = self.listing.getFontSize()
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)
559 selector.connect("changed", self.selection_changed)
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()
568 def show_confirmation_note(self, parent, title):
569 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
571 retcode = gtk.Dialog.run(note)
574 if retcode == gtk.RESPONSE_OK:
579 def displayListing(self):
581 self.window.remove(self.pannableListing)
584 self.vboxListing = gtk.VBox(False,10)
585 self.pannableListing = hildon.PannableArea()
586 self.pannableListing.add_with_viewport(self.vboxListing)
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()
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)
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()
612 self.window.connect("destroy", gtk.main_quit)
614 self.listing.saveConfig()
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):
627 print "Error: Can't create configuration directory"
630 dbusHandler = ServerObject(app)