Embed.py (7634B)
1 import FreeCAD 2 import FreeCADGui as Gui 3 import subprocess 4 import PySide 5 import re 6 from PySide import QtGui 7 from PySide import QtCore 8 9 import ExternalAppsList 10 from MyX11Utils import * 11 12 #class MyMdiSubWindow(QMdiSubWindow): 13 # def closeEvent 14 15 class EmbeddedWindow(QtCore.QObject): 16 def __init__(self, app, externalAppInstance, processId, windowId): 17 super(EmbeddedWindow, self).__init__() 18 self.app = app 19 self.externalAppInstance = externalAppInstance 20 self.processId = processId 21 self.windowId = windowId 22 self.mdi = Gui.getMainWindow().findChild(QtGui.QMdiArea) 23 self.xw = QtGui.QWindow.fromWinId(self.windowId) 24 self.xw.setFlags(QtGui.Qt.FramelessWindowHint) 25 self.xwd = QtGui.QWidget.createWindowContainer(self.xw) 26 self.mwx = QtGui.QMainWindow() 27 #self.mwx.layout().addWidget(self.xwd) 28 self.mwx.setCentralWidget(self.xwd) 29 self.mdiSub = self.mdi.addSubWindow(self.xwd) 30 self.xwd.setBaseSize(640,480) 31 self.mwx.setBaseSize(640,480) 32 self.mdiSub.setBaseSize(640,480) 33 self.mdiSub.setWindowTitle(app.name) 34 self.mdiSub.show() 35 self.xwd.installEventFilter(self) 36 self.mwx.installEventFilter(self) 37 self.mdiSub.installEventFilter(self) 38 self.timer = QtCore.QTimer(self) 39 self.timer.timeout.connect(self.pollWindowClosed) 40 self.timer.start(1000) 41 42 def eventFilter(self, obj, event): 43 # This doesn't seem to work, some events occur but no the close one. 44 if event.type() == QtCore.QEvent.Close and x11stillAlive(self.windowId): 45 #xdotool closes the window without asking for confirmation 46 #subprocess.Popen(['xdotool', 'windowclose', str(self.windowId)]) 47 48 # use decode('utf-8', 'ignore') to use strings instead of 49 # byte strings and discard ill-formed unicode in case this 50 # tool doesn't sanitize their output 51 xwininfo_output = subprocess.check_output(['xwininfo', '-root', '-int']).decode('utf-8', 'ignore').split('\n') 52 root_id = None 53 for line in xwininfo_output: 54 match = re.compile(r'^xwininfo: Window id: ([0-9]+)').match(line) 55 if match: 56 root_id = match.group(1) 57 break 58 if root_id is not None: 59 # detach 60 subprocess.Popen(['xdotool', 'windowreparent', str(self.windowId), root_id]) 61 # send friendly close signal 62 subprocess.Popen(['wmctrl', '-i', '-c', str(self.windowId)]) 63 # Cleanup 64 # TODO: destroy self.xw and .xwd if possible to avoid a leak 65 self.xw.setParent(None) 66 self.xwd.setParent(None) 67 self.timer.stop() 68 # remove from dictionary of found windows 69 self.externalAppInstance.foundWindows.pop(self.windowId, None) 70 # avoid GC 71 self.externalAppInstance.closedWindows[self.windowId] = self 72 # re-attach in case it didn't close (confirmation dialog etc.) 73 print('waitForWindow') 74 self.externalAppInstance.waitForWindow() 75 # try: 76 # self.xw = QtGui.QWindow.fromWinId(self.windowId) 77 # self.xwd = QtGui.QWidget.createWindowContainer(self.xw) 78 # self.mwx.setCentralWidget(self.xwd) 79 # except Exception as e: 80 # print(repr(e)) 81 # pass 82 else: 83 event.ignore() 84 return True 85 else: 86 return False 87 88 @QtCore.Slot() 89 def pollWindowClosed(self): 90 # TODO: find an event instead of polling 91 if not x11stillAlive(self.windowId) and not deleted(self.mdiSub): 92 self.mdiSub.close() 93 self.timer.stop() 94 95 # TODO: also kill or at least detach on application exit 96 97 # <optional spaces> <digits (captured in group 1)> <optional spaces> "<quoted string>" <optional spaces> : <anything> 98 xwininfo_re = re.compile(r'^\s*([0-9]+)\s*"[^"]*"\s*:.*$') 99 100 def try_pipe_lines(commandAndArguments): 101 try: 102 # use decode('utf-8', 'ignore') to use strings instead of 103 # byte strings and discard ill-formed unicode in case this 104 # tool doesn't sanitize their output 105 return subprocess.check_output(commandAndArguments).decode('utf-8', 'ignore').split('\n') 106 except: 107 return [] 108 109 # TODO: this is just a quick & dirty way to attach a field to the FreeCad object 110 class ExternalApps(): 111 def __init__(self): 112 setattr(FreeCAD, 'ExternalApps', self) 113 114 def deleted(widget): 115 """Detect RuntimeError: Internal C++ object (PySide2.QtGui.QWindow) already deleted.""" 116 try: 117 str(widget) # str fails on already-deleted Qt wrappers. 118 return False 119 except: 120 return True 121 122 class ExternalAppInstance(QtCore.QObject): 123 def __init__(self, appName): 124 super(ExternalAppInstance, self).__init__() 125 self.app = ExternalAppsList.apps[appName] 126 # Start the application 127 # TODO: popen_process shouldn't be exposed to in-document scripts, it would allow them to redirect output etc. 128 print('Starting ' + ' '.join(self.app.start_command_and_args)) 129 self.popen_process = subprocess.Popen(self.app.start_command_and_args) 130 self.appProcessIds = [self.popen_process.pid] 131 self.initWaitForWindow() 132 self.foundWindows = dict() 133 self.closedWindows = dict() 134 setattr(FreeCAD.ExternalApps, self.app.name, self) 135 136 def initWaitForWindow(self): 137 self.TimeoutHasOccurred = False # for other scritps to know the status 138 self.startupTimeout = 10000 139 self.elapsed = QtCore.QElapsedTimer() 140 self.elapsed.start() 141 self.timer = QtCore.QTimer(self) 142 self.timer.timeout.connect(self.attemptToFindWindow) 143 144 def waitForWindow(self): 145 self.timer.start(50) 146 147 @QtCore.Slot() 148 def attemptToFindWindow(self): 149 try: 150 self.attemptToFindWindowWrapped() 151 except: 152 self.timer.stop() 153 raise 154 155 def attemptToFindWindowWrapped(self): 156 for line in try_pipe_lines(['xwininfo', '-root', '-tree', '-int']): 157 self.attemptWithLine(line) 158 159 if self.elapsed.elapsed() > self.startupTimeout: 160 self.timer.stop() 161 self.TimeoutHasOccurred = True 162 163 def attemptWithLine(self, line): 164 if not self.app.xwininfo_filter_re.search(line): 165 return 166 xwininfo_re_match_line = xwininfo_re.match(line) 167 if not xwininfo_re_match_line: 168 return 169 windowId = int(xwininfo_re_match_line.group(1)) 170 xprop_try_process_id = x11prop(windowId, '_NET_WM_PID', 'CARDINAL') 171 if not xprop_try_process_id: 172 return 173 processId = int(xprop_try_process_id) # TODO try parse int and catch failure 174 if processId not in self.appProcessIds: 175 return 176 if not self.app.extra_xprop_filter(processId, windowId, len(self.foundWindows)): 177 return 178 self.foundWindow(processId, windowId) 179 180 def foundWindow(self, processId, windowId): 181 print('found ' + str(windowId)) 182 if windowId not in self.foundWindows.keys(): 183 self.foundWindows[windowId] = EmbeddedWindow(self.app, self, processId, windowId) 184 # for w in self.foundWindows.values(): 185 # #if not deleted(xw) and not xw.isActive(): 186 # if not x11stillAlive(w.windowId): 187 # w.mdiSub.close()