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