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