| 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 | |
| 46 | EVT_CLOSE(self, self.OnCloseWindow) |
| 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) |
| 159 | EVT_SIZE(self, self.OnSize) |
| 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 | |
| 232 | self.list = wxListCtrl(self, 1100, style=wxLC_REPORT) |
| 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): |
| 355 | frame = AppFrame(None, -1, "Slashdot Breaking News") |
| 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): |
| 369 | win = AppFrame(None, -1, "Slashdot Breaking News") |
| 370 | frame.otherWin = win |
| 371 | win.Show(true) |
| 372 | |
| 373 | |
| 374 | overview = __doc__ |
| 375 | |
| 376 | |
| 377 | #---------------------------------------------------------------------------- |
| 378 | |
| 379 | |