]>
Commit | Line | Data |
---|---|---|
cf694132 RD |
1 | #!/usr/bin/python |
2 | """This is SlashDot 1.2 | |
3 | ||
4 | It's the obligatory Slashdot.org headlines reader that | |
5 | any modern widget set/library must have in order to be taken | |
6 | seriously :-) | |
7 | ||
8 | Usage is quite simple; wxSlash attempts to download the | |
9 | 'ultramode.txt' file from http://slashdot.org, which | |
10 | contains the headlines in a computer friendly format. It | |
11 | then displays said headlines in a wxWindows list control. | |
12 | ||
13 | You can read articles using either Python's html library | |
14 | or an external browser. Uncheck the 'browser->internal' menu | |
15 | item to use the latter option. Use the settings dialog box | |
16 | to set which external browser is started. | |
17 | ||
18 | This code is available under the wxWindows license, see | |
19 | elsewhere. If you modify this code, be aware of the fact | |
20 | that slashdot.org's maintainer, CmdrTaco, explicitly asks | |
21 | 'ultramode.txt' downloaders not to do this automatically | |
22 | more than twice per hour. If this feature is abused, | |
23 | CmdrTaco may remove the ultramode file completely and that | |
24 | will make a *lot* of people unhappy. | |
25 | ||
26 | I want to thank Alex Shnitman whose slashes.pl | |
27 | (Perl/GTK) script gave me the idea for this applet. | |
28 | ||
29 | Have fun with it, | |
30 | ||
31 | Harm van der Heijden (H.v.d.Heijden@phys.tue.nl) | |
32 | """ | |
33 | ||
34 | from wxPython.wx import * | |
35 | from httplib import HTTP | |
36 | from htmllib import HTMLParser | |
37 | import os | |
38 | import re | |
39 | import formatter | |
40 | ||
41 | class HTMLTextView(wxFrame): | |
42 | def __init__(self, parent, id, title='HTMLTextView', url=None): | |
43 | wxFrame.__init__(self, parent, id, title, wxPyDefaultPosition, | |
44 | wxSize(600,400)) | |
45 | ||
f6bcfd97 | 46 | EVT_CLOSE(self, self.OnCloseWindow) |
cf694132 RD |
47 | self.mainmenu = wxMenuBar() |
48 | ||
49 | menu = wxMenu() | |
50 | menu.Append(201, '&Open URL...', 'Open URL') | |
51 | EVT_MENU(self, 201, self.OnFileOpen) | |
52 | menu.Append(209, 'E&xit', 'Exit viewer') | |
53 | EVT_MENU(self, 209, self.OnFileExit) | |
54 | ||
55 | self.mainmenu.Append(menu, '&File') | |
56 | self.SetMenuBar(self.mainmenu) | |
57 | self.CreateStatusBar(1) | |
58 | ||
59 | self.text = wxTextCtrl(self, -1, "", wxPyDefaultPosition, | |
60 | wxPyDefaultSize, wxTE_MULTILINE | wxTE_READONLY) | |
61 | ||
62 | if (url): | |
63 | self.OpenURL(url) | |
64 | ||
65 | def logprint(self, x): | |
66 | self.SetStatusText(x) | |
67 | ||
68 | def OpenURL(self, url): | |
69 | self.url = url | |
70 | m = re.match('file:(\S+)\s*', url) | |
71 | if m: | |
72 | f = open(m.groups()[0],'r') | |
73 | else: | |
74 | m = re.match('http://([^/]+)(/\S*)\s*', url) | |
75 | if m: | |
76 | host = m.groups()[0] | |
77 | path = m.groups()[1] | |
78 | else: | |
79 | m = re.match('http://(\S+)\s*', url) | |
80 | if not m: | |
81 | # Invalid URL | |
82 | self.logprint("Invalid or unsupported URL: %s" % (url)) | |
83 | return | |
84 | host = m.groups()[0] | |
85 | path = '' | |
86 | f = RetrieveAsFile(host,path,self.logprint) | |
87 | if not f: | |
88 | self.logprint("Could not open %s" % (url)) | |
89 | return | |
90 | self.logprint("Receiving data...") | |
91 | data = f.read() | |
92 | tmp = open('tmphtml.txt','w') | |
93 | fmt = formatter.AbstractFormatter(formatter.DumbWriter(tmp)) | |
94 | p = HTMLParser(fmt) | |
95 | self.logprint("Parsing data...") | |
96 | p.feed(data) | |
97 | p.close() | |
98 | tmp.close() | |
99 | tmp = open('tmphtml.txt', 'r') | |
100 | self.text.SetValue(tmp.read()) | |
101 | self.SetTitle(url) | |
102 | self.logprint(url) | |
103 | ||
104 | def OnFileOpen(self, event): | |
105 | dlg = wxTextEntryDialog(self, "Enter URL to open:", "") | |
106 | if dlg.ShowModal() == wxID_OK: | |
107 | url = dlg.GetValue() | |
108 | else: | |
109 | url = None | |
110 | if url: | |
111 | self.OpenURL(url) | |
112 | ||
113 | def OnFileExit(self, event): | |
114 | self.Close() | |
115 | ||
116 | def OnCloseWindow(self, event): | |
117 | self.Destroy() | |
118 | ||
119 | ||
120 | def ParseSlashdot(f): | |
121 | art_sep = re.compile('%%\r?\n') | |
122 | line_sep = re.compile('\r?\n') | |
123 | data = f.read() | |
124 | list = art_sep.split(data) | |
125 | art_list = [] | |
126 | for i in range(1,len(list)-1): | |
127 | art_list.append(line_sep.split(list[i])) | |
128 | return art_list | |
129 | ||
130 | def myprint(x): | |
131 | print x | |
132 | ||
133 | def RetrieveAsFile(host, path='', logprint = myprint): | |
134 | try: | |
135 | h = HTTP(host) | |
136 | except: | |
137 | logprint("Failed to create HTTP connection to %s... is the network available?" % (host)) | |
138 | return None | |
139 | h.putrequest('GET',path) | |
140 | h.putheader('Accept','text/html') | |
141 | h.putheader('Accept','text/plain') | |
142 | h.endheaders() | |
143 | errcode, errmsg, headers = h.getreply() | |
144 | if errcode != 200: | |
145 | logprint("HTTP error code %d: %s" % (errcode, errmsg)) | |
146 | return None | |
147 | f = h.getfile() | |
148 | # f = open('/home/harm/ultramode.txt','r') | |
149 | return f | |
150 | ||
151 | ||
152 | class AppStatusBar(wxStatusBar): | |
153 | def __init__(self, parent): | |
154 | wxStatusBar.__init__(self,parent, -1) | |
155 | self.SetFieldsCount(2) | |
156 | self.SetStatusWidths([-1, 100]) | |
157 | self.but = wxButton(self, 1001, "Refresh") | |
158 | EVT_BUTTON(self, 1001, parent.OnViewRefresh) | |
f6bcfd97 | 159 | EVT_SIZE(self, self.OnSize) |
cf694132 RD |
160 | self.OnSize(None) |
161 | ||
162 | def logprint(self,x): | |
163 | self.SetStatusText(x,0) | |
164 | ||
165 | def OnSize(self, event): | |
166 | rect = self.GetFieldRect(1) | |
167 | self.but.SetPosition(wxPoint(rect.x+2, rect.y+2)) | |
168 | self.but.SetSize(wxSize(rect.width-4, rect.height-4)) | |
169 | ||
170 | # This is a simple timer class to start a function after a short delay; | |
171 | class QuickTimer(wxTimer): | |
172 | def __init__(self, func, wait=100): | |
173 | wxTimer.__init__(self) | |
174 | self.callback = func | |
175 | self.Start(wait); # wait .1 second (.001 second doesn't work. why?) | |
176 | def Notify(self): | |
177 | self.Stop(); | |
178 | apply(self.callback, ()); | |
179 | ||
180 | class AppFrame(wxFrame): | |
181 | def __init__(self, parent, id, title): | |
182 | wxFrame.__init__(self, parent, id, title, wxPyDefaultPosition, | |
183 | wxSize(650, 250)) | |
184 | ||
185 | # if the window manager closes the window: | |
186 | EVT_CLOSE(self, self.OnCloseWindow); | |
187 | ||
188 | # Now Create the menu bar and items | |
189 | self.mainmenu = wxMenuBar() | |
190 | ||
191 | menu = wxMenu() | |
192 | menu.Append(209, 'E&xit', 'Enough of this already!') | |
193 | EVT_MENU(self, 209, self.OnFileExit) | |
194 | self.mainmenu.Append(menu, '&File') | |
195 | menu = wxMenu() | |
196 | menu.Append(210, '&Refresh', 'Refresh headlines') | |
197 | EVT_MENU(self, 210, self.OnViewRefresh) | |
198 | menu.Append(211, '&Slashdot Index', 'View Slashdot index') | |
199 | EVT_MENU(self, 211, self.OnViewIndex) | |
200 | menu.Append(212, 'Selected &Article', 'View selected article') | |
201 | EVT_MENU(self, 212, self.OnViewArticle) | |
202 | self.mainmenu.Append(menu, '&View') | |
203 | menu = wxMenu() | |
204 | menu.Append(220, '&Internal', 'Use internal text browser',TRUE) | |
205 | menu.Check(220, true) | |
206 | self.UseInternal = 1; | |
207 | EVT_MENU(self, 220, self.OnBrowserInternal) | |
208 | menu.Append(222, '&Settings...', 'External browser Settings') | |
209 | EVT_MENU(self, 222, self.OnBrowserSettings) | |
210 | self.mainmenu.Append(menu, '&Browser') | |
211 | menu = wxMenu() | |
212 | menu.Append(230, '&About', 'Some documentation'); | |
213 | EVT_MENU(self, 230, self.OnAbout) | |
214 | self.mainmenu.Append(menu, '&Help') | |
215 | ||
216 | self.SetMenuBar(self.mainmenu) | |
217 | ||
218 | if wxPlatform == '__WXGTK__': | |
219 | # I like lynx. Also Netscape 4.5 doesn't react to my cmdline opts | |
220 | self.BrowserSettings = "xterm -e lynx %s &" | |
221 | elif wxPlatform == '__WXMSW__': | |
222 | # netscape 4.x likes to hang out here... | |
223 | self.BrowserSettings = '\\progra~1\\Netscape\\Communicator\\Program\\netscape.exe %s' | |
224 | else: | |
225 | # a wild guess... | |
226 | self.BrowserSettings = 'netscape %s' | |
227 | ||
228 | # A status bar to tell people what's happening | |
229 | self.sb = AppStatusBar(self) | |
230 | self.SetStatusBar(self.sb) | |
231 | ||
06c0fba4 | 232 | self.list = wxListCtrl(self, 1100, style=wxLC_REPORT) |
cf694132 RD |
233 | self.list.InsertColumn(0, 'Subject') |
234 | self.list.InsertColumn(1, 'Date') | |
235 | self.list.InsertColumn(2, 'Posted by') | |
236 | self.list.InsertColumn(3, 'Comments') | |
237 | self.list.SetColumnWidth(0, 300) | |
238 | self.list.SetColumnWidth(1, 150) | |
239 | self.list.SetColumnWidth(2, 100) | |
240 | self.list.SetColumnWidth(3, 100) | |
241 | ||
242 | EVT_LIST_ITEM_SELECTED(self, 1100, self.OnItemSelected) | |
243 | EVT_LEFT_DCLICK(self.list, self.OnLeftDClick) | |
244 | ||
245 | self.logprint("Connecting to slashdot... Please wait.") | |
246 | # wxYield doesn't yet work here. That's why we use a timer | |
247 | # to make sure that we see some GUI stuff before the slashdot | |
248 | # file is transfered. | |
249 | self.timer = QuickTimer(self.DoRefresh, 1000) | |
250 | ||
251 | def logprint(self, x): | |
252 | self.sb.logprint(x) | |
253 | ||
254 | def OnFileExit(self, event): | |
255 | self.Destroy() | |
256 | ||
257 | def DoRefresh(self): | |
258 | f = RetrieveAsFile('slashdot.org','/ultramode.txt',self.sb.logprint) | |
259 | art_list = ParseSlashdot(f) | |
260 | self.list.DeleteAllItems() | |
261 | self.url = [] | |
262 | self.current = -1 | |
263 | i = 0; | |
264 | for article in art_list: | |
265 | self.list.InsertStringItem(i, article[0]) | |
266 | self.list.SetStringItem(i, 1, article[2]) | |
267 | self.list.SetStringItem(i, 2, article[3]) | |
268 | self.list.SetStringItem(i, 3, article[6]) | |
269 | self.url.append(article[1]) | |
270 | i = i + 1 | |
271 | self.logprint("File retrieved OK.") | |
272 | ||
273 | def OnViewRefresh(self, event): | |
274 | self.logprint("Connecting to slashdot... Please wait."); | |
275 | wxYield() | |
276 | self.DoRefresh() | |
277 | ||
278 | def DoViewIndex(self): | |
279 | if self.UseInternal: | |
280 | self.view = HTMLTextView(self, -1, 'slashdot.org', | |
281 | 'http://slashdot.org') | |
282 | self.view.Show(true) | |
283 | else: | |
284 | self.logprint(self.BrowserSettings % ('http://slashdot.org')) | |
285 | #os.system(self.BrowserSettings % ('http://slashdot.org')) | |
286 | wxExecute(self.BrowserSettings % ('http://slashdot.org')) | |
287 | self.logprint("OK") | |
288 | ||
289 | def OnViewIndex(self, event): | |
290 | self.logprint("Starting browser... Please wait.") | |
291 | wxYield() | |
292 | self.DoViewIndex() | |
293 | ||
294 | def DoViewArticle(self): | |
295 | if self.current<0: return | |
296 | url = self.url[self.current] | |
297 | if self.UseInternal: | |
298 | self.view = HTMLTextView(self, -1, url, url) | |
299 | self.view.Show(true) | |
300 | else: | |
301 | self.logprint(self.BrowserSettings % (url)) | |
302 | os.system(self.BrowserSettings % (url)) | |
303 | self.logprint("OK") | |
304 | ||
305 | def OnViewArticle(self, event): | |
306 | self.logprint("Starting browser... Please wait.") | |
307 | wxYield() | |
308 | self.DoViewArticle() | |
309 | ||
310 | def OnBrowserInternal(self, event): | |
311 | if self.mainmenu.Checked(220): | |
312 | self.UseInternal = 1 | |
313 | else: | |
314 | self.UseInternal = 0 | |
315 | ||
316 | def OnBrowserSettings(self, event): | |
317 | dlg = wxTextEntryDialog(self, "Enter command to view URL.\nUse %s as a placeholder for the URL.", "", self.BrowserSettings); | |
318 | if dlg.ShowModal() == wxID_OK: | |
319 | self.BrowserSettings = dlg.GetValue() | |
320 | ||
321 | def OnAbout(self, event): | |
322 | dlg = wxMessageDialog(self, __doc__, "wxSlash", wxOK | wxICON_INFORMATION) | |
323 | dlg.ShowModal() | |
324 | ||
325 | def OnItemSelected(self, event): | |
326 | self.current = event.m_itemIndex | |
327 | self.logprint("URL: %s" % (self.url[self.current])) | |
328 | ||
329 | def OnLeftDClick(self, event): | |
330 | (x,y) = event.Position(); | |
331 | # Actually, we should convert x,y to logical coords using | |
332 | # a dc, but only for a wxScrolledWindow widget. | |
333 | # Now wxGTK derives wxListCtrl from wxScrolledWindow, | |
334 | # and wxMSW from wxControl... So that doesn't work. | |
335 | #dc = wxClientDC(self.list) | |
336 | ##self.list.PrepareDC(dc) | |
337 | #x = dc.DeviceToLogicalX( event.GetX() ) | |
338 | #y = dc.DeviceToLogicalY( event.GetY() ) | |
339 | id = self.list.HitTest(wxPoint(x,y)) | |
340 | #print "Double click at %d %d" % (x,y), id | |
341 | # Okay, we got a double click. Let's assume it's the current selection | |
342 | wxYield() | |
343 | self.OnViewArticle(event) | |
344 | ||
345 | def OnCloseWindow(self, event): | |
346 | self.Destroy() | |
347 | ||
348 | ||
349 | #--------------------------------------------------------------------------- | |
350 | # if running standalone | |
351 | ||
352 | if __name__ == '__main__': | |
353 | class MyApp(wxApp): | |
354 | def OnInit(self): | |
493f1553 | 355 | frame = AppFrame(None, -1, "Slashdot Breaking News") |
cf694132 RD |
356 | frame.Show(true) |
357 | self.SetTopWindow(frame) | |
358 | return true | |
359 | ||
360 | app = MyApp(0) | |
361 | app.MainLoop() | |
362 | ||
363 | ||
364 | ||
365 | #--------------------------------------------------------------------------- | |
366 | # if running as part of the Demo Framework... | |
367 | ||
368 | def runTest(frame, nb, log): | |
493f1553 | 369 | win = AppFrame(None, -1, "Slashdot Breaking News") |
cf694132 RD |
370 | frame.otherWin = win |
371 | win.Show(true) | |
372 | ||
373 | ||
374 | overview = __doc__ | |
375 | ||
376 | ||
377 | #---------------------------------------------------------------------------- | |
378 | ||
379 |