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