minor edits
[feedingit] / src / FeedingIt.py
1 #!/usr/bin/env python2.5
2
3
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public License
16 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 # ============================================================================
20 __appname__ = 'FeedingIt'
21 __author__  = 'Yves Marcoz'
22 __version__ = '0.7.0'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
25
26 import gtk
27 from pango import FontDescription
28 import pango
29 import hildon
30 #import gtkhtml2
31 #try:
32 from webkit import WebView
33 #    has_webkit=True
34 #except:
35 #    import gtkhtml2
36 #    has_webkit=False
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
39 import gobject
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from threading import Thread, activeCount
43 from feedingitdbus import ServerObject
44 from updatedbus import UpdateServerObject, get_lock
45 from config import Config
46 from cgi import escape
47
48 from rss import Listing
49 from opml import GetOpmlData, ExportOpmlData
50
51 from urllib2 import install_opener, build_opener
52
53 from socket import setdefaulttimeout
54 timeout = 5
55 setdefaulttimeout(timeout)
56 del timeout
57
58 LIST_ICON_SIZE = 32
59 LIST_ICON_BORDER = 10
60
61 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
62 ABOUT_ICON = 'feedingit'
63 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
64 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
65 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
66 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
67
68 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
69 unread_color = color_style.lookup_color('ActiveTextColor')
70 read_color = color_style.lookup_color('DefaultTextColor')
71 del color_style
72
73 CONFIGDIR="/home/user/.feedingit/"
74 LOCK = CONFIGDIR + "update.lock"
75
76 from re import sub
77 from htmlentitydefs import name2codepoint
78
79 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
80
81 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
82
83 import style
84
85 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
86 MARKUP_TEMPLATE_ENTRY = '<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-20)  
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         (width, height) = self.get_size()
705         self.markup_renderer.set_property('wrap-width', width-20)
706         self.markup_renderer.set_property('ypad', 5)
707         self.markup_renderer.set_property('xpad', 5)
708         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
709                 markup=FEED_COLUMN_MARKUP)
710         self.feedList.append_column(markup_column)
711
712         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
713         hideReadArticles = self.config.getHideReadArticles()
714         hasArticle = False
715         for id in self.feed.getIds():
716             isRead = False
717             try:
718                 isRead = self.feed.isEntryRead(id)
719             except:
720                 pass
721             if not ( isRead and hideReadArticles ):
722             #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
723                 #title = self.feed.getTitle(id)
724                 title = self.fix_title(self.feed.getTitle(id))
725     
726                 #if self.feed.isEntryRead(id):
727                 if isRead:
728                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
729                 else:
730                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
731     
732                 self.feedItems.append((markup, id))
733                 hasArticle = True
734         if hasArticle:
735             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
736         else:
737             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
738             self.feedItems.append((markup, ""))
739
740         self.add(self.pannableFeed)
741         self.show_all()
742
743     def clear(self):
744         self.pannableFeed.destroy()
745         #self.remove(self.pannableFeed)
746
747     def on_feedList_row_activated(self, treeview, path): #, column):
748         selection = self.feedList.get_selection()
749         selection.set_mode(gtk.SELECTION_SINGLE)
750         self.feedList.get_selection().select_path(path)
751         model = treeview.get_model()
752         iter = model.get_iter(path)
753         key = model.get_value(iter, FEED_COLUMN_KEY)
754         # Emulate legacy "button_clicked" call via treeview
755         gobject.idle_add(self.button_clicked, treeview, key)
756         #return True
757
758     def button_clicked(self, button, index, previous=False, next=False):
759         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
760         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
761         stack = hildon.WindowStack.get_default()
762         if previous:
763             tmp = stack.peek()
764             stack.pop_and_push(1, newDisp, tmp)
765             newDisp.show()
766             gobject.timeout_add(200, self.destroyArticle, tmp)
767             #print "previous"
768             self.disp = newDisp
769         elif next:
770             newDisp.show_all()
771             if type(self.disp).__name__ == "DisplayArticle":
772                 gobject.timeout_add(200, self.destroyArticle, self.disp)
773             self.disp = newDisp
774         else:
775             self.disp = newDisp
776             self.disp.show_all()
777         
778         self.ids = []
779         if self.key == "ArchivedArticles":
780             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
781         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
782         self.ids.append(self.disp.connect("article-next", self.nextArticle))
783         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
784
785     def buttonPurgeArticles(self, *widget):
786         self.clear()
787         self.feed.purgeReadArticles()
788         self.feed.saveUnread(CONFIGDIR)
789         self.feed.saveFeed(CONFIGDIR)
790         self.displayFeed()
791
792     def destroyArticle(self, handle):
793         handle.destroyWindow()
794
795     def mark_item_read(self, key):
796         it = self.feedItems.get_iter_first()
797         while it is not None:
798             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
799             if k == key:
800                 title = self.fix_title(self.feed.getTitle(key))
801                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
802                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
803                 break
804             it = self.feedItems.iter_next(it)
805
806     def nextArticle(self, object, index):
807         self.mark_item_read(index)
808         id = self.feed.getNextId(index)
809         if self.config.getHideReadArticles():
810             isRead = False
811             try:
812                 isRead = self.feed.isEntryRead(id)
813             except:
814                 pass
815             while isRead and id != index:
816                 id = self.feed.getNextId(id)
817                 isRead = False
818                 try:
819                        isRead = self.feed.isEntryRead(id)
820                 except:
821                        pass
822         if id != index:
823             self.button_clicked(object, id, next=True)
824
825     def previousArticle(self, object, index):
826         self.mark_item_read(index)
827         id = self.feed.getPreviousId(index)
828         if self.config.getHideReadArticles():
829             isRead = False
830             try:
831                 isRead = self.feed.isEntryRead(id)
832             except:
833                 pass
834             while isRead and id != index:
835                 id = self.feed.getPreviousId(id)
836                 isRead = False
837                 try:
838                        isRead = self.feed.isEntryRead(id)
839                 except:
840                        pass
841         if id != index:
842             self.button_clicked(object, id, previous=True)
843
844     def onArticleClosed(self, object, index):
845         selection = self.feedList.get_selection()
846         selection.set_mode(gtk.SELECTION_NONE)
847         self.mark_item_read(index)
848
849     def onArticleDeleted(self, object, index):
850         self.clear()
851         self.feed.removeArticle(index)
852         self.feed.saveUnread(CONFIGDIR)
853         self.feed.saveFeed(CONFIGDIR)
854         self.displayFeed()
855
856     def button_update_clicked(self, button):
857         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
858         if not type(self.downloadDialog).__name__=="DownloadBar":
859             self.pannableFeed.destroy()
860             self.vbox = gtk.VBox(False, 10)
861             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
862             self.downloadDialog.connect("download-done", self.onDownloadsDone)
863             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
864             self.add(self.vbox)
865             self.show_all()
866             
867     def onDownloadsDone(self, *widget):
868         self.vbox.destroy()
869         self.feed = self.listing.getFeed(self.key)
870         self.displayFeed()
871         self.updateDbusHandler.ArticleCountUpdated()
872         
873     def buttonReadAllClicked(self, button):
874         for index in self.feed.getIds():
875             self.feed.setEntryRead(index)
876             self.mark_item_read(index)
877
878
879 class FeedingIt:
880     def __init__(self):
881         # Init the windows
882         self.window = hildon.StackableWindow()
883         self.window.set_title(__appname__)
884         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
885         self.mainVbox = gtk.VBox(False,10)
886         
887         self.introLabel = gtk.Label("Loading...")
888
889         
890         self.mainVbox.pack_start(self.introLabel)
891
892         self.window.add(self.mainVbox)
893         self.window.show_all()
894         self.config = Config(self.window, CONFIGDIR+"config.ini")
895         gobject.idle_add(self.createWindow)
896         
897     def createWindow(self):
898         self.app_lock = get_lock("app_lock")
899         if self.app_lock == None:
900             self.introLabel.set_label("Update in progress, please wait.")
901             gobject.timeout_add_seconds(3, self.createWindow)
902             return False
903         self.listing = Listing(CONFIGDIR)
904         
905         self.downloadDialog = False
906         try:
907             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
908             self.orientation.set_mode(self.config.getOrientation())
909         except:
910             print "Could not start rotation manager"
911         
912         menu = hildon.AppMenu()
913         # Create a button and add it to the menu
914         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
915         button.set_label("Update feeds")
916         button.connect("clicked", self.button_update_clicked, "All")
917         menu.append(button)
918         
919         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
920         button.set_label("Mark all as read")
921         button.connect("clicked", self.button_markAll)
922         menu.append(button)
923         
924         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925         button.set_label("Manage subscriptions")
926         button.connect("clicked", self.button_organize_clicked)
927         menu.append(button)
928
929         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930         button.set_label("Settings")
931         button.connect("clicked", self.button_preferences_clicked)
932         menu.append(button)
933        
934         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
935         button.set_label("About")
936         button.connect("clicked", self.button_about_clicked)
937         menu.append(button)
938         
939         self.window.set_app_menu(menu)
940         menu.show_all()
941         
942         #self.feedWindow = hildon.StackableWindow()
943         #self.articleWindow = hildon.StackableWindow()
944         self.introLabel.destroy()
945         self.pannableListing = hildon.PannableArea()
946         self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
947         self.feedList = gtk.TreeView(self.feedItems)
948         self.feedList.connect('row-activated', self.on_feedList_row_activated)
949         self.pannableListing.add(self.feedList)
950
951         icon_renderer = gtk.CellRendererPixbuf()
952         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
953         icon_column = gtk.TreeViewColumn('', icon_renderer, \
954                 pixbuf=COLUMN_ICON)
955         self.feedList.append_column(icon_column)
956
957         markup_renderer = gtk.CellRendererText()
958         markup_column = gtk.TreeViewColumn('', markup_renderer, \
959                 markup=COLUMN_MARKUP)
960         self.feedList.append_column(markup_column)
961         self.mainVbox.pack_start(self.pannableListing)
962         self.mainVbox.show_all()
963
964         self.displayListing()
965         self.autoupdate = False
966         self.checkAutoUpdate()
967         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
968         gobject.idle_add(self.enableDbus)
969         
970     def enableDbus(self):
971         self.dbusHandler = ServerObject(self)
972         self.updateDbusHandler = UpdateServerObject(self)
973
974     def button_markAll(self, button):
975         for key in self.listing.getListOfFeeds():
976             feed = self.listing.getFeed(key)
977             for id in feed.getIds():
978                 feed.setEntryRead(id)
979             feed.saveUnread(CONFIGDIR)
980             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
981         self.displayListing()
982
983     def button_about_clicked(self, button):
984         HeAboutDialog.present(self.window, \
985                 __appname__, \
986                 ABOUT_ICON, \
987                 __version__, \
988                 __description__, \
989                 ABOUT_COPYRIGHT, \
990                 ABOUT_WEBSITE, \
991                 ABOUT_BUGTRACKER, \
992                 ABOUT_DONATE)
993
994     def button_export_clicked(self, button):
995         opml = ExportOpmlData(self.window, self.listing)
996         
997     def button_import_clicked(self, button):
998         opml = GetOpmlData(self.window)
999         feeds = opml.getData()
1000         for (title, url) in feeds:
1001             self.listing.addFeed(title, url)
1002         self.displayListing()
1003
1004     def addFeed(self, urlIn="http://"):
1005         wizard = AddWidgetWizard(self.window, urlIn)
1006         ret = wizard.run()
1007         if ret == 2:
1008             (title, url) = wizard.getData()
1009             if (not title == '') and (not url == ''): 
1010                self.listing.addFeed(title, url)
1011         wizard.destroy()
1012         self.displayListing()
1013
1014     def button_organize_clicked(self, button):
1015         def after_closing():
1016             self.listing.saveConfig()
1017             self.displayListing()
1018         SortList(self.window, self.listing, self, after_closing)
1019
1020     def button_update_clicked(self, button, key):
1021         if not type(self.downloadDialog).__name__=="DownloadBar":
1022             self.updateDbusHandler.UpdateStarted()
1023             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1024             self.downloadDialog.connect("download-done", self.onDownloadsDone)
1025             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1026             self.mainVbox.show_all()
1027         #self.displayListing()
1028
1029     def onDownloadsDone(self, *widget):
1030         self.downloadDialog.destroy()
1031         self.downloadDialog = False
1032         self.displayListing()
1033         self.updateDbusHandler.UpdateFinished()
1034         self.updateDbusHandler.ArticleCountUpdated()
1035
1036     def button_preferences_clicked(self, button):
1037         dialog = self.config.createDialog()
1038         dialog.connect("destroy", self.prefsClosed)
1039
1040     def show_confirmation_note(self, parent, title):
1041         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1042
1043         retcode = gtk.Dialog.run(note)
1044         note.destroy()
1045         
1046         if retcode == gtk.RESPONSE_OK:
1047             return True
1048         else:
1049             return False
1050         
1051     def displayListing(self):
1052         icon_theme = gtk.icon_theme_get_default()
1053         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1054                 gtk.ICON_LOOKUP_USE_BUILTIN)
1055
1056         self.feedItems.clear()
1057         for key in self.listing.getListOfFeeds():
1058             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1059             if unreadItems > 0 or not self.config.getHideReadFeeds():
1060                 title = self.listing.getFeedTitle(key)
1061                 updateTime = self.listing.getFeedUpdateTime(key)
1062                 
1063                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1064     
1065                 if unreadItems:
1066                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1067                 else:
1068                     markup = FEED_TEMPLATE % (title, subtitle)
1069     
1070                 try:
1071                     icon_filename = self.listing.getFavicon(key)
1072                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1073                             LIST_ICON_SIZE, LIST_ICON_SIZE)
1074                 except:
1075                     pixbuf = default_pixbuf
1076     
1077                 self.feedItems.append((pixbuf, markup, key))
1078
1079     def on_feedList_row_activated(self, treeview, path, column):
1080         model = treeview.get_model()
1081         iter = model.get_iter(path)
1082         key = model.get_value(iter, COLUMN_KEY)
1083         self.openFeed(key)
1084             
1085     def openFeed(self, key):
1086         try:
1087             self.feed_lock
1088         except:
1089             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1090             self.feed_lock = get_lock(key)
1091             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1092                     self.listing.getFeedTitle(key), key, \
1093                     self.config, self.updateDbusHandler)
1094             self.disp.connect("feed-closed", self.onFeedClosed)
1095         
1096
1097     def onFeedClosed(self, object, key):
1098         #self.listing.saveConfig()
1099         #del self.feed_lock
1100         gobject.idle_add(self.onFeedClosedTimeout)
1101         self.displayListing()
1102         #self.updateDbusHandler.ArticleCountUpdated()
1103         
1104     def onFeedClosedTimeout(self):
1105         self.listing.saveConfig()
1106         del self.feed_lock
1107         self.updateDbusHandler.ArticleCountUpdated()
1108      
1109     def run(self):
1110         self.window.connect("destroy", gtk.main_quit)
1111         gtk.main()
1112         self.listing.saveConfig()
1113         del self.app_lock
1114
1115     def prefsClosed(self, *widget):
1116         try:
1117             self.orientation.set_mode(self.config.getOrientation())
1118         except:
1119             pass
1120         self.displayListing()
1121         self.checkAutoUpdate()
1122
1123     def checkAutoUpdate(self, *widget):
1124         interval = int(self.config.getUpdateInterval()*3600000)
1125         if self.config.isAutoUpdateEnabled():
1126             if self.autoupdate == False:
1127                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1128                 self.autoupdate = interval
1129             elif not self.autoupdate == interval:
1130                 # If auto-update is enabled, but not at the right frequency
1131                 gobject.source_remove(self.autoupdateId)
1132                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1133                 self.autoupdate = interval
1134         else:
1135             if not self.autoupdate == False:
1136                 gobject.source_remove(self.autoupdateId)
1137                 self.autoupdate = False
1138
1139     def automaticUpdate(self, *widget):
1140         # Need to check for internet connection
1141         # If no internet connection, try again in 10 minutes:
1142         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1143         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1144         #from time import localtime, strftime
1145         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1146         #file.close()
1147         self.button_update_clicked(None, None)
1148         return True
1149     
1150     def stopUpdate(self):
1151         # Not implemented in the app (see update_feeds.py)
1152         try:
1153             self.downloadDialog.listOfKeys = []
1154         except:
1155             pass
1156     
1157     def getStatus(self):
1158         status = ""
1159         for key in self.listing.getListOfFeeds():
1160             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1161                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1162         if status == "":
1163             status = "No unread items"
1164         return status
1165
1166 if __name__ == "__main__":
1167     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1168     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1169     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1170     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1171     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1172     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1173     gobject.threads_init()
1174     if not isdir(CONFIGDIR):
1175         try:
1176             mkdir(CONFIGDIR)
1177         except:
1178             print "Error: Can't create configuration directory"
1179             from sys import exit
1180             exit(1)
1181     app = FeedingIt()
1182     app.run()