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