Added option to always open links in external browser
[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.7.0'
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_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
87 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
88
89 # Build the markup template for the Maemo 5 text style
90 head_font = style.get_font_desc('SystemFont')
91 sub_font = style.get_font_desc('SmallSystemFont')
92
93 head_color = style.get_color('ButtonTextColor')
94 sub_color = style.get_color('DefaultTextColor')
95 active_color = style.get_color('ActiveTextColor')
96
97 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
98 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
99
100 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
101 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
102
103 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
104 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
105
106 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
107 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
108
109 FEED_TEMPLATE = '\n'.join((head, normal_sub))
110 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
111
112 ENTRY_TEMPLATE = entry_head
113 ENTRY_TEMPLATE_UNREAD = entry_active_head
114
115 ##
116 # Removes HTML or XML character references and entities from a text string.
117 #
118 # @param text The HTML (or XML) source text.
119 # @return The plain text, as a Unicode string, if necessary.
120 # http://effbot.org/zone/re-sub.htm#unescape-html
121 def unescape(text):
122     def fixup(m):
123         text = m.group(0)
124         if text[:2] == "&#":
125             # character reference
126             try:
127                 if text[:3] == "&#x":
128                     return unichr(int(text[3:-1], 16))
129                 else:
130                     return unichr(int(text[2:-1]))
131             except ValueError:
132                 pass
133         else:
134             # named entity
135             try:
136                 text = unichr(name2codepoint[text[1:-1]])
137             except KeyError:
138                 pass
139         return text # leave as is
140     return sub("&#?\w+;", fixup, text)
141
142
143 class AddWidgetWizard(gtk.Dialog):
144     def __init__(self, parent, urlIn, titleIn=None, isEdit=False):
145         gtk.Dialog.__init__(self)
146         self.set_transient_for(parent)
147
148         if isEdit:
149             self.set_title('Edit RSS feed')
150         else:
151             self.set_title('Add new RSS feed')
152
153         if isEdit:
154             self.btn_add = self.add_button('Save', 2)
155         else:
156             self.btn_add = self.add_button('Add', 2)
157
158         self.set_default_response(2)
159
160         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
161         self.nameEntry.set_placeholder('Feed name')
162         if not titleIn == None:
163             self.nameEntry.set_text(titleIn)
164             self.nameEntry.select_region(-1, -1)
165
166         self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
167         self.urlEntry.set_placeholder('Feed URL')
168         self.urlEntry.set_text(urlIn)
169         self.urlEntry.select_region(-1, -1)
170         self.urlEntry.set_activates_default(True)
171
172         self.table = gtk.Table(2, 2, False)
173         self.table.set_col_spacings(5)
174         label = gtk.Label('Name:')
175         label.set_alignment(1., .5)
176         self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
177         self.table.attach(self.nameEntry, 1, 2, 0, 1)
178         label = gtk.Label('URL:')
179         label.set_alignment(1., .5)
180         self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
181         self.table.attach(self.urlEntry, 1, 2, 1, 2)
182         self.vbox.pack_start(self.table)
183
184         self.show_all()
185
186     def getData(self):
187         return (self.nameEntry.get_text(), self.urlEntry.get_text())
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             else:
235                 opener = build_opener()
236
237             opener.addheaders = [('User-agent', USER_AGENT)]
238             install_opener(opener)
239
240             if self.total>0:
241                 # In preparation for i18n/l10n
242                 def N_(a, b, n):
243                     return (a if n == 1 else b)
244
245                 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
246
247                 self.fraction = 0
248                 self.set_fraction(self.fraction)
249                 self.show_all()
250                 # Create a timeout
251                 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
252
253     def update_progress_bar(self):
254         #self.progress_bar.pulse()
255         if activeCount() < 4:
256             x = activeCount() - 1
257             k = len(self.listOfKeys)
258             fin = self.total - k - x
259             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
260             #print x, k, fin, fraction
261             self.set_fraction(fraction)
262
263             if len(self.listOfKeys)>0:
264                 self.current = self.current+1
265                 key = self.listOfKeys.pop()
266                 #if self.single == True:
267                     # Check if the feed is being displayed
268                 download = Download(self.listing, key, self.config)
269                 download.start()
270                 return True
271             elif activeCount() > 1:
272                 return True
273             else:
274                 #self.waitingWindow.destroy()
275                 #self.destroy()
276                 try:
277                     del self.update_lock
278                 except:
279                     pass
280                 self.emit("download-done", "success")
281                 return False 
282         return True
283     
284     
285 class SortList(hildon.StackableWindow):
286     def __init__(self, parent, listing, feedingit, after_closing):
287         hildon.StackableWindow.__init__(self)
288         self.set_transient_for(parent)
289         self.set_title('Subscriptions')
290         self.listing = listing
291         self.feedingit = feedingit
292         self.after_closing = after_closing
293         self.connect('destroy', lambda w: self.after_closing())
294         self.vbox2 = gtk.VBox(False, 2)
295
296         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
297         button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
298         button.connect("clicked", self.buttonUp)
299         self.vbox2.pack_start(button, expand=False, fill=False)
300
301         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
302         button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
303         button.connect("clicked", self.buttonDown)
304         self.vbox2.pack_start(button, expand=False, fill=False)
305
306         self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
307
308         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
309         button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
310         button.connect("clicked", self.buttonAdd)
311         self.vbox2.pack_start(button, expand=False, fill=False)
312
313         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
314         button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
315         button.connect("clicked", self.buttonEdit)
316         self.vbox2.pack_start(button, expand=False, fill=False)
317
318         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
319         button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
320         button.connect("clicked", self.buttonDelete)
321         self.vbox2.pack_start(button, expand=False, fill=False)
322
323         #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
324         #button.set_label("Done")
325         #button.connect("clicked", self.buttonDone)
326         #self.vbox.pack_start(button)
327         self.hbox2= gtk.HBox(False, 10)
328         self.pannableArea = hildon.PannableArea()
329         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
330         self.treeview = gtk.TreeView(self.treestore)
331         self.hbox2.pack_start(self.pannableArea, expand=True)
332         self.displayFeeds()
333         self.hbox2.pack_end(self.vbox2, expand=False)
334         self.set_default_size(-1, 600)
335         self.add(self.hbox2)
336
337         menu = hildon.AppMenu()
338         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
339         button.set_label("Import from OPML")
340         button.connect("clicked", self.feedingit.button_import_clicked)
341         menu.append(button)
342
343         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
344         button.set_label("Export to OPML")
345         button.connect("clicked", self.feedingit.button_export_clicked)
346         menu.append(button)
347         self.set_app_menu(menu)
348         menu.show_all()
349         
350         self.show_all()
351         #self.connect("destroy", self.buttonDone)
352         
353     def displayFeeds(self):
354         self.treeview.destroy()
355         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
356         self.treeview = gtk.TreeView()
357         
358         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
359         hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
360         self.refreshList()
361         self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
362
363         self.pannableArea.add(self.treeview)
364
365         #self.show_all()
366
367     def refreshList(self, selected=None, offset=0):
368         #rect = self.treeview.get_visible_rect()
369         #y = rect.y+rect.height
370         self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
371         for key in self.listing.getListOfFeeds():
372             item = self.treestore.append([self.listing.getFeedTitle(key), key])
373             if key == selected:
374                 selectedItem = item
375         self.treeview.set_model(self.treestore)
376         if not selected == None:
377             self.treeview.get_selection().select_iter(selectedItem)
378             self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
379         self.pannableArea.show_all()
380
381     def getSelectedItem(self):
382         (model, iter) = self.treeview.get_selection().get_selected()
383         if not iter:
384             return None
385         return model.get_value(iter, 1)
386
387     def findIndex(self, key):
388         after = None
389         before = None
390         found = False
391         for row in self.treestore:
392             if found:
393                 return (before, row.iter)
394             if key == list(row)[0]:
395                 found = True
396             else:
397                 before = row.iter
398         return (before, None)
399
400     def buttonUp(self, button):
401         key  = self.getSelectedItem()
402         if not key == None:
403             self.listing.moveUp(key)
404             self.refreshList(key, -10)
405
406     def buttonDown(self, button):
407         key = self.getSelectedItem()
408         if not key == None:
409             self.listing.moveDown(key)
410             self.refreshList(key, 10)
411
412     def buttonDelete(self, button):
413         key = self.getSelectedItem()
414
415         message = 'Really remove this feed and its entries?'
416         dlg = hildon.hildon_note_new_confirmation(self, message)
417         response = dlg.run()
418         dlg.destroy()
419         if response == gtk.RESPONSE_OK:
420             self.listing.removeFeed(key)
421             self.refreshList()
422
423     def buttonEdit(self, button):
424         key = self.getSelectedItem()
425
426         if key == 'ArchivedArticles':
427             message = 'Cannot edit the archived articles feed.'
428             hildon.hildon_banner_show_information(self, '', message)
429             return
430
431         if key is not None:
432             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True)
433             ret = wizard.run()
434             if ret == 2:
435                 (title, url) = wizard.getData()
436                 if (not title == '') and (not url == ''):
437                     self.listing.editFeed(key, title, url)
438                     self.refreshList()
439             wizard.destroy()
440
441     def buttonDone(self, *args):
442         self.destroy()
443         
444     def buttonAdd(self, button, urlIn="http://"):
445         wizard = AddWidgetWizard(self, urlIn)
446         ret = wizard.run()
447         if ret == 2:
448             (title, url) = wizard.getData()
449             if (not title == '') and (not url == ''): 
450                self.listing.addFeed(title, url)
451         wizard.destroy()
452         self.refreshList()
453                
454
455 class DisplayArticle(hildon.StackableWindow):
456     def __init__(self, feed, id, key, config, listing):
457         hildon.StackableWindow.__init__(self)
458         #self.imageDownloader = ImageDownloader()
459         self.feed = feed
460         self.listing=listing
461         self.key = key
462         self.id = id
463         #self.set_title(feed.getTitle(id))
464         self.set_title(self.listing.getFeedTitle(key))
465         self.config = config
466         self.set_for_removal = False
467         
468         # Init the article display
469         #if self.config.getWebkitSupport():
470         self.view = WebView()
471             #self.view.set_editable(False)
472         #else:
473         #    import gtkhtml2
474         #    self.view = gtkhtml2.View()
475         #    self.document = gtkhtml2.Document()
476         #    self.view.set_document(self.document)
477         #    self.document.connect("link_clicked", self._signal_link_clicked)
478         self.pannable_article = hildon.PannableArea()
479         self.pannable_article.add(self.view)
480         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
481         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
482
483         #if self.config.getWebkitSupport():
484         contentLink = self.feed.getContentLink(self.id)
485         self.feed.setEntryRead(self.id)
486         #if key=="ArchivedArticles":
487         self.loadedArticle = False
488         if contentLink.startswith("/home/user/"):
489             self.view.open("file://%s" % contentLink)
490             self.currentUrl = self.feed.getExternalLink(self.id)
491         else:
492             self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
493             self.currentUrl = "%s" % contentLink
494         self.view.connect("motion-notify-event", lambda w,ev: True)
495         self.view.connect('load-started', self.load_started)
496         self.view.connect('load-finished', self.load_finished)
497
498         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
499         
500         menu = hildon.AppMenu()
501         # Create a button and add it to the menu
502         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
503         button.set_label("Allow horizontal scrolling")
504         button.connect("clicked", self.horiz_scrolling_button)
505         menu.append(button)
506         
507         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
508         button.set_label("Open in browser")
509         button.connect("clicked", self.open_in_browser)
510         menu.append(button)
511         
512         if key == "ArchivedArticles":
513             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
514             button.set_label("Remove from archived articles")
515             button.connect("clicked", self.remove_archive_button)
516         else:
517             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
518             button.set_label("Add to archived articles")
519             button.connect("clicked", self.archive_button)
520         menu.append(button)
521         
522         self.set_app_menu(menu)
523         menu.show_all()
524         
525         self.add(self.pannable_article)
526         
527         self.pannable_article.show_all()
528
529         self.destroyId = self.connect("destroy", self.destroyWindow)
530         
531         #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
532         ## Still using an old version of WebKit, so using navigation-requested signal
533         self.view.connect('navigation-requested', self.navigation_requested)
534         
535         self.view.connect("button_press_event", self.button_pressed)
536         self.gestureId = self.view.connect("button_release_event", self.button_released)
537
538     #def navigation_policy_decision(self, wv, fr, req, action, decision):
539     def navigation_requested(self, wv, fr, req):
540         if self.config.getOpenInExternalBrowser():
541             self.open_in_browser(None, req.get_uri())
542             return True
543         else:
544             return False
545
546     def load_started(self, *widget):
547         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
548         
549     def load_finished(self, *widget):
550         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
551         frame = self.view.get_main_frame()
552         if self.loadedArticle:
553             self.currentUrl = frame.get_uri()
554         else:
555             self.loadedArticle = True
556
557     def button_pressed(self, window, event):
558         #print event.x, event.y
559         self.coords = (event.x, event.y)
560         
561     def button_released(self, window, event):
562         x = self.coords[0] - event.x
563         y = self.coords[1] - event.y
564         
565         if (2*abs(y) < abs(x)):
566             if (x > 15):
567                 self.emit("article-previous", self.id)
568             elif (x<-15):
569                 self.emit("article-next", self.id)   
570
571     def destroyWindow(self, *args):
572         self.disconnect(self.destroyId)
573         if self.set_for_removal:
574             self.emit("article-deleted", self.id)
575         else:
576             self.emit("article-closed", self.id)
577         #self.imageDownloader.stopAll()
578         self.destroy()
579         
580     def horiz_scrolling_button(self, *widget):
581         self.pannable_article.disconnect(self.gestureId)
582         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
583         
584     def archive_button(self, *widget):
585         # Call the listing.addArchivedArticle
586         self.listing.addArchivedArticle(self.key, self.id)
587         
588     def remove_archive_button(self, *widget):
589         self.set_for_removal = True
590
591     def open_in_browser(self, object, link=None):
592         import dbus
593         bus = dbus.SessionBus()
594         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
595         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
596         if link == None:
597             iface.open_new_window(self.currentUrl)
598         else:
599             iface.open_new_window(link)
600
601 class DisplayFeed(hildon.StackableWindow):
602     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
603         hildon.StackableWindow.__init__(self)
604         self.listing = listing
605         self.feed = feed
606         self.feedTitle = title
607         self.set_title(title)
608         self.key=key
609         self.config = config
610         self.updateDbusHandler = updateDbusHandler
611         
612         self.downloadDialog = False
613         
614         #self.listing.setCurrentlyDisplayedFeed(self.key)
615         
616         self.disp = False
617         
618         menu = hildon.AppMenu()
619         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
620         button.set_label("Update feed")
621         button.connect("clicked", self.button_update_clicked)
622         menu.append(button)
623         
624         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
625         button.set_label("Mark all as read")
626         button.connect("clicked", self.buttonReadAllClicked)
627         menu.append(button)
628         
629         if key=="ArchivedArticles":
630             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631             button.set_label("Delete read articles")
632             button.connect("clicked", self.buttonPurgeArticles)
633             menu.append(button)
634         
635         self.set_app_menu(menu)
636         menu.show_all()
637         
638         self.displayFeed()
639         
640         self.connect('configure-event', self.on_configure_event)
641         self.connect("destroy", self.destroyWindow)
642
643     def on_configure_event(self, window, event):
644         if getattr(self, 'markup_renderer', None) is None:
645             return
646
647         # Fix up the column width for wrapping the text when the window is
648         # resized (i.e. orientation changed)
649         self.markup_renderer.set_property('wrap-width', event.width-20)  
650         it = self.feedItems.get_iter_first()
651         while it is not None:
652             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
653             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
654             it = self.feedItems.iter_next(it)
655
656     def destroyWindow(self, *args):
657         #self.feed.saveUnread(CONFIGDIR)
658         gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
659         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
660         self.emit("feed-closed", self.key)
661         self.destroy()
662         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
663         #self.listing.closeCurrentlyDisplayedFeed()
664
665     def fix_title(self, title):
666         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
667
668     def displayFeed(self):
669         self.pannableFeed = hildon.PannableArea()
670
671         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
672
673         self.feedItems = gtk.ListStore(str, str)
674         #self.feedList = gtk.TreeView(self.feedItems)
675         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
676         selection = self.feedList.get_selection()
677         selection.set_mode(gtk.SELECTION_NONE)
678         #selection.connect("changed", lambda w: True)
679         
680         self.feedList.set_model(self.feedItems)
681         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
682
683         
684         self.feedList.set_hover_selection(False)
685         #self.feedList.set_property('enable-grid-lines', True)
686         #self.feedList.set_property('hildon-mode', 1)
687         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
688         
689         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
690
691         vbox= gtk.VBox(False, 10)
692         vbox.pack_start(self.feedList)
693         
694         self.pannableFeed.add_with_viewport(vbox)
695
696         self.markup_renderer = gtk.CellRendererText()
697         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
698         self.markup_renderer.set_property('background', "#333333")
699         (width, height) = self.get_size()
700         self.markup_renderer.set_property('wrap-width', width-20)
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 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("Add new feed")
921         button.connect("clicked", lambda b: self.addFeed())
922         menu.append(button)
923
924         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925         button.set_label("Manage subscriptions")
926         button.connect("clicked", self.button_organize_clicked)
927         menu.append(button)
928
929         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930         button.set_label("Settings")
931         button.connect("clicked", self.button_preferences_clicked)
932         menu.append(button)
933        
934         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
935         button.set_label("About")
936         button.connect("clicked", self.button_about_clicked)
937         menu.append(button)
938         
939         self.window.set_app_menu(menu)
940         menu.show_all()
941         
942         #self.feedWindow = hildon.StackableWindow()
943         #self.articleWindow = hildon.StackableWindow()
944         self.introLabel.destroy()
945         self.pannableListing = hildon.PannableArea()
946         self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
947         self.feedList = gtk.TreeView(self.feedItems)
948         self.feedList.connect('row-activated', self.on_feedList_row_activated)
949         self.pannableListing.add(self.feedList)
950
951         icon_renderer = gtk.CellRendererPixbuf()
952         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
953         icon_column = gtk.TreeViewColumn('', icon_renderer, \
954                 pixbuf=COLUMN_ICON)
955         self.feedList.append_column(icon_column)
956
957         markup_renderer = gtk.CellRendererText()
958         markup_column = gtk.TreeViewColumn('', markup_renderer, \
959                 markup=COLUMN_MARKUP)
960         self.feedList.append_column(markup_column)
961         self.mainVbox.pack_start(self.pannableListing)
962         self.mainVbox.show_all()
963
964         self.displayListing()
965         self.autoupdate = False
966         self.checkAutoUpdate()
967         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
968         gobject.idle_add(self.enableDbus)
969         
970     def enableDbus(self):
971         self.dbusHandler = ServerObject(self)
972         self.updateDbusHandler = UpdateServerObject(self)
973
974     def button_markAll(self, button):
975         for key in self.listing.getListOfFeeds():
976             feed = self.listing.getFeed(key)
977             for id in feed.getIds():
978                 feed.setEntryRead(id)
979             feed.saveUnread(CONFIGDIR)
980             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
981         self.displayListing()
982
983     def button_about_clicked(self, button):
984         HeAboutDialog.present(self.window, \
985                 __appname__, \
986                 ABOUT_ICON, \
987                 __version__, \
988                 __description__, \
989                 ABOUT_COPYRIGHT, \
990                 ABOUT_WEBSITE, \
991                 ABOUT_BUGTRACKER, \
992                 ABOUT_DONATE)
993
994     def button_export_clicked(self, button):
995         opml = ExportOpmlData(self.window, self.listing)
996         
997     def button_import_clicked(self, button):
998         opml = GetOpmlData(self.window)
999         feeds = opml.getData()
1000         for (title, url) in feeds:
1001             self.listing.addFeed(title, url)
1002         self.displayListing()
1003
1004     def addFeed(self, urlIn="http://"):
1005         wizard = AddWidgetWizard(self.window, urlIn)
1006         ret = wizard.run()
1007         if ret == 2:
1008             (title, url) = wizard.getData()
1009             if (not title == '') and (not url == ''): 
1010                self.listing.addFeed(title, url)
1011         wizard.destroy()
1012         self.displayListing()
1013
1014     def button_organize_clicked(self, button):
1015         def after_closing():
1016             self.listing.saveConfig()
1017             self.displayListing()
1018         SortList(self.window, self.listing, self, after_closing)
1019
1020     def button_update_clicked(self, button, key):
1021         if not type(self.downloadDialog).__name__=="DownloadBar":
1022             self.updateDbusHandler.UpdateStarted()
1023             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1024             self.downloadDialog.connect("download-done", self.onDownloadsDone)
1025             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1026             self.mainVbox.show_all()
1027         #self.displayListing()
1028
1029     def onDownloadsDone(self, *widget):
1030         self.downloadDialog.destroy()
1031         self.downloadDialog = False
1032         self.displayListing()
1033         self.updateDbusHandler.UpdateFinished()
1034         self.updateDbusHandler.ArticleCountUpdated()
1035
1036     def button_preferences_clicked(self, button):
1037         dialog = self.config.createDialog()
1038         dialog.connect("destroy", self.prefsClosed)
1039
1040     def show_confirmation_note(self, parent, title):
1041         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1042
1043         retcode = gtk.Dialog.run(note)
1044         note.destroy()
1045         
1046         if retcode == gtk.RESPONSE_OK:
1047             return True
1048         else:
1049             return False
1050         
1051     def displayListing(self):
1052         icon_theme = gtk.icon_theme_get_default()
1053         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1054                 gtk.ICON_LOOKUP_USE_BUILTIN)
1055
1056         self.feedItems.clear()
1057         for key in self.listing.getListOfFeeds():
1058             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1059             if unreadItems > 0 or not self.config.getHideReadFeeds():
1060                 title = self.listing.getFeedTitle(key)
1061                 updateTime = self.listing.getFeedUpdateTime(key)
1062                 
1063                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1064     
1065                 if unreadItems:
1066                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1067                 else:
1068                     markup = FEED_TEMPLATE % (title, subtitle)
1069     
1070                 try:
1071                     icon_filename = self.listing.getFavicon(key)
1072                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1073                             LIST_ICON_SIZE, LIST_ICON_SIZE)
1074                 except:
1075                     pixbuf = default_pixbuf
1076     
1077                 self.feedItems.append((pixbuf, markup, key))
1078
1079     def on_feedList_row_activated(self, treeview, path, column):
1080         model = treeview.get_model()
1081         iter = model.get_iter(path)
1082         key = model.get_value(iter, COLUMN_KEY)
1083         self.openFeed(key)
1084             
1085     def openFeed(self, key):
1086         try:
1087             self.feed_lock
1088         except:
1089             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1090             self.feed_lock = get_lock(key)
1091             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1092                     self.listing.getFeedTitle(key), key, \
1093                     self.config, self.updateDbusHandler)
1094             self.disp.connect("feed-closed", self.onFeedClosed)
1095         
1096
1097     def onFeedClosed(self, object, key):
1098         #self.listing.saveConfig()
1099         #del self.feed_lock
1100         gobject.idle_add(self.onFeedClosedTimeout)
1101         self.displayListing()
1102         #self.updateDbusHandler.ArticleCountUpdated()
1103         
1104     def onFeedClosedTimeout(self):
1105         self.listing.saveConfig()
1106         del self.feed_lock
1107         self.updateDbusHandler.ArticleCountUpdated()
1108      
1109     def run(self):
1110         self.window.connect("destroy", gtk.main_quit)
1111         gtk.main()
1112         self.listing.saveConfig()
1113         del self.app_lock
1114
1115     def prefsClosed(self, *widget):
1116         try:
1117             self.orientation.set_mode(self.config.getOrientation())
1118         except:
1119             pass
1120         self.displayListing()
1121         self.checkAutoUpdate()
1122
1123     def checkAutoUpdate(self, *widget):
1124         interval = int(self.config.getUpdateInterval()*3600000)
1125         if self.config.isAutoUpdateEnabled():
1126             if self.autoupdate == False:
1127                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1128                 self.autoupdate = interval
1129             elif not self.autoupdate == interval:
1130                 # If auto-update is enabled, but not at the right frequency
1131                 gobject.source_remove(self.autoupdateId)
1132                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1133                 self.autoupdate = interval
1134         else:
1135             if not self.autoupdate == False:
1136                 gobject.source_remove(self.autoupdateId)
1137                 self.autoupdate = False
1138
1139     def automaticUpdate(self, *widget):
1140         # Need to check for internet connection
1141         # If no internet connection, try again in 10 minutes:
1142         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1143         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1144         #from time import localtime, strftime
1145         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1146         #file.close()
1147         self.button_update_clicked(None, None)
1148         return True
1149     
1150     def stopUpdate(self):
1151         # Not implemented in the app (see update_feeds.py)
1152         try:
1153             self.downloadDialog.listOfKeys = []
1154         except:
1155             pass
1156     
1157     def getStatus(self):
1158         status = ""
1159         for key in self.listing.getListOfFeeds():
1160             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1161                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1162         if status == "":
1163             status = "No unread items"
1164         return status
1165
1166 if __name__ == "__main__":
1167     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1168     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1169     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1170     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1171     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1172     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1173     gobject.threads_init()
1174     if not isdir(CONFIGDIR):
1175         try:
1176             mkdir(CONFIGDIR)
1177         except:
1178             print "Error: Can't create configuration directory"
1179             from sys import exit
1180             exit(1)
1181     app = FeedingIt()
1182     app.run()