www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | LICENSE

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()