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