src/extras/examples/rssreader.py
changeset 0 ca70ae20a155
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/extras/examples/rssreader.py	Tue Feb 16 10:07:05 2010 +0530
@@ -0,0 +1,294 @@
+# A simple RSS reader application.
+
+# Copyright (c) 2005 Nokia Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import anydbm
+
+import e32
+import appuifw
+from key_codes import *
+
+class RSSFeed:
+    def __init__(self,url,title=None):
+        self.url=url
+        if title is None:
+            title=url
+        self.listeners=[]
+        self.feed={'title': title,
+                   'entries': [],
+                   'busy': False}
+        self.updating=False
+    def listen(self,callback):
+        self.listeners.append(callback)
+    def _notify_listeners(self,*args):
+        for x in self.listeners:
+            x(*args)
+    def start_update(self):
+        if self.feed['busy']:
+            appuifw.note(u'Update already in progress','info')
+            return
+        self.feed['busy']=True
+        import thread
+        thread.start_new_thread(self._update,())
+    def _update(self):
+        import dumbfeedparser as feedparser
+        newfeed=feedparser.parse(self.url)
+        self.feed.update(newfeed)
+        self.feed['busy']=False
+        self._notify_listeners()
+    def __getitem__(self,key):
+        return self.feed.__getitem__(key)
+
+class CachingRSSFeed(RSSFeed):
+    def __init__(self,cache,url,title=None):
+        RSSFeed.__init__(self,url,title)
+        self.cache=cache
+        if cache.has_key(url):
+            self.feed=eval(cache[url])
+        self.dirty=False
+        RSSFeed.listen(self,self._invalidate_cache)
+    def _invalidate_cache(self):
+        self.dirty=True
+    # This method can't simply be a listener called by the RSSFeed,
+    # since that call is done in a different thread and currently the
+    # e32dbm module can only be used from the same thread it was
+    # opened in.
+    def save(self):
+        if self.dirty:
+            self.cache[self.url]=repr(self.feed)
+
+
+def format_feed(feed):
+    if feed['busy']:
+        busyflag='(loading) '
+    else:
+        busyflag=''
+    return unicode('%d: %s%s'%(len(feed['entries']),
+                               busyflag,
+                               feed['title']))
+
+def handle_screen(mode):
+    appuifw.app.screen = mode
+
+
+class ReaderApp:
+    def __init__(self,feedlist):
+        self.lock=e32.Ao_lock()
+        self.exit_flag=False
+        self.old_exit_key_handler=appuifw.app.exit_key_handler
+        self.old_app_body=appuifw.app.body
+        appuifw.app.exit_key_handler=self.handle_exit
+        self.feeds=feedlist
+        self.articleviewer=appuifw.Text()
+        self.feedmenu=appuifw.Listbox([u''],
+                                      self.handle_feedmenu_select)
+        self.articlemenu=appuifw.Listbox([u''],
+                                         self.handle_articlemenu_select)
+        screenmodemenu=(u'Screen mode',
+                        ((u'full', lambda:handle_screen('full')),
+                         (u'large', lambda:handle_screen('large')),
+                         (u'normal', lambda:handle_screen('normal'))))
+        self.statemap={
+            'feedmenu':
+            {'menu':[(u'Update this feed', self.handle_update),
+                     (u'Update all feeds', self.handle_update_all),
+                     (u'Debug',self.handle_debug),
+                     screenmodemenu,
+                     (u'Exit',self.abort)],
+             'exithandler': self.abort},
+            'articlemenu':
+            {'menu':[(u'Update this feed', self.handle_update),
+                     (u'Update all feeds', self.handle_update_all),
+                     screenmodemenu,
+                     (u'Exit',self.abort)],
+             'exithandler':lambda:self.goto_state('feedmenu')},
+            'articleview':
+            {'menu':[(u'Next article',self.handle_next),
+                     (u'Previous article',self.handle_prev),
+                     screenmodemenu,
+                     (u'Exit',self.abort)],
+             'exithandler':lambda:self.goto_state('articlemenu')}}
+        self.articleviewer.bind(EKeyDownArrow,self.handle_downarrow)
+        self.articleviewer.bind(EKeyUpArrow,self.handle_uparrow)
+        self.articleviewer.bind(EKeyLeftArrow,self.handle_exit)
+        self.articlemenu.bind(EKeyRightArrow,
+                              self.handle_articlemenu_select)
+        self.articlemenu.bind(EKeyLeftArrow,self.handle_exit)
+        self.feedmenu.bind(EKeyRightArrow,self.handle_feedmenu_select)
+        for k in self.feeds:
+            k.listen(self.notify)        
+        self.goto_state('feedmenu')
+    def abort(self):
+        self.exit_flag=True
+        self.lock.signal()
+    def close(self):
+        appuifw.app.menu=[]
+        appuifw.app.exit_key_handler=self.old_exit_key_handler
+        appuifw.app.body=self.old_app_body
+    def run(self):
+        try:
+            while not self.exit_flag:
+                self.lock.wait()
+                self.refresh()
+        finally:
+            self.close()
+    def notify(self):
+        self.lock.signal()
+    def refresh(self):
+        self.goto_state(self.state)
+    def goto_state(self,newstate):
+        # Workaround for a Series 60 bug: Prevent the cursor from
+        # showing up if the articleviewer widget is not visible.
+        self.articleviewer.focus=False
+        if newstate=='feedmenu':
+            self.feedmenu.set_list(
+                [format_feed(x) for x in self.feeds])
+            appuifw.app.body=self.feedmenu
+            appuifw.app.title=u'RSS reader'
+        elif newstate=='articlemenu':
+            if len(self.current_feed['entries'])==0:
+                if appuifw.query(u'Download articles now?','query'):
+                    self.handle_update()
+                self.goto_state('feedmenu')
+                return 
+            self.articlemenu.set_list(
+                [self.format_article_title(x)
+                 for x in self.current_feed['entries']])
+            appuifw.app.body=self.articlemenu
+            appuifw.app.title=format_feed(self.current_feed)
+        elif newstate=='articleview':
+            self.articleviewer.clear()
+            self.articleviewer.add(
+                self.format_title_in_article(self.current_article()))
+            self.articleviewer.add(
+                self.format_article(self.current_article()))
+            self.articleviewer.set_pos(0)
+            appuifw.app.body=self.articleviewer
+            appuifw.app.title=self.format_article_title(
+                self.current_article())
+        else:
+            raise RuntimeError("Invalid state %s"%state)
+        appuifw.app.menu=self.statemap[newstate]['menu']
+        self.state=newstate
+    def current_article(self):
+        return self.current_feed['entries'][self.current_article_index]
+    def handle_update(self):
+        if self.state=='feedmenu':
+            self.current_feed=self.feeds[self.feedmenu.current()]
+        self.current_feed.start_update()
+        self.refresh()
+    def handle_update_all(self):
+        for k in self.feeds:
+            if not k['busy']:
+                k.start_update()
+        self.refresh()
+    def handle_feedmenu_select(self):
+        self.current_feed=self.feeds[self.feedmenu.current()]
+        self.goto_state('articlemenu')
+    def handle_articlemenu_select(self):
+        self.current_article_index=self.articlemenu.current()
+        self.goto_state('articleview')
+    def handle_debug(self):
+        import btconsole
+        btconsole.run('Entering debug mode.',locals())
+    def handle_next(self):
+        if (self.current_article_index >=
+            len(self.current_feed['entries'])-1):
+            return
+        self.current_article_index += 1
+        self.refresh()
+    def handle_prev(self):
+        if self.current_article_index == 0:
+            return
+        self.current_article_index -= 1
+        self.refresh()
+    def handle_downarrow(self):
+        article_length=self.articleviewer.len()
+        cursor_pos=self.articleviewer.get_pos()
+        if cursor_pos==article_length:
+            self.handle_next()
+        else:
+            self.articleviewer.set_pos(min(cursor_pos+100,
+                                           article_length))
+    def handle_uparrow(self):
+        cursor_pos=self.articleviewer.get_pos()
+        if cursor_pos==0:
+            self.handle_prev()
+            self.articleviewer.set_pos(self.articleviewer.len())
+        else:
+            self.articleviewer.set_pos(max(cursor_pos-100,0))
+    def format_title_in_article(self, article):
+        self.articleviewer.highlight_color = (255,240,80)
+        self.articleviewer.style = (appuifw.STYLE_UNDERLINE|
+                                    appuifw.HIGHLIGHT_ROUNDED)
+        self.articleviewer.font = 'title'
+        self.articleviewer.color = (0,0,255)
+        return unicode("%(title)s\n"%article)
+
+    def format_article(self, article):
+        self.articleviewer.highlight_color = (0,0,0)
+        self.articleviewer.style = 0
+        self.articleviewer.font = 'normal'
+        self.articleviewer.color = (0,0,0)
+        return unicode("%(summary)s"%article)
+
+    def format_article_title(self, article):
+        return unicode("%(title)s"%article)
+    def handle_exit(self):
+        self.statemap[self.state]['exithandler']()
+
+class DummyFeed:
+    def __init__(self,data): self.data=data
+    def listen(self,callback): pass
+    def start_update(self): pass
+    def __getitem__(self,key): return self.data.__getitem__(key)
+    def save(self): pass
+dummyfeed=DummyFeed({'title': 'Dummy feed',
+                     'entries': [{'title':'Dummy story',
+                                  'summary':'Blah blah blah.'},
+                                 {'title':'Another dummy story',
+                                  'summary':'Foo, bar and baz.'}],
+                     'busy': False})
+
+def main():
+    old_title=appuifw.app.title
+    appuifw.app.title=u'RSS reader'
+    cache=anydbm.open(u'c:\\rsscache','c')
+    feeds=[ CachingRSSFeed(url='http://slashdot.org/index.rss',
+                           title='Slashdot',
+                           cache=cache),
+            CachingRSSFeed(url='http://news.bbc.co.uk/rss/newsonline_world_edition/front_page/rss091.xml',
+                           title='BBC',
+                           cache=cache),
+            dummyfeed]
+    app = ReaderApp(feeds)
+    # Import heavyweight modules in the background to improve application
+    # startup time.
+    def import_modules():
+        import dumbfeedparser as feedparser
+        import btconsole
+    import thread
+    thread.start_new_thread(import_modules,())
+    try:
+        app.run()
+    finally:
+        for feed in feeds:
+            feed.save()
+        cache.close()
+        appuifw.app.title=old_title
+
+if __name__=='__main__':
+    main()
+