# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2020 ifm electronic gmbh
#
# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
#
"""
This module provides a MainWindow GUI service for the nexxT framework.
"""
import logging
import re
import subprocess
import time
import sys
import nexxT.shiboken
from nexxT.Qt.QtWidgets import (QMainWindow, QMdiArea, QMdiSubWindow, QDockWidget, QWidget, QGridLayout,
QMenuBar, QMessageBox, QScrollArea, QLabel)
from nexxT.Qt.QtCore import (QObject, Signal, Slot, Qt, QByteArray, QDataStream, QIODevice, QRect, QPoint, QSettings,
QTimer, QUrl, QEvent)
from nexxT.Qt.QtGui import QDesktopServices, QAction, QActionGroup
import nexxT
from nexxT.interface import Filter
from nexxT.core.Application import Application
from nexxT.core.Utils import handleException, assertMainThread
logger = logging.getLogger(__name__)
[docs]
class NexxTMdiSubWindow(QMdiSubWindow):
"""
Need subclassing for getting close / show events and saving / restoring state.
"""
visibleChanged = Signal(bool)
[docs]
def closeEvent(self, closeEvent):
"""
override from QMdiSubWindow
:param closeEvent: a QCloseEvent instance
:return:
"""
logger.internal("closeEvent widget=%s", self.widget())
self.visibleChanged.emit(False)
return super().closeEvent(closeEvent)
[docs]
def showEvent(self, showEvent):
"""
override from QMdiSubWindow
:param closeEvent: a QShowEvent instance
:return:
"""
logger.internal("showEvent widget=%s", self.widget())
self.visibleChanged.emit(True)
res = super().showEvent(showEvent)
# no idea why this is necessary, but otherwise the child window is not shown
if self.widget() is not None:
self.widget().show()
return res
[docs]
def saveGeometry(self):
"""
Saves the geometry of this subwindow (see https://bugreports.qt.io/browse/QTBUG-18648)
:return: a ByteArray instance
"""
array = QByteArray()
stream = QDataStream(array, QIODevice.WriteOnly)
stream.writeUInt32(0x1D9D0CB)
stream.writeUInt16(1)
stream.writeUInt16(0)
frameGeom = self.frameGeometry()
stream.writeInt64(frameGeom.x())
stream.writeInt64(frameGeom.y())
stream.writeInt64(frameGeom.width())
stream.writeInt64(frameGeom.height())
normalGeom = self.normalGeometry()
stream.writeInt64(normalGeom.x())
stream.writeInt64(normalGeom.y())
stream.writeInt64(normalGeom.width())
stream.writeInt64(normalGeom.height())
stream.writeUInt32(int(bool(self.windowState() & Qt.WindowMaximized)))
stream.writeUInt32(int(bool(self.windowState() & Qt.WindowFullScreen)))
return array
[docs]
def restoreGeometry(self, geometry):
"""
Restores the geometry of this subwindow
:param geometry: the saved state as a QByteArray instance
:return:
"""
if geometry.size() < 4:
return False
stream = QDataStream(geometry)
if stream.readUInt32() != 0x1D9D0CB:
return False
if stream.readUInt16() != 1:
return False
stream.readUInt16() # minorVersion is ignored.
x = stream.readInt64()
y = stream.readInt64()
width = stream.readInt64()
height = stream.readInt64()
restoredFrameGeometry = QRect(x, y, width, height)
x = stream.readInt64()
y = stream.readInt64()
width = stream.readInt64()
height = stream.readInt64()
restoredNormalGeometry = QRect(x, y, width, height)
maximized = bool(stream.readUInt32())
fullScreen = bool(stream.readUInt32())
frameHeight = 0
if (not restoredNormalGeometry.isValid()) and (restoredFrameGeometry.isValid()):
# there seems to be an issue in PySide6 that the normalGeometry is always invalid (?)
restoredNormalGeometry = restoredFrameGeometry
if not restoredFrameGeometry.isValid():
restoredFrameGeometry = QRect(QPoint(0, 0), self.sizeHint())
if not restoredNormalGeometry.isValid():
restoredNormalGeometry = QRect(QPoint(0, frameHeight), self.sizeHint())
restoredFrameGeometry.moveTop(max(restoredFrameGeometry.top(), 0))
restoredNormalGeometry.moveTop(max(restoredNormalGeometry.top(), 0 + frameHeight))
if maximized or fullScreen:
self.setGeometry(restoredNormalGeometry)
ws = self.windowState()
if maximized:
ws |= Qt.WindowMaximized
if fullScreen:
ws |= Qt.WindowFullScreen
self.setWindowState(ws)
else:
offset = QPoint()
self.setWindowState(self.windowState() & ~(Qt.WindowMaximized|Qt.WindowFullScreen))
self.move(restoredFrameGeometry.topLeft() + offset)
self.resize(restoredNormalGeometry.size())
return True
[docs]
class MainWindow(QMainWindow):
"""
Main Window service for the nexxT frameworks. Other services usually create dock windows, filters use the
subplot functionality to create grid-layouted views.
"""
mdiSubWindowCreated = Signal(QMdiSubWindow) # TODO: deprecated, can be removed in later versions
aboutToClose = Signal(object)
userSelectionChanged = Signal(str, QPoint)
[docs]
def __init__(self, config):
super().__init__()
self.config = config
self.config.appActivated.connect(self._appActivated)
self.mdi = QMdiArea(self)
self.mdi.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.mdi.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.mdi.viewport().installEventFilter(self)
self.setCentralWidget(self.mdi)
self.menu = self.menuBar().addMenu("&Windows")
self.framerateMenu = self.menu.addMenu("Display Framerate")
self.framerateActionGroup = QActionGroup(self)
self.framerate = 25
for framerate in [1, 2, 5, 10, 15, 20, 25, 40, 50, 100]:
a = QAction(self)
a.setData(framerate)
a.setText(f"{framerate} Hz")
a.setCheckable(True)
if framerate == self.framerate:
a.setChecked(True)
a.toggled.connect(self._setFramerate)
self.framerateActionGroup.addAction(a)
self.framerateMenu.addAction(a)
self.config.configLoaded.connect(self.restoreConfigSpecifics)
self.config.configAboutToSave.connect(self.saveConfigSpecifics)
closeAllPlotsAction = QAction("Close all plots", self)
closeAllPlotsAction.triggered.connect(self._closeAllPlots)
self.menu.addAction(closeAllPlotsAction)
showAllPlotsAction = QAction("Show all plots", self)
showAllPlotsAction.triggered.connect(self._showAllPlots)
self.menu.addAction(showAllPlotsAction)
cascadeAction = QAction("Cascade plots", self)
cascadeAction.triggered.connect(self.mdi.cascadeSubWindows)
self.menu.addAction(cascadeAction)
tileAction = QAction("Tile plots", self)
tileAction.triggered.connect(self.mdi.tileSubWindows)
self.menu.addAction(tileAction)
self.dockWindowsMenu = self.menu.addMenu("Dock Windows")
self.menu.addSeparator()
self.aboutMenu = QMenuBar(self.menuBar())
self.menuBar().setCornerWidget(self.aboutMenu)
m = self.aboutMenu.addMenu("&Help")
self.helpNexxT = QAction("Help ...")
self.aboutNexxT = QAction("About nexxT ...")
self.aboutQt = QAction("About Qt ...")
self.aboutPython = QAction("About Python ...")
m.addActions([self.helpNexxT])
m.addSeparator()
m.addActions([self.aboutNexxT, self.aboutQt, self.aboutPython])
self.helpNexxT.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://nexxT.readthedocs.org")))
self.aboutNexxT.triggered.connect(lambda: QMessageBox.about(self, "About nexxT", f"""\
This program uses <b>nexxT</b> {nexxT.__version__}, a generic hybrid python/c++ framework for developing computer vision
algorithms.<br><br>
nexxT is available under the <a href='https://github.com/ifm/nexxT/blob/master/LICENSE'>Apache 2.0 License</a> together
with the <a href='https://github.com/ifm/nexxT/blob/master/NOTICE'>notice</a>.
"""))
self.aboutQt.triggered.connect(lambda: QMessageBox.aboutQt(self))
self.aboutPython.triggered.connect(self._aboutPython)
self.toolbar = None
self.managedMdiWindows = []
self.managedSubplots = {}
self.windows = {}
self.activeApp = None
self._ignoreCloseEvent = False
self._deferredUpdateHistory = {}
self._pendingUpdates = set()
self._deferredUpdateTimer = QTimer()
self._deferredUpdateTimer.setSingleShot(True)
self._deferredUpdateTimer.timeout.connect(self._deferredUpdateTimeout)
[docs]
def eventFilter(self, obj, event):
"""
Overwritten from QObject, for disabling mouse wheel scrolling on the main widget's viewport
"""
if obj is self.mdi.viewport() and event.type() == QEvent.Wheel:
# disable mouse wheel scrolling on the viewport widget it's pretty annoying...
return True
return False
[docs]
@Slot(QObject, str)
def deferredUpdate(self, obj, slotName):
"""
Call this method to defer a call to the sepcified slot (obj->slotName()) in accordance with the user-defined
framerate set. The slot will be called via getattr(obj, slotName)()
:param obj: A QObject instance
:param slotName: The name of the slot.
"""
self._deferredUpdate(obj, slotName)
@handleException
def _deferredUpdate(self, obj, slotName):
assertMainThread()
if (obj, slotName) in self._deferredUpdateHistory:
lastUpdateTime = self._deferredUpdateHistory[obj, slotName]
dt = 1e-9*(time.monotonic_ns() - lastUpdateTime)
if dt < 1/self.framerate:
if len(self._pendingUpdates) == 0:
self._deferredUpdateTimer.start(int((1/self.framerate - dt)*1000))
self._pendingUpdates.add((obj, slotName))
return
self._deferredUpdateHistory[obj, slotName] = time.monotonic_ns()
clb = getattr(obj, slotName)
clb()
def _deferredUpdateTimeout(self):
t = time.monotonic_ns()
for obj, slotName in self._pendingUpdates:
clb = getattr(obj, slotName)
self._deferredUpdateHistory[obj, slotName] = t
clb()
self._pendingUpdates.clear()
def _setFramerate(self, checked):
if checked:
self.framerate = self.sender().data()
[docs]
def closeEvent(self, closeEvent):
"""
Override from QMainWindow, saves the state.
:param closeEvent: a QCloseEvent instance
:return:
"""
self._ignoreCloseEvent = False
self.aboutToClose.emit(self)
if self._ignoreCloseEvent:
logger.info("Ignoring event")
closeEvent.ignore()
return
closeEvent.accept()
self.saveState()
self.saveMdiState()
super().closeEvent(closeEvent)
[docs]
def ignoreCloseEvent(self):
"""
Can be called in slots connected to aboutToClose for requesting to ignore the event.
Use case is the "There are unsaved changes" dialog.
:return:
"""
self._ignoreCloseEvent = True
[docs]
def restoreState(self):
"""
restores the state of the main window including the dock windows of Services
:return:
"""
logger.debug("restoring main window's state")
settings = QSettings()
v = settings.value("MainWindowState")
if v is not None:
super().restoreState(v)
v = settings.value("MainWindowGeometry")
if v is not None:
self.restoreGeometry(v)
if self.toolbar is not None:
self.toolbar.show()
[docs]
def restoreConfigSpecifics(self):
"""
restores the config-specific state of the main window.
"""
propertyCollection = self.config.guiState()
propertyCollection.defineProperty("MainWindow_framerate", 10, "Display framerate set by user.")
framerate = propertyCollection.getProperty("MainWindow_framerate")
logger.debug("Set framerate to %d", framerate)
for a in self.framerateActionGroup.actions():
if a.data() == framerate:
a.setChecked(True)
[docs]
def saveState(self):
"""
saves the state of the main window including the dock windows of Services
:return:
"""
logger.debug("saving main window's state")
settings = QSettings()
settings.setValue("MainWindowState", super().saveState())
settings.setValue("MainWindowGeometry", self.saveGeometry())
[docs]
def saveConfigSpecifics(self):
"""
saves the config-specific state of the main window.
"""
propertyCollection = self.config.guiState()
propertyCollection.defineProperty("MainWindow_framerate", 10, "Display framerate set by user.")
propertyCollection.setProperty("MainWindow_framerate", self.framerate)
logger.debug("Store framerate %d", self.framerate)
[docs]
def saveMdiState(self):
"""
saves the state of the individual MDI windows
:return:
"""
for i in self.managedMdiWindows:
window = i["window"]
propColl = i["propColl"]
prefix = i["prefix"]
logger.debug("save window geometry %s: %s", prefix, window.geometry())
geom = str(window.saveGeometry().toBase64(), "ascii")
visible = self.windows[nexxT.shiboken.getCppPointer(window)[0]].isChecked() # pylint: disable=no-member
propColl.setProperty(prefix + "_geom", geom)
logger.debug("%s is visible: %d", prefix, int(visible))
propColl.setProperty(prefix + "_visible", int(visible))
self.managedMdiWindows = []
@Slot()
def _closeAllPlots(self):
"""
Slot to close all MDI windows
"""
for i in self.managedMdiWindows:
w = i["window"]
w.hide()
self.windows[nexxT.shiboken.getCppPointer(w)[0]].setChecked(False)
@Slot()
def _showAllPlots(self):
"""
Slot to close all MDI windows
"""
for i in self.managedMdiWindows:
i["window"].show()
def __del__(self):
logging.getLogger(__name__).debug("deleting MainWindow")
[docs]
@Slot(str, QPoint)
def updateSelection(self, group, point):
"""
QT Meta-function which can be called to update the 2D selection.
:param group: the group name given as str/QString
:param point: the new selection point given as QPoint
"""
self.userSelectionChanged.emit(group, point)
[docs]
@Slot()
def getToolBar(self):
"""
Get the main toolbar (adds seperators as appropriate).
:return:
"""
if self.toolbar is None:
self.toolbar = self.addToolBar("NexxT")
self.toolbar.setObjectName("NexxT_main_toolbar")
else:
self.toolbar.addSeparator()
return self.toolbar
[docs]
@Slot(str, QObject, int, int)
def newDockWidget(self, name, parent, defaultArea, allowedArea=Qt.LeftDockWidgetArea|Qt.BottomDockWidgetArea,
defaultLoc=None):
"""
This function is supposed to be called by services
:param name: the name of the dock window
:param parent: the parent (usually None)
:param defaultArea: the default dock area
:param allowedArea: the allowed dock areas
:return: a new QDockWindow instance
"""
res = NexxTDockWidget(name, parent if parent is not None else self)
res.setAllowedAreas(allowedArea)
res.setAttribute(Qt.WA_DeleteOnClose, False)
self.addDockWidget(defaultArea, res)
self._registerWindow(res, res.objectNameChanged)
res.setObjectName(name)
if defaultLoc is not None:
dl = self.findChild(QDockWidget, defaultLoc)
if dl is not None:
self.tabifyDockWidget(dl, res)
return res
[docs]
@staticmethod
def parseWindowId(windowId):
"""
convers a subplot window id into windowTitle, row and column
:param windowId: the window id
:return: title, row, column
"""
regexp = re.compile(r"([^\[]+)\[(\d+),\s*(\d+)\]")
match = regexp.match(windowId)
if not match is None:
return match.group(1), int(match.group(2)), int(match.group(3))
return windowId, 0, 0
@staticmethod
def _substWindowTitle(title, theFilter):
if title == "":
title = "${FULLQUALIFIEDFILTERNAME}"
if ("${COMPOSITENAME}" in title or
"${FILTERNAME}" in title or
"${FULLQUALIFIEDFILTERNAME}" in title):
logger.warning("Deprecated: implicit variable substitution for window titles - "
"use explicit variable substitution instead.")
if isinstance(theFilter, Filter):
name = theFilter.environment().getFullQualifiedName()
if "/" in name:
pos = name.rfind("/")
composite_name = name[:pos]
filter_name = name[pos+1:]
else:
composite_name = "<root>"
filter_name = name
title = (title.replace("${COMPOSITENAME}", composite_name).
replace("${FILTERNAME}", filter_name).
replace("${FULLQUALIFIEDFILTERNAME}", name))
else:
logger.warning("MainWindow.subplot(...,theFilter,...) expected a "
"nexxT::Filter instance, but got '%s'", theFilter)
return title
[docs]
@Slot(str, QObject, QWidget)
def subplot(self, windowId, theFilter, widget):
"""
Adds widget to the GridLayout specified by windowId.
:param windowId: a string with the format "<windowTitle>[<row>,<col>]" where <windowTitle> is the caption
of the MDI window (and it is used as identifier for saving/restoring window state) and
<row>, <col> are the coordinates of the addressed subplots (starting at 0)
:param theFilter: a Filter instance which is requesting the subplot
:param widget: a QWidget which shall be placed into the grid layout. Note that this widget is reparented
as a result of this operation and the parents can be used to get access to the MDI sub window.
Use releaseSubplot to remove the window
:return: None
"""
logger.internal("subplot '%s'", windowId)
title, row, col = self.parseWindowId(windowId)
title = self._substWindowTitle(title, theFilter)
if title in self.managedSubplots and (row, col) in self.managedSubplots[title]["plots"]:
logger.warning("subplot %s[%d,%d] is already registered. Creating a new window for the plot.",
title, row, col)
i = 2
while f"{title}({i})" in self.managedSubplots:
i += 1
title = f"{title}({i})"
row = 0
col = 0
if title not in self.managedSubplots:
subWindow = self._newMdiSubWindow(theFilter, title)
swwidget = QWidget()
subWindow.setWidget(swwidget)
layout = QGridLayout(swwidget)
swwidget.setLayout(layout)
self.managedSubplots[title] = dict(mdiSubWindow=subWindow, layout=layout, swwidget=swwidget, plots={})
self.managedSubplots[title]["layout"].addWidget(widget, row, col)
self.managedSubplots[title]["mdiSubWindow"].updateGeometry()
widget.setParent(self.managedSubplots[title]["swwidget"])
# note: there seems to be a race condition when decreasing the single shot timeout to 0
# sometimes the window size is then not correctly adjusted
# with the 100 ms timeout this couldn't be reproduced
QTimer.singleShot(100, lambda: (
self.managedSubplots[title]["mdiSubWindow"].adjustSize() if
nexxT.shiboken.isValid(widget) and ( # pylint: disable=no-member
widget.parent().size().height() < widget.minimumSizeHint().height() or
widget.parent().size().height() < widget.minimumSize().height()) else None
))
self.managedSubplots[title]["plots"][row, col] = widget
[docs]
@Slot(QWidget)
@Slot(str)
def releaseSubplot(self, arg):
"""
This needs to be called to release the previously allocated subplot called windowId.
The managed widget is deleted as a consequence of this function.
:param arg: the widget as passed to subplot. Passing the windowId is also supported, but deprecated.
:return:
"""
if isinstance(arg, str):
windowId = arg
logger.warning("Using deprecated API to release a subplot. Please pass the widget instead of the windowId.")
logger.internal("releaseSubplot '%s'", windowId)
title, row, col = self.parseWindowId(windowId)
if title not in self.managedSubplots or (row, col) not in self.managedSubplots[title]["plots"]:
logger.warning("releasSubplot: cannot find %s", windowId)
return
widget = self.managedSubplots[title]["plots"][row, col]
elif isinstance(arg, QWidget):
widget = arg
found = False
for title, sp in self.managedSubplots.items():
for row, col in sp["plots"]:
if sp["plots"][row, col] is widget:
found = True
break
if found:
break
if not found:
raise RuntimeError("cannot find widget given for releaseSubplot.")
else:
raise RuntimeError("arg of releaseSubplot must be either a string or a QWidget instance.")
self.managedSubplots[title]["layout"].removeWidget(widget)
self.managedSubplots[title]["plots"][row, col].deleteLater()
del self.managedSubplots[title]["plots"][row, col]
if len(self.managedSubplots[title]["plots"]) == 0:
self.managedSubplots[title]["layout"].deleteLater()
self.managedSubplots[title]["swwidget"].deleteLater()
self.managedSubplots[title]["mdiSubWindow"].deleteLater()
del self.managedSubplots[title]
[docs]
@Slot(QObject)
@Slot(QObject, str)
def newMdiSubWindow(self, filterOrService, windowTitle=None):
"""
Deprectated (use subplot(...) instead): This function is supposed to be called by filters.
:param filterOrService: a Filter instance
:param windowTitle: the title of the window (might be None)
:return: a new QMdiSubWindow instance
"""
logger.warning("This function is deprecated. Use subplot function instead.")
return self._newMdiSubWindow(filterOrService, windowTitle)
def _newMdiSubWindow(self, filterOrService, windowTitle):
res = NexxTMdiSubWindow(None)
res.setAttribute(Qt.WA_DeleteOnClose, False)
self.mdi.addSubWindow(res)
self._registerWindow(res, res.windowTitleChanged)
if isinstance(filterOrService, Filter):
propColl = filterOrService.guiState()
res.setWindowTitle(propColl.objectName() if windowTitle is None else windowTitle)
else:
app = Application.activeApplication.getApplication()
propColl = app.guiState("services/MainWindow")
res.setWindowTitle("<unnamed>" if windowTitle is None else windowTitle)
prefix = re.sub(r'[^A-Za-z_0-9]', '_', "MainWindow_MDI_" + res.windowTitle())
i = dict(window=res, propColl=propColl, prefix=prefix)
self.managedMdiWindows.append(i)
window = i["window"]
propColl = i["propColl"]
prefix = i["prefix"]
propColl.defineProperty(prefix + "_geom", "", "Geometry of MDI window")
b = QByteArray.fromBase64(bytes(propColl.getProperty(prefix + "_geom"), "ascii"))
window.restoreGeometry(b)
logger.debug("restored geometry %s:%s (%s)", prefix, window.geometry(), b)
propColl.defineProperty(prefix + "_visible", 1, "Visibility of MDI window")
if propColl.getProperty(prefix + "_visible"):
window.show()
else:
window.hide()
self.mdiSubWindowCreated.emit(res)
return res
def _registerWindow(self, window, nameChangedSignal):
act = QAction("<unnamed>", self)
def ensureVisible():
# see issue https://github.com/ifm/nexxT/issues/64
if isinstance(window, QDockWidget) and window.isFloating():
window.setFloating(False)
window.setVisible(True)
act.setChecked(True)
window.raise_()
if isinstance(window, QMdiSubWindow):
self.mdi.setActiveSubWindow(window)
x = self.mdi.horizontalScrollBar().value()
y = self.mdi.verticalScrollBar().value()
w = self.mdi.viewport().width()
h = self.mdi.viewport().height()
r = QRect(x, y, w, h)
g = window.geometry()
logger.debug("r=%s w=%s", r, g)
if not r.intersects(g):
self.mdi.horizontalScrollBar().setValue(g.x())
self.mdi.verticalScrollBar().setValue(g.y())
act.setCheckable(True)
act.triggered.connect(ensureVisible)
window.visibleChanged.connect(act.setChecked)
nameChangedSignal.connect(act.setText)
self.windows[nexxT.shiboken.getCppPointer(window)[0]] = act # pylint: disable=no-member
if not isinstance(window, QDockWidget):
self.menu.addAction(act)
else:
self.dockWindowsMenu.addAction(act)
logger.debug("Registering window %s, new len=%d",
nexxT.shiboken.getCppPointer(window), len(self.windows)) # pylint: disable=no-member
window.destroyed.connect(self._windowDestroyed)
def _windowDestroyed(self, obj):
logger.internal("_windowDestroyed")
ptr = nexxT.shiboken.getCppPointer(obj) # pylint: disable=no-member
try:
ptr = ptr[0]
except TypeError:
pass
logger.debug("Deregistering window %s, old len=%d", ptr, len(self.windows))
self.windows[ptr].deleteLater()
del self.windows[ptr]
logger.debug("Deregistering window %s done", ptr)
def _appActivated(self, name, app):
if app is not None:
self.activeApp = name
app.aboutToClose.connect(self.saveMdiState, Qt.UniqueConnection)
else:
self.activeApp = None
def _aboutPython(self):
piplic = subprocess.check_output([sys.executable, "-m", "piplicenses", "--format=plain"],
encoding="utf-8").replace("\n", "<br>").replace(" ", " ")
piplic = piplic.replace("<br>", "<br><br>", 1)
msgBox = QMessageBox()
msgBox.setText("This program uses <b>python</b> %(version)s. The used packages are listed below." %
dict(version=sys.version, table=piplic))
view = QScrollArea(msgBox)
label = QLabel("<pre>%(table)s</pre>" % dict(version=sys.version, table=piplic), msgBox)
label.setTextInteractionFlags(Qt.TextSelectableByKeyboard|Qt.TextSelectableByMouse)
view.setWidgetResizable(True)
view.setWidget(label)
msgBox.layout().addWidget(view, 3, 0, 1, msgBox.layout().columnCount())
msgBox.exec_()