Cleaned up "Add feed" and "Edit feed" wizard
[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         if key == 'ArchivedArticles':
416             message = 'Cannot remove the archived articles feed.'
417             hildon.hildon_banner_show_information(self, '', message)
418         elif key is not None:
419             message = 'Really remove this feed and its entries?'
420             dlg = hildon.hildon_note_new_confirmation(self, message)
421             response = dlg.run()
422             dlg.destroy()
423             if response == gtk.RESPONSE_OK:
424                 self.listing.removeFeed(key)
425                 self.refreshList()
426
427     def buttonEdit(self, button):
428         key = self.getSelectedItem()
429
430         if key == 'ArchivedArticles':
431             message = 'Cannot edit the archived articles feed.'
432             hildon.hildon_banner_show_information(self, '', message)
433             return
434
435         if key is not None:
436             wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True)
437             ret = wizard.run()
438             if ret == 2:
439                 (title, url) = wizard.getData()
440                 if (not title == '') and (not url == ''):
441                     self.listing.editFeed(key, title, url)
442                     self.refreshList()
443             wizard.destroy()
444
445     def buttonDone(self, *args):
446         self.destroy()
447         
448     def buttonAdd(self, button, urlIn="http://"):
449         wizard = AddWidgetWizard(self, urlIn)
450         ret = wizard.run()
451         if ret == 2:
452             (title, url) = wizard.getData()
453             if (not title == '') and (not url == ''): 
454                self.listing.addFeed(title, url)
455         wizard.destroy()
456         self.refreshList()
457                
458
459 class DisplayArticle(hildon.StackableWindow):
460     def __init__(self, feed, id, key, config, listing):
461         hildon.StackableWindow.__init__(self)
462         #self.imageDownloader = ImageDownloader()
463         self.feed = feed
464         self.listing=listing
465         self.key = key
466         self.id = id
467         #self.set_title(feed.getTitle(id))
468         self.set_title(self.listing.getFeedTitle(key))
469         self.config = config
470         self.set_for_removal = False
471         
472         # Init the article display
473         #if self.config.getWebkitSupport():
474         self.view = WebView()
475             #self.view.set_editable(False)
476         #else:
477         #    import gtkhtml2
478         #    self.view = gtkhtml2.View()
479         #    self.document = gtkhtml2.Document()
480         #    self.view.set_document(self.document)
481         #    self.document.connect("link_clicked", self._signal_link_clicked)
482         self.pannable_article = hildon.PannableArea()
483         self.pannable_article.add(self.view)
484         #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
485         #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
486
487         #if self.config.getWebkitSupport():
488         contentLink = self.feed.getContentLink(self.id)
489         self.feed.setEntryRead(self.id)
490         #if key=="ArchivedArticles":
491         if contentLink.startswith("/home/user/"):
492             self.view.open("file://" + contentLink)
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.view.connect("motion-notify-event", lambda w,ev: True)
496         self.view.connect('load-started', self.load_started)
497         self.view.connect('load-finished', self.load_finished)
498
499         #else:
500         #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
501         self.view.set_zoom_level(float(config.getArtFontSize())/10.)
502         #else:
503         #    if not key == "ArchivedArticles":
504                 # Do not download images if the feed is "Archived Articles"
505         #        self.document.connect("request-url", self._signal_request_url)
506             
507         #    self.document.clear()
508         #    self.document.open_stream("text/html")
509         #    self.document.write_stream(self.text)
510         #    self.document.close_stream()
511         
512         menu = hildon.AppMenu()
513         # Create a button and add it to the menu
514         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
515         button.set_label("Allow horizontal scrolling")
516         button.connect("clicked", self.horiz_scrolling_button)
517         menu.append(button)
518         
519         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
520         button.set_label("Open in browser")
521         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
522         menu.append(button)
523         
524         if key == "ArchivedArticles":
525             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
526             button.set_label("Remove from archived articles")
527             button.connect("clicked", self.remove_archive_button)
528         else:
529             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
530             button.set_label("Add to archived articles")
531             button.connect("clicked", self.archive_button)
532         menu.append(button)
533         
534         self.set_app_menu(menu)
535         menu.show_all()
536         
537         #self.event_box = gtk.EventBox()
538         #self.event_box.add(self.pannable_article)
539         self.add(self.pannable_article)
540         
541         
542         self.pannable_article.show_all()
543
544         self.destroyId = self.connect("destroy", self.destroyWindow)
545         
546         self.view.connect("button_press_event", self.button_pressed)
547         self.gestureId = self.view.connect("button_release_event", self.button_released)
548         #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
549
550     def load_started(self, *widget):
551         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
552         
553     def load_finished(self, *widget):
554         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
555
556     def button_pressed(self, window, event):
557         #print event.x, event.y
558         self.coords = (event.x, event.y)
559         
560     def button_released(self, window, event):
561         x = self.coords[0] - event.x
562         y = self.coords[1] - event.y
563         
564         if (2*abs(y) < abs(x)):
565             if (x > 15):
566                 self.emit("article-previous", self.id)
567             elif (x<-15):
568                 self.emit("article-next", self.id)   
569         #print x, y
570         #print "Released"
571
572     #def gesture(self, widget, direction, startx, starty):
573     #    if (direction == 3):
574     #        self.emit("article-next", self.index)
575     #    if (direction == 2):
576     #        self.emit("article-previous", self.index)
577         #print startx, starty
578         #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
579
580     def destroyWindow(self, *args):
581         self.disconnect(self.destroyId)
582         if self.set_for_removal:
583             self.emit("article-deleted", self.id)
584         else:
585             self.emit("article-closed", self.id)
586         #self.imageDownloader.stopAll()
587         self.destroy()
588         
589     def horiz_scrolling_button(self, *widget):
590         self.pannable_article.disconnect(self.gestureId)
591         self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
592         
593     def archive_button(self, *widget):
594         # Call the listing.addArchivedArticle
595         self.listing.addArchivedArticle(self.key, self.id)
596         
597     def remove_archive_button(self, *widget):
598         self.set_for_removal = True
599         
600     #def reloadArticle(self, *widget):
601     #    if threading.activeCount() > 1:
602             # Image thread are still running, come back in a bit
603     #        return True
604     #    else:
605     #        for (stream, imageThread) in self.images:
606     #            imageThread.join()
607     #            stream.write(imageThread.data)
608     #            stream.close()
609     #        return False
610     #    self.show_all()
611
612     def _signal_link_clicked(self, object, link):
613         import dbus
614         bus = dbus.SessionBus()
615         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
616         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
617         iface.open_new_window(link)
618
619     #def _signal_request_url(self, object, url, stream):
620         #print url
621     #    self.imageDownloader.queueImage(url, stream)
622         #imageThread = GetImage(url)
623         #imageThread.start()
624         #self.images.append((stream, imageThread))
625
626
627 class DisplayFeed(hildon.StackableWindow):
628     def __init__(self, listing, feed, title, key, config, updateDbusHandler):
629         hildon.StackableWindow.__init__(self)
630         self.listing = listing
631         self.feed = feed
632         self.feedTitle = title
633         self.set_title(title)
634         self.key=key
635         self.config = config
636         self.updateDbusHandler = updateDbusHandler
637         
638         self.downloadDialog = False
639         
640         #self.listing.setCurrentlyDisplayedFeed(self.key)
641         
642         self.disp = False
643         
644         menu = hildon.AppMenu()
645         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
646         button.set_label("Update feed")
647         button.connect("clicked", self.button_update_clicked)
648         menu.append(button)
649         
650         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
651         button.set_label("Mark all as read")
652         button.connect("clicked", self.buttonReadAllClicked)
653         menu.append(button)
654         
655         if key=="ArchivedArticles":
656             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
657             button.set_label("Delete read articles")
658             button.connect("clicked", self.buttonPurgeArticles)
659             menu.append(button)
660         
661         self.set_app_menu(menu)
662         menu.show_all()
663         
664         self.displayFeed()
665         
666         self.connect('configure-event', self.on_configure_event)
667         self.connect("destroy", self.destroyWindow)
668
669     def on_configure_event(self, window, event):
670         if getattr(self, 'markup_renderer', None) is None:
671             return
672
673         # Fix up the column width for wrapping the text when the window is
674         # resized (i.e. orientation changed)
675         self.markup_renderer.set_property('wrap-width', event.width-20)  
676         it = self.feedItems.get_iter_first()
677         while it is not None:
678             markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
679             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
680             it = self.feedItems.iter_next(it)
681
682     def destroyWindow(self, *args):
683         #self.feed.saveUnread(CONFIGDIR)
684         gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
685         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
686         self.emit("feed-closed", self.key)
687         self.destroy()
688         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
689         #self.listing.closeCurrentlyDisplayedFeed()
690
691     def fix_title(self, title):
692         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
693
694     def displayFeed(self):
695         self.pannableFeed = hildon.PannableArea()
696
697         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
698
699         self.feedItems = gtk.ListStore(str, str)
700         #self.feedList = gtk.TreeView(self.feedItems)
701         self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
702         selection = self.feedList.get_selection()
703         selection.set_mode(gtk.SELECTION_NONE)
704         #selection.connect("changed", lambda w: True)
705         
706         self.feedList.set_model(self.feedItems)
707         self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
708
709         
710         self.feedList.set_hover_selection(False)
711         #self.feedList.set_property('enable-grid-lines', True)
712         #self.feedList.set_property('hildon-mode', 1)
713         #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
714         
715         #self.feedList.connect('row-activated', self.on_feedList_row_activated)
716
717         vbox= gtk.VBox(False, 10)
718         vbox.pack_start(self.feedList)
719         
720         self.pannableFeed.add_with_viewport(vbox)
721
722         self.markup_renderer = gtk.CellRendererText()
723         self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
724         self.markup_renderer.set_property('background', "#333333")
725         (width, height) = self.get_size()
726         self.markup_renderer.set_property('wrap-width', width-20)
727         self.markup_renderer.set_property('ypad', 5)
728         self.markup_renderer.set_property('xpad', 5)
729         markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
730                 markup=FEED_COLUMN_MARKUP)
731         self.feedList.append_column(markup_column)
732
733         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
734         hideReadArticles = self.config.getHideReadArticles()
735         hasArticle = False
736         for id in self.feed.getIds():
737             isRead = False
738             try:
739                 isRead = self.feed.isEntryRead(id)
740             except:
741                 pass
742             if not ( isRead and hideReadArticles ):
743             #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
744                 #title = self.feed.getTitle(id)
745                 title = self.fix_title(self.feed.getTitle(id))
746     
747                 #if self.feed.isEntryRead(id):
748                 if isRead:
749                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
750                 else:
751                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
752     
753                 self.feedItems.append((markup, id))
754                 hasArticle = True
755         if hasArticle:
756             self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
757         else:
758             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
759             self.feedItems.append((markup, ""))
760
761         self.add(self.pannableFeed)
762         self.show_all()
763
764     def clear(self):
765         self.pannableFeed.destroy()
766         #self.remove(self.pannableFeed)
767
768     def on_feedList_row_activated(self, treeview, path): #, column):
769         selection = self.feedList.get_selection()
770         selection.set_mode(gtk.SELECTION_SINGLE)
771         self.feedList.get_selection().select_path(path)
772         model = treeview.get_model()
773         iter = model.get_iter(path)
774         key = model.get_value(iter, FEED_COLUMN_KEY)
775         # Emulate legacy "button_clicked" call via treeview
776         gobject.idle_add(self.button_clicked, treeview, key)
777         #return True
778
779     def button_clicked(self, button, index, previous=False, next=False):
780         #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
781         newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
782         stack = hildon.WindowStack.get_default()
783         if previous:
784             tmp = stack.peek()
785             stack.pop_and_push(1, newDisp, tmp)
786             newDisp.show()
787             gobject.timeout_add(200, self.destroyArticle, tmp)
788             #print "previous"
789             self.disp = newDisp
790         elif next:
791             newDisp.show_all()
792             if type(self.disp).__name__ == "DisplayArticle":
793                 gobject.timeout_add(200, self.destroyArticle, self.disp)
794             self.disp = newDisp
795         else:
796             self.disp = newDisp
797             self.disp.show_all()
798         
799         self.ids = []
800         if self.key == "ArchivedArticles":
801             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
802         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
803         self.ids.append(self.disp.connect("article-next", self.nextArticle))
804         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
805
806     def buttonPurgeArticles(self, *widget):
807         self.clear()
808         self.feed.purgeReadArticles()
809         self.feed.saveUnread(CONFIGDIR)
810         self.feed.saveFeed(CONFIGDIR)
811         self.displayFeed()
812
813     def destroyArticle(self, handle):
814         handle.destroyWindow()
815
816     def mark_item_read(self, key):
817         it = self.feedItems.get_iter_first()
818         while it is not None:
819             k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
820             if k == key:
821                 title = self.fix_title(self.feed.getTitle(key))
822                 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
823                 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
824                 break
825             it = self.feedItems.iter_next(it)
826
827     def nextArticle(self, object, index):
828         self.mark_item_read(index)
829         id = self.feed.getNextId(index)
830         if self.config.getHideReadArticles():
831             isRead = False
832             try:
833                 isRead = self.feed.isEntryRead(id)
834             except:
835                 pass
836             while isRead and id != index:
837                 id = self.feed.getNextId(id)
838                 isRead = False
839                 try:
840                        isRead = self.feed.isEntryRead(id)
841                 except:
842                        pass
843         if id != index:
844             self.button_clicked(object, id, next=True)
845
846     def previousArticle(self, object, index):
847         self.mark_item_read(index)
848         id = self.feed.getPreviousId(index)
849         if self.config.getHideReadArticles():
850             isRead = False
851             try:
852                 isRead = self.feed.isEntryRead(id)
853             except:
854                 pass
855             while isRead and id != index:
856                 id = self.feed.getPreviousId(id)
857                 isRead = False
858                 try:
859                        isRead = self.feed.isEntryRead(id)
860                 except:
861                        pass
862         if id != index:
863             self.button_clicked(object, id, previous=True)
864
865     def onArticleClosed(self, object, index):
866         selection = self.feedList.get_selection()
867         selection.set_mode(gtk.SELECTION_NONE)
868         self.mark_item_read(index)
869
870     def onArticleDeleted(self, object, index):
871         self.clear()
872         self.feed.removeArticle(index)
873         self.feed.saveUnread(CONFIGDIR)
874         self.feed.saveFeed(CONFIGDIR)
875         self.displayFeed()
876
877     def button_update_clicked(self, button):
878         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
879         if not type(self.downloadDialog).__name__=="DownloadBar":
880             self.pannableFeed.destroy()
881             self.vbox = gtk.VBox(False, 10)
882             self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
883             self.downloadDialog.connect("download-done", self.onDownloadsDone)
884             self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
885             self.add(self.vbox)
886             self.show_all()
887             
888     def onDownloadsDone(self, *widget):
889         self.vbox.destroy()
890         self.feed = self.listing.getFeed(self.key)
891         self.displayFeed()
892         self.updateDbusHandler.ArticleCountUpdated()
893         
894     def buttonReadAllClicked(self, button):
895         for index in self.feed.getIds():
896             self.feed.setEntryRead(index)
897             self.mark_item_read(index)
898
899
900 class FeedingIt:
901     def __init__(self):
902         # Init the windows
903         self.window = hildon.StackableWindow()
904         self.window.set_title(__appname__)
905         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
906         self.mainVbox = gtk.VBox(False,10)
907         
908         self.introLabel = gtk.Label("Loading...")
909
910         
911         self.mainVbox.pack_start(self.introLabel)
912
913         self.window.add(self.mainVbox)
914         self.window.show_all()
915         self.config = Config(self.window, CONFIGDIR+"config.ini")
916         gobject.idle_add(self.createWindow)
917         
918     def createWindow(self):
919         self.app_lock = get_lock("app_lock")
920         if self.app_lock == None:
921             self.introLabel.set_label("Update in progress, please wait.")
922             gobject.timeout_add_seconds(3, self.createWindow)
923             return False
924         self.listing = Listing(CONFIGDIR)
925         
926         self.downloadDialog = False
927         try:
928             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
929             self.orientation.set_mode(self.config.getOrientation())
930         except:
931             print "Could not start rotation manager"
932         
933         menu = hildon.AppMenu()
934         # Create a button and add it to the menu
935         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
936         button.set_label("Update feeds")
937         button.connect("clicked", self.button_update_clicked, "All")
938         menu.append(button)
939         
940         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
941         button.set_label("Mark all as read")
942         button.connect("clicked", self.button_markAll)
943         menu.append(button)
944         
945         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
946         button.set_label("Manage subscriptions")
947         button.connect("clicked", self.button_organize_clicked)
948         menu.append(button)
949
950         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
951         button.set_label("Settings")
952         button.connect("clicked", self.button_preferences_clicked)
953         menu.append(button)
954        
955         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
956         button.set_label("About")
957         button.connect("clicked", self.button_about_clicked)
958         menu.append(button)
959         
960         self.window.set_app_menu(menu)
961         menu.show_all()
962         
963         #self.feedWindow = hildon.StackableWindow()
964         #self.articleWindow = hildon.StackableWindow()
965         self.introLabel.destroy()
966         self.pannableListing = hildon.PannableArea()
967         self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
968         self.feedList = gtk.TreeView(self.feedItems)
969         self.feedList.connect('row-activated', self.on_feedList_row_activated)
970         self.pannableListing.add(self.feedList)
971
972         icon_renderer = gtk.CellRendererPixbuf()
973         icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
974         icon_column = gtk.TreeViewColumn('', icon_renderer, \
975                 pixbuf=COLUMN_ICON)
976         self.feedList.append_column(icon_column)
977
978         markup_renderer = gtk.CellRendererText()
979         markup_column = gtk.TreeViewColumn('', markup_renderer, \
980                 markup=COLUMN_MARKUP)
981         self.feedList.append_column(markup_column)
982         self.mainVbox.pack_start(self.pannableListing)
983         self.mainVbox.show_all()
984
985         self.displayListing()
986         self.autoupdate = False
987         self.checkAutoUpdate()
988         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
989         gobject.idle_add(self.enableDbus)
990         
991     def enableDbus(self):
992         self.dbusHandler = ServerObject(self)
993         self.updateDbusHandler = UpdateServerObject(self)
994
995     def button_markAll(self, button):
996         for key in self.listing.getListOfFeeds():
997             feed = self.listing.getFeed(key)
998             for id in feed.getIds():
999                 feed.setEntryRead(id)
1000             feed.saveUnread(CONFIGDIR)
1001             self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
1002         self.displayListing()
1003
1004     def button_about_clicked(self, button):
1005         HeAboutDialog.present(self.window, \
1006                 __appname__, \
1007                 ABOUT_ICON, \
1008                 __version__, \
1009                 __description__, \
1010                 ABOUT_COPYRIGHT, \
1011                 ABOUT_WEBSITE, \
1012                 ABOUT_BUGTRACKER, \
1013                 ABOUT_DONATE)
1014
1015     def button_export_clicked(self, button):
1016         opml = ExportOpmlData(self.window, self.listing)
1017         
1018     def button_import_clicked(self, button):
1019         opml = GetOpmlData(self.window)
1020         feeds = opml.getData()
1021         for (title, url) in feeds:
1022             self.listing.addFeed(title, url)
1023         self.displayListing()
1024
1025     def addFeed(self, urlIn="http://"):
1026         wizard = AddWidgetWizard(self.window, urlIn)
1027         ret = wizard.run()
1028         if ret == 2:
1029             (title, url) = wizard.getData()
1030             if (not title == '') and (not url == ''): 
1031                self.listing.addFeed(title, url)
1032         wizard.destroy()
1033         self.displayListing()
1034
1035     def button_organize_clicked(self, button):
1036         def after_closing():
1037             self.listing.saveConfig()
1038             self.displayListing()
1039         SortList(self.window, self.listing, self, after_closing)
1040
1041     def button_update_clicked(self, button, key):
1042         if not type(self.downloadDialog).__name__=="DownloadBar":
1043             self.updateDbusHandler.UpdateStarted()
1044             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1045             self.downloadDialog.connect("download-done", self.onDownloadsDone)
1046             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1047             self.mainVbox.show_all()
1048         #self.displayListing()
1049
1050     def onDownloadsDone(self, *widget):
1051         self.downloadDialog.destroy()
1052         self.downloadDialog = False
1053         self.displayListing()
1054         self.updateDbusHandler.UpdateFinished()
1055         self.updateDbusHandler.ArticleCountUpdated()
1056
1057     def button_preferences_clicked(self, button):
1058         dialog = self.config.createDialog()
1059         dialog.connect("destroy", self.prefsClosed)
1060
1061     def show_confirmation_note(self, parent, title):
1062         note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1063
1064         retcode = gtk.Dialog.run(note)
1065         note.destroy()
1066         
1067         if retcode == gtk.RESPONSE_OK:
1068             return True
1069         else:
1070             return False
1071         
1072     def displayListing(self):
1073         icon_theme = gtk.icon_theme_get_default()
1074         default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1075                 gtk.ICON_LOOKUP_USE_BUILTIN)
1076
1077         self.feedItems.clear()
1078         for key in self.listing.getListOfFeeds():
1079             unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1080             if unreadItems > 0 or not self.config.getHideReadFeeds():
1081                 title = self.listing.getFeedTitle(key)
1082                 updateTime = self.listing.getFeedUpdateTime(key)
1083                 
1084                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1085     
1086                 if unreadItems:
1087                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1088                 else:
1089                     markup = FEED_TEMPLATE % (title, subtitle)
1090     
1091                 try:
1092                     icon_filename = self.listing.getFavicon(key)
1093                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1094                             LIST_ICON_SIZE, LIST_ICON_SIZE)
1095                 except:
1096                     pixbuf = default_pixbuf
1097     
1098                 self.feedItems.append((pixbuf, markup, key))
1099
1100     def on_feedList_row_activated(self, treeview, path, column):
1101         model = treeview.get_model()
1102         iter = model.get_iter(path)
1103         key = model.get_value(iter, COLUMN_KEY)
1104         self.openFeed(key)
1105             
1106     def openFeed(self, key):
1107         try:
1108             self.feed_lock
1109         except:
1110             # If feed_lock doesn't exist, we can open the feed, else we do nothing
1111             self.feed_lock = get_lock(key)
1112             self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1113                     self.listing.getFeedTitle(key), key, \
1114                     self.config, self.updateDbusHandler)
1115             self.disp.connect("feed-closed", self.onFeedClosed)
1116         
1117
1118     def onFeedClosed(self, object, key):
1119         #self.listing.saveConfig()
1120         #del self.feed_lock
1121         gobject.idle_add(self.onFeedClosedTimeout)
1122         self.displayListing()
1123         #self.updateDbusHandler.ArticleCountUpdated()
1124         
1125     def onFeedClosedTimeout(self):
1126         self.listing.saveConfig()
1127         del self.feed_lock
1128         self.updateDbusHandler.ArticleCountUpdated()
1129      
1130     def run(self):
1131         self.window.connect("destroy", gtk.main_quit)
1132         gtk.main()
1133         self.listing.saveConfig()
1134         del self.app_lock
1135
1136     def prefsClosed(self, *widget):
1137         try:
1138             self.orientation.set_mode(self.config.getOrientation())
1139         except:
1140             pass
1141         self.displayListing()
1142         self.checkAutoUpdate()
1143
1144     def checkAutoUpdate(self, *widget):
1145         interval = int(self.config.getUpdateInterval()*3600000)
1146         if self.config.isAutoUpdateEnabled():
1147             if self.autoupdate == False:
1148                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1149                 self.autoupdate = interval
1150             elif not self.autoupdate == interval:
1151                 # If auto-update is enabled, but not at the right frequency
1152                 gobject.source_remove(self.autoupdateId)
1153                 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1154                 self.autoupdate = interval
1155         else:
1156             if not self.autoupdate == False:
1157                 gobject.source_remove(self.autoupdateId)
1158                 self.autoupdate = False
1159
1160     def automaticUpdate(self, *widget):
1161         # Need to check for internet connection
1162         # If no internet connection, try again in 10 minutes:
1163         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1164         #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1165         #from time import localtime, strftime
1166         #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1167         #file.close()
1168         self.button_update_clicked(None, None)
1169         return True
1170     
1171     def stopUpdate(self):
1172         # Not implemented in the app (see update_feeds.py)
1173         try:
1174             self.downloadDialog.listOfKeys = []
1175         except:
1176             pass
1177     
1178     def getStatus(self):
1179         status = ""
1180         for key in self.listing.getListOfFeeds():
1181             if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1182                 status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1183         if status == "":
1184             status = "No unread items"
1185         return status
1186
1187 if __name__ == "__main__":
1188     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1189     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1190     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1191     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1192     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1193     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1194     gobject.threads_init()
1195     if not isdir(CONFIGDIR):
1196         try:
1197             mkdir(CONFIGDIR)
1198         except:
1199             print "Error: Can't create configuration directory"
1200             from sys import exit
1201             exit(1)
1202     app = FeedingIt()
1203     app.run()