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