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