diff --git a/nexxT/core/FilterMockup.py b/nexxT/core/FilterMockup.py index 7e2c347..86d23b0 100644 --- a/nexxT/core/FilterMockup.py +++ b/nexxT/core/FilterMockup.py @@ -13,7 +13,8 @@ from nexxT.interface import InputPort, OutputPort, InputPortInterface, OutputPortInterface from nexxT.core.FilterEnvironment import FilterEnvironment from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl -from nexxT.core.Exceptions import PortNotFoundError, PortExistsError, PropertyCollectionChildExists +from nexxT.core.Exceptions import (PortNotFoundError, PortExistsError, PropertyCollectionChildExists, + PropertyCollectionPropertyNotFound) from nexxT.core.Utils import assertMainThread, MethodInvoker import nexxT @@ -33,6 +34,19 @@ def __init__(self, library, factoryFunction, propertyCollection, graph): self._propertyCollectionImpl = propertyCollection self._pluginClass = None self._createFilterAndUpdatePending = None + rootPc = propertyCollection + while rootPc.parent() is not None: + rootPc = rootPc.parent() + tmpRootPc = PropertyCollectionImpl("root", None) + try: + cfgfile = rootPc.getProperty("CFGFILE") + tmpRootPc.defineProperty("CFGFILE", cfgfile, "copy of original CFGFILE.", options=dict(enum=[cfgfile])) + except PropertyCollectionPropertyNotFound: + pass + tmpPc = PropertyCollectionImpl("temp", tmpRootPc) + with FilterEnvironment(self._library, self._factoryFunction, tmpPc, self) as tmpEnv: + self.updatePortInformation(tmpEnv) + del tmpPc try: # add also a child collection for the nexxT internals pc = PropertyCollectionImpl("_nexxT", propertyCollection) diff --git a/nexxT/core/Graph.py b/nexxT/core/Graph.py index 071717f..03490b6 100644 --- a/nexxT/core/Graph.py +++ b/nexxT/core/Graph.py @@ -75,7 +75,8 @@ def cleanup(self): # BaseGraph gets the node name, this method gets arguments # for constructing a filter @Slot(str, str, object) - def addNode(self, library, factoryFunction, suggestedName=None): + def addNode(self, library, factoryFunction, suggestedName=None, + dynamicInputPorts=None, dynamicOutputPorts=None): """ Add a node to the graph, given a library and a factory function for instantiating the plugin. :param library: definition file of the plugin @@ -86,6 +87,10 @@ def addNode(self, library, factoryFunction, suggestedName=None): assertMainThread() if suggestedName is None: suggestedName = factoryFunction + if dynamicInputPorts is None: + dynamicInputPorts = [] + if dynamicOutputPorts is None: + dynamicOutputPorts = [] name = super().uniqueNodeName(suggestedName) try: propColl = self._properties.getChildCollection(name) @@ -93,8 +98,12 @@ def addNode(self, library, factoryFunction, suggestedName=None): propColl = PropertyCollectionImpl(name, self._properties) propColl.propertyChanged.connect(self.setDirty) filterMockup = FilterMockup(library, factoryFunction, propColl, self) - filterMockup.createFilterAndUpdate() self._filters[name] = filterMockup + for din in dynamicInputPorts: + self.addDynamicInputPort(name, din) + for dout in dynamicOutputPorts: + self.addDynamicOutputPort(name, dout) + filterMockup.createFilterAndUpdate() assert super().addNode(name) == name if factoryFunction == "compositeNode" and hasattr(library, "checkRecursion"): try: diff --git a/nexxT/core/PropertyCollectionImpl.py b/nexxT/core/PropertyCollectionImpl.py index 44e8e19..af9e1d4 100644 --- a/nexxT/core/PropertyCollectionImpl.py +++ b/nexxT/core/PropertyCollectionImpl.py @@ -115,12 +115,18 @@ def defineProperty(self, name, defaultVal, helpstr, options=None, propertyHandle "Pass either options or propertyHandler to defineProperty but not both.") if options is None: options = {} + ignoreInconsistentOptions = False + if "ignoreInconsistentOptions" in options: + ignoreInconsistentOptions = options["ignoreInconsistentOptions"] + del options["ignoreInconsistentOptions"] if propertyHandler is None: propertyHandler = defaultHandler(defaultVal)(options) assert isinstance(propertyHandler, PropertyHandler) assert isinstance(options, dict) if propertyHandler.validate(defaultVal) != defaultVal: - raise PropertyInconsistentDefinition("The validation of the default value must be the identity!") + raise PropertyInconsistentDefinition( + "The validation of the default value must be the identity (%s != %s)!" % + (repr(propertyHandler.validate(defaultVal)), repr(defaultVal))) if not name in self._properties: self._properties[name] = Property(defaultVal, helpstr, propertyHandler) p = self._properties[name] @@ -138,7 +144,12 @@ def defineProperty(self, name, defaultVal, helpstr, options=None, propertyHandle if p.defaultVal != defaultVal or p.helpstr != helpstr: raise PropertyInconsistentDefinition(name) if not isinstance(p.handler, type(propertyHandler)) or options != p.handler.options(): - raise PropertyInconsistentDefinition(name) + if ignoreInconsistentOptions: + p.handler = propertyHandler + logger.debug("option %s has inconsistent options but ignoring as requested.", name) + else: + raise PropertyInconsistentDefinition(name) + p.used = True return p.value diff --git a/nexxT/core/SubConfiguration.py b/nexxT/core/SubConfiguration.py index a4ca3cf..3e2b898 100644 --- a/nexxT/core/SubConfiguration.py +++ b/nexxT/core/SubConfiguration.py @@ -123,24 +123,27 @@ def load(self, cfg, compositeLookup): # apply node gui state nextP = PropertyCollectionImpl("_nexxT", p, {"thread": n["thread"]}) logger.debug("loading: subconfig %s / node %s -> thread: %s", self._name, n["name"], n["thread"]) - tmp = self._graph.addNode(n["library"], n["factoryFunction"], suggestedName=n["name"]) + tmp = self._graph.addNode(n["library"], n["factoryFunction"], suggestedName=n["name"], + dynamicInputPorts=n["dynamicInputPorts"], + dynamicOutputPorts=n["dynamicOutputPorts"]) if tmp != n["name"]: raise NexTInternalError("addNode(...) has set unexpected name for node.") else: # composite node handling if n["library"] == "composite://port": # the special nodes are already there, nothing to do here - pass + for dip in n["dynamicInputPorts"]: + self._graph.addDynamicInputPort(n["name"], dip) + for dop in n["dynamicOutputPorts"]: + self._graph.addDynamicOutputPort(n["name"], dop) elif n["library"] == "composite://ref": name = n["factoryFunction"] cf = compositeLookup(name) - tmp = self._graph.addNode(cf, "compositeNode", suggestedName=n["name"]) + tmp = self._graph.addNode(cf, "compositeNode", suggestedName=n["name"], + dynamicInputPorts=n["dynamicInputPorts"], + dynamicOutputPorts=n["dynamicOutputPorts"]) if tmp != n["name"]: raise NexTInternalError("addNode(...) has set unexpected name for node.") - for dip in n["dynamicInputPorts"]: - self._graph.addDynamicInputPort(n["name"], dip) - for dop in n["dynamicOutputPorts"]: - self._graph.addDynamicOutputPort(n["name"], dop) # make sure that the filter is instantiated and the port information is updated immediately self._graph.getMockup(n["name"]).createFilterAndUpdate() for c in cfg["connections"]: diff --git a/nexxT/examples/framework/ImageView.py b/nexxT/examples/framework/ImageView.py index 248b6db..fe41c39 100644 --- a/nexxT/examples/framework/ImageView.py +++ b/nexxT/examples/framework/ImageView.py @@ -79,18 +79,26 @@ def onClose(self): # delete the widget reference self._widget = None - def onPortDataChanged(self, port): + def interpretAndUpdate(self): + """ + The deferred update method, called from the MainWindow service at user-defined framerate. + """ + sample = self.inPort.getData() + if sample.getDatatype() == "example/image": + npa = byteArrayToNumpy(sample.getContent()) + self._widget.setData(npa) + + def onPortDataChanged(self, port): # pylint: disable=unused-argument """ Notification of new data. :param port: the port where the data arrived. :return: """ - if port.getData().getDatatype() == "example/image": - # convert to numpy array - npa = byteArrayToNumpy(port.getData().getContent()) - # send to the widget - self._widget.setData(npa) + if self._widget.isVisible(): + # don't consume processing time if not shown + mw = Services.getService("MainWindow") + mw.deferredUpdate(self, "interpretAndUpdate") class DisplayWidget(QWidget): """ diff --git a/nexxT/interface/PropertyCollections.py b/nexxT/interface/PropertyCollections.py index 3c3341d..30b7792 100644 --- a/nexxT/interface/PropertyCollections.py +++ b/nexxT/interface/PropertyCollections.py @@ -182,7 +182,10 @@ def defineProperty(self, name, defaultVal, helpstr, options=None, propertyHandle :param defaultVal: the default value of the property. Note that this value will be used to determine the property's type. Currently supported types are string, int and float :param helpstr: a help string for the user (presented as a tool tip) - :param options: a dict mapping string to qvariant (common options: min, max, enum) + :param options: a dict mapping string to qvariant (common options: 'min', 'max', 'enum') + all properties support the option 'ignoreInconsistentOptions' (default: False). If this option + is True, then nexxT allows that the options change over time. Even if present, the option type + and its default values are not allowed to change. :param propertyHandler: a PropertyHandler instance, or None for automatic choice according to defaultVal :return: the current value of this property """ diff --git a/nexxT/services/gui/BrowserWidget.py b/nexxT/services/gui/BrowserWidget.py index 82cc73a..9df1a7a 100644 --- a/nexxT/services/gui/BrowserWidget.py +++ b/nexxT/services/gui/BrowserWidget.py @@ -18,12 +18,31 @@ logger = logging.getLogger(__name__) +class StatCache: + """ + Class for caching file-system related accesses to prevent unnecessary slowliness for network drives. + """ + MAX_NUM_CACHE_ENTRIES = 20*1024 # 1024 entries are ~40 kB -> ~ 1 MB cache + + def __init__(self): + self._cache = {} + + def __call__(self, method, *args): + if (method, args) not in self._cache: + # remove entries from cache until the size is within reasonable limits + while len(self._cache) > self.MAX_NUM_CACHE_ENTRIES: + self._cache.pop(next(iter(self._cache))) + self._cache[method, args] = method(*args) + return self._cache[method, args] + class FolderListModel(QAbstractTableModel): """ This class provides a model for browsing a folder. """ folderChanged = Signal(str) # emitted when the folder changes + statCache = StatCache() + def __init__(self, parent): super().__init__(parent=parent) self._folder = None @@ -68,7 +87,7 @@ def fileToIndex(self, filename): return QModelIndex() def _match(self, path): - if path.is_dir(): + if self.statCache(path.is_dir): return True res = QDir.match(self._filter, path.name) return res @@ -79,7 +98,7 @@ def _reset(self, folder, flt): self.endRemoveRows() if folder is not None: listDrives = False - f = Path(folder).resolve() + f = self.statCache(Path(folder).resolve) if platform.system() == "Windows": folder = Path(folder) if folder.name == ".." and folder.parent == Path(folder.drive + "/"): @@ -89,20 +108,22 @@ def _reset(self, folder, flt): self._filter = flt if platform.system() == "Windows": if listDrives: - self._children = [Path("%s:/" % dl) for dl in string.ascii_uppercase if Path("%s:/" % dl).exists()] + self._children = [Path("%s:/" % dl) for dl in string.ascii_uppercase + if self.statCache(Path("%s:/" % dl).exists)] else: self._children = [f / ".."] else: self._children = ([] if f.root == f else [f / ".."]) if not listDrives: self._children += [x for x in f.glob("*") if self._match(x)] - self._children.sort(key=lambda c: (c.is_file(), c.drive, int(c.name != ".."), c.name)) + self._children.sort(key=lambda c: (self.statCache(c.is_file), c.drive, int(c.name != ".."), c.name)) self.beginInsertRows(QModelIndex(), 0, len(self._children)-1) self.endInsertRows() if listDrives: self.folderChanged.emit("") else: - self.folderChanged.emit(str(self._folder) + (os.path.sep if self._folder.is_dir() else "")) + self.folderChanged.emit(str(self._folder) + (os.path.sep if self.statCache(self._folder.is_dir) + else "")) def folder(self): """ @@ -154,7 +175,7 @@ def data(self, index, role): if c.is_dir(): return "" try: - s = c.stat().st_size + s = self.statCache(c.stat).st_size except Exception: # pylint: disable=broad-except return "" if s >= 1024*1024*1024: @@ -171,7 +192,7 @@ def data(self, index, role): return "" if role == Qt.DecorationRole: if index.column() == 0: - if c.is_dir(): + if self.statCache(c.is_dir): return self._iconProvider.icon(QFileIconProvider.Drive) return self._iconProvider.icon(QFileInfo(str(c.absolute()))) if role == Qt.UserRole: @@ -180,7 +201,7 @@ def data(self, index, role): if role in [Qt.DisplayRole, Qt.EditRole]: if index.column() == 3: if index.row() > 0: - return str(c) + (os.path.sep if c.is_dir() else "") + return str(c) + (os.path.sep if self.statCache(c.is_dir) else "") return str(c.parent) + os.path.sep return None diff --git a/nexxT/services/gui/MainWindow.py b/nexxT/services/gui/MainWindow.py index 5ebc572..2bd5c9b 100644 --- a/nexxT/services/gui/MainWindow.py +++ b/nexxT/services/gui/MainWindow.py @@ -11,15 +11,17 @@ import re import subprocess import sys +import time import shiboken2 from PySide2.QtWidgets import (QMainWindow, QMdiArea, QMdiSubWindow, QDockWidget, QAction, QWidget, QGridLayout, - QMenuBar, QMessageBox, QScrollArea, QLabel) + QMenuBar, QMessageBox, QScrollArea, QLabel, QActionGroup) from PySide2.QtCore import (QObject, Signal, Slot, Qt, QByteArray, QDataStream, QIODevice, QRect, QPoint, QSettings, QTimer, QUrl, QEvent) from PySide2.QtGui import QDesktopServices import nexxT from nexxT.interface import Filter from nexxT.core.Application import Application +from nexxT.core.Utils import handleException, assertMainThread logger = logging.getLogger(__name__) @@ -174,6 +176,22 @@ def __init__(self, config): 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("%d Hz" % framerate) + 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) + self.menu.addSeparator() self.aboutMenu = QMenuBar(self.menuBar()) self.menuBar().setCornerWidget(self.aboutMenu) m = self.aboutMenu.addMenu("&Help") @@ -200,6 +218,11 @@ def __init__(self, config): 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) def eventFilter(self, obj, event): if obj is self.mdi.viewport() and event.type() == QEvent.Wheel: @@ -207,6 +230,46 @@ def eventFilter(self, obj, event): return True return False + @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)) + #logger.info("Deferred call: %s", (obj, slotName)) + return + #logger.info("Instant call: %s", (obj, slotName)) + 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() + def closeEvent(self, closeEvent): """ Override from QMainWindow, saves the state. @@ -240,7 +303,7 @@ def restoreState(self): :return: """ - logger.info("restoring main window's state") + logger.debug("restoring main window's state") settings = QSettings() v = settings.value("MainWindowState") if v is not None: @@ -251,17 +314,38 @@ def restoreState(self): if self.toolbar is not None: self.toolbar.show() + 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) + def saveState(self): """ saves the state of the main window including the dock windows of Services :return: """ - logger.info("saving main window's state") + logger.debug("saving main window's state") settings = QSettings() settings.setValue("MainWindowState", super().saveState()) settings.setValue("MainWindowGeometry", self.saveGeometry()) + 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) + def saveMdiState(self): """ saves the state of the individual MDI windows diff --git a/nexxT/tests/core/test_ConfigFiles.py b/nexxT/tests/core/test_ConfigFiles.py index c2a8d75..ada1e0a 100644 --- a/nexxT/tests/core/test_ConfigFiles.py +++ b/nexxT/tests/core/test_ConfigFiles.py @@ -95,4 +95,5 @@ def test_smoke(): simple_setup(4) if __name__ == "__main__": + setup() test_smoke() diff --git a/nexxT/tests/integration/test_gui.py b/nexxT/tests/integration/test_gui.py index a88e14b..c675396 100644 --- a/nexxT/tests/integration/test_gui.py +++ b/nexxT/tests/integration/test_gui.py @@ -328,6 +328,44 @@ def setFilterProperty(self, conf, subConfig, filterName, propName, propVal, expe expectedVal = propVal assert conf.model.data(idxPropVal, Qt.DisplayRole) == expectedVal + def getFilterProperty(self, conf, subConfig, filterName, propName): + """ + Sets a filter property in the configuration gui service. + :param conf: the configuration gui service + :param subConfig: the SubConfiguration instance + :param filterName: the name of the filter + :param propName: the name of the property + :return: the current property value + """ + idxapp = conf.model.indexOfSubConfig(subConfig) + # search for filter + idxFilter = None + for r in range(conf.model.rowCount(idxapp)): + idxFilter = conf.model.index(r, 0, idxapp) + name = conf.model.data(idxFilter, Qt.DisplayRole) + if name == filterName: + break + else: + idxFilter = None + assert idxFilter is not None + # search for property + idxProp = None + row = None + for r in range(conf.model.rowCount(idxFilter)): + idxProp = conf.model.index(r, 0, idxFilter) + name = conf.model.data(idxProp, Qt.DisplayRole) + if name == propName: + row = r + break + else: + idxProp = None + assert idxProp is not None + assert row is not None + # start the editor by pressing F2 on the property value + idxPropVal = conf.model.index(row, 1, idxFilter) + return conf.model.data(idxPropVal, Qt.DisplayRole) + + def getLastLogFrameIdx(self, log): """ Convert the last received log line to a frame index (assuming that the PySimpleStaticFilter has been used) @@ -904,6 +942,92 @@ def __init__(self, env): QTimer.singleShot(self.delay, self.clickDiscardChanges) mw.close() + def _dynamic_properties(self): + conf = None + mw = None + thefilter_py = (Path(self.tmpdir) / "thedynfilter.py") + thefilter_py.write_text( +""" +from nexxT.interface import Filter + +class TheDynFilter(Filter): + def __init__(self, env): + super().__init__(True, True, env) + + def onInit(self): + pc = self.propertyCollection() + din = self.getDynamicInputPorts() + dout = self.getDynamicOutputPorts() + pc.defineProperty("enum_input_ports", "(none)", "help", + dict(enum=["(none)"] +[p.name() for p in din], ignoreInconsistentOptions=True)) + pc.defineProperty("enum_output_ports", "(none)", "help", + dict(enum=["(none)"] +[p.name() for p in dout], ignoreInconsistentOptions=True)) +""" + ) + try: + # load last config + mw = Services.getService("MainWindow") + conf = Services.getService("Configuration") + idxComposites = conf.model.index(0, 0) + idxApplications = conf.model.index(1, 0) + # add application + conf.treeView.setMinimumSize(QSize(300, 300)) + conf.treeView.scrollTo(idxApplications) + region = conf.treeView.visualRegionForSelection(QItemSelection(idxApplications, idxApplications)) + self.qtbot.mouseMove(conf.treeView.viewport(), region.boundingRect().center(), delay=self.delay) + QTimer.singleShot(self.delay, lambda: self.activateContextMenu(CM_ADD_APPLICATION)) + conf._execTreeViewContextMenu(region.boundingRect().center()) + app = conf.configuration().applicationByName("application") + # start graph editor + gev = self.startGraphEditor(conf, mw, "application") + self.qtbot.wait(self.delay) + # create a node "TheFilter" + the_filter = self.addNodeToGraphEditor(gev, QPoint(20, 20), + CM_FILTER_FROM_FILE, str(thefilter_py), "TheDynFilter") + self.qtbot.keyClick(self.aw(), Qt.Key_Return, delay=self.delay) + self.qtbot.keyClick(None, Qt.Key_Return, delay=self.delay) + + QTimer.singleShot(self.delay, lambda: self.activateContextMenu(CM_ADDDYNINPORT)) + QTimer.singleShot(self.delay * 2, lambda: self.enterText("input_port_1")) + self.gsContextMenu(gev, the_filter.nodeGrItem.sceneBoundingRect().center()) + + QTimer.singleShot(self.delay, lambda: self.activateContextMenu(CM_ADDDYNINPORT)) + QTimer.singleShot(self.delay * 2, lambda: self.enterText("input_port_2")) + self.gsContextMenu(gev, the_filter.nodeGrItem.sceneBoundingRect().center()) + + QTimer.singleShot(self.delay, lambda: self.activateContextMenu(CM_ADDDYNOUTPORT)) + QTimer.singleShot(self.delay * 2, lambda: self.enterText("output_port")) + self.gsContextMenu(gev, the_filter.nodeGrItem.sceneBoundingRect().center()) + + logger.info("Filter: %s", repr(the_filter)) + self.setFilterProperty(conf, app, "TheDynFilter", "enum_input_ports", + [Qt.Key_Down, Qt.Key_Return], "input_port_1") + self.setFilterProperty(conf, app, "TheDynFilter", "enum_input_ports", + [Qt.Key_Down, Qt.Key_Return], "input_port_2") + self.setFilterProperty(conf, app, "TheDynFilter", "enum_output_ports", + [Qt.Key_Down, Qt.Key_Return], "output_port") + + cfgfile = str((Path(self.tmpdir) / "dynprops.json").absolute()) + QTimer.singleShot(self.delay, lambda: self.enterText(cfgfile)) + conf.actSave.trigger() + + logger.info("saved application") + + QTimer.singleShot(self.delay, lambda: self.enterText(cfgfile)) + conf.actLoad.trigger() + + logger.info("loaded application") + app = conf.configuration().applicationByName("application") + assert self.getFilterProperty(conf, app, "TheDynFilter", "enum_input_ports") == "input_port_2" + assert self.getFilterProperty(conf, app, "TheDynFilter", "enum_output_ports") == "output_port" + + + finally: + if not self.keep_open: + if conf.configuration().dirty(): + QTimer.singleShot(self.delay, self.clickDiscardChanges) + mw.close() + def test(self): """ test property editing in config editor @@ -912,12 +1036,26 @@ def test(self): QTimer.singleShot(self.delay, self._properties) startNexT(None, None, [], [], True) + def test_dyn(self): + """ + test property editing in config editor + :return: + """ + QTimer.singleShot(self.delay, self._dynamic_properties) + startNexT(None, None, [], [], True) + @pytest.mark.gui @pytest.mark.parametrize("delay", [300]) def test_properties(qtbot, xvfb, keep_open, delay, tmpdir): test = PropertyTest(qtbot, xvfb, keep_open, delay, tmpdir) test.test() +@pytest.mark.gui +@pytest.mark.parametrize("delay", [300]) +def test_dyn_properties(qtbot, xvfb, keep_open, delay, tmpdir): + test = PropertyTest(qtbot, xvfb, keep_open, delay, tmpdir) + test.test_dyn() + class GuiStateTest(GuiTestBase): """ Concrete test class for the guistate test case