codekingpro/portable-devtools
115k
1# winout.py2#3# generic "output window"4#5# This Window will detect itself closing, and recreate next time output is6# written to it.7 8# This has the option of writing output at idle time (by hooking the9# idle message, and queueing output) or writing as each10# write is executed.11# Updating the window directly gives a jerky appearance as many writes12# take place between commands, and the windows scrolls, and updates etc13# Updating at idle-time may defer all output of a long process, giving the14# appearence nothing is happening.15# There is a compromise "line" mode, which will output whenever16# a complete line is available.17 18# behaviour depends on self.writeQueueing19 20# This module is thread safe - output can originate from any thread. If any thread21# other than the main thread attempts to print, it is always queued until next idle time22 23import queue24import re25 26import win32api27import win32con28import win32ui29from pywin.framework import app, window30from pywin.mfc import docview31 32debug = lambda msg: None33 34##debug=win32ui.OutputDebugString35##import win32trace;win32trace.InitWrite() # for debugging - delete me!36##debug = win32trace.write37 38 39class flags:40 # queueing of output.41 WQ_NONE = 042 WQ_LINE = 143 WQ_IDLE = 244 45 46# WindowOutputDocumentParent=docview.RichEditDoc47# WindowOutputDocumentParent=docview.Document48import pywin.scintilla.document49from pywin import default_scintilla_encoding50from pywin.scintilla import scintillacon51 52WindowOutputDocumentParent = pywin.scintilla.document.CScintillaDocument53 54 55class WindowOutputDocument(WindowOutputDocumentParent):56 def SaveModified(self):57 return 1 # say it is OK to destroy my document58 59 def OnSaveDocument(self, fileName):60 win32ui.SetStatusText("Saving file...", 1)61 try:62 self.SaveFile(fileName)63 except IOError as details:64 win32ui.MessageBox("Error - could not save file\r\n\r\n%s" % details)65 return 066 win32ui.SetStatusText("Ready")67 return 168 69 70class WindowOutputFrame(window.MDIChildWnd):71 def __init__(self, wnd=None):72 window.MDIChildWnd.__init__(self, wnd)73 self.HookMessage(self.OnSizeMove, win32con.WM_SIZE)74 self.HookMessage(self.OnSizeMove, win32con.WM_MOVE)75 76 def LoadFrame(self, idResource, style, wndParent, context):77 self.template = context.template78 return self._obj_.LoadFrame(idResource, style, wndParent, context)79 80 def PreCreateWindow(self, cc):81 cc = self._obj_.PreCreateWindow(cc)82 if (83 self.template.defSize84 and self.template.defSize[0] != self.template.defSize[1]85 ):86 rect = app.RectToCreateStructRect(self.template.defSize)87 cc = cc[0], cc[1], cc[2], cc[3], rect, cc[5], cc[6], cc[7], cc[8]88 return cc89 90 def OnSizeMove(self, msg):91 # so recreate maintains position.92 # Need to map coordinates from the93 # frame windows first child.94 mdiClient = self.GetParent()95 self.template.defSize = mdiClient.ScreenToClient(self.GetWindowRect())96 97 def OnDestroy(self, message):98 self.template.OnFrameDestroy(self)99 return 1100 101 102class WindowOutputViewImpl:103 def __init__(self):104 self.patErrorMessage = re.compile('\W*File "(.*)", line ([0-9]+)')105 self.template = self.GetDocument().GetDocTemplate()106 107 def HookHandlers(self):108 # Hook for the right-click menu.109 self.HookMessage(self.OnRClick, win32con.WM_RBUTTONDOWN)110 111 def OnDestroy(self, msg):112 self.template.OnViewDestroy(self)113 114 def OnInitialUpdate(self):115 self.RestoreKillBuffer()116 self.SetSel(-2) # end of buffer117 118 def GetRightMenuItems(self):119 ret = []120 flags = win32con.MF_STRING | win32con.MF_ENABLED121 ret.append((flags, win32ui.ID_EDIT_COPY, "&Copy"))122 ret.append((flags, win32ui.ID_EDIT_SELECT_ALL, "&Select all"))123 return ret124 125 #126 # Windows command handlers, virtuals, etc.127 #128 def OnRClick(self, params):129 paramsList = self.GetRightMenuItems()130 menu = win32ui.CreatePopupMenu()131 for appendParams in paramsList:132 if type(appendParams) != type(()):133 appendParams = (appendParams,)134 menu.AppendMenu(*appendParams)135 menu.TrackPopupMenu(params[5]) # track at mouse position.136 return 0137 138 # as this is often used as an output window, exeptions will often139 # be printed. Therefore, we support this functionality at this level.140 # Returns TRUE if the current line is an error message line, and will141 # jump to it. FALSE if no error (and no action taken)142 def HandleSpecialLine(self):143 from . import scriptutils144 145 line = self.GetLine()146 if line[:11] == "com_error: ":147 # An OLE Exception - pull apart the exception148 # and try and locate a help file.149 try:150 import win32api151 import win32con152 153 det = eval(line[line.find(":") + 1 :].strip())154 win32ui.SetStatusText("Opening help file on OLE error...")155 from . import help156 157 help.OpenHelpFile(det[2][3], win32con.HELP_CONTEXT, det[2][4])158 return 1159 except win32api.error as details:160 win32ui.SetStatusText(161 "The help file could not be opened - %s" % details.strerror162 )163 return 1164 except:165 win32ui.SetStatusText(166 "Line is a COM error, but no WinHelp details can be parsed"167 )168 # Look for a Python traceback.169 matchResult = self.patErrorMessage.match(line)170 if matchResult is None:171 # No match - try the previous line172 lineNo = self.LineFromChar()173 if lineNo > 0:174 line = self.GetLine(lineNo - 1)175 matchResult = self.patErrorMessage.match(line)176 if matchResult is not None:177 # we have an error line.178 fileName = matchResult.group(1)179 if fileName[0] == "<":180 win32ui.SetStatusText("Can not load this file")181 return 1 # still was an error message.182 else:183 lineNoString = matchResult.group(2)184 # Attempt to locate the file (in case it is a relative spec)185 fileNameSpec = fileName186 fileName = scriptutils.LocatePythonFile(fileName)187 if fileName is None:188 # Dont force update, so it replaces the idle prompt.189 win32ui.SetStatusText(190 "Cant locate the file '%s'" % (fileNameSpec), 0191 )192 return 1193 194 win32ui.SetStatusText(195 "Jumping to line " + lineNoString + " of file " + fileName, 1196 )197 if not scriptutils.JumpToDocument(fileName, int(lineNoString)):198 win32ui.SetStatusText("Could not open %s" % fileName)199 return 1 # still was an error message.200 return 1201 return 0 # not an error line202 203 def write(self, msg):204 return self.template.write(msg)205 206 def writelines(self, lines):207 for line in lines:208 self.write(line)209 210 def flush(self):211 self.template.flush()212 213 214class WindowOutputViewRTF(docview.RichEditView, WindowOutputViewImpl):215 def __init__(self, doc):216 docview.RichEditView.__init__(self, doc)217 WindowOutputViewImpl.__init__(self)218 219 def OnInitialUpdate(self):220 WindowOutputViewImpl.OnInitialUpdate(self)221 return docview.RichEditView.OnInitialUpdate(self)222 223 def OnDestroy(self, msg):224 WindowOutputViewImpl.OnDestroy(self, msg)225 docview.RichEditView.OnDestroy(self, msg)226 227 def HookHandlers(self):228 WindowOutputViewImpl.HookHandlers(self)229 # Hook for finding and locating error messages230 self.HookMessage(self.OnLDoubleClick, win32con.WM_LBUTTONDBLCLK)231 232 # docview.RichEditView.HookHandlers(self)233 234 def OnLDoubleClick(self, params):235 if self.HandleSpecialLine():236 return 0 # dont pass on237 return 1 # pass it on by default.238 239 def RestoreKillBuffer(self):240 if len(self.template.killBuffer):241 self.StreamIn(win32con.SF_RTF, self._StreamRTFIn)242 self.template.killBuffer = []243 244 def SaveKillBuffer(self):245 self.StreamOut(win32con.SF_RTFNOOBJS, self._StreamRTFOut)246 247 def _StreamRTFOut(self, data):248 self.template.killBuffer.append(data)249 return 1 # keep em coming!250 251 def _StreamRTFIn(self, bytes):252 try:253 item = self.template.killBuffer[0]254 self.template.killBuffer.remove(item)255 if bytes < len(item):256 print("Warning - output buffer not big enough!")257 return item258 except IndexError:259 return None260 261 def dowrite(self, str):262 self.SetSel(-2)263 self.ReplaceSel(str)264 265 266import pywin.scintilla.view267 268 269class WindowOutputViewScintilla(270 pywin.scintilla.view.CScintillaView, WindowOutputViewImpl271):272 def __init__(self, doc):273 pywin.scintilla.view.CScintillaView.__init__(self, doc)274 WindowOutputViewImpl.__init__(self)275 276 def OnInitialUpdate(self):277 pywin.scintilla.view.CScintillaView.OnInitialUpdate(self)278 self.SCISetMarginWidth(3)279 WindowOutputViewImpl.OnInitialUpdate(self)280 281 def OnDestroy(self, msg):282 WindowOutputViewImpl.OnDestroy(self, msg)283 pywin.scintilla.view.CScintillaView.OnDestroy(self, msg)284 285 def HookHandlers(self):286 WindowOutputViewImpl.HookHandlers(self)287 pywin.scintilla.view.CScintillaView.HookHandlers(self)288 self.GetParent().HookNotify(289 self.OnScintillaDoubleClick, scintillacon.SCN_DOUBLECLICK290 )291 292 ## self.HookMessage(self.OnLDoubleClick,win32con.WM_LBUTTONDBLCLK)293 294 def OnScintillaDoubleClick(self, std, extra):295 self.HandleSpecialLine()296 297 ## def OnLDoubleClick(self,params):298 ## return 0 # never dont pass on299 300 def RestoreKillBuffer(self):301 assert len(self.template.killBuffer) in (0, 1), "Unexpected killbuffer contents"302 if self.template.killBuffer:303 self.SCIAddText(self.template.killBuffer[0])304 self.template.killBuffer = []305 306 def SaveKillBuffer(self):307 self.template.killBuffer = [self.GetTextRange(0, -1)]308 309 def dowrite(self, str):310 end = self.GetTextLength()311 atEnd = end == self.GetSel()[0]312 self.SCIInsertText(str, end)313 if atEnd:314 self.SetSel(self.GetTextLength())315 316 def SetWordWrap(self, bWrapOn=1):317 if bWrapOn:318 wrap_mode = scintillacon.SC_WRAP_WORD319 else:320 wrap_mode = scintillacon.SC_WRAP_NONE321 self.SCISetWrapMode(wrap_mode)322 323 def _MakeColorizer(self):324 return None # No colorizer for me!325 326 327WindowOutputView = WindowOutputViewScintilla328 329 330# The WindowOutput class is actually an MFC template. This is a conventient way of331# making sure that my state can exist beyond the life of the windows themselves.332# This is primarily to support the functionality of a WindowOutput window automatically333# being recreated if necessary when written to.334class WindowOutput(docview.DocTemplate):335 """Looks like a general Output Window - text can be written by the 'write' method.336 Will auto-create itself on first write, and also on next write after being closed"""337 338 softspace = 1339 340 def __init__(341 self,342 title=None,343 defSize=None,344 queueing=flags.WQ_LINE,345 bAutoRestore=1,346 style=None,347 makeDoc=None,348 makeFrame=None,349 makeView=None,350 ):351 """init the output window -352 Params353 title=None -- What is the title of the window354 defSize=None -- What is the default size for the window - if this355 is a string, the size will be loaded from the ini file.356 queueing = flags.WQ_LINE -- When should output be written357 bAutoRestore=1 -- Should a minimized window be restored.358 style -- Style for Window, or None for default.359 makeDoc, makeFrame, makeView -- Classes for frame, view and window respectively.360 """361 if makeDoc is None:362 makeDoc = WindowOutputDocument363 if makeFrame is None:364 makeFrame = WindowOutputFrame365 if makeView is None:366 makeView = WindowOutputViewScintilla367 docview.DocTemplate.__init__(368 self, win32ui.IDR_PYTHONTYPE, makeDoc, makeFrame, makeView369 )370 self.SetDocStrings("\nOutput\n\nText Documents (*.txt)\n.txt\n\n\n")371 win32ui.GetApp().AddDocTemplate(self)372 self.writeQueueing = queueing373 self.errorCantRecreate = 0374 self.killBuffer = []375 self.style = style376 self.bAutoRestore = bAutoRestore377 self.title = title378 self.bCreating = 0379 self.interruptCount = 0380 if type(defSize) == type(""): # is a string - maintain size pos from ini file.381 self.iniSizeSection = defSize382 self.defSize = app.LoadWindowSize(defSize)383 self.loadedSize = self.defSize384 else:385 self.iniSizeSection = None386 self.defSize = defSize387 self.currentView = None388 self.outputQueue = queue.Queue(-1)389 self.mainThreadId = win32api.GetCurrentThreadId()390 self.idleHandlerSet = 0391 self.SetIdleHandler()392 393 def __del__(self):394 self.Close()395 396 def Create(self, title=None, style=None):397 self.bCreating = 1398 if title:399 self.title = title400 if style:401 self.style = style402 doc = self.OpenDocumentFile()403 if doc is None:404 return405 self.currentView = doc.GetFirstView()406 self.bCreating = 0407 if self.title:408 doc.SetTitle(self.title)409 410 def Close(self):411 self.RemoveIdleHandler()412 try:413 parent = self.currentView.GetParent()414 except (AttributeError, win32ui.error): # Already closed415 return416 parent.DestroyWindow()417 418 def SetTitle(self, title):419 self.title = title420 if self.currentView:421 self.currentView.GetDocument().SetTitle(self.title)422 423 def OnViewDestroy(self, view):424 self.currentView.SaveKillBuffer()425 self.currentView = None426 427 def OnFrameDestroy(self, frame):428 if self.iniSizeSection:429 # use GetWindowPlacement(), as it works even when min'd or max'd430 newSize = frame.GetWindowPlacement()[4]431 if self.loadedSize != newSize:432 app.SaveWindowSize(self.iniSizeSection, newSize)433 434 def SetIdleHandler(self):435 if not self.idleHandlerSet:436 debug("Idle handler set\n")437 win32ui.GetApp().AddIdleHandler(self.QueueIdleHandler)438 self.idleHandlerSet = 1439 440 def RemoveIdleHandler(self):441 if self.idleHandlerSet:442 debug("Idle handler reset\n")443 if win32ui.GetApp().DeleteIdleHandler(self.QueueIdleHandler) == 0:444 debug("Error deleting idle handler\n")445 self.idleHandlerSet = 0446 447 def RecreateWindow(self):448 if self.errorCantRecreate:449 debug("Error = not trying again")450 return 0451 try:452 # This will fail if app shutting down453 win32ui.GetMainFrame().GetSafeHwnd()454 self.Create()455 return 1456 except (win32ui.error, AttributeError):457 self.errorCantRecreate = 1458 debug("Winout can not recreate the Window!\n")459 return 0460 461 # this handles the idle message, and does the printing.462 def QueueIdleHandler(self, handler, count):463 try:464 bEmpty = self.QueueFlush(20)465 # If the queue is empty, then we are back to idle and restart interrupt logic.466 if bEmpty:467 self.interruptCount = 0468 except KeyboardInterrupt:469 # First interrupt since idle we just pass on.470 # later ones we dump the queue and give up.471 self.interruptCount = self.interruptCount + 1472 if self.interruptCount > 1:473 # Drop the queue quickly as the user is already annoyed :-)474 self.outputQueue = queue.Queue(-1)475 print("Interrupted.")476 bEmpty = 1477 else:478 raise # re-raise the error so the users exception filters up.479 return not bEmpty # More to do if not empty.480 481 # Returns true if the Window needs to be recreated.482 def NeedRecreateWindow(self):483 try:484 if self.currentView is not None and self.currentView.IsWindow():485 return 0486 except (487 win32ui.error,488 AttributeError,489 ): # Attribute error if the win32ui object has died.490 pass491 return 1492 493 # Returns true if the Window is OK (either cos it was, or because it was recreated494 def CheckRecreateWindow(self):495 if self.bCreating:496 return 1497 if not self.NeedRecreateWindow():498 return 1499 if self.bAutoRestore:500 if self.RecreateWindow():501 return 1502 return 0503 504 def QueueFlush(self, max=None):505 # Returns true if the queue is empty after the flush506 # debug("Queueflush - %d, %d\n" % (max, self.outputQueue.qsize()))507 if self.bCreating:508 return 1509 items = []510 rc = 0511 while max is None or max > 0:512 try:513 item = self.outputQueue.get_nowait()514 items.append(item)515 except queue.Empty:516 rc = 1517 break518 if max is not None:519 max = max - 1520 if len(items) != 0:521 if not self.CheckRecreateWindow():522 debug(":Recreate failed!\n")523 return 1 # In trouble - so say we have nothing to do.524 win32ui.PumpWaitingMessages() # Pump paint messages525 self.currentView.dowrite("".join(items))526 return rc527 528 def HandleOutput(self, message):529 # debug("QueueOutput on thread %d, flags %d with '%s'...\n" % (win32api.GetCurrentThreadId(), self.writeQueueing, message ))530 self.outputQueue.put(message)531 if win32api.GetCurrentThreadId() != self.mainThreadId:532 pass533 # debug("not my thread - ignoring queue options!\n")534 elif self.writeQueueing == flags.WQ_LINE:535 pos = message.rfind("\n")536 if pos >= 0:537 # debug("Line queueing - forcing flush\n")538 self.QueueFlush()539 return540 elif self.writeQueueing == flags.WQ_NONE:541 # debug("WQ_NONE - flushing!\n")542 self.QueueFlush()543 return544 # Let our idle handler get it - wake it up545 try:546 win32ui.GetMainFrame().PostMessage(547 win32con.WM_USER548 ) # Kick main thread off.549 except win32ui.error:550 # This can happen as the app is shutting down, so we send it to the C++ debugger551 win32api.OutputDebugString(message)552 553 # delegate certain fns to my view.554 def writelines(self, lines):555 for line in lines:556 self.write(line)557 558 def write(self, message):559 self.HandleOutput(message)560 561 def flush(self):562 self.QueueFlush()563 564 def HandleSpecialLine(self):565 self.currentView.HandleSpecialLine()566 567 568def RTFWindowOutput(*args, **kw):569 kw["makeView"] = WindowOutputViewRTF570 return WindowOutput(*args, **kw)571 572 573def thread_test(o):574 for i in range(5):575 o.write("Hi from thread %d\n" % (win32api.GetCurrentThreadId()))576 win32api.Sleep(100)577 578 579def test():580 w = WindowOutput(queueing=flags.WQ_IDLE)581 w.write("First bit of text\n")582 import _thread583 584 for i in range(5):585 w.write("Hello from the main thread\n")586 _thread.start_new(thread_test, (w,))587 for i in range(2):588 w.write("Hello from the main thread\n")589 win32api.Sleep(50)590 return w591 592 593if __name__ == "__main__":594 test()595 