| 1 | # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
| 2 | # |
| 3 | # o wxPyInformationalMessagesFrame -> PyInformationalMessagesFrame |
| 4 | # o dummy_wxPyInformationalMessagesFrame -> dummy_PyInformationalMessagesFrame |
| 5 | # |
| 6 | |
| 7 | """ |
| 8 | infoframe.py |
| 9 | Released under wxWindows license etc. |
| 10 | |
| 11 | This is a fairly rudimentary, but slightly fancier tha |
| 12 | wxPyOnDemandOutputWindow (on which it's based; thanks Robin), version |
| 13 | of the same sort of thing: a file-like class called |
| 14 | wxInformationalMessagesFrame. This window also has a status bar with a |
| 15 | couple of buttons for controlling the echoing of all output to a file |
| 16 | with a randomly-chosen filename... |
| 17 | |
| 18 | The class behaves similarly to wxPyOnDemandOutputWindow in that (at |
| 19 | least by default) the frame does not appear until written to, but is |
| 20 | somewhat different in that, either under programmatic (the |
| 21 | DisableOutput method) or user (the frame's close button, it's status |
| 22 | bar's "Dismiss" button, or a "Disable output" item of some menu, |
| 23 | perhaps of some other frame), the frame will be destroyed, an |
| 24 | associated file closed, and writing to it will then do nothing. This |
| 25 | can be reversed: either under programmatic (the EnableOutput method) |
| 26 | or user (an "Enable output" item of some menu), a new frame will be |
| 27 | opened,And an associated file (with a "randomly"selected filename) |
| 28 | opened for writing [to which all subsequent displayed messages will be |
| 29 | echoed]. |
| 30 | |
| 31 | Please note that, like wxPyOnDemandOutputWindow, the instance is not |
| 32 | itself a subclass of wxWindow: when the window is open (and ONLY |
| 33 | then), it's "frame" attribute is the actual instance of wFrame... |
| 34 | |
| 35 | Typical usage: |
| 36 | from wxPython.lib.infoframe import * |
| 37 | ... # ... modify your wxApp as follows: |
| 38 | class myApp(wxApp): |
| 39 | outputWindowClass = PyInformationalMessagesFrame |
| 40 | ... |
| 41 | If you're running on Linux, you'll also have to supply an argument 1 to your |
| 42 | constructor of myApp to redirect stdout/stderr to this window (it's done |
| 43 | automatically for you on Windows). |
| 44 | |
| 45 | If you don't want to redirect stdout/stderr, but use the class directly: do |
| 46 | it this way: |
| 47 | |
| 48 | InformationalMessagesFrame = PyInformationalMessagesFrame\ |
| 49 | ([options from progname (default ""), |
| 50 | txt (default "informational |
| 51 | messages"]) |
| 52 | #^^^^ early in the program |
| 53 | ... |
| 54 | InformationalMessagesFrame([comma-separated list of items to |
| 55 | display. Note that these will never |
| 56 | be separated by spaces as they may |
| 57 | be when used in the Python 'print' |
| 58 | command]) |
| 59 | |
| 60 | The latter statement, of course, may be repeated arbitrarily often. |
| 61 | The window will not appear until it is written to, and it may be |
| 62 | manually closed by the user, after which it will reappear again until |
| 63 | written to... Also note that all output is echoed to a file with a |
| 64 | randomly-generated name [see the mktemp module in the standard |
| 65 | library], in the directory given as the 'dir' keyword argument to the |
| 66 | InformationalMessagesFrame constructor [which has a default value of |
| 67 | '.'), or set via the method SetOutputDirectory... This file will be |
| 68 | closed with the window--a new one will be created [by default] upon |
| 69 | each subsequent reopening. |
| 70 | |
| 71 | Please also note the methods EnableOutput and DisableOutput, and the |
| 72 | possible arguments for the constructor in the code below... (* TO DO: |
| 73 | explain this here...*) Neither of these methods need be used at all, |
| 74 | and in this case the frame will only be displayed once it has been |
| 75 | written to, like wxPyOnDemandOutputWindow. |
| 76 | |
| 77 | The former, EnableOutput, displays the frame with an introductory |
| 78 | message, opens a random file to which future displayed output also |
| 79 | goes (unless the nofile attribute is present), and sets the __debug__ |
| 80 | variable of each module to 1 (unless the no __debug__ attribute is |
| 81 | present]. This is so that you can say, in any module whatsoever, |
| 82 | |
| 83 | if __debug__: |
| 84 | InformationalMessagesFrame("... with lots of %<Character> constructs" |
| 85 | % TUPLE) |
| 86 | |
| 87 | without worrying about the overhead of evaluating the arguments, and |
| 88 | calling the wxInformationalMessagesFrame instance, in the case where |
| 89 | debugging is not turned on. (This won't happen if the instance has an |
| 90 | attribute no__debug__; you can arrange this by sub-classing...) |
| 91 | |
| 92 | "Debug mode" can also be turned on by selecting the item-"Enable |
| 93 | output" from the "Debug" menu [the default--see the optional arguments |
| 94 | to the SetOtherMenuBar method] of a frame which has been either passed |
| 95 | appropriately to the constructor of the wxInformationalMessagesFrame |
| 96 | (see the code), or set via the SetOtherMenuBar method thereof. This |
| 97 | also writes an empty string to the instance, meaning that the frame |
| 98 | will open (unless DisablOutput has been called) with an appropriate |
| 99 | introductory message (which will vary according to whether the |
| 100 | instance/class has the "no __debug__" attribute)^ I have found this to |
| 101 | be an extremely useful tool, in lieu of a full wxPython debugger... |
| 102 | |
| 103 | Following this, the menu item is also disabled, and an item "Disable |
| 104 | output" (again, by default) is enabled. Note that these need not be |
| 105 | done: e.g., you don't NEED to have a menu with appropriate items; in |
| 106 | this case simply do not call the SetOtherMenuBar method or use the |
| 107 | othermenubar keyword argument of the class instance constructor. |
| 108 | |
| 109 | The DisableOutput method does the reverse of this; it closes the |
| 110 | window (and associated file), and sets the __debug__ variable of each |
| 111 | module whose name begins with a capital letter {this happens to be the |
| 112 | author's personal practice; all my python module start with capital |
| 113 | letters} to 0. It also enables/disabled the appropriate menu items, |
| 114 | if this was done previously (or SetOtherMenuBar has been called...). |
| 115 | Note too that after a call to DisableOutput, nothing further will be |
| 116 | done upon subsequent write()'s, until the EnableOutput method is |
| 117 | called, either explicitly or by the menu selection above... |
| 118 | |
| 119 | Finally, note that the file-like method close() destroys the window |
| 120 | (and closes any associated file) and there is a file-like method |
| 121 | write() which displays it's argument. |
| 122 | |
| 123 | All (well, most) of this is made clear by the example code at the end |
| 124 | of this file, which is run if the file is run by itself; otherwise, |
| 125 | see the appropriate "stub" file in the wxPython demo. |
| 126 | |
| 127 | """ |
| 128 | |
| 129 | import os |
| 130 | import sys |
| 131 | import tempfile |
| 132 | |
| 133 | import wx |
| 134 | |
| 135 | class _MyStatusBar(wx.StatusBar): |
| 136 | def __init__(self, parent, callbacks=None, useopenbutton=0): |
| 137 | wx.StatusBar.__init__(self, parent, -1, style=wx.TAB_TRAVERSAL) |
| 138 | self.SetFieldsCount(3) |
| 139 | |
| 140 | self.SetStatusText("",0) |
| 141 | |
| 142 | self.button1 = wx.Button(self, -1, "Dismiss", style=wx.TAB_TRAVERSAL) |
| 143 | self.Bind(wx.EVT_BUTTON, self.OnButton1, self.button1) |
| 144 | |
| 145 | if not useopenbutton: |
| 146 | self.button2 = wx.Button(self, -1, "Close File", style=wx.TAB_TRAVERSAL) |
| 147 | else: |
| 148 | self.button2 = wx.Button(self, -1, "Open New File", style=wx.TAB_TRAVERSAL) |
| 149 | |
| 150 | self.Bind(wx.EVT_BUTTON, self.OnButton2, self.button2) |
| 151 | self.useopenbutton = useopenbutton |
| 152 | self.callbacks = callbacks |
| 153 | |
| 154 | # figure out how tall to make the status bar |
| 155 | dc = wx.ClientDC(self) |
| 156 | dc.SetFont(self.GetFont()) |
| 157 | (w,h) = dc.GetTextExtent('X') |
| 158 | h = int(h * 1.8) |
| 159 | self.SetSize((100, h)) |
| 160 | self.OnSize("dummy") |
| 161 | self.Bind(wx.EVT_SIZE, self.OnSize) |
| 162 | |
| 163 | # reposition things... |
| 164 | def OnSize(self, event): |
| 165 | self.CalculateSizes() |
| 166 | rect = self.GetFieldRect(1) |
| 167 | self.button1.SetPosition((rect.x+5, rect.y+2)) |
| 168 | self.button1.SetSize((rect.width-10, rect.height-4)) |
| 169 | rect = self.GetFieldRect(2) |
| 170 | self.button2.SetPosition((rect.x+5, rect.y+2)) |
| 171 | self.button2.SetSize((rect.width-10, rect.height-4)) |
| 172 | |
| 173 | # widths........ |
| 174 | def CalculateSizes(self): |
| 175 | dc = wx.ClientDC(self.button1) |
| 176 | dc.SetFont(self.button1.GetFont()) |
| 177 | (w1,h) = dc.GetTextExtent(self.button1.GetLabel()) |
| 178 | |
| 179 | dc = wx.ClientDC(self.button2) |
| 180 | dc.SetFont(self.button2.GetFont()) |
| 181 | (w2,h) = dc.GetTextExtent(self.button2.GetLabel()) |
| 182 | |
| 183 | self.SetStatusWidths([-1,w1+15,w2+15]) |
| 184 | |
| 185 | def OnButton1(self,event): |
| 186 | self.callbacks[0] () |
| 187 | |
| 188 | def OnButton2(self,event): |
| 189 | if self.useopenbutton and self.callbacks[2] (): |
| 190 | self.button2.SetLabel ("Close File") |
| 191 | elif self.callbacks[1] (): |
| 192 | self.button2.SetLabel ("Open New File") |
| 193 | |
| 194 | self.useopenbutton = 1 - self.useopenbutton |
| 195 | self.OnSize("") |
| 196 | self.button2.Refresh(True) |
| 197 | self.Refresh() |
| 198 | |
| 199 | |
| 200 | |
| 201 | class PyInformationalMessagesFrame: |
| 202 | def __init__(self, |
| 203 | progname="", |
| 204 | text="informational messages", |
| 205 | dir='.', |
| 206 | menuname="Debug", |
| 207 | enableitem="Enable output", |
| 208 | disableitem="Disable output", |
| 209 | othermenubar=None): |
| 210 | |
| 211 | self.SetOtherMenuBar(othermenubar, |
| 212 | menuname=menuname, |
| 213 | enableitem=enableitem, |
| 214 | disableitem=disableitem) |
| 215 | |
| 216 | if hasattr(self,"othermenu") and self.othermenu is not None: |
| 217 | i = self.othermenu.FindMenuItem(self.menuname,self.disableitem) |
| 218 | self.othermenu.Enable(i,0) |
| 219 | i = self.othermenu.FindMenuItem(self.menuname,self.enableitem) |
| 220 | self.othermenu.Enable(i,1) |
| 221 | |
| 222 | self.frame = None |
| 223 | self.title = "%s %s" % (progname,text) |
| 224 | self.parent = None # use the SetParent method if desired... |
| 225 | self.softspace = 1 # of rather limited use |
| 226 | |
| 227 | if dir: |
| 228 | self.SetOutputDirectory(dir) |
| 229 | |
| 230 | |
| 231 | def SetParent(self, parent): |
| 232 | self.parent = parent |
| 233 | |
| 234 | |
| 235 | def SetOtherMenuBar(self, |
| 236 | othermenu, |
| 237 | menuname="Debug", |
| 238 | enableitem="Enable output", |
| 239 | disableitem="Disable output"): |
| 240 | self.othermenu = othermenu |
| 241 | self.menuname = menuname |
| 242 | self.enableitem = enableitem |
| 243 | self.disableitem = disableitem |
| 244 | |
| 245 | |
| 246 | f = None |
| 247 | |
| 248 | |
| 249 | def write(self, string): |
| 250 | if not wx.Thread_IsMain(): |
| 251 | # Aquire the GUI mutex before making GUI calls. Mutex is released |
| 252 | # when locker is deleted at the end of this function. |
| 253 | # |
| 254 | # TODO: This should be updated to use wx.CallAfter similarly to how |
| 255 | # PyOnDemandOutputWindow.write was so it is not necessary |
| 256 | # to get the gui mutex |
| 257 | locker = wx.MutexGuiLocker() |
| 258 | |
| 259 | if self.Enabled: |
| 260 | if self.f: |
| 261 | self.f.write(string) |
| 262 | self.f.flush() |
| 263 | |
| 264 | move = 1 |
| 265 | if (hasattr(self,"text") |
| 266 | and self.text is not None |
| 267 | and self.text.GetInsertionPoint() != self.text.GetLastPosition()): |
| 268 | move = 0 |
| 269 | |
| 270 | if not self.frame: |
| 271 | self.frame = wx.Frame(self.parent, -1, self.title, size=(450, 300), |
| 272 | style=wx.DEFAULT_FRAME_STYLE|wx.NO_FULL_REPAINT_ON_RESIZE) |
| 273 | |
| 274 | self.text = wx.TextCtrl(self.frame, -1, "", |
| 275 | style = wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_RICH) |
| 276 | |
| 277 | self.frame.sb = _MyStatusBar(self.frame, |
| 278 | callbacks=[self.DisableOutput, |
| 279 | self.CloseFile, |
| 280 | self.OpenNewFile], |
| 281 | useopenbutton=hasattr(self, |
| 282 | "nofile")) |
| 283 | self.frame.SetStatusBar(self.frame.sb) |
| 284 | self.frame.Show(True) |
| 285 | self.frame.Bind(wx.EVT_CLOSE, self.OnCloseWindow) |
| 286 | |
| 287 | if hasattr(self,"nofile"): |
| 288 | self.text.AppendText( |
| 289 | "Please close this window (or select the " |
| 290 | "'Dismiss' button below) when desired. By " |
| 291 | "default all messages written to this window " |
| 292 | "will NOT be written to a file--you " |
| 293 | "may change this by selecting 'Open New File' " |
| 294 | "below, allowing you to select a " |
| 295 | "new file...\n\n") |
| 296 | else: |
| 297 | tempfile.tempdir = self.dir |
| 298 | filename = os.path.abspath(tempfile.mktemp ()) |
| 299 | |
| 300 | self.text.AppendText( |
| 301 | "Please close this window (or select the " |
| 302 | "'Dismiss' button below) when desired. By " |
| 303 | "default all messages written to this window " |
| 304 | "will also be written to the file '%s'--you " |
| 305 | "may close this file by selecting 'Close " |
| 306 | "File' below, whereupon this button will be " |
| 307 | "replaced with one allowing you to select a " |
| 308 | "new file...\n\n" % filename) |
| 309 | try: |
| 310 | self.f = open(filename, 'w') |
| 311 | self.frame.sb.SetStatusText("File '%s' opened..." |
| 312 | % filename, |
| 313 | 0) |
| 314 | except EnvironmentError: |
| 315 | self.frame.sb.SetStatusText("File creation failed " |
| 316 | "(filename '%s')..." |
| 317 | % filename, |
| 318 | 0) |
| 319 | self.text.AppendText(string) |
| 320 | |
| 321 | if move: |
| 322 | self.text.ShowPosition(self.text.GetLastPosition()) |
| 323 | |
| 324 | if not hasattr(self,"no__debug__"): |
| 325 | for m in sys.modules.values(): |
| 326 | if m is not None:# and m.__dict__.has_key("__debug__"): |
| 327 | m.__dict__["__debug__"] = 1 |
| 328 | |
| 329 | if hasattr(self,"othermenu") and self.othermenu is not None: |
| 330 | i = self.othermenu.FindMenuItem(self.menuname,self.disableitem) |
| 331 | self.othermenu.Enable(i,1) |
| 332 | i = self.othermenu.FindMenuItem(self.menuname,self.enableitem) |
| 333 | self.othermenu.Enable(i,0) |
| 334 | |
| 335 | |
| 336 | Enabled = 1 |
| 337 | |
| 338 | def OnCloseWindow(self, event, exiting=0): |
| 339 | if self.f: |
| 340 | self.f.close() |
| 341 | self.f = None |
| 342 | |
| 343 | if (hasattr(self,"othermenu") and self.othermenu is not None |
| 344 | and self.frame is not None |
| 345 | and not exiting): |
| 346 | |
| 347 | i = self.othermenu.FindMenuItem(self.menuname,self.disableitem) |
| 348 | self.othermenu.Enable(i,0) |
| 349 | i = self.othermenu.FindMenuItem(self.menuname,self.enableitem) |
| 350 | self.othermenu.Enable(i,1) |
| 351 | |
| 352 | if not hasattr(self,"no__debug__"): |
| 353 | for m in sys.modules.values(): |
| 354 | if m is not None:# and m.__dict__.has_key("__debug__"): |
| 355 | m.__dict__["__debug__"] = 0 |
| 356 | |
| 357 | if self.frame is not None: # typically True, but, e.g., allows |
| 358 | # DisableOutput method (which calls this |
| 359 | # one) to be called when the frame is not |
| 360 | # actually open, so that it is always safe |
| 361 | # to call this method... |
| 362 | frame = self.frame |
| 363 | self.frame = self.text = None |
| 364 | frame.Destroy() |
| 365 | self.Enabled = 1 |
| 366 | |
| 367 | |
| 368 | def EnableOutput(self, |
| 369 | event=None,# event must be the first optional argument... |
| 370 | othermenubar=None, |
| 371 | menuname="Debug", |
| 372 | enableitem="Enable output", |
| 373 | disableitem="Disable output"): |
| 374 | |
| 375 | if othermenubar is not None: |
| 376 | self.SetOtherMenuBar(othermenubar, |
| 377 | menuname=menuname, |
| 378 | enableitem=enableitem, |
| 379 | disableitem=disableitem) |
| 380 | self.Enabled = 1 |
| 381 | if self.f: |
| 382 | self.f.close() |
| 383 | self.f = None |
| 384 | self.write("") |
| 385 | |
| 386 | |
| 387 | def CloseFile(self): |
| 388 | if self.f: |
| 389 | if self.frame: |
| 390 | self.frame.sb.SetStatusText("File '%s' closed..." |
| 391 | % os.path.abspath(self.f.name), |
| 392 | 0) |
| 393 | self.f.close () |
| 394 | self.f = None |
| 395 | else: |
| 396 | if self.frame: |
| 397 | self.frame.sb.SetStatusText("") |
| 398 | if self.frame: |
| 399 | self.frame.sb.Refresh() |
| 400 | return 1 |
| 401 | |
| 402 | |
| 403 | def OpenNewFile(self): |
| 404 | self.CloseFile() |
| 405 | dlg = wx.FileDialog(self.frame, |
| 406 | "Choose a new log file", self.dir,"","*", |
| 407 | wx.SAVE | wx.HIDE_READONLY | wx.OVERWRITE_PROMPT) |
| 408 | if dlg.ShowModal() == wx.ID_CANCEL: |
| 409 | dlg.Destroy() |
| 410 | return 0 |
| 411 | else: |
| 412 | try: |
| 413 | self.f = open(os.path.abspath(dlg.GetPath()),'w') |
| 414 | except EnvironmentError: |
| 415 | dlg.Destroy() |
| 416 | return 0 |
| 417 | dlg.Destroy() |
| 418 | if self.frame: |
| 419 | self.frame.sb.SetStatusText("File '%s' opened..." |
| 420 | % os.path.abspath(self.f.name), |
| 421 | 0) |
| 422 | if hasattr(self,"nofile"): |
| 423 | self.frame.sb = _MyStatusBar(self.frame, |
| 424 | callbacks=[self.DisableOutput, |
| 425 | self.CloseFile, |
| 426 | self.OpenNewFile]) |
| 427 | self.frame.SetStatusBar(self.frame.sb) |
| 428 | if hasattr(self,"nofile"): |
| 429 | delattr(self,"nofile") |
| 430 | return 1 |
| 431 | |
| 432 | |
| 433 | def DisableOutput(self, |
| 434 | event=None,# event must be the first optional argument... |
| 435 | exiting=0): |
| 436 | self.write("<InformationalMessagesFrame>.DisableOutput()\n") |
| 437 | if hasattr(self,"frame") \ |
| 438 | and self.frame is not None: |
| 439 | self.OnCloseWindow("Dummy",exiting=exiting) |
| 440 | self.Enabled = 0 |
| 441 | |
| 442 | |
| 443 | def close(self): |
| 444 | self.DisableOutput() |
| 445 | |
| 446 | |
| 447 | def flush(self): |
| 448 | if self.text: |
| 449 | self.text.SetInsertionPointEnd() |
| 450 | wx.Yield() |
| 451 | |
| 452 | |
| 453 | def __call__(self,* args): |
| 454 | for s in args: |
| 455 | self.write (str (s)) |
| 456 | |
| 457 | |
| 458 | def SetOutputDirectory(self,dir): |
| 459 | self.dir = os.path.abspath(dir) |
| 460 | ## sys.__stderr__.write("Directory: os.path.abspath(%s) = %s\n" |
| 461 | ## % (dir,self.dir)) |
| 462 | |
| 463 | |
| 464 | |
| 465 | class Dummy_PyInformationalMessagesFrame: |
| 466 | def __init__(self,progname=""): |
| 467 | self.softspace = 1 |
| 468 | def __call__(self,*args): |
| 469 | pass |
| 470 | def write(self,s): |
| 471 | pass |
| 472 | def flush(self): |
| 473 | pass |
| 474 | def close(self): |
| 475 | pass |
| 476 | def EnableOutput(self): |
| 477 | pass |
| 478 | def __call__(self,* args): |
| 479 | pass |
| 480 | def DisableOutput(self,exiting=0): |
| 481 | pass |
| 482 | def SetParent(self,wX): |
| 483 | pass |
| 484 | |