|
1 # A simple RSS reader application. |
|
2 |
|
3 # Copyright (c) 2005 Nokia Corporation |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 |
|
17 import anydbm |
|
18 |
|
19 import e32 |
|
20 import appuifw |
|
21 from key_codes import * |
|
22 |
|
23 class RSSFeed: |
|
24 def __init__(self,url,title=None): |
|
25 self.url=url |
|
26 if title is None: |
|
27 title=url |
|
28 self.listeners=[] |
|
29 self.feed={'title': title, |
|
30 'entries': [], |
|
31 'busy': False} |
|
32 self.updating=False |
|
33 def listen(self,callback): |
|
34 self.listeners.append(callback) |
|
35 def _notify_listeners(self,*args): |
|
36 for x in self.listeners: |
|
37 x(*args) |
|
38 def start_update(self): |
|
39 if self.feed['busy']: |
|
40 appuifw.note(u'Update already in progress','info') |
|
41 return |
|
42 self.feed['busy']=True |
|
43 import thread |
|
44 thread.start_new_thread(self._update,()) |
|
45 def _update(self): |
|
46 import dumbfeedparser as feedparser |
|
47 newfeed=feedparser.parse(self.url) |
|
48 self.feed.update(newfeed) |
|
49 self.feed['busy']=False |
|
50 self._notify_listeners() |
|
51 def __getitem__(self,key): |
|
52 return self.feed.__getitem__(key) |
|
53 |
|
54 class CachingRSSFeed(RSSFeed): |
|
55 def __init__(self,cache,url,title=None): |
|
56 RSSFeed.__init__(self,url,title) |
|
57 self.cache=cache |
|
58 if cache.has_key(url): |
|
59 self.feed=eval(cache[url]) |
|
60 self.dirty=False |
|
61 RSSFeed.listen(self,self._invalidate_cache) |
|
62 def _invalidate_cache(self): |
|
63 self.dirty=True |
|
64 # This method can't simply be a listener called by the RSSFeed, |
|
65 # since that call is done in a different thread and currently the |
|
66 # e32dbm module can only be used from the same thread it was |
|
67 # opened in. |
|
68 def save(self): |
|
69 if self.dirty: |
|
70 self.cache[self.url]=repr(self.feed) |
|
71 |
|
72 |
|
73 def format_feed(feed): |
|
74 if feed['busy']: |
|
75 busyflag='(loading) ' |
|
76 else: |
|
77 busyflag='' |
|
78 return unicode('%d: %s%s'%(len(feed['entries']), |
|
79 busyflag, |
|
80 feed['title'])) |
|
81 |
|
82 def handle_screen(mode): |
|
83 appuifw.app.screen = mode |
|
84 |
|
85 |
|
86 class ReaderApp: |
|
87 def __init__(self,feedlist): |
|
88 self.lock=e32.Ao_lock() |
|
89 self.exit_flag=False |
|
90 self.old_exit_key_handler=appuifw.app.exit_key_handler |
|
91 self.old_app_body=appuifw.app.body |
|
92 appuifw.app.exit_key_handler=self.handle_exit |
|
93 self.feeds=feedlist |
|
94 self.articleviewer=appuifw.Text() |
|
95 self.feedmenu=appuifw.Listbox([u''], |
|
96 self.handle_feedmenu_select) |
|
97 self.articlemenu=appuifw.Listbox([u''], |
|
98 self.handle_articlemenu_select) |
|
99 screenmodemenu=(u'Screen mode', |
|
100 ((u'full', lambda:handle_screen('full')), |
|
101 (u'large', lambda:handle_screen('large')), |
|
102 (u'normal', lambda:handle_screen('normal')))) |
|
103 self.statemap={ |
|
104 'feedmenu': |
|
105 {'menu':[(u'Update this feed', self.handle_update), |
|
106 (u'Update all feeds', self.handle_update_all), |
|
107 (u'Debug',self.handle_debug), |
|
108 screenmodemenu, |
|
109 (u'Exit',self.abort)], |
|
110 'exithandler': self.abort}, |
|
111 'articlemenu': |
|
112 {'menu':[(u'Update this feed', self.handle_update), |
|
113 (u'Update all feeds', self.handle_update_all), |
|
114 screenmodemenu, |
|
115 (u'Exit',self.abort)], |
|
116 'exithandler':lambda:self.goto_state('feedmenu')}, |
|
117 'articleview': |
|
118 {'menu':[(u'Next article',self.handle_next), |
|
119 (u'Previous article',self.handle_prev), |
|
120 screenmodemenu, |
|
121 (u'Exit',self.abort)], |
|
122 'exithandler':lambda:self.goto_state('articlemenu')}} |
|
123 self.articleviewer.bind(EKeyDownArrow,self.handle_downarrow) |
|
124 self.articleviewer.bind(EKeyUpArrow,self.handle_uparrow) |
|
125 self.articleviewer.bind(EKeyLeftArrow,self.handle_exit) |
|
126 self.articlemenu.bind(EKeyRightArrow, |
|
127 self.handle_articlemenu_select) |
|
128 self.articlemenu.bind(EKeyLeftArrow,self.handle_exit) |
|
129 self.feedmenu.bind(EKeyRightArrow,self.handle_feedmenu_select) |
|
130 for k in self.feeds: |
|
131 k.listen(self.notify) |
|
132 self.goto_state('feedmenu') |
|
133 def abort(self): |
|
134 self.exit_flag=True |
|
135 self.lock.signal() |
|
136 def close(self): |
|
137 appuifw.app.menu=[] |
|
138 appuifw.app.exit_key_handler=self.old_exit_key_handler |
|
139 appuifw.app.body=self.old_app_body |
|
140 def run(self): |
|
141 try: |
|
142 while not self.exit_flag: |
|
143 self.lock.wait() |
|
144 self.refresh() |
|
145 finally: |
|
146 self.close() |
|
147 def notify(self): |
|
148 self.lock.signal() |
|
149 def refresh(self): |
|
150 self.goto_state(self.state) |
|
151 def goto_state(self,newstate): |
|
152 # Workaround for a Series 60 bug: Prevent the cursor from |
|
153 # showing up if the articleviewer widget is not visible. |
|
154 self.articleviewer.focus=False |
|
155 if newstate=='feedmenu': |
|
156 self.feedmenu.set_list( |
|
157 [format_feed(x) for x in self.feeds]) |
|
158 appuifw.app.body=self.feedmenu |
|
159 appuifw.app.title=u'RSS reader' |
|
160 elif newstate=='articlemenu': |
|
161 if len(self.current_feed['entries'])==0: |
|
162 if appuifw.query(u'Download articles now?','query'): |
|
163 self.handle_update() |
|
164 self.goto_state('feedmenu') |
|
165 return |
|
166 self.articlemenu.set_list( |
|
167 [self.format_article_title(x) |
|
168 for x in self.current_feed['entries']]) |
|
169 appuifw.app.body=self.articlemenu |
|
170 appuifw.app.title=format_feed(self.current_feed) |
|
171 elif newstate=='articleview': |
|
172 self.articleviewer.clear() |
|
173 self.articleviewer.add( |
|
174 self.format_title_in_article(self.current_article())) |
|
175 self.articleviewer.add( |
|
176 self.format_article(self.current_article())) |
|
177 self.articleviewer.set_pos(0) |
|
178 appuifw.app.body=self.articleviewer |
|
179 appuifw.app.title=self.format_article_title( |
|
180 self.current_article()) |
|
181 else: |
|
182 raise RuntimeError("Invalid state %s"%state) |
|
183 appuifw.app.menu=self.statemap[newstate]['menu'] |
|
184 self.state=newstate |
|
185 def current_article(self): |
|
186 return self.current_feed['entries'][self.current_article_index] |
|
187 def handle_update(self): |
|
188 if self.state=='feedmenu': |
|
189 self.current_feed=self.feeds[self.feedmenu.current()] |
|
190 self.current_feed.start_update() |
|
191 self.refresh() |
|
192 def handle_update_all(self): |
|
193 for k in self.feeds: |
|
194 if not k['busy']: |
|
195 k.start_update() |
|
196 self.refresh() |
|
197 def handle_feedmenu_select(self): |
|
198 self.current_feed=self.feeds[self.feedmenu.current()] |
|
199 self.goto_state('articlemenu') |
|
200 def handle_articlemenu_select(self): |
|
201 self.current_article_index=self.articlemenu.current() |
|
202 self.goto_state('articleview') |
|
203 def handle_debug(self): |
|
204 import btconsole |
|
205 btconsole.run('Entering debug mode.',locals()) |
|
206 def handle_next(self): |
|
207 if (self.current_article_index >= |
|
208 len(self.current_feed['entries'])-1): |
|
209 return |
|
210 self.current_article_index += 1 |
|
211 self.refresh() |
|
212 def handle_prev(self): |
|
213 if self.current_article_index == 0: |
|
214 return |
|
215 self.current_article_index -= 1 |
|
216 self.refresh() |
|
217 def handle_downarrow(self): |
|
218 article_length=self.articleviewer.len() |
|
219 cursor_pos=self.articleviewer.get_pos() |
|
220 if cursor_pos==article_length: |
|
221 self.handle_next() |
|
222 else: |
|
223 self.articleviewer.set_pos(min(cursor_pos+100, |
|
224 article_length)) |
|
225 def handle_uparrow(self): |
|
226 cursor_pos=self.articleviewer.get_pos() |
|
227 if cursor_pos==0: |
|
228 self.handle_prev() |
|
229 self.articleviewer.set_pos(self.articleviewer.len()) |
|
230 else: |
|
231 self.articleviewer.set_pos(max(cursor_pos-100,0)) |
|
232 def format_title_in_article(self, article): |
|
233 self.articleviewer.highlight_color = (255,240,80) |
|
234 self.articleviewer.style = (appuifw.STYLE_UNDERLINE| |
|
235 appuifw.HIGHLIGHT_ROUNDED) |
|
236 self.articleviewer.font = 'title' |
|
237 self.articleviewer.color = (0,0,255) |
|
238 return unicode("%(title)s\n"%article) |
|
239 |
|
240 def format_article(self, article): |
|
241 self.articleviewer.highlight_color = (0,0,0) |
|
242 self.articleviewer.style = 0 |
|
243 self.articleviewer.font = 'normal' |
|
244 self.articleviewer.color = (0,0,0) |
|
245 return unicode("%(summary)s"%article) |
|
246 |
|
247 def format_article_title(self, article): |
|
248 return unicode("%(title)s"%article) |
|
249 def handle_exit(self): |
|
250 self.statemap[self.state]['exithandler']() |
|
251 |
|
252 class DummyFeed: |
|
253 def __init__(self,data): self.data=data |
|
254 def listen(self,callback): pass |
|
255 def start_update(self): pass |
|
256 def __getitem__(self,key): return self.data.__getitem__(key) |
|
257 def save(self): pass |
|
258 dummyfeed=DummyFeed({'title': 'Dummy feed', |
|
259 'entries': [{'title':'Dummy story', |
|
260 'summary':'Blah blah blah.'}, |
|
261 {'title':'Another dummy story', |
|
262 'summary':'Foo, bar and baz.'}], |
|
263 'busy': False}) |
|
264 |
|
265 def main(): |
|
266 old_title=appuifw.app.title |
|
267 appuifw.app.title=u'RSS reader' |
|
268 cache=anydbm.open(u'c:\\rsscache','c') |
|
269 feeds=[ CachingRSSFeed(url='http://slashdot.org/index.rss', |
|
270 title='Slashdot', |
|
271 cache=cache), |
|
272 CachingRSSFeed(url='http://news.bbc.co.uk/rss/newsonline_world_edition/front_page/rss091.xml', |
|
273 title='BBC', |
|
274 cache=cache), |
|
275 dummyfeed] |
|
276 app = ReaderApp(feeds) |
|
277 # Import heavyweight modules in the background to improve application |
|
278 # startup time. |
|
279 def import_modules(): |
|
280 import dumbfeedparser as feedparser |
|
281 import btconsole |
|
282 import thread |
|
283 thread.start_new_thread(import_modules,()) |
|
284 try: |
|
285 app.run() |
|
286 finally: |
|
287 for feed in feeds: |
|
288 feed.save() |
|
289 cache.close() |
|
290 appuifw.app.title=old_title |
|
291 |
|
292 if __name__=='__main__': |
|
293 main() |
|
294 |