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