Subscription manager window (with OPML AppMenu)
[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 __appname__ = 'FeedingIt'
21 __author__  = 'Yves Marcoz'
22 __version__ = '0.6.2'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
25
26 import gtk
27 from pango import FontDescription
28 import pango
29 import hildon
30 #import gtkhtml2
31 #try:
32 from webkit import WebView
33 #    has_webkit=True
34 #except:
35 #    import gtkhtml2
36 #    has_webkit=False
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
39 import gobject
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from threading import Thread, activeCount
43 from feedingitdbus import ServerObject
44 from updatedbus import UpdateServerObject, get_lock
45 from config import Config
46 from cgi import escape
47
48 from rss import Listing
49 from opml import GetOpmlData, ExportOpmlData
50
51 from urllib2 import install_opener, build_opener
52
53 from socket import setdefaulttimeout
54 timeout = 5
55 setdefaulttimeout(timeout)
56 del timeout
57
58 LIST_ICON_SIZE = 32
59 LIST_ICON_BORDER = 10
60
61 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
62 ABOUT_ICON = 'feedingit'
63 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
64 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
65 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
66 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
67
68 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
69 unread_color = color_style.lookup_color('ActiveTextColor')
70 read_color = color_style.lookup_color('DefaultTextColor')
71 del color_style
72
73 CONFIGDIR="/home/user/.feedingit/"
74 LOCK = CONFIGDIR + "update.lock"
75
76 from re import sub
77 from htmlentitydefs import name2codepoint
78
79 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
80
81 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
82
83 import style
84
85 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
86 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
87
88 # Build the markup template for the Maemo 5 text style
89 head_font = style.get_font_desc('SystemFont')
90 sub_font = style.get_font_desc('SmallSystemFont')
91
92 head_color = style.get_color('ButtonTextColor')
93 sub_color = style.get_color('DefaultTextColor')
94 active_color = style.get_color('ActiveTextColor')
95
96 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
97 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
98
99 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
100 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
101
102 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
103 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
104
105 entry_active_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), active_color.to_string())
106 entry_active_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), active_color.to_string())
107
108 FEED_TEMPLATE = '\n'.join((head, normal_sub))
109 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
110
111 ENTRY_TEMPLATE = entry_head
112 ENTRY_TEMPLATE_UNREAD = entry_active_head
113
114 ##
115 # Removes HTML or XML character references and entities from a text string.
116 #
117 # @param text The HTML (or XML) source text.
118 # @return The plain text, as a Unicode string, if necessary.
119 # http://effbot.org/zone/re-sub.htm#unescape-html
120 def unescape(text):
121     def fixup(m):
122         text = m.group(0)
123         if text[:2] == "&#":
124             # character reference
125             try:
126                 if text[:3] == "&#x":
127                     return unichr(int(text[3:-1], 16))
128                 else:
129                     return unichr(int(text[2:-1]))
130             except ValueError:
131                 pass
132         else:
133             # named entity
134             try:
135                 text = unichr(name2codepoint[text[1:-1]])
136             except KeyError:
137                 pass
138         return text # leave as is
139     return sub("&#?\w+;", fixup, text)
140
141
142 class AddWidgetWizard(hildon.WizardDialog):
143     
144     def __init__(self, parent, urlIn, titleIn=None):
145         # Create a Notebook
146         self.notebook = gtk.Notebook()
147
148         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
149         self.nameEntry.set_placeholder("Enter Feed Name")
150         vbox = gtk.VBox(False,10)
151         label = gtk.Label("Enter Feed Name:")
152         vbox.pack_start(label)
153         vbox.pack_start(self.nameEntry)
154         if not titleIn == None:
155             self.nameEntry.set_text(titleIn)
156         self.notebook.append_page(vbox, None)
157         
158         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
159         self.urlEntry.set_placeholder("Enter a URL")
160         self.urlEntry.set_text(urlIn)
161         self.urlEntry.select_region(0,-1)
162         
163         vbox = gtk.VBox(False,10)
164         label = gtk.Label("Enter Feed URL:")
165         vbox.pack_start(label)
166         vbox.pack_start(self.urlEntry)
167         self.notebook.append_page(vbox, None)
168
169         labelEnd = gtk.Label("Success")
170         
171         self.notebook.append_page(labelEnd, None)      
172
173         hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
174    
175         # Set a handler for "switch-page" signal
176         #self.notebook.connect("switch_page", self.on_page_switch, self)
177    
178         # Set a function to decide if user can go to next page
179         self.set_forward_page_func(self.some_page_func)
180    
181         self.show_all()
182         
183     def getData(self):
184         return (self.nameEntry.get_text(), self.urlEntry.get_text())
185         
186     def on_page_switch(self, notebook, page, num, dialog):
187         return True
188    
189     def some_page_func(self, nb, current, userdata):
190         # Validate data for 1st page
191         if current == 0:
192             return len(self.nameEntry.get_text()) != 0
193         elif current == 1:
194             # Check the url is not null, and starts with http
195             return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
196         elif current != 2:
197             return False
198         else:
199             return True
200         
201 class Download(Thread):
202     def __init__(self, listing, key, config):
203         Thread.__init__(self)
204         self.listing = listing
205         self.key = key
206         self.config = config
207         
208     def run (self):
209         (use_proxy, proxy) = self.config.getProxy()
210         key_lock = get_lock(self.key)
211         if key_lock != None:
212             if use_proxy:
213                 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
214             else:
215                 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
216         del key_lock
217
218         
219 class DownloadBar(gtk.ProgressBar):
220     def __init__(self, parent, listing, listOfKeys, config, single=False):
221         
222         update_lock = get_lock("update_lock")
223         if update_lock != None:
224             gtk.ProgressBar.__init__(self)
225             self.listOfKeys = listOfKeys[:]
226             self.listing = listing
227             self.total = len(self.listOfKeys)
228             self.config = config
229             self.current = 0
230             self.single = single
231             (use_proxy, proxy) = self.config.getProxy()
232             if use_proxy:
233                 opener = build_opener(proxy)
234                 opener.addheaders = [('User-agent', USER_AGENT)]
235                 install_opener(opener)
236             else:
237                 opener = build_opener()
238                 opener.addheaders = [('User-agent', USER_AGENT)]
239                 install_opener(opener)
240
241             if self.total>0:
242                 self.set_text("Updating...")
243                 self.fraction = 0
244                 self.set_fraction(self.fraction)
245                 self.show_all()
246                 # Create a timeout
247                 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
248
249     def update_progress_bar(self):
250         #self.progress_bar.pulse()
251         if activeCount() < 4:
252             x = activeCount() - 1
253             k = len(self.listOfKeys)
254             fin = self.total - k - x
255             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
256             #print x, k, fin, fraction
257             self.set_fraction(fraction)
258
259             if len(self.listOfKeys)>0:
260                 self.current = self.current+1
261                 key = self.listOfKeys.pop()
262                 #if self.single == True:
263                     # Check if the feed is being displayed
264                 download = Download(self.listing, key, self.config)
265                 download.start()
266                 return True
267             elif activeCount() > 1:
268                 return True
269             else:
270                 #self.waitingWindow.destroy()
271                 #self.destroy()
272                 try:
273                     del self.update_lock
274                 except:
275                     pass
276                 self.emit("download-done", "success")
277                 return False 
278         return True
279     
280     
281 class SortList(hildon.StackableWindow):
282     def __init__(self, parent, listing, feedingit, after_closing):
283         hildon.StackableWindow.__init__(self)
284         self.set_transient_for(parent)
285         self.set_title('Subscriptions')
286         self.listing = listing
287         self.feedingit = feedingit
288         self.after_closing = after_closing
289         self.connect('destroy', lambda w: self.after_closing())
290         self.vbox2 = gtk.VBox(False, 2)
291
292         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
293         button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
294         button.connect("clicked", self.buttonUp)
295         self.vbox2.pack_start(button, expand=False, fill=False)
296
297         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
298         button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
299         button.connect("clicked", self.buttonDown)
300         self.vbox2.pack_start(button, expand=False, fill=False)
301
302         self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
303
304         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
305         button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
306         button.connect("clicked", self.buttonAdd)
307         self.vbox2.pack_start(button, expand=False, fill=False)
308
309         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
310         button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
311         button.connect("clicked", self.buttonEdit)
312         self.vbox2.pack_start(button, expand=False, fill=False)
313
314         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
315         button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
316         button.connect("clicked", self.buttonDelete)
317         self.vbox2.pack_start(button, expand=False, fill=False)
318
319         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
320         #button.set_label("Done")
321         #button.connect("clicked", self.buttonDone)
322         #self.vbox.pack_start(button)
323         self.hbox2= gtk.HBox(False, 10)
324         self.pannableArea = hildon.PannableArea()
325         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
326         self.treeview = gtk.TreeView(self.treestore)
327         self.hbox2.pack_start(self.pannableArea, expand=True)
328         self.displayFeeds()
329         self.hbox2.pack_end(self.vbox2, expand=False)
330         self.set_default_size(-1, 600)
331         self.add(self.hbox2)
332
333         menu = hildon.AppMenu()
334         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
335         button.set_label("Import from OPML")
336         button.connect("clicked", self.feedingit.button_import_clicked)
337         menu.append(button)
338
339         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
340         button.set_label("Export to OPML")
341         button.connect("clicked", self.feedingit.button_export_clicked)
342         menu.append(button)
343         self.set_app_menu(menu)
344         menu.show_all()
345         
346         self.show_all()
347         #self.connect("destroy", self.buttonDone)
348         
349     def displayFeeds(self):
350         self.treeview.destroy()
351         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
352         self.treeview = gtk.TreeView()
353         
354         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
355         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
356         self.refreshList()
357         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
358
359         self.pannableArea.add(self.treeview)
360
361         #self.show_all()
362
363     def refreshList(self, selected=None, offset=0):
364         #rect = self.treeview.get_visible_rect()
365         #y = rect.y+rect.height
366         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
367         for key in self.listing.getListOfFeeds():
368             item = self.treestore.append([self.listing.getFeedTitle(key), key])
369             if key == selected:
370                 selectedItem = item
371         self.treeview.set_model(self.treestore)
372         if not selected == None:
373             self.treeview.get_selection().select_iter(selectedItem)
374             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
375         self.pannableArea.show_all()
376
377     def getSelectedItem(self):
378         (model, iter) = self.treeview.get_selection().get_selected()
379         if not iter:
380             return None
381         return model.get_value(iter, 1)
382
383     def findIndex(self, key):
384         after = None
385         before = None
386         found = False
387         for row in self.treestore:
388             if found:
389                 return (before, row.iter)
390             if key == list(row)[0]:
391                 found = True
392             else:
393                 before = row.iter
394         return (before, None)
395
396     def buttonUp(self, button):
397         key  = self.getSelectedItem()
398         if not key == None:
399             self.listing.moveUp(key)
400             self.refreshList(key, -10)
401
402     def buttonDown(self, button):
403         key = self.getSelectedItem()
404         if not key == None:
405             self.listing.moveDown(key)
406             self.refreshList(key, 10)
407
408     def buttonDelete(self, button):
409         key = self.getSelectedItem()
410         if not key == None:
411             self.listing.removeFeed(key)
412         self.refreshList()
413
414     def buttonEdit(self, button):
415         key = self.getSelectedItem()
416         if not key == None:
417             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
418             ret = wizard.run()
419             if ret == 2:
420                 (title, url) = wizard.getData()
421                 if (not title == '') and (not url == ''):
422                     self.listing.editFeed(key, title, url)
423             wizard.destroy()
424         self.refreshList()
425
426     def buttonDone(self, *args):
427         self.destroy()
428         
429     def buttonAdd(self, button, urlIn="http://"):
430         wizard = AddWidgetWizard(self, urlIn)
431         ret = wizard.run()
432         if ret == 2:
433             (title, url) = wizard.getData()
434             if (not title == '') and (not url == ''): 
435                self.listing.addFeed(title, url)
436         wizard.destroy()
437         self.refreshList()
438                
439
440 class DisplayArticle(hildon.StackableWindow):
441     def __init__(self, feed, id, key, config, listing):
442         hildon.StackableWindow.__init__(self)
443         #self.imageDownloader = ImageDownloader()
444         self.feed = feed
445         self.listing=listing
446         self.key = key
447         self.id = id
448         #self.set_title(feed.getTitle(id))
449         self.set_title(self.listing.getFeedTitle(key))
450         self.config = config
451         self.set_for_removal = False
452         
453         # Init the article display
454         #if self.config.getWebkitSupport():
455         self.view = WebView()
456             #self.view.set_editable(False)
457         #else:
458         #    import gtkhtml2
459         #    self.view = gtkhtml2.View()
460         #    self.document = gtkhtml2.Document()
461         #    self.view.set_document(self.document)
462         #    self.document.connect("link_clicked", self._signal_link_clicked)
463         self.pannable_article = hildon.PannableArea()
464         self.pannable_article.add(self.view)
465         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
466         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
467
468         #if self.config.getWebkitSupport():
469         contentLink = self.feed.getContentLink(self.id)
470         self.feed.setEntryRead(self.id)
471         #if key=="ArchivedArticles":
472         if contentLink.startswith("/home/user/"):
473             self.view.open("file://" + contentLink)
474         else:
475             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
476         self.view.connect("motion-notify-event", lambda w,ev: True)
477         self.view.connect('load-started', self.load_started)
478         self.view.connect('load-finished', self.load_finished)
479
480         #else:
481         #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
482         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
483         #else:
484         #    if not key == "ArchivedArticles":
485                 # Do not download images if the feed is "Archived Articles"
486         #        self.document.connect("request-url", self._signal_request_url)
487             
488         #    self.document.clear()
489         #    self.document.open_stream("text/html")
490         #    self.document.write_stream(self.text)
491         #    self.document.close_stream()
492         
493         menu = hildon.AppMenu()
494         # Create a button and add it to the menu
495         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
496         button.set_label("Allow Horizontal Scrolling")
497         button.connect("clicked", self.horiz_scrolling_button)
498         menu.append(button)
499         
500         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
501         button.set_label("Open in Browser")
502         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
503         menu.append(button)
504         
505         if key == "ArchivedArticles":
506             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
507             button.set_label("Remove from Archived Articles")
508             button.connect("clicked", self.remove_archive_button)
509         else:
510             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
511             button.set_label("Add to Archived Articles")
512             button.connect("clicked", self.archive_button)
513         menu.append(button)
514         
515         self.set_app_menu(menu)
516         menu.show_all()
517         
518         #self.event_box = gtk.EventBox()
519         #self.event_box.add(self.pannable_article)
520         self.add(self.pannable_article)
521         
522         
523         self.pannable_article.show_all()
524
525         self.destroyId = self.connect("destroy", self.destroyWindow)
526         
527         self.view.connect("button_press_event", self.button_pressed)
528         self.gestureId = self.view.connect("button_release_event", self.button_released)
529         #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
530
531     def load_started(self, *widget):
532         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
533         
534     def load_finished(self, *widget):
535         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
536
537     def button_pressed(self, window, event):
538         #print event.x, event.y
539         self.coords = (event.x, event.y)
540         
541     def button_released(self, window, event):
542         x = self.coords[0] - event.x
543         y = self.coords[1] - event.y
544         
545         if (2*abs(y) < abs(x)):
546             if (x > 15):
547                 self.emit("article-previous", self.id)
548             elif (x<-15):
549                 self.emit("article-next", self.id)   
550         #print x, y
551         #print "Released"
552
553     #def gesture(self, widget, direction, startx, starty):
554     #    if (direction == 3):
555     #        self.emit("article-next", self.index)
556     #    if (direction == 2):
557     #        self.emit("article-previous", self.index)
558         #print startx, starty
559         #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
560
561     def destroyWindow(self, *args):
562         self.disconnect(self.destroyId)
563         if self.set_for_removal:
564             self.emit("article-deleted", self.id)
565         else:
566             self.emit("article-closed", self.id)
567         #self.imageDownloader.stopAll()
568         self.destroy()
569         
570     def horiz_scrolling_button(self, *widget):
571         self.pannable_article.disconnect(self.gestureId)
572         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
573         
574     def archive_button(self, *widget):
575         # Call the listing.addArchivedArticle
576         self.listing.addArchivedArticle(self.key, self.id)
577         
578     def remove_archive_button(self, *widget):
579         self.set_for_removal = True
580         
581     #def reloadArticle(self, *widget):
582     #    if threading.activeCount() > 1:
583             # Image thread are still running, come back in a bit
584     #        return True
585     #    else:
586     #        for (stream, imageThread) in self.images:
587     #            imageThread.join()
588     #            stream.write(imageThread.data)
589     #            stream.close()
590     #        return False
591     #    self.show_all()
592
593     def _signal_link_clicked(self, object, link):
594         import dbus
595         bus = dbus.SessionBus()
596         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
597         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
598         iface.open_new_window(link)
599
600     #def _signal_request_url(self, object, url, stream):
601         #print url
602     #    self.imageDownloader.queueImage(url, stream)
603         #imageThread = GetImage(url)
604         #imageThread.start()
605         #self.images.append((stream, imageThread))
606
607
608 class DisplayFeed(hildon.StackableWindow):
609     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
610         hildon.StackableWindow.__init__(self)
611         self.listing = listing
612         self.feed = feed
613         self.feedTitle = title
614         self.set_title(title)
615         self.key=key
616         self.config = config
617         self.updateDbusHandler = updateDbusHandler
618         
619         self.downloadDialog = False
620         
621         #self.listing.setCurrentlyDisplayedFeed(self.key)
622         
623         self.disp = False
624         
625         menu = hildon.AppMenu()
626         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
627         button.set_label("Update Feed")
628         button.connect("clicked", self.button_update_clicked)
629         menu.append(button)
630         
631         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
632         button.set_label("Mark All As Read")
633         button.connect("clicked", self.buttonReadAllClicked)
634         menu.append(button)
635         
636         if key=="ArchivedArticles":
637             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
638             button.set_label("Purge Read Articles")
639             button.connect("clicked", self.buttonPurgeArticles)
640             menu.append(button)
641         
642         self.set_app_menu(menu)
643         menu.show_all()
644         
645         self.displayFeed()
646         
647         self.connect('configure-event', self.on_configure_event)
648         self.connect("destroy", self.destroyWindow)
649
650     def on_configure_event(self, window, event):
651         if getattr(self, 'markup_renderer', None) is None:
652             return
653
654         # Fix up the column width for wrapping the text when the window is
655         # resized (i.e. orientation changed)
656         self.markup_renderer.set_property('wrap-width', event.width-10)
657
658     def destroyWindow(self, *args):
659         #self.feed.saveUnread(CONFIGDIR)
660         gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
661         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
662         self.emit("feed-closed", self.key)
663         self.destroy()
664         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
665         #self.listing.closeCurrentlyDisplayedFeed()
666
667     def fix_title(self, title):
668         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
669
670     def displayFeed(self):
671         self.pannableFeed = hildon.PannableArea()
672
673         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
674
675         self.feedItems = gtk.ListStore(str, str)
676         #self.feedList = gtk.TreeView(self.feedItems)
677         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
678         selection = self.feedList.get_selection()
679         selection.set_mode(gtk.SELECTION_NONE)
680         #selection.connect("changed", lambda w: True)
681         
682         self.feedList.set_model(self.feedItems)
683         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
684
685         
686         self.feedList.set_hover_selection(False)
687         #self.feedList.set_property('enable-grid-lines', True)
688         #self.feedList.set_property('hildon-mode', 1)
689         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
690         
691         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
692
693         vbox= gtk.VBox(False, 10)
694         vbox.pack_start(self.feedList)
695         
696         self.pannableFeed.add_with_viewport(vbox)
697
698         self.markup_renderer = gtk.CellRendererText()
699         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
700         self.markup_renderer.set_property('wrap-width', 780)
701         self.markup_renderer.set_property('ypad', 5)
702         self.markup_renderer.set_property('xpad', 5)
703         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
704                 markup=FEED_COLUMN_MARKUP)
705         self.feedList.append_column(markup_column)
706
707         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
708         hideReadArticles = self.config.getHideReadArticles()
709         hasArticle = False
710         for id in self.feed.getIds():
711             isRead = False
712             try:
713                 isRead = self.feed.isEntryRead(id)
714             except:
715                 pass
716             if not ( isRead and hideReadArticles ):
717             #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
718                 #title = self.feed.getTitle(id)
719                 title = self.fix_title(self.feed.getTitle(id))
720     
721                 #if self.feed.isEntryRead(id):
722                 if isRead:
723                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
724                 else:
725                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
726     
727                 self.feedItems.append((markup, id))
728                 hasArticle = True
729         if hasArticle:
730             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
731         else:
732             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
733             self.feedItems.append((markup, ""))
734
735         self.add(self.pannableFeed)
736         self.show_all()
737
738     def clear(self):
739         self.pannableFeed.destroy()
740         #self.remove(self.pannableFeed)
741
742     def on_feedList_row_activated(self, treeview, path): #, column):
743         selection = self.feedList.get_selection()
744         selection.set_mode(gtk.SELECTION_SINGLE)
745         self.feedList.get_selection().select_path(path)
746         model = treeview.get_model()
747         iter = model.get_iter(path)
748         key = model.get_value(iter, FEED_COLUMN_KEY)
749         # Emulate legacy "button_clicked" call via treeview
750         gobject.idle_add(self.button_clicked, treeview, key)
751         #return True
752
753     def button_clicked(self, button, index, previous=False, next=False):
754         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
755         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
756         stack = hildon.WindowStack.get_default()
757         if previous:
758             tmp = stack.peek()
759             stack.pop_and_push(1, newDisp, tmp)
760             newDisp.show()
761             gobject.timeout_add(200, self.destroyArticle, tmp)
762             #print "previous"
763             self.disp = newDisp
764         elif next:
765             newDisp.show_all()
766             if type(self.disp).__name__ == "DisplayArticle":
767                 gobject.timeout_add(200, self.destroyArticle, self.disp)
768             self.disp = newDisp
769         else:
770             self.disp = newDisp
771             self.disp.show_all()
772         
773         self.ids = []
774         if self.key == "ArchivedArticles":
775             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
776         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
777         self.ids.append(self.disp.connect("article-next", self.nextArticle))
778         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
779
780     def buttonPurgeArticles(self, *widget):
781         self.clear()
782         self.feed.purgeReadArticles()
783         self.feed.saveUnread(CONFIGDIR)
784         self.feed.saveFeed(CONFIGDIR)
785         self.displayFeed()
786
787     def destroyArticle(self, handle):
788         handle.destroyWindow()
789
790     def mark_item_read(self, key):
791         it = self.feedItems.get_iter_first()
792         while it is not None:
793             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
794             if k == key:
795                 title = self.fix_title(self.feed.getTitle(key))
796                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
797                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
798                 break
799             it = self.feedItems.iter_next(it)
800
801     def nextArticle(self, object, index):
802         self.mark_item_read(index)
803         id = self.feed.getNextId(index)
804         if self.config.getHideReadArticles():
805             isRead = False
806             try:
807                 isRead = self.feed.isEntryRead(id)
808             except:
809                 pass
810             while isRead and id != index:
811                 id = self.feed.getNextId(id)
812                 isRead = False
813                 try:
814                        isRead = self.feed.isEntryRead(id)
815                 except:
816                        pass
817         if id != index:
818             self.button_clicked(object, id, next=True)
819
820     def previousArticle(self, object, index):
821         self.mark_item_read(index)
822         id = self.feed.getPreviousId(index)
823         if self.config.getHideReadArticles():
824             isRead = False
825             try:
826                 isRead = self.feed.isEntryRead(id)
827             except:
828                 pass
829             while isRead and id != index:
830                 id = self.feed.getPreviousId(id)
831                 isRead = False
832                 try:
833                        isRead = self.feed.isEntryRead(id)
834                 except:
835                        pass
836         if id != index:
837             self.button_clicked(object, id, previous=True)
838
839     def onArticleClosed(self, object, index):
840         selection = self.feedList.get_selection()
841         selection.set_mode(gtk.SELECTION_NONE)
842         self.mark_item_read(index)
843
844     def onArticleDeleted(self, object, index):
845         self.clear()
846         self.feed.removeArticle(index)
847         self.feed.saveUnread(CONFIGDIR)
848         self.feed.saveFeed(CONFIGDIR)
849         self.displayFeed()
850
851     def button_update_clicked(self, button):
852         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
853         if not type(self.downloadDialog).__name__=="DownloadBar":
854             self.pannableFeed.destroy()
855             self.vbox = gtk.VBox(False, 10)
856             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
857             self.downloadDialog.connect("download-done", self.onDownloadsDone)
858             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
859             self.add(self.vbox)
860             self.show_all()
861             
862     def onDownloadsDone(self, *widget):
863         self.vbox.destroy()
864         self.feed = self.listing.getFeed(self.key)
865         self.displayFeed()
866         self.updateDbusHandler.ArticleCountUpdated()
867         
868     def buttonReadAllClicked(self, button):
869         for index in self.feed.getIds():
870             self.feed.setEntryRead(index)
871             self.mark_item_read(index)
872
873
874 class FeedingIt:
875     def __init__(self):
876         # Init the windows
877         self.window = hildon.StackableWindow()
878         self.window.set_title(__appname__)
879         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
880         self.mainVbox = gtk.VBox(False,10)
881         
882         self.introLabel = gtk.Label("Loading...")
883
884         
885         self.mainVbox.pack_start(self.introLabel)
886
887         self.window.add(self.mainVbox)
888         self.window.show_all()
889         self.config = Config(self.window, CONFIGDIR+"config.ini")
890         gobject.idle_add(self.createWindow)
891         
892     def createWindow(self):
893         self.app_lock = get_lock("app_lock")
894         if self.app_lock == None:
895             self.introLabel.set_label("Update in progress, please wait.")
896             gobject.timeout_add_seconds(3, self.createWindow)
897             return False
898         self.listing = Listing(CONFIGDIR)
899         
900         self.downloadDialog = False
901         try:
902             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
903             self.orientation.set_mode(self.config.getOrientation())
904         except:
905             print "Could not start rotation manager"
906         
907         menu = hildon.AppMenu()
908         # Create a button and add it to the menu
909         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
910         button.set_label("Update All Feeds")
911         button.connect("clicked", self.button_update_clicked, "All")
912         menu.append(button)
913         
914         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
915         button.set_label("Mark All As Read")
916         button.connect("clicked", self.button_markAll)
917         menu.append(button)
918         
919         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
920         button.set_label("Manage subscriptions")
921         button.connect("clicked", self.button_organize_clicked)
922         menu.append(button)
923
924         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925         button.set_label("Settings")
926         button.connect("clicked", self.button_preferences_clicked)
927         menu.append(button)
928        
929         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930         button.set_label("About")
931         button.connect("clicked", self.button_about_clicked)
932         menu.append(button)
933         
934         self.window.set_app_menu(menu)
935         menu.show_all()
936         
937         #self.feedWindow = hildon.StackableWindow()
938         #self.articleWindow = hildon.StackableWindow()
939         self.introLabel.destroy()
940         self.pannableListing = hildon.PannableArea()
941         self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
942         self.feedList = gtk.TreeView(self.feedItems)
943         self.feedList.connect('row-activated', self.on_feedList_row_activated)
944         self.pannableListing.add(self.feedList)
945
946         icon_renderer = gtk.CellRendererPixbuf()
947         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
948         icon_column = gtk.TreeViewColumn('', icon_renderer, \
949                 pixbuf=COLUMN_ICON)
950         self.feedList.append_column(icon_column)
951
952         markup_renderer = gtk.CellRendererText()
953         markup_column = gtk.TreeViewColumn('', markup_renderer, \
954                 markup=COLUMN_MARKUP)
955         self.feedList.append_column(markup_column)
956         self.mainVbox.pack_start(self.pannableListing)
957         self.mainVbox.show_all()
958
959         self.displayListing()
960         self.autoupdate = False
961         self.checkAutoUpdate()
962         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
963         gobject.idle_add(self.enableDbus)
964         
965     def enableDbus(self):
966         self.dbusHandler = ServerObject(self)
967         self.updateDbusHandler = UpdateServerObject(self)
968
969     def button_markAll(self, button):
970         for key in self.listing.getListOfFeeds():
971             feed = self.listing.getFeed(key)
972             for id in feed.getIds():
973                 feed.setEntryRead(id)
974             feed.saveUnread(CONFIGDIR)
975             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
976         self.displayListing()
977
978     def button_about_clicked(self, button):
979         HeAboutDialog.present(self.window, \
980                 __appname__, \
981                 ABOUT_ICON, \
982                 __version__, \
983                 __description__, \
984                 ABOUT_COPYRIGHT, \
985                 ABOUT_WEBSITE, \
986                 ABOUT_BUGTRACKER, \
987                 ABOUT_DONATE)
988
989     def button_export_clicked(self, button):
990         opml = ExportOpmlData(self.window, self.listing)
991         
992     def button_import_clicked(self, button):
993         opml = GetOpmlData(self.window)
994         feeds = opml.getData()
995         for (title, url) in feeds:
996             self.listing.addFeed(title, url)
997         self.displayListing()
998
999     def addFeed(self, urlIn="http://"):
1000         wizard = AddWidgetWizard(self.window, urlIn)
1001         ret = wizard.run()
1002         if ret == 2:
1003             (title, url) = wizard.getData()
1004             if (not title == '') and (not url == ''): 
1005                self.listing.addFeed(title, url)
1006         wizard.destroy()
1007         self.displayListing()
1008
1009     def button_organize_clicked(self, button):
1010         def after_closing():
1011             self.listing.saveConfig()
1012             self.displayListing()
1013         SortList(self.window, self.listing, self, after_closing)
1014
1015     def button_update_clicked(self, button, key):
1016         if not type(self.downloadDialog).__name__=="DownloadBar":
1017             self.updateDbusHandler.UpdateStarted()
1018             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1019             self.downloadDialog.connect("download-done", self.onDownloadsDone)
1020             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1021             self.mainVbox.show_all()
1022         #self.displayListing()
1023
1024     def onDownloadsDone(self, *widget):
1025         self.downloadDialog.destroy()
1026         self.downloadDialog = False
1027         self.displayListing()
1028         self.updateDbusHandler.UpdateFinished()
1029         self.updateDbusHandler.ArticleCountUpdated()
1030
1031     def button_preferences_clicked(self, button):
1032         dialog = self.config.createDialog()
1033         dialog.connect("destroy", self.prefsClosed)
1034
1035     def show_confirmation_note(self, parent, title):
1036         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1037
1038         retcode = gtk.Dialog.run(note)
1039         note.destroy()
1040         
1041         if retcode == gtk.RESPONSE_OK:
1042             return True
1043         else:
1044             return False
1045         
1046     def displayListing(self):
1047         icon_theme = gtk.icon_theme_get_default()
1048         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1049                 gtk.ICON_LOOKUP_USE_BUILTIN)
1050
1051         self.feedItems.clear()
1052         for key in self.listing.getListOfFeeds():
1053             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1054             if unreadItems > 0 or not self.config.getHideReadFeeds():
1055                 title = self.listing.getFeedTitle(key)
1056                 updateTime = self.listing.getFeedUpdateTime(key)
1057                 
1058                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1059     
1060                 if unreadItems:
1061                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1062                 else:
1063                     markup = FEED_TEMPLATE % (title, subtitle)
1064     
1065                 try:
1066                     icon_filename = self.listing.getFavicon(key)
1067                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1068                             LIST_ICON_SIZE, LIST_ICON_SIZE)
1069                 except:
1070                     pixbuf = default_pixbuf
1071     
1072                 self.feedItems.append((pixbuf, markup, key))
1073
1074     def on_feedList_row_activated(self, treeview, path, column):
1075         model = treeview.get_model()
1076         iter = model.get_iter(path)
1077         key = model.get_value(iter, COLUMN_KEY)
1078         self.openFeed(key)
1079             
1080     def openFeed(self, key):
1081         try:
1082             self.feed_lock
1083         except:
1084             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1085             self.feed_lock = get_lock(key)
1086             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1087                     self.listing.getFeedTitle(key), key, \
1088                     self.config, self.updateDbusHandler)
1089             self.disp.connect("feed-closed", self.onFeedClosed)
1090         
1091
1092     def onFeedClosed(self, object, key):
1093         #self.listing.saveConfig()
1094         #del self.feed_lock
1095         gobject.idle_add(self.onFeedClosedTimeout)
1096         self.displayListing()
1097         #self.updateDbusHandler.ArticleCountUpdated()
1098         
1099     def onFeedClosedTimeout(self):
1100         self.listing.saveConfig()
1101         del self.feed_lock
1102         self.updateDbusHandler.ArticleCountUpdated()
1103      
1104     def run(self):
1105         self.window.connect("destroy", gtk.main_quit)
1106         gtk.main()
1107         self.listing.saveConfig()
1108         del self.app_lock
1109
1110     def prefsClosed(self, *widget):
1111         try:
1112             self.orientation.set_mode(self.config.getOrientation())
1113         except:
1114             pass
1115         self.displayListing()
1116         self.checkAutoUpdate()
1117
1118     def checkAutoUpdate(self, *widget):
1119         interval = int(self.config.getUpdateInterval()*3600000)
1120         if self.config.isAutoUpdateEnabled():
1121             if self.autoupdate == False:
1122                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1123                 self.autoupdate = interval
1124             elif not self.autoupdate == interval:
1125                 # If auto-update is enabled, but not at the right frequency
1126                 gobject.source_remove(self.autoupdateId)
1127                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1128                 self.autoupdate = interval
1129         else:
1130             if not self.autoupdate == False:
1131                 gobject.source_remove(self.autoupdateId)
1132                 self.autoupdate = False
1133
1134     def automaticUpdate(self, *widget):
1135         # Need to check for internet connection
1136         # If no internet connection, try again in 10 minutes:
1137         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1138         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1139         #from time import localtime, strftime
1140         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1141         #file.close()
1142         self.button_update_clicked(None, None)
1143         return True
1144     
1145     def stopUpdate(self):
1146         # Not implemented in the app (see update_feeds.py)
1147         try:
1148             self.downloadDialog.listOfKeys = []
1149         except:
1150             pass
1151     
1152     def getStatus(self):
1153         status = ""
1154         for key in self.listing.getListOfFeeds():
1155             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1156                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1157         if status == "":
1158             status = "No unread items"
1159         return status
1160
1161 if __name__ == "__main__":
1162     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1163     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1164     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1165     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1166     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1167     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1168     gobject.threads_init()
1169     if not isdir(CONFIGDIR):
1170         try:
1171             mkdir(CONFIGDIR)
1172         except:
1173             print "Error: Can't create configuration directory"
1174             from sys import exit
1175             exit(1)
1176     app = FeedingIt()
1177     app.run()