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