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