From b4b269f0213ffeb9d5a826e57ac6b1ead4127709 Mon Sep 17 00:00:00 2001 From: cwiede <62332054+cwiede@users.noreply.github.com> Date: Sat, 21 Mar 2020 13:15:24 +0100 Subject: [PATCH 01/16] add dll files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b6e4761..4bea2d7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ # C extensions *.so +*.dll # Distribution / packaging .Python From be7cfda6f2821229b6608ae4acad86c16f241e92 Mon Sep 17 00:00:00 2001 From: cwiede <62332054+cwiede@users.noreply.github.com> Date: Sat, 21 Mar 2020 13:21:23 +0100 Subject: [PATCH 02/16] add NOTICE file --- NOTICE | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..86c1190 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +nexxT +Copyright (C) 2020 ifm electronic gmbh From a4be8ab3e54562729654b941c2a97f7715a1542f Mon Sep 17 00:00:00 2001 From: cwiede <62332054+cwiede@users.noreply.github.com> Date: Sat, 21 Mar 2020 13:28:06 +0100 Subject: [PATCH 03/16] initial checkin --- MANIFEST.in | 19 + nexxT/__init__.py | 54 + nexxT/core/ActiveApplication.py | 394 ++++++ nexxT/core/AppConsole.py | 147 +++ nexxT/core/Application.py | 97 ++ nexxT/core/BaseGraph.py | 347 ++++++ nexxT/core/CompositeFilter.py | 148 +++ nexxT/core/ConfigFileSchema.json | 118 ++ nexxT/core/ConfigFiles.py | 220 ++++ nexxT/core/Configuration.py | 282 +++++ nexxT/core/Exceptions.py | 161 +++ nexxT/core/FilterEnvironment.py | 437 +++++++ nexxT/core/FilterMockup.py | 169 +++ nexxT/core/Graph.py | 264 ++++ nexxT/core/GuiStateSchema.json | 56 + nexxT/core/PluginManager.py | 233 ++++ nexxT/core/PortImpl.py | 174 +++ nexxT/core/PropertyCollectionImpl.py | 371 ++++++ nexxT/core/SubConfiguration.py | 190 +++ nexxT/core/Thread.py | 163 +++ nexxT/core/Utils.py | 261 ++++ nexxT/core/__init__.py | 19 + nexxT/interface/DataSamples.py | 59 + nexxT/interface/Filters.py | 160 +++ nexxT/interface/Ports.py | 181 +++ nexxT/interface/PropertyCollections.py | 63 + nexxT/interface/Services.py | 45 + nexxT/interface/__init__.py | 48 + nexxT/services/ConsoleLogger.py | 84 ++ nexxT/services/__init__.py | 5 + nexxT/services/gui/Configuration.py | 707 +++++++++++ nexxT/services/gui/GraphEditor.py | 1080 +++++++++++++++++ nexxT/services/gui/GraphLayering.py | 209 ++++ nexxT/services/gui/MainWindow.py | 380 ++++++ nexxT/services/gui/PlaybackControl.py | 631 ++++++++++ nexxT/services/gui/PropertyDelegate.py | 109 ++ nexxT/services/gui/__init__.py | 5 + nexxT/src/DataSamples.cpp | 63 + nexxT/src/DataSamples.hpp | 50 + nexxT/src/FilterEnvironment.cpp | 126 ++ nexxT/src/FilterEnvironment.hpp | 70 ++ nexxT/src/Filters.cpp | 137 +++ nexxT/src/Filters.hpp | 76 ++ nexxT/src/Logger.cpp | 31 + nexxT/src/Logger.hpp | 43 + nexxT/src/NexTConfig.hpp | 26 + nexxT/src/NexTLinkage.hpp | 31 + nexxT/src/NexTPlugins.cpp | 97 ++ nexxT/src/NexTPlugins.hpp | 69 ++ nexxT/src/Ports.cpp | 243 ++++ nexxT/src/Ports.hpp | 119 ++ nexxT/src/PropertyCollection.cpp | 41 + nexxT/src/PropertyCollection.hpp | 37 + nexxT/src/SConscript.py | 101 ++ nexxT/src/Services.cpp | 115 ++ nexxT/src/Services.hpp | 44 + nexxT/src/cnexxT.h | 16 + nexxT/src/cnexxT.xml | 131 ++ nexxT/tests/__init__.py | 5 + nexxT/tests/core/__init__.py | 5 + nexxT/tests/core/test1.json | 58 + nexxT/tests/core/test2.json | 58 + nexxT/tests/core/test_ActiveApplication.py | 184 +++ nexxT/tests/core/test_BaseGraph.py | 172 +++ nexxT/tests/core/test_CompositeFilter.py | 308 +++++ nexxT/tests/core/test_ConfigFiles.py | 84 ++ nexxT/tests/core/test_FilterEnvironment.py | 196 +++ nexxT/tests/core/test_FilterExceptions.py | 375 ++++++ nexxT/tests/core/test_FilterMockup.py | 22 + nexxT/tests/core/test_Graph.py | 87 ++ .../tests/core/test_PropertyCollectionImpl.py | 145 +++ nexxT/tests/interface/AviFilePlayback.py | 155 +++ nexxT/tests/interface/QImageDisplay.py | 54 + nexxT/tests/interface/SimpleDynamicFilter.py | 69 ++ nexxT/tests/interface/SimplePlaybackDevice.py | 82 ++ nexxT/tests/interface/SimpleStaticFilter.py | 102 ++ nexxT/tests/interface/TestExceptionFilter.py | 37 + nexxT/tests/interface/__init__.py | 5 + nexxT/tests/interface/test_dataSample.py | 24 + nexxT/tests/src/AviFilePlayback.cpp | 324 +++++ nexxT/tests/src/AviFilePlayback.hpp | 75 ++ nexxT/tests/src/Plugins.cpp | 16 + nexxT/tests/src/SConscript.py | 25 + nexxT/tests/src/SimpleSource.cpp | 52 + nexxT/tests/src/SimpleSource.hpp | 35 + nexxT/tests/src/TestExceptionFilter.cpp | 71 ++ nexxT/tests/src/TestExceptionFilter.hpp | 33 + setup.py | 78 ++ 88 files changed, 12692 insertions(+) create mode 100644 MANIFEST.in create mode 100644 nexxT/__init__.py create mode 100644 nexxT/core/ActiveApplication.py create mode 100644 nexxT/core/AppConsole.py create mode 100644 nexxT/core/Application.py create mode 100644 nexxT/core/BaseGraph.py create mode 100644 nexxT/core/CompositeFilter.py create mode 100644 nexxT/core/ConfigFileSchema.json create mode 100644 nexxT/core/ConfigFiles.py create mode 100644 nexxT/core/Configuration.py create mode 100644 nexxT/core/Exceptions.py create mode 100644 nexxT/core/FilterEnvironment.py create mode 100644 nexxT/core/FilterMockup.py create mode 100644 nexxT/core/Graph.py create mode 100644 nexxT/core/GuiStateSchema.json create mode 100644 nexxT/core/PluginManager.py create mode 100644 nexxT/core/PortImpl.py create mode 100644 nexxT/core/PropertyCollectionImpl.py create mode 100644 nexxT/core/SubConfiguration.py create mode 100644 nexxT/core/Thread.py create mode 100644 nexxT/core/Utils.py create mode 100644 nexxT/core/__init__.py create mode 100644 nexxT/interface/DataSamples.py create mode 100644 nexxT/interface/Filters.py create mode 100644 nexxT/interface/Ports.py create mode 100644 nexxT/interface/PropertyCollections.py create mode 100644 nexxT/interface/Services.py create mode 100644 nexxT/interface/__init__.py create mode 100644 nexxT/services/ConsoleLogger.py create mode 100644 nexxT/services/__init__.py create mode 100644 nexxT/services/gui/Configuration.py create mode 100644 nexxT/services/gui/GraphEditor.py create mode 100644 nexxT/services/gui/GraphLayering.py create mode 100644 nexxT/services/gui/MainWindow.py create mode 100644 nexxT/services/gui/PlaybackControl.py create mode 100644 nexxT/services/gui/PropertyDelegate.py create mode 100644 nexxT/services/gui/__init__.py create mode 100644 nexxT/src/DataSamples.cpp create mode 100644 nexxT/src/DataSamples.hpp create mode 100644 nexxT/src/FilterEnvironment.cpp create mode 100644 nexxT/src/FilterEnvironment.hpp create mode 100644 nexxT/src/Filters.cpp create mode 100644 nexxT/src/Filters.hpp create mode 100644 nexxT/src/Logger.cpp create mode 100644 nexxT/src/Logger.hpp create mode 100644 nexxT/src/NexTConfig.hpp create mode 100644 nexxT/src/NexTLinkage.hpp create mode 100644 nexxT/src/NexTPlugins.cpp create mode 100644 nexxT/src/NexTPlugins.hpp create mode 100644 nexxT/src/Ports.cpp create mode 100644 nexxT/src/Ports.hpp create mode 100644 nexxT/src/PropertyCollection.cpp create mode 100644 nexxT/src/PropertyCollection.hpp create mode 100644 nexxT/src/SConscript.py create mode 100644 nexxT/src/Services.cpp create mode 100644 nexxT/src/Services.hpp create mode 100644 nexxT/src/cnexxT.h create mode 100644 nexxT/src/cnexxT.xml create mode 100644 nexxT/tests/__init__.py create mode 100644 nexxT/tests/core/__init__.py create mode 100644 nexxT/tests/core/test1.json create mode 100644 nexxT/tests/core/test2.json create mode 100644 nexxT/tests/core/test_ActiveApplication.py create mode 100644 nexxT/tests/core/test_BaseGraph.py create mode 100644 nexxT/tests/core/test_CompositeFilter.py create mode 100644 nexxT/tests/core/test_ConfigFiles.py create mode 100644 nexxT/tests/core/test_FilterEnvironment.py create mode 100644 nexxT/tests/core/test_FilterExceptions.py create mode 100644 nexxT/tests/core/test_FilterMockup.py create mode 100644 nexxT/tests/core/test_Graph.py create mode 100644 nexxT/tests/core/test_PropertyCollectionImpl.py create mode 100644 nexxT/tests/interface/AviFilePlayback.py create mode 100644 nexxT/tests/interface/QImageDisplay.py create mode 100644 nexxT/tests/interface/SimpleDynamicFilter.py create mode 100644 nexxT/tests/interface/SimplePlaybackDevice.py create mode 100644 nexxT/tests/interface/SimpleStaticFilter.py create mode 100644 nexxT/tests/interface/TestExceptionFilter.py create mode 100644 nexxT/tests/interface/__init__.py create mode 100644 nexxT/tests/interface/test_dataSample.py create mode 100644 nexxT/tests/src/AviFilePlayback.cpp create mode 100644 nexxT/tests/src/AviFilePlayback.hpp create mode 100644 nexxT/tests/src/Plugins.cpp create mode 100644 nexxT/tests/src/SConscript.py create mode 100644 nexxT/tests/src/SimpleSource.cpp create mode 100644 nexxT/tests/src/SimpleSource.hpp create mode 100644 nexxT/tests/src/TestExceptionFilter.cpp create mode 100644 nexxT/tests/src/TestExceptionFilter.hpp create mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..3f057d0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,19 @@ +include nexxT/include\DataSamples.hpp +include nexxT/include\FilterEnvironment.hpp +include nexxT/include\Filters.hpp +include nexxT/include\Logger.hpp +include nexxT/include\NexTConfig.hpp +include nexxT/include\NexTLinkage.hpp +include nexxT/include\NexTPlugins.hpp +include nexxT/include\Ports.hpp +include nexxT/include\PropertyCollection.hpp +include nexxT/include\Services.hpp +include nexxT/binary/msvc_x86_64/nonopt/cnexxT.pyd +include nexxT/binary/msvc_x86_64/nonopt/nexxT.dll +include nexxT/binary/msvc_x86_64/nonopt/nexxT.exp +include nexxT/binary/msvc_x86_64/nonopt/nexxT.lib +include nexxT/binary/msvc_x86_64/release/cnexxT.pyd +include nexxT/binary/msvc_x86_64/release/nexxT.dll +include nexxT/binary/msvc_x86_64/release/nexxT.exp +include nexxT/binary/msvc_x86_64/release/nexxT.lib +include nexxT/core/ConfigFileSchema.json \ No newline at end of file diff --git a/nexxT/__init__.py b/nexxT/__init__.py new file mode 100644 index 0000000..c957491 --- /dev/null +++ b/nexxT/__init__.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +""" +Setup the logging here until we have a better place. +""" + +def setup(): + import logging + from pathlib import Path + import os + import sys + + logger = logging.getLogger() + INTERNAL = 5 # setup log level for internal messages + logging.addLevelName(INTERNAL, "INTERNAL") + def internal(self, message, *args, **kws): + if self.isEnabledFor(INTERNAL): + # Yes, logger takes its '*args' as 'args'. + self._log(INTERNAL, message, args, **kws) + logging.Logger.internal = internal + + console = logging.StreamHandler() + console.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) + logger.addHandler(console) + logger.info("configured logger") + logger.setLevel(logging.INFO) + + global useCImpl + useCImpl = not bool(int(os.environ.get("NEXT_DISABLE_CIMPL", "0"))) + if useCImpl: + # make sure to import PySide2 before loading the cnexxT extension module because + # there is a link-time dependency which would be impossible to resolve otherwise + import PySide2.QtCore + p = os.environ.get("NEXT_CEXT_PATH", None) + if p is None: + p = [p for p in [Path(__file__).parent / "binary" / "msvc_x86_64" / "release", + Path(__file__).parent / "binary" / "linux_x86_64" / "release"] if p.exists()] + if len(p) > 0: + p = p[0].absolute() + else: + p = None + if p is not None: + p = str(Path(p).absolute()) + logger.info("c extension module search path: %s", p) + sys.path.append(p) + import cnexxT as imp_cnexxT + global cnexxT + cnexxT = imp_cnexxT + +setup() \ No newline at end of file diff --git a/nexxT/core/ActiveApplication.py b/nexxT/core/ActiveApplication.py new file mode 100644 index 0000000..b54aedf --- /dev/null +++ b/nexxT/core/ActiveApplication.py @@ -0,0 +1,394 @@ +# 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 contains the class definition of ActiveApplication +""" + +import logging +from PySide2.QtCore import QObject, Slot, Signal, Qt, QCoreApplication +from nexxT.interface import FilterState, OutputPortInterface, InputPortInterface +from nexxT.core.Exceptions import FilterStateMachineError, NexTInternalError +from nexxT.core.CompositeFilter import CompositeFilter +from nexxT.core.Utils import Barrier, assertMainThread +from nexxT.core.Thread import NexTThread + +logger = logging.getLogger(__name__) # pylint: disable=invalid-name + +class ActiveApplication(QObject): + """ + Class for managing an active filter graph. This class lives in the main thread. It is assumed that the graph + is fixed during the livetime of the active application. + """ + + performOperation = Signal(str, object) # Signal is connected to all the threads (operation, barrier) + stateChanged = Signal(int) # Signal is emitted after the state of the graph has been changed + aboutToStop = Signal() # Signal is emitted before stop operation takes place + + def __init__(self, graph): + super().__init__() + assertMainThread() + self._graph = graph + self._threads = {} + self._filters2threads = {} + self._composite2graphs = {} + self._traverseAndSetup(graph) + # initialize private variables + self._numThreadsSynced = 0 + self._state = FilterState.CONSTRUCTING + self._graphConnected = False + self._interThreadConns = [] + self._operationInProgress = False + # connect signals and slots + for tname in self._threads: + t = self._threads[tname] + t.operationFinished.connect(self._operationFinished) + # we use a queued connection because we want to be able to connect signals + # to and from this object after constructor has passed + self.performOperation.connect(t.performOperation, type=Qt.QueuedConnection) + # finally, create the filters + self.create() + + def getApplication(self): + """ + Return the corresponding application instance + :return: + """ + return self._graph.getSubConfig() + + def _traverseAndSetup(self, graph, namePrefix=""): + """ + Recursively create threads and add the filter mockups to them + """ + for basename in graph.allNodes(): + filtername = namePrefix + "/" + basename + mockup = graph.getMockup(basename) + if issubclass(mockup.getPluginClass(), CompositeFilter.CompositeNode): + with mockup.createFilter() as cf: + self._composite2graphs[filtername] = cf.getPlugin().getGraph() + self._traverseAndSetup(cf.getPlugin().getGraph(), filtername) + elif issubclass(mockup.getPluginClass(), CompositeFilter.CompositeInputNode): + pass + elif issubclass(mockup.getPluginClass(), CompositeFilter.CompositeOutputNode): + pass + else: + props = mockup.getPropertyCollectionImpl() + nexTprops = props.getChildCollection("_nexxT") + threadName = nexTprops.getProperty("thread") + if not threadName in self._threads: + # create threads as needed + self._threads[threadName] = NexTThread(threadName) + self._threads[threadName].addMockup(filtername, mockup) + self._filters2threads[filtername] = threadName + + def __del__(self): + logger.debug("destructor of ActiveApplication") + if self._state != FilterState.DESTRUCTING and self._state != FilterState.DESTRUCTED: + logger.warning("ActiveApplication: shutdown in destructor") + self.cleanup() + logger.debug("destructor of ActiveApplication done") + + def cleanup(self): + """ + Clean up all memory objects held. Afterwards the object shall not be used anymore. + :return: + """ + self.shutdown() + self._graph = None + self._threads = {} + self._filters2threads = {} + self._composite2graphs = {} + # initialize private variables + self._numThreadsSynced = 0 + self._interThreadConns = [] + + def getState(self): + """ + return current state + :return: a FilterState integer + """ + return self._state + + @Slot() + def shutdown(self): + """ + Transfer graph to DESTRUCTED state. + :return: None + """ + assertMainThread() + if self._state == FilterState.ACTIVE: + self.stop() + if self._state == FilterState.INITIALIZED: + self.deinit() + if self._state == FilterState.CONSTRUCTED: + self.destruct() + if not self._state == FilterState.DESTRUCTED: + raise NexTInternalError("Unexpected state after shutdown.") + + def stopThreads(self): + """ + stop all threads (except main) + :return: None + """ + logger.internal("stopping threads...") + assertMainThread() + for tname in self._threads: + self._threads[tname].cleanup() + self._threads.clear() + + @staticmethod + def _compress(proxy): + """ + compress transitive composite proxy dependencies (e.g. when a composite input is itself connected to a composite + filter) + """ + changed = True + while changed: + changed = False + for compName, fromPort in proxy: + for idx in range(len(proxy[compName, fromPort])): + if proxy[compName, fromPort][idx] is None: + continue + proxyNode, proxyPort = proxy[compName, fromPort][idx] + # if fromNode is itself a composite node, resolve it + if (proxyNode, proxyPort) in proxy: + changed = True + proxy[compName, fromPort][idx] = None + proxy[compName, fromPort].extend(proxy[proxyNode, proxyPort]) + # remove None's + for compName, fromPort in proxy: + toDel = set() + for idx in range(len(proxy[compName, fromPort])): + if proxy[compName, fromPort][idx] is None: + toDel.add(idx) + for idx in sorted(toDel)[::-1]: + assert proxy[compName, fromPort][idx] is None + proxy[compName, fromPort] = proxy[compName, fromPort][:idx] + proxy[compName, fromPort][idx + 1:] + return proxy + + def _calculateProxyPorts(self): + """ + collect ports which are connected to the proxy nodes in composite graphs + """ + proxyInputPorts = {} + proxyOutputPorts = {} + for compName in self._composite2graphs: + subgraph = self._composite2graphs[compName] + cin_node = "CompositeInput" + for fromPort in subgraph.allOutputPorts(cin_node): + proxyInputPorts[compName, fromPort] = [] + for _, _, toNode, toPort in subgraph.allConnectionsFromOutputPort(cin_node, fromPort): + proxyInputPorts[compName, fromPort].append((compName + "/" + toNode, toPort)) + proxyOutputPorts[compName + "/" + cin_node, fromPort] = [] + cout_node = "CompositeOutput" + for toPort in subgraph.allInputPorts(cout_node): + proxyOutputPorts[compName, toPort] = [] + for fromNode, fromPort, _, _ in subgraph.allConnectionsToInputPort(cout_node, toPort): + proxyOutputPorts[compName, toPort].append((compName + "/" + fromNode, fromPort)) + proxyInputPorts[compName + "/" + cout_node, toPort] = [] + + return self._compress(proxyInputPorts), self._compress(proxyOutputPorts) + + + def _allConnections(self): + """ + return all connections of this application including the connections from and to composite nodes + """ + proxyInputPorts, proxyOutputPorts = self._calculateProxyPorts() + allGraphs = set([(n, self._composite2graphs[n]) for n in self._composite2graphs] + [("", self._graph)]) + res = [] + + for namePrefix, graph in allGraphs: + for fromNode, fromPort, toNode, toPort in graph.allConnections(): + fromName = namePrefix + "/" + fromNode + toName = namePrefix + "/" + toNode + + if (fromName, fromPort) in proxyOutputPorts: + src = proxyOutputPorts[fromName, fromPort] + else: + src = [(fromName, fromPort)] + + if (toName, toPort) in proxyInputPorts: + dest = proxyInputPorts[toName, toPort] + else: + dest = [(toName, toPort)] + + for s in src: + for d in dest: + res.append(s + d) + return res + + def _setupConnections(self): + """ + Setup the connections for actual datasample transport. It is assumed that connections are fixed during the + livetime of the active application + :return: None + """ + assertMainThread() + if self._graphConnected: + return + for fromNode, fromPort, toNode, toPort in self._allConnections(): + fromThread = self._filters2threads[fromNode] + toThread = self._filters2threads[toNode] + p0 = self._threads[fromThread].getFilter(fromNode).getPort(fromPort, OutputPortInterface) + p1 = self._threads[toThread].getFilter(toNode).getPort(toPort, InputPortInterface) + if toThread == fromThread: + OutputPortInterface.setupDirectConnection(p0, p1) + else: + itc = OutputPortInterface.setupInterThreadConnection(p0, p1, self._threads[fromThread].qthread()) + #itc = self.InterThreadConnection(self._threads[fromThread].qthread()) + #p0.transmitSample.connect(itc.receiveSample) + #itc.transmitInterThread.connect(p1.receiveAsync) + self._interThreadConns.append(itc) + self._graphConnected = True + + @Slot() + def _operationFinished(self): + """ + slot called once from each thread which has been finished with an operation + """ + logger.internal("operation finished callback") + assertMainThread() + self._numThreadsSynced += 1 + if self._numThreadsSynced == len(self._threads): + # received the finished signal from all threads + # perform state transition + self._numThreadsSynced = 0 + if self._state == FilterState.CONSTRUCTING: + self._state = FilterState.CONSTRUCTED + elif self._state == FilterState.INITIALIZING: + self._state = FilterState.INITIALIZED + elif self._state == FilterState.STARTING: + self._state = FilterState.ACTIVE + elif self._state == FilterState.STOPPING: + self._state = FilterState.INITIALIZED + elif self._state == FilterState.DEINITIALIZING: + self._state = FilterState.CONSTRUCTED + elif self._state == FilterState.DESTRUCTING: + self._state = FilterState.DESTRUCTED + self.stopThreads() + self.stateChanged.emit(self._state) + + @Slot() + def create(self): + """ + Perform create operation + :return:None + """ + assertMainThread() + while self._operationInProgress and self._state != FilterState.CONSTRUCTING: + QCoreApplication.processEvents() + if self._state != FilterState.CONSTRUCTING: + raise FilterStateMachineError(self._state, FilterState.CONSTRUCTING) + self._operationInProgress = True + self.performOperation.emit("create", Barrier(len(self._threads))) + while self._state == FilterState.CONSTRUCTING: + QCoreApplication.processEvents() + self._operationInProgress = False + + @Slot() + def init(self): + """ + Perform init operation + :return:None + """ + logger.internal("entering init operation, old state %s", FilterState.state2str(self._state)) + assertMainThread() + while self._operationInProgress and self._state != FilterState.CONSTRUCTED: + QCoreApplication.processEvents() + if self._state != FilterState.CONSTRUCTED: + raise FilterStateMachineError(self._state, FilterState.INITIALIZING) + self._operationInProgress = True + self._state = FilterState.INITIALIZING + self.performOperation.emit("init", Barrier(len(self._threads))) + while self._state == FilterState.INITIALIZING: + QCoreApplication.processEvents() + self._operationInProgress = False + logger.internal("leaving operation done, new state %s", FilterState.state2str(self._state)) + + @Slot() + def start(self): + """ + Setup connections if necessary and perform start operation + :return:None + """ + logger.internal("entering start operation, old state %s", FilterState.state2str(self._state)) + assertMainThread() + while self._operationInProgress and self._state != FilterState.INITIALIZED: + QCoreApplication.processEvents() + if self._state != FilterState.INITIALIZED: + raise FilterStateMachineError(self._state, FilterState.STARTING) + self._operationInProgress = True + self._state = FilterState.STARTING + self._setupConnections() + self.performOperation.emit("start", Barrier(len(self._threads))) + while self._state == FilterState.STARTING: + QCoreApplication.processEvents() + self._operationInProgress = False + logger.internal("leaving start operation, new state %s", FilterState.state2str(self._state)) + + @Slot() + def stop(self): + """ + Perform stop operation + :return: None + """ + logger.internal("entering stop operation, old state %s", FilterState.state2str(self._state)) + self.aboutToStop.emit() + assertMainThread() + while self._operationInProgress and self._state != FilterState.ACTIVE: + QCoreApplication.processEvents() + if self._state != FilterState.ACTIVE: + raise FilterStateMachineError(self._state, FilterState.STOPPING) + self._operationInProgress = True + self._state = FilterState.STOPPING + self.performOperation.emit("stop", Barrier(len(self._threads))) + while self._state == FilterState.STOPPING: + QCoreApplication.processEvents() + self._operationInProgress = False + logger.internal("leaving stop operation, new state %s", FilterState.state2str(self._state)) + + + @Slot() + def deinit(self): + """ + Perform deinit operation + :return: None + """ + logger.internal("entering deinit operation, old state %s", FilterState.state2str(self._state)) + assertMainThread() + while self._operationInProgress and self._state != FilterState.INITIALIZED: + QCoreApplication.processEvents() + if self._state != FilterState.INITIALIZED: + raise FilterStateMachineError(self._state, FilterState.DEINITIALIZING) + self._operationInProgress = True + self._state = FilterState.DEINITIALIZING + self.performOperation.emit("deinit", Barrier(len(self._threads))) + while self._state == FilterState.DEINITIALIZING: + QCoreApplication.processEvents() + self._operationInProgress = False + logger.internal("leaving stop operation, new state %s", FilterState.state2str(self._state)) + + @Slot() + def destruct(self): + """ + Perform destruct operation + :return: None + """ + logger.internal("entering destruct operation, old state %s", FilterState.state2str(self._state)) + assertMainThread() + while self._operationInProgress and self._state != FilterState.CONSTRUCTED: + QCoreApplication.processEvents() + if self._state != FilterState.CONSTRUCTED: + raise FilterStateMachineError(self._state, FilterState.DESTRUCTING) + self._state = FilterState.DESTRUCTING + self.performOperation.emit("destruct", Barrier(len(self._threads))) + logger.internal("waiting...") + while self._state == FilterState.DESTRUCTING: + QCoreApplication.processEvents() + logger.internal("waiting...") + logger.internal("waiting done") + logger.internal("leaving destruct operation, old state %s", FilterState.state2str(self._state)) diff --git a/nexxT/core/AppConsole.py b/nexxT/core/AppConsole.py new file mode 100644 index 0000000..82b80d1 --- /dev/null +++ b/nexxT/core/AppConsole.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +""" +Console entry point script for starting nexxT from command line without GUI. +""" + +from argparse import ArgumentParser +import logging +import sys +from PySide2.QtCore import QCoreApplication +from PySide2.QtWidgets import QApplication + +from nexxT.core.Utils import SQLiteHandler, MethodInvoker +from nexxT.core.ConfigFiles import ConfigFileLoader +from nexxT.core.Configuration import Configuration +from nexxT.core.PluginManager import PluginManager +from nexxT.core.Application import Application +from nexxT.interface import Services + +from nexxT.services.ConsoleLogger import ConsoleLogger +from nexxT.services.gui.MainWindow import MainWindow +from nexxT.services.gui.Configuration import MVCConfigurationGUI +from nexxT.services.gui.PlaybackControl import MVCPlaybackControlGUI + +logger = logging.getLogger(__name__) + +def setupConsoleServices(config): # pylint: disable=unused-argument + """ + Adds services available in console mode. + :param config: a nexxT.core.Configuration instance + :return: None + """ + Services.addService("Logging", ConsoleLogger()) + +def setupGuiServices(config): + """ + Adds services available in console mode. + :param config: a nexxT.core.Configuration instance + :return: None + """ + Services.addService("Logging", ConsoleLogger()) # TODO: provide gui logging service + mainWindow = MainWindow(config) + Services.addService("MainWindow", mainWindow) + Services.addService("PlaybackControl", MVCPlaybackControlGUI(config)) + Services.addService("Configuration", MVCConfigurationGUI(config)) + +def startNexT(cfgfile, active, withGui): + """ + Starts next with the given config file and activates the given application. + :param cfgfile: path to config file + :param active: active application (if None, the first application in the config will be used) + :return: None + """ + logger.debug("Starting nexxT...") + config = Configuration() + if withGui: + app = QApplication() + setupGuiServices(config) + else: + app = QCoreApplication() + setupConsoleServices(config) + + app.setOrganizationName("nexxT") + app.setApplicationName("nexxT") + ConfigFileLoader.load(config, cfgfile) + if withGui: + mainWindow = Services.getService("MainWindow") + mainWindow.restoreState() + mainWindow.show() + if active is not None: + config.activate(active) + # need the reference of this + i2 = MethodInvoker(Application.initialize, MethodInvoker.IDLE_TASK) # pylint: disable=unused-variable + + def cleanup(): + logger.debug("cleaning up loaded services") + Services.removeAll() + logger.debug("cleaning up loaded plugins") + for v in ("last_traceback", "last_type", "last_value"): + if hasattr(sys, v): + del sys.__dict__[v] + #PluginManager.singleton().unloadAll() + logger.debug("cleaning up complete") + + res = app.exec_() + logger.debug("closing config") + config.close() + cleanup() + + logger.internal("app.exec_ returned") + + return res + +def main(withGui): + """ + main function used as entry point + :return: None + """ + parser = ArgumentParser(description="nexxT console application") + parser.add_argument("cfg", nargs=1, help=".json configuration file of the project to be loaded.") + parser.add_argument("-a", "--active", default=None, type=str, + help="active application; default: first application in config file") + parser.add_argument("-l", "--logfile", default=None, type=str, + help="log file location (.db extension will use sqlite).") + parser.add_argument("-v", "--verbosity", default="INFO", + choices=["INTERNAL", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "CRITICAL"], + help="sets the log verbosity") + parser.add_argument("-q", "--quiet", action="store_true", default=False, help="disble logging to stderr") + args = parser.parse_args() + + nexT_logger = logging.getLogger() + nexT_logger.setLevel(args.verbosity) + nexT_logger.debug("Setting verbosity: %s", args.verbosity) + if args.quiet: + for h in nexT_logger.handlers: + nexT_logger.removeHandler(h) + if args.logfile is not None: + if args.logfile.endswith(".db"): + handler = SQLiteHandler(args.logfile) + nexT_logger.addHandler(handler) + else: + handler = logging.FileHandler(args.logfile) + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) + nexT_logger.addHandler(handler) + startNexT(args.cfg[0], args.active, withGui=withGui) + +def mainConsole(): + """ + entry point for console application + :return: + """ + main(withGui=False) + +def mainGui(): + """ + entry point for gui application + :return: + """ + main(withGui=True) + +if __name__ == "__main__": + mainGui() + logger.internal("python script end") diff --git a/nexxT/core/Application.py b/nexxT/core/Application.py new file mode 100644 index 0000000..daa076c --- /dev/null +++ b/nexxT/core/Application.py @@ -0,0 +1,97 @@ +# 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 defines the nexxT framework class Application +""" + +import logging +import re +from PySide2.QtCore import QCoreApplication, Qt +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +from nexxT.core.SubConfiguration import SubConfiguration +from nexxT.core.ActiveApplication import ActiveApplication +from nexxT.core.Exceptions import NexTRuntimeError, PropertyCollectionChildNotFound +from nexxT.core.Utils import MethodInvoker, assertMainThread + +logger = logging.getLogger(__name__) + +class Application(SubConfiguration): + """ + This class is an application. In addition to subconfigurations, it also controls the active application. + """ + + activeApplication = None + + def __init__(self, name, configuration): + super().__init__(name, configuration) + configuration.addApplication(self) + PropertyCollectionImpl("_guiState", self.getPropertyCollection()) + + def guiState(self, name): + """ + Return the gui state of the entity referenced by 'name' (eitehr a full qualified filter name or a service) + :param name: a string + :return: a PropertyCollectionImpl instance + """ + name = re.sub(r'[^a-zA-Z0-9_]', '_', name) + pc = self.getPropertyCollection() + gs = pc.getChildCollection("_guiState") + try: + cc = gs.getChildCollection(name) + except PropertyCollectionChildNotFound: + cc = PropertyCollectionImpl(name, gs) + return cc + + + @staticmethod + def unactivate(): + """ + if an active application exists, close it. + :return: + """ + logger.internal("entering unactivate") + if Application.activeApplication is not None: + logger.internal("need to stop existing application first") + Application.activeApplication.cleanup() + del Application.activeApplication + Application.activeApplication = None + logger.internal("leaving unactivate") + + def activate(self): + """ + puts this application to active + :return: None + """ + logger.internal("entering activate") + self.unactivate() + Application.activeApplication = ActiveApplication(self._graph) + QCoreApplication.instance().aboutToQuit.connect(Application.activeApplication.shutdown) + logger.internal("leaving activate") + + @staticmethod + def initialize(): + """ + Initialize the active application such that the filters are active. + :return: + """ + assertMainThread() + if Application.activeApplication is None: + raise NexTRuntimeError("No active application to initialize") + MethodInvoker(Application.activeApplication.init, Qt.DirectConnection) + MethodInvoker(Application.activeApplication.start, Qt.DirectConnection) + + @staticmethod + def deInitialize(): + """ + Deinitialize the active application such that the filters are in CONSTRUCTED state + :return: + """ + assertMainThread() + if Application.activeApplication is None: + raise NexTRuntimeError("No active application to initialize") + MethodInvoker(Application.activeApplication.stop, Qt.DirectConnection) + MethodInvoker(Application.activeApplication.deinit, Qt.DirectConnection) diff --git a/nexxT/core/BaseGraph.py b/nexxT/core/BaseGraph.py new file mode 100644 index 0000000..028972c --- /dev/null +++ b/nexxT/core/BaseGraph.py @@ -0,0 +1,347 @@ +# 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 defines the class BaseGraph +""" + +from collections import OrderedDict +from PySide2.QtCore import QObject, Signal, Slot +from nexxT.core.Utils import assertMainThread +from nexxT.core.Exceptions import (NodeExistsError, NodeNotFoundError, PortExistsError, PortNotFoundError, + ConnectionExistsError, ConnectionNotFound, NodeProtectedError) + +class BaseGraph(QObject): + """ + This class defines a graph where the nodes can have input and output ports and + these ports can be connected together. All operations are performed with QT + signals and slots. + """ + nodeAdded = Signal(str) + nodeRenamed = Signal(str, str) + nodeDeleted = Signal(str) + inPortAdded = Signal(str, str) + inPortRenamed = Signal(str, str, str) + inPortDeleted = Signal(str, str) + outPortAdded = Signal(str, str) + outPortRenamed = Signal(str, str, str) + outPortDeleted = Signal(str, str) + connectionAdded = Signal(str, str, str, str) + connectionDeleted = Signal(str, str, str, str) + + def __init__(self): + assertMainThread() + super().__init__() + self._protected = set() + self._nodes = OrderedDict() + self._connections = [] + + def _uniqueNodeName(self, nodeName): + assertMainThread() + if not nodeName in self._nodes: + return nodeName + t = 2 + while "%s%d" % (nodeName, t) in self._nodes: + t += 1 + return "%s%d" % (nodeName, t) + + def protect(self, name): + """ + Adds node to protected set, which prevents renaming and deletion + :param name: + :return: None + """ + if name not in self._nodes: + raise NodeNotFoundError(name) + self._protected.add(name) + + @Slot(str) + def addNode(self, name): + """ + Add a new node to the graph. + :param name: the name of the new node + :return: the name of the added node + """ + assertMainThread() + if name in self._nodes: + raise NodeExistsError(name) + self._nodes[name] = dict(inports=[], outports=[]) + self.nodeAdded.emit(name) + return name + + @Slot(str, str) + def renameNode(self, oldName, newName): + """ + Rename a node in the graph (connections will be adapted as well) + :param oldName: the original name of the node + :param newName: the new name of the node + :return: None + """ + assertMainThread() + if not oldName in self._nodes: + raise NodeNotFoundError(oldName) + if newName in self._nodes: + raise NodeExistsError(newName) + if oldName in self._protected: + raise NodeProtectedError(oldName) + of = self._nodes[oldName] + del self._nodes[oldName] + self._nodes[newName] = of + for i in range(len(self._connections)): + c = self._connections[i] + if c[0] == oldName: + c = (newName, c[1], c[2], c[3]) + if c[2] == oldName: + c = (c[0], c[1], newName, c[3]) + self._connections[i] = c + self.nodeRenamed.emit(oldName, newName) + + @Slot(str) + def deleteNode(self, name): + """ + Delete a node in the graph (connections are deleted as well) + :param name: the name of the node to be deleted + :return: None + """ + assertMainThread() + if not name in self._nodes: + raise NodeNotFoundError(name) + if name in self._protected: + raise NodeProtectedError(name) + for inport in self._nodes[name]["inports"][::-1]: + self.deleteInputPort(name, inport) + for outport in self._nodes[name]["outports"][::-1]: + self.deleteOutputPort(name, outport) + del self._nodes[name] + self.nodeDeleted.emit(name) + + @Slot(str, str, str, str) + def addConnection(self, nodeNameFrom, portNameFrom, nodeNameTo, portNameTo): + """ + Add a connection to the graph. + :param nodeNameFrom: the source node + :param portNameFrom: the source port + :param nodeNameTo: the target node + :param portNameTo: the target port + :return: None + """ + assertMainThread() + if not nodeNameFrom in self._nodes: + raise NodeNotFoundError(nodeNameFrom) + if not nodeNameTo in self._nodes: + raise NodeNotFoundError(nodeNameTo) + if not portNameFrom in self._nodes[nodeNameFrom]["outports"]: + raise PortNotFoundError(nodeNameFrom, portNameFrom, "Output") + if not portNameTo in self._nodes[nodeNameTo]["inports"]: + raise PortNotFoundError(nodeNameTo, portNameTo, "Input") + if (nodeNameFrom, portNameFrom, nodeNameTo, portNameTo) in self._connections: + raise ConnectionExistsError(nodeNameFrom, portNameFrom, nodeNameTo, portNameTo) + self._connections.append((nodeNameFrom, portNameFrom, nodeNameTo, portNameTo)) + self.connectionAdded.emit(nodeNameFrom, portNameFrom, nodeNameTo, portNameTo) + + @Slot(str, str, str, str) + def deleteConnection(self, nodeNameFrom, portNameFrom, nodeNameTo, portNameTo): + """ + Remove a connection from the graph + :param nodeNameFrom: the source node + :param portNameFrom: the source port + :param nodeNameTo: the target node + :param portNameTo: the target port + :return: None + """ + assertMainThread() + if (nodeNameFrom, portNameFrom, nodeNameTo, portNameTo) not in self._connections: + raise ConnectionNotFound(nodeNameFrom, portNameFrom, nodeNameTo, portNameTo) + self._connections.remove((nodeNameFrom, portNameFrom, nodeNameTo, portNameTo)) + self.connectionDeleted.emit(nodeNameFrom, portNameFrom, nodeNameTo, portNameTo) + + @Slot(str, str) + def addInputPort(self, node, portName): + """ + Add an input port to the node. + :param node: the name of the node + :param portName: the name of the new port + :return: None + """ + assertMainThread() + if not node in self._nodes: + raise NodeNotFoundError(node) + if portName in self._nodes[node]["inports"]: + raise PortExistsError(node, portName) + self._nodes[node]["inports"].append(portName) + self.inPortAdded.emit(node, portName) + + @Slot(str, str) + def deleteInputPort(self, node, portName): + """ + Remove an input port from a node (connections will be deleted as required) + :param node: the node name + :param portName: the port name to be deleted + :return: None + """ + assertMainThread() + if not node in self._nodes: + raise NodeNotFoundError(node) + if not portName in self._nodes[node]["inports"]: + raise PortNotFoundError(node, portName) + toDel = [] + for i in range(len(self._connections)): + fromNode, fromPort, toNode, toPort = self._connections[i] + if (toNode == node and toPort == portName): + toDel.append((fromNode, fromPort, toNode, toPort)) + for c in toDel: + self.deleteConnection(*c) + self._nodes[node]["inports"].remove(portName) + self.inPortDeleted.emit(node, portName) + + @Slot(str, str, str) + def renameInputPort(self, node, oldPortName, newPortName): + """ + Rename an input port of a node (connections will be renamed as needed) + :param node: the name of the node + :param oldPortName: the original port name + :param newPortName: the new port name + :return: None + """ + assertMainThread() + if not node in self._nodes: + raise NodeNotFoundError(node) + if not oldPortName in self._nodes[node]["inports"]: + if newPortName in self._nodes[node]["inports"]: + # already renamed. + return + raise PortNotFoundError(node, oldPortName) + if newPortName in self._nodes[node]["inports"]: + raise PortExistsError(node, newPortName) + idx = self._nodes[node]["inports"].index(oldPortName) + self._nodes[node]["inports"][idx] = newPortName + for i in range(len(self._connections)): + fromNode, fromPort, toNode, toPort = self._connections[i] + if (toNode == node and toPort == oldPortName): + toPort = newPortName + self._connections[i] = (fromNode, fromPort, toNode, toPort) + self.inPortRenamed.emit(node, oldPortName, newPortName) + + @Slot(str, str) + def addOutputPort(self, node, portName): + """ + Add an output port to a node + :param node: the node name + :param portName: the name of the new port + :return: None + """ + assertMainThread() + if not node in self._nodes: + raise NodeNotFoundError(node) + if portName in self._nodes[node]["outports"]: + raise PortExistsError(node, portName) + self._nodes[node]["outports"].append(portName) + self.outPortAdded.emit(node, portName) + + @Slot(str, str) + def deleteOutputPort(self, node, portName): + """ + Remove an output port from a node (connections will be deleted as needed) + :param node: the node name + :param portName: the port name to be deleted + :return: None + """ + assertMainThread() + if not node in self._nodes: + raise NodeNotFoundError(node) + if not portName in self._nodes[node]["outports"]: + raise PortNotFoundError(node, portName) + toDel = [] + for i in range(len(self._connections)): + fromNode, fromPort, toNode, toPort = self._connections[i] + if (fromNode == node and fromPort == portName): + toDel.append((fromNode, fromPort, toNode, toPort)) + for c in toDel: + self.deleteConnection(*c) + self._nodes[node]["outports"].remove(portName) + self.outPortDeleted.emit(node, portName) + + @Slot(str, str, str) + def renameOutputPort(self, node, oldPortName, newPortName): + """ + Rename an output port of a node (connections will be renamed as needed) + :param node: the node name + :param oldPortName: the original port name + :param newPortName: the new port name + :return: None + """ + assertMainThread() + if not node in self._nodes: + raise NodeNotFoundError(node) + if not oldPortName in self._nodes[node]["outports"]: + if newPortName in self._nodes[node]["outports"]: + # already renamed. + return + raise PortNotFoundError(node, oldPortName) + if newPortName in self._nodes[node]["outports"]: + raise PortExistsError(node, newPortName) + idx = self._nodes[node]["outports"].index(oldPortName) + self._nodes[node]["outports"][idx] = newPortName + for i in range(len(self._connections)): + fromNode, fromPort, toNode, toPort = self._connections[i] + if (fromNode == node and fromPort == oldPortName): + fromPort = newPortName + self._connections[i] = (fromNode, fromPort, toNode, toPort) + self.outPortRenamed.emit(node, oldPortName, newPortName) + + def allNodes(self): + """ + Return all node names. + :return: list of nodes + """ + assertMainThread() + return list(self._nodes.keys()) + + def allConnections(self): + """ + Return all connections + :return: list of 4 tuples of strings (nodeFrom, portFrom, nodeTo, portTo) + """ + assertMainThread() + return self._connections + + def allConnectionsToInputPort(self, toNode, toPort): + """ + Return all connections to the specified port. + :param toNode: name of node + :param toPort: name of port + :return: a list of 4 tuples of strings (nodeFrom, portFrom, nodeTo, portTo) + """ + return [(a, b, c, d) for a, b, c, d, in self._connections if (c == toNode and d == toPort)] + + def allConnectionsFromOutputPort(self, fromNode, fromPort): + """ + Return all connections from the specified port. + :param fromNode: name of node + :param fromPort: name of port + :return: a list of 4 tuples of strings (nodeFrom, portFrom, nodeTo, portTo) + """ + return [(a, b, c, d) for a, b, c, d, in self._connections if (a == fromNode and b == fromPort)] + + def allInputPorts(self, node): + """ + Return all input port names of the node. + :param node: node name + :return: list of port names + """ + if not node in self._nodes: + raise NodeNotFoundError(node) + return self._nodes[node]["inports"] + + def allOutputPorts(self, node): + """ + Return all output port names of the node. + :param node: node name + :return: list of port names + """ + if not node in self._nodes: + raise NodeNotFoundError(node) + return self._nodes[node]["outports"] diff --git a/nexxT/core/CompositeFilter.py b/nexxT/core/CompositeFilter.py new file mode 100644 index 0000000..6f10da9 --- /dev/null +++ b/nexxT/core/CompositeFilter.py @@ -0,0 +1,148 @@ +# 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 defines the CompositeFilter class +""" + +import logging +from nexxT.interface import Filter, OutputPort, InputPort +from nexxT.core.SubConfiguration import SubConfiguration +from nexxT.core.Exceptions import NexTRuntimeError, NexTInternalError + +logger = logging.getLogger(__name__) + +class CompositeFilter(SubConfiguration): + """ + This class handles a sub-configuration of a nexxT application. Sub configs are either applications or + SubGraphs (which behave like a filter). + """ + + @staticmethod + def getThreadSet(mockup): + """ + Returns all threads (as strings) used by the given filter mockup. Usually this is only one, but + for composite filters, this function performs a recursive lookup. + :param mockup: + :return: set of strings + """ + if (issubclass(mockup.getPluginClass(), CompositeFilter.CompositeInputNode) or + issubclass(mockup.getPluginClass(), CompositeFilter.CompositeOutputNode)): + return set() + if not issubclass(mockup.getPluginClass(), CompositeFilter.CompositeNode): + pc = mockup.propertyCollection().getChildCollection("_nexxT") + thread = pc.getProperty("thread") + return set([thread]) + g = mockup.getLibrary().getGraph() + res = set() + for n in g.allNodes(): + res = res.union(CompositeFilter.getThreadSet(g.getMockup(n))) + return res + + class CompositeInputNode(Filter): + """ + This filter acts as a dummy filter inside the composite subgraph; because it represents + the input to the subgraph, it uses dynamic output ports + """ + def __init__(self, env): + Filter.__init__(self, False, True, env) + + class CompositeOutputNode(Filter): + """ + This filter acts as a dummy filter inside the composite subgraph; because it represents + the output of the subgraph, it uses dynamic input ports + """ + def __init__(self, env): + Filter.__init__(self, True, False, env) + + class CompositeNode(Filter): + """ + This class is used to represent a composite subgraph in a filter graph. + """ + def __init__(self, env, envCInput, envCOutput, parent): + Filter.__init__(self, False, False, env) + self._parent = parent + for src in envCInput.getDynamicOutputPorts(): + dest = InputPort(False, src.name(), env) + self.addStaticPort(dest) + for src in envCOutput.getDynamicInputPorts(): + dest = OutputPort(False, src.name(), env) + self.addStaticPort(dest) + + def getGraph(self): + """ + Returns the filter graph implementing this composite node (child filter graph) + :return: a FilterGraph instance + """ + return self._parent.getGraph() + + def getCompositeName(self): + """ + Returns the type name of this composite filter (this is the same for all instances of a composite filter) + :return: a string + """ + return self._parent.getName() + + def __init__(self, name, configuration): + super().__init__(name, configuration) + self._configuration = configuration + _compositeInputNode = self._graph.addNode(CompositeFilter, "CompositeInputNode", "CompositeInput") + _compositeOutputNode = self._graph.addNode(CompositeFilter, "CompositeOutputNode", "CompositeOutput") + if _compositeInputNode != "CompositeInput" or _compositeOutputNode != "CompositeOutput": + raise NexTInternalError("unexpected node names.") + # prevent renaming and deletion of these special nodes + self._graph.protect("CompositeInput") + self._graph.protect("CompositeOutput") + configuration.addComposite(self) + + def compositeNode(self, env): + """ + Factory function for creating a dummy filter instance (this one will never get active). + :param env: the FilterEnvironment instance + :return: a Filter instance + """ + mockup = env.getMockup() + compIn = self._graph.getMockup("CompositeInput") + compOut = self._graph.getMockup("CompositeOutput") + res = CompositeFilter.CompositeNode(env, compIn, compOut, self) + + def renameCompositeInputPort(node, oldPortName, newPortName): + graph = mockup.getGraph() + try: + node = graph.nodeName(mockup) + except NexTRuntimeError: + # node has been already removed from graph + logger.internal("Node '%s' already has been removed.", node, exc_info=True) + return + graph.renameInputPort(node, oldPortName, newPortName) + + def renameCompositeOutputPort(node, oldPortName, newPortName): + graph = mockup.getGraph() + try: + node = graph.nodeName(mockup) + except NexTRuntimeError: + # node has been already removed from graph + logger.internal("Node '%s' already has been removed.", node, exc_info=True) + return + graph.renameOutputPort(node, oldPortName, newPortName) + + self._graph.dynOutputPortRenamed.connect(renameCompositeInputPort) + self._graph.dynInputPortRenamed.connect(renameCompositeOutputPort) + if mockup is not None: + self._graph.dynOutputPortAdded.connect(mockup.createFilterAndUpdate) + self._graph.dynInputPortAdded.connect(mockup.createFilterAndUpdate) + self._graph.dynOutputPortDeleted.connect(mockup.createFilterAndUpdate) + self._graph.dynInputPortDeleted.connect(mockup.createFilterAndUpdate) + + return res + + def checkRecursion(self): + """ + Check for composite recursions and raise a CompositeRecursion exception if necessary. Called from FilterGraph + after adding a composite filter. + :return: + """ + self._configuration.checkRecursion() diff --git a/nexxT/core/ConfigFileSchema.json b/nexxT/core/ConfigFileSchema.json new file mode 100644 index 0000000..0d9d9a7 --- /dev/null +++ b/nexxT/core/ConfigFileSchema.json @@ -0,0 +1,118 @@ +{ + "$schema": "http://json-schema.org/schema#", + "definitions": { + "identifier": { + "description": "Used for matching identifier names. Usual c identifiers including minus sign", + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$" + }, + "portlist": { + "description": "Used for specifying filter ports.", + "type": "array", + "items": { + "$ref": "#/definitions/identifier" + } + }, + "connection": { + "description": "Used for specifying a connection.", + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_-]*[.][A-Za-z_][A-Za-z0-9_-]*\\s*[-][>]\\s*[A-Za-z_][A-Za-z0-9_-]*[.][A-Za-z_][A-Za-z0-9_-]*$" + }, + "propertySection": { + "type": "object", + "propertyNames": { "$ref": "#/definitions/identifier" }, + "patternProperties": { + "^.*$": {"type": ["string", "number", "boolean"]} + } + }, + "sub_graph": { + "description": "sub-graph definition as used by applications and composite filters.", + "type": "object", + "additionalProperties": false, + "required": ["name", "nodes", "connections"], + "properties": { + "name": { + "$ref": "#/definitions/identifier" + }, + "_guiState": { + "propertyNames": { "$ref": "#/definitions/identifier" }, + "properties" : { + "$ref": "#/definitions/propertySection" + }, + "default": {} + }, + "nodes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "library", "factoryFunction"], + "properties": { + "name": { + "$ref": "#/definitions/identifier" + }, + "library": { + "type": "string" + }, + "factoryFunction": { + "type": "string" + }, + "thread": { + "$ref": "#/definitions/identifier", + "default": "main" + }, + "dynamicInputPorts": { + "$ref": "#/definitions/portlist", + "default": [] + }, + "dynamicOutputPorts": { + "$ref": "#/definitions/portlist", + "default": [] + }, + "staticInputPorts": { + "$ref": "#/definitions/portlist", + "default": [] + }, + "staticOutputPorts": { + "$ref": "#/definitions/portlist", + "default": [] + }, + "properties": { + "$ref": "#/definitions/propertySection", + "default": {} + } + } + } + }, + "connections": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/connection" + } + } + } + } + }, + "type": "object", + "required": ["applications"], + "properties": { + "composite_filters": { + "type": "array", + "default": [], + "items": { + "$ref": "#/definitions/sub_graph" + } + }, + "applications": { + "type": "array", + "items": { + "$ref": "#/definitions/sub_graph" + } + }, + "_guiState": { + "$ref": "#/definitions/propertySection", + "default": {} + } + } +} diff --git a/nexxT/core/ConfigFiles.py b/nexxT/core/ConfigFiles.py new file mode 100644 index 0000000..db2a1f9 --- /dev/null +++ b/nexxT/core/ConfigFiles.py @@ -0,0 +1,220 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +""" +This modules defines classes for nexxT config file handling. +""" + +import json +import logging +from pathlib import Path +from jsonschema import Draft7Validator, validators + +logger = logging.getLogger(__name__) + +class ConfigFileLoader: + """ + Class for loading configurations from disk using a json format along with an appropriate schema. + """ + _validator = None + _validatorGuiState = None + + @staticmethod + def load(config, file): + """ + Load configuration from file. + :param file: string or Path instance + :param config: Configuration instance to be populated + :return: dictionary with configuration contents (default values from schema are already applied) + """ + validator, validatorGuiState = ConfigFileLoader._getValidator() + if not isinstance(file, Path): + file = Path(file) + with file.open("r", encoding='utf-8') as fp: + cfg = json.load(fp) + validator.validate(cfg) + guistateFile = file.parent / (file.name + ".guistate") + if guistateFile.exists(): + try: + with guistateFile.open("r", encoding="utf-8") as fp: + guistate = json.load(fp) + validatorGuiState.validate(guistate) + except Exception as e: # pylint: disable=broad-except + # catching a broad exception is exactly wanted here. + logger.warning("ignoring error while loading %s: %s", guistateFile, e) + guistate = None + else: + logger.info("no gui state file for config, using default values from original file.") + guistate = None + cfg["CFGFILE"] = str(file.absolute()) + if not guistate is None: + cfg = ConfigFileLoader._merge(cfg, guistate) + config.load(cfg) + return config + + @staticmethod + def save(config, file=None): + """ + Save the configuration to the given file (or overwrite the existing file). + :param config: A Configuration instance + :param file: a file given as string or Path + :return: None + """ + # TODO: saving to new file will eventually destroy relative paths. + cfg = config.save() + if file is None: + file = cfg["CFGFILE"] + del cfg["CFGFILE"] + + validator, validatorGuiState = ConfigFileLoader._getValidator() + validator.validate(cfg) + if not isinstance(file, Path): + file = Path(file) + oldCfg = None + if file.exists(): + try: + with file.open("r", encoding="utf-8") as fp: + oldCfg = json.load(fp) + validator.validate(oldCfg) + except Exception: # pylint: disable=broad-except + # catching a broad exception is exactly wanted here + oldCfg = None + if oldCfg is not None: + cfg, guistate = ConfigFileLoader._split(cfg, oldCfg) + validatorGuiState.validate(guistate) + else: + guistate = None + with file.open("w", encoding='utf-8') as fp: + json.dump(cfg, fp, indent=2, ensure_ascii=False) + if guistate is not None: + guistateFile = file.parent / (file.name + ".guistate") + with guistateFile.open("w", encoding="utf-8") as fp: + json.dump(guistate, fp, indent=2, ensure_ascii=False) + + @staticmethod + def saveGuiState(config): + """ + save the gui state related to config (doesn't touch the original config file) + :param config: a Configuration instance + :return: None + """ + validator, validatorGuiState = ConfigFileLoader._getValidator() + cfg = config.save() + file = cfg["CFGFILE"] + if file is None: + return + if not isinstance(file, Path): + file = Path(file) + oldCfg = None + # first, read original cfg file contents + if file.exists(): + try: + with file.open("r", encoding="utf-8") as fp: + oldCfg = json.load(fp) + validator.validate(oldCfg) + except Exception: # pylint: disable=broad-except + # catching a general exception is exactly wanted here + oldCfg = None + if oldCfg is None: + return + _, guistate = ConfigFileLoader._split(cfg, oldCfg) + validatorGuiState.validate(guistate) + guistateFile = file.parent / (file.name + ".guistate") + with guistateFile.open("w", encoding="utf-8") as fp: + json.dump(guistate, fp, indent=2, ensure_ascii=False) + + @staticmethod + def _extendWithDefault(validatorClass): + """ + see https://python-jsonschema.readthedocs.io/en/stable/faq/ + :param validator_class: + :return: + """ + validate_properties = validatorClass.VALIDATORS["properties"] + def setDefaults(validator, properties, instance, schema): + for jsonProperty, subschema in properties.items(): + if "default" in subschema: + instance.setdefault(jsonProperty, subschema["default"]) + for error in validate_properties(validator, properties, instance, schema): + yield error + return validators.extend( + validatorClass, {"properties": setDefaults}, + ) + + @staticmethod + def _getValidator(): + if ConfigFileLoader._validator is None: + with (Path(__file__).parent / "ConfigFileSchema.json").open("rb") as fp: + schema = json.load(fp) + ConfigFileLoader._validator = ConfigFileLoader._extendWithDefault(Draft7Validator)(schema) + if ConfigFileLoader._validatorGuiState is None: + with (Path(__file__).parent / "GuiStateSchema.json").open("rb") as fp: + schema = json.load(fp) + ConfigFileLoader._validatorGuiState = ConfigFileLoader._extendWithDefault(Draft7Validator)(schema) + return ConfigFileLoader._validator, ConfigFileLoader._validatorGuiState + + @staticmethod + def _merge(cfg, guistate): + + def mergeGuiStateSection(gsCfg, gsGuiState): + res = gsCfg.copy() + # merge two gui state sections + for p in gsGuiState: + res[p] = gsGuiState[p] + return res + + def findNamedItemInList(name, listOfItems): + for item in listOfItems: + if item["name"] == name: + return item + return None + + cfg["_guiState"] = mergeGuiStateSection(cfg["_guiState"], guistate["_guiState"]) + for subgraphType in ["applications"]: + + for cf in cfg[subgraphType]: + cfGS = findNamedItemInList(cf["name"], guistate[subgraphType]) + if cfGS is None: + continue + cf["_guiState"] = mergeGuiStateSection(cf["_guiState"], cfGS["_guiState"]) + + return cfg + + @staticmethod + def _split(newCfg, oldCfg): + """ + returns cfg, guistate such that the gui state in cfg is preserved + :param newCfg: current config state as returned form Configuration.save(...) + :param oldCfg: values read from current file + :return: cfg, guistate + """ + + def findNamedItemInList(name, listOfItems): + for item in listOfItems: + if item["name"] == name: + return item + return None + + def splitCommonGuiStateSections(newGuiState, oldGuiState): + res = newGuiState.copy() + for p in oldGuiState: + if p in newGuiState and oldGuiState[p] != newGuiState[p]: + newGuiState[p] = oldGuiState[p] + return res + + guistate = {} + cfg = newCfg.copy() + guistate["_guiState"] = splitCommonGuiStateSections(cfg["_guiState"], oldCfg["_guiState"]) + cfg["_guiState"] = oldCfg["_guiState"] + + for subgraphType in ["applications"]: + guistate[subgraphType] = [] + for cf in cfg[subgraphType]: + guistate[subgraphType].append(dict(name=cf["name"])) + cfOld = findNamedItemInList(cf["name"], oldCfg[subgraphType]) + guistate[subgraphType][-1]["_guiState"] = \ + splitCommonGuiStateSections(cf["_guiState"], cfOld["_guiState"] if cfOld is not None else {}) + return cfg, guistate diff --git a/nexxT/core/Configuration.py b/nexxT/core/Configuration.py new file mode 100644 index 0000000..596e169 --- /dev/null +++ b/nexxT/core/Configuration.py @@ -0,0 +1,282 @@ +# 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 the nexxT class Configuration +""" + +import logging +import shiboken2 +from PySide2.QtCore import QObject, Slot, Signal +from nexxT.core.Application import Application +from nexxT.core.CompositeFilter import CompositeFilter +from nexxT.core.Exceptions import (NexTRuntimeError, CompositeRecursion, NodeNotFoundError, NexTInternalError, + PropertyCollectionPropertyNotFound) +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl, ReadonlyValidator +from nexxT.core.PluginManager import PluginManager +from nexxT.core.ConfigFiles import ConfigFileLoader + +logger = logging.getLogger(__name__) + +class Configuration(QObject): + """ + A configuration is a collection of subgraphs, i.e., applications and composite filters. + """ + + subConfigAdded = Signal(object) + subConfigRemoved = Signal(str, int) + configNameChanged = Signal(object) + appActivated = Signal(str, object) + configLoaded = Signal() + configAboutToSave = Signal() + + CONFIG_TYPE_COMPOSITE = 0 + CONFIG_TYPE_APPLICATION = 1 + + @staticmethod + def configType(subConfig): + """ + return the config type (either CONFIG_TYPE_COMPOSITE or CONFIG_TYPE_APPLICATION) of a SubConfiguration + instance + :param subConfig: a SubConfiguration instance + :return: + """ + if isinstance(subConfig, Application): + return Configuration.CONFIG_TYPE_APPLICATION + if isinstance(subConfig, CompositeFilter): + return Configuration.CONFIG_TYPE_COMPOSITE + raise NexTRuntimeError("Unexpected instance type") + + def __init__(self): + super().__init__() + self._compositeFilters = [] + self._applications = [] + self._propertyCollection = PropertyCollectionImpl("root", None) + self._guiState = PropertyCollectionImpl("_guiState", self._propertyCollection) + + @Slot() + def close(self): + """ + Closing the configuration instance and free allocated resources. + :return: + """ + logger.internal("entering Configuration.close") + ConfigFileLoader.saveGuiState(self) + Application.unactivate() + for sc in self._compositeFilters + self._applications: + sc.cleanup() + for c in self._compositeFilters: + self.subConfigRemoved.emit(c.getName(), self.CONFIG_TYPE_COMPOSITE) + self._compositeFilters = [] + for a in self._applications: + self.subConfigRemoved.emit(a.getName(), self.CONFIG_TYPE_APPLICATION) + self._applications = [] + self._propertyCollection.deleteLater() + self._propertyCollection = PropertyCollectionImpl("root", None) + self.configNameChanged.emit(None) + self.appActivated.emit("", None) + PluginManager.singleton().unloadAll() + logger.internal("leaving Configuration.close") + + @Slot(object) + def load(self, cfg): + """ + Load the configuration from a dictionary. + :param cfg: dictionary loaded from a json file + :return: None + """ + self.close() + try: + self._propertyCollection.defineProperty("CFGFILE", cfg["CFGFILE"], + "The absolute path to the configuration file.", + validator=ReadonlyValidator(cfg["CFGFILE"])) + shiboken2.delete(self._guiState) + self._guiState = PropertyCollectionImpl("_guiState", self._propertyCollection, cfg["_guiState"]) + recursiveset = set() + def compositeLookup(name): + nonlocal recursiveset + if name in recursiveset: + raise CompositeRecursion(name) + try: + return self.compositeFilterByName(name) + except NodeNotFoundError: + recursiveset.add(name) + try: + for cfg_cf in cfg["composite_filters"]: + if cfg_cf["name"] == name: + cf = CompositeFilter(name, self) + cf.load(cfg_cf, compositeLookup) + return cf + raise NodeNotFoundError("name") + finally: + recursiveset.remove(name) + + for cfg_cf in cfg["composite_filters"]: + compositeLookup(cfg_cf["name"]) + for cfg_app in cfg["applications"]: + app = Application(cfg_app["name"], self) + app.load(cfg_app, compositeLookup) + self.configNameChanged.emit(cfg["CFGFILE"]) + self.configLoaded.emit() + except RuntimeError as e: + self.close() + raise e + + def save(self): + """ + return a dictionary suitable for saving to json (inverse of load) + :return: dictionary + """ + self.configAboutToSave.emit() + cfg = {} + try: + cfg["CFGFILE"] = self._propertyCollection.getProperty("CFGFILE") + except PropertyCollectionPropertyNotFound: + cfg["CFGFILE"] = None + cfg["_guiState"] = self._guiState.saveDict() + cfg["composite_filters"] = [cf.save() for cf in self._compositeFilters] + cfg["applications"] = [app.save() for app in self._applications] + self.configNameChanged.emit(cfg["CFGFILE"]) + return cfg + + def propertyCollection(self): + """ + Get the (root) property collection. + :return: PropertyCollectionImpl instance + """ + return self._propertyCollection + + def guiState(self): + """ + Return the per-config gui state. + :return: a PropertyCollection instance + """ + return self._guiState + + def compositeFilterByName(self, name): + """ + Search for the composite filter with the given name + :param name: a string + :return: a CompositeFilter instance + """ + match = [cf for cf in self._compositeFilters if cf.getName() == name] + if len(match) == 1: + return match[0] + if len(match) == 0: + raise NodeNotFoundError(name) + raise NexTInternalError("non unique name %s" % name) + + def applicationByName(self, name): + """ + Search for the application with the given name + :param name: a string + :return: an Application instance + """ + match = [app for app in self._applications if app.getName() == name] + if len(match) == 1: + return match[0] + if len(match) == 0: + raise NodeNotFoundError(name) + raise NexTInternalError("non unique name %s" % name) + + @Slot(str) + def activate(self, appname): + """ + Activate the application with the given name. + :param appname: a string + :return: None + """ + for app in self._applications: + if app.getName() == appname: + app.activate() + self.appActivated.emit(appname, Application.activeApplication) + return + raise NexTRuntimeError("Application '%s' not found." % appname) + + @Slot(str, str) + def renameComposite(self, oldName, newName): + """ + Rename a composite subgraph + :param oldName: the old name + :param newName: the new name + :return: + """ + if oldName != newName: + self._checkUniqueName(self._compositeFilters, newName) + self.compositeFilterByName(oldName).setName(newName) + + @Slot(str, str) + def renameApp(self, oldName, newName): + """ + Rename an application subgraph + :param oldName: the old name + :param newName: the new name + :return: + """ + if oldName != newName: + self._checkUniqueName(self._applications, newName) + self.applicationByName(oldName).setName(newName) + + def addComposite(self, compFilter): + """ + Add a composite filter instance to this configuration. + :param compFilter: a CompositeFilter instance + :return: None + """ + self._checkUniqueName(self._compositeFilters, compFilter.getName()) + self._compositeFilters.append(compFilter) + self.subConfigAdded.emit(compFilter) + + def addApplication(self, app): + """ + Add an application instance to this configuration + :param app: an Application instance + :return: None + """ + self._checkUniqueName(self._applications, app.getName()) + self._applications.append(app) + self.subConfigAdded.emit(app) + + def getApplicationNames(self): + """ + Return list of application names + :return: list of strings + """ + return [app.getName() for app in self._applications] + + @staticmethod + def _checkUniqueName(collection, name): + for i in collection: + if i.getName() == name: + raise NexTRuntimeError("Name '%s' is not unique." % name) + + def checkRecursion(self): + """ + Checks for recursions in the composite filters, raises a CompositeRecursion exception if necessary. + :return: None + """ + for cf in self._compositeFilters: + self._checkRecursion(cf.getGraph(), set([cf.getName()])) + + @staticmethod + def _checkRecursion(graph, activeNames): + if activeNames is None: + activeNames = set() + nodes = graph.allNodes() + allComposites = [] + for n in nodes: + # check whether this node is itself a composite node + mockup = graph.getMockup(n) + if issubclass(mockup.getPluginClass(), CompositeFilter.CompositeNode): + allComposites.append(mockup) + cf = mockup.getLibrary() + name = cf.getName() + #print("active:", activeNames, "; curr:", name) + if name in activeNames: + raise CompositeRecursion(name) + activeNames.add(name) + Configuration._checkRecursion(cf.getGraph(), activeNames) + activeNames.remove(name) diff --git a/nexxT/core/Exceptions.py b/nexxT/core/Exceptions.py new file mode 100644 index 0000000..6a7dbff --- /dev/null +++ b/nexxT/core/Exceptions.py @@ -0,0 +1,161 @@ +# 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 defines exceptions used in the nexxT framework. +""" + +from nexxT.interface.Ports import InputPortInterface, OutputPortInterface +from nexxT.interface.Filters import FilterState + +def _factoryToString(factory): + if isinstance(factory, str): + return factory + return "Input" if factory is InputPortInterface else ("Output" if factory is OutputPortInterface else "") + +class NexTRuntimeError(RuntimeError): + """ + Generic runtime error of nexxT framework. + """ + +class NexTInternalError(NexTRuntimeError): + """ + Raised when we found a bug in nexxT. + """ + +class NodeExistsError(NexTRuntimeError): + """ + raised node is added or renamed which already exists + """ + def __init__(self, nodeName): + super().__init__("Node %s already exists." % (nodeName)) + +class NodeNotFoundError(NexTRuntimeError): + """ + raised node is added or renamed which already exists + """ + def __init__(self, nodeName): + super().__init__("Node %s not found." % (nodeName)) + +class NodeProtectedError(NexTRuntimeError): + """ + raised if protected node is to be deleted or renamed + """ + def __init__(self, nodeName): + super().__init__("Node %s is protected and cannot be deleted or renamed." % (nodeName)) + +class PortExistsError(NexTRuntimeError): + """ + raised when a port is added or renamed which already exists + """ + def __init__(self, nodeName, portName, factory=None): + super().__init__("%sPort %s/%s already exists." % (_factoryToString(factory), nodeName, portName)) + +class PortNotFoundError(NexTRuntimeError): + """ + raised when a referenced port is not found. + """ + def __init__(self, nodeName, portName, factory=None): + super().__init__("%sPort %s/%s not found." % (_factoryToString(factory), nodeName, portName)) + +class DynamicPortUnsupported(NexTRuntimeError): + """ + raised for unsupported dynamic port operations + """ + def __init__(self, portName, factory=None): + super().__init__("No dynamic %sPort support; port name: %s" % (_factoryToString(factory), portName)) + +class ConnectionExistsError(NexTRuntimeError): + """ + raised when a connection is added twice + """ + def __init__(self, nodeFrom, portFrom, nodeTo, portTo): + super().__init__("Connection from %s/%s to %s/%s already exists." % (nodeFrom, portFrom, nodeTo, portTo)) + +class ConnectionNotFound(NexTRuntimeError): + """ + raised when a connection is added twice + """ + def __init__(self, nodeFrom, portFrom, nodeTo, portTo): + super().__init__("Connection from %s/%s to %s/%s not found." % (nodeFrom, portFrom, nodeTo, portTo)) + +class UnknownPluginType(NexTRuntimeError): + """ + raised when a plugin has unknown extension + """ + +class PluginException(NexTRuntimeError): + """ + raised when a plugin raises an unexpected unhandled exception + """ + +class UnexpectedFilterState(NexTRuntimeError): + """ + raised when operations are performed in unexpected filter states + """ + def __init__(self, state, operation): + super().__init__("Operation '%s' cannot be performed in state %s" % (operation, FilterState.state2str(state))) + +class FilterStateMachineError(UnexpectedFilterState): + """ + raised when a state transition is invalid. + """ + def __init__(self, oldState, newState): + super().__init__(oldState, "Transition to " + FilterState.state2str(newState)) + +class PropertyCollectionChildExists(NexTRuntimeError): + """ + raised when trying to create an already existing property collection + """ + def __init__(self, name): + super().__init__("PropertyCollection already has a child named %s" % name) + +class PropertyCollectionChildNotFound(NexTRuntimeError): + """ + raised when trying to access a non-existing property collection + """ + def __init__(self, name): + super().__init__("PropertyCollection has no child named %s" % name) + +class PropertyCollectionPropertyNotFound(NexTRuntimeError): + """ + raised when trying to set the value of an unknown property + """ + def __init__(self, name): + super().__init__("PropertyCollection has no property named %s" % name) + +class PropertyCollectionUnknownType(NexTRuntimeError): + """ + raised when the type given to getProperty or setProperty is unknown to the property system + """ + def __init__(self, value): + super().__init__("PropertyCollection has been provided with an invalid typed value %s" % (repr(value))) + +class PropertyParsingError(NexTRuntimeError): + """ + raised when a property cannot be parsed from a string + """ + +class PropertyInconsistentDefinition(NexTRuntimeError): + """ + raised when the same property is defined in different ways + """ + def __init__(self, name): + super().__init__("Inconsistent definitions for property named %s" % name) + +class InvalidIdentifierException(NexTRuntimeError): + """ + raised when a provided name doesn't confirm to identifier specification + """ + def __init__(self, name): + super().__init__("Invalid identifier '%s'" % name) + +class CompositeRecursion(NexTRuntimeError): + """ + raised when a composite filter is dependent on itself + """ + def __init__(self, name): + super().__init__("Composite filter '%s' depends on itself." % name) diff --git a/nexxT/core/FilterEnvironment.py b/nexxT/core/FilterEnvironment.py new file mode 100644 index 0000000..5551cdd --- /dev/null +++ b/nexxT/core/FilterEnvironment.py @@ -0,0 +1,437 @@ +# 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 defines the class FilterEnvironment. +""" + +import copy +import logging +from PySide2.QtCore import QObject, Signal, QMutex, QMutexLocker, QThread +from nexxT.interface import FilterState, InputPortInterface, OutputPortInterface +from nexxT import useCImpl +from nexxT.core.PluginManager import PluginManager +from nexxT.core.Exceptions import (PortExistsError, PortNotFoundError, FilterStateMachineError, NexTRuntimeError, + NexTInternalError, DynamicPortUnsupported, UnexpectedFilterState) + +logger = logging.getLogger(__name__) + +# TODO: make new file for BaseFilterEnvironment +if useCImpl: + import cnexxT + # this is not really a constant, but a class name + BaseFilterEnvironment = cnexxT.nexxT.BaseFilterEnvironment # pylint: disable=invalid-name +else: + class BaseFilterEnvironment(QObject): + """ + This class is a base class for the FilterEnvironment class. It contains all methods + necessary to port to C++. + """ + def _assertMyThread(self): + if QThread.currentThread() is not self._thread: + raise NexTInternalError("Function is called from unexpected thread") + + def __init__(self, propertyCollection): + super().__init__() + self._plugin = None + self._thread = QThread.currentThread() + self._propertyCollection = propertyCollection + self._dynamicInputPortsSupported = False + self._dynamicOutputPortsSupported = False + + def setPlugin(self, plugin): + """ + Sets the plugin managed by this BaseFilterEnvironment instance. + :param plugin: a Filter instance + :return: None + """ + if self._plugin is not None: + self._plugin.deleteLater() # pylint: disable=no-member + self._plugin = None + self._plugin = plugin + + def resetPlugin(self): + """ + Resets the plugin managed by this BaseFilterEnvironment instance. + :return: None + """ + self.setPlugin(None) + + def getPlugin(self): + """ + Get the corresponding Filter instance. + :return: a Filter instance + """ + return self._plugin + + def propertyCollection(self): + """ + Get the property collection of this filter. + :return: PropertyCollection instance + """ + self._assertMyThread() + return self._propertyCollection + + def guiState(self): + """ + Return the gui state associated with this filter. + :return: PropertyCollection instance + """ + raise NotImplementedError() + + def setDynamicPortsSupported(self, dynInPortsSupported, dynOutPortsSupported): + """ + Changes dynamic port supported settings for input and output ports. Will raise an exception if dynamic ports + are not supported but dynamic ports are already existing. + :param dynInPortsSupported: boolean whether dynamic input ports are supported + :param dynOutPortsSupported: boolean whether dynamic output ports are supported + :return: None + """ + self._assertMyThread() + self._dynamicInputPortsSupported = dynInPortsSupported + self._dynamicOutputPortsSupported = dynOutPortsSupported + if not dynInPortsSupported: + p = self.getDynamicInputPorts() + assert len(p) == 0 # TOOD: we would need to delete the existing dynamic ports + if not dynOutPortsSupported: + p = self.getDynamicOutputPorts() + assert len(p) == 0 + + def getDynamicPortsSupported(self): + """ + Return the dynamic port supported flags. + :return: 2-tuple of booleans + """ + self._assertMyThread() + return self._dynamicInputPortsSupported, self._dynamicOutputPortsSupported + + def portDataChanged(self, inputPort): + """ + Calls the onPortDataChanged of the filter and catches exceptions as needed. + :param inputPort: The InputPort instance where the data arrived. + :return: None + """ + self._assertMyThread() + if self._state != FilterState.ACTIVE: + if self._state != FilterState.INITIALIZED: + raise UnexpectedFilterState(self._state, "portDataChanged") + logger.info("DataSample discarded because application has been stopped already.") + return + try: + self._plugin.onPortDataChanged(inputPort) + except Exception: # pylint: disable=broad-except + # catching a general exception is exactly what is wanted here + logger.exception("Uncaught exception") + +class FilterEnvironment(BaseFilterEnvironment): # pylint: disable=too-many-public-methods + """ + This class implements the environment of a filter. It implements the filter state machine, manages dynamic + filter ports (these ports are defined by the user, not by the filter developer). Currently it also manages + plugins, but this functionality will be moved to a seperate class in the future. It can serve as a context + manager for automatic de-initializing of the embedded filter instance. + """ + portInformationUpdated = Signal(object, object) + + def __init__(self, library, factoryFunction, propertyCollection, mockup=None): + BaseFilterEnvironment.__init__(self, propertyCollection) + # ports are accessed by multiple threads (from FilterMockup.createFilter) + self._portMutex = QMutex(QMutex.Recursive) + self._ports = [] + self._mockup = mockup + self._state = FilterState.CONSTRUCTING + if library is not None: + plugin = PluginManager.singleton().create(library, factoryFunction, self) + if plugin is not None: + self.setPlugin(plugin) + self._state = FilterState.CONSTRUCTED + + if useCImpl: + def _assertMyThread(self): + self.assertMyThread() + + def getMockup(self): + """ + Returns the FilterMockup instance this environment belongs to. + :return: FilterMockup instance (or None) + """ + return self._mockup + + def close(self): + """ + Deinitialize filter if necessary. + :return: None + """ + if not (self.getPlugin() is None or (useCImpl and self.getPlugin().data() is None)): + if self._state == FilterState.ACTIVE: + self.stop() + if self._state == FilterState.INITIALIZED: + self.deinit() + if not self._state in [FilterState.CONSTRUCTED, FilterState.DESTRUCTING]: + raise FilterStateMachineError(self._state, FilterState.DESTRUCTING) + self._state = FilterState.DESTRUCTING + self.resetPlugin() + + def __enter__(self): + return self + + def __exit__(self, *args): #exctype, value, traceback + self.close() + + def guiState(self): + """ + Gets the gui state of this filter. Note that the gui state, in contrast to the properties, is dependent on the + concrete instance of a filter. + :return: + """ + from nexxT.core.Thread import NexTThread + from nexxT.core.Application import Application + if isinstance(self.parent(), NexTThread) and Application.activeApplication is not None: + try: + path = self.parent().getName(self) + app = Application.activeApplication.getApplication() + return app.guiState("filters/" + path) + except NexTRuntimeError: + logger.warning("Cannot find guiState.") + return None + + def addPort(self, port): + """ + Register a port of this filter. + :param port: instance of InputPort ot OutputPort + :return: None + """ + if useCImpl: + # make sure to make copies of the shared pointers + port = copy.copy(port) + with QMutexLocker(self._portMutex): + assert self.state() in [FilterState.CONSTRUCTING, FilterState.CONSTRUCTED] + dynInSupported, dynOutSupported = self.getDynamicPortsSupported() + if port.dynamic() and ((port.isInput() and not dynInSupported) or + (port.isOutput() and not dynOutSupported)): + raise DynamicPortUnsupported(port.name(), type(port)) + for p in self._ports: + if p.isInput() and port.isInput() and p.name() == port.name(): + raise PortExistsError("", port.name(), InputPortInterface) + if p.isOutput() and port.isOutput() and p.name() == port.name(): + raise PortExistsError("", port.name(), OutputPortInterface) + self._ports.append(port) + + def removePort(self, port): + """ + Unregister a port of this filter + :param port: instacne of InputPort or OutputPort + :return: None + """ + with QMutexLocker(self._portMutex): + self._ports.remove(port) + + def _getInputPorts(self, dynamic): + with QMutexLocker(self._portMutex): + return [p for p in self._ports if p.isInput() + and (p.dynamic() == dynamic or dynamic is None)] + + def emitPortInformationUpdated(self, inPorts, outPorts): + """ + Emits the signal portInformationUpdated (this is necessary due to some shiboken fails). + :param inPorts: list of input ports + :param outPorts: list of output ports + :return: None + """ + self.portInformationUpdated.emit(inPorts, outPorts) + + def getDynamicInputPorts(self): + """ + Get dynamic input ports + :return: list of InputPort instances + """ + return self._getInputPorts(True) + + def getStaticInputPorts(self): + """ + Get static input ports + :return: list of InputPort instances + """ + return self._getInputPorts(False) + + def getAllInputPorts(self): + """ + Get all input ports + :return: list of InputPort instances + """ + return self._getInputPorts(None) + + def _getOutputPorts(self, dynamic): + with QMutexLocker(self._portMutex): + return [p for p in self._ports if p.isOutput() + and (p.dynamic() == dynamic or dynamic is None)] + + def getDynamicOutputPorts(self): + """ + Get dynamic output ports + :return: list of OutputPort instances + """ + return self._getOutputPorts(True) + + def getStaticOutputPorts(self): + """ + Get static output ports + :return: list of OutputPort instances + """ + return self._getOutputPorts(False) + + def getAllOutputPorts(self): + """ + Get all output ports + :return: list of OutputPort instances + """ + return self._getOutputPorts(None) + + def getPort(self, portName, portType): + """ + Get port by name + :param portName: the name of the port + :param portType: either InputPort or OutputPort + :return: port instance + """ + query = {InputPortInterface:"isInput", OutputPortInterface:"isOutput"} + with QMutexLocker(self._portMutex): + f = [p for p in self._ports if getattr(p, query[portType])() and p.name() == portName] + if len(f) != 1: + raise PortNotFoundError("", portName, portType) + return f[0] + + def getOutputPort(self, portName): + """ + Get output port by name + :param portName: the name of the port + :return: port instance + """ + return self.getPort(portName, OutputPortInterface) + + def getInputPort(self, portName): + """ + Get input port by name + :param portName: the name of the port + :return: port instance + """ + return self.getPort(portName, InputPortInterface) + + def updatePortInformation(self, otherInstance): + """ + Copy port information from another FilterEnvironment instance to this. + :param otherInstance: FilterEnvironment instance + :return: None + """ + with QMutexLocker(self._portMutex): + oldIn = self.getAllInputPorts() + oldOut = self.getAllOutputPorts() + self._ports = [p.clone(self) for p in otherInstance.getAllInputPorts() + otherInstance.getAllOutputPorts()] + self.setDynamicPortsSupported(*otherInstance.getDynamicPortsSupported()) + self.emitPortInformationUpdated(oldIn, oldOut) + + def preStateTransition(self, operation): + """ + State transitions might be explicitely set all filters to the operation state before executing the + operation on the filters. This method makes sure that the state transitions are sane. + :param operation: The FilterState operation. + :return: None + """ + self._assertMyThread() + if self.getPlugin() is None or (useCImpl and self.getPlugin().data() is None): + raise NexTInternalError("Cannot perform state transitions on uninitialized plugin") + operations = { + FilterState.CONSTRUCTING: (None, FilterState.CONSTRUCTED, None), + FilterState.INITIALIZING: (FilterState.CONSTRUCTED, FilterState.INITIALIZED, self.getPlugin().onInit), + FilterState.STARTING: (FilterState.INITIALIZED, FilterState.ACTIVE, self.getPlugin().onStart), + FilterState.STOPPING: (FilterState.ACTIVE, FilterState.INITIALIZED, self.getPlugin().onStop), + FilterState.DEINITIALIZING: (FilterState.INITIALIZED, FilterState.CONSTRUCTED, self.getPlugin().onDeinit), + FilterState.DESTRUCTING: (FilterState.CONSTRUCTED, None, None), + } + fromState, toState, function = operations[operation] + if self._state != fromState: + raise FilterStateMachineError(self._state, operation) + self._state = operation + + def _stateTransition(self, operation): + """ + Perform state transition according to operation. + :param operation: The FilterState operation. + :return: None + """ + self._assertMyThread() + if self.getPlugin() is None or (useCImpl and self.getPlugin().data() is None): + raise NexTInternalError("Cannot perform state transitions on uninitialized plugin") + operations = { + FilterState.CONSTRUCTING: (None, FilterState.CONSTRUCTED, None), + FilterState.INITIALIZING: (FilterState.CONSTRUCTED, FilterState.INITIALIZED, self.getPlugin().onInit), + FilterState.STARTING: (FilterState.INITIALIZED, FilterState.ACTIVE, self.getPlugin().onStart), + FilterState.STOPPING: (FilterState.ACTIVE, FilterState.INITIALIZED, self.getPlugin().onStop), + FilterState.DEINITIALIZING: (FilterState.INITIALIZED, FilterState.CONSTRUCTED, self.getPlugin().onDeinit), + FilterState.DESTRUCTING: (FilterState.CONSTRUCTED, None, None), + } + fromState, toState, function = operations[operation] + # filters must be either in fromState or already in operation state (if preStateTransition has been used) + if self._state not in (fromState, operation): + raise FilterStateMachineError(self._state, operation) + self._state = operation + try: + function() + except Exception as e: # pylint: disable=broad-except + # What should be done on errors? + # 1. inhibit state transition to higher state + # pro: prevent activation of not properly intialized filter + # con: some actions of onInit might already be executed, we really want to call onDeinit for cleanup + # 2. ignore the exception and perform state transition anyways + # pro/con is inverse to 1 + # 3. introduce an error state + # pro: filters in error state are clearly identifyable and prevented from being used + # con: higher complexity, cleanup issues, ... + # --> for now we use 2. + self._state = fromState + logger.exception("Exception while executing operation %s of filter %s", + FilterState.state2str(operation), + self.propertyCollection().objectName()) + self._state = toState + + def init(self): + """ + Perform filter initialization (state transition CONSTRUCTED -> INITIALIZING -> INITIALIZED) + :return: None + """ + self._assertMyThread() + self._stateTransition(FilterState.INITIALIZING) + + def start(self): + """ + Perform filter start (state transition INITIALIZED -> STARTING -> ACTIVE) + :return: None + """ + self._assertMyThread() + self._stateTransition(FilterState.STARTING) + + def stop(self): + """ + Perform filter stop (state transition ACTIVE -> STOPPING -> INITIALIZED) + :return: None + """ + self._assertMyThread() + self._stateTransition(FilterState.STOPPING) + + def deinit(self): + """ + Perform filter deinitialization (state transition INITIALIZED -> DEINITIALIZING -> CONSTRUCTED) + :return: None + """ + self._assertMyThread() + self._stateTransition(FilterState.DEINITIALIZING) + + def state(self): + """ + Return the filter state. + :return: filter state integer + """ + self._assertMyThread() + return self._state diff --git a/nexxT/core/FilterMockup.py b/nexxT/core/FilterMockup.py new file mode 100644 index 0000000..779e991 --- /dev/null +++ b/nexxT/core/FilterMockup.py @@ -0,0 +1,169 @@ +# 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 defines the FilterMockup class +""" + +import logging +from PySide2.QtCore import QMutexLocker, Qt +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.Utils import assertMainThread, MethodInvoker +import nexxT + +logger = logging.getLogger(__name__) + +class FilterMockup(FilterEnvironment): + """ + The filter mockup class caches the port information of a filter without having the filter loaded and running + all the time. + """ + def __init__(self, library, factoryFunction, propertyCollection, graph): + super().__init__(None, None, propertyCollection, self) + assertMainThread() + self._library = library + self._graph = graph + self._factoryFunction = factoryFunction + self._propertyCollectionImpl = propertyCollection + self._pluginClass = None + self._createFilterAndUpdatePending = None + try: + # add also a child collection for the nexxT internals + pc = PropertyCollectionImpl("_nexxT", propertyCollection) + except PropertyCollectionChildExists: + pc = propertyCollection.getChildCollection("_nexxT") + pc.defineProperty("thread", "main", "The thread this filter belongs to.") + + def getGraph(self): + """ + Returns the FilterGraph instance this mockup belongs to + :return: FilterGraph instance + """ + return self._graph + + def getLibrary(self): + """ + Returns the library of this filter. + :return: library as given to constructor + """ + return self._library + + def getFactoryFunction(self): + """ + Returns the factory function of this filter. + :return: factory function as given to constructor + """ + return self._factoryFunction + + def createFilterAndUpdate(self, immediate=True): + """ + Creates the filter, performs init() operation and updates the port information. + :return: None + """ + # TODO is it really a good idea to use queued connections for dynamic port changes? + # maybe there is a better way to prevent too many calls to _createFilterAndUpdate + if immediate: + self._createFilterAndUpdate() + self._createFilterAndUpdatePending = None + elif self._createFilterAndUpdatePending is None: + self._createFilterAndUpdatePending = MethodInvoker(self._createFilterAndUpdate, Qt.QueuedConnection) + + def _createFilterAndUpdate(self): + self._createFilterAndUpdatePending = None + assertMainThread() + self._propertyCollectionImpl.markAllUnused() + with FilterEnvironment(self._library, self._factoryFunction, self._propertyCollectionImpl, self) as tempEnv: + for p in self._ports: + if p.dynamic(): + tempEnv.addPort(p.clone(tempEnv)) + tempEnv.init() + self._propertyCollectionImpl.deleteUnused() + self.updatePortInformation(tempEnv) + self._pluginClass = tempEnv.getPlugin().__class__ + if nexxT.useCImpl: + import cnexxT + if self._pluginClass is cnexxT.__dict__["QSharedPointer"]: + self._pluginClass = tempEnv.getPlugin().data().__class__ + + def createFilter(self): + """ + Creates the filter for real usage. State is CONSTRUCTED. This function is thread safe and can be called + from multiple threads. + :return: None + """ + # called from threads + res = FilterEnvironment(self._library, self._factoryFunction, self._propertyCollectionImpl) + with QMutexLocker(self._portMutex): + for p in self._ports: + if p.dynamic(): + res.addPort(p.clone(res)) + return res + + def addDynamicPort(self, portname, factory): + """ + Add a dynamic port to the filter and re-acquire the port information. + :param portname: name of the new port + :param factory: either InputPort or OutputPort + :return: None + """ + assertMainThread() + if factory is InputPortInterface: + factory = InputPort + if factory is OutputPortInterface: + # if InputPort or OutputPort classes are given as argument, make sure to actually use the factories + factory = OutputPort + self.addPort(factory(True, portname, self)) + self.createFilterAndUpdate(False) + + def renameDynamicPort(self, oldPortName, newPortName, factory): + """ + Rename a dynamic port of the filter. + :param oldPortName: original name of the port + :param newPortName: new name of the port + :param factory: either InputPort or OutputPort + :return: None + """ + assertMainThread() + found = False + try: + self.getPort(newPortName, factory) + found = True + except PortNotFoundError: + pass + if found: + raise PortExistsError("", newPortName) + p = self.getPort(oldPortName, factory) + p.setName(newPortName) + self.createFilterAndUpdate(False) + + def deleteDynamicPort(self, portname, factory): + """ + Remove a dynamic port of the filter. + :param portname: name of the dynamic port + :param factory: either InputPort or OutputPort + :return: + """ + assertMainThread() + p = self.getPort(portname, factory) + self.removePort(p) + self.createFilterAndUpdate(False) + + def getPropertyCollectionImpl(self): + """ + return the PropertyCollectionImpl instance associated with this filter + :return: PropertyCollectionImpl instance + """ + return self._propertyCollectionImpl + + def getPluginClass(self): + """ + Returns the class of the plugin. + :return: python class information for use with issubclass(...) + """ + return self._pluginClass diff --git a/nexxT/core/Graph.py b/nexxT/core/Graph.py new file mode 100644 index 0000000..ebfb35c --- /dev/null +++ b/nexxT/core/Graph.py @@ -0,0 +1,264 @@ +# 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 defines the class FilterGraph +""" + +import logging +from PySide2.QtCore import Signal, Slot +from nexxT.interface import InputPortInterface, OutputPortInterface +from nexxT.core.FilterMockup import FilterMockup +from nexxT.core.BaseGraph import BaseGraph +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +from nexxT.core.Utils import assertMainThread +from nexxT.core.Exceptions import NexTRuntimeError, PropertyCollectionChildNotFound, CompositeRecursion + +logger = logging.getLogger(__name__) + +class FilterGraph(BaseGraph): + """ + This class defines the filter graph. It adds dynamic (user-defined) ports top the BaseGraph class and connects + the graph with FilterMockup instances. It additionally manages PropertyCollections of the related filters. + """ + dynInputPortAdded = Signal(str, str) + dynInputPortRenamed = Signal(str, str, str) + dynInputPortDeleted = Signal(str, str) + dynOutputPortAdded = Signal(str, str) + dynOutputPortRenamed = Signal(str, str, str) + dynOutputPortDeleted = Signal(str, str) + + def __init__(self, subConfig): + super().__init__() + assertMainThread() + self._parent = subConfig + self._filters = {} + self._properties = subConfig.getPropertyCollection() + self.nodeDeleted.connect(self.onNodeDeleted) + self.nodeRenamed.connect(self.onNodeRename) + + #def dump(self): + # for name in self._filters: + # logger.internal("Dump %s:\n %s", name, shiboken2.dump(self._filters[name])) + + def getSubConfig(self): + """ + returns this graph's parent. + :return: a SubConfiguration instance + """ + return self._parent + + def nodeName(self, filterEnv): + """ + Given a FilterEnvironment instance, get the corresponding unique name. + :param filterEnv: a FilterEnvironment instance + :return: the name as string + """ + names = [n for n in self._filters if self._filters[n] is filterEnv] + if len(names) != 1: + raise NexTRuntimeError("Lookup of filter failed; either non-unique or not in graph.") + return names[0] + + def cleanup(self): + """ + cleanup function + :return: + """ + self._filters.clear() + + # pylint: disable=arguments-differ + # different arguments to BaseGraph are wanted in this case + # BaseGraph gets the node name, this method gets arguments + # for constructing a filter + @Slot(str, str, object) + def addNode(self, library, factoryFunction, suggestedName=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 + :param factoryFunction: function for instantiating the plugin + :param suggestedName: name suggested by used (if None, factoryFunction is used) + :return: the name of the created node + """ + assertMainThread() + if suggestedName is None: + suggestedName = factoryFunction + name = super()._uniqueNodeName(suggestedName) + try: + propColl = self._properties.getChildCollection(name) + except PropertyCollectionChildNotFound: + propColl = PropertyCollectionImpl(name, self._properties) + filterMockup = FilterMockup(library, factoryFunction, propColl, self) + filterMockup.createFilterAndUpdate() + self._filters[name] = filterMockup + assert super().addNode(name) == name + for port in filterMockup.getStaticInputPorts(): + self.addInputPort(name, port.name()) + for port in filterMockup.getStaticOutputPorts(): + self.addOutputPort(name, port.name()) + filterMockup.portInformationUpdated.connect(self.portInformationUpdated) + if factoryFunction == "compositeNode" and hasattr(library, "checkRecursion"): + try: + library.checkRecursion() + except CompositeRecursion as e: + self.deleteNode(name) + raise e + return name + # pylint: enable=arguments-differ + + def getMockup(self, name): + """ + Get the mockup related to the given filter. + :param name: the node name + :return: FilterMockup instance + """ + assertMainThread() + return self._filters[name] + + @Slot(str) + def onNodeDeleted(self, name): + """ + Delete corresponding FilterMockup and PropertyCollection instances. + :param name: the node name + :return: None + """ + assertMainThread() + logger.debug("onNodeDeleted: Deleted filter %s", name) + self._properties.deleteChild(name) + del self._filters[name] + + @Slot(str, str) + def onNodeRename(self, oldName, newName): + """ + Renames the corresponding filter of the node. + :param oldName: the old node name + :param newName: the new node name + :return: None + """ + assertMainThread() + f = self._filters[oldName] + del self._filters[oldName] + self._filters[newName] = f + self._properties.renameChild(oldName, newName) + + @Slot(str, str) + def addDynamicInputPort(self, node, port): + """ + Add a dynamic input port to the referenced node. + :param node: the affected node name + :param port: the name of the new port + :return: None + """ + assertMainThread() + self._filters[node].addDynamicPort(port, InputPortInterface) + self.dynInputPortAdded.emit(node, port) + + @Slot(str, str, str) + def renameDynamicInputPort(self, node, oldPort, newPort): + """ + Rename a dynamic input port of a node. + :param node: the affected node name + :param oldPort: the original name of the port + :param newPort: the new name of the port + :return: None + """ + assertMainThread() + self.renameInputPort(node, oldPort, newPort) + self._filters[node].renameDynamicPort(oldPort, newPort, InputPortInterface) + self.dynInputPortRenamed.emit(node, oldPort, newPort) + + @Slot(str, str) + def deleteDynamicInputPort(self, node, port): + """ + Remove a dynamic input port of a node. + :param node: the affected node name + :param port: the name of the port to be deleted + :return: None + """ + assertMainThread() + self._filters[node].deleteDynamicPort(port, InputPortInterface) + self.dynInputPortDeleted.emit(node, port) + + @Slot(str, str) + def addDynamicOutputPort(self, node, port): + """ + Add a dynamic output port to the referenced node. + :param node: the name of the affected node + :param port: the name of the new port + :return: None + """ + assertMainThread() + self._filters[node].addDynamicPort(port, OutputPortInterface) + self.dynOutputPortAdded.emit(node, port) + + @Slot(str, str, str) + def renameDynamicOutputPort(self, node, oldPort, newPort): + """ + Rename a dynamic output port of a node. + :param node: the affected node name + :param oldPort: the original name of the port + :param newPort: the new name of the port + :return: None + """ + assertMainThread() + self.renameOutputPort(node, oldPort, newPort) + self._filters[node].renameDynamicPort(oldPort, newPort, OutputPortInterface) + self.dynOutputPortRenamed.emit(node, oldPort, newPort) + + @Slot(str, str) + def deleteDynamicOutputPort(self, node, port): + """ + Remove a dynamic output port of a node. + :param node: the affected node name + :param port: the name of the port to be deleted + :return: None + """ + assertMainThread() + self._filters[node].deleteDynamicPort(port, OutputPortInterface) + self.dynOutputPortDeleted.emit(node, port) + + @Slot(object, object) + def portInformationUpdated(self, oldIn, oldOut): + """ + Called after the port information of a filter has changed. Makes sure that ports are deleted and + added in the graph as necessary. + :param oldIn: InputPort instances with old input ports + :param oldOut: OutputPort instances with old output ports + :return: + """ + # pylint: disable=too-many-locals + # cleaning up seems not to be an easy option here + assertMainThread() + def _nodeName(): + fe = self.sender() + name = [k for k in self._filters if self._filters[k] is fe] + assert len(name) == 1 + return name[0] + + name = _nodeName() + + for portskey, oldPorts, getPorts, deletePort, deleteDynPort, addPort in [ + ("inports", oldIn, self._filters[name].getAllInputPorts, self.deleteInputPort, + self.deleteDynamicInputPort, self.addInputPort), + ("outports", oldOut, self._filters[name].getAllOutputPorts, self.deleteOutputPort, + self.deleteDynamicOutputPort, self.addOutputPort)]: + + stalePorts = set(self._nodes[name][portskey]) + allPorts = set(self._nodes[name][portskey]) + newInputPorts = [] + # iterate over new ports + for np in getPorts(): + # check if port is already existing in configuration + if np.name() in stalePorts: + stalePorts.remove(np.name()) + if not np.name() in allPorts: + newInputPorts.append(np) + for p in stalePorts: + if len([op for op in oldPorts if (op.name() == p and op.dynamic())]) > 0: + deleteDynPort(name, p) + else: + deletePort(name, p) + for np in newInputPorts: + addPort(name, np.name()) diff --git a/nexxT/core/GuiStateSchema.json b/nexxT/core/GuiStateSchema.json new file mode 100644 index 0000000..1c4c309 --- /dev/null +++ b/nexxT/core/GuiStateSchema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/schema#", + "definitions": { + "identifier": { + "description": "Used for matching identifier names. Usual c identifiers including minus sign", + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$" + }, + "propertySection": { + "type": "object", + "propertyNames": { "$ref": "#/definitions/identifier" }, + "patternProperties": { + "^.*$": {"type": ["string", "number", "boolean"]} + } + }, + "sub_graph": { + "description": "sub-graph definition as used by applications and composite filters.", + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "$ref": "#/definitions/identifier" + }, + "_guiState": { + "propertyNames": { "$ref": "#/definitions/identifier" }, + "properties" : { + "$ref": "#/definitions/propertySection" + }, + "default": {} + } + } + } + }, + "type": "object", + "required": ["applications"], + "properties": { + "composite_filters": { + "type": "array", + "default": [], + "items": { + "$ref": "#/definitions/sub_graph" + } + }, + "applications": { + "type": "array", + "items": { + "$ref": "#/definitions/sub_graph" + } + }, + "_guiState": { + "$ref": "#/definitions/propertySection", + "default": {} + } + } +} diff --git a/nexxT/core/PluginManager.py b/nexxT/core/PluginManager.py new file mode 100644 index 0000000..c899024 --- /dev/null +++ b/nexxT/core/PluginManager.py @@ -0,0 +1,233 @@ +# 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 defines the class PluginManager +""" + +import sys +import inspect +import os.path +import logging +import gc +from collections import OrderedDict +import importlib.util +from importlib.machinery import ExtensionFileLoader, EXTENSION_SUFFIXES +from PySide2.QtCore import QObject +from nexxT.core.Exceptions import UnknownPluginType, NexTRuntimeError, PluginException +from nexxT.interface import Filter +from nexxT.core import PluginInterface +import nexxT + +logger = logging.getLogger(__name__) + +class BinaryLibrary: + """ + This class holds a reference to a QLibrary instance. Attributes can be used to access filter creation factories. + """ + def __init__(self, library): + self._library = library + self._availableFilterTypes = PluginInterface.singleton().availableFilters(library) + + def __getattr__(self, attr): + if attr in self._availableFilterTypes: + return lambda env, library=self._library: PluginInterface.singleton().create(library, attr, env) + raise NexTRuntimeError("requested creation func '%s' not found in %s" % (attr, self._library)) + + def __getitem__(self, idx): + return self._availableFilterTypes[idx] + + def __len__(self): + return len(self._availableFilterTypes) + + def unload(self): + """ + Should unload the shared object / DLL. ATM, this can only be achieved via unloadAll, so this method is empty. + :return: + """ + # TODO: currently we only have unlaodAll() + +class PythonLibrary: + """ + This class holds a reference to an imported python plugin. Attributes can be used to access filter creation + factories. + """ + _pyLoadCnt = 0 + + def __init__(self, library): + PythonLibrary._pyLoadCnt += 1 + self._library = library + modulesBefore = set(sys.modules.keys()) + logging.getLogger(__name__).debug("importing python module from file '%s'", library) + # https://stackoverflow.com/questions/67631/how-to-import-a-module-given-the-full-path + spec = importlib.util.spec_from_file_location("nexxT.plugins.plugin%d" % PythonLibrary._pyLoadCnt, library) + self._mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self._mod) + modulesAfter = set(sys.modules.keys()) + self._loadedModules = modulesAfter.difference(modulesBefore) + self._availableFilters = None + + def __getattr__(self, attr): + res = getattr(self._mod, attr, None) + if res is not None: + return res + raise NexTRuntimeError("requested creation func '%s' not found in %s" % (attr, self._library)) + + def _checkAvailableFilters(self): + self._availableFilters = [] + for attr in sorted(dir(self._mod)): + if isinstance(attr, str) and attr[0] != "_": + f = getattr(self._mod, attr) + try: + if issubclass(f, Filter) and not f is Filter: + self._availableFilters.append(attr) + except TypeError: + pass + + def __getitem__(self, idx): + if self._availableFilters is None: + self._checkAvailableFilters() + return self._availableFilters[idx] + + def __len__(self): + if self._availableFilters is None: + self._checkAvailableFilters() + return len(self._availableFilters) + + def unload(self): + """ + Unloads this python module. During loading, transitive dependent modules are detected and they are unloaded + as well. + :return: + """ + + for m in self._loadedModules: + if m in sys.modules: + mod = sys.modules[m] + try: + fn = inspect.getfile(mod) + except Exception: # pylint: disable=broad-except + # whatever goes wrong in the above call, we don't want to unload this module... + fn = None + loader = getattr(mod, '__loader__', None) + if not (fn is None or isinstance(loader, ExtensionFileLoader) or + os.path.splitext(fn)[1] in EXTENSION_SUFFIXES): + logger.internal("Unloading pure python module '%s' (%s)", m, fn) + del sys.modules[m] + +class PluginManager(QObject): + """ + This class handles the loading of plugins. It should be accessed by the singleton() static method. + """ + + _singleton = None + + @staticmethod + def singleton(): + """ + Returns the singleton instance + :return: PluginManager instance + """ + if PluginManager._singleton is None: + PluginManager._singleton = PluginManager() + return PluginManager._singleton + + def __init__(self): + super().__init__() + # loaded libraries + self._libraries = OrderedDict() + + def create(self, library, factoryFunction, filterEnvironment): + """ + Creates a filter from library by calling factoryFunction. + :param library: file name string (currently supported: .py ending); alternatively, it might be a python object, + in which case the loading will be omitted + :param factoryFunction: function name to construct the filter + :param filterEnvironment: the calling environment + :return: a nexxT Filter instance + """ + if isinstance(library, str): + prop = filterEnvironment.propertyCollection() + if library not in self._libraries: + try: + self._libraries[library] = self._load(library, prop) + except UnknownPluginType: + # pass exception from loader through + raise + except Exception as e: # pylint: disable=broad-except + # catching a general exception is exactly what is wanted here + logging.getLogger(__name__).exception("Exception while creating %s from library '%s'", + factoryFunction, library) + raise PluginException("Unexpected exception while loading the plugin %s:%s (%s)" % + (library, factoryFunction, e)) + res = getattr(self._libraries[library], factoryFunction)(filterEnvironment) + else: + res = getattr(library, factoryFunction)(filterEnvironment) + if nexxT.useCImpl and isinstance(res, Filter): + res = Filter.make_shared(res) + return res + + def getFactoryFunctions(self, library): + """ + returns all factory functions in the given library + :param library: library as string instance + :return: list of strings + """ + if library not in self._libraries: + try: + self._libraries[library] = self._load(library) + except UnknownPluginType: + # pass exception from loader through + raise + except Exception as e: # pylint: disable=broad-except + # catching a general exception is exactly what is wanted here + logging.getLogger(__name__).exception("Exception while loading library '%s'", + library) + raise PluginException("Unexpected exception while loading the library %s (%s)" % + (library, e)) + lib = self._libraries[library] + return [lib[i] for i in range(len(lib))] + + def unloadAll(self): + """ + unloads all loaded libraries (python and c++). + :return: + """ + for library in list(self._libraries.keys())[::-1]: + self._unload(library) + if PluginInterface is not None: + gc.collect() + # TODO: decide what to do about unloading dll's + # unloading the .dll's / shared objects might be causing a hard to debug seg-fault, if there are still + # objects left in the python world which are deallocated later. Calling the destructor will then + # result in a segmentation fault. So the safe option here would be not to unload any .dll's. + #PluginInterface.singleton().unloadAll() + self._libraries.clear() + + def _unload(self, library): + self._libraries[library].unload() + + def _load(self, library, prop=None): + if library.startswith("pyfile://"): + return self._loadPyfile(library[len("pyfile://"):], prop) + if library.startswith("binary://"): + return self._loadBinary(library[len("binary://"):], prop) + raise UnknownPluginType("don't know how to load library '%s'" % library) + + @staticmethod + def _loadPyfile(library, prop=None): + if prop is not None: + library = prop.evalpath(library) + return PythonLibrary(library) + + @staticmethod + def _loadBinary(library, prop=None): + if PluginInterface is None: + raise UnknownPluginType("binary plugins can only be loaded with c extension enabled.") + if prop is not None: + library = prop.evalpath(library) + logging.getLogger(__name__).debug("loading binary plugin from file '%s'", library) + return BinaryLibrary(library) diff --git a/nexxT/core/PortImpl.py b/nexxT/core/PortImpl.py new file mode 100644 index 0000000..1ca964c --- /dev/null +++ b/nexxT/core/PortImpl.py @@ -0,0 +1,174 @@ +# 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 contains implementations for abstract classes InputPort and OutputPort +""" + +import logging +from PySide2.QtCore import QThread, QSemaphore, Signal, QObject, Qt +from nexxT.interface.Ports import InputPortInterface, OutputPortInterface +from nexxT.interface.DataSamples import DataSample +from nexxT.core.Exceptions import NexTRuntimeError, NexTInternalError + +logger = logging.getLogger(__name__) + +class InterThreadConnection(QObject): + """ + Helper class for transmitting data samples between threads + """ + transmitInterThread = Signal(object, QSemaphore) + + def __init__(self, qthread_from): + super().__init__() + self.moveToThread(qthread_from) + self.semaphore = QSemaphore(1) + + def receiveSample(self, dataSample): + """ + Receive a sample, called in the source's thread. Uses a semaphore to avoid buffering infinitely. + :param dataSample: the sample to be received + :return: None + """ + assert QThread.currentThread() is self.thread() + self.semaphore.acquire(1) + self.transmitInterThread.emit(dataSample, self.semaphore) + +class OutputPortImpl(OutputPortInterface): + """ + This class defines an output port of a filter. + """ + + # pylint: disable=abstract-method + # the Factory function is static and will be assigned later in this module + + # constructor inherited from OutputPort + + def transmit(self, dataSample): + """ + transmit a data sample over this port + :param dataSample: sample to transmit + """ + if not QThread.currentThread() is self.thread(): + raise NexTRuntimeError("OutputPort.transmit has been called from an unexpected thread.") + self.transmitSample.emit(dataSample) + + def clone(self, newEnvironment): + """ + Return a copy of this port attached to a new environment. + :param newEnvironment: the new FilterEnvironment instance + :return: a new Port instance + """ + return OutputPortImpl(self.dynamic(), self.name(), newEnvironment) + + @staticmethod + def setupDirectConnection(outputPort, inputPort): + """ + Setup a direct (intra-thread) connection between outputPort and inputPort + Note: both instances must live in same thread! + :param outputPort: the output port instance to be connected + :param inputPort: the input port instance to be connected + :return:None + """ + logger.info("setup direct connection between %s -> %s", outputPort.name(), inputPort.name()) + outputPort.transmitSample.connect(inputPort.receiveSync, Qt.DirectConnection) + + @staticmethod + def setupInterThreadConnection(outputPort, inputPort, outputPortThread): + """ + Setup an inter thread connection between outputPort and inputPort + :param outputPort: the output port instance to be connected + :param inputPort: the input port instance to be connected + :param outputPortThread: the QThread instance of the outputPort instance + :return: an InterThreadConnection instance which manages the connection (has + to survive until connections is deleted) + """ + logger.info("setup inter thread connection between %s -> %s", outputPort.name(), inputPort.name()) + itc = InterThreadConnection(outputPortThread) + outputPort.transmitSample.connect(itc.receiveSample, Qt.DirectConnection) + itc.transmitInterThread.connect(inputPort.receiveAsync, Qt.QueuedConnection) + return itc + +class InputPortImpl(InputPortInterface): + """ + This class defines an input port of a filter. In addition to the normal port attributes, there are + two new attributes related to automatic buffering of input data samples. + queueSizeSamples sets the maximum number of samples buffered (it can be None, if queueSizeSeconds is not None) + queueSizeSeconds sets the maximum time of samples buffered (it can be None, if queueSizeSamples is not None) + If both attributes are set, they are and-combined. + """ + + # pylint: disable=abstract-method + # the Factory function is static and will be assigned later in this module + + def __init__(self, dynamic, name, environment, queueSizeSamples=1, queueSizeSeconds=None): + super().__init__(dynamic, name, environment) + self.queueSizeSamples = queueSizeSamples + self.queueSizeSeconds = queueSizeSeconds + self.queue = [] # TODO use something better suited + + def getData(self, delaySamples=0, delaySeconds=None): + """ + Return a data sample stored in the queue (called by the filter). + :param delaySamples: 0 related the most actual sample, numbers > 0 relates to historic samples (None can be + given if delaySeconds is not None) + :param delaySeconds: if not None, a delay of 0.0 is related to the current sample, positive numbers are related + to historic samples (TODO specify the exact semantics of delaySeconds) + :return: DataSample instance + """ + if not QThread.currentThread() is self.thread(): + raise NexTRuntimeError("InputPort.getData has been called from an unexpected thread.") + if delaySamples is not None: + assert delaySeconds is None + return self.queue[delaySamples] + if delaySeconds is not None: + assert delaySamples is None + delayTime = delaySeconds / DataSample.TIMESTAMP_RES + i = 0 + while i < len(self.queue) and self.queue[0].getTimestamp() - self.queue[i].getTimestamp() < delayTime: + i += 1 + return self.queue[i] + raise RuntimeError("delaySamples and delaySeconds are both None.") + + def _addToQueue(self, dataSample): + self.queue.insert(0, dataSample) + if self.queueSizeSamples is not None and len(self.queue) > self.queueSizeSamples: + self.queue = self.queue[:self.queueSizeSamples] + if self.queueSizeSeconds is not None: + queueSizeTime = self.queueSizeSeconds / DataSample.TIMESTAMP_RES + while len(self.queue) > 0 and self.queue[0].getTimestamp() - self.queue[-1].getTimestamp() > queueSizeTime: + self.queue.pop() + self.environment().portDataChanged(self) + + def receiveAsync(self, dataSample, semaphore): + """ + Called from framework only and implements the asynchronous receive mechanism using a semaphore. TODO implement + :param dataSample: the transmitted DataSample instance + :param semaphore: a QSemaphore instance + :return: None + """ + if not QThread.currentThread() is self.thread(): + raise NexTInternalError("InputPort.receiveAsync has been called from an unexpected thread.") + semaphore.release(1) + self._addToQueue(dataSample) + + def receiveSync(self, dataSample): + """ + Called from framework only and implements the synchronous receive mechanism. TODO implement + :param dataSample: the transmitted DataSample instance + :return: None + """ + if not QThread.currentThread() is self.thread(): + raise NexTInternalError("InputPort.receiveSync has been called from an unexpected thread.") + self._addToQueue(dataSample) + + def clone(self, newEnvironment): + """ + Return a copy of this port attached to a new environment. + :param newEnvironment: the new FilterEnvironment instance + :return: a new Port instance + """ + return InputPortImpl(self.dynamic(), self.name(), newEnvironment, self.queueSizeSamples, self.queueSizeSeconds) diff --git a/nexxT/core/PropertyCollectionImpl.py b/nexxT/core/PropertyCollectionImpl.py new file mode 100644 index 0000000..f1575d5 --- /dev/null +++ b/nexxT/core/PropertyCollectionImpl.py @@ -0,0 +1,371 @@ +# 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 defines the PropertyCollection interface class of the nexxT framework. +""" + +from collections import OrderedDict +from pathlib import Path +import logging +import string +import os +import shiboken2 +from PySide2.QtGui import QValidator, QRegExpValidator, QIntValidator, QDoubleValidator +from PySide2.QtCore import QRegExp, QLocale, Signal, Slot, QObject, QMutex, QMutexLocker +from nexxT.core.Exceptions import (PropertyCollectionChildNotFound, PropertyCollectionChildExists, + PropertyCollectionUnknownType, PropertyParsingError, NexTInternalError, + PropertyInconsistentDefinition, PropertyCollectionPropertyNotFound) +from nexxT.core.Utils import assertMainThread, checkIdentifier +from nexxT.interface import PropertyCollection + +class Property: + """ + This class represents a specific property. + """ + def __init__(self, defaultVal, helpstr, converter, validator): + self.defaultVal = defaultVal + self.value = defaultVal + self.helpstr = helpstr + self.converter = converter + self.validator = validator + self.used = True + +class ReadonlyValidator(QValidator): + """ + A validator implementing readonly values. + """ + def __init__(self, value): + super().__init__() + self._value = value + + def fixup(self, inputstr): # pylint: disable=unused-argument + """ + override from QValidator + :param inputstr: string + :return: fixed string + """ + return str(self._value) + + def validate(self, inputstr, pos): + """ + override from QValidator + :param inputstr: string + :param pos: int + :return: state, newpos + """ + state = QValidator.Acceptable if inputstr == str(self._value) else QValidator.Invalid + return state, inputstr, pos + +class PropertyCollectionImpl(PropertyCollection): + """ + This class represents a collection of properties. These collections are organized in a tree, such that there + are parent/child relations. + """ + propertyChanged = Signal(object, str) + propertyAdded = Signal(object, str) + propertyRemoved = Signal(object, str) + childAdded = Signal(object, str) + childRemoved = Signal(object, str) + childRenamed = Signal(object, str, str) + + def __init__(self, name, parentPropColl, loadedFromConfig=None): + PropertyCollection.__init__(self) + assertMainThread() + self._properties = {} + self._accessed = False # if no access to properties has been made, we stick with configs from config file. + self._loadedFromConfig = loadedFromConfig if loadedFromConfig is not None else {} + self._propertyMutex = QMutex(QMutex.Recursive) + if parentPropColl is not None: + if not isinstance(parentPropColl, PropertyCollectionImpl): + raise NexTInternalError("parentPropColl should always be a property collection instance but it isn't") + parentPropColl.addChild(name, self) + + def applyLoadedConfig(self, loadedFromConfig): + """ + applies the loaded configuration after the instance has been already created. This is used for guiState items. + :param loadedFromConfig: dictionary loaded from json file + :return: None + """ + if len(self._loadedFromConfig) > 0: + raise NexTInternalError("Expected that no loaded config is present.") + self._accessed = False + self._loadedFromConfig = loadedFromConfig + + def childEvent(self, event): + assertMainThread() + if event.added(): + self.childAdded.emit(self, event.child().objectName()) + elif event.removed(): + self.childRemoved.emit(self, event.child().objectName()) + + def getChildCollection(self, name): + """ + Return child property collection with given name + :param name: the name of the child + :return: PropertyCollection instance + """ + assertMainThread() + # note findChild seems to be recursive, that's really a boomer; the option Qt::FindDirectChildrenOnly + # doesn't seem to be there on the python side. So we do this by hand + #res = self.findChild(QObject, name) + res = [c for c in self.children() if c.objectName() == name] + if len(res) == 0: + raise PropertyCollectionChildNotFound(name) + return res[0] + + @staticmethod + def _defaultIntConverter(theObject): + if isinstance(theObject, str): + c = QLocale(QLocale.C) + ret, ok = c.toInt(theObject) + if not ok: + raise PropertyParsingError("Cannot convert '%s' to int." % theObject) + return ret + c = QLocale(QLocale.C) + return c.toString(theObject) + + @staticmethod + def _defaultFloatConverter(theObject): + if isinstance(theObject, str): + c = QLocale(QLocale.C) + ret, ok = c.toDouble(theObject) + if not ok: + raise PropertyParsingError("Cannot convert '%s' to double." % theObject) + return ret + c = QLocale(QLocale.C) + return c.toString(theObject) + + @staticmethod + def _defaultValidator(defaultVal): + if isinstance(defaultVal, str): + validator = QRegExpValidator(QRegExp(".*")) + elif isinstance(defaultVal, int): + validator = QIntValidator() + elif isinstance(defaultVal, float): + validator = QDoubleValidator() + else: + raise PropertyCollectionUnknownType(defaultVal) + return validator + + @staticmethod + def _defaultStringConverter(defaultVal): + if isinstance(defaultVal, str): + stringConverter = str + elif isinstance(defaultVal, int): + stringConverter = PropertyCollectionImpl._defaultIntConverter + elif isinstance(defaultVal, float): + stringConverter = PropertyCollectionImpl._defaultFloatConverter + else: + raise PropertyCollectionUnknownType(defaultVal) + return stringConverter + + def defineProperty(self, name, defaultVal, helpstr, stringConverter=None, validator=None): + """ + Return the value of the given property, creating a new property if it doesn't exist. + :param name: the name of the property + :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 + :param stringConverter: a conversion function which converts a string to the property type. If not given, + a default conversion based on QLocale::C will be used. + :param validator: an optional QValidator instance. If not given, the validator will be chosen according to the + defaultVal type. + :return: the current value of this property + """ + self._accessed = True + checkIdentifier(name) + with QMutexLocker(self._propertyMutex): + if not name in self._properties: + if validator is None: + validator = self._defaultValidator(defaultVal) + if stringConverter is None: + stringConverter = self._defaultStringConverter(defaultVal) + self._properties[name] = Property(defaultVal, helpstr, stringConverter, validator) + p = self._properties[name] + if name in self._loadedFromConfig: + l = self._loadedFromConfig[name] + if type(l) is type(defaultVal): + p.value = l + else: + try: + p.value = stringConverter(str(l)) + except PropertyParsingError: + logging.getLogger(__name__).warning("Property %s: can't convert value '%s'.", name, str(l)) + self.propertyAdded.emit(self, name) + else: + # the arguments to getProperty shall be consistent among calls + p = self._properties[name] + if p.defaultVal != defaultVal or p.helpstr != helpstr: + raise PropertyInconsistentDefinition(name) + if stringConverter is not None and p.converter is not stringConverter: + raise PropertyInconsistentDefinition(name) + if validator is not None and p.validator is not validator: + raise PropertyInconsistentDefinition(name) + + p.used = True + return p.value + + @Slot(str) + def getProperty(self, name): + self._accessed = True + with QMutexLocker(self._propertyMutex): + if name not in self._properties: + raise PropertyCollectionPropertyNotFound(name) + p = self._properties[name] + p.used = True + return p.value + + def getPropertyDetails(self, name): + """ + returns the property details of the property identified by name. + :param name: the property name + :return: a Property instance + """ + with QMutexLocker(self._propertyMutex): + if name not in self._properties: + raise PropertyCollectionPropertyNotFound(name) + p = self._properties[name] + return p + + def getAllPropertyNames(self): + """ + Query all property names handled in this collection + :return: list of strings + """ + return list(self._properties.keys()) + + @Slot(str, str) + def setProperty(self, name, value): + """ + Set the value of a named property. + :param name: property name + :param value: the value to be set + :return: None + """ + self._accessed = True + with QMutexLocker(self._propertyMutex): + if name not in self._properties: + raise PropertyCollectionPropertyNotFound(name) + p = self._properties[name] + if isinstance(value, str): + state, newValue, newCursorPos = p.validator.validate(value, len(value)) + if state != QValidator.Acceptable: + raise PropertyParsingError("Property %s: '%s' is not compatible to given validator." + % (name, value)) + value = p.converter(newValue) + if value != p.value: + p.value = value + self.propertyChanged.emit(self, name) + + def markAllUnused(self): + """ + Mark all properties of the collection as unused (TODO: do we need recursion?) + :return: None + """ + with QMutexLocker(self._propertyMutex): + for n in self._properties: + self._properties[n].used = False + + def deleteUnused(self): + """ + Delete properties marked as unused (TODO: do we need recursion?) + :return: None + """ + if not self._accessed: + # this function is only meaningful if something in the store has been used. + return + with QMutexLocker(self._propertyMutex): + toDel = [] + for n in self._properties: + if not self._properties[n].used: + toDel.append(n) + for n in toDel: + del self._properties[n] + self.propertyRemoved.emit(self, n) + self._loadedFromConfig.clear() + + def saveDict(self): + """ + Save properties into a dictionary suited for json output. + :return: dictionary with key/value pairs + """ + if self._accessed: + res = OrderedDict() + with QMutexLocker(self._propertyMutex): + for n in sorted(self._properties): + res[n] = self._properties[n].value + return res + return self._loadedFromConfig + + def addChild(self, name, propColl): + """ + register child + :param name: name of the child + :param propColl: the child, an instance of PropertyCollectionImpl + :return: None + """ + assertMainThread() + try: + self.getChildCollection(name) + raise PropertyCollectionChildExists(name) + except PropertyCollectionChildNotFound: + pass + propColl.setObjectName(name) + propColl.setParent(self) + logging.getLogger(__name__).internal("Propcoll %s: add child %s", self.objectName(), name) + + def renameChild(self, oldName, newName): + """ + Rename a child collection. + :param oldName: original name of collection + :param newName: new name of collection + :return: None + """ + assertMainThread() + c = self.getChildCollection(oldName) + try: + self.getChildCollection(newName) + raise PropertyCollectionChildExists(newName) + except PropertyCollectionChildNotFound: + pass + c.setObjectName(newName) + self.childRenamed.emit(self, oldName, newName) + + def deleteChild(self, name): + """ + Remove a child collection. + :param name: the name of the collection. + :return: None + """ + assertMainThread() + cc = self.getChildCollection(name) + for c in cc.children(): + if isinstance(c, PropertyCollectionImpl): + cc.deleteChild(c.objectName()) + if shiboken2.isValid(cc): # pylint: disable=no-member + shiboken2.delete(cc) # pylint: disable=no-member + + def evalpath(self, path): + """ + Evaluates the string path. If it is an absolute path it is unchanged, otherwise it is converted + to an absolute path relative to the config file path. + :param path: a string + :return: absolute path as string + """ + if Path(path).is_absolute(): + return path + root_prop = self + while root_prop.parent() is not None: + root_prop = root_prop.parent() + # substitute ${VAR} with environment variables + path = string.Template(path).safe_substitute(os.environ) + if Path(path).is_absolute(): + return path + try: + return str((Path(root_prop.getProperty("CFGFILE")).parent / path).absolute()) + except PropertyCollectionPropertyNotFound: + return path diff --git a/nexxT/core/SubConfiguration.py b/nexxT/core/SubConfiguration.py new file mode 100644 index 0000000..45bda1e --- /dev/null +++ b/nexxT/core/SubConfiguration.py @@ -0,0 +1,190 @@ +# 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 defines the class SubConfiguration +""" + +import logging +from collections import OrderedDict +from PySide2.QtCore import QObject, Signal +from nexxT.core.Graph import FilterGraph +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +from nexxT.core.Exceptions import NexTInternalError, PropertyCollectionPropertyNotFound, PropertyCollectionChildNotFound +from nexxT.core.Utils import checkIdentifier + +logger = logging.getLogger(__name__) + +class SubConfiguration(QObject): + """ + This class handles a sub-configuration of a nexxT application. Sub configs are either applications or + SubGraphs (which behave like a filter). + """ + nameChanged = Signal(object, str) + + def __init__(self, name, configuration): + super().__init__() + checkIdentifier(name) + self._propertyCollection = PropertyCollectionImpl(name, configuration.propertyCollection()) + self._graph = FilterGraph(self) + self._name = name + + #def dump(self): + # self._graph.dump() + + def cleanup(self): + """ + Cleanup function + :return: + """ + self._graph.cleanup() + + def getName(self): + """ + Get the name of this subconfiguration + :return: string + """ + return self._name + + def setName(self, name): + """ + Sets the name of this subconfiguration + :param name: new name (string) + :return: None + """ + if name != self._name: + oldName = self._name + self._name = name + self.nameChanged.emit(self, oldName) + + def getPropertyCollection(self): + """ + Get the property collection of this sub configuration + :return: a PropertyCollectionImpl instance + """ + return self._propertyCollection + + def getGraph(self): + """ + Get the filter graph of this sub config + :return: a FilterGraph instance + """ + return self._graph + + @staticmethod + def _connectionStringToTuple(con): + f, t = con.split("->") + fromNode, fromPort = f.strip().split(".") + toNode, toPort = t.strip().split(".") + return fromNode.strip(), fromPort.strip(), toNode.strip(), toPort.strip() + + @staticmethod + def _tupleToConnectionString(connection): + return "%s.%s -> %s.%s" % connection + + def load(self, cfg, compositeLookup): + """ + load graph from config dictionary (inverse operation of save(...)) + :param cfg: dictionary loaded from json file + :return: None + """ + # apply subconfig gui state + if "_guiState" in cfg and len(cfg["_guiState"]) > 0: + guistateCC = self._propertyCollection.getChildCollection("_guiState") + for k in cfg["_guiState"]: + PropertyCollectionImpl(k, guistateCC, cfg["_guiState"][k]) + for n in cfg["nodes"]: + if not n["library"].startswith("composite://"): + p = PropertyCollectionImpl(n["name"], self._propertyCollection, n["properties"]) + # 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"]) + 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 + elif n["library"] == "composite://ref": + name = n["factoryFunction"] + cf = compositeLookup(name) + tmp = self._graph.addNode(cf, "compositeNode", suggestedName=n["name"]) + 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"]: + contuple = self._connectionStringToTuple(c) + self._graph.addConnection(*contuple) + + def save(self): + """ + save graph to config dictionary (inverse operation of load(...)) + :return: dictionary which can be saved as json file + """ + def adaptLibAndFactory(lib, factory): + if not isinstance(lib, str): + if factory in ["CompositeInputNode", "CompositeOutputNode"]: + ncfg["library"] = "composite://port" + ncfg["factoryFunction"] = factory[:-len("Node")] + elif factory in ["compositeNode"]: + ncfg["library"] = "composite://ref" + ncfg["factoryFunction"] = lib.getName() + else: + raise NexTInternalError("Unexpected factory function '%s'" % factory) + else: + ncfg["library"] = lib + ncfg["factoryFunction"] = factory + return lib, factory + #self.dump() + cfg = dict(name=self.getName()) + try: + gs = self._propertyCollection.getChildCollection("_guiState") + cfg["_guiState"] = {} + for object in gs.children(): + if isinstance(object, PropertyCollectionImpl): + cfg["_guiState"][object.objectName()] = object.saveDict() + except PropertyCollectionChildNotFound: + pass + cfg["nodes"] = [] + for name in self._graph.allNodes(): + ncfg = OrderedDict(name=name) + mockup = self._graph.getMockup(name) + lib = mockup.getLibrary() + factory = mockup.getFactoryFunction() + lib, factory = adaptLibAndFactory(lib, factory) + ncfg["dynamicInputPorts"] = [] + ncfg["staticInputPorts"] = [] + ncfg["dynamicOutputPorts"] = [] + ncfg["staticOutputPorts"] = [] + for ip in mockup.getDynamicInputPorts(): + ncfg["dynamicInputPorts"].append(ip.name()) + for ip in self._graph.allInputPorts(name): + if not ip in ncfg["dynamicInputPorts"]: + ncfg["staticInputPorts"].append(ip) + for op in mockup.getDynamicOutputPorts(): + ncfg["dynamicOutputPorts"].append(op.name()) + for op in self._graph.allOutputPorts(name): + if not op in ncfg["dynamicOutputPorts"]: + ncfg["staticOutputPorts"].append(op) + p = self._propertyCollection.getChildCollection(name) + try: + ncfg["thread"] = p.getChildCollection("_nexxT").getProperty("thread") + logger.debug("saving: subconfig %s / node %s -> thread: %s", self._name, name, ncfg["thread"]) + except PropertyCollectionChildNotFound: + pass + except PropertyCollectionPropertyNotFound: + pass + ncfg["properties"] = p.saveDict() + cfg["nodes"].append(ncfg) + cfg["connections"] = [self._tupleToConnectionString(c) for c in self._graph.allConnections()] + return cfg diff --git a/nexxT/core/Thread.py b/nexxT/core/Thread.py new file mode 100644 index 0000000..0f04775 --- /dev/null +++ b/nexxT/core/Thread.py @@ -0,0 +1,163 @@ +# 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 defines the class NexTThread. +""" + +import logging +from PySide2.QtCore import QObject, Signal, Slot, QCoreApplication, QThread +from nexxT.interface import FilterState +from nexxT.core.Exceptions import NodeExistsError, NexTInternalError, NodeNotFoundError, NexTRuntimeError + +logger = logging.getLogger(__name__) + +class NexTThread(QObject): + """ + A thread of the active application + """ + operationFinished = Signal() # used to synchronize threads from active application + + _operations = dict( + init=FilterState.INITIALIZING, + start=FilterState.STARTING, + stop=FilterState.STOPPING, + deinit=FilterState.DEINITIALIZING, + ) + + def __init__(self, name): + """ + Creates a NexTThread instance with a name. If this is not the main thread, create a corresponding + QThread and start it (i.e., the event loop). + :param name: name of the thread + """ + super().__init__() + self._filters = {} + self._filter2name = {} + self._mockups = {} + self._name = name + if not self.thread() is QCoreApplication.instance().thread(): + raise NexTInternalError("unexpected thread") + if name == "main": + self._qthread = QCoreApplication.instance().thread() + else: + self._qthread = QThread(parent=self) + self._qthread.setObjectName(name) + self._qthread.start() + self.moveToThread(self._qthread) + self.cleanUpCalled = False + + def __del__(self): + logger.debug("destructor of Thread") + if not self.cleanUpCalled: + logger.warning("Thread:: calling cleanup in destructor.") + self.cleanup() + logger.debug("destructor of Thread done") + + def cleanup(self): + """ + Stop threads and deallocate all resources. + :return: + """ + self.cleanUpCalled = True + if self._name != "main" and self._qthread is not None: + logger.internal("stopping thread %s", self._name) + self._qthread.quit() + self._qthread.wait() + self._qthread = None + logger.internal("cleanup filters") + for name in self._filters: + self._filters[name].close() + self._filters.clear() + self._filter2name.clear() + logger.internal("cleanup mockups") + # Note: the mockups are in ownership of the corresponding graph, we don't delete them + self._mockups.clear() + logger.internal("Thread cleanup done") + + def addMockup(self, name, mockup): + """ + Add a FilterMockup instance by name. + :param name: name of the filter + :param mockup: the corresponding FilterMockup instance + :return: + """ + if name in self._mockups: + raise NodeExistsError(name) + self._mockups[name] = mockup + + def getFilter(self, name): + """ + Return a filter by name. + :param name: the filter name + :return: A nexxT Filter instance + """ + if not name in self._filters: + raise NodeNotFoundError(name) + return self._filters[name] + + def getName(self, filterEnvironment): + """ + Return the path to the filter environment + :param filterEnvironment: a FilterEnvironment instance + :return: [Application instance] + [CompositeFilter instance]* + """ + if not filterEnvironment in self._filter2name: + raise NexTRuntimeError("Filterenvironment not found. Not active?") + return self._filter2name[filterEnvironment] + + def qthread(self): + """ + Return the corresponding qthread. + :return: a QThread instance + """ + return self._qthread + + @Slot(str, object) + def performOperation(self, operation, barrier): + """ + Perform the given operation on all filters. + :param operation: one of "create", "destruct", "init", "start", "stop", "deinit" + :param barrier: a barrier object to synchronize threads + :return: None + """ + # wait that all threads are in their event loop. + barrier.wait() + if operation in self._operations: + # pre-adaptation of states (e.g. from CONSTRUCTED to INITIALIZING) + # before one of the actual operations is called, all filters are in the adapted state + for name in self._mockups: + self._filters[name].preStateTransition(self._operations[operation]) + # wait for all threads + barrier.wait() + # perform operation for all filters + for name in self._mockups: + try: + if operation == "create": + res = self._mockups[name].createFilter() + res.setParent(self) + self._filters[name] = res + self._filter2name[res] = name + logger.internal("Created filter %s in thread %s", name, self._name) + elif operation == "destruct": + self._filters[name].close() + logging.getLogger(__name__).internal("deleting filter...") + del self._filters[name] + logging.getLogger(__name__).internal("filter deleted") + else: + op = getattr(self._filters[name], operation) + op() + except Exception: # pylint: disable=broad-except + # catching a general exception is exactly what is wanted here + logging.getLogger(__name__).exception("Exception while performing operation '%s' on %s", + operation, name) + # notify ActiveApplication + logging.getLogger(__name__).internal("emitting finished") + self.operationFinished.emit() + # and wait for all other threads to complete + logging.getLogger(__name__).internal("waiting for barrier") + barrier.wait() + logging.getLogger(__name__).internal("performOperation done") diff --git a/nexxT/core/Utils.py b/nexxT/core/Utils.py new file mode 100644 index 0000000..1e4cddc --- /dev/null +++ b/nexxT/core/Utils.py @@ -0,0 +1,261 @@ +# 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 contains various small utility classes. +""" + +import io +import re +import sys +import logging +import datetime +import os.path +import sqlite3 +from PySide2.QtCore import (QObject, Signal, Slot, QMutex, QWaitCondition, QCoreApplication, QThread, + QMutexLocker, QRecursiveMutex, QTimer, QSortFilterProxyModel, Qt) +from nexxT.core.Exceptions import NexTInternalError, InvalidIdentifierException + +class MethodInvoker(QObject): + """ + a workaround for broken QMetaObject.invokeMethod wrapper. See also + https://stackoverflow.com/questions/53296261/usage-of-qgenericargument-q-arg-when-using-invokemethod-in-pyside2 + """ + + signal = Signal() # 10 arguments + + IDLE_TASK = "IDLE_TASK" + + def __init__(self, callback, connectiontype, *args): + super().__init__() + self.args = args + self.callback = callback + if connectiontype is self.IDLE_TASK: + QTimer.singleShot(0, self.callbackWrapper) + else: + self.signal.connect(self.callbackWrapper, connectiontype) + self.signal.emit() + + @Slot(object) + def callbackWrapper(self): + """ + Slot which actuall performs the method call. + :return: None + """ + self.callback(*self.args) + +class Barrier: + """ + Implement a barrier, such that threads block until other monitored threads reach a specific location. + The barrier can be used multiple times (it is reinitialized after the threads passed). + + See https://stackoverflow.com/questions/9637374/qt-synchronization-barrier/9639624#9639624 + """ + def __init__(self, count): + self.count = count + self.origCount = count + self.mutex = QMutex() + self.condition = QWaitCondition() + + def wait(self): + """ + Wait until all monitored threads called wait. + :return: None + """ + self.mutex.lock() + self.count -= 1 + if self.count > 0: + self.condition.wait(self.mutex) + else: + self.count = self.origCount + self.condition.wakeAll() + self.mutex.unlock() + +def assertMainThread(): + """ + assert that function is called in main thread, otherwise, a NexTInternalError is raised + :return: None + """ + if QCoreApplication.instance() and not QThread.currentThread() == QCoreApplication.instance().thread(): + raise NexTInternalError("Non thread-safe function is called in unexpected thread.") + + +def checkIdentifier(name): + """ + Check that name is a valid nexxT name (c identifier including minus signs). Raises InvalidIdentifierException. + :param name: string + :return: None + """ + if re.match(r'^[A-Za-z_][A-Za-z0-9_-]*$', name) is None: + InvalidIdentifierException(name) + +# https://github.com/ar4s/python-sqlite-logging/blob/master/sqlite_handler.py +class SQLiteHandler(logging.Handler): + """ + Logging handler that write logs to SQLite DB + """ + ONE_CONNECTION_PER_THREAD = 0 + SINGLE_CONNECTION = 1 + + def __init__(self, filename, threadSafety=ONE_CONNECTION_PER_THREAD): + """ + Construct sqlite handler appending to filename + :param filename: + """ + logging.Handler.__init__(self) + self.filename = filename + self.threadSafety = threadSafety + if self.threadSafety == self.SINGLE_CONNECTION: + self.dbConn = sqlite3.connect(self.filename, check_same_thread=False) + self.dbConn.execute( + "CREATE TABLE IF NOT EXISTS " + "debug(date datetime, loggername text, filename, srclineno integer, func text, level text, msg text)") + self.dbConn.commit() + elif self.threadSafety == self.ONE_CONNECTION_PER_THREAD: + self.mutex = QRecursiveMutex() + self.dbs = {} + else: + raise RuntimeError("Unknown threadSafety option %s" % repr(self.threadSafety)) + + def _getDB(self): + if self.threadSafety == self.SINGLE_CONNECTION: + return self.dbConn + # create a new connection for each thread + with QMutexLocker(self.mutex): + tid = QThread.currentThread() + if not tid in self.dbs: + # Our custom argument + db = sqlite3.connect(self.filename) # might need to use self.filename + if len(self.dbs) == 0: + db.execute( + "CREATE TABLE IF NOT EXISTS " + "debug(date datetime, loggername text, filename, srclineno integer, " + "func text, level text, msg text)") + db.commit() + self.dbs[tid] = db + return self.dbs[tid] + + def emit(self, record): + """ + save record to sqlite db + :param record a logging record + :return:None + """ + db = self._getDB() + thisdate = datetime.datetime.now() + db.execute( + 'INSERT INTO debug(date, loggername, filename, srclineno, func, level, msg) VALUES(?,?,?,?,?,?,?)', + ( + thisdate, + record.name, + os.path.abspath(record.filename), + record.lineno, + record.funcName, + record.levelname, + record.msg % record.args, + ) + ) + if self.threadSafety == self.SINGLE_CONNECTION: + pass + else: + db.commit() + +class FileSystemModelSortProxy(QSortFilterProxyModel): + """ + Proxy model for sorting a file system models with "directories first" strategy. + See also https://stackoverflow.com/questions/10789284/qfilesystemmodel-sorting-dirsfirst + """ + def lessThan(self, left, right): + if self.sortColumn() == 0: + asc = self.sortOrder() == Qt.SortOrder.AscendingOrder + left_fi = self.sourceModel().fileInfo(left) + right_fi = self.sourceModel().fileInfo(right) + if self.sourceModel().data(left) == "..": + return asc + if self.sourceModel().data(right) == "..": + return not asc + + if not left_fi.isDir() and right_fi.isDir(): + return not asc + if left_fi.isDir() and not right_fi.isDir(): + return asc + return QSortFilterProxyModel.lessThan(self, left, right) + +class QByteArrayBuffer(io.IOBase): + """ + Efficient IOBase wrapper around QByteArray for pythonic access, for memoryview doesn't seem + supported. + """ + def __init__(self, qByteArray): + super().__init__() + self.ba = qByteArray + self.p = 0 + + def readable(self): + return True + + def read(self, size=-1): + if size < 0: + size = self.ba.size() - self.p + oldP = self.p + self.p += size + if self.p > self.ba.size(): + self.p = self.ba.size() + return self.ba[oldP:self.p].data() + + def seekable(self): + return True + + def seek(self, offset, whence): + if whence == io.SEEK_SET: + self.p = offset + elif whence == io.SEEK_CUR: + self.p += offset + elif whence == io.SEEK_END: + self.p = self.ba.size() + if self.p < 0: + self.p = 0 + elif self.p > self.ba.size(): + self.p = self.ba.size() + +def handle_exception(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + sys.excepthook(*sys.exc_info()) + return wrapper + +if __name__ == "__main__": # pragma: no cover + def _smokeTestBarrier(): + # pylint: disable=import-outside-toplevel + # pylint: disable=missing-class-docstring + import time + import random + + n = 10 + + barrier = Barrier(n) + + def threadWork(): + st = random.randint(0, 5000)/1000. + time.sleep(st) + barrier.wait() + + class MyThread(QThread): + def run(self): + threadWork() + + threads = [] + for _ in range(n): + t = MyThread() + t.start() + threads.append(t) + + for t in threads: + t.wait() + + _smokeTestBarrier() diff --git a/nexxT/core/__init__.py b/nexxT/core/__init__.py new file mode 100644 index 0000000..51197ed --- /dev/null +++ b/nexxT/core/__init__.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +""" +This __init__.py handles c++ parts of the core (if enabled) +""" + +import nexxT +# constants here are really classes +# pylint: disable=invalid-name +if nexxT.useCImpl: + import cnexxT + PluginInterface = cnexxT.nexxT.PluginInterface +else: + PluginInterface = None + \ No newline at end of file diff --git a/nexxT/interface/DataSamples.py b/nexxT/interface/DataSamples.py new file mode 100644 index 0000000..f792070 --- /dev/null +++ b/nexxT/interface/DataSamples.py @@ -0,0 +1,59 @@ +# 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 defines the nexxT interface class DataSample. +""" + +from PySide2.QtCore import QByteArray + +class DataSample: + """ + This class is used for storing a data sample of the nexxT framework. For most generic usage, a QByteArray is used + for the storage. This means that deserialization has to be performed on every usage and data generators need to + serialize the data. Assumes that serializing / deserializing are efficient operations. + DataSample instances additionally have a type, which is a string and it should uniquely define the serialization + method. Last but not least, an integer timestamp is stored for all DataSample instances. + """ + + TIMESTAMP_RES = 1e-6 # the resolution of the timestamps + + def __init__(self, content, datatype, timestamp): + self._content = QByteArray(content) + self._timestamp = timestamp + self._type = datatype + self._transmitted = False + + def getContent(self): + """ + Get the contents of this sample as a QByteArray. Note that this is an efficient operation due to the copy on + write semantics of QByteArray. It also asserts that the original contents cannot be modified. + :return: QByteArray instance copy + """ + return QByteArray(self._content) + + def getTimestamp(self): + """ + Return the timestamp associated to the data. + :return: integer timestamp + """ + return self._timestamp + + def getDatatype(self): + """ + Return the data type. + :return: data type string + """ + return self._type + + @staticmethod + def copy(src): + """ + Create a copy of this DataSample instance + :param src: the instance to be copied + :return: the cloned data sample + """ + return DataSample(src.getContent(), src.getDatatype(), src.getTimestamp()) diff --git a/nexxT/interface/Filters.py b/nexxT/interface/Filters.py new file mode 100644 index 0000000..6f575fd --- /dev/null +++ b/nexxT/interface/Filters.py @@ -0,0 +1,160 @@ +# 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 defines the FilterState and Filter classes of the nexxT interface. +""" + +from PySide2.QtCore import QObject + +class FilterState: + """ + This class defines an enum for the filter states. + """ + CONSTRUCTING = 0 + CONSTRUCTED = 1 + INITIALIZING = 2 + INITIALIZED = 3 + STARTING = 4 + ACTIVE = 5 + STOPPING = 6 + DEINITIALIZING = 7 + DESTRUCTING = 8 + DESTRUCTED = 9 + + @staticmethod + def state2str(state): + """ + converts a state integer to the corresponding string + :param state: the state integer + :return: string + """ + for k in FilterState.__dict__: + if k[0] != "_" and k.upper() == k and getattr(FilterState, k) == state: + return k + raise RuntimeError("Unknown state %s" % state) + + +class Filter(QObject): + """ + This class is the base class for defining a nexxT filter. A minimal nexxT filter class looks like this: + + class SimpleStaticFilter(Filter): + + def __init__(self, environment): + super().__init__(False, False, environment) + pc = self.propertyCollection() + self.sample_property = pc.getProperty("sample_property", 0.1, "a sample property for demonstration purpose") + self.inPort = InputPort.Factory(False, "inPort", environment) + self.addStaticPort(self.inPort) + self.outPort = OutputPort.Factory(False, "outPort", environment) + self.addStaticPort(self.outPort) + + def onPortDataChanged(self, inputPort): + dataSample = inputPort.getData() + newSample = DataSample.copy(dataSample) + self.outPort.transmit(dataSample) + + The constructor has a single argument environment which is passed through to this base class. It configures + dynamic port usage with the two boolean flags. In the constructor, the filter can define and query properties + and create ports. + + The onPortDataChanged is called whenever new data arrives on an input port. + """ + def __init__(self, dynInPortsSupported, dynOutPortsSupported, environment): + super().__init__() + environment.setDynamicPortsSupported(dynInPortsSupported, dynOutPortsSupported) + self._environment = environment + + # protected methods (called by filter implementations) + + def propertyCollection(self): + """ + Return the property collection associated with this filter. + :return: PropertyCollection instance + """ + return self._environment.propertyCollection() + + def guiState(self): + """ + Return the gui state associated with this filter. Note: the gui state shall not be used for properties which + are important for data transport, for these cases the propertyCollection() shall be used. Typical gui state + variables are: the geometry of a user-managed window, the last directory path used in a file dialog, etc. + The gui state might not be initialized during mockup-phase. + :return: PropertyCollection instance + """ + return self._environment.guiState() + + def addStaticPort(self, port): + """ + Register a static port for this filter. Only possible in CONSTRUCTING state. + :param port: InputPort or OutputPort instance + :return: None + """ + if port.dynamic(): + raise RuntimeError("The given port should be static but is dynamic.") + self._environment.addPort(port) + + def removeStaticPort(self, port): + """ + Remove a static port of this filter. Only possible in CONSTRUCTING state. + :param port: InputPort or OutputPort instance + :return: None + """ + if port.dynamic(): + raise RuntimeError("The given port should be static but is dynamic.") + self._environment.removePort(port) + + def getDynamicInputPorts(self): + """ + Get dynamic input ports of this filter. Only possible in and after INITIALIZING state. + :return: list of dynamic input ports + """ + return self._environment.getDynamicInputPorts() + + def getDynamicOutputPorts(self): + """ + Get dynamic output ports of this filter. Only possible in and after INITIALIZING state. + :return: list of dynamic output ports + """ + return self._environment.getDynamicOutputPorts() + + # protected methods (overwritten by filter implementations) + + def onInit(self): + """ + This function can be overwritten for performing initialization tasks related to dynamic ports. + :return: None + """ + + def onStart(self): + """ + This function can be overwritten for general initialization tasks (e.g. acquire resources needed to + run the filter, open files, etc.). + :return: None + """ + + def onPortDataChanged(self, inputPort): + """ + This function can be overwritten to be notified when new data samples arrive at input ports. For each + data sample arrived this function will be called exactly once. + :param inputPort: the port where the data arrived + :return: None + """ + + def onStop(self): + """ + This function can be overwritten for general de-initialization tasks (e.g. release resources needed to + run the filter, close files, etc.). It is the opoosite to onStart(...). + :return: None + """ + + def onDeinit(self): + """ + This function can be overwritten for performing de-initialization tasks related to dynamic ports. It is + the opposite to onInit(...) + :return: None + """ diff --git a/nexxT/interface/Ports.py b/nexxT/interface/Ports.py new file mode 100644 index 0000000..50c9fa6 --- /dev/null +++ b/nexxT/interface/Ports.py @@ -0,0 +1,181 @@ +# 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 defines the Port, InputPort and OutputPort interface classes of the nexxT framework. +""" + +from PySide2.QtCore import QObject, Signal + +class Port(QObject): + """ + This class is the base class for ports. It is used mainly as structure, containing the 3 properties + dynamic (boolean), name (str) and environment (a FilterEnvironment instance) + """ + INPUT_PORT = 0 + OUTPUT_PORT = 1 + + def __init__(self, dynamic, name, environment): + super().__init__() + self._dynamic = dynamic + self._name = name + self._environment = environment + if not isinstance(self, OutputPortInterface) and not isinstance(self, InputPortInterface): + raise RuntimeError("Ports must be either InputPorts or OutputPorts") + self._type = self.OUTPUT_PORT if isinstance(self, OutputPortInterface) else self.INPUT_PORT + + def dynamic(self): + """ + Returns whether this is a dynamic port + :return: a boolean + """ + return self._dynamic + + def name(self): + """ + Returns the port name + :return: a string + """ + return self._name + + def setName(self, name): + """ + Sets the port name + :param name: the port name given as a string + :return: None + """ + self._name = name + + def environment(self): + """ + Returns the environment instance managing this port + :return: FilterEnvironment instance + """ + return self._environment + + def isInput(self): + """ + Returns true if this is an input port + :return: bool + """ + return self._type == self.INPUT_PORT + + def isOutput(self): + """ + Returns true if this is an output port + :return: bool + """ + return self._type == self.OUTPUT_PORT + + def clone(self, newEnvironment): + """ + This function must be overwritten in inherited classes to create a clone of this port attached to a + different environment. + :param newEnvironment: the new FilterEnvironment instance + :return: a new Port instance + """ + raise NotImplementedError() + +@staticmethod +def OutputPort(dynamic, name, environment): # pylint: disable=invalid-name + # camel case is used because this is a factory function for class OutputPortInterface + """ + Creates an OutputPort instance with an actual implementation attached. Will be dynamically implemented by + the nexxT framework. This is done to prevent having implementation details in the class + + Note: import this function with 'from next.interface import OutputPort'. + + :param dynamic: boolean whether this is a dynamic input port + :param name: the name of the port + :param environment: the FilterEnvironment instance + :return: + """ + raise NotImplementedError() + +class OutputPortInterface(Port): + """ + This class defines an output port of a filter. + """ + + # constructor inherited from Port + transmitSample = Signal(object) + + def transmit(self, dataSample): + """ + transmit a data sample over this port + :param dataSample: sample to transmit + """ + raise NotImplementedError() + + def clone(self, newEnvironment): + """ + Return a copy of this port attached to a new environment. + :param newEnvironment: the new FilterEnvironment instance + :return: a new Port instance + """ + raise NotImplementedError() + +def InputPort(dynamic, name, environment, queueSizeSamples=1, queueSizeSeconds=None): # pylint: disable=invalid-name + # camel case is used because this is a factory function for class OutputPortInterface + """ + Creates an InputPortInterface instance with an actual implementation attached. Will be dynamically implemented by + the nexxT framework. This is done to prevent having implementation details in the class + + Note: import this function with 'from next.interface import InputPort'. + + :param dynamic: boolean whether this is a dynamic input port + :param name: the name of the port + :param environment: the FilterEnvironment instance + :param queueSizeSamples: the size of the queue in samples + :param queueSizeSeconds: the size of the queue in seconds + :return: an InputPort instance (actually an InputPortImpl instance) + """ + raise NotImplementedError() + +class InputPortInterface(Port): + """ + This class defines an input port of a filter. In addition to the normal port attributes, there are + two new attributes related to automatic buffering of input data samples. + queueSizeSamples sets the maximum number of samples buffered (it can be None, if queueSizeSeconds is not None) + queueSizeSeconds sets the maximum time of samples buffered (it can be None, if queueSizeSamples is not None) + If both attributes are set, they are and-combined. + """ + + def getData(self, delaySamples=0, delaySeconds=None): + """ + Return a data sample stored in the queue (called by the filter). TODO implement this + :param delaySamples: 0 related the most actual sample, numbers > 0 relates to historic samples (None can be + given if delaySeconds is not None) + :param delaySeconds: if not None, a delay of 0.0 is related to the current sample, positive numbers are related + to historic samples (TODO specify the exact semantics of delaySeconds) + :return: DataSample instance + """ + raise NotImplementedError() + + def receiveAsync(self, dataSample, semaphore): + """ + Called from framework only and implements the asynchronous receive mechanism using a semaphore. TODO implement + :param dataSample: the transmitted DataSample instance + :param semaphore: a QSemaphore instance + :return: None + """ + raise NotImplementedError() + + def receiveSync(self, dataSample): + """ + Called from framework only and implements the synchronous receive mechanism. TODO implement + :param dataSample: the transmitted DataSample instance + :return: None + """ + raise NotImplementedError() + + def clone(self, newEnvironment): + """ + Return a copy of this port attached to a new environment. + :param newEnvironment: the new FilterEnvironment instance + :return: a new Port instance + """ + raise NotImplementedError() diff --git a/nexxT/interface/PropertyCollections.py b/nexxT/interface/PropertyCollections.py new file mode 100644 index 0000000..cd2ed84 --- /dev/null +++ b/nexxT/interface/PropertyCollections.py @@ -0,0 +1,63 @@ +# 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 defines the PropertyCollection interface class of the nexxT framework. +""" + +from PySide2.QtCore import QObject, Signal, Slot + +class PropertyCollection(QObject): + """ + This class represents a collection of properties. These collections are organized in a tree, such that there + are parent/child relations. This is a generic base class, which is implemented in the core package. Access to + properties throug the methods below is thread safe. + """ + + propertyChanged = Signal(object, str) + + def defineProperty(self, name, defaultVal, helpstr, stringConverter=None, validator=None): + """ + Return the value of the given property, creating a new property if it doesn't exist. If it does exist, + the definition must be consistent, otherwise an error is raised. + :param name: the name of the property + :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 + :param stringConverter: a conversion function which converts a string to the property type. If not given, + a default conversion based on QLocale::C will be used. + :param validator: an optional QValidator instance. If not given, the validator will be chosen according to the + defaultVal type. + :return: the current value of this property + """ + raise NotImplementedError() + + def getProperty(self, name): + """ + return the property identified by name + :param name: a string + :return: the current property value + """ + raise NotImplementedError() + + @Slot(str, object) + def setProperty(self, name, value): + """ + Set the value of a named property. + :param name: property name + :param value: the value to be set + :return: None + """ + raise NotImplementedError() + + def evalpath(self, path): + """ + Evaluates the string path. If it is an absolute path it is unchanged, otherwise it is converted + to an absolute path relative to the config file path. + :param path: a string + :return: absolute path as string + """ + raise NotImplementedError() diff --git a/nexxT/interface/Services.py b/nexxT/interface/Services.py new file mode 100644 index 0000000..d0f8eb7 --- /dev/null +++ b/nexxT/interface/Services.py @@ -0,0 +1,45 @@ +# 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 defines the Services class of the nexxT framework. +""" + +class Services: + """ + This class can be used to publish and query services. Services are QObjects with specific signal and slot + interfaces you can connect to. Note that slots can be called directly via QMetaObject::invokeMethod + """ + services = {} + + @staticmethod + def addService(name, service): + """ + Publish a named service. + :param name: the name of the service + :param service: a QObject instance + :return: None + """ + if name in Services.services: + raise RuntimeError("Service %s already exists" % name) + Services.services[name] = service + + @staticmethod + def getService(name): + """ + Query a named service + :param name: the name of the service + :return: the related QObject instance + """ + return Services.services[name] + + @staticmethod + def removeAll(): + """ + Remove all registered services + :return: None + """ + Services.services = {} diff --git a/nexxT/interface/__init__.py b/nexxT/interface/__init__.py new file mode 100644 index 0000000..c2d53be --- /dev/null +++ b/nexxT/interface/__init__.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +""" +This __init__.py generates shortcuts for exported classes +""" + +import nexxT +if nexxT.useCImpl: + import cnexxT + # constants here are really classes + # pylint: disable=invalid-name + cnexxT = cnexxT.nexxT + Filter = cnexxT.Filter + FilterState = cnexxT.FilterState + DataSample = lambda *args, **kw: cnexxT.DataSample.make_shared(cnexxT.DataSample(*args, **kw)) + DataSample.TIMESTAMP_RES = cnexxT.DataSample.TIMESTAMP_RES + DataSample.copy = cnexxT.DataSample.copy + cnexxT.DataSample.registerMetaType() + cnexxT.DataSample.registerMetaType() + Port = cnexxT.Port + OutputPortInterface = cnexxT.OutputPortInterface + InputPortInterface = cnexxT.InputPortInterface + PropertyCollection = cnexxT.PropertyCollection + Services = cnexxT.Services + OutputPort = lambda *args, **kw: Port.make_shared(OutputPortInterface(*args, **kw)) + InputPort = (lambda dynamic, name, environment, queueSizeSamples=1, queueSizeSeconds=-1: + Port.make_shared(InputPortInterface(dynamic, name, environment, queueSizeSamples, queueSizeSeconds))) +else: + # pylint: enable=invalid-name + from nexxT.interface.Filters import Filter, FilterState + from nexxT.interface.DataSamples import DataSample + from nexxT.interface.Ports import Port + from nexxT.interface.Ports import OutputPortInterface + from nexxT.interface.Ports import InputPortInterface + from nexxT.interface.PropertyCollections import PropertyCollection + from nexxT.interface.Services import Services + # make sure that PortImpl is actually imported to correctly set up the factory functions + from nexxT.core import PortImpl + OutputPort = PortImpl.OutputPortImpl + InputPort = PortImpl.InputPortImpl + OutputPortInterface.setupDirectConnection = OutputPort.setupDirectConnection + OutputPortInterface.setupInterThreadConnection = OutputPort.setupInterThreadConnection + del PortImpl +del nexxT diff --git a/nexxT/services/ConsoleLogger.py b/nexxT/services/ConsoleLogger.py new file mode 100644 index 0000000..fe90fd2 --- /dev/null +++ b/nexxT/services/ConsoleLogger.py @@ -0,0 +1,84 @@ +# 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 service which maps log messages coming from c++ and qt to python log messages. +It is automatically used by NEXT_LOG_*() macros in c++. +""" + +import logging +import os.path +import sys +from PySide2.QtCore import QObject, Slot, qInstallMessageHandler, QtMsgType + +logger = logging.getLogger(__name__) + +# see https://stackoverflow.com/questions/32443808/best-way-to-override-lineno-in-python-logger +# pylint: disable=too-many-arguments +# pylint: disable=unused-argument +def makeRecord(self, name, level, filename, lineno, msg, args, excInfo, func=None, extra=None, sinfo=None): + """ + A factory method which can be overridden in subclasses to create + specialized LogRecords. + """ + if extra is not None: + filename, lineno = extra + name = "c++/%s" % (os.path.split(filename)[1]) + return logging.LogRecord(name, level, filename, lineno, msg, args, excInfo, func, sinfo) +# pylint: enable=too-many-arguments +# pylint: enable=unused-argument + +logger.__class__ = type("CplusplusLogger", (logger.__class__,), dict(makeRecord=makeRecord)) + +class ConsoleLogger(QObject): + """ + Logging service to console (using python logging module) + """ + @Slot(int, str, str, int) + def log(self, level, message, file, line): # pylint: disable=no-self-use + """ + Called from c++ to log a message. + :param level: logging compatible log level + :param message: message as a string + :param file: file which originated the log message + :param line: line of log message statement + :return: + """ + logger.log(level, message, extra=(file, line)) + + @staticmethod + def qtMessageHandler(qtMsgType, qMessageLogContext, msg): + """ + Qt message handler for handling qt messages in normal logging. + :param qtMsgType: qt log level + :param qMessageLogContext: qt log context + :param msg: message as a string + :return: + """ + typeMap = {QtMsgType.QtDebugMsg : logging.DEBUG, + QtMsgType.QtInfoMsg : logging.INFO, + QtMsgType.QtWarningMsg : logging.WARNING, + QtMsgType.QtCriticalMsg : logging.CRITICAL, + QtMsgType.QtFatalMsg : logging.FATAL} + logger.log(typeMap[qtMsgType], msg, extra=(qMessageLogContext.file if qMessageLogContext.file is not None + else "", qMessageLogContext.line)) + +# https://stackoverflow.com/questions/6234405/logging-uncaught-exceptions-in-python +def handleException(*args): + """ + Generic exception handler for logging uncaught exceptions in plugin code. + :param args: + :return: + """ + exc_type = args[0] + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(*args) + return + logger.error("Uncaught exception", exc_info=args) + + +qInstallMessageHandler(ConsoleLogger.qtMessageHandler) +sys.excepthook = handleException diff --git a/nexxT/services/__init__.py b/nexxT/services/__init__.py new file mode 100644 index 0000000..5eabbc7 --- /dev/null +++ b/nexxT/services/__init__.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# diff --git a/nexxT/services/gui/Configuration.py b/nexxT/services/gui/Configuration.py new file mode 100644 index 0000000..db2e9e4 --- /dev/null +++ b/nexxT/services/gui/Configuration.py @@ -0,0 +1,707 @@ +# 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 the Configuration GUI service of the nexxT framework. +""" + +import logging +from PySide2.QtCore import QObject, Signal, Slot, Qt, QAbstractItemModel, QModelIndex +from PySide2.QtGui import QFont, QPainter +from PySide2.QtWidgets import (QTreeView, QAction, QStyle, QApplication, QFileDialog, QAbstractItemView, QMessageBox, + QHeaderView, QMenu, QDockWidget, QGraphicsView) +from nexxT.interface import Services, FilterState +from nexxT.core.Exceptions import NexTRuntimeError +from nexxT.core.ConfigFiles import ConfigFileLoader +from nexxT.core.Configuration import Configuration +from nexxT.core.Application import Application +from nexxT.core.Utils import assertMainThread +from nexxT.services.gui.PropertyDelegate import PropertyDelegate +from nexxT.services.gui.GraphEditor import GraphScene + +logger = logging.getLogger(__name__) + +ITEM_ROLE = Qt.UserRole + 4223 + +class ConfigurationModel(QAbstractItemModel): + """ + This class encapsulates a next Configuration item in a QAbstractItemModel ready for usage in a QTreeView. + + Internally, item-trees are constructed which duplicate the relevant information from the python classes. + """ + + # Helper classes + + class Item: + """ + An item instance for creating the item tree. Items have children, a parent and arbitrary content. + """ + def __init__(self, parent, content): + if parent is not None: + parent.children.append(self) + self.children = [] + self.parent = parent + self.content = content + + def row(self): + """ + :return: the index of this item in the parent item. + """ + return self.parent.children.index(self) + + class NodeContent: + """ + A node in a subconfig, for usage within the model + """ + def __init__(self, subConfig, name): + self.subConfig = subConfig + self.name = name + + class PropertyContent: + """ + A property in a propertyCollection, for usage within the model. + """ + def __init__(self, name, propertyCollection): + self.name = name + self.property = propertyCollection + + class SubConfigContent: + """ + A subConfiguration, for usage within the model. + """ + def __init__(self, subConfig): + self.subConfig = subConfig + + # Model implementation + + def __init__(self, configuration, parent): + super().__init__(parent) + self.root = self.Item(None, configuration) + self.Item(self.root, "composite") + self.Item(self.root, "apps") + self.activeApp = None + configuration.subConfigAdded.connect(self.subConfigAdded) + configuration.subConfigRemoved.connect(self.subConfigRemoved) + configuration.appActivated.connect(self.appActivated) + + @staticmethod + def isApplication(index): + """ + Returns true, if this index relates to an applications, false otherwise. + :param index: a QModelIndex instance + :return: bool + """ + parent = index.parent() + return parent.isValid() and not parent.parent().isValid() and parent.row() == 1 + + def indexOfSubConfigParent(self, subConfig): + """ + Returns the index of the given subConfig's parent + :param subConfig: a SubConfiguration instance + :return: a QModelIndex instance. + """ + sctype = Configuration.configType(subConfig) + if sctype is Configuration.CONFIG_TYPE_APPLICATION: + parent = self.index(1, 0, QModelIndex()) + elif sctype is Configuration.CONFIG_TYPE_COMPOSITE: + parent = self.index(0, 0, QModelIndex()) + else: + raise NexTRuntimeError("Unexpected subconfig type") + return parent + + def indexOfSubConfig(self, subConfig): + """ + Returns the index of the given subConfig + :param subConfig: a SubConfiguration instance + :return: a QModelIndex instance. + """ + parent = self.indexOfSubConfigParent(subConfig) + parentItem = parent.internalPointer() + if isinstance(subConfig, str): + idx = [i for i in range(len(parentItem.children)) + if parentItem.children[i].content.subConfig.getName() == subConfig] + else: + idx = [i for i in range(len(parentItem.children)) + if parentItem.children[i].content.subConfig is subConfig] + if len(idx) != 1: + raise NexTRuntimeError("Unable to locate subconfig.") + return self.index(idx[0], 0, parent) + + def indexOfNode(self, subConfig, node): + """ + Returns the index of the given node inside a subconfig. + :param subConfig: a SubConfiguration instance + :param node: a node name + :return: a QModelIndex instance + """ + parent = self.indexOfSubConfig(subConfig) + parentItem = parent.internalPointer() + idx = [i for i in range(len(parentItem.children)) + if parentItem.children[i].content.name == node] + if len(idx) != 1: + raise NexTRuntimeError("Unable to locate node.") + return self.index(idx[0], 0, parent) + + def subConfigByNameAndType(self, name, sctype): + """ + Returns a SubConfiguration instance, given its name and type + :param name: the name as a string + :param sctype: either CONFIG_TYPE_APPLICATION or CONFIG_TYPE_COMPOSITE + :return: a SubConfiguration instance + """ + if sctype is Configuration.CONFIG_TYPE_APPLICATION: + found = [c.content.subConfig + for c in self.root.children[1].children if c.content.subConfig.getName() == name] + elif sctype is Configuration.CONFIG_TYPE_COMPOSITE: + found = [c.content.subConfig + for c in self.root.children[0].children if c.content.subConfig.getName() == name] + else: + raise NexTRuntimeError("Unexpected subconfig type") + if len(found) != 1: + raise NexTRuntimeError("Unable to locate subConfig") + return found[0] + + @Slot(object) + def subConfigAdded(self, subConfig): + """ + This slot is called when a subconfig is added to the configuration instance. It inserts a row as needed. + :param subConfig: a SubConfiguration instance + :return: + """ + parent = self.indexOfSubConfigParent(subConfig) + parentItem = parent.internalPointer() + graph = subConfig.getGraph() + subConfig.nameChanged.connect(self.subConfigRenamed) + graph.nodeAdded.connect(lambda node: self.nodeAdded(subConfig, node)) + graph.nodeDeleted.connect(lambda node: self.nodeDeleted(subConfig, node)) + graph.nodeRenamed.connect(lambda oldName, newName: self.nodeRenamed(subConfig, oldName, newName)) + self.beginInsertRows(parent, len(parentItem.children), len(parentItem.children)) + self.Item(parentItem, self.SubConfigContent(subConfig)) + self.endInsertRows() + + @Slot(object) + def subConfigRenamed(self, subConfig, oldName): # pylint: disable=unused-argument + """ + This slot is called when a subconfig is renamed in the configuration instance. + :param subConfig: a SubConfiguration instance + :param oldName: the old name of this subConfig + :return: + """ + index = self.indexOfSubConfig(subConfig) + if (self.activeApp is not None and + subConfig is self.subConfigByNameAndType(self.activeApp, Configuration.CONFIG_TYPE_APPLICATION)): + self.activeApp = subConfig.getName() + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + + @Slot(str, int) + def subConfigRemoved(self, name, sctype): + """ + This slot is called when a subconfig is removed from the configuration + :param name. the name of the removed subConfig + :param sctype: the type ( either CONFIG_TYPE_APPLICATION or CONFIG_TYPE_COMPOSITE) + :return: + """ + logger.debug("sub config removed %s %d", name, sctype) + subConfig = self.subConfigByNameAndType(name, sctype) + parent = self.indexOfSubConfigParent(subConfig) + parentItem = parent.internalPointer() + index = self.indexOfSubConfig(subConfig) + if sctype == Configuration.CONFIG_TYPE_APPLICATION and name == self.activeApp: + self.appActivated("", None) + idx = index.row() + self.beginRemoveRows(parent, idx, idx) + parentItem.children = parentItem.children[:idx] + parentItem.children[idx+1:] + self.endRemoveRows() + + @Slot(str, object) + def appActivated(self, name, app): + """ + This slot is called when an application has been activated. + :param name: the name of the application + :param app: the Application instance + :return: + """ + if self.activeApp is not None: + try: + subConfig = self.subConfigByNameAndType(self.activeApp, Configuration.CONFIG_TYPE_APPLICATION) + parent = self.indexOfSubConfig(subConfig) + index = parent.row() + self.dataChanged.emit(index, index, [Qt.FontRole]) + self.activeApp = None + except NexTRuntimeError: + logger.exception("error during resetting active app (ignored)") + + if name != "" and app is not None: + subConfig = self.subConfigByNameAndType(name, Configuration.CONFIG_TYPE_APPLICATION) + parent = self.indexOfSubConfig(subConfig) + self.activeApp = name + index = parent.row() + self.dataChanged.emit(index, index, [Qt.FontRole]) + + def nodeAdded(self, subConfig, node): + """ + This slot is called when a node is added to a subConfig + :param subConfig: a SubConfiguration instance + :param node: the node name. + :return: + """ + parent = self.indexOfSubConfig(subConfig) + parentItem = parent.internalPointer() + self.beginInsertRows(parent, len(parentItem.children), len(parentItem.children)) + item = self.Item(parentItem, self.NodeContent(subConfig, node)) + self.endInsertRows() + mockup = subConfig.getGraph().getMockup(node) + propColl = mockup.getPropertyCollectionImpl() + logger.debug("register propColl: %s", propColl) + for pname in propColl.getAllPropertyNames(): + self.propertyAdded(item, propColl, pname) + propColl.propertyAdded.connect(lambda pc, name: self.propertyAdded(item, pc, name)) + propColl.propertyRemoved.connect(lambda pc, name: self.propertyRemoved(item, pc, name)) + propColl.propertyChanged.connect(lambda pc, name: self.propertyChanged(item, pc, name)) + + def nodeDeleted(self, subConfig, node): + """ + This slot is called when a node is removed from a subConfig + :param subConfig: a SubConfiguration instance + :param node: the node name. + :return: + """ + index = self.indexOfNode(subConfig, node) + parent = index.parent() + parentItem = parent.internalPointer() + idx = index.row() + self.beginRemoveRows(parent, idx, idx) + parentItem.children = parentItem.children[:idx] + parentItem.children[idx+1:] + self.endRemoveRows() + + def nodeRenamed(self, subConfig, oldName, newName): + """ + This slot is called when a node is renamed in a subConfig + :param subConfig: a SubConfiguration instance + :param oldName: the original name. + :param newName: the new name + :return: + """ + if oldName != newName: + index = self.indexOfNode(subConfig, oldName) + item = index.internalPointer() + item.content.name = newName + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + + def propertyAdded(self, parentItem, propColl, name): + """ + Slot called when a property was added. + :param parentItem: a NodeItem instance + :param propColl: the PropertyCollection instance + :param name: the name of the new property. + :return: + """ + parent = self.indexOfNode(parentItem.content.subConfig, parentItem.content.name) + self.beginInsertRows(parent, len(parentItem.children), len(parentItem.children)) + self.Item(parentItem, self.PropertyContent(name, propColl)) + self.endInsertRows() + + def indexOfProperty(self, nodeItem, propName): + """ + Returns the model index of the specified property + :param nodeItem: a NodeItem instance + :param propName: a property name + :return: a QModelIndex instance + """ + idx = [idx for idx in range(len(nodeItem.children)) if nodeItem.children[idx].content.name == propName] + if len(idx) != 1: + raise NexTRuntimeError("Property item not found.") + idx = idx[0] + parent = self.indexOfNode(nodeItem.content.subConfig, nodeItem.content.name) + return self.index(idx, 0, parent) + + def propertyRemoved(self, parentItem, propColl, name): # pylint: disable=unused-argument + """ + Slot called when a property has been removed. + :param parentItem: a NodeItem instance + :param propColl: a PropertyCollection instance + :param name: the name of the removed property + :return: + """ + index = self.indexOfProperty(parentItem, name) + self.beginRemoveRows(index.parent(), index.row(), index.row()) + parentItem.children = parentItem.children[:index.row()] + parentItem.children[index.row()+1:] + self.endRemoveRows() + + def propertyChanged(self, item, propColl, name): + """ + Slot called when a property has been changed. + :param item: a PropertyItem instance + :param propColl: a PropertyCollection instance + :param name: the name of the changed property + :return: + """ + index = self.indexOfProperty(item, name) + self.setData(index, propColl.getProperty(name), Qt.DisplayRole) + + def index(self, row, column, parent=QModelIndex()): + """ + Creates a model index according to QAbstractItemModel conventions. + :param row: the row index + :param column: the column index + :param parent: the parent index + :return: + """ + if not self.hasIndex(row, column, parent): + return QModelIndex() + if not parent.isValid(): + parentItem = self.root + else: + parentItem = parent.internalPointer() + try: + child = parentItem.children[row] + except IndexError: + return QModelIndex() + return self.createIndex(row, column, child) + + def parent(self, index): + """ + Returns the indice's parent according to QAbstractItemModel convetions. + :param index: a QModelIndex instance. + :return: + """ + if not index.isValid(): + return QModelIndex() + + child = index.internalPointer() + parentItem = child.parent + if parentItem is self.root: + return QModelIndex() + return self.createIndex(parentItem.row(), 0, parentItem) + + def rowCount(self, parent): + """ + Returns the number of children of the given model index + :param parent: a QModelIndex instance + :return: + """ + if parent.column() > 0: + return 0 + if not parent.isValid(): + parentItem = self.root + else: + parentItem = parent.internalPointer() + return len(parentItem.children) + + def columnCount(self, parent): + """ + Returns the number of columns of the given model index + :param parent: a QModelIndex instance + :return: + """ + if parent.isValid(): + parentItem = parent.internalPointer() + if isinstance(parentItem.content, self.NodeContent): + return 2 # nodes children have the editable properties + return 1 + return 2 + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return ["Name", "Property"][section] + return super().headerData(section, orientation, role) + + def data(self, index, role): # pylint: disable=too-many-return-statements,too-many-branches + """ + Generic data query + :param index: a QModelIndex instance + :param role: the data role (see QAbstractItemModel) + :return: + """ + if not index.isValid(): + return None + item = index.internalPointer().content + if role == Qt.DisplayRole: + if isinstance(item, str): + return item if index.column() == 0 else None + if isinstance(item, self.SubConfigContent): + return item.subConfig.getName() if index.column() == 0 else None + if isinstance(item, self.NodeContent): + return item.name if index.column() == 0 else None + if isinstance(item, self.PropertyContent): + if index.column() == 0: + return item.name + p = item.property.getPropertyDetails(item.name) + return p.converter(item.property.getProperty(item.name)) + logger.warning("Unknown item %s", repr(item)) + if role == Qt.DecorationRole: + if index.column() != 0: + return None + if isinstance(item, str): + if not index.parent().isValid(): + return QApplication.style().standardIcon(QStyle.SP_DriveHDIcon) + if isinstance(item, self.SubConfigContent): + if Configuration.configType(item.subConfig) == Configuration.CONFIG_TYPE_COMPOSITE: + return QApplication.style().standardIcon(QStyle.SP_DirLinkIcon) + if Configuration.configType(item.subConfig) == Configuration.CONFIG_TYPE_APPLICATION: + return QApplication.style().standardIcon(QStyle.SP_DirIcon) + if isinstance(item, self.NodeContent): + return QApplication.style().standardIcon(QStyle.SP_FileIcon) + if isinstance(item, self.PropertyContent): + # TODO + return None + logger.warning("Unknown item %s", repr(item)) + if role == Qt.FontRole: + if index.column() != 0: + return None + if isinstance(item, self.SubConfigContent): + if index.parent().row() == 1: + font = QFont() + if item.subConfig.getName() == self.activeApp: + font.setBold(True) + return font + if role == ITEM_ROLE: + return item + return None + + def flags(self, index): # pylint: disable=too-many-return-statements,too-many-branches + """ + Returns teh item flags of the given index + :param index: a QModelIndex instance + :return: + """ + if not index.isValid(): + return Qt.NoItemFlags + item = index.internalPointer().content + if isinstance(item, str): + return Qt.ItemIsEnabled + if isinstance(item, self.SubConfigContent): + return Qt.ItemIsEnabled | Qt.ItemIsEditable + if isinstance(item, self.NodeContent): + return Qt.ItemIsEnabled | Qt.ItemIsEditable + if isinstance(item, self.PropertyContent): + if index.column() == 0: + return Qt.ItemIsEnabled + return Qt.ItemIsEnabled | Qt.ItemIsEditable + return Qt.ItemIsEnabled + + def setData(self, index, value, role):# pylint: disable=too-many-return-statements,too-many-branches,unused-argument + """ + Generic data modification (see QAbstractItemModel for details) + :param index: a QModelIndex instance + :param value: the new value + :param role: the role to be changed + :return: + """ + if not index.isValid(): + return False + item = index.internalPointer().content + if isinstance(item, str): + return False + if isinstance(item, self.SubConfigContent): + subConfig = item.subConfig + if value == subConfig.getName(): + return False + config = self.root.content + if Configuration.configType(subConfig) == Configuration.CONFIG_TYPE_APPLICATION: + try: + if subConfig.getName() == self.activeApp: + self.activeApp = value + config.renameApp(subConfig.getName(), value) + except NexTRuntimeError: + return False + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + return True + if Configuration.configType(subConfig) == Configuration.CONFIG_TYPE_COMPOSITE: + try: + config.renameComposite(subConfig.getName(), value) + except NexTRuntimeError: + return False + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + return True + if isinstance(item, self.NodeContent): + if item.name == value: + return False + subConfig = index.parent().internalPointer().content.subConfig + graph = subConfig.getGraph() + try: + graph.renameNode(item.name, value) + except NexTRuntimeError: + return False + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + return True + if isinstance(item, self.PropertyContent): + # TODO + try: + item.property.setProperty(item.name, value) + except NexTRuntimeError: + return False + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + return True + return False + + def headerDate(self, section, orientation, role): # pylint: disable=no-self-use + """ + Returns the header data of this model + :param section: section number starting from 0 + :param orientation: orientation + :param role: the role to be returned + :return: + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + if section == 0: + return "Name" + return "Value" + return None + +class MVCConfigurationGUI(QObject): + """ + GUI implementation of MVCConfigurationBase + """ + + activeAppChanged = Signal(str) + + def __init__(self, configuration): + super().__init__() + assertMainThread() + srv = Services.getService("MainWindow") + confMenu = srv.menuBar().addMenu("&Configuration") + toolBar = srv.getToolBar() + + self.actLoad = QAction(QApplication.style().standardIcon(QStyle.SP_DialogOpenButton), "Open", self) + self.actLoad.triggered.connect(lambda *args: self._execLoad(configuration, *args)) + self.actSave = QAction(QApplication.style().standardIcon(QStyle.SP_DialogSaveButton), "Save", self) + self.actSave.triggered.connect(lambda *args: self._execSave(configuration, *args)) + self.actNew = QAction(QApplication.style().standardIcon(QStyle.SP_FileIcon), "New", self) + self.actNew.triggered.connect(lambda *args: self._execNew(configuration, *args)) + + self.cfgfile = None + configuration.configNameChanged.connect(self._configNameChanged) + + self.actActivate = QAction(QApplication.style().standardIcon(QStyle.SP_ArrowUp), "Initialize", self) + self.actActivate.triggered.connect(Application.initialize) + self.actDeactivate = QAction(QApplication.style().standardIcon(QStyle.SP_ArrowDown), "Deinitialize", self) + self.actDeactivate.triggered.connect(Application.deInitialize) + + confMenu.addAction(self.actLoad) + confMenu.addAction(self.actSave) + confMenu.addAction(self.actNew) + confMenu.addAction(self.actActivate) + confMenu.addAction(self.actDeactivate) + toolBar.addAction(self.actLoad) + toolBar.addAction(self.actSave) + toolBar.addAction(self.actNew) + toolBar.addAction(self.actActivate) + toolBar.addAction(self.actDeactivate) + + self.mainWidget = srv.newDockWidget("Configuration", None, Qt.LeftDockWidgetArea) + self.model = ConfigurationModel(configuration, self.mainWidget) + self.treeView = QTreeView(self.mainWidget) + self.treeView.setHeaderHidden(False) + self.treeView.setSelectionMode(QAbstractItemView.NoSelection) + self.treeView.setEditTriggers(self.treeView.EditKeyPressed|self.treeView.AnyKeyPressed) + self.treeView.setAllColumnsShowFocus(True) + self.treeView.setExpandsOnDoubleClick(False) + self.mainWidget.setWidget(self.treeView) + self.treeView.setModel(self.model) + self.treeView.header().setStretchLastSection(True) + self.treeView.header().setSectionResizeMode(0, QHeaderView.ResizeToContents) + self.treeView.doubleClicked.connect(self._onItemDoubleClicked) + self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) + self.treeView.customContextMenuRequested.connect(self._execTreeViewContextMenu) + # expand applications by default + self.treeView.setExpanded(self.model.index(1, 0), True) + self.delegate = PropertyDelegate(self.model, ITEM_ROLE, ConfigurationModel.PropertyContent, self) + self.treeView.setItemDelegate(self.delegate) + + configuration.appActivated.connect(self.appActivated) + self.activeAppChanged.connect(configuration.activate) + + def _execLoad(self, configuration): + assertMainThread() + fn, _ = QFileDialog.getOpenFileName(self.mainWidget, "Load configuration", self.cfgfile, filter="*.json") + if fn is not None and fn != "": + logger.debug("Loading config file %s", fn) + try: + ConfigFileLoader.load(configuration, fn) + except Exception as e: # pylint: disable=broad-except + logger.exception("Error while loading configuration %s: %s", fn, str(e)) + QMessageBox.warning(self.mainWidget, "Error while loading configuration", str(e)) + + @staticmethod + def _execSave(configuration): + assertMainThread() + logger.debug("Saving config file") + ConfigFileLoader.save(configuration) + + def _execNew(self, configuration): + assertMainThread() + fn, _ = QFileDialog.getSaveFileName(self.mainWidget, "Save configuration", filer="*.json") + if fn is not None and fn != "": + logger.debug("Creating config file %s", fn) + configuration.close() + ConfigFileLoader.save(configuration, fn) + + def _execTreeViewContextMenu(self, point): + index = self.treeView.indexAt(point) + item = self.model.data(index, ITEM_ROLE) + if isinstance(item, ConfigurationModel.SubConfigContent): + m = QMenu() + a = QAction("Edit graph ...") + m.addAction(a) + a = m.exec_(self.treeView.mapToGlobal(point)) + if a is not None: + srv = Services.getService("MainWindow") + graphDw = srv.newDockWidget("Graph (%s)" % (item.subConfig.getName()), parent=None, + defaultArea=Qt.RightDockWidgetArea, + allowedArea=Qt.RightDockWidgetArea|Qt.BottomDockWidgetArea) + graphDw.setAttribute(Qt.WA_DeleteOnClose, True) + assert isinstance(graphDw, QDockWidget) + graphView = QGraphicsView(graphDw) + graphView.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) + graphView.setScene(GraphScene(item.subConfig.getGraph(), graphDw)) + graphDw.setWidget(graphView) + + def _configNameChanged(self, cfgfile): + assertMainThread() + self.cfgfile = cfgfile + srv = Services.getService("MainWindow") + if cfgfile is None: + srv.setWindowTitle("nexxT") + else: + srv.setWindowTitle("nexxT: " + cfgfile) + + def _onItemDoubleClicked(self, index): + assertMainThread() + if self.model.isApplication(index): + app = self.model.data(index, Qt.DisplayRole) + self.activeAppChanged.emit(app) + + def appActivated(self, name, app): # pylint: disable=unused-argument + """ + Called when the application is activated. + :param name: the application name + :param app: An ActiveApplication instance. + :return: + """ + assertMainThread() + if app is not None: + self.activeAppStateChange(app.getState()) + app.stateChanged.connect(self.activeAppStateChange) + else: + self.actActivate.setEnabled(False) + self.actDeactivate.setEnabled(False) + + def activeAppStateChange(self, newState): + """ + Called when the active application changes its state. + :param newState: the new application's state (see FilterState) + :return: + """ + assertMainThread() + if newState == FilterState.CONSTRUCTED: + self.actActivate.setEnabled(True) + else: + self.actActivate.setEnabled(False) + if newState == FilterState.ACTIVE: + self.actDeactivate.setEnabled(True) + else: + self.actDeactivate.setEnabled(False) diff --git a/nexxT/services/gui/GraphEditor.py b/nexxT/services/gui/GraphEditor.py new file mode 100644 index 0000000..07bf3b2 --- /dev/null +++ b/nexxT/services/gui/GraphEditor.py @@ -0,0 +1,1080 @@ +# 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 the graph editor GUI service of the nexxT service. +""" + +import platform +import os.path +from PySide2.QtWidgets import (QGraphicsScene, QGraphicsItemGroup, QGraphicsSimpleTextItem, + QGraphicsPathItem, QGraphicsItem, QMenu, QAction, QInputDialog, QMessageBox, + QGraphicsLineItem, QFileDialog) +from PySide2.QtGui import QBrush, QPen, QColor, QPainterPath, QImage +from PySide2.QtCore import QPointF, Signal, QObject, QRectF, QSizeF, Qt +from nexxT.core.BaseGraph import BaseGraph +from nexxT.core.Graph import FilterGraph +from nexxT.core.CompositeFilter import CompositeFilter +from nexxT.core.PluginManager import PluginManager +from nexxT.interface import InputPortInterface, OutputPortInterface +from nexxT.services.gui import GraphLayering + +class MyGraphicsPathItem(QGraphicsPathItem, QObject): + """ + Little subclass for receiving hover events and scene position changes outside the items + """ + hoverEnter = Signal() + hoverLeave = Signal() + scenePosChanged = Signal(QPointF) + + def __init__(self, *args, **kw): + QGraphicsPathItem.__init__(self, *args, **kw) + QObject.__init__(self) + self.setAcceptHoverEvents(True) + + def hoverEnterEvent(self, event): + """ + emit corresponding singal + :param event: the qt event + :return: + """ + self.hoverEnter.emit() + return super().hoverEnterEvent(event) + + def hoverLeaveEvent(self, event): + """ + emit corresponding signal + :param event: the qt event + :return: + """ + self.hoverLeave.emit() + return super().hoverLeaveEvent(event) + + def itemChange(self, change, value): + """ + in case of scene position changes, emit the corresponding signal + :param change: what has changed + :param value: the new value + :return: + """ + if change == QGraphicsItem.ItemScenePositionHasChanged: + self.scenePosChanged.emit(value) + return super().itemChange(change, value) + +class MySimpleTextItem(QGraphicsSimpleTextItem): + """ + QGraphicsSimpleTextItem with a background brush + """ + def __init__(self, *args, **kw): + super().__init__(*args, **kw) + self.brush = QBrush() + + def setBackgroundBrush(self, brush): + """ + set the background brush + :param brush: a QBrush instance + :return: + """ + self.brush = brush + + def paint(self, painter, option, widget): + """ + first paint the background and afterwards use standard paint method + :param painter: a QPainter instance + :param option: unused + :param widget: unused + :return: + """ + b = painter.brush() + p = painter.pen() + painter.setBrush(self.brush) + painter.setPen(QPen(QColor(0, 0, 0, 0))) + painter.drawRect(self.boundingRect()) + painter.setBrush(b) + painter.setPen(p) + super().paint(painter, option, widget) + +class BaseGraphScene(QGraphicsScene): + """ + Basic graph display and manipulation scene. Generic base class intended to be overwritten. + """ + + connectionAddRequest = Signal(str, str, str, str) + + STYLE_ROLE_SIZE = 0 # expects a QSizeF instance + STYLE_ROLE_PEN = 1 # expects a Pen instance + STYLE_ROLE_BRUSH = 2 # expects a Brush instance + STYLE_ROLE_RRRADIUS = 3 # expects a float, the radius of rounded rectangles + STYLE_ROLE_VSPACING = 4 # expects a float + STYLE_ROLE_HSPACING = 5 # expects a float + STYLE_ROLE_TEXT_BRUSH = 6 # expects a brush, will be used as background of fonts + + KEY_ITEM = 0 + + class NodeItem(QGraphicsItemGroup): + """ + An item which represents a node in the graph. The item group is also used for grouping the port items. + """ + @staticmethod + def itemTypeName(): + """ + return a class identification name. + :return: + """ + return "node" + + def __init__(self, name): + super().__init__(None) + self.name = name + self.inPortItems = [] + self.outPortItems = [] + self.setHandlesChildEvents(False) + self.setFlag(QGraphicsItem.ItemClipsToShape, True) + self.setFlag(QGraphicsItem.ItemClipsChildrenToShape, True) + self.hovered = False + self.sync() + + def getInPortItem(self, name): + """ + Searches for the input port named by name + :param name: a string instance + :return: a PortItem instance + """ + found = [i for i in self.inPortItems if i.name == name] + if len(found) == 1: + return found[0] + return None + + def getOutPortItem(self, name): + """ + Searches for the output port named by name + :param name: a string instance + :return: a PortItem instance + """ + found = [i for i in self.outPortItems if i.name == name] + if len(found) == 1: + return found[0] + return None + + def addInPortItem(self, name): + """ + Adds a new input port to the node + :param name: the port name + :return: + """ + assert self.getInPortItem(name) is None + portItem = BaseGraphScene.PortItem(name, self) + self.inPortItems.append(portItem) + self.sync() + + def addOutPortItem(self, name): + """ + Adds a new output port to the node + :param name: the port name + :return: + """ + assert self.getOutPortItem(name) is None + portItem = BaseGraphScene.PortItem(name, self) + self.outPortItems.append(portItem) + self.sync() + + def nodeHeight(self): + style = BaseGraphScene.getData if self.scene() is None else self.scene().getData + size = style(self, BaseGraphScene.STYLE_ROLE_SIZE) + vspacing = style(self, BaseGraphScene.STYLE_ROLE_VSPACING) + inPortHeight = sum([style(ip, BaseGraphScene.STYLE_ROLE_VSPACING) for ip in self.inPortItems]) + outPortHeight = sum([style(op, BaseGraphScene.STYLE_ROLE_VSPACING) for op in self.outPortItems]) + nodeHeight = size.height() + max(inPortHeight, outPortHeight) + return nodeHeight+2*vspacing + + def nodeWidth(self): + style = BaseGraphScene.getData if self.scene() is None else self.scene().getData + size = style(self, BaseGraphScene.STYLE_ROLE_SIZE) + hspacing = style(self, BaseGraphScene.STYLE_ROLE_HSPACING) + return size.width() + 2*hspacing + + def sync(self): + """ + synchronize the item with the model (also the ports) + :return: + """ + self.prepareGeometryChange() + nodePP = QPainterPath() + style = BaseGraphScene.getData if self.scene() is None else self.scene().getData + + size = style(self, BaseGraphScene.STYLE_ROLE_SIZE) + vspacing = style(self, BaseGraphScene.STYLE_ROLE_VSPACING) + hspacing = style(self, BaseGraphScene.STYLE_ROLE_HSPACING) + radius = style(self, BaseGraphScene.STYLE_ROLE_RRRADIUS) + + inPortHeight = sum([style(ip, BaseGraphScene.STYLE_ROLE_VSPACING) for ip in self.inPortItems]) + outPortHeight = sum([style(op, BaseGraphScene.STYLE_ROLE_VSPACING) for op in self.outPortItems]) + nodeHeight = size.height() + max(inPortHeight, outPortHeight) + nodePP.addRoundedRect(hspacing, vspacing, size.width(), nodeHeight, radius, radius) + if not hasattr(self, "nodeGrItem"): + self.nodeGrItem = MyGraphicsPathItem(nodePP, None) + self.nodeTextItem = MySimpleTextItem() + self.nodeGrItem.hoverEnter.connect(self.hoverEnter) + self.nodeGrItem.hoverLeave.connect(self.hoverLeave) + self.nodeGrItem.setData(BaseGraphScene.KEY_ITEM, self) + else: + self.nodeGrItem.prepareGeometryChange() + self.nodeTextItem.prepareGeometryChange() + self.removeFromGroup(self.nodeGrItem) + self.removeFromGroup(self.nodeTextItem) + self.nodeGrItem.setPath(nodePP) + self.nodeGrItem.setPen(style(self, BaseGraphScene.STYLE_ROLE_PEN)) + self.nodeGrItem.setBrush(style(self, BaseGraphScene.STYLE_ROLE_BRUSH)) + self.nodeTextItem.setText(self.name) + self.nodeTextItem.setBackgroundBrush(style(self, BaseGraphScene.STYLE_ROLE_TEXT_BRUSH)) + self.addToGroup(self.nodeGrItem) + self.addToGroup(self.nodeTextItem) + br = self.nodeTextItem.boundingRect() + self.nodeTextItem.setPos(hspacing + size.width()/2 - br.width()/2, + vspacing + nodeHeight/2 - br.height()/2) + + y = vspacing + size.height()/2 + for p in self.inPortItems: + y += style(p, BaseGraphScene.STYLE_ROLE_VSPACING)/2 + p.setPos(hspacing, y, False) + y += style(p, BaseGraphScene.STYLE_ROLE_VSPACING)/2 + p.sync() + + y = vspacing + size.height()/2 + for p in self.outPortItems: + y += style(p, BaseGraphScene.STYLE_ROLE_VSPACING)/2 + p.setPos(hspacing + size.width(), y, True) + y += style(p, BaseGraphScene.STYLE_ROLE_VSPACING)/2 + p.sync() + + def hoverEnter(self): + """ + Slot called on hover enter + :return: + """ + self.hovered = True + self.setFlag(QGraphicsItem.ItemIsMovable, True) + self.sync() + + def hoverLeave(self): + """ + Slot called on hover leave. + :return: + """ + self.hovered = False + self.setFlag(QGraphicsItem.ItemIsMovable, False) + self.sync() + + class PortItem: + """ + This class represents a port in a node. + """ + @staticmethod + def itemTypeName(): + """ + Returns a class identification string. + :return: + """ + return "port" + + def __init__(self, name, nodeItem): + self.name = name + self.nodeItem = nodeItem + self.connections = [] + self.hovered = False + self.isOutput = False + self.sync() + + def setPos(self, x, y, isOutput): # pylint: disable=invalid-name + """ + Sets the position of this item to the given coordinates, assigns output / input property. + :param x: the x coordinate + :param y: the y coordinate + :param isOutput: a boolean + :return: + """ + self.sync() + self.isOutput = isOutput + self.portGrItem.setPos(x, y) + br = self.portTextItem.boundingRect() + if isOutput: + self.portTextItem.setPos(x+3, y - br.height()) + else: + self.portTextItem.setPos(x-3-br.width(), y - br.height()) + + def sync(self): + """ + Synchronizes the item to the model. + :return: + """ + portPP = QPainterPath() + style = BaseGraphScene.getData if self.nodeItem.scene() is None else self.nodeItem.scene().getData + size = style(self, BaseGraphScene.STYLE_ROLE_SIZE) + if self.isOutput: + x = size.width()/2 + else: + x = -size.width()/2 + portPP.addEllipse(QPointF(x, 0), size.width()/2, size.height()/2) + if not hasattr(self, "portGrItem"): + self.portGrItem = MyGraphicsPathItem(None) + self.portTextItem = MySimpleTextItem(self.name, None) + self.portGrItem.hoverEnter.connect(self.hoverEnter) + self.portGrItem.hoverLeave.connect(self.hoverLeave) + self.portGrItem.setData(BaseGraphScene.KEY_ITEM, self) + else: + self.portGrItem.prepareGeometryChange() + self.portTextItem.prepareGeometryChange() + self.nodeItem.removeFromGroup(self.portGrItem) + self.nodeItem.removeFromGroup(self.portTextItem) + self.portGrItem.setPath(portPP) + self.portGrItem.setPen(style(self, BaseGraphScene.STYLE_ROLE_PEN)) + self.portGrItem.setBrush(style(self, BaseGraphScene.STYLE_ROLE_BRUSH)) + self.portGrItem.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True) + self.portGrItem.scenePosChanged.connect(self.scenePosChanged) + self.portGrItem.setZValue(1) + self.nodeItem.addToGroup(self.portGrItem) + self.nodeItem.addToGroup(self.portTextItem) + self.portTextItem.setZValue(1) + self.portTextItem.setBackgroundBrush(style(self, BaseGraphScene.STYLE_ROLE_TEXT_BRUSH)) + self.portTextItem.setText(self.name) + for c in self.connections: + c.sync() + + def hoverEnter(self): + """ + Slot called on hover enter + :return: + """ + self.hovered = True + self.sync() + + def hoverLeave(self): + """ + Slot called on hover leave. + :return: + """ + self.hovered = False + self.sync() + + def scenePosChanged(self, value): # pylint: disable=unused-argument + """ + Slot called on scene position changes, need to synchronize connections. + :param value: + :return: + """ + for c in self.connections: + c.sync() + + def remove(self): + """ + Removes this port from its item group. + :return: + """ + if hasattr(self, "portGrItem"): + self.nodeItem.removeFromGroup(self.portGrItem) + self.nodeItem.removeFromGroup(self.portTextItem) + self.portGrItem.scene().removeItem(self.portGrItem) + self.portTextItem.scene().removeItem(self.portTextItem) + if self in self.nodeItem.outPortItems: + self.nodeItem.outPortItems.remove(self) + if self in self.nodeItem.inPortItems: + self.nodeItem.inPortItems.remove(self) + + class ConnectionItem(QGraphicsPathItem): + """ + This item corresponds with a connection between an output and an input port. + """ + @staticmethod + def itemTypeName(): + """ + Returns an identificytion string. + :return: + """ + return "connection" + + def __init__(self, portFrom, portTo): + super().__init__() + self.portFrom = portFrom + self.portTo = portTo + self.hovered = False + self.setAcceptHoverEvents(True) + self.setData(BaseGraphScene.KEY_ITEM, self) + self.setZValue(-1) + self.sync() + + def sync(self): + """ + Synchronizes the view with the model. + :return: + """ + pp = QPainterPath() + pFrom = self.mapFromScene(self.portFrom.nodeItem.mapToScene(self.portFrom.portGrItem.pos())) + pTo = self.mapFromScene(self.portTo.nodeItem.mapToScene(self.portTo.portGrItem.pos())) + style = BaseGraphScene.getData if self.scene() is None else self.scene().getData + if pTo.x() > pFrom.x(): + # forward connection + pp.moveTo(pFrom) + pp.lineTo(pFrom + QPointF(style(self.portFrom, BaseGraphScene.STYLE_ROLE_HSPACING), 0)) + pp.lineTo(pTo - QPointF(style(self.portTo, BaseGraphScene.STYLE_ROLE_HSPACING), 0)) + pp.lineTo(pTo) + else: + # backward connection + if self.portFrom.nodeItem is self.portTo.nodeItem: + upper = self.portTo.nodeItem.mapToScene(QPointF(0, 0)) + upper -= QPointF(0, style(self.portTo.nodeItem, BaseGraphScene.STYLE_ROLE_VSPACING)/2) + upper = self.mapFromScene(upper) + y = upper.y() + else: + y = pFrom.y()*0.5 + pTo.y()*0.5 + p = pFrom + pp.moveTo(p) + p += QPointF(style(self.portFrom, BaseGraphScene.STYLE_ROLE_HSPACING), 0) + pp.lineTo(p) + p.setY(y) + pp.lineTo(p) + p.setX(pTo.x() - style(self.portTo, BaseGraphScene.STYLE_ROLE_HSPACING)) + pp.lineTo(p) + p.setY(pTo.y()) + pp.lineTo(p) + pp.lineTo(pTo) + self.prepareGeometryChange() + self.setPen(style(self, BaseGraphScene.STYLE_ROLE_PEN)) + self.setPath(pp) + + def hoverEnterEvent(self, event): + """ + override for hover enter events + :param event: the QT event + :return: + """ + self.hovered = True + self.sync() + return super().hoverEnterEvent(event) + + def hoverLeaveEvent(self, event): + """ + override for hover leave events + :param event: the QT event + :return: + """ + self.hovered = False + self.sync() + return super().hoverLeaveEvent(event) + + def shape(self): + """ + Unsure if this is needed, but it may give better hover positions + :return: + """ + return self.path() + + + def __init__(self, parent): + super().__init__(parent) + self.nodes = {} + self.connections = [] + self.itemOfContextMenu = None + self.addingConnection = None + + def addNode(self, name): + """ + Add a named node to the graph + :param name: a string instance + :return: + """ + assert not name in self.nodes + self.nodes[name] = self.NodeItem(name) + self.addItem(self.nodes[name]) + + def renameNode(self, oldName, newName): + """ + Rename a node in the graph + :param oldName: the old name + :param newName: the new name + :return: + """ + ni = self.nodes[oldName] + ni.name = newName + del self.nodes[oldName] + self.nodes[newName] = ni + ni.sync() + + def removeNode(self, name): + """ + Remove a node from the graph + :param name: the node name + :return: + """ + ni = self.nodes[name] + toDel = [] + for c in self.connections: + if c.portFrom.nodeItem is ni or c.portTo.nodeItem is ni: + toDel.append(c) + for c in toDel: + self.removeConnection(c.portFrom.nodeItem.name, c.portFrom.name, + c.portTo.nodeItem.name, c.portTo.name) + del self.nodes[name] + self.removeItem(ni) + + def addInPort(self, node, name): + """ + add an input port to a node + :param node: the node name + :param name: the port name + :return: + """ + nodeItem = self.nodes[node] + nodeItem.addInPortItem(name) + + def renameInPort(self, node, oldName, newName): + """ + Rename an input port from a node + :param node: the node name + :param oldName: the old port name + :param newName: the new port name + :return: + """ + ni = self.nodes[node] + pi = ni.getInPortItem(oldName) + pi.name = newName + ni.sync() + + def renameOutPort(self, node, oldName, newName): + """ + Rename an output port from a node + :param node: the node name + :param oldName: the old port name + :param newName: the new port name + :return: + """ + ni = self.nodes[node] + pi = ni.getOutPortItem(oldName) + pi.name = newName + ni.sync() + + def removeInPort(self, node, name): + """ + Remove an input port from a node + :param node: the node name + :param name: the port name + :return: + """ + ni = self.nodes[node] + pi = ni.getInPortItem(name) + for c in pi.connections: + if c.portTo is pi: + self.removeConnection(c.portFrom.nodeItem.name, c.portFrom.name, + c.portTo.nodeItem.name, c.portTo.name) + pi.remove() + ni.sync() + + def removeOutPort(self, node, name): + """ + Remove an output port from a node + :param node: the node name + :param name: the port name + :return: + """ + ni = self.nodes[node] + pi = ni.getOutPortItem(name) + for c in pi.connections: + if c.portFrom is pi: + self.removeConnection(c.portFrom.nodeItem.name, c.portFrom.name, + c.portTo.nodeItem.name, c.portTo.name) + pi.remove() + ni.sync() + + def addOutPort(self, node, name): + """ + Adds an output port to a node + :param node: the node name + :param name: the port name + :return: + """ + nodeItem = self.nodes[node] + nodeItem.addOutPortItem(name) + + def addConnection(self, nodeFrom, portFrom, nodeTo, portTo): + """ + Add a connection to the graph + :param nodeFrom: the start node's name + :param portFrom: the start node's port + :param nodeTo: the end node's name + :param portTo: the end node's port + :return: + """ + nodeFromItem = self.nodes[nodeFrom] + portFromItem = nodeFromItem.getOutPortItem(portFrom) + nodeToItem = self.nodes[nodeTo] + portToItem = nodeToItem.getInPortItem(portTo) + self.connections.append(self.ConnectionItem(portFromItem, portToItem)) + portFromItem.connections.append(self.connections[-1]) + portToItem.connections.append(self.connections[-1]) + self.addItem(self.connections[-1]) + + def removeConnection(self, nodeFrom, portFrom, nodeTo, portTo): + """ + Removes a connection from the graph + :param nodeFrom: the start node's name + :param portFrom: the start node's port + :param nodeTo: the end node's name + :param portTo: the end node's port + :return: + """ + ni1 = self.nodes[nodeFrom] + pi1 = ni1.getOutPortItem(portFrom) + ni2 = self.nodes[nodeTo] + pi2 = ni2.getInPortItem(portTo) + for ci in [c for c in self.connections if c.portFrom is pi1 and c.portTo is pi2]: + pi1.connections.remove(ci) + pi2.connections.remove(ci) + self.connections.remove(ci) + self.removeItem(ci) + ni1.sync() + ni2.sync() + + @staticmethod + def getData(item, role): + """ + returns render-relevant information about the specified item + can be overriden in concrete editor instances + :param item: an instance of BaseGraphScene.NodeItem, BaseGraphScene.PortItem or BaseGraphScene.ConnectionItem + :param role: one of STYLE_ROLE_SIZE, STYLE_ROLE_PEN, STYLE_ROLE_BRUSH, STYLE_ROLE_RRRADIUS, STYLE_ROLE_VSPACING, + STYLE_ROLE_HSPACING + :return: the expected item related to the role + """ + # pylint: disable=invalid-name + DEFAULTS = { + BaseGraphScene.STYLE_ROLE_HSPACING : 0, + BaseGraphScene.STYLE_ROLE_VSPACING : 0, + BaseGraphScene.STYLE_ROLE_SIZE : QSizeF(), + BaseGraphScene.STYLE_ROLE_RRRADIUS : 0, + BaseGraphScene.STYLE_ROLE_PEN : QPen(), + BaseGraphScene.STYLE_ROLE_BRUSH : QBrush(), + BaseGraphScene.STYLE_ROLE_TEXT_BRUSH : QBrush(), + } + + if isinstance(item, BaseGraphScene.NodeItem): + NODE_STYLE = { + BaseGraphScene.STYLE_ROLE_HSPACING : 50, + BaseGraphScene.STYLE_ROLE_VSPACING : 10, + BaseGraphScene.STYLE_ROLE_SIZE : QSizeF(115, 30), + BaseGraphScene.STYLE_ROLE_RRRADIUS : 4, + BaseGraphScene.STYLE_ROLE_PEN : QPen(QColor(10, 10, 10)), + BaseGraphScene.STYLE_ROLE_BRUSH : QBrush(QColor(10, 200, 10, 180)), + } + NODE_STYLE_HOVERED = { + BaseGraphScene.STYLE_ROLE_PEN : QPen(QColor(10, 10, 10), 3), + } + if item.hovered: + return NODE_STYLE_HOVERED.get(role, NODE_STYLE.get(role, DEFAULTS.get(role))) + return NODE_STYLE.get(role, DEFAULTS.get(role)) + if isinstance(item, BaseGraphScene.PortItem): + def portIdx(portItem): + nodeItem = portItem.nodeItem + if portItem in nodeItem.inPortItems: + return nodeItem.inPortItems.index(portItem) + if portItem in nodeItem.outPortItems: + return len(nodeItem.outPortItems) - 1 - nodeItem.outPortItems.index(portItem) + return 0 + + PORT_STYLE = { + BaseGraphScene.STYLE_ROLE_SIZE : QSizeF(5, 5), + BaseGraphScene.STYLE_ROLE_VSPACING : 20, + BaseGraphScene.STYLE_ROLE_HSPACING : (BaseGraphScene.getData(item.nodeItem, + BaseGraphScene.STYLE_ROLE_HSPACING) + + portIdx(item) * 5), + BaseGraphScene.STYLE_ROLE_PEN : QPen(QColor(10, 10, 10)), + BaseGraphScene.STYLE_ROLE_BRUSH : QBrush(QColor(50, 50, 50, 180)), + } + PORT_STYLE_HOVERED = { + BaseGraphScene.STYLE_ROLE_SIZE : QSizeF(8, 8), + } + if item.hovered: + return PORT_STYLE_HOVERED.get(role, PORT_STYLE.get(role, DEFAULTS.get(role))) + return PORT_STYLE.get(role, DEFAULTS.get(role)) + if isinstance(item, BaseGraphScene.ConnectionItem): + CONN_STYLE = { + BaseGraphScene.STYLE_ROLE_PEN : QPen(QColor(10, 10, 10), 1.5), + } + CONN_STYLE_HOVERED = { + BaseGraphScene.STYLE_ROLE_PEN : QPen(QColor(10, 10, 10), 3), + } + if item.hovered: + return CONN_STYLE_HOVERED.get(role, CONN_STYLE.get(role, DEFAULTS.get(role))) + return CONN_STYLE.get(role, DEFAULTS.get(role)) + # pylint: enable=invalid-name + raise TypeError("Unexpected item.") + + def graphItemAt(self, scenePos): + """ + Returns the graph item at the specified scene position + :param scenePos: a QPoint instance + :return: a NodeItem, PortItem or ConnectionItem instance + """ + gitems = self.items(scenePos) + gitems_relaxed = self.items(QRectF(scenePos - QPointF(2, 2), QSizeF(4, 4))) + for gi in gitems + gitems_relaxed: + item = gi.data(BaseGraphScene.KEY_ITEM) + self.itemOfContextMenu = item + if isinstance(item, (BaseGraphScene.NodeItem, BaseGraphScene.PortItem, BaseGraphScene.ConnectionItem)): + return item + return None + + def mousePressEvent(self, event): + """ + Override from QGraphicsScene (used for dragging connections) + :param event: the QT event + :return: + """ + if event.button() == Qt.LeftButton: + item = self.graphItemAt(event.scenePos()) + if isinstance(item, BaseGraphScene.PortItem): + fromPos = item.portGrItem.scenePos() + lineItem = QGraphicsLineItem(None) + fromPos = lineItem.mapFromScene(fromPos) + lineItem.setLine(fromPos.x(), fromPos.y(), fromPos.x(), fromPos.y()) + lineItem.setPen(QPen(Qt.DotLine)) + self.addItem(lineItem) + self.addingConnection = dict(port=item, lineItem=lineItem) + self.update() + return None + return super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """ + Override from QGraphicsScene (used for dragging connections) + :param event: the QT event + :return: + """ + if event.buttons() & Qt.LeftButton == Qt.LeftButton and self.addingConnection is not None: + lineItem = self.addingConnection["lineItem"] + toPos = lineItem.mapFromScene(event.scenePos()) + lineItem.prepareGeometryChange() + lineItem.setLine(lineItem.line().x1(), lineItem.line().y1(), toPos.x(), toPos.y()) + self.update() + return None + return super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """ + Override from QGraphicsScene (used for dragging connections) + :param event: the QT event + :return: + """ + if event.button() == Qt.LeftButton and self.addingConnection is not None: + portOther = self.addingConnection["port"] + self.removeItem(self.addingConnection["lineItem"]) + self.addingConnection = None + portHere = self.graphItemAt(event.scenePos()) + if portOther.isOutput != portHere.isOutput: + if portOther.isOutput: + portFrom = portOther + portTo = portHere + else: + portFrom = portHere + portTo = portOther + self.connectionAddRequest.emit(portFrom.nodeItem.name, portFrom.name, portTo.nodeItem.name, portTo.name) + return None + return super().mouseReleaseEvent(event) + + def autoLayout(self): + gl = GraphLayering.GraphRep(self) + layers, _ = gl.sortLayers() + layeredNodes = gl.layersToNodeNames(layers) + x = 0 + for col,l in enumerate(layeredNodes): + y = 0 + maxdx = 0 + for row,n in enumerate(l): + self.nodes[n].setPos(x, y) + y += self.nodes[n].nodeHeight() + maxdx = max(maxdx, self.nodes[n].nodeWidth()) + x += maxdx + + +class GraphScene(BaseGraphScene): + """ + Concrete class interacting with a BaseGraph or FilterGraph instance + """ + def __init__(self, graph, parent): + super().__init__(parent) + self.graph = graph + self._threadBrushes = { + "main" : BaseGraphScene.getData(BaseGraphScene.NodeItem(""), BaseGraphScene.STYLE_ROLE_BRUSH), + } + for n in self.graph.allNodes(): + self.addNode(n) + for p in self.graph.allInputPorts(n): + self.addInPort(n, p) + for p in self.graph.allOutputPorts(n): + self.addOutPort(n, p) + for c in self.graph.allConnections(): + self.addConnection(*c) + self.graph.nodeAdded.connect(self.addNode) + self.graph.nodeRenamed.connect(self.renameNode) + self.graph.nodeDeleted.connect(self.removeNode) + self.graph.inPortAdded.connect(self.addInPort) + self.graph.inPortRenamed.connect(self.renameInPort) + self.graph.inPortDeleted.connect(self.removeInPort) + self.graph.outPortAdded.connect(self.addOutPort) + self.graph.outPortRenamed.connect(self.renameOutPort) + self.graph.outPortDeleted.connect(self.removeOutPort) + self.graph.connectionAdded.connect(self.addConnection) + self.graph.connectionDeleted.connect(self.removeConnection) + self.connectionAddRequest.connect(self.graph.addConnection) + + self.itemOfContextMenu = None + + self.actRenameNode = QAction("Rename node ...", self) + self.actRemoveNode = QAction("Remove node ...", self) + self.actAddNode = QAction("Add node ...", self) + self.actAutoLayout = QAction("Auto layourt", self) + self.actRemoveConnection = QAction("Remove connection ...", self) + self.actRenameNode.triggered.connect(self.renameDialog) + self.actRemoveNode.triggered.connect(self.removeDialog) + self.actRemoveConnection.triggered.connect(self.onConnectionRemove) + self.actAutoLayout.triggered.connect(self.autoLayout) + if isinstance(self.graph, FilterGraph): + self.actRenamePort = QAction("Rename dynamic port ...", self) + self.actRemovePort = QAction("Remove dynamic port ...", self) + self.actAddInputPort = QAction("Add dynamic input port ...", self) + self.actAddOutputPort = QAction("Add dynamic output port ...", self) + self.actSetThread = QAction("Set thread ...", self) + self.actAddNode.triggered.connect(self.onAddFilter) + self.actSetThread.triggered.connect(self.setThread) + elif isinstance(self.graph, BaseGraph): + self.actRenamePort = QAction("Rename port ...", self) + self.actRemovePort = QAction("Remove port ...", self) + self.actAddInputPort = QAction("Add input port ...", self) + self.actAddOutputPort = QAction("Add output port ...", self) + self.actAddNode.triggered.connect(self.onAddNode) + self.actRenamePort.triggered.connect(self.renameDialog) + self.actRemovePort.triggered.connect(self.removeDialog) + self.actAddInputPort.triggered.connect(self.addInputPort) + self.actAddOutputPort.triggered.connect(self.addOutputPort) + + def getData(self, item, role): + if isinstance(item, BaseGraphScene.NodeItem) and isinstance(self.graph, FilterGraph): + if role == BaseGraphScene.STYLE_ROLE_BRUSH: + def newColor(): + n = len(self._threadBrushes) + # [0] -> default ~ 120 + if n < 6: + return QColor.fromHsv((120 + n*60) % 360, 200, 255) + if n < 12: + return QColor.fromHsv((30 + (n-6)*60) % 360, 200, 255) + if n < 18: + return QColor.fromHsv((30 + (n-6)*60) % 360, 100, 255) + if n < 24: + return QColor.fromHsv((30 + (n-6)*60) % 360, 100, 255) + return QColor.fromHsv(0, 0, 200) + mockup = self.graph.getMockup(item.name) + threads = tuple(sorted(CompositeFilter.getThreadSet(mockup))) + for t in threads: + if not t in self._threadBrushes: + self._threadBrushes[t] = QBrush(newColor()) + if len(threads) == 1: + return self._threadBrushes[threads[0]] + if threads not in self._threadBrushes: + img = QImage(len(threads)*3, len(threads)*3, QImage.Format_BGR888) + for x in range(img.width()): + for y in range(img.height()): + tidx = ((x + y)//3) % len(threads) + c = self._threadBrushes[threads[tidx]].color() + img.setPixelColor(x, y, c) + self._threadBrushes[threads] = QBrush(img) + return self._threadBrushes[threads] + if role == BaseGraphScene.STYLE_ROLE_TEXT_BRUSH: + mockup = self.graph.getMockup(item.name) + threads = tuple(sorted(CompositeFilter.getThreadSet(mockup))) + if len(threads) > 1: + return QBrush(QColor(255, 255, 255, 200)) + return QBrush(QColor(255, 255, 255, 100)) + return BaseGraphScene.getData(item, role) + + + def contextMenuEvent(self, event): + item = self.graphItemAt(event.scenePos()) + self.itemOfContextMenu = item + if isinstance(item, BaseGraphScene.NodeItem): + m = QMenu(self.views()[0]) + m.addActions([self.actRenameNode, self.actRemoveNode, self.actAddInputPort, self.actAddOutputPort]) + if isinstance(self.graph, FilterGraph): + m.addAction(self.actSetThread) + mockup = self.graph.getMockup(item.name) + din, dout = mockup.getDynamicPortsSupported() + self.actAddInputPort.setEnabled(din) + self.actAddOutputPort.setEnabled(dout) + if (issubclass(mockup.getPluginClass(), CompositeFilter) or + issubclass(mockup.getPluginClass(), CompositeFilter.CompositeNode) or + issubclass(mockup.getPluginClass(), CompositeFilter.CompositeOutputNode) or + issubclass(mockup.getPluginClass(), CompositeFilter.CompositeInputNode)): + self.actSetThread.setEnabled(False) + else: + self.actSetThread.setEnabled(True) + m.exec_(event.screenPos()) + elif isinstance(item, BaseGraphScene.PortItem): + m = QMenu(self.views()[0]) + if isinstance(self.graph, FilterGraph): + mockup = self.graph.getMockup(item.nodeItem.name) + port = mockup.getPort(item.name, OutputPortInterface if item.isOutput else InputPortInterface) + self.actRenamePort.setEnabled(port.dynamic()) + self.actRemovePort.setEnabled(port.dynamic()) + m.addActions([self.actRenamePort, self.actRemovePort]) + m.exec_(event.screenPos()) + elif isinstance(item, BaseGraphScene.ConnectionItem): + m = QMenu(self.views()[0]) + m.addActions([self.actRemoveConnection]) + m.exec_(event.screenPos()) + else: + self.itemOfContextMenu = event.scenePos() + m = QMenu(self.views()[0]) + m.addActions([self.actAddNode, self.actAutoLayout]) + m.exec_(event.screenPos()) + self.itemOfContextMenu = None + + def renameDialog(self): + """ + Opens a dialog for renamign an item (node or port) + :return: + """ + item = self.itemOfContextMenu + newName, ok = QInputDialog.getText(self.views()[0], self.sender().text(), + "Enter new name of " + item.itemTypeName(), + text=self.itemOfContextMenu.name) + if ok and newName != "" and newName is not None: + if isinstance(item, BaseGraphScene.NodeItem): + self.graph.renameNode(item.name, newName) + elif isinstance(item, BaseGraphScene.PortItem): + if item.isOutput: + if isinstance(self.graph, FilterGraph): + self.graph.renameDynamicOutputPort(item.nodeItem.name, item.name, newName) + else: + self.graph.renameOutputPort(item.nodeItem.name, item.name, newName) + else: + if isinstance(self.graph, FilterGraph): + self.graph.renameDynamicInputPort(item.nodeItem.name, item.name, newName) + else: + self.graph.renameInputPort(item.nodeItem.name, item.name, newName) + + def removeDialog(self): + """ + Opens a dialog for removing an item (Node or Port) + :return: + """ + item = self.itemOfContextMenu + btn = QMessageBox.question(self.views()[0], self.sender().text(), + "Do you really want to remove the " + item.itemTypeName() + "?") + if btn == QMessageBox.Yes: + if isinstance(item, BaseGraphScene.NodeItem): + self.graph.deleteNode(item.name) + elif isinstance(item, BaseGraphScene.PortItem): + if item.isOutput: + if isinstance(self.graph, FilterGraph): + self.graph.deleteDynamicOutputPort(item.nodeItem.name, item.name) + else: + self.graph.deleteOutputPort(item.nodeItem.name, item.name) + else: + if isinstance(self.graph, FilterGraph): + self.graph.deleteDynamicInputPort(item.nodeItem.name, item.name) + else: + self.graph.deleteInputPort(item.nodeItem.name, item.name) + + def onConnectionRemove(self): + """ + Removes a connection + :return: + """ + item = self.itemOfContextMenu + self.graph.deleteConnection(item.portFrom.nodeItem.name, item.portFrom.name, + item.portTo.nodeItem.name, item.portTo.name) + + def addInputPort(self): + """ + Adds an input port to a node + :return: + """ + item = self.itemOfContextMenu + if isinstance(item, BaseGraphScene.NodeItem): + newName, ok = QInputDialog.getText(self.views()[0], self.sender().text(), + "Enter name of input port of " + item.itemTypeName()) + if not ok: + return + if isinstance(self.graph, FilterGraph): + self.graph.addDynamicInputPort(item.name, newName) + else: + self.graph.addInputPort(item.name, newName) + + def addOutputPort(self): + """ + Adds an output port to a node + :return: + """ + item = self.itemOfContextMenu + if isinstance(item, BaseGraphScene.NodeItem): + newName, ok = QInputDialog.getText(self.views()[0], self.sender().text(), + "Enter name of output port of " + item.itemTypeName()) + if not ok: + return + if isinstance(self.graph, FilterGraph): + self.graph.addDynamicOutputPort(item.name, newName) + else: + self.graph.addOutputPort(item.name, newName) + + def setThread(self): + """ + Opens a dialog to enter the new thread of the node. + :return: + """ + item = self.itemOfContextMenu + newThread, ok = QInputDialog.getText(self.views()[0], self.sender().text(), + "Enter name of new thread of " + item.name) + if not ok or newThread is None or newThread == "": + return + mockup = self.graph.getMockup(item.name) + pc = mockup.propertyCollection().getChildCollection("_nexxT") + pc.setProperty("thread", newThread) + item.sync() + + def onAddNode(self): + """ + Called when the user wants to add a new node. (Generic variant) + :return: + """ + newName, ok = QInputDialog.getText(self.views()[0], self.sender().text(), + "Enter name of new node") + if ok: + self.graph.addNode(newName) + self.nodes[newName].setPos(self.itemOfContextMenu) + + def onAddFilter(self): + """ + Called when the user wants to add a new filter (FilterGraph variant). Opens a dialog to select files. + :return: + """ + pm = PluginManager.singleton() + if platform.system().lower() == "linux": + suff = "*.so" + else: + suff = "*.dll" + library, ok = QFileDialog.getOpenFileName(self.views()[0], "Choose Library", + filter="Python Files (*.py);;Binary Files (%s)" % suff) + if not (ok and library is not None and os.path.exists(library)): + return + if library.endswith(".py"): + library = "pyfile://" + library + else: + library = "binary://" + library + filters = pm.getFactoryFunctions(library) + if len(filters) > 0: + factory, ok = QInputDialog.getItem(self.views()[0], "Choose filter", "Choose filter", filters) + if not ok or not factory in filters: + return + else: + factory = filters[0] + name = self.graph.addNode(library, factory) + self.nodes[name].setPos(self.itemOfContextMenu) diff --git a/nexxT/services/gui/GraphLayering.py b/nexxT/services/gui/GraphLayering.py new file mode 100644 index 0000000..be52244 --- /dev/null +++ b/nexxT/services/gui/GraphLayering.py @@ -0,0 +1,209 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from collections import deque + +class GraphRep: + def __init__(self, baseGraphScene = None): + self.id2name = {} + self.name2id = {} + self.dgForward = {} # mapping id's to successor sets + self.dgBackward = {} # mapping id's to predessor sets + self.cycleEdges = set() + self.longEdges = set() + if baseGraphScene is not None: + for n in baseGraphScene.nodes.keys(): + self.addNode(n) + for c in baseGraphScene.connections: + self.addEdge(c.portFrom.nodeItem.name, c.portTo.nodeItem.name) + + def addNode(self, n): + i = len(self.name2id) + self.id2name[i] = n + self.name2id[n] = i + self.dgForward[i] = set() + self.dgBackward[i] = set() + self.n = i+1 + self.vn = self.n + + def addEdge(self, n1, n2): + fromId = self.name2id[n1] + toId = self.name2id[n2] + if toId in self.dgForward[fromId]: + return + self.dgForward[fromId].add(toId) + self.dgBackward[toId].add(fromId) + + def dump(self, title = None): + if title is not None: print(title) + for n1 in self.dgForward: + print(n1, end=": ") + for n2 in self.dgForward[n1]: + if (n1,n2) not in self.cycleEdges: + print(n2, end=",") + print() + print() + + def topological_sort(self): + self.dump("original:") + permanent = [False] * self.n + temporary = [False] * self.n + self.cycleEdges = set() + + result = deque() + def visit(cId, pId): + if permanent[cId]: + return + if temporary[cId]: + # not a DAG, but we just continue as if the edge + # does not exist + self.cycleEdges.add((pId, cId)) + return + temporary[cId] = True + for nId in self.dgForward[cId]: + visit(nId, cId) + temporary[cId] = False + permanent[cId] = True + result.appendleft(cId) + while 1: + found = False + for cId in range(self.n): + if not permanent[cId]: + found = True + visit(cId, None) + if not found: + break + self.dump("after removing cycles:") + return list(result) + + def assignLayers(self): + topsorted = self.topological_sort() + node2layer = [None]*self.n + # the layer index is the shortest path to one of the input nodes + for cId in topsorted: + l = None + for pId in self.dgBackward[cId]: + if (pId, cId) in self.cycleEdges: + continue + assert node2layer[pId] is not None + if l is None: + l = node2layer[pId] + 1 + l = max(l, node2layer[pId] + 1) + node2layer[cId] = l if l is not None else 0 + layers = [] + for l in range(max(node2layer) + 1): + layers.append([idx for idx in range(self.n) if node2layer[idx] == l]) + return layers, node2layer + + def sortLayers(self): + def numberOfCrossings(layer1, layer2): + res = 0 + for i,ni in enumerate(layer1): + for nj in self.dgForward[ni]: + if (ni,nj) in self.cycleEdges or (ni,nj) in self.longEdges: + continue + j = layer2.index(nj) + for k, nk in enumerate(layer2): + for nh in self.dgBackward[nk]: + if (nh,nk) in self.cycleEdges or (nh,nk) in self.longEdges: + continue + h = layer1.index(nh) + if (h < i and k > j) or (h > i and k < j): + res += 1 + return res + + layers, node2layer = self.assignLayers() + self.longEdges = set() + # add virtual nodes for edges which span multiple layers + for n1 in list(self.dgForward.keys()): + for n2 in self.dgForward[n1].copy(): + if (n1,n2) in self.cycleEdges: + continue + if node2layer[n2] != node2layer[n1]+1: + assert node2layer[n2] > node2layer[n1]+1 + self.longEdges.add((n1, n2)) + nc = n1 + for l in range(node2layer[n1]+1, node2layer[n2]): + n = self.vn + self.vn += 1 + node2layer.append(l) + layers[l].append(n) + self.dgForward[nc].add(n) + self.dgBackward[n] = set() + self.dgBackward[n].add(nc) + self.dgForward[n] = set() + nc = n + self.dgForward[nc].add(n2) + self.dgBackward[n2].add(nc) + self.dump("after adding virtual nodes") + nc = sum([numberOfCrossings(layers[i-1], layers[i]) for i in range(1, len(layers))]) + print("numCrosses before heuristic:", nc) + # heuristic for rearranging the layer according to the average position of previous nodes + numCrosses = 0 + for cl in range(1, len(layers)): + averagePrevPos = [] + for cn in layers[cl]: + prevPos = [] + for pn in self.dgBackward[cn]: + if (pn, cn) in self.cycleEdges or (pn,cn) in self.longEdges: + continue + prevPos.append(layers[cl-1].index(pn)) + averagePrevPos.append(sum(prevPos)/len(prevPos)) + initial_perm = sorted(list(range(len(layers[cl]))), key=lambda x: averagePrevPos[x]) + layers[cl] = [layers[cl][i] for i in initial_perm] + numCrosses += numberOfCrossings(layers[cl-1], layers[cl]) + print("numCrosses after heuristic: ", numCrosses) + # swap pairs until convergence + for cl in range(len(layers)): + def getNumCrosses(cLayer): + return (numberOfCrossings(layers[cl-1], cLayer) if cl > 0 else 0 + + numberOfCrossings(cLayer, layers[cl+1]) if cl < len(layers) - 1 else 0) + while 1: + numCrosses = getNumCrosses(layers[cl]) + found = False + for i in range(len(layers[cl])-1): + testL = layers[cl][:i] + [layers[cl][i+1],layers[cl][i]] + layers[cl][i+2:] + testCrosses = getNumCrosses(testL) + if testCrosses < numCrosses: + found = True + numCrosses = testCrosses + layers[cl] = testL + if not found: + break + numCrosses = sum([numberOfCrossings(layers[i-1], layers[i]) for i in range(1, len(layers))]) + return layers, numCrosses + + def layersToNodeNames(self, layers): + res = [] + for l in layers: + lr = [] + for n in l: + if n in self.id2name: + lr.append(self.id2name[n]) + res.append(lr) + return res + +if __name__ == "__main__": + import random + import time + + t0 = time.time() + random.seed(0) + gr = GraphRep() + numNodes = 15 + maxNumEdges = 3 + minNumEdges = 1 + for i in range(numNodes): + gr.addNode(i) + for i in range(numNodes): + for k in range(random.randint(minNumEdges, maxNumEdges)): + j = random.randint(0, numNodes-1) + gr.addEdge(i,j) + layers, numCrosses = gr.sortLayers() + for l in layers: + print(l) + print("numCrosses", numCrosses) + print("time spent: %.3s" % (time.time() - t0)) \ No newline at end of file diff --git a/nexxT/services/gui/MainWindow.py b/nexxT/services/gui/MainWindow.py new file mode 100644 index 0000000..ced0d01 --- /dev/null +++ b/nexxT/services/gui/MainWindow.py @@ -0,0 +1,380 @@ +# 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 shiboken2 +from PySide2.QtWidgets import QMainWindow, QMdiArea, QMdiSubWindow, QDockWidget, QAction, QWidget, QGridLayout +from PySide2.QtCore import QObject, Signal, Slot, Qt, QByteArray, QDataStream, QIODevice, QRect, QPoint, QSettings +from nexxT.interface import Filter +from nexxT.core.Application import Application + +logger = logging.getLogger(__name__) + +class NexxTMdiSubWindow(QMdiSubWindow): + """ + Need subclassing for getting close / show events and saving / restoring state. + """ + visibleChanged = Signal(bool) + + def closeEvent(self, closeEvent): + """ + override from QMdiSubWindow + :param closeEvent: a QCloseEvent instance + :return: + """ + self.visibleChanged.emit(False) + super().closeEvent(closeEvent) + + def showEvent(self, showEvent): + """ + override from QMdiSubWindow + :param closeEvent: a QShowEvent instance + :return: + """ + self.visibleChanged.emit(True) + super().showEvent(showEvent) + + 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(self.windowState() & Qt.WindowMaximized) + stream.writeUInt32(self.windowState() & Qt.WindowFullScreen) + return array + + 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 = stream.readUInt32() + fullScreen = stream.readUInt32() + frameHeight = 20 + 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 + +class NexxTDockWidget(QDockWidget): + """ + Need subclassing for getting close / show events + """ + visibleChanged = Signal(bool) + + def closeEvent(self, closeEvent): + """ + override from QMdiSubWindow + :param closeEvent: a QCloseEvent instance + :return: + """ + self.visibleChanged.emit(False) + super().closeEvent(closeEvent) + + def showEvent(self, showEvent): + """ + override from QMdiSubWindow + :param closeEvent: a QShowEvent instance + :return: + """ + self.visibleChanged.emit(True) + super().showEvent(showEvent) + +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: remove, is not necessary anymore with subplot feature + + 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.setCentralWidget(self.mdi) + self.menu = self.menuBar().addMenu("Windows") + self.toolbar = None + self.managedMdiWindows = [] + self.managedSubplots = {} + self.windows = {} + self.activeApp = None + + def closeEvent(self, closeEvent): + """ + Override from QMainWindow, saves the state. + :param closeEvent: a QCloseEvent instance + :return: + """ + self.saveState() + self.saveMdiState() + return super().closeEvent(closeEvent) + + def restoreState(self): + """ + restores the state of the main window including the dock windows of Services + :return: + """ + logger.info("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: + # TODO: add toolbar to windows menu, so we don't need this + self.toolbar.show() + + def saveState(self): + """ + saves the state of the main window including the dock windows of Services + :return: + """ + logger.info("saving main window's state") + settings = QSettings() + settings.setValue("MainWindowState", super().saveState()) + settings.setValue("MainWindowGeometry", self.saveGeometry()) + + 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[shiboken2.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 = [] + + def __del__(self): + logging.getLogger(__name__).debug("deleting MainWindow") + + @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 + + @Slot(str, QObject, int, int) + def newDockWidget(self, name, parent, defaultArea, allowedArea=Qt.LeftDockWidgetArea|Qt.BottomDockWidgetArea): + """ + 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) + res.setAllowedAreas(allowedArea) + res.setAttribute(Qt.WA_DeleteOnClose, False) + self.addDockWidget(defaultArea, res) + self._registerWindow(res, res.objectNameChanged) + res.setObjectName(name) + return res + + @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 + + @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 "[,]" where is the caption + of the MDI window (and it is used as identifier for saving/restoring window state) and + , 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 + """ + title, row, col = self.parseWindowId(windowId) + if not title in self.managedSubplots: + subWindow = self._newMdiSubWindow(theFilter, title) + swwidget = QWidget() + subWindow.setWidget(swwidget) + layout = QGridLayout(swwidget) + self.managedSubplots[title] = dict(mdiSubWindow=subWindow, layout=layout, swwidget=swwidget, plots={}) + self.managedSubplots[title]["layout"].addWidget(widget, row, col) + widget.setParent(self.managedSubplots[title]["swwidget"]) + self.managedSubplots[title]["plots"][row, col] = widget + + @Slot(str) + def releaseSubplot(self, windowId): + """ + 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 windowId: see subplot(...) for details. + :return: + """ + 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 + self.managedSubplots[title]["layout"].removeWidget(self.managedSubplots[title]["plots"][row, col]) + 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] + + @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("" 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("", self) + act.setCheckable(True) + act.toggled.connect(window.setVisible) + window.visibleChanged.connect(act.setChecked) + nameChangedSignal.connect(act.setText) + self.windows[shiboken2.getCppPointer(window)[0]] = act # pylint: disable=no-member + self.menu.addAction(act) + logger.debug("Registering window %s, new len=%d", + shiboken2.getCppPointer(window), len(self.windows)) # pylint: disable=no-member + window.destroyed.connect(self._windowDestroyed) + + def _windowDestroyed(self, obj): + ptr = shiboken2.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.aboutToStop.connect(self.saveMdiState, Qt.UniqueConnection) + else: + self.activeApp = None diff --git a/nexxT/services/gui/PlaybackControl.py b/nexxT/services/gui/PlaybackControl.py new file mode 100644 index 0000000..124fa0b --- /dev/null +++ b/nexxT/services/gui/PlaybackControl.py @@ -0,0 +1,631 @@ +# 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 the playback control GUI service for the nexxT framework. +""" + +import functools +import pathlib +import logging +import os +from PySide2.QtCore import QObject, Signal, Slot, QDateTime, Qt, QDir, QTimer, QMutex, QMutexLocker +from PySide2.QtWidgets import (QWidget, QGridLayout, QLabel, QBoxLayout, QSlider, QToolBar, QAction, QApplication, + QStyle, QLineEdit, QFileSystemModel, QTreeView, QHeaderView) +from nexxT.interface import Services +from nexxT.core.Exceptions import NexTRuntimeError, PropertyCollectionPropertyNotFound +from nexxT.core.Utils import FileSystemModelSortProxy, assertMainThread, MethodInvoker + +logger = logging.getLogger(__name__) + +class MVCPlaybackControlBase(QObject): + """ + Base class for interacting with playback controller, usually this is connected to a + harddisk player. + """ + startPlayback = Signal() + pausePlayback = Signal() + stepForward = Signal() + stepBackward = Signal() + seekBeginning = Signal() + seekEnd = Signal() + seekTime = Signal(QDateTime) + setSequence = Signal(str) + setTimeFactor = Signal(float) + + def __init__(self): + super().__init__() + self._deviceId = 0 + self._registeredDevices = {} + self._mutex = QMutex() + + @Slot(QObject, "QStringList") + def setupConnections(self, playbackDevice, nameFilters): + """ + Sets up signal/slot connections between this view/controller instance and the given playbackDevice. This + function is thread safe and shall be called by a direct QT connection. + It is intended, that this function is called in the onStart(...) method of a filter. It expects playbackDevice + to provide the following slots: + + - startPlayback() (starts generating DataSamples) + - pausePlayback() (pause mode, stop generating DataSamples) + - stepForward(QString stream) (optional; in case given, a single step operation shall be performed. + if stream is not None, the playback shall stop when receiving the next data sample + of stream; otherwise the playback shall proceed to the next data sample of any stream) + - stepBackward(QString stream) (optional; see stepForward) + - seekBeginning(QString stream) (optional; goes to the beginning of the sequence) + - seekEnd() (optional; goes to the end of the stream) + - seekTime(QString QDateTime) (optional; goes to the specified time stamp) + - setSequence(QString) (optional; opens the given sequence) + - setTimeFactor(float) (optional; sets the playback time factor, factor > 0) + + It expects playbackDevice to provide the following signals (all signals are optional): + + - sequenceOpened(QString filename, QDateTime begin, QDateTime end, QStringList streams) + - currentTimestampChanged(QDateTime currentTime) + - playbackStarted() + - playbackPaused() + - timeRatioChanged() + + :param playbackDevice: a QObject providing the aforementioned signals and slots + :param nameFilters: a QStringList providing information about suported fileextensions (e.g. ["*.avi", "*.mp4"]) + :return: + """ + with QMutexLocker(self._mutex) as locker: + for devid in self._registeredDevices: + if self._registeredDevices[devid]["object"] is playbackDevice: + raise NexTRuntimeError("Trying to register a playbackDevice object twice.") + + if not self.startPlayback.connect(playbackDevice.startPlayback): + raise NexTRuntimeError("cannot connect to slot startPlayback()") + if not self.pausePlayback.connect(playbackDevice.pausePlayback): + raise NexTRuntimeError("cannot connect to slot pausePlayback()") + + connections = [(self.startPlayback, playbackDevice.startPlayback), + (self.pausePlayback, playbackDevice.pausePlayback)] + featureset = set(["startPlayback", "pausePlayback"]) + for feature in ["stepForward", "stepBackward", "seekTime", "seekBeginning", "seekEnd", + "setTimeFactor"]: + signal = getattr(self, feature) + slot = getattr(playbackDevice, feature, None) + if slot is not None and signal.connect(slot, Qt.UniqueConnection): + featureset.add(feature) + connections.append((signal, slot)) + + #@Slot(str) + def setSequenceWrapper(filename): + if QDir.match(nameFilters, pathlib.Path(filename).name): + logger.debug("setSequence %s", filename) + setSequenceWrapper.invoke = MethodInvoker(playbackDevice.setSequence, Qt.QueuedConnection, filename) + else: + logger.debug("%s does not match filters: %s", filename, nameFilters) + + # setSequence is called only if filename matches the given filters + if self.setSequence.connect(setSequenceWrapper): + featureset.add("setSequence") + connections.append((self.setSequence, setSequenceWrapper)) + for feature in ["sequenceOpened", "currentTimestampChanged", "playbackStarted", "playbackPaused", + "timeRatioChanged"]: + slot = getattr(self, feature) + signal = getattr(playbackDevice, feature, None) + if signal is not None and signal.connect(slot, Qt.UniqueConnection): + featureset.add(feature) + connections.append((signal, slot)) + + self._registeredDevices[self._deviceId] = dict(object=playbackDevice, + featureset=featureset, + nameFilters=nameFilters, + connections=connections) + self._deviceId += 1 + self._updateFeatureSet() + + @Slot(QObject) + def removeConnections(self, playbackDevice): + """ + unregisters the given playbackDevice and disconnects all. It is intended that this function is called in the + onStop(...) method of a filter. + + :param playbackDevice: the playback device to be unregistered. + :return: None + """ + with QMutexLocker(self._mutex): + found = [] + for devid in self._registeredDevices: + if self._registeredDevices[devid]["object"] is playbackDevice: + found.append(devid) + if len(found) > 0: + for devid in found: + for signal, slot in self._registeredDevices[devid]["connections"]: + signal.disconnect(slot) + del self._registeredDevices[devid] + logger.debug("disconnected connections of playback device. number of devices left: %d", + len(self._registeredDevices)) + self._updateFeatureSet() + + def _updateFeatureSet(self): + featureset = set() + nameFilters = set() + featureCount = {} + for devid in self._registeredDevices: + for f in self._registeredDevices[devid]["featureset"]: + if not f in featureCount: + featureCount[f] = 0 + featureCount[f] += 1 + featureset = featureset.union(self._registeredDevices[devid]["featureset"]) + nameFilters = nameFilters.union(self._registeredDevices[devid]["nameFilters"]) + for f in featureCount: + if featureCount[f] > 1 and f in ["seekTime", "setSequence", "setTimeFactor"]: + logger.warning("Multiple playback devices are providing slots intended for single usage." + "Continuing anyways.") + self.supportedFeaturesChanged(featureset, nameFilters) + + def supportedFeaturesChanged(self, featureset, nameFilters): + """ + Can be overriden to get the supported features of the connected playbackDevice(s). This function is called + from multiple threads, but not at the same time. + :param featureset set of supported features + :param nameFilters set of supported nameFilters + :return: + """ + + def sequenceOpened(self, filename, begin, end, streams): + """ + Notifies about an opened sequence. + :param filename: the filename which has been opened + :param begin: timestamp of sequence's first sample + :param end: timestamp of sequence's last sample + :param streams: list of streams in the sequence + :return: None + """ + + def currentTimestampChanged(self, currentTime): + """ + Notifies about a changed timestamp + :param currentTime: the new current timestamp + :return: None + """ + + def playbackStarted(self): + """ + Notifies about starting playback + :return: None + """ + + def playbackPaused(self): + """ + Notifies about pause playback + :return: None + """ + + def timeRatioChanged(self, newRatio): + """ + Notifies about a changed playback time ratio, + :param newRatio the new playback ratio as a float + :return: None + """ + +class MVCPlaybackControlGUI(MVCPlaybackControlBase): + """ + GUI implementation of MVCPlaybackControlBase + """ + nameFiltersChanged = Signal("QStringList") + + def __init__(self, config): + assertMainThread() + super().__init__() + + # state + self.preventSeek = False + self.beginTime = None + self.timeRatio = 1.0 + + # gui + srv = Services.getService("MainWindow") + config.configLoaded.connect(self.restoreState) + config.configAboutToSave.connect(self.saveState) + self.config = config + playbackMenu = srv.menuBar().addMenu("&Playback") + + self.actStart = QAction(QApplication.style().standardIcon(QStyle.SP_MediaPlay), "Start Playback", self) + self.actPause = QAction(QApplication.style().standardIcon(QStyle.SP_MediaPause), "Pause Playback", self) + self.actPause.setEnabled(False) + self.actStepFwd = QAction(QApplication.style().standardIcon(QStyle.SP_MediaSeekForward), "Step Forward", self) + self.actStepBwd = QAction(QApplication.style().standardIcon(QStyle.SP_MediaSeekBackward), "Step Backward", self) + self.actSeekEnd = QAction(QApplication.style().standardIcon(QStyle.SP_MediaSkipForward), "Seek End", self) + self.actSeekBegin = QAction(QApplication.style().standardIcon(QStyle.SP_MediaSkipBackward), "Seek Begin", self) + self.actSetTimeFactor = {r : QAction("x 1/%d" % (1/r), self) if r < 1 else QAction("x %d" % r, self) + for r in (1/8, 1/4, 1/2, 1, 2, 4, 8)} + + # pylint: disable=unnecessary-lambda + # let's stay on the safe side and do not use emit as a slot... + self.actStart.triggered.connect(lambda: self.startPlayback.emit()) + self.actPause.triggered.connect(lambda: self.pausePlayback.emit()) + self.actStepFwd.triggered.connect(lambda: self.stepForward.emit()) + self.actStepBwd.triggered.connect(lambda: self.stepBackward.emit()) + self.actSeekEnd.triggered.connect(lambda: self.seekEnd.emit()) + self.actSeekBegin.triggered.connect(lambda: self.seekBeginning.emit()) + # pylint: enable=unnecessary-lambda + + def setTimeFactor(newFactor): + logger.debug("new time factor %f", newFactor) + self.setTimeFactor.emit(newFactor) + + for r in self.actSetTimeFactor: + logger.debug("adding action for time factor %f", r) + self.actSetTimeFactor[r].triggered.connect(functools.partial(setTimeFactor, r)) + + self.dockWidget = srv.newDockWidget("PlaybackControl", None, Qt.LeftDockWidgetArea) + self.dockWidgetContents = QWidget(self.dockWidget) + self.dockWidget.setWidget(self.dockWidgetContents) + toolLayout = QBoxLayout(QBoxLayout.TopToBottom, self.dockWidgetContents) + toolLayout.setContentsMargins(0, 0, 0, 0) + toolBar = QToolBar() + toolLayout.addWidget(toolBar) + toolBar.addAction(self.actSeekBegin) + toolBar.addAction(self.actStepBwd) + toolBar.addAction(self.actStart) + toolBar.addAction(self.actPause) + toolBar.addAction(self.actStepFwd) + toolBar.addAction(self.actSeekEnd) + playbackMenu.addAction(self.actSeekBegin) + playbackMenu.addAction(self.actStepBwd) + playbackMenu.addAction(self.actStart) + playbackMenu.addAction(self.actPause) + playbackMenu.addAction(self.actStepFwd) + playbackMenu.addAction(self.actSeekEnd) + playbackMenu.addSeparator() + for r in self.actSetTimeFactor: + playbackMenu.addAction(self.actSetTimeFactor[r]) + self.timeRatioLabel = QLabel("x 1") + self.timeRatioLabel.addActions(list(self.actSetTimeFactor.values())) + self.timeRatioLabel.setContextMenuPolicy(Qt.ActionsContextMenu) + toolBar.addSeparator() + toolBar.addWidget(self.timeRatioLabel) + contentsLayout = QGridLayout() + toolLayout.addLayout(contentsLayout, 10) + # now we add a position view + self.positionSlider = QSlider(Qt.Horizontal, self.dockWidgetContents) + self.beginLabel = QLabel(parent=self.dockWidgetContents) + self.beginLabel.setAlignment(Qt.AlignLeft|Qt.AlignCenter) + self.currentLabel = QLabel(parent=self.dockWidgetContents) + self.currentLabel.setAlignment(Qt.AlignHCenter|Qt.AlignCenter) + self.endLabel = QLabel(parent=self.dockWidgetContents) + self.endLabel.setAlignment(Qt.AlignRight|Qt.AlignCenter) + contentsLayout.addWidget(self.beginLabel, 0, 0, alignment=Qt.AlignLeft) + contentsLayout.addWidget(self.currentLabel, 0, 1, alignment=Qt.AlignHCenter) + contentsLayout.addWidget(self.endLabel, 0, 2, alignment=Qt.AlignRight) + contentsLayout.addWidget(self.positionSlider, 1, 0, 1, 3) + self.filenameLabel = QLineEdit(parent=self.dockWidgetContents) + self.filenameLabel.setReadOnly(True) + self.filenameLabel.setFrame(False) + self.filenameLabel.setStyleSheet("* { background-color: rgba(0, 0, 0, 0); }") + contentsLayout.addWidget(self.filenameLabel, 2, 0, 1, 3) + self.positionSlider.setTracking(False) + self.positionSlider.valueChanged.connect(self.onSliderValueChanged, Qt.DirectConnection) + self.positionSlider.sliderMoved.connect(self.displayPosition) + + # file browser + self.fileSystemModel = QFileSystemModel() + self.fileSystemModel.setNameFilterDisables(False) + self.fileSystemModel.setRootPath("/") + self.nameFiltersChanged.connect(lambda nameFilt: (getattr(self, "fileSystemModel").setNameFilters(nameFilt), + self.refreshBrowser() + ), Qt.QueuedConnection) + + self.browser = QTreeView(parent=self.dockWidgetContents) + self.useProxy = True + if self.useProxy: + self.proxyFileSystemModel = FileSystemModelSortProxy(self) + self.proxyFileSystemModel.setSourceModel(self.fileSystemModel) + self.browser.setModel(self.proxyFileSystemModel) + else: + self.browser.setModel(self.fileSystemModel) + self.browser.setSortingEnabled(True) + self.browser.sortByColumn(0, Qt.SortOrder.AscendingOrder) + self.browser.setUniformRowHeights(True) + self.browser.header().setSectionResizeMode(0, QHeaderView.Interactive) + self.browser.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) + self.browser.header().setSectionResizeMode(2, QHeaderView.ResizeToContents) + self.browser.header().setSectionResizeMode(3, QHeaderView.ResizeToContents) + self.browser.header().resizeSection(0, 500) + contentsLayout.addWidget(self.browser, 3, 0, 1, 3) + contentsLayout.setRowStretch(3, 100) + #self.browser.doubleClicked.connect(self.browserCurrentChanged) + self.browser.selectionModel().currentChanged.connect(self.browserCurrentChanged) + + self.actShowAllFiles = QAction("Show all files") + self.actShowAllFiles.setCheckable(True) + self.actShowAllFiles.setChecked(False) + self.actShowAllFiles.toggled.connect(lambda on: (getattr(self, "fileSystemModel").setNameFilterDisables(on), + self.refreshBrowser())) + self.actRefreshBrowser = QAction("Refresh browser") + self.actRefreshBrowser.triggered.connect(self.refreshBrowser) + playbackMenu.addSeparator() + playbackMenu.addAction(self.actShowAllFiles) + playbackMenu.addAction(self.actRefreshBrowser) + + self.recentSeqs = [QAction() for i in range(10)] + playbackMenu.addSeparator() + recentMenu = playbackMenu.addMenu("Recent") + for a in self.recentSeqs: + a.setVisible(False) + a.triggered.connect(self.openRecent) + recentMenu.addAction(a) + + self.supportedFeaturesChanged(set(), set()) + + def __del__(self): + logger.debug("deleting playback control") + + def supportedFeaturesChanged(self, featureset, nameFilters): + """ + overwritten from MVCPlaybackControlBase. This function is called + from multiple threads, but not at the same time. + :param featureset: the current featureset + :return: + """ + self.featureset = featureset + self.actStepFwd.setEnabled("stepForward" in featureset) + self.actStepBwd.setEnabled("stepBackward" in featureset) + self.actSeekBegin.setEnabled("seekBeginning" in featureset) + self.actSeekEnd.setEnabled("seekEnd" in featureset) + self.positionSlider.setEnabled("seekTime" in featureset) + self.browser.setEnabled("setSequence" in featureset) + self.timeRatioLabel.setEnabled("setTimeFactor" in featureset) + for f in self.actSetTimeFactor: + self.actSetTimeFactor[f].setEnabled("setTimeFactor" in featureset) + self.timeRatioLabel.setEnabled("setTimeFactor" in featureset) + self.timeRatioLabel.setEnabled("setTimeFactor" in featureset) + self.timeRatioLabel.setEnabled("setTimeFactor" in featureset) + self.timeRatioLabel.setEnabled("setTimeFactor" in featureset) + if "startPlayback" not in featureset: + self.actStart.setEnabled(False) + if "pausePlayback" not in featureset: + self.actPause.setEnabled(False) + logger.info("current feature set: %s", featureset) + logger.debug("Setting name filters of browser: %s", list(nameFilters)) + self.nameFiltersChanged.emit(list(nameFilters)) + + def refreshBrowser(self): + """ + It looks like QFileSystemModel is rather broken when it comes to automatically refresh + This is a workaround... + :return: + """ + assertMainThread() + newModel = QFileSystemModel() + newModel.setRootPath("/") + logger.debug("setNameFilterDisables %d", self.fileSystemModel.nameFilterDisables()) + newModel.setNameFilterDisables(self.fileSystemModel.nameFilterDisables()) + logger.debug("setNameFilters %s", self.fileSystemModel.nameFilters()) + newModel.setNameFilters(self.fileSystemModel.nameFilters()) + currentIdx = self.browser.currentIndex() + if self.useProxy: + currentIdx = self.proxyFileSystemModel.mapToSource(currentIdx) + currentFile = self.fileSystemModel.filePath(currentIdx) + logger.debug("got current file: %s", currentFile) + if self.useProxy: + self.proxyFileSystemModel.setSourceModel(newModel) + oldModel = self.fileSystemModel + self.fileSystemModel = newModel + oldModel.deleteLater() + idx = self.fileSystemModel.index(currentFile) + if self.useProxy: + idx = self.proxyFileSystemModel.mapFromSource(idx) + self.browser.setCurrentIndex(idx) + self.browser.scrollTo(idx) + QTimer.singleShot(250, self.scrollToCurrent) + + def scrollToCurrent(self): + """ + Scrolls to the current item in the browser + :return: + """ + assertMainThread() + idx = self.browser.currentIndex() + if idx.isValid(): + self.browser.scrollTo(idx) + + def sequenceOpened(self, filename, begin, end, streams): + """ + Notifies about an opened sequence. + :param filename: the filename which has been opened + :param begin: timestamp of sequence's first sample + :param end: timestamp of sequence's last sample + :param streams: list of streams in the sequence + :return: None + """ + assertMainThread() + self.beginTime = begin + self.positionSlider.setRange(0, end.toMSecsSinceEpoch() - begin.toMSecsSinceEpoch()) + self.beginLabel.setText(begin.toString("hh:mm:ss.zzz")) + self.endLabel.setText(end.toString("hh:mm:ss.zzz")) + self.currentTimestampChanged(begin) + self.filenameLabel.setText(filename) + idx = self.fileSystemModel.index(filename) + if self.useProxy: + idx = self.proxyFileSystemModel.mapFromSource(idx) + self.browser.setCurrentIndex(idx) + self.browser.scrollTo(idx) + QTimer.singleShot(250, self.scrollToCurrent) + + def currentTimestampChanged(self, currentTime): + """ + Notifies about a changed timestamp + :param currentTime: the new current timestamp + :return: None + """ + assertMainThread() + if self.beginTime is None: + self.currentLabel.setText("") + else: + sliderVal = currentTime.toMSecsSinceEpoch() - self.beginTime.toMSecsSinceEpoch() + self.preventSeek = True + self.positionSlider.setValue(sliderVal) + self.preventSeek = False + self.positionSlider.blockSignals(False) + self.currentLabel.setEnabled(True) + self.currentLabel.setText(currentTime.toString("hh:mm:ss.zzz")) + + def onSliderValueChanged(self, value): + """ + Slot called whenever the slider value is changed. + :param value: the new slider value + :return: + """ + assertMainThread() + if self.beginTime is None or self.preventSeek: + return + if self.actStart.isEnabled(): + ts = QDateTime.fromMSecsSinceEpoch(self.beginTime.toMSecsSinceEpoch() + value, self.beginTime.timeSpec()) + self.seekTime.emit(ts) + else: + logger.warning("Can't seek while playing.") + + def displayPosition(self, value): + """ + Slot called when the slider is moved. Displays the position without actually seeking to it. + :param value: the new slider value. + :return: + """ + assertMainThread() + if self.beginTime is None: + return + if self.positionSlider.isSliderDown(): + ts = QDateTime.fromMSecsSinceEpoch(self.beginTime.toMSecsSinceEpoch() + value, self.beginTime.timeSpec()) + self.currentLabel.setEnabled(False) + self.currentLabel.setText(ts.toString("hh:mm:ss.zzz")) + + def playbackStarted(self): + """ + Notifies about starting playback + :return: None + """ + assertMainThread() + self.actStart.setEnabled(False) + if "pausePlayback" in self.featureset: + self.actPause.setEnabled(True) + + def playbackPaused(self): + """ + Notifies about pause playback + :return: None + """ + assertMainThread() + logger.debug("playbackPaused received") + if "startPlayback" in self.featureset: + self.actStart.setEnabled(True) + self.actPause.setEnabled(False) + + def openRecent(self): + """ + Called when the user clicks on a recent sequence. + :return: + """ + action = self.sender() + index = self.fileSystemModel.index(action.data(), 0) + if self.useProxy: + index = self.proxyFileSystemModel.mapFromSource(index) + self.browser.setCurrentIndex(index) + + def browserCurrentChanged(self, current): + """ + Called when the current item of the file browser changed. + :param current: the proxyFileSystemModel model index + :return: + """ + assertMainThread() + if self.useProxy: + current = self.proxyFileSystemModel.mapToSource(current) + if self.fileSystemModel.flags(current) & Qt.ItemIsEnabled and self.fileSystemModel.fileInfo(current).isFile(): + filename = self.fileSystemModel.filePath(current) + foundIdx = None + for i, a in enumerate(self.recentSeqs): + if a.data() == filename: + foundIdx = i + if foundIdx is None: + foundIdx = len(self.recentSeqs)-1 + for i in range(foundIdx, 0, -1): + self.recentSeqs[i].setText(self.recentSeqs[i-1].text()) + self.recentSeqs[i].setData(self.recentSeqs[i-1].data()) + logger.debug("%d data: %s", i, self.recentSeqs[i-1].data()) + self.recentSeqs[i].setVisible(self.recentSeqs[i-1].data() is not None) + self.recentSeqs[0].setText(filename) + self.recentSeqs[0].setData(filename) + self.recentSeqs[0].setVisible(True) + self.setSequence.emit(filename) + + def timeRatioChanged(self, newRatio): + """ + Notifies about a changed playback time ratio, + :param newRatio the new playback ratio as a float + :return: None + """ + assertMainThread() + self.timeRatio = newRatio + logger.debug("new timeRatio: %f", newRatio) + for r in [1/8, 1/4, 1/2, 1, 2, 4, 8]: + if abs(newRatio / r - 1) < 0.01: + self.timeRatioLabel.setText(("x 1/%d"%(1/r)) if r < 1 else ("x %d"%r)) + return + self.timeRatioLabel.setText("%.2f" % newRatio) + + def saveState(self): + """ + Saves the state of the playback control + :return: + """ + assertMainThread() + propertyCollection = self.config.guiState() + showAllFiles = self.actShowAllFiles.isChecked() + current = self.browser.currentIndex() + if self.useProxy: + current = self.proxyFileSystemModel.mapToSource(current) + if self.fileSystemModel.flags(current) & Qt.ItemIsEnabled and self.fileSystemModel.fileInfo(current).isFile(): + filename = self.fileSystemModel.filePath(current) + else: + filename = "" + try: + propertyCollection.setProperty("PlaybackControl_showAllFiles", int(showAllFiles)) + propertyCollection.setProperty("PlaybackControl_filename", filename) + recentFiles = [a.data() for a in self.recentSeqs if a.data() is not None] + propertyCollection.setProperty("PlaybackControl_recent", "|".join(recentFiles)) + except PropertyCollectionPropertyNotFound: + pass + + def restoreState(self): + """ + Restores the state of the playback control from the given property collection + :param propertyCollection: a PropertyCollection instance + :return: + """ + assertMainThread() + propertyCollection = self.config.guiState() + propertyCollection.defineProperty("PlaybackControl_showAllFiles", 0, "show all files setting") + showAllFiles = propertyCollection.getProperty("PlaybackControl_showAllFiles") + self.actShowAllFiles.setChecked(bool(showAllFiles)) + propertyCollection.defineProperty("PlaybackControl_filename", "", "current file name") + filename = propertyCollection.getProperty("PlaybackControl_filename") + index = self.fileSystemModel.index(os.path.split(filename)[0]) + if self.useProxy: + index = self.proxyFileSystemModel.mapFromSource(index) + self.browser.setExpanded(index, True) + self.browser.scrollTo(index) + propertyCollection.defineProperty("PlaybackControl_recent", "", "recent opened sequences") + recentFiles = propertyCollection.getProperty("PlaybackControl_recent") + idx = 0 + for f in recentFiles.split("|"): + if f != "": + self.recentSeqs[idx].setData(f) + self.recentSeqs[idx].setText(f) + self.recentSeqs[idx].setVisible(True) + idx += 1 + if idx >= len(self.recentSeqs): + break + for a in self.recentSeqs[idx:]: + a.setData(None) + a.setText("") + a.setVisible(False) \ No newline at end of file diff --git a/nexxT/services/gui/PropertyDelegate.py b/nexxT/services/gui/PropertyDelegate.py new file mode 100644 index 0000000..e7c1a81 --- /dev/null +++ b/nexxT/services/gui/PropertyDelegate.py @@ -0,0 +1,109 @@ +# 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 delegate for use in the Configuration GUI service to edit properties. +""" + +from PySide2.QtCore import Qt +from PySide2.QtWidgets import QStyledItemDelegate, QLineEdit, QSpinBox + +class PropertyDelegate(QStyledItemDelegate): + """ + This class provides a delegate for providing editor widgets for the nexxT gui service Configuration. + """ + def __init__(self, model, role, PropertyContent, parent): + """ + Constructor. + :param model: An instance of the nexxT gui service implementation of QAbstractItemModle + :param role: the role which can be used to query the property items + :param PropertyContent: the class of the property items queried with model->data(..., self.role) + :param parent: the parent QObject + """ + super().__init__(parent) + self.model = model + self.role = role + # in fact this is a type name and not a variable + self.PropertyContent = PropertyContent # pylint: disable=invalid-name + + def createEditor(self, parent, option, index): + """ + Create an editor for the given index (if this is not a PropertyContent, the default implementation is used) + :param parent: the parent of the editor + :param option: unused + :param index: the model index + :return: an editor widget + """ + d = self.model.data(index, self.role) + if isinstance(d, self.PropertyContent): + p = d.property.getPropertyDetails(d.name) + res = None + if isinstance(p.defaultVal, str): + res = QLineEdit(parent) + res.setFrame(False) + res.setValidator(p.validator) + elif isinstance(p.defaultVal, int): + res = QSpinBox(parent) + res.setFrame(False) + res.setMinimum(p.validator.bottom()) + res.setMaximum(p.validator.top()) + elif isinstance(p.defaultVal, float): + res = QLineEdit(parent) + res.setFrame(False) + res.setValidator(p.validator) + if res is not None: + return res + return super().createEditor(parent, option, index) + + def setEditorData(self, editor, index): + """ + Populate the editor with the data from the model + :param editor: the editor as created by createEditor + :param index: the index into the model + :return: + """ + d = self.model.data(index, self.role) + if isinstance(d, self.PropertyContent): + p = d.property.getPropertyDetails(d.name) + if isinstance(p.defaultVal, str): + # editor is line edit + editor.setText(p.converter(d.property.getProperty(d.name))) + return None + if isinstance(p.defaultVal, int): + # editor is spin box + editor.setValue(p.converter(d.property.getProperty(d.name))) + return None + if isinstance(p.defaultVal, float): + # editor is line edit + editor.setText(str(p.converter(d.property.getProperty(d.name)))) + return None + return super().setEditorData(editor, index) + + def setModelData(self, editor, model, index): + """ + Commit the data from the editor into the model + :param editor: the editor as returned by createEditor + :param model: the model + :param index: an index to the model + :return: + """ + assert model is self.model + d = self.model.data(index, self.role) + if isinstance(d, self.PropertyContent): + p = d.property.getPropertyDetails(d.name) + value = None + if isinstance(p.defaultVal, str): + # editor is line edit + value = editor.text() + elif isinstance(p.defaultVal, int): + # editor is spin box + value = editor.value() + elif isinstance(p.defaultVal, float): + # editor is line edit + value = p.converter(editor.text()) + if value is not None: + model.setData(index, value, Qt.EditRole) + return super().setModelData(editor, model, index) diff --git a/nexxT/services/gui/__init__.py b/nexxT/services/gui/__init__.py new file mode 100644 index 0000000..5eabbc7 --- /dev/null +++ b/nexxT/services/gui/__init__.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# diff --git a/nexxT/src/DataSamples.cpp b/nexxT/src/DataSamples.cpp new file mode 100644 index 0000000..6e27328 --- /dev/null +++ b/nexxT/src/DataSamples.cpp @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "DataSamples.hpp" +#include "Logger.hpp" + +USE_NAMESPACE + +const double DataSample::TIMESTAMP_RES = 1e-6; +START_NAMESPACE + struct DataSampleD + { + QByteArray content; + QString datatype; + int64_t timestamp; + }; +STOP_NAMESPACE + +DataSample::DataSample(const QByteArray &content, const QString &datatype, int64_t timestamp) : + d(new DataSampleD{content,datatype,timestamp}) +{ + NEXT_LOG_INTERNAL("DataSample::DataSample"); +} + +DataSample::~DataSample() +{ + NEXT_LOG_INTERNAL("DataSample::~DataSample"); + delete d; +} + +QByteArray DataSample::getContent() const +{ + return d->content; +} + +int64_t DataSample::getTimestamp() const +{ + return d->timestamp; +} + +QString DataSample::getDatatype() const +{ + return d->datatype; +} + +SharedDataSamplePtr DataSample::copy(const SharedDataSamplePtr &src) +{ + return SharedDataSamplePtr(new DataSample(src->d->content, src->d->datatype, src->d->timestamp)); +} + +SharedDataSamplePtr DataSample::make_shared(DataSample *sample) +{ + return SharedDataSamplePtr(sample); +} + +void DataSample::registerMetaType() +{ + int id = qRegisterMetaType >(); +} diff --git a/nexxT/src/DataSamples.hpp b/nexxT/src/DataSamples.hpp new file mode 100644 index 0000000..6132958 --- /dev/null +++ b/nexxT/src/DataSamples.hpp @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_DATA_SAMPLES_HPP +#define NEXT_DATA_SAMPLES_HPP + +#include +#include +#include + +#include "NexTConfig.hpp" +#include "NexTLinkage.hpp" + +START_NAMESPACE + + struct DataSampleD; + class DataSample; +#if 1 + typedef NEXT_SHARED_PTR SharedDataSamplePtr; +#else +# define SharedDataSamplePtr NEXT_SHARED_PTR +#endif + + class DLLEXPORT DataSample + { + DataSampleD *d; + public: + static const double TIMESTAMP_RES; + + DataSample(const QByteArray &content, const QString &datatype, int64_t timestamp); + virtual ~DataSample(); + + QByteArray getContent() const; + int64_t getTimestamp() const; + QString getDatatype() const; + + static SharedDataSamplePtr copy(const SharedDataSamplePtr &src); + static SharedDataSamplePtr make_shared(DataSample *sample); + static void registerMetaType(); + }; + +STOP_NAMESPACE + +Q_DECLARE_METATYPE(QSharedPointer); + +#endif diff --git a/nexxT/src/FilterEnvironment.cpp b/nexxT/src/FilterEnvironment.cpp new file mode 100644 index 0000000..df76693 --- /dev/null +++ b/nexxT/src/FilterEnvironment.cpp @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "FilterEnvironment.hpp" +#include "Filters.hpp" +#include "Logger.hpp" +#include "PropertyCollection.hpp" + +#include +#include + +USE_NAMESPACE + +START_NAMESPACE + struct BaseFilterEnvironmentD + { + SharedFilterPtr plugin; + QThread *thread; + /* propertyCollection is owned by the property subsystem and it is ensured that the object stays valid over the filter lifetime */ + PropertyCollection *propertyCollection; + bool dynamicInputPortsSupported; + bool dynamicOutputPortsSupported; + }; +STOP_NAMESPACE + +BaseFilterEnvironment::BaseFilterEnvironment(PropertyCollection *propertyCollection) + : d(new BaseFilterEnvironmentD{SharedFilterPtr(), QThread::currentThread(), propertyCollection, false, false}) +{ + NEXT_LOG_INTERNAL(QString("BaseFilterEnvironment::BaseFilterEnvironment %1").arg(uint64_t(this), 0, 16)); +} + +BaseFilterEnvironment::~BaseFilterEnvironment() +{ + NEXT_LOG_INTERNAL(QString("BaseFilterEnvironment::~BaseFilterEnvironment %1").arg(uint64_t(this), 0, 16)); + delete d; +} + +void BaseFilterEnvironment::setPlugin(const SharedFilterPtr &plugin) +{ + d->plugin = plugin; +} + +void BaseFilterEnvironment::resetPlugin() +{ + d->plugin.reset(); +} + +SharedFilterPtr BaseFilterEnvironment::getPlugin() +{ + return d->plugin; +} + +void BaseFilterEnvironment::setDynamicPortsSupported(bool dynInPortsSupported, bool dynOutPortsSupported) +{ + assertMyThread(); + d->dynamicInputPortsSupported = dynInPortsSupported; + d->dynamicOutputPortsSupported = dynOutPortsSupported; + if(!dynInPortsSupported) + { + PortList p = getDynamicInputPorts(); + if( p.size() > 0 ) + { + throw std::runtime_error("Dynamic input ports are not supported"); + } + } + if(!dynOutPortsSupported) + { + PortList p = getDynamicOutputPorts(); + if( p.size() > 0 ) + { + throw std::runtime_error("Dynamic output ports are not supported"); + } + } +} + +void BaseFilterEnvironment::getDynamicPortsSupported(bool &dynInPortsSupported, bool &dynOutPortsSupported) +{ + assertMyThread(); + dynInPortsSupported = d->dynamicInputPortsSupported; + dynOutPortsSupported = d->dynamicOutputPortsSupported; +} + +void BaseFilterEnvironment::portDataChanged(const InputPortInterface &port) +{ + assertMyThread(); + if( state() != FilterState::ACTIVE ) + { + if( state() != FilterState::INITIALIZED ) + { + throw std::runtime_error(QString("Unexpected filter state %1, expected ACTIVE or INITIALIZED.").arg(FilterState::state2str(state())).toStdString()); + } + NEXT_LOG_INFO("DataSample discarded because application has been stopped already."); + } else + { + try + { + if( getPlugin() ) + { + getPlugin()->onPortDataChanged(port); + } else + { + NEXT_LOG_ERROR(QString("no plugin found")); + } + } catch(std::exception &e) + { + NEXT_LOG_ERROR(QString("Unexpected exception during onPortDataChanged from filter %1: %2").arg(d->propertyCollection->objectName()).arg(e.what())); + } + } +} + +PropertyCollection *BaseFilterEnvironment::propertyCollection() const +{ + return d->propertyCollection; +} + +void BaseFilterEnvironment::assertMyThread() +{ + if( QThread::currentThread() != d->thread ) + { + throw std::runtime_error("Unexpected thread."); + } +} diff --git a/nexxT/src/FilterEnvironment.hpp b/nexxT/src/FilterEnvironment.hpp new file mode 100644 index 0000000..e21a3d1 --- /dev/null +++ b/nexxT/src/FilterEnvironment.hpp @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_FILTER_ENVIRONMENT_HPP +#define NEXT_FILTER_ENVIRONMENT_HPP + +#include + +#include "Ports.hpp" +#include "Filters.hpp" +#include "NexTConfig.hpp" +#include "NexTLinkage.hpp" + +START_NAMESPACE + struct BaseFilterEnvironmentD; + class PropertyCollection; + + class DLLEXPORT BaseFilterEnvironment: public QObject + { + Q_OBJECT + + BaseFilterEnvironmentD *d; + public: + BaseFilterEnvironment(PropertyCollection* propertyCollection); + BaseFilterEnvironment(const BaseFilterEnvironment &) = delete; + virtual ~BaseFilterEnvironment(); + + void setPlugin(const SharedFilterPtr &plugin); + void resetPlugin(); + SharedFilterPtr getPlugin(); + + void setDynamicPortsSupported(bool dynInPortsSupported, bool dynOutPortsSupported); + void getDynamicPortsSupported(bool &dynInPortsSupported, bool &dynOutPortsSupported); + + void portDataChanged(const InputPortInterface &port); + + PropertyCollection *propertyCollection() const; + + virtual PropertyCollection *guiState() const = 0; + + virtual void addPort(const SharedPortPtr &port) = 0; + virtual void removePort(const SharedPortPtr &port) = 0; + + virtual QList > getDynamicInputPorts() = 0; + virtual QList > getStaticInputPorts() = 0; + virtual QList > getAllInputPorts() = 0; + + virtual QList > getDynamicOutputPorts() = 0; + virtual QList > getStaticOutputPorts() = 0; + virtual QList > getAllOutputPorts() = 0; + + /*virtual SharedPortPtr getPort(QString portName, bool input) = 0; + virtual SharedPortPtr getOutputPort(QString portName) = 0; + virtual SharedPortPtr getInputPort(QString portName) = 0;*/ + + virtual void updatePortInformation(const BaseFilterEnvironment &other) = 0; + + public: + virtual int state() const = 0; + + protected: + void assertMyThread(); + }; +STOP_NAMESPACE + +#endif diff --git a/nexxT/src/Filters.cpp b/nexxT/src/Filters.cpp new file mode 100644 index 0000000..e7f1409 --- /dev/null +++ b/nexxT/src/Filters.cpp @@ -0,0 +1,137 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "Filters.hpp" +#include "FilterEnvironment.hpp" +#include "Ports.hpp" +#include "Logger.hpp" + +USE_NAMESPACE; + +// we need these on linux for some reason +const int FilterState::CONSTRUCTING; +const int FilterState::CONSTRUCTED; +const int FilterState::INITIALIZING; +const int FilterState::INITIALIZED; +const int FilterState::STARTING; +const int FilterState::ACTIVE; +const int FilterState::STOPPING; +const int FilterState::DEINITIALIZING; +const int FilterState::DESTRUCTING; +const int FilterState::DESTRUCTED; + +QString FilterState::state2str(int state) +{ + switch (state) + { + case FilterState::CONSTRUCTING: return "CONSTRUCTING"; + case FilterState::CONSTRUCTED: return "CONSTRUCTED"; + case FilterState::INITIALIZING: return "INITIALIZING"; + case FilterState::INITIALIZED: return "INITIALIZED"; + case FilterState::STARTING: return "STARTING"; + case FilterState::ACTIVE: return "ACTIVE"; + case FilterState::STOPPING: return "STOPPING"; + case FilterState::DEINITIALIZING: return "DEINITIALIZING"; + case FilterState::DESTRUCTING: return "DESTRUCTING"; + case FilterState::DESTRUCTED: return "DESTRUCTED"; + default: + throw(std::runtime_error("Unknown state")); + } +} + +START_NAMESPACE + struct FilterD + { + BaseFilterEnvironment *environment; + }; +STOP_NAMESPACE + +Filter::Filter(bool dynInPortsSupported, bool dynOutPortsSupported, BaseFilterEnvironment *environment) + : d(new FilterD{environment}) +{ + NEXT_LOG_INTERNAL("Filter::Filter"); + d->environment->setDynamicPortsSupported(dynInPortsSupported, dynOutPortsSupported); +} + +Filter::~Filter() +{ + NEXT_LOG_INTERNAL("Filter::~Filter"); + delete d; +} + +PropertyCollection *Filter::propertyCollection() +{ + return d->environment->propertyCollection(); +} + +PropertyCollection *Filter::guiState() +{ + return d->environment->guiState(); +} + +void Filter::addStaticPort(const SharedPortPtr &port) +{ + if( port->dynamic() ) + { + throw std::runtime_error("The given port should be static but is dynamic."); + } + d->environment->addPort(port); +} + +void Filter::removeStaticPort(const SharedPortPtr &port) +{ + if( port->dynamic() ) + { + throw std::runtime_error("The given port should be static but is dynamic."); + } + d->environment->removePort(port); +} + +PortList Filter::getDynamicInputPorts() +{ + return d->environment->getDynamicInputPorts(); +} + +PortList Filter::getDynamicOutputPorts() +{ + return d->environment->getDynamicOutputPorts(); +} + +void Filter::onInit() +{ + /* intentionally empty */ +} + +void Filter::onStart() +{ + /* intentionally empty */ +} + +void Filter::onPortDataChanged(const InputPortInterface &) +{ + /* intentionally empty */ +} + +void Filter::onStop() +{ + /* intentionally empty */ +} + +void Filter::onDeinit() +{ + /* intentionally empty */ +} + +BaseFilterEnvironment *Filter::environment() const +{ + return d->environment; +} + + SharedFilterPtr Filter::make_shared(Filter *filter) + { + return SharedFilterPtr(filter); + } diff --git a/nexxT/src/Filters.hpp b/nexxT/src/Filters.hpp new file mode 100644 index 0000000..a7cff56 --- /dev/null +++ b/nexxT/src/Filters.hpp @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_FILTERS_HPP +#define NEXT_FILTERS_HPP + +#include +#include +#include +#include + +#include "NexTConfig.hpp" +#include "NexTLinkage.hpp" +#include "Ports.hpp" + +START_NAMESPACE + + struct DLLEXPORT FilterState + { + static const int CONSTRUCTING = 0; + static const int CONSTRUCTED = 1; + static const int INITIALIZING = 2; + static const int INITIALIZED = 3; + static const int STARTING = 4; + static const int ACTIVE = 5; + static const int STOPPING = 6; + static const int DEINITIALIZING = 7; + static const int DESTRUCTING = 8; + static const int DESTRUCTED = 9; + + static QString state2str(int state); + }; + + class Filter; + struct FilterD; + class BaseFilterEnvironment; + class PropertyCollection; + + typedef NEXT_SHARED_PTR SharedFilterPtr; + + class DLLEXPORT Filter : public QObject + { + Q_OBJECT + + FilterD *const d; + protected: + Filter(bool dynInPortsSupported, bool dynOutPortsSupported, BaseFilterEnvironment *env); + + PropertyCollection *propertyCollection(); + PropertyCollection *guiState(); + + void addStaticPort(const SharedPortPtr &port); + void removeStaticPort(const SharedPortPtr &port); + PortList getDynamicInputPorts(); + PortList getDynamicOutputPorts(); + + public: + virtual ~Filter(); + virtual void onInit(); + virtual void onStart(); + virtual void onPortDataChanged(const InputPortInterface &inputPort); + virtual void onStop(); + virtual void onDeinit(); + + BaseFilterEnvironment *environment() const; + + static SharedFilterPtr make_shared(Filter *filter); + }; + +STOP_NAMESPACE + +#endif diff --git a/nexxT/src/Logger.cpp b/nexxT/src/Logger.cpp new file mode 100644 index 0000000..5d236a3 --- /dev/null +++ b/nexxT/src/Logger.cpp @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "Logger.hpp" + +USE_NAMESPACE + +START_NAMESPACE + void log(unsigned int level, const QString &message, const QString &file, unsigned int line) + { + SharedQObjectPtr logger = Services::getService("Logging"); + if( !logger.isNull() ) + { + bool res = QMetaObject::invokeMethod(logger.get(), "log", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(int, level), Q_ARG(const QString &, message), Q_ARG(const QString &, file), Q_ARG(int, line)); + if(!res) + { + fprintf(stderr, "WARNING: invokeMetod returned false!\n"); + } + } else + { + if( level >= NEXT_LOG_LEVEL_INFO ) + { + fprintf(stderr, "LOG: level=%d msg=%s file=%s line=%d\n", level, message.toStdString().c_str(), file.toStdString().c_str(), line); + } + } + } +STOP_NAMESPACE diff --git a/nexxT/src/Logger.hpp b/nexxT/src/Logger.hpp new file mode 100644 index 0000000..966d6f5 --- /dev/null +++ b/nexxT/src/Logger.hpp @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_LOGGER_HPP +#define NEXT_LOGGER_HPP + +#include "Services.hpp" +#include "NexTLinkage.hpp" +#include "QtCore/QMetaObject" + +#define NEXT_LOG_LEVEL_NOTSET 0 +#define NEXT_LOG_LEVEL_INTERNAL 5 +#define NEXT_LOG_LEVEL_DEBUG 10 +#define NEXT_LOG_LEVEL_INFO 20 +#define NEXT_LOG_LEVEL_WARN 30 +#define NEXT_LOG_LEVEL_ERROR 40 +#define NEXT_LOG_LEVEL_CRITICAL 50 + +#ifdef AVOID_NAMESPACE +#define NEXT_LOG_INTERNAL(msg) log(NEXT_LOG_LEVEL_INTERNAL, msg, __FILE__, __LINE__) +#define NEXT_LOG_DEBUG(msg) log(NEXT_LOG_LEVEL_DEBUG, msg, __FILE__, __LINE__) +#define NEXT_LOG_INFO(msg) log(NEXT_LOG_LEVEL_INFO, msg, __FILE__, __LINE__) +#define NEXT_LOG_WARN(msg) log(NEXT_LOG_LEVEL_WARN, msg, __FILE__, __LINE__) +#define NEXT_LOG_ERROR(msg) log(NEXT_LOG_LEVEL_ERROR, msg, __FILE__, __LINE__) +#define NEXT_LOG_CRITICAL(msg) log(NEXT_LOG_LEVEL_CRITICAL, msg, __FILE__, __LINE__) +#else +#define NEXT_LOG_INTERNAL(msg) nexxT::log(NEXT_LOG_LEVEL_INTERNAL, msg, __FILE__, __LINE__) +#define NEXT_LOG_DEBUG(msg) nexxT::log(NEXT_LOG_LEVEL_DEBUG, msg, __FILE__, __LINE__) +#define NEXT_LOG_INFO(msg) nexxT::log(NEXT_LOG_LEVEL_INFO, msg, __FILE__, __LINE__) +#define NEXT_LOG_WARN(msg) nexxT::log(NEXT_LOG_LEVEL_WARN, msg, __FILE__, __LINE__) +#define NEXT_LOG_ERROR(msg) nexxT::log(NEXT_LOG_LEVEL_ERROR, msg, __FILE__, __LINE__) +#define NEXT_LOG_CRITICAL(msg) nexxT::log(NEXT_LOG_LEVEL_CRITICAL, msg, __FILE__, __LINE__) +#endif + +START_NAMESPACE + DLLEXPORT void log(unsigned int level, const QString &message, const QString &file, unsigned int line); +STOP_NAMESPACE + +#endif diff --git a/nexxT/src/NexTConfig.hpp b/nexxT/src/NexTConfig.hpp new file mode 100644 index 0000000..04a1a71 --- /dev/null +++ b/nexxT/src/NexTConfig.hpp @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_CONFIG_HPP +#define NEXT_CONFIG_HPP + +//#define AVOID_NAMESPACE +#ifndef AVOID_NAMESPACE + #define START_NAMESPACE namespace nexxT { + #define STOP_NAMESPACE }; + #define USE_NAMESPACE using namespace nexxT; +#else + #define START_NAMESPACE + #define STOP_NAMESPACE + #define USE_NAMESPACE +#endif + +#include + +#define NEXT_SHARED_PTR QSharedPointer + +#endif diff --git a/nexxT/src/NexTLinkage.hpp b/nexxT/src/NexTLinkage.hpp new file mode 100644 index 0000000..5e693af --- /dev/null +++ b/nexxT/src/NexTLinkage.hpp @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_LINKAGE_HPP +#define NEXT_LINKAGE_HPP + +# ifdef __GNUC__ +# define FORCE_DLLEXPORT __attribute__ ((visibility("default"))) +# else +# define FORCE_DLLEXPORT __declspec(dllexport) +# endif + +#ifdef NEXT_LIBRARY_COMPILATION + +# define DLLEXPORT FORCE_DLLEXPORT + +#else + +# ifdef __GNUC__ +# define DLLEXPORT +# else +# define DLLEXPORT __declspec(dllimport) +# endif + +#endif + +#endif diff --git a/nexxT/src/NexTPlugins.cpp b/nexxT/src/NexTPlugins.cpp new file mode 100644 index 0000000..2e3efb1 --- /dev/null +++ b/nexxT/src/NexTPlugins.cpp @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "NexTPlugins.hpp" +#include "Logger.hpp" +#include + +using namespace nexxT; + +namespace nexxT +{ + struct PluginInterfaceD + { + QMap > loadedLibs; + }; +} + +PluginInterface *PluginInterface::_singleton; + +void PluginInterface::loadLib(const QString &file) +{ + if( !d->loadedLibs.contains(file) ) + { + NEXT_LOG_DEBUG(QString("Loading plugin %1").arg(file)); + QSharedPointer lib(new QLibrary(file)); + if(!lib->load()) + { + throw std::runtime_error((QString("Cannot load lib %1 (%2).").arg(file).arg(lib->errorString())).toStdString()); + } + d->loadedLibs.insert(file, lib); + } +} + +PluginInterface* PluginInterface::singleton() +{ + if( !_singleton ) + { + _singleton = new PluginInterface(); + } + return _singleton; +} + +PluginInterface::PluginInterface() : d(new PluginInterfaceD()) +{ + NEXT_LOG_INTERNAL(QString("PluginInterface::PluginInterface %1").arg((uint64_t)this, 0, 16)); +} + +PluginInterface::~PluginInterface() +{ + NEXT_LOG_INTERNAL(QString("PluginInterface::~PluginInterface %1").arg((uint64_t)this, 0, 16)); + unloadAll(); + delete d; +} + +Filter *PluginInterface::create(const QString &lib, const QString &function, BaseFilterEnvironment *env) +{ + PluginDefinitionFunc f = (PluginDefinitionFunc)d->loadedLibs[lib]->resolve("nexT_pluginDefinition"); + if(!f) + { + throw std::runtime_error((QString("Cannot resolve '%1' in %2 (%3).").arg(function).arg(lib).arg(d->loadedLibs[lib]->errorString())).toStdString()); + } + QMap m; + f(m); + if(!m.contains(function)) + { + throw std::runtime_error((QString("Cannot find function '%1' in function table of %a.").arg(function).arg(lib)).toStdString()); + } + Filter *res = m[function](env); + return res; +} + +QStringList PluginInterface::availableFilters(const QString &lib) +{ + loadLib(lib); + PluginDefinitionFunc f = (PluginDefinitionFunc)d->loadedLibs[lib]->resolve("nexT_pluginDefinition"); + if(!f) + { + throw std::runtime_error((QString("Cannot resolve 'nexT_pluginDefinition' in %1 (%2).").arg(lib).arg(d->loadedLibs[lib]->errorString())).toStdString()); + } + QMap m; + f(m); + return m.keys(); +} + +void PluginInterface::unloadAll() +{ + foreach(QSharedPointer lib, d->loadedLibs) + { + NEXT_LOG_DEBUG(QString("Unloading plugin %1").arg(lib->fileName())); + lib->unload(); + } + d->loadedLibs.clear(); +} diff --git a/nexxT/src/NexTPlugins.hpp b/nexxT/src/NexTPlugins.hpp new file mode 100644 index 0000000..05004f2 --- /dev/null +++ b/nexxT/src/NexTPlugins.hpp @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_PLUGINS_HPP +#define NEXT_PLUGINS_HPP + +#include "Filters.hpp" +#include "NexTConfig.hpp" +#include + +#define NEXT_PLUGIN_DECLARE_FILTER(classname) \ + static nexxT::Filter *next_plugin_create(nexxT::BaseFilterEnvironment *env) \ + { \ + nexxT::Filter *res = new classname(env); \ + return res; \ + } + +#define NEXT_PLUGIN_DEFINE_START() \ + struct { \ + QString name; \ + nexxT::PluginCreateFunc func; \ + } next_plugin_functions[] = { {"", 0} + +#define NEXT_PLUGIN_ADD_FILTER(filtertype) \ + , {#filtertype, &filtertype::next_plugin_create} + +#define NEXT_PLUGIN_DEFINE_FINISH() \ + }; \ + \ + extern "C" FORCE_DLLEXPORT void nexT_pluginDefinition(QMap &res) \ + { \ + res.clear(); \ + for(int i = 1; \ + i < sizeof(next_plugin_functions)/sizeof(next_plugin_functions[0]); \ + i++) \ + { \ + res[next_plugin_functions[i].name] = next_plugin_functions[i].func; \ + } \ + } + +START_NAMESPACE + typedef nexxT::Filter *(*PluginCreateFunc)(nexxT::BaseFilterEnvironment *env); + typedef void (*PluginDefinitionFunc)(QMap &res); + + struct PluginInterfaceD; + + class DLLEXPORT PluginInterface + { + PluginInterfaceD *d; + static PluginInterface *_singleton; + + void loadLib(const QString &lib); + PluginInterface(); + public: + static PluginInterface* singleton(); + + virtual ~PluginInterface(); + + Filter *create(const QString &lib, const QString &function, BaseFilterEnvironment *env); + QStringList availableFilters(const QString &lib); + void unloadAll(); + }; +STOP_NAMESPACE + +#endif diff --git a/nexxT/src/Ports.cpp b/nexxT/src/Ports.cpp new file mode 100644 index 0000000..df7d931 --- /dev/null +++ b/nexxT/src/Ports.cpp @@ -0,0 +1,243 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "Ports.hpp" +#include "DataSamples.hpp" +#include "FilterEnvironment.hpp" +#include "Logger.hpp" +#include +#include + +USE_NAMESPACE + +START_NAMESPACE + struct PortD + { + bool dynamic; + QString name; + BaseFilterEnvironment *environment; + }; + + struct InputPortD + { + int queueSizeSamples; + double queueSizeSeconds; + QList queue; + }; + + struct InterThreadConnectionD + { + QSemaphore semaphore; + InterThreadConnectionD(int n) : semaphore(n) {} + }; + +STOP_NAMESPACE + +Port::Port(bool dynamic, const QString &name, BaseFilterEnvironment *env) + : d(new PortD{dynamic, name, env}) +{ + NEXT_LOG_INTERNAL(QString("Port::Port %1").arg(uint64_t(this), 0, 16)); +} + +Port::~Port() +{ + NEXT_LOG_INTERNAL(QString("Port::~Port %1").arg(uint64_t(this), 0, 16)); + delete d; +} + +bool Port::dynamic() const +{ + return d->dynamic; +} + +const QString &Port::name() const +{ + return d->name; +} + +void Port::setName(const QString &name) +{ + d->name = name; +} + +BaseFilterEnvironment *Port::environment() const +{ + return d->environment; +} + +bool Port::isOutput() const +{ + return dynamic_cast(this) != 0; +} + +bool Port::isInput() const +{ + return dynamic_cast(this) != 0; +} + +SharedPortPtr Port::clone(BaseFilterEnvironment *env) const +{ + if( dynamic_cast(this) ) + { + return dynamic_cast(this)->clone(env); + } else if( dynamic_cast(this) ) + { + return dynamic_cast(this)->clone(env); + } + throw(std::runtime_error("Unknown port class. Must be either OutputPortInterface or InputPortInterface.")); +} + +SharedPortPtr Port::make_shared(Port *port) +{ + return SharedPortPtr(port); +} + +OutputPortInterface::OutputPortInterface(bool dynamic, const QString &name, BaseFilterEnvironment *env) : + Port(dynamic, name, env) +{ +} + +void OutputPortInterface::transmit(const SharedDataSamplePtr &sample) +{ + if( QThread::currentThread() != thread() ) + { + throw std::runtime_error("OutputPort::transmit has been called from unexpected thread."); + } + emit transmitSample(sample); +} + +SharedPortPtr OutputPortInterface::clone(BaseFilterEnvironment *env) const +{ + return SharedPortPtr(new OutputPortInterface(dynamic(), name(), env)); +} + +void OutputPortInterface::setupDirectConnection(const SharedPortPtr &op, const SharedPortPtr &ip) +{ + const OutputPortInterface *p0 = dynamic_cast(op.data()); + const InputPortInterface *p1 = dynamic_cast(ip.data()); + QObject::connect(p0, SIGNAL(transmitSample(const QSharedPointer&)), + p1, SLOT(receiveSync(const QSharedPointer &))); +} + +QObject *OutputPortInterface::setupInterThreadConnection(const SharedPortPtr &op, const SharedPortPtr &ip, QThread &outputThread) +{ + InterThreadConnection *itc = new InterThreadConnection(&outputThread); + const OutputPortInterface *p0 = dynamic_cast(op.data()); + const InputPortInterface *p1 = dynamic_cast(ip.data()); + QObject::connect(p0, SIGNAL(transmitSample(const QSharedPointer&)), + itc, SLOT(receiveSample(const QSharedPointer&))); + QObject::connect(itc, SIGNAL(transmitInterThread(const QSharedPointer &, QSemaphore *)), + p1, SLOT(receiveAsync(const QSharedPointer &, QSemaphore *))); + return itc; +} + +InputPortInterface::InputPortInterface(bool dynamic, const QString &name, BaseFilterEnvironment *env, int queueSizeSamples, double queueSizeSeconds) : + Port(dynamic, name, env), + d(new InputPortD{queueSizeSamples, queueSizeSeconds}) +{ +} + +InputPortInterface::~InputPortInterface() +{ + delete d; +} + +SharedDataSamplePtr InputPortInterface::getData(int delaySamples, double delaySeconds) const +{ + if( QThread::currentThread() != thread() ) + { + throw std::runtime_error("InputPort.getData has been called from an unexpected thread."); + } + if( delaySamples >= 0 && delaySeconds >= 0. ) + { + throw std::runtime_error("Both delaySamples and delaySecons are positive"); + } + if( delaySamples >= 0 ) + { + if( delaySamples >= d->queue.size() ) + { + throw std::out_of_range("delaySamples is out of range."); + } + return d->queue[delaySamples]; + } + if( delaySeconds >= 0. ) + { + double delayTime = delaySeconds / (double)DataSample::TIMESTAMP_RES; + int i; + for(i = 0; (i < d->queue.size()) && (double(d->queue[0]->getTimestamp() - d->queue[i]->getTimestamp()) < delayTime); i++) + { + } + if( i >= d->queue.size() ) + { + throw std::out_of_range("delaySeconds is out of range."); + } + return d->queue[i]; + } + throw std::runtime_error("Both delaySamples and delaySeconds are negative"); +} + +SharedPortPtr InputPortInterface::clone(BaseFilterEnvironment*env) const +{ + return SharedPortPtr(new InputPortInterface(dynamic(), name(), env, d->queueSizeSamples, d->queueSizeSeconds)); +} + +void InputPortInterface::addToQueue(const SharedDataSamplePtr &sample) +{ + d->queue.prepend(sample); + if(d->queueSizeSamples > 0) + { + while(d->queue.size() > d->queueSizeSamples) + { + d->queue.removeLast(); + } + } + if(d->queueSizeSeconds > 0) + { + double queueSizeTime = d->queueSizeSeconds / (double)DataSample::TIMESTAMP_RES; + while( d->queue.size() > 0 && (double(d->queue.first()->getTimestamp() - d->queue.last()->getTimestamp()) > queueSizeTime) ) + { + d->queue.removeLast(); + } + } + environment()->portDataChanged(*this); +} + +void InputPortInterface::receiveAsync(const QSharedPointer &sample, QSemaphore *semaphore) +{ + if( QThread::currentThread() != thread() ) + { + throw std::runtime_error("InputPort.getData has been called from an unexpected thread."); + } + semaphore->release(1); + addToQueue(sample); +} + +void InputPortInterface::receiveSync (const QSharedPointer &sample) +{ + if( QThread::currentThread() != thread() ) + { + throw std::runtime_error("InputPort.getData has been called from an unexpected thread."); + } + addToQueue(sample); +} + +InterThreadConnection::InterThreadConnection(QThread *from_thread) + : d(new InterThreadConnectionD(1)) +{ + moveToThread(from_thread); +} + +InterThreadConnection::~InterThreadConnection() +{ + delete d; +} + +void InterThreadConnection::receiveSample(const QSharedPointer &sample) +{ + d->semaphore.acquire(1); + emit transmitInterThread(sample, &d->semaphore); +} diff --git a/nexxT/src/Ports.hpp b/nexxT/src/Ports.hpp new file mode 100644 index 0000000..e73c80b --- /dev/null +++ b/nexxT/src/Ports.hpp @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_PORTS_HPP +#define NEXT_PORTS_HPP + +#include +#include +#include "NexTConfig.hpp" +#include "NexTLinkage.hpp" +#include "DataSamples.hpp" + +START_NAMESPACE + + class BaseFilterEnvironment; + struct PortD; + struct InputPortD; + struct InterThreadConnectionD; + class Port; + class InputPortInterface; + class OutputPortInterface; + typedef NEXT_SHARED_PTR SharedPortPtr; + typedef NEXT_SHARED_PTR SharedOutputPortPtr; + typedef NEXT_SHARED_PTR SharedInputPortPtr; + + class DLLEXPORT Port : public QObject + { + Q_OBJECT + + PortD *const d; + + public: + Port(bool dynamic, const QString &name, BaseFilterEnvironment *env); + virtual ~Port(); + + bool dynamic() const; + const QString &name() const; + void setName(const QString &name); + BaseFilterEnvironment *environment() const; + bool isOutput() const; + bool isInput() const; + + SharedPortPtr clone(BaseFilterEnvironment *) const; + + static SharedPortPtr make_shared(Port *port); + }; +#if 0 + class DLLEXPORT PortList + { + typedef QSharedPointer T; + QList< T > ports; + public: + void append(T p) {ports.append(p);} + T at(int i) {return ports[i];} + int size() {return ports.size();} + }; +#endif + typedef QList > PortList; + + class DLLEXPORT OutputPortInterface final : public Port + { + Q_OBJECT + + signals: + void transmitSample(const QSharedPointer &sample); + + public: + OutputPortInterface(bool dynamic, const QString &name, BaseFilterEnvironment *env); + void transmit(const SharedDataSamplePtr &sample); + SharedPortPtr clone(BaseFilterEnvironment *) const; + + static void setupDirectConnection(const SharedPortPtr &, const SharedPortPtr &); + static QObject *setupInterThreadConnection(const SharedPortPtr &, const SharedPortPtr &, QThread &); + }; + + class DLLEXPORT InputPortInterface final : public Port + { + Q_OBJECT + + InputPortD *const d; + + public: + InputPortInterface(bool dynamic, const QString &name, BaseFilterEnvironment *env, int queueSizeSamples, double queueSizeSeconds); + virtual ~InputPortInterface(); + + SharedDataSamplePtr getData(int delaySamples=0, double delaySeconds=-1.) const; + SharedPortPtr clone(BaseFilterEnvironment *) const; + + public slots: + void receiveAsync(const QSharedPointer &sample, QSemaphore *semaphore); + void receiveSync (const QSharedPointer &sample); + + private: + void addToQueue(const SharedDataSamplePtr &sample); + }; + + class DLLEXPORT InterThreadConnection : public QObject + { + Q_OBJECT + + InterThreadConnectionD *const d; + public: + InterThreadConnection(QThread *qthread_from); + virtual ~InterThreadConnection(); + + signals: + void transmitInterThread(const QSharedPointer &sample, QSemaphore *semaphore); + + public slots: + void receiveSample(const QSharedPointer &sample); + }; + +STOP_NAMESPACE + +#endif diff --git a/nexxT/src/PropertyCollection.cpp b/nexxT/src/PropertyCollection.cpp new file mode 100644 index 0000000..1a7a87e --- /dev/null +++ b/nexxT/src/PropertyCollection.cpp @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "PropertyCollection.hpp" + +#include + +USE_NAMESPACE + +PropertyCollection::PropertyCollection() +{ +} + +PropertyCollection::~PropertyCollection() +{ +} + +void PropertyCollection::defineProperty(const QString &name, const QVariant &defaultVal, const QString &helpstr) +{ + throw std::runtime_error("not implemented."); +} + +QVariant PropertyCollection::getProperty(const QString &name) const +{ + throw std::runtime_error("not implemented."); +} + +void PropertyCollection::setProperty(const QString &name, const QVariant &variant) +{ + throw std::runtime_error("not implemented."); +} + +QString PropertyCollection::evalpath(const QString &path) const +{ + throw std::runtime_error("not implemented."); +} + diff --git a/nexxT/src/PropertyCollection.hpp b/nexxT/src/PropertyCollection.hpp new file mode 100644 index 0000000..84e4283 --- /dev/null +++ b/nexxT/src/PropertyCollection.hpp @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_PROPERTY_COLLECTION_HPP +#define NEXT_PROPERTY_COLLECTION_HPP + +#include +#include +#include "NexTLinkage.hpp" +#include "NexTConfig.hpp" + +START_NAMESPACE + class DLLEXPORT PropertyCollection : public QObject + { + Q_OBJECT + + public: + PropertyCollection(); + virtual ~PropertyCollection(); + + virtual void defineProperty(const QString &name, const QVariant &defaultVal, const QString &helpstr); + virtual QVariant getProperty(const QString &name) const; + + public slots: + virtual void setProperty(const QString &name, const QVariant &variant); + virtual QString evalpath(const QString &path) const; + + signals: + void propertyChanged(const PropertyCollection &sender, const QString &name); + }; +STOP_NAMESPACE + +#endif diff --git a/nexxT/src/SConscript.py b/nexxT/src/SConscript.py new file mode 100644 index 0000000..ca3cd26 --- /dev/null +++ b/nexxT/src/SConscript.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import sysconfig + +Import("env") + +env = env.Clone() + +srcDir = Dir(".").srcnode() +purelib = Dir(sysconfig.get_paths()['purelib']) +include = Dir(sysconfig.get_paths()['include']) +platinclude = Dir(sysconfig.get_paths()['platinclude']) + +env.Append(CPPPATH=[".", + purelib.abspath + "/shiboken2_generator/include", + purelib.abspath + "/PySide2/include/QtCore", + purelib.abspath + "/PySide2/include", + include.abspath, + platinclude.abspath, + ], + LIBPATH=[".", + sysconfig.get_paths()['stdlib'], + sysconfig.get_paths()['platstdlib'], + sysconfig.get_config_vars()['installed_platbase'] + "/libs", + sysconfig.get_paths()['purelib'] + "/shiboken2", + sysconfig.get_paths()['purelib'] + "/PySide2", + ] + ) + +if "linux" in env["target_platform"]: + env["SHIBOKEN_INCFLAGS"] = ":".join(env["CPPPATH"]) + env.Append( LINKFLAGS = Split('-z origin') ) + env.Append( RPATH = env.Literal('\\$$ORIGIN')) +else: + env["SHIBOKEN_INCFLAGS"] = ";".join(env["CPPPATH"]) + +nexT_headers = env.RegisterSources( + [srcDir.File("NexTLinkage.hpp"), + srcDir.File("DataSamples.hpp"), + srcDir.File("Filters.hpp"), + ]) +apilib = env.SharedLibrary("nexxT", env.RegisterSources(Split(""" + DataSamples.cpp + FilterEnvironment.cpp + Filters.cpp + Logger.cpp + Ports.cpp + Services.cpp + PropertyCollection.cpp + NexTPlugins.cpp +""")), CPPDEFINES=["NEXT_LIBRARY_COMPILATION"]) +env.RegisterTargets(apilib) + +spath = Dir("./cnexxT-shiboken") +targets = [] +targets += [spath.Dir("cnexxT").File("cnexxt_module_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_datasample_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_port_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_interthreadconnection_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_outputportinterface_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_inputportinterface_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_services_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_filterstate_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_filter_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_propertycollection_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_basefilterenvironment_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("nexxt_plugininterface_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("qsharedpointer_datasample_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("qsharedpointer_filter_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("qsharedpointer_port_wrapper.cpp")] +targets += [spath.Dir("cnexxT").File("qsharedpointer_qobject_wrapper.cpp")] + +env = env.Clone() +env.Append(LIBS=["nexxT"]) +if "linux" in env["target_platform"]: + env.Append(SHLINKFLAGS=["-l:libpyside2.abi3.so.$QT5VERSION", + "-l:libshiboken2.abi3.so.$QT5VERSION"]) +else: + env.Append(LIBS=["shiboken2.abi3", "pyside2.abi3"]) +dummy = env.Command(targets, env.RegisterSources(Split("cnexxT.h cnexxT.xml")), + [ + Delete("$SPATH"), + sysconfig.get_paths()["scripts"] + "/shiboken2 --generator-set=shiboken --avoid-protected-hack --output-directory=${SPATH} " + "--language-level=c++14 --include-paths=$SHIBOKEN_INCFLAGS --enable-pyside-extensions " + "--typesystem-paths=%(purelib)s/PySide2/typesystems $SOURCES" % sysconfig.get_paths(), + ], SPATH=spath) + +pyext = env.SharedLibrary("cnexxT", dummy, + SHLIBPREFIX=sysconfig.get_config_var("EXT_PREFIX"), + SHLIBSUFFIX=sysconfig.get_config_var("EXT_SUFFIX"), no_import_lib=True) +env.RegisterTargets(pyext) +Depends(dummy, apilib) + +# install python extension and library files into project directory +env.RegisterTargets(env.Install(srcDir.Dir("..").Dir("binary").Dir(env.subst("$target_platform")).Dir(env.subst("$variant")).abspath, pyext+apilib)) +env.RegisterTargets(env.Install(srcDir.Dir("..").Dir("include").abspath, Glob(srcDir.abspath + "/*.hpp"))) diff --git a/nexxT/src/Services.cpp b/nexxT/src/Services.cpp new file mode 100644 index 0000000..b2db466 --- /dev/null +++ b/nexxT/src/Services.cpp @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "Services.hpp" +#include "Logger.hpp" +#include +#include + +USE_NAMESPACE + +Services *Services::_singleton = 0; + +typedef NEXT_SHARED_PTR SharedMutexPtr; + +START_NAMESPACE + struct ServicesD + { + SharedMutexPtr mutex; + QMap map; + }; +STOP_NAMESPACE + +Services::Services() + : d(new ServicesD{SharedMutexPtr(new QMutex(QMutex::Recursive))}) +{ +} + +Services::~Services() +{ + delete d; +} + +SharedQObjectPtr Services::_getService(const QString &name) +{ + QMutexLocker locker(d->mutex.get()); + auto it = d->map.find(name); + if(it == d->map.end()) + { + if( name != "Logging" ) + { + NEXT_LOG_WARN(QString("Service %1 not found. Returning NULL.").arg(name)); + } + return SharedQObjectPtr(); + } else + { + return it.value(); + } +} + +void Services::_addService(const QString &name, const SharedQObjectPtr &service) +{ + QMutexLocker locker(d->mutex.get()); + if( (d->map.find(name) != d->map.end() ) ) + { + NEXT_LOG_WARN(QString("Service %1 already existing; automatically removing it.").arg(name)); + removeService(name); + } + NEXT_LOG_INFO(QString("adding service %1").arg(name)); + d->map[name] = service; +} + +void Services::_removeService(const QString &name) +{ + QMutexLocker locker(d->mutex.get()); + if( (d->map.find(name) == d->map.end() ) ) + { + NEXT_LOG_WARN(QString("Service %1 doesn't exist. Not removing.").arg(name)); + } + NEXT_LOG_INFO(QString("removing service %1").arg(name)); + d->map.remove(name); +} + +void Services::_removeAll() +{ + QMutexLocker locker(d->mutex.get()); + QStringList keys = d->map.keys(); + for(QString key : keys) + { + _removeService(key); + } +} + +Services *Services::singleton() +{ + if( !_singleton ) + { + _singleton = new Services(); + } + return _singleton; +} + +SharedQObjectPtr Services::getService(const QString &name) +{ + return singleton()->_getService(name); +} + +void Services::addService(const QString &name, QObject *service) +{ + SharedQObjectPtr srv = SharedQObjectPtr(service); + singleton()->_addService(name, srv); +} + +void Services::removeService(const QString &name) +{ + singleton()->_removeService(name); +} + +void Services::removeAll() +{ + singleton()->_removeAll(); +} diff --git a/nexxT/src/Services.hpp b/nexxT/src/Services.hpp new file mode 100644 index 0000000..13636dd --- /dev/null +++ b/nexxT/src/Services.hpp @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef NEXT_SERVICES_HPP +#define NEXT_SERVICES_HPP + +#include + +#include "NexTConfig.hpp" +#include "NexTLinkage.hpp" + +START_NAMESPACE + struct ServicesD; + + typedef NEXT_SHARED_PTR SharedQObjectPtr; + + class DLLEXPORT Services + { + ServicesD *d; + static Services *_singleton; + + SharedQObjectPtr _getService(const QString &name); + void _addService(const QString &name, const SharedQObjectPtr &service); + void _removeService(const QString &name); + void _removeAll(); + + static Services *singleton(); + + public: + Services(); + virtual ~Services(); + + static SharedQObjectPtr getService(const QString &name); + static void addService(const QString &name, QObject *service); + static void removeService(const QString &name); + static void removeAll(); + }; +STOP_NAMESPACE + +#endif diff --git a/nexxT/src/cnexxT.h b/nexxT/src/cnexxT.h new file mode 100644 index 0000000..4cbb5d1 --- /dev/null +++ b/nexxT/src/cnexxT.h @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#define QT_ANNOTATE_ACCESS_SPECIFIER(a) __attribute__((annotate(#a))) + +#include "DataSamples.hpp" +#include "Ports.hpp" +#include "Filters.hpp" +#include "FilterEnvironment.hpp" +#include "Services.hpp" +#include "PropertyCollection.hpp" +#include "NexTPlugins.hpp" diff --git a/nexxT/src/cnexxT.xml b/nexxT/src/cnexxT.xml new file mode 100644 index 0000000..ce6fe55 --- /dev/null +++ b/nexxT/src/cnexxT.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nexxT/tests/__init__.py b/nexxT/tests/__init__.py new file mode 100644 index 0000000..5eabbc7 --- /dev/null +++ b/nexxT/tests/__init__.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# diff --git a/nexxT/tests/core/__init__.py b/nexxT/tests/core/__init__.py new file mode 100644 index 0000000..5eabbc7 --- /dev/null +++ b/nexxT/tests/core/__init__.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# diff --git a/nexxT/tests/core/test1.json b/nexxT/tests/core/test1.json new file mode 100644 index 0000000..529de8a --- /dev/null +++ b/nexxT/tests/core/test1.json @@ -0,0 +1,58 @@ +{ + "composite_filters": [ + { + "name": "subGraph", + "nodes": [ + { + "name":"CompositeInput", + "library":"composite://port", + "factoryFunction": "CompositeInput", + "dynamicOutputPorts": ["graph_in"] + }, + { + "name":"CompositeOutput", + "library":"composite://port", + "factoryFunction": "CompositeOutput", + "dynamicInputPorts": ["graph_out"] + }, + { + "name": "filter", + "library": "pyfile://../interface/SimpleStaticFilter.py", + "factoryFunction": "SimpleStaticFilter", + "properties": {"sleep_time": 0.25} + } + ], + "connections": [ + "CompositeInput.graph_in -> filter.inPort", + "filter.outPort -> CompositeOutput.graph_out" + ] + } + ], + "applications": [ + { + "name": "testApp", + "nodes": [ + { + "name": "source", + "library": "pyfile://../interface/SimpleStaticFilter.py", + "factoryFunction": "SimpleSource", + "thread": "thread-source", + "staticOutputPorts": [ + "outPort" + ], + "properties": { + "frequency": 2 + } + }, + { + "name": "filter", + "library": "composite://ref", + "factoryFunction": "subGraph" + } + ], + "connections": [ + "source.outPort -> filter.graph_in" + ] + } + ] +} diff --git a/nexxT/tests/core/test2.json b/nexxT/tests/core/test2.json new file mode 100644 index 0000000..f74e84c --- /dev/null +++ b/nexxT/tests/core/test2.json @@ -0,0 +1,58 @@ +{ + "composite_filters": [ + { + "name": "subGraph", + "nodes": [ + { + "name":"CompositeInput", + "library":"composite://port", + "factoryFunction": "CompositeInput", + "dynamicOutputPorts": ["graph_in"] + }, + { + "name":"CompositeOutput", + "library":"composite://port", + "factoryFunction": "CompositeOutput", + "dynamicInputPorts": ["graph_out"] + }, + { + "name": "filter", + "library": "pyfile://../interface/SimpleStaticFilter.py", + "factoryFunction": "SimpleStaticFilter", + "properties": {"sleep_time": 0.25} + } + ], + "connections": [ + "CompositeInput.graph_in -> filter.inPort", + "filter.outPort -> CompositeOutput.graph_out" + ] + } + ], + "applications": [ + { + "name": "testApp", + "nodes": [ + { + "name": "source", + "library": "binary://$NEXT_CPLUGIN_PATH/test_plugins", + "factoryFunction": "SimpleSource", + "thread": "thread-source", + "staticOutputPorts": [ + "outPort" + ], + "properties": { + "frequency": 2 + } + }, + { + "name": "filter", + "library": "composite://ref", + "factoryFunction": "subGraph" + } + ], + "connections": [ + "source.outPort -> filter.graph_in" + ] + } + ] +} diff --git a/nexxT/tests/core/test_ActiveApplication.py b/nexxT/tests/core/test_ActiveApplication.py new file mode 100644 index 0000000..38b8b8c --- /dev/null +++ b/nexxT/tests/core/test_ActiveApplication.py @@ -0,0 +1,184 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from nexxT.core.ActiveApplication import ActiveApplication +from nexxT.core.Graph import FilterGraph +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +from nexxT.interface import FilterState +import os +import time +import pprint +from PySide2.QtCore import QCoreApplication, QTimer + +app = QCoreApplication.instance() +if app is None: + app = QCoreApplication() + +def simple_setup(multithread, sourceFreq, sinkTime, activeTime_s, dynamicFilter): + t = QTimer() + t.setSingleShot(True) + # timeout if test case hangs + t2 = QTimer() + t2.start((activeTime_s + 3)*1000) + + try: + class DummySubConfig(object): + def __init__(self): + self.pc = PropertyCollectionImpl("root", None) + + def getPropertyCollection(self): + return self.pc + + fg = FilterGraph(DummySubConfig()) + n1 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleSource") + p = fg.getMockup(n1).getPropertyCollectionImpl() + if multithread: + p.getChildCollection("_nexxT").setProperty("thread", "thread-2") + p.setProperty("frequency", sourceFreq) + if not dynamicFilter: + n2 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + else: + n2 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", "SimpleDynInFilter") + fg.renameNode(n2, "SimpleStaticFilter") + n2 = "SimpleStaticFilter" + fg.addDynamicInputPort(n2, "inPort") + app.processEvents() + p = fg.getMockup(n2).getPropertyCollectionImpl() + p.setProperty("sleep_time", sinkTime) + + fg.addConnection(n1, "outPort", n2, "inPort") + app.processEvents() + + if dynamicFilter: + fg.renameDynamicInputPort(n2, "inPort", "renamedInPort") + + aa = ActiveApplication(fg) + init = True + + def timeout(): + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(0) + + def timeout2(): + print("Application timeout hit!") + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(1) + t2.timeout.connect(timeout2) + t.timeout.connect(timeout) + + events = [] + def logger(object, function, datasample): + nonlocal events + events.append(dict(object=object, function=function, datasample=datasample, time=time.time())) + + def state_changed(state): + if state == FilterState.ACTIVE: + t.setSingleShot(True) + t.start(activeTime_s*1000) + elif not init and state == FilterState.CONSTRUCTED: + t.start(1000) + aa.stateChanged.connect(state_changed) + + t1 = aa._filters2threads["/SimpleSource"] + f1 = aa._threads[t1]._filters["/SimpleSource"].getPlugin() + f1.beforeTransmit = lambda ds: logger(object="SimpleSource", function="beforeTransmit", datasample=ds) + f1.afterTransmit = lambda: logger(object="SimpleSource", function="afterTransmit", datasample=None) + + t2 = aa._filters2threads["/SimpleStaticFilter"] + f2 = aa._threads[t2]._filters["/SimpleStaticFilter"].getPlugin() + f2.afterReceive = lambda ds: logger(object="SimpleStaticFilter", function="afterReceive", datasample=ds) + f2.beforeTransmit = lambda ds: logger(object="SimpleStaticFilter", function="beforeTransmit", datasample=ds) + f2.afterTransmit = lambda: logger(object="SimpleStaticFilter", function="afterTransmit", datasample=None) + + aa.init() + aa.start() + + app.exec_() + + return events + finally: + del t + del t2 + +def printEvents(events): + t0 = None + dst0 = None + for e in events: + if t0 is None: + t0 = e["time"] + if dst0 is None and e["datasample"] is not None: + dst0 = e["datasample"].getTimestamp() + print("%10.6f: %20s.%15s ds.t=%s" % (e["time"] - t0, e["object"], e["function"], e["datasample"].getTimestamp() - dst0 if e["datasample"] is not None else "")) + +def test_multiThreadSimple(): + events = simple_setup(multithread=True, sourceFreq=4.0, sinkTime=0.5, activeTime_s=2, dynamicFilter=False) + t_transmit_source = [e["time"] for e in events if e["object"] == "SimpleSource" and e["function"] == "afterTransmit"] + t_receive_sink = [e["time"] for e in events if e["object"] == "SimpleStaticFilter" and e["function"] == "afterReceive"] + try: + # t = 0.00: the sink takes the data and transmit returns instantly -> second transmit is with sourceFreq framerate + # t = 0.25: the inter thread connection buffers the data (while the sink computes) and transmit returns instantly + assert t_transmit_source[1] - t_transmit_source[0] < 0.3 + # t = 0.50: the sink computation is done, and the sink gets the second data while the semaphore is released + # t = 0.50: the inter thread connection buffers the data (while the sink computes) and transmit returns instantly + assert t_transmit_source[2] - t_transmit_source[1] < 0.3 + # t = 0.75: the source's transmit function blocks at the semaphore + # t = 1.00: the sink computation of second data is done, and the sink gets the third data while the semaphore is released + assert all([t_transmit_source[i] - t_transmit_source[i-1] > 0.4 and t_transmit_source[i] - t_transmit_source[i-1] < 0.6 for i in range(3, len(t_transmit_source))]) + # t = 1.00: the source's transmit function returns + # t = 1.00: new data at source arrived already, the source's transmit function blocks at the semaphore + # t = 1.50: the sink computation of third data is done, and the sink gets the fourth data while the semaphore is released + # t = 1.50: the source's transmit function returns + # t = 1.50: new data at source arrived already, the source's transmit function blocks. + # ... and so on + assert len(t_transmit_source) >= 3 + (2-0.5)/0.5 - 1 + assert len(t_receive_sink) in [len(t_transmit_source), len(t_transmit_source)-1] + assert all([t_receive_sink[i] - t_receive_sink[i-1] > 0.4 and t_receive_sink[i] - t_receive_sink[i-1] < 0.6 for i in range(1, len(t_receive_sink))]) + except: + printEvents(events) + raise + +def test_singleThreadSimple(): + events = simple_setup(multithread=False, sourceFreq=4.0, sinkTime=0.5, activeTime_s = 2, dynamicFilter=False) + t_transmit_source = [e["time"] for e in events if e["object"] == "SimpleSource" and e["function"] == "afterTransmit"] + t_receive_sink = [e["time"] for e in events if e["object"] == "SimpleStaticFilter" and e["function"] == "afterReceive"] + try: + # because the receiver is in same thread than transmitter, we effectively have a framerate of 2 Hz + assert all([t_transmit_source[i] - t_transmit_source[i-1] > 0.4 and t_transmit_source[i] - t_transmit_source[i-1] < 0.6 for i in range(1, len(t_transmit_source))]) + assert len(t_transmit_source) >= 2/0.5 - 1 + assert len(t_receive_sink) == len(t_transmit_source) + assert all([t_receive_sink[i] - t_receive_sink[i-1] > 0.4 and t_receive_sink[i] - t_receive_sink[i-1] < 0.6 for i in range(1, len(t_receive_sink))]) + except: + printEvents(events) + raise + +def test_singleThreadDynamic(): + events = simple_setup(multithread=False, sourceFreq=4.0, sinkTime=0.5, activeTime_s = 2, dynamicFilter=True) + t_transmit_source = [e["time"] for e in events if e["object"] == "SimpleSource" and e["function"] == "afterTransmit"] + t_receive_sink = [e["time"] for e in events if e["object"] == "SimpleStaticFilter" and e["function"] == "afterReceive"] + try: + # because the receiver is in same thread than transmitter, we effectively have a framerate of 2 Hz + assert all([t_transmit_source[i] - t_transmit_source[i-1] > 0.4 and t_transmit_source[i] - t_transmit_source[i-1] < 0.6 for i in range(1, len(t_transmit_source))]) + assert len(t_transmit_source) >= 2/0.5 - 1 + assert len(t_receive_sink) == len(t_transmit_source) + assert all([t_receive_sink[i] - t_receive_sink[i-1] > 0.4 and t_receive_sink[i] - t_receive_sink[i-1] < 0.6 for i in range(1, len(t_receive_sink))]) + except: + printEvents(events) + raise + +if __name__ == "__main__": + test_singleThreadDynamic() + test_singleThreadSimple() + test_multiThreadSimple() diff --git a/nexxT/tests/core/test_BaseGraph.py b/nexxT/tests/core/test_BaseGraph.py new file mode 100644 index 0000000..0a097a3 --- /dev/null +++ b/nexxT/tests/core/test_BaseGraph.py @@ -0,0 +1,172 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from nexxT.core.BaseGraph import BaseGraph + +def expect_exception(f, *args, **kw): + ok = False + try: + f(*args, **kw) + except: + ok = True + assert ok + +def test_smoke(): + signals_received = [] + def trace_signal(args, signal): + signals_received.append( (signal,) + args ) + + g = BaseGraph() + for s in [ "nodeAdded", + "nodeRenamed", + "nodeDeleted", + "inPortAdded", + "inPortRenamed", + "inPortDeleted", + "outPortAdded", + "outPortRenamed", + "outPortDeleted", + "connectionAdded", + "connectionDeleted"]: + getattr(g, s).connect(lambda *args, signal=s: trace_signal(args, signal)) + + g.addNode("n1") + assert signals_received == [("nodeAdded", "n1")] + signals_received.clear() + + g.addNode("n2") + assert signals_received == [("nodeAdded", "n2")] + signals_received.clear() + + expect_exception(g.addNode, "n1") + + g.renameNode("n1", "n3") + assert signals_received == [("nodeRenamed", "n1", "n3")] + signals_received.clear() + + g.renameNode("n3", "n1") + assert signals_received == [("nodeRenamed", "n3", "n1")] + signals_received.clear() + + expect_exception(g.renameNode, "n1", "n2") + expect_exception(g.renameNode, "n2", "n1") + expect_exception(g.renameNode, "non_existing", "nonono") + + g.addInputPort("n2", "i1") + assert signals_received == [("inPortAdded", "n2", "i1")] + signals_received.clear() + + expect_exception(g.addInputPort, "n3", "i1") + expect_exception(g.addInputPort, "n2", "i1") + + g.addOutputPort("n1", "o1") + assert signals_received == [("outPortAdded", "n1", "o1")] + signals_received.clear() + + expect_exception(g.addOutputPort, "n3", "o1") + expect_exception(g.addOutputPort, "n1", "o1") + + g.addConnection("n1", "o1", "n2", "i1") + assert signals_received == [("connectionAdded", "n1", "o1", "n2", "i1")] + signals_received.clear() + + expect_exception(g.addConnection, "n3", "o1", "n2", "i1") + expect_exception(g.addConnection, "n1", "o2", "n2", "i1") + expect_exception(g.addConnection, "n1", "o1", "n3", "i1") + expect_exception(g.addConnection, "n1", "o1", "n2", "i2") + + g.renameNode("n1", "n3") + assert signals_received == [("nodeRenamed", "n1", "n3")] + assert g._connections == [("n3","o1","n2","i1")] + signals_received.clear() + + g.renameNode("n3", "n1") + assert signals_received == [("nodeRenamed", "n3", "n1")] + assert g._connections == [("n1","o1","n2","i1")] + signals_received.clear() + + g.renameNode("n2", "n3") + assert signals_received == [("nodeRenamed", "n2", "n3")] + assert g._connections == [("n1","o1","n3","i1")] + signals_received.clear() + + g.renameNode("n3", "n2") + assert signals_received == [("nodeRenamed", "n3", "n2")] + assert g._connections == [("n1","o1","n2","i1")] + signals_received.clear() + + g.renameInputPort("n2", "i1", "i2") + assert signals_received == [("inPortRenamed", "n2", "i1", "i2")] + assert g._connections == [("n1","o1","n2","i2")] + signals_received.clear() + + g.renameInputPort("n2", "i2", "i1") + assert signals_received == [("inPortRenamed", "n2", "i2", "i1")] + assert g._connections == [("n1","o1","n2","i1")] + signals_received.clear() + + g.renameOutputPort("n1", "o1", "o2") + assert signals_received == [("outPortRenamed", "n1", "o1", "o2")] + assert g._connections == [("n1","o2","n2","i1")] + signals_received.clear() + + g.renameOutputPort("n1", "o2", "o1") + assert signals_received == [("outPortRenamed", "n1", "o2", "o1")] + assert g._connections == [("n1","o1","n2","i1")] + signals_received.clear() + + expect_exception(g.deleteConnection, "n2","o1","n2","i1") + expect_exception(g.deleteConnection, "n1","o2","n2","i1") + expect_exception(g.deleteConnection, "n1","o1","n1","i1") + expect_exception(g.deleteConnection, "n1","o1","n2","i2") + + g.deleteConnection("n1","o1","n2","i1") + assert signals_received == [("connectionDeleted", "n1","o1","n2","i1")] + signals_received.clear() + + g.addConnection("n1","o1","n2","i1") + assert signals_received == [("connectionAdded", "n1", "o1", "n2", "i1")] + signals_received.clear() + + expect_exception(g.deleteNode, "n3") + + g.deleteNode("n1") + assert signals_received == [("connectionDeleted", "n1","o1","n2","i1"), ("outPortDeleted", "n1", "o1"), ("nodeDeleted", "n1")] + signals_received.clear() + + g.addNode("n1") + g.addOutputPort("n1", "o1") + g.addConnection("n1","o1","n2","i1") + signals_received.clear() + g.deleteNode("n2") + assert signals_received == [("connectionDeleted", "n1","o1","n2","i1"), ("inPortDeleted", "n2", "i1"), ("nodeDeleted", "n2")] + signals_received.clear() + + expect_exception(g.deleteInputPort, "n1", "i1") + expect_exception(g.deleteInputPort, "n3", "i1") + + expect_exception(g.deleteOutputPort, "n2", "o1") + expect_exception(g.deleteOutputPort, "n3", "o1") + + assert signals_received ==[] + + g.addNode("n2") + g.addInputPort("n2", "i1") + g.addConnection("n1","o1","n2","i1") + signals_received.clear() + g.deleteInputPort("n2", "i1") + assert signals_received == [("connectionDeleted", "n1", "o1", "n2", "i1"), ("inPortDeleted", "n2", "i1")] + signals_received.clear() + + g.addInputPort("n2", "i1") + g.addConnection("n1","o1","n2","i1") + signals_received.clear() + g.deleteOutputPort("n1", "o1") + assert signals_received == [("connectionDeleted", "n1", "o1", "n2", "i1"), ("outPortDeleted", "n1", "o1")] + signals_received.clear() + +if __name__ == "__main__": + test_smoke() \ No newline at end of file diff --git a/nexxT/tests/core/test_CompositeFilter.py b/nexxT/tests/core/test_CompositeFilter.py new file mode 100644 index 0000000..fe6c69d --- /dev/null +++ b/nexxT/tests/core/test_CompositeFilter.py @@ -0,0 +1,308 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import os +import time +from PySide2.QtCore import QCoreApplication, QTimer +from nexxT.interface import FilterState +from nexxT.core.CompositeFilter import CompositeFilter +from nexxT.core.Application import Application +from nexxT.core.ActiveApplication import ActiveApplication +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +from nexxT.core.Exceptions import CompositeRecursion +from nexxT.core.Configuration import Configuration + +app = QCoreApplication.instance() +if app is None: + app = QCoreApplication() + +def expect_exception(excClass, f, *args, **kw): + ok = False + try: + f(*args, **kw) + except excClass: + ok = True + assert ok + +def simple_setup(sourceFreq, activeTime_s): + t = QTimer() + t.setSingleShot(True) + # timeout if test case hangs + t2 = QTimer() + t2.start((activeTime_s + 3)*1000) + try: + config = Configuration() + cf_inner = CompositeFilter("cf_inner", config) + cg_inner = cf_inner.getGraph() + f1 = cg_inner.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + cg_inner.addDynamicOutputPort("CompositeInput", "compositeIn") + cg_inner.addDynamicInputPort("CompositeOutput", "compositeOut") + app.processEvents() + cg_inner.addConnection("CompositeInput", "compositeIn", f1, "inPort") + cg_inner.addConnection(f1, "outPort", "CompositeOutput", "compositeOut") + + cf = CompositeFilter("cf", config) + cg = cf.getGraph() + f1 = cg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + f2 = cg.addNode(cf_inner, "compositeNode") + app.processEvents() + + cg.addDynamicOutputPort("CompositeInput", "compositeIn") + cg.addDynamicInputPort("CompositeOutput", "compositeOut") + app.processEvents() + + cg.addConnection("CompositeInput", "compositeIn", f1, "inPort") + cg.addConnection(f1, "outPort", f2, "compositeIn") + cg.addConnection(f2, "compositeOut", "CompositeOutput", "compositeOut") + + expect_exception(CompositeRecursion, cg.addNode, cf, "compositeNode") + expect_exception(CompositeRecursion, cg_inner.addNode, cf, "compositeNode") + + a = Application("app", config) + ag = a.getGraph() + cn = ag.addNode(cf, "compositeNode") + + app.processEvents() + app.processEvents() + + cn_ip = [p.name() for p in ag.getMockup(cn).getAllInputPorts()] + cn_op = [p.name() for p in ag.getMockup(cn).getAllOutputPorts()] + assert cn_ip == ["compositeIn"] + assert cn_op == ["compositeOut"] + + sn = ag.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleSource") + p = ag.getMockup(sn).propertyCollection() + p.setProperty("frequency", sourceFreq) + ag.addConnection(sn, "outPort", cn, "compositeIn") + fn = ag.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + ag.addConnection(cn, "compositeOut", fn, "inPort") + + cg.renameDynamicInputPort("CompositeOutput", "compositeOut", "renamedOut") + app.processEvents() + cg.renameDynamicOutputPort("CompositeInput", "compositeIn", "renamedIn") + app.processEvents() + + aa = ActiveApplication(ag) + init = True + + def timeout(): + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(0) + + def timeout2(): + print("Application timeout hit!") + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(1) + t2.timeout.connect(timeout2) + t.timeout.connect(timeout) + + events = [] + def logger(object, function, datasample): + nonlocal events + events.append(dict(object=object, function=function, datasample=datasample, time=time.time())) + + def state_changed(state): + if state == FilterState.ACTIVE: + t.setSingleShot(True) + t.start(activeTime_s*1000) + elif not init and state == FilterState.CONSTRUCTED: + t.start(1000) + aa.stateChanged.connect(state_changed) + + t1 = aa._filters2threads["/SimpleSource"] + f1 = aa._threads[t1]._filters["/SimpleSource"].getPlugin() + f1.beforeTransmit = lambda ds: logger(object="SimpleSource", function="beforeTransmit", datasample=ds) + f1.afterTransmit = lambda: logger(object="SimpleSource", function="afterTransmit", datasample=None) + + t2 = aa._filters2threads["/SimpleStaticFilter"] + f2 = aa._threads[t2]._filters["/SimpleStaticFilter"].getPlugin() + f2.afterReceive = lambda ds: logger(object="SimpleStaticFilter", function="afterReceive", datasample=ds) + f2.beforeTransmit = lambda ds: logger(object="SimpleStaticFilter", function="beforeTransmit", datasample=ds) + f2.afterTransmit = lambda: logger(object="SimpleStaticFilter", function="afterTransmit", datasample=None) + + aa.init() + aa.start() + + app.exec_() + + return events + finally: + del t + del t2 + +def printEvents(events): + t0 = None + dst0 = None + for e in events: + if t0 is None: + t0 = e["time"] + if dst0 is None and e["datasample"] is not None: + dst0 = e["datasample"].getTimestamp() + print("%10.6f: %20s.%15s ds.t=%s" % (e["time"] - t0, e["object"], e["function"], e["datasample"].getTimestamp() - dst0 if e["datasample"] is not None else "")) + +def test_smoke(): + events = simple_setup(sourceFreq=2.0, activeTime_s=2) + t_transmit_source = [e["time"] for e in events if + e["object"] == "SimpleSource" and e["function"] == "afterTransmit"] + t_receive_sink = [e["time"] for e in events if + e["object"] == "SimpleStaticFilter" and e["function"] == "afterReceive"] + try: + # because the receiver is in same thread than transmitter, we effectively have a framerate of 2 Hz + assert all([t_transmit_source[i] - t_transmit_source[i - 1] > 0.4 and t_transmit_source[i] - t_transmit_source[ + i - 1] < 0.6 for i in range(1, len(t_transmit_source))]) + assert len(t_transmit_source) >= 4-1 + assert len(t_receive_sink) == len(t_transmit_source) + assert all( + [t_receive_sink[i] - t_receive_sink[i - 1] > 0.4 and t_receive_sink[i] - t_receive_sink[i - 1] < 0.6 for i + in range(1, len(t_receive_sink))]) + except: + printEvents(events) + raise + +def test_recursion(): + config = Configuration() + cf_inner = CompositeFilter("cf_inner", config) + cg_inner = cf_inner.getGraph() + f1 = cg_inner.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + + cf = CompositeFilter("cf", config) + cg = cf.getGraph() + f1 = cg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + f2 = cg.addNode(cf_inner, "compositeNode") + + # add composite node to itself + cg_oldNodes = set(cg.allNodes()) + cg_inner_oldNodes = set(cg_inner.allNodes()) + expect_exception(CompositeRecursion, cg.addNode, cf, "compositeNode") + assert cg_oldNodes == set(cg.allNodes()) + + # add composite node to an inner node + expect_exception(CompositeRecursion, cg_inner.addNode, cf, "compositeNode") + assert cg_inner_oldNodes == set(cg_inner.allNodes()) + + # double dependency + cf1 = CompositeFilter("cf1", config) + cf2 = CompositeFilter("cf2", config) + cf1.getGraph().addNode(cf2, "compositeNode") + expect_exception(CompositeRecursion, cf2.getGraph().addNode, cf1, "compositeNode") + +def test_doubleNames(): + activeTime_s = 2 + sourceFreq = 2 + t = QTimer() + t.setSingleShot(True) + # timeout if test case hangs + t2 = QTimer() + t2.start((activeTime_s + 3)*1000) + try: + config = Configuration() + cf_inner = CompositeFilter("cf_inner", config) + cg_inner = cf_inner.getGraph() + f1 = cg_inner.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + cg_inner.addDynamicOutputPort("CompositeInput", "compositeIn") + cg_inner.addDynamicInputPort("CompositeOutput", "compositeOut") + app.processEvents() + cg_inner.addConnection("CompositeInput", "compositeIn", f1, "inPort") + cg_inner.addConnection(f1, "outPort", "CompositeOutput", "compositeOut") + + a = Application("app", config) + ag = a.getGraph() + cn = ag.addNode(cf_inner, "compositeNode") + #f2 = ag.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + + app.processEvents() + app.processEvents() + + cn_ip = [p.name() for p in ag.getMockup(cn).getAllInputPorts()] + cn_op = [p.name() for p in ag.getMockup(cn).getAllOutputPorts()] + assert cn_ip == ["compositeIn"] + assert cn_op == ["compositeOut"] + + sn = ag.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleSource") + p = ag.getMockup(sn).propertyCollection() + p.setProperty("frequency", sourceFreq) + ag.addConnection(sn, "outPort", cn, "compositeIn") + fn = ag.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + ag.addConnection(cn, "compositeOut", fn, "inPort") + + cg_inner.renameDynamicInputPort("CompositeOutput", "compositeOut", "renamedOut") + app.processEvents() + cg_inner.renameDynamicOutputPort("CompositeInput", "compositeIn", "renamedIn") + app.processEvents() + + aa = ActiveApplication(ag) + init = True + + def timeout(): + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(0) + + def timeout2(): + print("Application timeout hit!") + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(1) + t2.timeout.connect(timeout2) + t.timeout.connect(timeout) + + events = [] + def logger(object, function, datasample): + nonlocal events + events.append(dict(object=object, function=function, datasample=datasample, time=time.time())) + + def state_changed(state): + if state == FilterState.ACTIVE: + t.setSingleShot(True) + t.start(activeTime_s*1000) + elif not init and state == FilterState.CONSTRUCTED: + t.start(1000) + aa.stateChanged.connect(state_changed) + + t1 = aa._filters2threads["/SimpleSource"] + f1 = aa._threads[t1]._filters["/SimpleSource"].getPlugin() + f1.beforeTransmit = lambda ds: logger(object="SimpleSource", function="beforeTransmit", datasample=ds) + f1.afterTransmit = lambda: logger(object="SimpleSource", function="afterTransmit", datasample=None) + + t2 = aa._filters2threads["/SimpleStaticFilter"] + f2 = aa._threads[t2]._filters["/SimpleStaticFilter"].getPlugin() + f2.afterReceive = lambda ds: logger(object="SimpleStaticFilter", function="afterReceive", datasample=ds) + f2.beforeTransmit = lambda ds: logger(object="SimpleStaticFilter", function="beforeTransmit", datasample=ds) + f2.afterTransmit = lambda: logger(object="SimpleStaticFilter", function="afterTransmit", datasample=None) + + aa.init() + aa.start() + + app.exec_() + + return events + finally: + del t + del t2 + + +if __name__ == "__main__": + test_recursion() + test_smoke() + test_doubleNames() diff --git a/nexxT/tests/core/test_ConfigFiles.py b/nexxT/tests/core/test_ConfigFiles.py new file mode 100644 index 0000000..bd7df4c --- /dev/null +++ b/nexxT/tests/core/test_ConfigFiles.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import json +import logging +from pathlib import Path +import pytest +from PySide2.QtCore import QCoreApplication, QTimer +from nexxT.interface import FilterState, Services +from nexxT.core.ConfigFiles import ConfigFileLoader +from nexxT.core.Application import Application +from nexxT.core.Configuration import Configuration +import nexxT + +app = QCoreApplication.instance() +if app is None: + app = QCoreApplication() + +def simple_setup(activeTime_s): + t = QTimer() + t.setSingleShot(True) + # timeout if test case hangs + t2 = QTimer() + t2.start((activeTime_s + 3)*1000) + try: + if nexxT.useCImpl: + test_json = Path(__file__).parent / "test2.json" + else: + test_json = Path(__file__).parent / "test1.json" + config = Configuration() + ConfigFileLoader.load(config, test_json) + ConfigFileLoader.save(config, test_json.parent / "test1.saved.json") + config.activate("testApp") + app.processEvents() + + aa = Application.activeApplication + + init = True + def timeout(): + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(0) #logging.INTERNAL = INTERNAL + + + def timeout2(): + print("Application timeout hit!") + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(1) + t2.timeout.connect(timeout2) + t.timeout.connect(timeout) + def state_changed(state): + if state == FilterState.ACTIVE: + t.setSingleShot(True) + t.start(activeTime_s*1000) + elif not init and state == FilterState.CONSTRUCTED: + t.start(1000) + aa.stateChanged.connect(state_changed) + + aa.init() + aa.start() + + app.exec_() + finally: + del t + del t2 + +def test_smoke(): + simple_setup(2) + simple_setup(4) + +if __name__ == "__main__": + test_smoke() diff --git a/nexxT/tests/core/test_FilterEnvironment.py b/nexxT/tests/core/test_FilterEnvironment.py new file mode 100644 index 0000000..a85a439 --- /dev/null +++ b/nexxT/tests/core/test_FilterEnvironment.py @@ -0,0 +1,196 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from nexxT.core.FilterEnvironment import FilterEnvironment +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +from nexxT.interface import Port, InputPort, OutputPort, FilterState, DataSample +from nexxT import useCImpl +import os + +def expect_exception(f, *args, **kw): + ok = False + try: + f(*args, **kw) + except: + ok = True + assert ok + +def test_static_filter(): + function_calls = [] + with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter", + PropertyCollectionImpl("root", None) ) as staticPyFilter: + f = staticPyFilter.getPlugin() + + origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) + for callback in origCallbacks: + setattr(f, callback, + lambda *args, callback=callback: function_calls.append((callback, origCallbacks[callback](*args)))) + + def exceptionCallback(*args): + raise RuntimeError() + + assert staticPyFilter.getDynamicInputPorts() == [] + assert staticPyFilter.getDynamicOutputPorts() == [] + sip = staticPyFilter.getStaticInputPorts() + assert len(sip) == 1 + assert sip[0].name() == "inPort" + assert sip[0] is staticPyFilter.getInputPort("inPort") + sop = staticPyFilter.getStaticOutputPorts() + assert len(sop) == 1 + assert sop[0].name() == "outPort" + assert sop[0] is staticPyFilter.getOutputPort("outPort") + expect_exception(staticPyFilter.addPort, InputPort(True, "dynOutPort", staticPyFilter)) + expect_exception(staticPyFilter.addPort, OutputPort(True, "dynOutPort", staticPyFilter)) + assert staticPyFilter.state() == FilterState.CONSTRUCTED + + assert len(function_calls) == 0 + expect_exception(staticPyFilter.start) + expect_exception(staticPyFilter.stop) + expect_exception(staticPyFilter.deinit) + staticPyFilter.init() + assert function_calls == [("onInit", None)] + assert staticPyFilter.state() == FilterState.INITIALIZED + function_calls.clear() + + expect_exception(staticPyFilter.init) + expect_exception(staticPyFilter.stop) + staticPyFilter.start() + assert function_calls == [("onStart", None)] + assert staticPyFilter.state() == FilterState.ACTIVE + function_calls.clear() + + assert len(function_calls) == 0 + expect_exception(staticPyFilter.init) + expect_exception(staticPyFilter.start) + expect_exception(staticPyFilter.deinit) + staticPyFilter.stop() + assert function_calls == [("onStop", None)] + assert staticPyFilter.state() == FilterState.INITIALIZED + function_calls.clear() + + assert len(function_calls) == 0 + staticPyFilter.deinit() + assert function_calls == [("onDeinit", None)] + assert staticPyFilter.state() == FilterState.CONSTRUCTED + function_calls.clear() + + # check exception call backs + f.onInit = exceptionCallback + staticPyFilter.init() + assert staticPyFilter.state() == FilterState.INITIALIZED + staticPyFilter.deinit() + function_calls.clear() + + # check auto cleanup functionality + with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter", + PropertyCollectionImpl("root", None) ) as staticPyFilter: + f = staticPyFilter.getPlugin() + origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) + for callback in origCallbacks: + setattr(f, callback, + lambda *args, callback=callback: function_calls.append((callback, origCallbacks[callback](*args)))) + + assert len(function_calls) == 0 + + with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter", + PropertyCollectionImpl("root", None) ) as staticPyFilter: + f = staticPyFilter.getPlugin() + origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) + for callback in origCallbacks: + setattr(f, callback, + lambda *args, callback=callback: function_calls.append((callback, origCallbacks[callback](*args)))) + staticPyFilter.init() + + assert function_calls == [("onInit", None), ("onDeinit", None)] + function_calls.clear() + + with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter", + PropertyCollectionImpl("root", None) ) as staticPyFilter: + f = staticPyFilter.getPlugin() + origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) + for callback in origCallbacks: + setattr(f, callback, + lambda *args, callback=callback: function_calls.append((callback, origCallbacks[callback](*args)))) + staticPyFilter.init() + staticPyFilter.start() + + assert function_calls == [("onInit", None), ("onStart", None), ("onStop", None), ("onDeinit", None)] + function_calls.clear() + + expect_exception(FilterEnvironment, "weird.plugin.extension", "factory", PropertyCollectionImpl("root", None) ) + +def test_dynamic_in_filter(): + with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", "SimpleDynInFilter", + PropertyCollectionImpl("root", None) ) as dynInPyFilter: + f = dynInPyFilter.getPlugin() + + dip = InputPort(True, "dynInPort", dynInPyFilter) + dynInPyFilter.addPort(dip) + expect_exception(dynInPyFilter.addPort, dip) + dop = OutputPort(True, "dynOutPort", dynInPyFilter) + expect_exception(dynInPyFilter.addPort, dop) + + if useCImpl: + assert [p.data() for p in dynInPyFilter.getDynamicInputPorts()] == [dip.data()] + else: + assert dynInPyFilter.getDynamicInputPorts() == [dip] + assert dynInPyFilter.getDynamicOutputPorts() == [] + sip = dynInPyFilter.getStaticInputPorts() + assert len(sip) == 0 + sop = dynInPyFilter.getStaticOutputPorts() + assert len(sop) == 1 + assert sop[0].name() == "outPort" + assert sop[0] is dynInPyFilter.getOutputPort("outPort") + assert dynInPyFilter.state() == FilterState.CONSTRUCTED + + dynInPyFilter.init() + dip2 = InputPort(True, "dynInPort2", dynInPyFilter) + expect_exception(dynInPyFilter.addPort, dip2) + + dynInPyFilter.start() + expect_exception(dynInPyFilter.addPort, dip2) + +def test_dynamic_out_filter(): + with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", "SimpleDynOutFilter", + PropertyCollectionImpl("root", None) ) as dynOutPyFilter: + f = dynOutPyFilter.getPlugin() + + dip = InputPort(True, "dynInPort", dynOutPyFilter) + expect_exception(dynOutPyFilter.addPort, dip) + dop = OutputPort(True, "dynOutPort", dynOutPyFilter) + dynOutPyFilter.addPort(dop) + expect_exception(dynOutPyFilter.addPort, dop) + + assert dynOutPyFilter.getDynamicInputPorts() == [] + if useCImpl: + assert [p.data() for p in dynOutPyFilter.getDynamicOutputPorts()] == [dop.data()] + else: + assert dynOutPyFilter.getDynamicOutputPorts() == [dop] + sip = dynOutPyFilter.getStaticInputPorts() + assert len(sip) == 1 + assert sip[0].name() == "inPort" + assert sip[0] is dynOutPyFilter.getInputPort("inPort") + sop = dynOutPyFilter.getStaticOutputPorts() + assert len(sop) == 1 + assert sop[0].name() == "outPort" + assert sop[0] is dynOutPyFilter.getOutputPort("outPort") + assert dynOutPyFilter.state() == FilterState.CONSTRUCTED + + dynOutPyFilter.init() + dop2 = OutputPort(True, "dynOutPort2", dynOutPyFilter) + expect_exception(dynOutPyFilter.addPort, dop2) + + dynOutPyFilter.start() + expect_exception(dynOutPyFilter.addPort, dop2) + +if __name__ == "__main__": + test_static_filter() + #test_dynamic_in_filter() + #test_dynamic_out_filter() diff --git a/nexxT/tests/core/test_FilterExceptions.py b/nexxT/tests/core/test_FilterExceptions.py new file mode 100644 index 0000000..20f0e27 --- /dev/null +++ b/nexxT/tests/core/test_FilterExceptions.py @@ -0,0 +1,375 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import json +import logging +from pathlib import Path +import pytest +from PySide2.QtCore import QCoreApplication, QTimer +from nexxT.interface import FilterState, Services +from nexxT.core.ConfigFiles import ConfigFileLoader +from nexxT.core.Application import Application +from nexxT.core.Configuration import Configuration +import nexxT + +app = QCoreApplication.instance() +if app is None: + app = QCoreApplication() + +def exception_setup(python, thread, where, activeTime_s): + logging.getLogger(__name__).info("------------------------------------------------------") + logging.getLogger(__name__).info("Starting exception_setup %d %s %s %f", python, thread, where, activeTime_s) + from nexxT.services.ConsoleLogger import ConsoleLogger + logger = ConsoleLogger() + Services.addService("Logging", logger) + class LogCollector(logging.StreamHandler): + def __init__(self): + super().__init__() + self.logs = [] + def emit(self, record): + self.logs.append(record) + collector = LogCollector() + logging.getLogger().addHandler(collector) + try: + t = QTimer() + t.setSingleShot(True) + # timeout if test case hangs + t2 = QTimer() + t2.start((activeTime_s + 3)*1000) + try: + test_json = Path(__file__).parent / "test_except_constr.json" + with test_json.open("r", encoding='utf-8') as fp: + cfg = json.load(fp) + if nexxT.useCImpl and not python: + cfg["composite_filters"][0]["nodes"][2]["library"] = "binary://$NEXT_CPLUGIN_PATH/test_plugins" + cfg["composite_filters"][0]["nodes"][2]["thread"] = thread + cfg["composite_filters"][0]["nodes"][2]["properties"]["whereToThrow"] = where + mod_json = Path(__file__).parent / "test_except_constr_tmp.json" + with mod_json.open("w", encoding="utf-8") as fp: + json.dump(cfg, fp) + + config = Configuration() + ConfigFileLoader.load(config, mod_json) + config.activate("testApp") + app.processEvents() + + aa = Application.activeApplication + + init = True + def timeout(): + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + app.exit(0) + + def timeout2(): + print("Application timeout hit!") + nonlocal init + if init: + init = False + aa.stop() + aa.deinit() + else: + print("application exit!") + app.exit(1) + t2.timeout.connect(timeout2) + t.timeout.connect(timeout) + def state_changed(state): + if state == FilterState.ACTIVE: + t.setSingleShot(True) + t.start(activeTime_s*1000) + elif not init and state == FilterState.CONSTRUCTED: + t.start(1000) + aa.stateChanged.connect(state_changed) + + aa.init() + aa.start() + + app.exec_() + finally: + del t + del t2 + finally: + logging.getLogger().removeHandler(collector) + Services.removeAll() + return collector.logs + +def test_exception_python_main_none(): + logs = exception_setup(True, "main", "nowhere", 2) + +# --------------- +# port exceptions +# --------------- + +def test_exception_python_main_port(): + logs = exception_setup(True, "main", "port", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) > 0 + assert all(e == "Uncaught exception" for e in errors) + +def test_exception_python_source_port(): + logs = exception_setup(True, "thread-source", "port", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) > 0 + assert all(e == "Uncaught exception" for e in errors) + +def test_exception_python_compute_port(): + logs = exception_setup(True, "compute", "port", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) > 0 + assert all(e == "Uncaught exception" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_main_port(): + logs = exception_setup(False, "main", "port", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) > 0 + assert all(e == "Unexpected exception during onPortDataChanged from filter filter: exception in port" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_source_port(): + logs = exception_setup(False, "thread-source", "port", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) > 0 + assert all(e == "Unexpected exception during onPortDataChanged from filter filter: exception in port" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_compute_port(): + logs = exception_setup(False, "compute", "port", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) > 0 + assert all(e == "Unexpected exception during onPortDataChanged from filter filter: exception in port" for e in errors) + +# --------------- +# init exceptions +# --------------- + +def test_exception_python_main_init(): + logs = exception_setup(True, "main", "init", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation INITIALIZING of filter filter" for e in errors) + +def test_exception_python_source_init(): + logs = exception_setup(True, "thread-source", "init", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation INITIALIZING of filter filter" for e in errors) + +def test_exception_python_compute_init(): + logs = exception_setup(True, "compute", "init", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation INITIALIZING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_main_init(): + logs = exception_setup(False, "main", "init", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation INITIALIZING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_source_init(): + logs = exception_setup(False, "thread-source", "init", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation INITIALIZING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_compute_init(): + logs = exception_setup(False, "compute", "init", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation INITIALIZING of filter filter" for e in errors) + +# --------------- +# start exceptions +# --------------- + +def test_exception_python_main_start(): + logs = exception_setup(True, "main", "start", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STARTING of filter filter" for e in errors) + +def test_exception_python_source_start(): + logs = exception_setup(True, "thread-source", "start", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STARTING of filter filter" for e in errors) + +def test_exception_python_compute_start(): + logs = exception_setup(True, "compute", "start", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STARTING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_main_start(): + logs = exception_setup(False, "main", "start", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STARTING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_source_start(): + logs = exception_setup(False, "thread-source", "start", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STARTING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_compute_start(): + logs = exception_setup(False, "compute", "start", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STARTING of filter filter" for e in errors) + +# --------------- +# stop exceptions +# --------------- + +def test_exception_python_main_stop(): + logs = exception_setup(True, "main", "stop", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STOPPING of filter filter" for e in errors) + +def test_exception_python_source_stop(): + logs = exception_setup(True, "thread-source", "stop", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STOPPING of filter filter" for e in errors) + +def test_exception_python_compute_stop(): + logs = exception_setup(True, "compute", "stop", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STOPPING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_main_stop(): + logs = exception_setup(False, "main", "stop", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STOPPING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_source_stop(): + logs = exception_setup(False, "thread-source", "stop", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STOPPING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_compute_stop(): + logs = exception_setup(False, "compute", "stop", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert len(errors) == 1 + assert all(e == "Exception while executing operation STOPPING of filter filter" for e in errors) + +# --------------- +# deinit exceptions +# --------------- + +def test_exception_python_main_deinit(): + logs = exception_setup(True, "main", "deinit", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation DEINITIALIZING of filter filter" for e in errors) + +def test_exception_python_source_deinit(): + logs = exception_setup(True, "thread-source", "deinit", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation DEINITIALIZING of filter filter" for e in errors) + +def test_exception_python_compute_deinit(): + logs = exception_setup(True, "compute", "deinit", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation DEINITIALIZING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_main_deinit(): + logs = exception_setup(False, "main", "deinit", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation DEINITIALIZING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_source_deinit(): + logs = exception_setup(False, "thread-source", "deinit", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation DEINITIALIZING of filter filter" for e in errors) + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_compute_deinit(): + logs = exception_setup(False, "compute", "deinit", 2) + errors = [r.message for r in logs if r.levelno >= logging.ERROR] + assert 1 <= len(errors) <= 3 + assert all(e == "Exception while executing operation DEINITIALIZING of filter filter" for e in errors) + +# --------------- +# constructor exceptions +# --------------- + +def test_exception_python_main_constr(): + try: + logs = exception_setup(True, "main", "constructor", 2) + exception = False + except Exception as e: + exception = True + assert exception + +def test_exception_python_source_constr(): + try: + logs = exception_setup(True, "thread-source", "constructor", 2) + exception = False + except Exception as e: + exception = True + assert exception + +def test_exception_python_compute_constr(): + try: + logs = exception_setup(True, "compute", "constructor", 2) + exception = False + except Exception as e: + exception = True + assert exception + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_main_constr(): + try: + logs = exception_setup(False, "main", "constructor", 2) + exception = False + except Exception as e: + exception = True + assert exception + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_source_constr(): + try: + logs = exception_setup(False, "thread-source", "constructor", 2) + exception = False + except Exception as e: + exception = True + assert exception + +@pytest.mark.skipif(not nexxT.useCImpl, reason="python only test") +def test_exception_c_compute_constr(): + try: + logs = exception_setup(False, "compute", "constructor", 2) + exception = False + except Exception as e: + exception = True + assert exception + diff --git a/nexxT/tests/core/test_FilterMockup.py b/nexxT/tests/core/test_FilterMockup.py new file mode 100644 index 0000000..a1050fb --- /dev/null +++ b/nexxT/tests/core/test_FilterMockup.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from nexxT.interface import InputPortInterface +from nexxT.core.FilterMockup import FilterMockup +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +import os + +def test_smoke(): + # most of this is already tested in testGraph + mockup = FilterMockup("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", + "SimpleDynInFilter", PropertyCollectionImpl("mockup", None), None) + mockup.createFilterAndUpdate() + mockup.addDynamicPort("dynin", InputPortInterface) + res = mockup.createFilter() + + +if __name__ == "__main__": + test_smoke() \ No newline at end of file diff --git a/nexxT/tests/core/test_Graph.py b/nexxT/tests/core/test_Graph.py new file mode 100644 index 0000000..f823522 --- /dev/null +++ b/nexxT/tests/core/test_Graph.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from PySide2.QtCore import QCoreApplication +from nexxT.core.Graph import FilterGraph +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +import os + +def expect_exception(f, *args, **kw): + ok = False + try: + f(*args, **kw) + except: + ok = True + assert ok + +app = QCoreApplication.instance() +if app is None: + app = QCoreApplication() + +def test_smoke(): + class DummySubConfig(object): + def __init__(self): + self.pc = PropertyCollectionImpl("root", None) + + def getPropertyCollection(self): + return self.pc + + fg = FilterGraph(DummySubConfig()) + n1 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter") + n2 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", "SimpleDynInFilter") + n3 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", "SimpleDynOutFilter") + n3_2 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", "SimpleDynOutFilter") + assert n3_2 == "SimpleDynOutFilter2" + n3_3 = fg.addNode("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleDynamicFilter.py", "SimpleDynOutFilter") + assert n3_3 == "SimpleDynOutFilter3" + fg.deleteNode(n3_2) + fg.deleteNode(n3_3) + + fg.addDynamicInputPort(n2, "inPort") + fg.addDynamicInputPort(n2, "din1") + fg.addDynamicInputPort(n2, "din2") + fg.deleteDynamicInputPort(n2, "din2") + fg.addDynamicInputPort(n2, "din2") + + fg.addDynamicOutputPort(n3, "dout1") + fg.addDynamicOutputPort(n3, "dout2") + fg.deleteDynamicOutputPort(n3, "dout2") + fg.addDynamicOutputPort(n3, "dout2") + + app.processEvents() + + fg.addConnection(n1, "outPort", n2, "inPort") + fg.addConnection(n3, "outPort", n2, "din1") + fg.addConnection(n3, "dout1", n2, "din2") + fg.addConnection(n3, "dout2", n1, "inPort") + fg.addConnection(n2, "outPort", n3, "inPort") + + fg.renameDynamicInputPort(n2, "din2", "din3") + fg.renameDynamicInputPort(n2, "din3", "din2") + + expect_exception(fg.renameDynamicInputPort, n2, "din2", "din1") + + fg.renameDynamicOutputPort(n3, "dout2", "dout3") + fg.renameDynamicOutputPort(n3, "dout3", "dout2") + + fg.renameNode(n1, "static") + fg.renameNode(n2, "dynin") + fg.renameNode(n3, "dynout") + + assert set(fg._nodes.keys()) == set(["static", "dynin", "dynout"]) + assert fg._nodes["static"]["inports"] == ["inPort"] and fg._nodes["static"]["outports"] == ["outPort"] + assert fg._nodes["dynin"]["inports"] == ["inPort", "din1", "din2"] and fg._nodes["dynin"]["outports"] == ["outPort"] + assert fg._nodes["dynout"]["inports"] == ["inPort"] and fg._nodes["dynout"]["outports"] == ["outPort", "dout1", "dout2"] + + assert ("static", "outPort", "dynin" , "inPort") in fg._connections + assert ("dynout", "outPort", "dynin" , "din1") in fg._connections + assert ("dynout", "dout1" , "dynin" , "din2") in fg._connections + assert ("dynout", "dout2" , "static", "inPort") in fg._connections + assert ("dynin" , "outPort", "dynout", "inPort") in fg._connections + + +if __name__ == "__main__": + test_smoke() \ No newline at end of file diff --git a/nexxT/tests/core/test_PropertyCollectionImpl.py b/nexxT/tests/core/test_PropertyCollectionImpl.py new file mode 100644 index 0000000..d62afcc --- /dev/null +++ b/nexxT/tests/core/test_PropertyCollectionImpl.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from nexxT.core.PropertyCollectionImpl import PropertyCollectionImpl +from nexxT.interface import PropertyCollection +from nexxT.core.Exceptions import * +from PySide2.QtGui import QDoubleValidator, QRegExpValidator +from PySide2.QtCore import QRegExp, QCoreApplication +import gc + +def expect_exception(f, etype, *args, **kw): + ok = False + try: + f(*args, **kw) + except etype: + ok = True + assert ok + +# we need a QCoreApplication for the child events +app = QCoreApplication.instance() +if app is None: + app = QCoreApplication() + +def test_smoke(): + signals_received = [] + + def trace_signal(args, signal): + signals_received.append((signal,) + args) + + def newPropColl(name, parent): + res = PropertyCollectionImpl(name, parent) + for s in ["propertyChanged", + "propertyAdded", + "propertyRemoved", + "childAdded", + "childRemoved", + "childRenamed"]: + getattr(res, s).connect(lambda *args, signal=s: trace_signal(args, signal)) + return res + + p_root = newPropColl("root", None) + assert len(signals_received) == 0 + p_child1 = newPropColl("child1", p_root) + assert signals_received == [("childAdded", p_root, "child1")] + signals_received.clear() + p_child2 = newPropColl("child2", p_root) + assert signals_received == [("childAdded", p_root, "child2")] + signals_received.clear() + p_child11 = newPropColl("child11", p_child1) + assert signals_received == [("childAdded", p_child1, "child11")] + signals_received.clear() + p_child12 = newPropColl("child12", p_child1) + assert signals_received == [("childAdded", p_child1, "child12")] + signals_received.clear() + + expect_exception(newPropColl, PropertyCollectionChildExists, "child1", p_root) + expect_exception(p_root.deleteChild, PropertyCollectionChildNotFound, "child5") + + assert p_root.getChildCollection("child2") is p_child2 + + expect_exception(p_child1.setProperty, PropertyCollectionPropertyNotFound, "nonexisting", "no") + + assert p_child1.defineProperty("prop1", 1.0, "a sample float prop") == 1.0 + assert signals_received == [("propertyAdded", p_child1, "prop1")] + signals_received.clear() + p_child1.setProperty("prop1", 2.0) + assert p_child1.defineProperty("prop1", 1.0, "a sample float prop") == 2.0 + assert signals_received == [("propertyChanged", p_child1, "prop1")] + signals_received.clear() + p_child1.setProperty("prop1", "3.0") + assert p_child1.defineProperty("prop1", 1.0, "a sample float prop") == 3.0 + assert signals_received == [("propertyChanged", p_child1, "prop1")] + signals_received.clear() + + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop1", 2.0, "a sample float prop") + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop1", 1.0, "aa sample float prop") + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop1", 1.0, "a sample float prop", str) + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop1", 1.0, "a sample float prop", None, QDoubleValidator()) + expect_exception(p_child1.setProperty, PropertyParsingError, "prop1", "a") + + assert p_child1.defineProperty("prop2", 4, "a sample int prop") == 4 + assert signals_received == [("propertyAdded", p_child1, "prop2")] + signals_received.clear() + p_child1.setProperty("prop2", 3) + assert p_child1.defineProperty("prop2", 4, "a sample int prop") == 3 + assert signals_received == [("propertyChanged", p_child1, "prop2")] + signals_received.clear() + p_child1.setProperty("prop2", "2") + assert p_child1.defineProperty("prop2", 4, "a sample int prop") == 2 + assert signals_received == [("propertyChanged", p_child1, "prop2")] + signals_received.clear() + + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop2", 5, "a sample int prop") + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop2", 4, "aa sample int prop") + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop2", 4, "a sample int prop", str) + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop2", 4, "a sample int prop", None, QDoubleValidator()) + expect_exception(p_child1.setProperty, PropertyParsingError, "prop2", "a") + + assert p_child1.defineProperty("prop3", "a", "a sample str prop") == "a" + assert signals_received == [("propertyAdded", p_child1, "prop3")] + signals_received.clear() + p_child1.setProperty("prop3", "b") + assert p_child1.defineProperty("prop3", "a", "a sample str prop") == "b" + assert signals_received == [("propertyChanged", p_child1, "prop3")] + signals_received.clear() + + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop3", "b", "a sample str prop") + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop3", "a", "aa sample str prop") + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop3", "a", "a sample str prop", int) + expect_exception(p_child1.defineProperty, PropertyInconsistentDefinition, "prop3", "a", "a sample str prop", None, QDoubleValidator()) + + p_child2.defineProperty("nonsense", 1, "", None, QDoubleValidator()) + assert signals_received == [("propertyAdded", p_child2, "nonsense")] + signals_received.clear() + expect_exception(p_child2.setProperty, PropertyParsingError, "nonsense", "1.4") + + p_child2.defineProperty("nonsense2", 1.0, "", None, QRegExpValidator(QRegExp(".*"))) + assert signals_received == [("propertyAdded", p_child2, "nonsense2")] + signals_received.clear() + expect_exception(p_child2.setProperty, PropertyParsingError, "nonsense2", "abv") + + expect_exception(p_child2.defineProperty, PropertyCollectionUnknownType, "nonsense3", [], "") + expect_exception(p_child2.defineProperty, PropertyCollectionUnknownType, "nonsense3", [], "", str) + expect_exception(p_child2.defineProperty, PropertyCollectionUnknownType, "nonsense3", [], "", None, QDoubleValidator()) + + p_child1.markAllUnused() + assert p_child1.defineProperty("prop3", "a", "a sample str prop") == "b" + p_child1.deleteUnused() + assert set(signals_received) == set([("propertyRemoved", p_child1, "prop1"), + ("propertyRemoved", p_child1, "prop2")]) + signals_received.clear() + + del p_child1 + del p_child11 + del p_child12 + p_root.deleteChild("child1") + assert set([(s[0], s[2]) for s in signals_received]) == set([("childRemoved", "child11"), + ("childRemoved", "child12"), + ("childRemoved", "child1")]) + +if __name__ == "__main__": + test_smoke() \ No newline at end of file diff --git a/nexxT/tests/interface/AviFilePlayback.py b/nexxT/tests/interface/AviFilePlayback.py new file mode 100644 index 0000000..4359ae7 --- /dev/null +++ b/nexxT/tests/interface/AviFilePlayback.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import logging +import time +from PySide2.QtCore import Signal, Slot, QTimer, QDateTime, QUrl +from PySide2.QtMultimedia import QMediaPlayer, QMediaPlaylist, QAbstractVideoSurface, QVideoFrame +from PySide2.QtMultimediaWidgets import QVideoWidget +from nexxT.interface import Filter, OutputPort, DataSample, Services + +logger = logging.getLogger(__name__) + +class DummyVideoSurface(QAbstractVideoSurface): + def __init__(self, parent=None): + super().__init__(parent) + + def supportedPixelFormats(self, handleType): + return [QVideoFrame.Format_ARGB32, QVideoFrame.Format_ARGB32_Premultiplied, + QVideoFrame.Format_RGB32, QVideoFrame.Format_RGB24, QVideoFrame.Format_RGB565, + QVideoFrame.Format_RGB555, QVideoFrame.Format_ARGB8565_Premultiplied, + QVideoFrame.Format_BGRA32, QVideoFrame.Format_BGRA32_Premultiplied, QVideoFrame.Format_BGR32, + QVideoFrame.Format_BGR24, QVideoFrame.Format_BGR565, QVideoFrame.Format_BGR555, + QVideoFrame.Format_BGRA5658_Premultiplied, QVideoFrame.Format_AYUV444, + QVideoFrame.Format_AYUV444_Premultiplied, QVideoFrame.Format_YUV444, + QVideoFrame.Format_YUV420P, QVideoFrame.Format_YV12, QVideoFrame.Format_UYVY, + QVideoFrame.Format_YUYV, QVideoFrame.Format_NV12, QVideoFrame.Format_NV21, + QVideoFrame.Format_IMC1, QVideoFrame.Format_IMC2, QVideoFrame.Format_IMC3, + QVideoFrame.Format_IMC4, QVideoFrame.Format_Y8, QVideoFrame.Format_Y16, + QVideoFrame.Format_Jpeg, QVideoFrame.Format_CameraRaw, QVideoFrame.Format_AdobeDng] + + def isFormatSupported(self, format): + imageFormat = QVideoFrame.imageFormatFromPixelFormat(format.pixelFormat()) + size = format.frameSize() + return True + return imageFormat != QImage.Format_Invalid and not size.isEmpty() and \ + format.handleType() == QAbstractVideoBuffer.NoHandle + + def present(self, frame): + logger.debug("video frame arrived") + + def start(self): + logger.debug("start") + super().start() + + def stop(self): + logger.debug("stop") + super().stop() + + +class VideoPlaybackDevice(Filter): + playbackStarted = Signal() + playbackPaused = Signal() + sequenceOpened = Signal(str, QDateTime, QDateTime, list) + currentTimestampChanged = Signal(QDateTime) + + def __init__(self, environment): + super().__init__(False, False, environment) + self.video_out = OutputPort(False, "video", environment) + self.audio_out = OutputPort(False, "audio", environment) + self.addStaticPort(self.video_out) + self.addStaticPort(self.audio_out) + self.filename = self.propertyCollection().defineProperty("filename", "", "avi file name") + + def newDuration(self, newDuration): + logger.debug("newDuration %s", newDuration) + self.sequenceOpened.emit(self.filename, + QDateTime.fromMSecsSinceEpoch(0), + QDateTime.fromMSecsSinceEpoch(newDuration), + ["video"]) + + def currentMediaChanged(self, media): + logger.debug("currentMediaChanged videoAv=%s audioAv=%s", self.player.isVideoAvailable(), self.player.isAudioAvailable()) + + + def _openVideo(self): + logger.debug("entering _openVideo") + self.player = QMediaPlayer(None, QMediaPlayer.VideoSurface) + self.videoOutput = DummyVideoSurface(self.player) + #self.videoOutput = QVideoWidget() + #self.videoOutput.show() + self.player.setVideoOutput(self.videoOutput) + #self.player.setMuted(True) + self.player.durationChanged.connect(self.newDuration) + self.player.currentMediaChanged.connect(self.currentMediaChanged) + self.player.setMedia(QUrl.fromLocalFile(self.filename)) + logger.debug("leaving _openVideo; videoAv=%s audioAv=%s", self.player.isVideoAvailable(), self.player.isAudioAvailable()) + + def _closeVideo(self): + try: + del self.player + del self.playlist + except: + pass + + def onStart(self): + ctrlSrv = Services.getService("PlaybackControl") + ctrlSrv.setupConnections(self) + self.playbackPaused.emit() + if self.filename != "": + self._openVideo() + + def onStop(self): + ctrlSrv = Services.getService("PlaybackControl") + ctrlSrv.removeConnections(self) + self._closeVideo() + + @Slot() + def startPlayback(self): + self.player.play() + self.playbackStarted.emit() + logger.debug("leaving startPlayback; videoAv=%s audioAv=%s", self.player.isVideoAvailable(), self.player.isAudioAvailable()) + + @Slot() + def pausePlayback(self): + self.player.pause() + self.playbackPaused.emit() + + def newDataEvent(self): + t = time.monotonic() + if self.lastSendTime is not None: + if t - self.lastSendTime < self.timeout_ms * 1e-3: + # we are still earlier than the requested framerate + return + self.lastSendTime = t + self.counter += 1 + c = "Sample %d" % self.counter + s = DataSample(c.encode("utf8"), "text/utf8", int(time.time() / DataSample.TIMESTAMP_RES)) + logging.getLogger(__name__).info("transmit: %s", c) + self.beforeTransmit(s) + self.outPort.transmit(s) + self.afterTransmit() + + def stepForward(self): + pass + + def stepBackward(self): + pass + + def seekBeginning(self): + pass + + def seekEnd(self): + pass + + def seekTime(self, timestamp): + pass + + def setSequence(self, filename): + self.filename + + def setTimeFactor(self, factor): + pass diff --git a/nexxT/tests/interface/QImageDisplay.py b/nexxT/tests/interface/QImageDisplay.py new file mode 100644 index 0000000..8d48562 --- /dev/null +++ b/nexxT/tests/interface/QImageDisplay.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import logging +import shiboken2 +from PySide2.QtCore import QBuffer +from PySide2.QtGui import QImageReader, QPixmap +from PySide2.QtWidgets import QLabel, QWidget, QMdiSubWindow +from nexxT.interface import Filter, InputPort, Services + +logger = logging.getLogger(__name__) + +class QImageDisplay(Filter): + + def __init__(self, environment): + Filter.__init__(self, False, False, environment) + self.inPort = InputPort(False, "inPort", environment) + self.addStaticPort(self.inPort) + self.propertyCollection().defineProperty("SubplotID", "ImageDisplay", "The parent subplot.") + self.lastSize = None + + def onStart(self): + srv = Services.getService("MainWindow") + self.display = QLabel() + self.subplotID = self.propertyCollection().getProperty("SubplotID") + srv.subplot(self.subplotID, self, self.display) + + def onStop(self): + srv = Services.getService("MainWindow") + srv.releaseSubplot(self.subplotID) + + def onPortDataChanged(self, inputPort): + dataSample = inputPort.getData() + c = dataSample.getContent() + b = QBuffer(c) + r = QImageReader() + r.setDevice(b) + img = r.read() + self.display.setPixmap(QPixmap.fromImage(img)) + logger.debug("got image, size %d %d", img.size().width(), img.size().height()) + if self.lastSize != img.size(): + self.lastSize = img.size() + self.display.setMinimumSize(img.size()) + # propagate the size change to the parents + w = self.display + while w is not None: + if isinstance(w, QWidget): + w.adjustSize() + if isinstance(w, QMdiSubWindow): + break + w = w.parent() \ No newline at end of file diff --git a/nexxT/tests/interface/SimpleDynamicFilter.py b/nexxT/tests/interface/SimpleDynamicFilter.py new file mode 100644 index 0000000..5c31530 --- /dev/null +++ b/nexxT/tests/interface/SimpleDynamicFilter.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import logging +import time +from nexxT.interface import Filter, InputPort, OutputPort, DataSample + +class SimpleDynInFilter(Filter): + + def __init__(self, environment): + super().__init__(True, False, environment) + self.outPort = OutputPort(False, "outPort", environment) + self.addStaticPort(self.outPort) + self.dynInPorts = None + self.sleep_time = self.propertyCollection().defineProperty("sleep_time", 0.0, + "sleep time to simulate computational load [s]") + + def onInit(self): + self.dynInPorts = self.getDynamicInputPorts() + assert len(self.getDynamicOutputPorts()) == 0 + + def onPortDataChanged(self, inputPort): + dataSample = inputPort.getData() + self.afterReceive(dataSample) + if dataSample.getDatatype() == "text/utf8": + logging.getLogger(__name__).info("received: %s", dataSample.getContent().data().decode("utf8")) + newSample = DataSample.copy(dataSample) + time.sleep(self.sleep_time) + self.beforeTransmit(dataSample) + self.outPort.transmit(dataSample) + self.afterTransmit() + + def onDeinit(self): + self.dynInPorts = None + + # used by tests + def afterReceive(self, dataSample): + pass + + def beforeTransmit(self, dataSample): + pass + + def afterTransmit(self): + pass + + +class SimpleDynOutFilter(Filter): + + def __init__(self, environment): + super().__init__(False, True, environment) + self.inPort = InputPort(False, "inPort", environment) + self.addStaticPort(self.inPort) + self.outPort = OutputPort(False, "outPort", environment) + self.addStaticPort(self.outPort) + self.dynOutPorts = None + + def onInit(self): + self.dynOutPorts = self.getDynamicInputPorts() + assert len(self.getDynamicInputPorts()) == 0 + + def onPortDataChanged(self, inputPort): + dataSample = inputPort.getData() + newSample = DataSample.copy(dataSample) + for p in self.dynOutPorts + [self.outPort]: + p.transmit(dataSample) + diff --git a/nexxT/tests/interface/SimplePlaybackDevice.py b/nexxT/tests/interface/SimplePlaybackDevice.py new file mode 100644 index 0000000..086a19e --- /dev/null +++ b/nexxT/tests/interface/SimplePlaybackDevice.py @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import logging +import time +from PySide2.QtCore import Signal, Slot, QTimer, QThread +from nexxT.interface import Filter, OutputPort, DataSample, Services + +import sys, os +sys.path.append(os.path.abspath(__file__ + "/..")) +import test_dataSample +import PySide2.Qt3DRender + +logger = logging.getLogger(__name__) + +class MinimalPlaybackDevice(Filter): + playbackStarted = Signal() + playbackPaused = Signal() + + def __init__(self, environment): + super().__init__(False, False, environment) + self.outPort = OutputPort(False, "outPort", environment) + self.addStaticPort(self.outPort) + self.timeout_ms = int( + 1000 / self.propertyCollection().defineProperty("frequency", 1.0, "frequency of data generation [Hz]")) + self.timer = QTimer() + self.timer.timeout.connect(self.newDataEvent) + self.thread = QThread.currentThread() + + def onStart(self): + assert QThread.currentThread() is self.thread + ctrlSrv = Services.getService("PlaybackControl") + ctrlSrv.setupConnections(self, []) + self.playbackPaused.emit() + + def onStop(self): + assert QThread.currentThread() is self.thread + ctrlSrv = Services.getService("PlaybackControl") + ctrlSrv.removeConnections(self) + + @Slot() + def startPlayback(self): + assert QThread.currentThread() is self.thread + # prevent different behaviour between linux and windows (the timer will fire 10 times as fast as needed + # and the events are filtered in newDataEvent) + self.timer.start(self.timeout_ms/10) + self.counter = 0 + self.lastSendTime = None + self.playbackStarted.emit() + + @Slot() + def pausePlayback(self): + assert QThread.currentThread() is self.thread + self.timer.stop() + self.playbackPaused.emit() + + def newDataEvent(self): + assert QThread.currentThread() is self.thread + t = time.monotonic() + if self.lastSendTime is not None: + if t - self.lastSendTime < self.timeout_ms*1e-3: + # we are still earlier than the requested framerate + return + self.lastSendTime = t + self.counter += 1 + c = "Sample %d" % self.counter + s = DataSample(c.encode("utf8"), "text/utf8", int(time.time()/DataSample.TIMESTAMP_RES)) + logging.getLogger(__name__).info("transmit: %s", c) + self.beforeTransmit(s) + self.outPort.transmit(s) + self.afterTransmit() + + # overwritten by tests + def beforeTransmit(self, dataSample): + pass + + def afterTransmit(self): + pass + diff --git a/nexxT/tests/interface/SimpleStaticFilter.py b/nexxT/tests/interface/SimpleStaticFilter.py new file mode 100644 index 0000000..9d97395 --- /dev/null +++ b/nexxT/tests/interface/SimpleStaticFilter.py @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import logging +import time +from nexxT.interface import Filter, InputPort, InputPortInterface, OutputPort, DataSample +from PySide2.QtCore import QTimer, Slot + +class SimpleStaticFilter(Filter): + + def __init__(self, environment): + Filter.__init__(self, False, False, environment) + self.inPort = InputPort(False, "inPort", environment) + self.addStaticPort(self.inPort) + self.outPort = OutputPort(False, "outPort", environment) + self.addStaticPort(self.outPort) + self.sleep_time = self.propertyCollection().defineProperty("sleep_time", 0.0, + "sleep time to simulate computational load [s]") + + def onPortDataChanged(self, inputPort): + dataSample = inputPort.getData() + self.afterReceive(dataSample) + if dataSample.getDatatype() == "text/utf8": + logging.getLogger(__name__).info("received: %s", dataSample.getContent().data().decode("utf8")) + newSample = DataSample.copy(dataSample) + time.sleep(self.sleep_time) + self.beforeTransmit(dataSample) + self.outPort.transmit(dataSample) + self.afterTransmit() + + # used by tests + def afterReceive(self, dataSample): + pass + + def beforeTransmit(self, dataSample): + pass + + def afterTransmit(self): + pass + +class SimpleSource(Filter): + def __init__(self, environment): + Filter.__init__(self, False, False, environment) + self.outPort = OutputPort(False, "outPort", environment) + self.addStaticPort(self.outPort) + self.timeout_ms = int(1000 / self.propertyCollection().defineProperty("frequency", 1.0, "frequency of data generation [Hz]")) + + def onStart(self): + self.timer = QTimer() + self.timer.timeout.connect(self.newDataEvent) + # prevent different behaviour between linux and windows (the timer will fire 10 times as fast as needed + # and the events are filtered in newDataEvent) + self.timer.start(self.timeout_ms/10) + self.counter = 0 + self.lastSendTime = None + + def newDataEvent(self): + t = time.monotonic() + if self.lastSendTime is not None: + if t - self.lastSendTime < self.timeout_ms*1e-3: + # we are still earlier than the requested framerate + return + self.lastSendTime = t + self.counter += 1 + c = "Sample %d" % self.counter + s = DataSample(c.encode("utf8"), "text/utf8", int(time.time()/DataSample.TIMESTAMP_RES)) + logging.getLogger(__name__).info("transmit: %s", c) + self.beforeTransmit(s) + self.outPort.transmit(s) + self.afterTransmit() + + def onStop(self): + self.timer.stop() + del self.timer + + def onPortDataChanged(self, inputPort): + dataSample = inputPort.getData() + newSample = DataSample.copy(dataSample) + self.outPort.transmit(dataSample) + + # overwritten by tests + def beforeTransmit(self, dataSample): + pass + + def afterTransmit(self): + pass + + +def test_create(): + class EnvironmentMockup(object): + def addStaticPort(self, port): + pass + env = EnvironmentMockup() + filter = SimpleStaticFilter(env) + inData = DataSample(b'Hello', "bytes", 1) + filter.onPortDataChanged(filter.inPort) + +if __name__ == "__main__": + test_create() \ No newline at end of file diff --git a/nexxT/tests/interface/TestExceptionFilter.py b/nexxT/tests/interface/TestExceptionFilter.py new file mode 100644 index 0000000..07fc40d --- /dev/null +++ b/nexxT/tests/interface/TestExceptionFilter.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +from nexxT.interface import Filter, InputPort + +class TestExceptionFilter(Filter): + def __init__(self, env): + super().__init__(False, False, env) + self.propertyCollection().defineProperty("whereToThrow", "nowhere", + "one of nowhere,constructor,init,start,port,stop,deinit") + if self.propertyCollection().getProperty("whereToThrow") == "constructor": + raise RuntimeError("exception in constructor") + self.port = InputPort(False, "port", env) + self.addStaticPort(self.port) + + def onInit(self): + if self.propertyCollection().getProperty("whereToThrow") == "init": + raise RuntimeError("exception in init") + + def onStart(self): + if self.propertyCollection().getProperty("whereToThrow") == "start": + raise RuntimeError("exception in start") + + def onStop(self): + if self.propertyCollection().getProperty("whereToThrow") == "stop": + raise RuntimeError("exception in stop") + + def onDeinit(self): + if self.propertyCollection().getProperty("whereToThrow") == "deinit": + raise RuntimeError("exception in deinit") + + def onPortDataChanged(self, port): + if self.propertyCollection().getProperty("whereToThrow") == "port": + raise RuntimeError("exception in port") diff --git a/nexxT/tests/interface/__init__.py b/nexxT/tests/interface/__init__.py new file mode 100644 index 0000000..5eabbc7 --- /dev/null +++ b/nexxT/tests/interface/__init__.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# diff --git a/nexxT/tests/interface/test_dataSample.py b/nexxT/tests/interface/test_dataSample.py new file mode 100644 index 0000000..c9bba6f --- /dev/null +++ b/nexxT/tests/interface/test_dataSample.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import logging +from nexxT.interface import DataSample + +logging.getLogger(__name__).debug("executing test_dataSample.py") + +def test_basic(): + dataSample = DataSample(b"Hello", "String", 38) + assert dataSample.getContent().data() == b'Hello' + + # get the content and modify it + c = dataSample.getContent() + c[:] = b'a'*c.size() + assert c.data() == b'aaaaa' + # but the modification is not affecting the original data + assert dataSample.getContent().data() == b'Hello' + +if __name__ == "__main__": + test_basic() \ No newline at end of file diff --git a/nexxT/tests/src/AviFilePlayback.cpp b/nexxT/tests/src/AviFilePlayback.cpp new file mode 100644 index 0000000..967273a --- /dev/null +++ b/nexxT/tests/src/AviFilePlayback.cpp @@ -0,0 +1,324 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "AviFilePlayback.hpp" +#include "Services.hpp" +#include "PropertyCollection.hpp" +#include "Logger.hpp" +#include +#include +#include +#include +#include + +using namespace nexxT; + +QImage qt_imageFromVideoFrame( const QVideoFrame& f ); + +class DummyVideoSurface : public QAbstractVideoSurface +{ + Q_OBJECT + +signals: + void newImage(const QImage &); +public: + DummyVideoSurface(QObject *parent) : QAbstractVideoSurface(parent) {} + virtual ~DummyVideoSurface() + { + qDebug("DummyVideoSurface::~DummyVideoSurface (qt message)"); + } + + + QList supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const + { + NEXT_LOG_DEBUG("QVideoSurfaceFormat::supportedPixelFormats called"); + + Q_UNUSED(handleType); + return QList() + << QVideoFrame::Format_ARGB32 + << QVideoFrame::Format_ARGB32_Premultiplied + << QVideoFrame::Format_RGB32 + << QVideoFrame::Format_RGB24 + << QVideoFrame::Format_RGB565 + << QVideoFrame::Format_RGB555 + << QVideoFrame::Format_ARGB8565_Premultiplied + << QVideoFrame::Format_BGRA32 + << QVideoFrame::Format_BGRA32_Premultiplied + << QVideoFrame::Format_BGR32 + << QVideoFrame::Format_BGR24 + << QVideoFrame::Format_BGR565 + << QVideoFrame::Format_BGR555 + << QVideoFrame::Format_BGRA5658_Premultiplied + << QVideoFrame::Format_AYUV444 + << QVideoFrame::Format_AYUV444_Premultiplied + << QVideoFrame::Format_YUV444 + << QVideoFrame::Format_YUV420P + << QVideoFrame::Format_YV12 + << QVideoFrame::Format_UYVY + << QVideoFrame::Format_YUYV + << QVideoFrame::Format_NV12 + << QVideoFrame::Format_NV21 + << QVideoFrame::Format_IMC1 + << QVideoFrame::Format_IMC2 + << QVideoFrame::Format_IMC3 + << QVideoFrame::Format_IMC4 + << QVideoFrame::Format_Y8 + << QVideoFrame::Format_Y16 + << QVideoFrame::Format_Jpeg + << QVideoFrame::Format_CameraRaw + << QVideoFrame::Format_AdobeDng; + } + + bool isFormatSupported(const QVideoSurfaceFormat &format) const + { + NEXT_LOG_DEBUG("QVideoSurfaceFormat::isFormatSupported called"); + + const QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat()); + const QSize size = format.frameSize(); + + return imageFormat != QImage::Format_Invalid + && !size.isEmpty() + && format.handleType() == QAbstractVideoBuffer::NoHandle; + } + + bool start(const QVideoSurfaceFormat &format) + { + NEXT_LOG_DEBUG("QVideoSurfaceFormat::start called"); + + QAbstractVideoSurface::start(format); + return true; + } + + void stop() + { + NEXT_LOG_DEBUG("QVideoSurfaceFormat::stop called"); + QAbstractVideoSurface::stop(); + } + + bool present(const QVideoFrame &_frame) + { + QImage img = qt_imageFromVideoFrame(_frame); + if(!img.isNull()) + { + emit newImage(img); + return true; + } else + { + return false; + } + } + +}; + +void VideoPlaybackDevice::openVideo() +{ + NEXT_LOG_DEBUG("entering openVideo"); + pauseOnNextImage = false; + player = new QMediaPlayer(this, QMediaPlayer::VideoSurface); + player->setMuted(true); + videoSurface = new DummyVideoSurface(this); + connect(player, SIGNAL(durationChanged(qint64)), + this, SLOT(newDuration(qint64))); + connect(player, SIGNAL(positionChanged(qint64)), + this, SLOT(newPosition(qint64))); + connect(player, SIGNAL(currentMediaChanged(const QMediaContent &)), + this, SLOT(currentMediaChanged(const QMediaContent &))); + connect(videoSurface, SIGNAL(newImage(const QImage &)), + this, SLOT(newImage(const QImage &))); + connect(player, SIGNAL(error(QMediaPlayer::Error)), + this, SLOT(mediaPlayerError(QMediaPlayer::Error))); + connect(player, SIGNAL(stateChanged(QMediaPlayer::State)), + this, SLOT(mediaPlayerStateChanged(QMediaPlayer::State))); + connect(player, SIGNAL(playbackRateChanged(qreal)), + this, SLOT(mediaPlayerPlaybackRateChanged(qreal))); + player->setMedia(QUrl::fromLocalFile(filename)); + player->setVideoOutput(videoSurface); + player->setPlaybackRate(playbackRate); + player->pause(); + NEXT_LOG_DEBUG("leaving openVideo"); +} + +void VideoPlaybackDevice::closeVideo() +{ + NEXT_LOG_DEBUG("entering closeVideo"); + if(player) + { + delete player; + player = nullptr; + } + if(videoSurface) + { + delete videoSurface; + videoSurface = nullptr; + } + NEXT_LOG_INFO("emitting playback paused."); + emit playbackPaused(); + NEXT_LOG_DEBUG("leaving closeVideo"); +} + +VideoPlaybackDevice::VideoPlaybackDevice(BaseFilterEnvironment *env) : + Filter(false, false, env), + player(nullptr), + videoSurface(nullptr) +{ + pauseOnNextImage = false; + playbackRate = 1.0; + video_out = SharedOutputPortPtr(new OutputPortInterface(false, "video_out", env)); + addStaticPort(video_out); + propertyCollection()->defineProperty("filename", "", "video file name"); +} + +VideoPlaybackDevice::~VideoPlaybackDevice() +{ + closeVideo(); +} + +void VideoPlaybackDevice::newImage(const QImage &img) +{ + if(pauseOnNextImage) + { + pauseOnNextImage = false; + QMetaObject::invokeMethod(this, "pausePlayback", Qt::QueuedConnection); + } + QByteArray a; + { + QBuffer b(&a); + QImageWriter w; + w.setFormat("png"); + w.setDevice(&b); + if( !w.write(img) ) + { + NEXT_LOG_ERROR(QString("Can't serialize image, %1").arg(w.errorString())); + } + } + SharedDataSamplePtr newSample(new DataSample(a, "qimage", QDateTime::currentDateTime().toMSecsSinceEpoch())); + video_out->transmit(newSample); +} + +void VideoPlaybackDevice::mediaPlayerError(QMediaPlayer::Error) +{ + if(player) NEXT_LOG_WARN(QString("error from QMediaPlayer: %1").arg(player->errorString())); +} + +void VideoPlaybackDevice::mediaPlayerStateChanged(QMediaPlayer::State newState) +{ + if(newState == QMediaPlayer::PlayingState) + { + emit playbackStarted(); + } else + { + NEXT_LOG_INFO("emitting playback paused."); + emit playbackPaused(); + } +} + +void VideoPlaybackDevice::mediaPlayerPlaybackRateChanged(qreal newRate) +{ + playbackRate = newRate; + emit timeRatioChanged(newRate); +} + +void VideoPlaybackDevice::newDuration(qint64 duration) +{ + NEXT_LOG_DEBUG(QString("newDuration %1").arg(duration)); + emit sequenceOpened(filename, + QDateTime::fromMSecsSinceEpoch(0, Qt::UTC), + QDateTime::fromMSecsSinceEpoch(duration, Qt::UTC), + QStringList() << "video"); +} + +void VideoPlaybackDevice::newPosition(qint64 position) +{ + emit currentTimestampChanged(QDateTime::fromMSecsSinceEpoch(position, Qt::UTC)); +} + +void VideoPlaybackDevice::currentMediaChanged(const QMediaContent &) +{ + NEXT_LOG_DEBUG("currentMediaChanged called"); +} + +void VideoPlaybackDevice::startPlayback() +{ + NEXT_LOG_DEBUG("startPlayback called"); + if(player) player->play(); +} + +void VideoPlaybackDevice::pausePlayback() +{ + NEXT_LOG_DEBUG("pausePlayback called"); + if(player) player->pause(); +} + +void VideoPlaybackDevice::stepForward() +{ + NEXT_LOG_DEBUG("stepForward called"); + pauseOnNextImage = true; + if( player && player->state() != QMediaPlayer::PlayingState ) + { + NEXT_LOG_DEBUG("calling play"); + if(player) player->play(); + } +} + +void VideoPlaybackDevice::seekBeginning() +{ + NEXT_LOG_DEBUG("seekBeginning called"); + if(player) player->setPosition(0); +} + +void VideoPlaybackDevice::seekEnd() +{ + NEXT_LOG_DEBUG("seekEnd called"); + if(player) player->setPosition(player->duration()-1); +} + +void VideoPlaybackDevice::seekTime(const QDateTime &pos) +{ + NEXT_LOG_DEBUG("seekTime called"); + if(player) player->setPosition(pos.toMSecsSinceEpoch()); +} + +void VideoPlaybackDevice::setSequence(const QString &_filename) +{ + NEXT_LOG_DEBUG("setSequence called"); + closeVideo(); + filename = _filename; + openVideo(); +} + +void VideoPlaybackDevice::setTimeFactor(double factor) +{ + NEXT_LOG_DEBUG("setTimeFactor called"); + if(player) player->setPlaybackRate(factor); +} + +void VideoPlaybackDevice::onStart() +{ + QStringList filters; + filters << "*.avi" << "*.mp4" << "*.wmv"; + SharedQObjectPtr ctrlSrv = Services::getService("PlaybackControl"); + QMetaObject::invokeMethod(ctrlSrv.data(), + "setupConnections", + Qt::DirectConnection, + Q_ARG(QObject*, this), + Q_ARG(const QStringList &, filters)); + filename = propertyCollection()->getProperty("filename").toString(); + openVideo(); +} + +void VideoPlaybackDevice::onStop() +{ + closeVideo(); + SharedQObjectPtr ctrlSrv = Services::getService("PlaybackControl"); + QMetaObject::invokeMethod(ctrlSrv.data(), + "removeConnections", + Qt::DirectConnection, + Q_ARG(QObject*,this)); +} + +#include "AviFilePlayback.moc" + diff --git a/nexxT/tests/src/AviFilePlayback.hpp b/nexxT/tests/src/AviFilePlayback.hpp new file mode 100644 index 0000000..74a270a --- /dev/null +++ b/nexxT/tests/src/AviFilePlayback.hpp @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef AVIFILEPLAYBACK_HPP +#define AVIFILEPLAYBACK_HPP + +#include +#include +#include "Filters.hpp" +#include "Ports.hpp" +#include "NexTPlugins.hpp" + + +class DummyVideoSurface; + +using namespace nexxT; + +class VideoPlaybackDevice : public Filter +{ + Q_OBJECT + + SharedOutputPortPtr video_out; + QString filename; + double playbackRate; + bool pauseOnNextImage; + QMediaPlayer *player; + DummyVideoSurface *videoSurface; + + void openVideo(); + void closeVideo(); + +public: + NEXT_PLUGIN_DECLARE_FILTER(VideoPlaybackDevice) + + VideoPlaybackDevice(BaseFilterEnvironment *env); + virtual ~VideoPlaybackDevice(); + +signals: + void playbackStarted(); + void playbackPaused(); + void sequenceOpened(const QString &file, + const QDateTime &begin, + const QDateTime &end, + const QStringList &streams); + void currentTimestampChanged(const QDateTime &); + void timeRatioChanged(double); + +public slots: + void newImage(const QImage &img); + void mediaPlayerError(QMediaPlayer::Error); + void mediaPlayerStateChanged(QMediaPlayer::State newState); + void mediaPlayerPlaybackRateChanged(qreal newRate); + + void newDuration(qint64 duration); + void newPosition(qint64 position); + void currentMediaChanged(const QMediaContent &); + void startPlayback(); + void pausePlayback(); + void stepForward(); + void seekBeginning(); + void seekEnd(); + void seekTime(const QDateTime &pos); + void setSequence(const QString &_filename); + void setTimeFactor(double factor); +protected: + void onStart(); + void onStop(); + +}; + +#endif // AVIFILEPLAYBACK_HPP diff --git a/nexxT/tests/src/Plugins.cpp b/nexxT/tests/src/Plugins.cpp new file mode 100644 index 0000000..c081772 --- /dev/null +++ b/nexxT/tests/src/Plugins.cpp @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "AviFilePlayback.hpp" +#include "SimpleSource.hpp" +#include "TestExceptionFilter.hpp" + +NEXT_PLUGIN_DEFINE_START() +NEXT_PLUGIN_ADD_FILTER(SimpleSource) +NEXT_PLUGIN_ADD_FILTER(VideoPlaybackDevice) +NEXT_PLUGIN_ADD_FILTER(TestExceptionFilter) +NEXT_PLUGIN_DEFINE_FINISH() diff --git a/nexxT/tests/src/SConscript.py b/nexxT/tests/src/SConscript.py new file mode 100644 index 0000000..ac6bb55 --- /dev/null +++ b/nexxT/tests/src/SConscript.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import sysconfig + +Import("env") + +env = env.Clone() +env.EnableQt5Modules(['QtCore', "QtMultimedia", "QtGui"]) + +env.Append(CPPPATH=["../../src", "."], + LIBPATH=["../../src"], + LIBS=["nexxT"]) + +env['QT5_DEBUG'] = 1 + +plugin = env.RegisterTargets(env.SharedLibrary("test_plugins", env.RegisterSources(Split(""" + SimpleSource.cpp + AviFilePlayback.cpp + TestExceptionFilter.cpp + Plugins.cpp +""")))) diff --git a/nexxT/tests/src/SimpleSource.cpp b/nexxT/tests/src/SimpleSource.cpp new file mode 100644 index 0000000..dc2800c --- /dev/null +++ b/nexxT/tests/src/SimpleSource.cpp @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "SimpleSource.hpp" +#include "PropertyCollection.hpp" +#include "DataSamples.hpp" +#include "Logger.hpp" +#include + +SimpleSource::SimpleSource(nexxT::BaseFilterEnvironment *env) : + nexxT::Filter(false, false, env), + timer(), + outPort(new nexxT::OutputPortInterface(false, "outPort", env)), + counter(0) +{ + NEXT_LOG_DEBUG("SimpleSource::SimpleSource"); + addStaticPort(outPort); + propertyCollection()->defineProperty("frequency", double(1.0), "frequency of data generation [Hz]"); + connect(&timer, &QTimer::timeout, this, &SimpleSource::newDataEvent); +} + +SimpleSource::~SimpleSource() +{ + NEXT_LOG_DEBUG("SimpleSource::~SimpleSource"); +} + +void SimpleSource::onStart() +{ + int timeout_ms = int(1000. / propertyCollection()->getProperty("frequency").toDouble()); + timer.start(timeout_ms); +} + +void SimpleSource::onStop() +{ + timer.stop(); +} + +void SimpleSource::newDataEvent() +{ + std::chrono::duration t = std::chrono::high_resolution_clock::now().time_since_epoch(); + int64_t it = t.count() / nexxT::DataSample::TIMESTAMP_RES; + counter++; + QString c = QString("Sample %1").arg(counter); + QSharedPointer s(new nexxT::DataSample(c.toUtf8(), "text/utf8", it)); + NEXT_LOG_INFO(QString("Transmitting %1").arg(c)); + outPort->transmit(s); +} + diff --git a/nexxT/tests/src/SimpleSource.hpp b/nexxT/tests/src/SimpleSource.hpp new file mode 100644 index 0000000..e910abb --- /dev/null +++ b/nexxT/tests/src/SimpleSource.hpp @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef SIMPLE_SOURCE_HPP +#define SIMPLE_SOURCE_HPP + +#include "NexTPlugins.hpp" +#include +#include + +class SimpleSource : public nexxT::Filter +{ + Q_OBJECT + + QTimer timer; + nexxT::SharedOutputPortPtr outPort; + uint32_t counter; +public: + SimpleSource(nexxT::BaseFilterEnvironment *env); + virtual ~SimpleSource(); + + virtual void onStart(); + virtual void onStop(); + + NEXT_PLUGIN_DECLARE_FILTER(SimpleSource) + +private slots: + virtual void newDataEvent(); +}; + +#endif \ No newline at end of file diff --git a/nexxT/tests/src/TestExceptionFilter.cpp b/nexxT/tests/src/TestExceptionFilter.cpp new file mode 100644 index 0000000..dc822ec --- /dev/null +++ b/nexxT/tests/src/TestExceptionFilter.cpp @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#include "TestExceptionFilter.hpp" +#include "PropertyCollection.hpp" + +TestExceptionFilter::TestExceptionFilter(BaseFilterEnvironment *env) + : Filter(false, false, env) +{ + propertyCollection()->defineProperty("whereToThrow", "nowhere", "one of nowhere,constructor,init,start,port,stop,deinit"); + if( propertyCollection()->getProperty("whereToThrow") == "constructor" ) + { + throw std::runtime_error("exception in constructor"); + } + port = SharedInputPortPtr(new InputPortInterface(false, "port", env, 1, -1.)); + addStaticPort(port); +} + +TestExceptionFilter::~TestExceptionFilter() +{ + /* + * c++11 has the convention to terminate() when destructors throw exceptions + * we stick to this and do not support throwing exceptions in filter destructors. + */ +} + + +void TestExceptionFilter::onInit() +{ + if( propertyCollection()->getProperty("whereToThrow") == "init" ) + { + throw std::runtime_error("exception in init"); + } +} + +void TestExceptionFilter::onStart() +{ + if( propertyCollection()->getProperty("whereToThrow") == "start" ) + { + throw std::runtime_error("exception in start"); + } +} + +void TestExceptionFilter::onPortDataChanged(const InputPortInterface &) +{ + if( propertyCollection()->getProperty("whereToThrow") == "port" ) + { + throw std::runtime_error("exception in port"); + } +} + +void TestExceptionFilter::onStop() +{ + if( propertyCollection()->getProperty("whereToThrow") == "stop" ) + { + throw std::runtime_error("exception in stop"); + } +} + +void TestExceptionFilter::onDeinit() +{ + if( propertyCollection()->getProperty("whereToThrow") == "deinit" ) + { + throw std::runtime_error("exception in deinit"); + } +} + diff --git a/nexxT/tests/src/TestExceptionFilter.hpp b/nexxT/tests/src/TestExceptionFilter.hpp new file mode 100644 index 0000000..60fdfc3 --- /dev/null +++ b/nexxT/tests/src/TestExceptionFilter.hpp @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 ifm electronic gmbh + * + * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. + */ + +#ifndef TESTEXCEPTIONFILTER_HPP +#define TESTEXCEPTIONFILTER_HPP + +#include "Filters.hpp" +#include "Ports.hpp" +#include "NexTPlugins.hpp" + +using namespace nexxT; + +class TestExceptionFilter : public Filter +{ + SharedInputPortPtr port; +public: + TestExceptionFilter(BaseFilterEnvironment *env); + ~TestExceptionFilter(); + + NEXT_PLUGIN_DECLARE_FILTER(TestExceptionFilter) + + void onInit(); + void onStart(); + void onPortDataChanged(const InputPortInterface &inputPort); + void onStop(); + void onDeinit(); +}; + +#endif // TESTEXCEPTIONFILTER_HPP diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bac8719 --- /dev/null +++ b/setup.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import glob +import os +import sys +import platform +import sysconfig +import setuptools +import subprocess +from setuptools.command.build_ext import build_ext +from distutils.core import setup +from distutils.command.install import INSTALL_SCHEMES + +# create platform specific wheel +try: + from wheel.bdist_wheel import bdist_wheel as _bdist_wheel + + class bdist_wheel(_bdist_wheel): + def finalize_options(self): + super().finalize_options() + self.root_is_pure = False + + def get_tag(self): + python, abi, plat = _bdist_wheel.get_tag(self) + # uncomment for non-python extensions + #python, abi = 'py3', 'none' + return python, abi, plat +except ImportError: + bdist_wheel = None + +if platform.system() == "Linux": + p = "linux_x86_64" + presuf = [("lib", ".so")] +else: + p = "msvc_x86_64" + presuf = [("", ".dll"), ("", ".exp"), ("", ".lib")] + + +cv = sysconfig.get_config_vars() +cnexT = cv.get("EXT_PREFIX", "") + "cnexxT" + cv.get("EXT_SUFFIX", "") + +build_files = [] +for variant in ["nonopt", "release"]: + build_files.append('nexxT/binary/' + p + '/' + variant + "/" + cnexT) + for prefix,suffix in presuf: + build_files.append('nexxT/binary/' + p + '/' + variant + "/" + prefix + "nexxT" + suffix) + +# generate MANIFEST.in to add build files and include files +with open("MANIFEST.in", "w") as manifest: + for fn in glob.glob('nexxT/include/*.hpp'): + manifest.write("include " + fn + "\n") + for bf in build_files: + manifest.write("include " + bf + "\n") + # json schema + manifest.write("include nexxT/core/ConfigFileSchema.json") + +setup(name='nexxT', + install_requires=["PySide2 >=5.14.0, <5.15", "shiboken2 >=5.14.0, <5.15", "jsonschema>=3.2.0"], + version='0.0.0', + description='nexxT extensible framework', + author='pca', + include_package_data = True, + packages=['nexxT', 'nexxT.interface', 'nexxT.tests', 'nexxT.services', 'nexxT.services.gui', 'nexxT.tests.interface', 'nexxT.core', 'nexxT.tests.core'], + cmdclass={ + 'bdist_wheel': bdist_wheel, + }, + entry_points = { + 'console_scripts' : ['nexxT-gui=nexxT.core.AppConsole:mainGui', + 'nexxT-console=nexxT.core.AppConsole:mainConsole', + ] + }, + ) From 9a61f819b3b690ff94003302d5434f06f33c147e Mon Sep 17 00:00:00 2001 From: cwiede <62332054+cwiede@users.noreply.github.com> Date: Sat, 21 Mar 2020 13:58:01 +0100 Subject: [PATCH 04/16] add build scripts in workspace --- .gitignore | 8 + nexxT/src/SConscript.py | 3 +- workspace/SConstruct | 40 + workspace/sconstools3/qt5/README.rst | 351 ++++++ workspace/sconstools3/qt5/__init__.py | 1017 +++++++++++++++++ workspace/sconstools3/qt5/docs/SConstruct | 34 + workspace/sconstools3/qt5/docs/html.xsl | 55 + workspace/sconstools3/qt5/docs/manual.xml | 388 +++++++ workspace/sconstools3/qt5/docs/pdf.xsl | 62 + workspace/sconstools3/qt5/docs/qt5.xml | 600 ++++++++++ workspace/sconstools3/qt5/docs/reference.xml | 717 ++++++++++++ workspace/sconstools3/qt5/docs/scons.css | 263 +++++ .../CPPPATH/CPPPATH-appended/SConscript-after | 6 + .../CPPPATH-appended/SConscript-before | 5 + .../CPPPATH/CPPPATH-appended/image/SConscript | 2 + .../CPPPATH/CPPPATH-appended/image/SConstruct | 6 + .../CPPPATH-appended/image/sub/aaa.cpp | 13 + .../CPPPATH/CPPPATH-appended/image/sub/aaa.h | 12 + .../image/sub/local_include/local_include.h | 3 + .../sconstest-CPPPATH-appended-fail.py | 56 + .../sconstest-CPPPATH-appended.py | 54 + .../basic/CPPPATH/CPPPATH/SConscript-after | 3 + .../basic/CPPPATH/CPPPATH/SConscript-before | 3 + .../basic/CPPPATH/CPPPATH/image/SConstruct | 6 + .../test/basic/CPPPATH/CPPPATH/image/aaa.cpp | 13 + .../test/basic/CPPPATH/CPPPATH/image/aaa.h | 12 + .../image/local_include/local_include.h | 2 + .../CPPPATH/CPPPATH/sconstest-CPPPATH-fail.py | 54 + .../CPPPATH/CPPPATH/sconstest-CPPPATH.py | 53 + .../test/basic/copied-env/image/MyFile.cpp | 5 + .../qt5/test/basic/copied-env/image/MyFile.h | 2 + .../test/basic/copied-env/image/SConscript | 11 + .../test/basic/copied-env/image/SConstruct | 6 + .../basic/copied-env/sconstest-copied-env.py | 52 + .../qt5/test/basic/empty-env/image/SConstruct | 4 + .../qt5/test/basic/empty-env/image/foo6.h | 8 + .../qt5/test/basic/empty-env/image/main.cpp | 3 + .../basic/empty-env/sconstest-empty-env.py | 47 + .../qt5/test/basic/manual/image/SConscript | 20 + .../qt5/test/basic/manual/image/SConstruct | 6 + .../qt5/test/basic/manual/image/aaa.cpp | 2 + .../qt5/test/basic/manual/image/bbb.cpp | 18 + .../qt5/test/basic/manual/image/bbb.h | 2 + .../qt5/test/basic/manual/image/ddd.cpp | 2 + .../qt5/test/basic/manual/image/eee.cpp | 16 + .../qt5/test/basic/manual/image/eee.h | 2 + .../qt5/test/basic/manual/image/fff.cpp | 1 + .../qt5/test/basic/manual/image/main.cpp | 17 + .../qt5/test/basic/manual/image/ui/ccc.h | 9 + .../qt5/test/basic/manual/image/ui/ccc.ui | 19 + .../qt5/test/basic/manual/image/ui/fff.ui | 19 + .../qt5/test/basic/manual/sconstest-manual.py | 57 + .../test/basic/moc-from-cpp/image/SConscript | 7 + .../test/basic/moc-from-cpp/image/SConstruct | 25 + .../qt5/test/basic/moc-from-cpp/image/aaa.cpp | 18 + .../qt5/test/basic/moc-from-cpp/image/aaa.h | 2 + .../test/basic/moc-from-cpp/image/useit.cpp | 4 + .../moc-from-cpp/sconstest-moc-from-cpp.py | 91 ++ .../image/SConscript | 8 + .../image/SConstruct | 25 + .../moc-from-header-nocompile/image/aaa.cpp | 7 + .../moc-from-header-nocompile/image/aaa.h | 9 + .../sconstest-moc-from-header-nocompile.py | 88 ++ .../basic/moc-from-header/image/SConscript | 6 + .../basic/moc-from-header/image/SConstruct | 25 + .../test/basic/moc-from-header/image/aaa.cpp | 7 + .../test/basic/moc-from-header/image/aaa.h | 9 + .../sconstest-moc-from-header.py | 77 ++ .../qt5/test/basic/reentrant/image/SConscript | 4 + .../qt5/test/basic/reentrant/image/SConstruct | 6 + .../qt5/test/basic/reentrant/image/foo5.h | 8 + .../qt5/test/basic/reentrant/image/main.cpp | 7 + .../basic/reentrant/sconstest-reentrant.py | 46 + .../test/basic/variantdir/image/MyForm.cpp | 14 + .../qt5/test/basic/variantdir/image/MyForm.h | 18 + .../test/basic/variantdir/image/SConstruct | 12 + .../test/basic/variantdir/image/anUiFile.ui | 19 + .../qt5/test/basic/variantdir/image/main.cpp | 17 + .../basic/variantdir/image/mocFromCpp.cpp | 16 + .../test/basic/variantdir/image/mocFromCpp.h | 2 + .../test/basic/variantdir/image/mocFromH.cpp | 8 + .../test/basic/variantdir/image/mocFromH.h | 11 + .../basic/variantdir/sconstest-installed.py | 47 + .../test/moc/auto/ccomment/image/SConscript | 9 + .../test/moc/auto/ccomment/image/SConstruct | 6 + .../qt5/test/moc/auto/ccomment/image/main.cpp | 14 + .../moc/auto/ccomment/image/mocFromCpp.cpp | 41 + .../test/moc/auto/ccomment/image/mocFromCpp.h | 2 + .../test/moc/auto/ccomment/image/mocFromH.cpp | 10 + .../test/moc/auto/ccomment/image/mocFromH.h | 32 + .../moc/auto/ccomment/sconstest-ccomment.py | 46 + .../moc/auto/literalstring/image/SConscript | 6 + .../moc/auto/literalstring/image/SConstruct | 6 + .../moc/auto/literalstring/image/main.cpp | 14 + .../auto/literalstring/image/mocFromCpp.cpp | 27 + .../moc/auto/literalstring/image/mocFromCpp.h | 2 + .../moc/auto/literalstring/image/mocFromH.cpp | 10 + .../moc/auto/literalstring/image/mocFromH.h | 21 + .../literalstring/sconstest-literalstring.py | 45 + .../qt5/test/moc/cpppath/default/SConscript | 8 + .../test/moc/cpppath/default/SConscript-fails | 9 + .../test/moc/cpppath/default/image/SConstruct | 6 + .../moc/cpppath/default/image/src/main.cpp | 14 + .../cpppath/default/image/src/mocFromCpp.cpp | 15 + .../cpppath/default/image/src/mocFromH.cpp | 10 + .../default/sconstest-default-fails.py | 46 + .../moc/cpppath/default/sconstest-default.py | 46 + .../qt5/test/moc/cpppath/specific/SConscript | 9 + .../moc/cpppath/specific/SConscript-fails | 9 + .../moc/cpppath/specific/image/SConstruct | 6 + .../moc/cpppath/specific/image/src/main.cpp | 14 + .../cpppath/specific/image/src/mocFromCpp.cpp | 15 + .../cpppath/specific/image/src/mocFromH.cpp | 10 + .../specific/sconstest-specific-fails.py | 49 + .../cpppath/specific/sconstest-specific.py | 48 + .../qt5/test/moc/explicit/image/SConscript | 10 + .../qt5/test/moc/explicit/image/SConstruct | 6 + .../qt5/test/moc/explicit/image/main.cpp | 14 + .../test/moc/explicit/image/mocFromCpp.cpp | 16 + .../qt5/test/moc/explicit/image/mocFromCpp.h | 2 + .../qt5/test/moc/explicit/image/mocFromH.cpp | 8 + .../qt5/test/moc/explicit/image/mocFromH.h | 11 + .../test/moc/explicit/sconstest-explicit.py | 45 + .../moc/order_independent/image/SConscript | 12 + .../moc/order_independent/image/SConstruct | 26 + .../test/moc/order_independent/image/aaa.cpp | 7 + .../test/moc/order_independent/image/aaa.h | 9 + .../test/moc/order_independent/image/bbb.cpp | 4 + .../sconstest-order-independent.py | 46 + .../sconstools3/qt5/test/qrc/basic/SConscript | 8 + .../qt5/test/qrc/basic/SConscript-wflags | 9 + .../qt5/test/qrc/basic/image/SConstruct | 6 + .../qt5/test/qrc/basic/image/icons.qrc | 5 + .../qt5/test/qrc/basic/image/icons/scons.png | Bin 0 -> 1613 bytes .../qt5/test/qrc/basic/image/main.cpp | 11 + .../qt5/test/qrc/basic/sconstest-basic.py | 45 + .../test/qrc/basic/sconstest-basicwflags.py | 45 + .../qt5/test/qrc/manual/SConscript | 9 + .../qt5/test/qrc/manual/SConscript-wflags | 11 + .../qt5/test/qrc/manual/image/SConstruct | 6 + .../qt5/test/qrc/manual/image/icons.qrc | 5 + .../qt5/test/qrc/manual/image/icons/scons.png | Bin 0 -> 1613 bytes .../qt5/test/qrc/manual/image/main.cpp | 11 + .../qt5/test/qrc/manual/sconstest-manual.py | 45 + .../test/qrc/manual/sconstest-manualwflags.py | 45 + .../qt5/test/qrc/multifiles/SConscript | 8 + .../qt5/test/qrc/multifiles/SConscript-manual | 9 + .../qt5/test/qrc/multifiles/image/SConstruct | 6 + .../qt5/test/qrc/multifiles/image/icons.qrc | 5 + .../test/qrc/multifiles/image/icons/scons.png | Bin 0 -> 1613 bytes .../qt5/test/qrc/multifiles/image/main.cpp | 12 + .../qt5/test/qrc/multifiles/image/other.qrc | 5 + .../test/qrc/multifiles/image/other/rocks.png | Bin 0 -> 1613 bytes .../multifiles/sconstest-multifiles-manual.py | 45 + .../qrc/multifiles/sconstest-multifiles.py | 46 + .../qt5/test/qrc/othername/image/SConscript | 11 + .../qt5/test/qrc/othername/image/SConstruct | 6 + .../qt5/test/qrc/othername/image/icons.qrc | 5 + .../test/qrc/othername/image/icons/scons.png | Bin 0 -> 1613 bytes .../qt5/test/qrc/othername/image/main.cpp | 11 + .../test/qrc/othername/sconstest-othername.py | 45 + .../test/qrc/samefilename/image/SConscript | 8 + .../test/qrc/samefilename/image/SConstruct | 6 + .../qt5/test/qrc/samefilename/image/icons.cpp | 11 + .../qt5/test/qrc/samefilename/image/icons.qrc | 5 + .../qrc/samefilename/image/icons/scons.png | Bin 0 -> 1613 bytes .../samefilename/sconstest-samefilename.py | 46 + .../qt5/test/qrc/subdir/image/SConscript | 8 + .../qt5/test/qrc/subdir/image/SConstruct | 6 + .../qt5/test/qrc/subdir/image/main.cpp | 11 + .../qt5/test/qrc/subdir/image/qrc/icons.qrc | 5 + .../test/qrc/subdir/image/qrc/icons/scons.png | Bin 0 -> 1613 bytes .../qt5/test/qrc/subdir/sconstest-subdir.py | 46 + .../test/qt_examples/create_scons_tests.py | 748 ++++++++++++ .../qt5/test/qt_examples/sconstest.skip | 0 workspace/sconstools3/qt5/test/qtenv.py | 107 ++ workspace/sconstools3/qt5/test/sconstest.skip | 0 .../qt5/test/ts_qm/clean/image/MyFile.cpp | 7 + .../qt5/test/ts_qm/clean/image/MyFile.h | 13 + .../qt5/test/ts_qm/clean/image/SConscript | 6 + .../qt5/test/ts_qm/clean/image/SConstruct | 6 + .../qt5/test/ts_qm/clean/sconstest-clean.py | 54 + .../qt5/test/ts_qm/mixdir/image/MyFile.cpp | 7 + .../qt5/test/ts_qm/mixdir/image/MyFile.h | 13 + .../qt5/test/ts_qm/mixdir/image/SConscript | 3 + .../qt5/test/ts_qm/mixdir/image/SConstruct | 6 + .../test/ts_qm/mixdir/image/subdir/bbb.cpp | 6 + .../qt5/test/ts_qm/mixdir/image/subdir/bbb.h | 13 + .../qt5/test/ts_qm/mixdir/sconstest-mixdir.py | 49 + .../test/ts_qm/multisource/image/MyFile.cpp | 7 + .../qt5/test/ts_qm/multisource/image/MyFile.h | 13 + .../test/ts_qm/multisource/image/SConscript | 6 + .../test/ts_qm/multisource/image/SConstruct | 6 + .../qt5/test/ts_qm/multisource/image/bbb.cpp | 6 + .../qt5/test/ts_qm/multisource/image/bbb.h | 13 + .../multisource/sconstest-multisource.py | 61 + .../test/ts_qm/multitarget/image/MyFile.cpp | 7 + .../qt5/test/ts_qm/multitarget/image/MyFile.h | 13 + .../test/ts_qm/multitarget/image/SConscript | 4 + .../test/ts_qm/multitarget/image/SConstruct | 6 + .../multitarget/sconstest-multitarget.py | 59 + .../qt5/test/ts_qm/noclean/image/MyFile.cpp | 7 + .../qt5/test/ts_qm/noclean/image/MyFile.h | 13 + .../qt5/test/ts_qm/noclean/image/SConscript | 4 + .../qt5/test/ts_qm/noclean/image/SConstruct | 6 + .../test/ts_qm/noclean/sconstest-noclean.py | 54 + 206 files changed, 7582 insertions(+), 1 deletion(-) create mode 100644 workspace/SConstruct create mode 100644 workspace/sconstools3/qt5/README.rst create mode 100644 workspace/sconstools3/qt5/__init__.py create mode 100644 workspace/sconstools3/qt5/docs/SConstruct create mode 100644 workspace/sconstools3/qt5/docs/html.xsl create mode 100644 workspace/sconstools3/qt5/docs/manual.xml create mode 100644 workspace/sconstools3/qt5/docs/pdf.xsl create mode 100644 workspace/sconstools3/qt5/docs/qt5.xml create mode 100644 workspace/sconstools3/qt5/docs/reference.xml create mode 100644 workspace/sconstools3/qt5/docs/scons.css create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-after create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-before create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.h create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/local_include/local_include.h create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended-fail.py create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended.py create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-after create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-before create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.h create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/local_include/local_include.h create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH-fail.py create mode 100644 workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH.py create mode 100644 workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.h create mode 100644 workspace/sconstools3/qt5/test/basic/copied-env/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/basic/copied-env/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/copied-env/sconstest-copied-env.py create mode 100644 workspace/sconstools3/qt5/test/basic/empty-env/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/empty-env/image/foo6.h create mode 100644 workspace/sconstools3/qt5/test/basic/empty-env/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/empty-env/sconstest-empty-env.py create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/aaa.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/bbb.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/bbb.h create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/ddd.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/eee.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/eee.h create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/fff.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.h create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.ui create mode 100644 workspace/sconstools3/qt5/test/basic/manual/image/ui/fff.ui create mode 100644 workspace/sconstools3/qt5/test/basic/manual/sconstest-manual.py create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.h create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/useit.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-cpp/sconstest-moc-from-cpp.py create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.h create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/sconstest-moc-from-header-nocompile.py create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.h create mode 100644 workspace/sconstools3/qt5/test/basic/moc-from-header/sconstest-moc-from-header.py create mode 100644 workspace/sconstools3/qt5/test/basic/reentrant/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/basic/reentrant/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/reentrant/image/foo5.h create mode 100644 workspace/sconstools3/qt5/test/basic/reentrant/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/reentrant/sconstest-reentrant.py create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.h create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/anUiFile.ui create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.h create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.cpp create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.h create mode 100644 workspace/sconstools3/qt5/test/basic/variantdir/sconstest-installed.py create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.h create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.h create mode 100644 workspace/sconstools3/qt5/test/moc/auto/ccomment/sconstest-ccomment.py create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.h create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.h create mode 100644 workspace/sconstools3/qt5/test/moc/auto/literalstring/sconstest-literalstring.py create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript-fails create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/main.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromCpp.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromH.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default-fails.py create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default.py create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript-fails create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/main.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromCpp.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromH.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific-fails.py create mode 100644 workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific.py create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.h create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.h create mode 100644 workspace/sconstools3/qt5/test/moc/explicit/sconstest-explicit.py create mode 100644 workspace/sconstools3/qt5/test/moc/order_independent/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/moc/order_independent/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.h create mode 100644 workspace/sconstools3/qt5/test/moc/order_independent/image/bbb.cpp create mode 100644 workspace/sconstools3/qt5/test/moc/order_independent/sconstest-order-independent.py create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/SConscript create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/SConscript-wflags create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/image/icons.qrc create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/image/icons/scons.png create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/sconstest-basic.py create mode 100644 workspace/sconstools3/qt5/test/qrc/basic/sconstest-basicwflags.py create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/SConscript create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/SConscript-wflags create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/image/icons.qrc create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/image/icons/scons.png create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/sconstest-manual.py create mode 100644 workspace/sconstools3/qt5/test/qrc/manual/sconstest-manualwflags.py create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/SConscript create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/SConscript-manual create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/image/icons.qrc create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/image/icons/scons.png create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/image/other.qrc create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/image/other/rocks.png create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles-manual.py create mode 100644 workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles.py create mode 100644 workspace/sconstools3/qt5/test/qrc/othername/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/qrc/othername/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/qrc/othername/image/icons.qrc create mode 100644 workspace/sconstools3/qt5/test/qrc/othername/image/icons/scons.png create mode 100644 workspace/sconstools3/qt5/test/qrc/othername/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/qrc/othername/sconstest-othername.py create mode 100644 workspace/sconstools3/qt5/test/qrc/samefilename/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/qrc/samefilename/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.cpp create mode 100644 workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.qrc create mode 100644 workspace/sconstools3/qt5/test/qrc/samefilename/image/icons/scons.png create mode 100644 workspace/sconstools3/qt5/test/qrc/samefilename/sconstest-samefilename.py create mode 100644 workspace/sconstools3/qt5/test/qrc/subdir/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/qrc/subdir/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/qrc/subdir/image/main.cpp create mode 100644 workspace/sconstools3/qt5/test/qrc/subdir/image/qrc/icons.qrc create mode 100644 workspace/sconstools3/qt5/test/qrc/subdir/image/qrc/icons/scons.png create mode 100644 workspace/sconstools3/qt5/test/qrc/subdir/sconstest-subdir.py create mode 100644 workspace/sconstools3/qt5/test/qt_examples/create_scons_tests.py create mode 100644 workspace/sconstools3/qt5/test/qt_examples/sconstest.skip create mode 100644 workspace/sconstools3/qt5/test/qtenv.py create mode 100644 workspace/sconstools3/qt5/test/sconstest.skip create mode 100644 workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.cpp create mode 100644 workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.h create mode 100644 workspace/sconstools3/qt5/test/ts_qm/clean/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/ts_qm/clean/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/ts_qm/clean/sconstest-clean.py create mode 100644 workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.cpp create mode 100644 workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.h create mode 100644 workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.cpp create mode 100644 workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.h create mode 100644 workspace/sconstools3/qt5/test/ts_qm/mixdir/sconstest-mixdir.py create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.cpp create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.h create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.cpp create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.h create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multisource/sconstest-multisource.py create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.cpp create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.h create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/ts_qm/multitarget/sconstest-multitarget.py create mode 100644 workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.cpp create mode 100644 workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.h create mode 100644 workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConscript create mode 100644 workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConstruct create mode 100644 workspace/sconstools3/qt5/test/ts_qm/noclean/sconstest-noclean.py diff --git a/.gitignore b/.gitignore index 4bea2d7..1e3795a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,14 @@ __pycache__/ # C extensions *.so *.dll +binary/ + +# installed include files +include/ + +# scons +.sconsign.dblite +workspace/build # Distribution / packaging .Python diff --git a/nexxT/src/SConscript.py b/nexxT/src/SConscript.py index ca3cd26..f8ea46e 100644 --- a/nexxT/src/SConscript.py +++ b/nexxT/src/SConscript.py @@ -98,4 +98,5 @@ # install python extension and library files into project directory env.RegisterTargets(env.Install(srcDir.Dir("..").Dir("binary").Dir(env.subst("$target_platform")).Dir(env.subst("$variant")).abspath, pyext+apilib)) -env.RegisterTargets(env.Install(srcDir.Dir("..").Dir("include").abspath, Glob(srcDir.abspath + "/*.hpp"))) +if env["variant"] == "release": + env.RegisterTargets(env.Install(srcDir.Dir("..").Dir("include").abspath, Glob(srcDir.abspath + "/*.hpp"))) diff --git a/workspace/SConstruct b/workspace/SConstruct new file mode 100644 index 0000000..b76b1ce --- /dev/null +++ b/workspace/SConstruct @@ -0,0 +1,40 @@ +import platform + +if platform.system() == "Linux": + env = Environment(target_platform="linux_x86_64", + toolpath=["#/sconstools3"], + variant="unknown") + + env['ENV']['PKG_CONFIG_PATH'] = env.subst("$QT5DIR") + '/lib/pkgconfig' + env.PrependENVPath('LD_LIBRARY_PATH', env.subst("$QT5DIR") + "/lib") + env.Tool("qt5") + +else: + # windows environment + env = Environment(MSVC_VERSION="14.0", + tools=["default", "qt5"], + toolpath=["#/sconstools3"], + target_platform="msvc_x86_64", + variant="unknown") + +env.EnableQt5Modules(['QtCore']) +env.AddMethod(lambda env, args: args, "RegisterSources") +env.AddMethod(lambda env, args: None, "RegisterTargets") + +if platform.system() == "Linux": + dbg_env = env.Clone(CCFLAGS=Split("-g -std=c++14 -O0"), #-fvisibility=hidden + LINKFLAGS=Split("-g"), + variant="debug") + rel_env = env.Clone(CCFLAGS=Split("-std=c++14 -O3"), #-fvisibility=hidden + variant="release") +else: + dbg_env = env.Clone(CCFLAGS=Split("/nologo /EHsc /TP /W3 /Od /Ob2 /Z7 /MD /std:c++14"), + LINKFLAGS=Split("/nologo /DEBUG"), + variant="nonopt", + CPPDEFINES=["Py_LIMITED_API"]) # not exactly sure why we need this in debug mode and not in release mode... + rel_env = env.Clone(CCFLAGS=Split("/nologo /EHsc /TP /W3 /Ox /Z7 /MD /std:c++14"), + LINKFLAGS=Split("/nologo /DEBUG"), + variant="release") + +SConscript('../nexxT/src/SConscript.py', variant_dir="build/nonopt", exports=dict(env=dbg_env), duplicate=0) +SConscript('../nexxT/src/SConscript.py', variant_dir="build/release", exports=dict(env=rel_env), duplicate=0) diff --git a/workspace/sconstools3/qt5/README.rst b/workspace/sconstools3/qt5/README.rst new file mode 100644 index 0000000..b9f8e27 --- /dev/null +++ b/workspace/sconstools3/qt5/README.rst @@ -0,0 +1,351 @@ +#################################### +The SCons qt5 tool +#################################### + +Basics +====== +This tool can be used to compile Qt projects, designed for versions 5.x.y and higher. +It is not usable for Qt3 and older versions, since some of the helper tools +(``moc``, ``uic``) behave different. + +Install +------- +Installing it, requires you to copy (or, even better: checkout) the contents of the +package's ``qt5`` folder to + +#. "``/path_to_your_project/site_scons/site_tools/qt5``", if you need the Qt5 Tool in one project only, or +#. "``~/.scons/site_scons/site_tools/qt5``", for a system-wide installation under your current login. + +For more infos about this, please refer to + +* the SCons User's Guide, sect. "Where to put your custom Builders and Tools" and +* the SCons Tools Wiki page at `http://scons.org/wiki/ToolsIndex `_. + +How to activate +--------------- +For activating the tool "qt5", you have to add its name to the Environment constructor, +like this + +:: + + env = Environment(tools=['default','qt5']) + + +On its startup, the Qt5 tool tries to read the variable ``QT5DIR`` from the current +Environment and ``os.environ``. If it is not set, the value of ``QTDIR`` (in +Environment/``os.environ``) is used as a fallback. + +So, you either have to explicitly give the path of your Qt5 installation to the +Environment with + +:: + + env['QT5DIR'] = '/usr/local/Trolltech/Qt-5.2.3' + + +or set the ``QT5DIR`` as environment variable in your shell. + + +Requirements +------------ +Under Linux, "qt5" uses the system tool ``pkg-config`` for automatically +setting the required compile and link flags of the single Qt5 modules (like QtCore, +QtGui,...). +This means that + +#. you should have ``pkg-config`` installed, and +#. you additionally have to set ``PKG_CONFIG_PATH`` in your shell environment, such + that it points to $``QT5DIR/lib/pkgconfig`` (or $``QT5DIR/lib`` for some older versions). + +Based on these two environment variables (``QT5DIR`` and ``PKG_CONFIG_PATH``), +the "qt5" tool initializes all ``QT5_*`` +construction variables listed in the Reference manual. This happens when the tool +is "detected" during Environment construction. As a consequence, the setup +of the tool gets a two-stage process, if you want to override the values provided +by your current shell settings: + +:: + + # Stage 1: create plain environment + qtEnv = Environment() + # Set new vars + qtEnv['QT5DIR'] = '/usr/local/Trolltech/Qt-5.2.3 + qtEnv['ENV']['PKG_CONFIG_PATH'] = '/usr/local/Trolltech/Qt-5.2.3/lib/pkgconfig' + # Stage 2: add qt5 tool + qtEnv.Tool('qt5') + + + + +Suggested boilerplate +===================== +Based on the requirements above, we suggest a simple ready-to-go setup +as follows: + +SConstruct + +:: + + # Detect Qt version + qtdir = detectLatestQtDir() + + # Create base environment + baseEnv = Environment() + #...further customization of base env + + # Clone Qt environment + qtEnv = baseEnv.Clone() + # Set QT5DIR and PKG_CONFIG_PATH + qtEnv['ENV']['PKG_CONFIG_PATH'] = os.path.join(qtdir, 'lib/pkgconfig') + qtEnv['QT5DIR'] = qtdir + # Add qt5 tool + qtEnv.Tool('qt5') + #...further customization of qt env + + # Export environments + Export('baseEnv qtEnv') + + # Your other stuff... + # ...including the call to your SConscripts + + +In a SConscript + +:: + + # Get the Qt5 environment + Import('qtEnv') + # Clone it + env = qtEnv.clone() + # Patch it + env.Append(CCFLAGS=['-m32']) # or whatever + # Use it + env.StaticLibrary('foo', Glob('*.cpp')) + + +The detection of the Qt directory could be as simple as directly assigning +a fixed path + +:: + + def detectLatestQtDir(): + return "/usr/local/qt5.3.2" + + +or a little more sophisticated + +:: + + # Tries to detect the path to the installation of Qt with + # the highest version number + def detectLatestQtDir(): + if sys.platform.startswith("linux"): + # Simple check: inspect only '/usr/local/Trolltech' + paths = glob.glob('/usr/local/Trolltech/*') + if len(paths): + paths.sort() + return paths[-1] + else: + return "" + else: + # Simple check: inspect only 'C:\Qt' + paths = glob.glob('C:\\Qt\\*') + if len(paths): + paths.sort() + return paths[-1] + else: + return os.environ.get("QTDIR","") + + + +A first project +=============== +The following SConscript is for a simple project with +some cxx files, using the QtCore, QtGui +and QtNetwork modules: + +:: + + Import('qtEnv') + env = qtEnv.Clone() + env.EnableQt5Modules([ + 'QtGui', + 'QtCore', + 'QtNetwork' + ]) + # Add your CCFLAGS and CPPPATHs to env here... + + env.Program('foo', Glob('*.cpp')) + + + +MOC it up +========= +For the basic support of automocing, nothing needs to be +done by the user. The tool usually detects the ``Q_OBJECT`` +macro and calls the "``moc``" executable accordingly. + +If you don't want this, you can switch off the automocing +by a + +:: + + env['QT5_AUTOSCAN'] = 0 + + +in your SConscript file. Then, you have to moc your files +explicitly, using the Moc5 builder. + +You can also switch to an extended automoc strategy with + +:: + + env['QT5_AUTOSCAN_STRATEGY'] = 1 + + +Please read the description of the ``QT5_AUTOSCAN_STRATEGY`` +variable in the Reference manual for details. + +For debugging purposes, you can set the variable ``QT5_DEBUG`` +with + +:: + + env['QT5_DEBUG'] = 1 + + +which outputs a lot of messages during automocing. + + +Forms (.ui) +=========== +The header files with setup code for your GUI classes, are not +compiled automatically from your ``.ui`` files. You always +have to call the Uic5 builder explicitly like + +:: + + env.Uic5(Glob('*.ui')) + env.Program('foo', Glob('*.cpp')) + + + +Resource files (.qrc) +===================== +Resource files are not built automatically, you always +have to add the names of the ``.qrc`` files to the source list +for your program or library: + +:: + + env.Program('foo', Glob('*.cpp')+Glob('*.qrc')) + + +For each of the Resource input files, its prefix defines the +name of the resulting resource. An appropriate "``-name``" option +is added to the call of the ``rcc`` executable +by default. + +You can also call the Qrc5 builder explicitly as + +:: + + qrccc = env.Qrc5('foo') # ['foo.qrc'] -> ['qrc_foo.cc'] + + +or (overriding the default suffix) + +:: + + qrccc = env.Qrc5('myprefix_foo.cxx','foo.qrc') # -> ['qrc_myprefix_foo.cxx'] + + +and then add the resulting cxx file to the sources of your +Program/Library: + +:: + + env.Program('foo', Glob('*.cpp') + qrccc) + + + +Translation files +================= +The update of the ``.ts`` files and the conversion to binary +``.qm`` files is not done automatically. You have to call the +corresponding builders on your own. + +Example for updating a translation file: + +:: + + env.Ts5('foo.ts','.') # -> ['foo.ts'] + + +By default, the ``.ts`` files are treated as *precious* targets. This means that +they are not removed prior to a rebuild, but simply get updated. Additionally, they +do not get cleaned on a "``scons -c``". If you want to delete the translation files +on the "``-c``" SCons command, you can set the variable "``QT5_CLEAN_TS``" like this + +:: + + env['QT5_CLEAN_TS']=1 + + +Example for releasing a translation file, i.e. compiling +it to a ``.qm`` binary file: + +:: + + env.Qm5('foo') # ['foo.ts'] -> ['foo.qm'] + + +or (overriding the output prefix) + +:: + + env.Qm5('myprefix','foo') # ['foo.ts'] -> ['myprefix.qm'] + + +As an extension both, the Ts5() and Qm5 builder, support the definition of +multiple targets. So, calling + +:: + + env.Ts5(['app_en','app_de'], Glob('*.cpp')) + + +and + +:: + + env.Qm5(['app','copy'], Glob('*.ts')) + + +should work fine. + +Finally, two short notes about the support of directories for the Ts5() builder. You can +pass an arbitrary mix of cxx files and subdirs to it, as in + +:: + + env.Ts5('app_en',['sub1','appwindow.cpp','main.cpp'])) + + +where ``sub1`` is a folder that gets scanned recursively for cxx files by ``lupdate``. +But like this, you lose all dependency information for the subdir, i.e. if a file +inside the folder changes, the .ts file is not updated automatically! In this case +you should tell SCons to always update the target: + +:: + + ts = env.Ts5('app_en',['sub1','appwindow.cpp','main.cpp']) + env.AlwaysBuild(ts) + + +Last note: specifying the current folder "``.``" as input to Ts5() and storing the resulting +.ts file in the same directory, leads to a dependency cycle! You then have to store the .ts +and .qm files outside of the current folder, or use ``Glob('*.cpp'))`` instead. + + + diff --git a/workspace/sconstools3/qt5/__init__.py b/workspace/sconstools3/qt5/__init__.py new file mode 100644 index 0000000..b943dc2 --- /dev/null +++ b/workspace/sconstools3/qt5/__init__.py @@ -0,0 +1,1017 @@ + +"""SCons.Tool.qt5 + +Tool-specific initialization for Qt5. + +There normally shouldn't be any need to import this module directly. +It will usually be imported through the generic SCons.Tool.Tool() +selection method. + +""" + +# +# Copyright (c) 2001-7,2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +from __future__ import print_function + +import os.path +import re +import sys + +import SCons.Action +import SCons.Builder +import SCons.Defaults +import SCons.Scanner +import SCons.Tool +import SCons.Util + +class ToolQt5Warning(SCons.Warnings.Warning): + pass + +class GeneratedMocFileNotIncluded(ToolQt5Warning): + pass + +class QtdirNotFound(ToolQt5Warning): + pass + +SCons.Warnings.enableWarningClass(ToolQt5Warning) + +try: + sorted +except NameError: + # Pre-2.4 Python has no sorted() function. + # + # The pre-2.4 Python list.sort() method does not support + # list.sort(key=) nor list.sort(reverse=) keyword arguments, so + # we must implement the functionality of those keyword arguments + # by hand instead of passing them to list.sort(). + def sorted(iterable, cmp=None, key=None, reverse=0): + if key is not None: + result = [(key(x), x) for x in iterable] + else: + result = iterable[:] + if cmp is None: + # Pre-2.3 Python does not support list.sort(None). + result.sort() + else: + result.sort(cmp) + if key is not None: + result = [t1 for t0,t1 in result] + if reverse: + result.reverse() + return result + +def _contents_regex(e): + # get_contents() of scons nodes returns a binary buffer, so we convert the regexes also to binary here + # this won't work for specific encodings like UTF-16, but most of the time we will be fine here. + # note that the regexes used here are always pure ascii, so we don't have an issue here. + if sys.version_info.major >= 3: + e = e.encode('ascii') + return e + +qrcinclude_re = re.compile(r']*>([^<]*)', re.M) + +mocver_re = re.compile(_contents_regex(r'.*(\d+)\.(\d+)\.(\d+).*')) + +def transformToWinePath(path) : + return os.popen('winepath -w "%s"'%path).read().strip().replace('\\','/') + +header_extensions = [".h", ".hxx", ".hpp", ".hh"] +if SCons.Util.case_sensitive_suffixes('.h', '.H'): + header_extensions.append('.H') +# TODO: The following two lines will work when integrated back to SCons +# TODO: Meanwhile the third line will do the work +#cplusplus = __import__('c++', globals(), locals(), []) +#cxx_suffixes = cplusplus.CXXSuffixes +cxx_suffixes = [".c", ".cxx", ".cpp", ".cc"] + +def checkMocIncluded(target, source, env): + moc = target[0] + cpp = source[0] + # looks like cpp.includes is cleared before the build stage :-( + # not really sure about the path transformations (moc.cwd? cpp.cwd?) :-/ + path = SCons.Defaults.CScan.path_function(env, moc.cwd) + includes = SCons.Defaults.CScan(cpp, env, path) + if not moc in includes: + SCons.Warnings.warn( + GeneratedMocFileNotIncluded, + "Generated moc file '%s' is not included by '%s'" % + (str(moc), str(cpp))) + +def find_file(filename, paths, node_factory): + for dir in paths: + node = node_factory(filename, dir) + if node.rexists(): + return node + return None + +class _Automoc: + """ + Callable class, which works as an emitter for Programs, SharedLibraries and + StaticLibraries. + """ + + def __init__(self, objBuilderName): + self.objBuilderName = objBuilderName + # some regular expressions: + # Q_OBJECT detection + self.qo_search = re.compile(_contents_regex(r'[^A-Za-z0-9](Q_OBJECT)|(Q_GADGET)[^A-Za-z0-9]')) + # cxx and c comment 'eater' + self.ccomment = re.compile(_contents_regex(r'/\*(.*?)\*/'),re.S) + self.cxxcomment = re.compile(_contents_regex(r'//.*$'),re.M) + # we also allow Q_OBJECT in a literal string + self.literal_qobject = re.compile(_contents_regex(r'"[^\n]*(Q_OBJECT)|(Q_GADGET)[^\n]*"')) + + def create_automoc_options(self, env): + """ + Create a dictionary with variables related to Automocing, + based on the current environment. + Is executed once in the __call__ routine. + """ + moc_options = {'auto_scan' : True, + 'auto_scan_strategy' : 0, + 'gobble_comments' : 0, + 'debug' : 0, + 'auto_cpppath' : True, + 'cpppaths' : []} + try: + if int(env.subst('$QT5_AUTOSCAN')) == 0: + moc_options['auto_scan'] = False + except ValueError: + pass + try: + moc_options['auto_scan_strategy'] = int(env.subst('$QT5_AUTOSCAN_STRATEGY')) + except ValueError: + pass + try: + moc_options['gobble_comments'] = int(env.subst('$QT5_GOBBLECOMMENTS')) + except ValueError: + pass + try: + moc_options['debug'] = int(env.subst('$QT5_DEBUG')) + except ValueError: + pass + try: + if int(env.subst('$QT5_AUTOMOC_SCANCPPPATH')) == 0: + moc_options['auto_cpppath'] = False + except ValueError: + pass + if moc_options['auto_cpppath']: + paths = env.get('QT5_AUTOMOC_CPPPATH', []) + if not paths: + paths = env.get('CPPPATH', []) + moc_options['cpppaths'].extend(paths) + + return moc_options + + def __automoc_strategy_simple(self, env, moc_options, + cpp, cpp_contents, out_sources): + """ + Default Automoc strategy (Q_OBJECT driven): detect a header file + (alongside the current cpp/cxx) that contains a Q_OBJECT + macro...and MOC it. + If a Q_OBJECT macro is also found in the cpp/cxx itself, + it gets MOCed too. + """ + + h=None + for h_ext in header_extensions: + # try to find the header file in the corresponding source + # directory + hname = self.splitext(cpp.name)[0] + h_ext + h = find_file(hname, [cpp.get_dir()]+moc_options['cpppaths'], env.File) + if h: + if moc_options['debug']: + print("scons: qt5: Scanning '%s' (header of '%s')" % (str(h), str(cpp))) + h_contents = h.get_contents() + if moc_options['gobble_comments']: + h_contents = self.ccomment.sub(_contents_regex(''), h_contents) + h_contents = self.cxxcomment.sub(_contents_regex(''), h_contents) + h_contents = self.literal_qobject.sub(_contents_regex('""'), h_contents) + break + if not h and moc_options['debug']: + print("scons: qt5: no header for '%s'." % (str(cpp))) + if h and self.qo_search.search(h_contents): + # h file with the Q_OBJECT macro found -> add moc_cpp + moc_cpp = env.Moc5(h) + if moc_options['debug']: + print("scons: qt5: found Q_OBJECT macro in '%s', moc'ing to '%s'" % (str(h), str(moc_cpp))) + + # Now, check whether the corresponding CPP file + # includes the moc'ed output directly... + inc_moc_cpp = _contents_regex(r'^\s*#\s*include\s+"%s"' % str(moc_cpp[0])) + if cpp and re.search(inc_moc_cpp, cpp_contents, re.M): + if moc_options['debug']: + print("scons: qt5: CXX file '%s' directly includes the moc'ed output '%s', no compiling required" % (str(cpp), str(moc_cpp))) + env.Depends(cpp, moc_cpp) + else: + moc_o = self.objBuilder(moc_cpp) + if moc_options['debug']: + print("scons: qt5: compiling '%s' to '%s'" % (str(cpp), str(moc_o))) + out_sources.extend(moc_o) + if cpp and self.qo_search.search(cpp_contents): + # cpp file with Q_OBJECT macro found -> add moc + # (to be included in cpp) + moc = env.Moc5(cpp) + env.Ignore(moc, moc) + if moc_options['debug']: + print("scons: qt5: found Q_OBJECT macro in '%s', moc'ing to '%s'" % (str(cpp), str(moc))) + + def __automoc_strategy_include_driven(self, env, moc_options, + cpp, cpp_contents, out_sources): + """ + Automoc strategy #1 (include driven): searches for "include" + statements of MOCed files in the current cpp/cxx file. + This strategy tries to add support for the compilation + of the qtsolutions... + """ + if self.splitext(str(cpp))[1] in cxx_suffixes: + added = False + h_moc = "%s%s%s" % (env.subst('$QT5_XMOCHPREFIX'), + self.splitext(cpp.name)[0], + env.subst('$QT5_XMOCHSUFFIX')) + cxx_moc = "%s%s%s" % (env.subst('$QT5_XMOCCXXPREFIX'), + self.splitext(cpp.name)[0], + env.subst('$QT5_XMOCCXXSUFFIX')) + inc_h_moc = _contents_regex(r'#include\s+"%s"' % h_moc) + inc_cxx_moc = _contents_regex(r'#include\s+"%s"' % cxx_moc) + + # Search for special includes in qtsolutions style + if cpp and re.search(inc_h_moc, cpp_contents): + # cpp file with #include directive for a MOCed header found -> add moc + + # Try to find header file + h=None + hname="" + for h_ext in header_extensions: + # Try to find the header file in the + # corresponding source directory + hname = self.splitext(cpp.name)[0] + h_ext + h = find_file(hname, [cpp.get_dir()]+moc_options['cpppaths'], env.File) + if h: + if moc_options['debug']: + print("scons: qt5: Scanning '%s' (header of '%s')" % (str(h), str(cpp))) + h_contents = h.get_contents() + if moc_options['gobble_comments']: + h_contents = self.ccomment.sub('', h_contents) + h_contents = self.cxxcomment.sub('', h_contents) + h_contents = self.literal_qobject.sub('""', h_contents) + break + if not h and moc_options['debug']: + print("scons: qt5: no header for '%s'." % (str(cpp))) + if h and self.qo_search.search(h_contents): + # h file with the Q_OBJECT macro found -> add moc_cpp + moc_cpp = env.XMoc5(h) + env.Ignore(moc_cpp, moc_cpp) + added = True + # Removing file from list of sources, because it is not to be + # compiled but simply included by the cpp/cxx file. + for idx, s in enumerate(out_sources): + if hasattr(s, "sources") and len(s.sources) > 0: + if str(s.sources[0]) == h_moc: + out_sources.pop(idx) + break + if moc_options['debug']: + print("scons: qt5: found Q_OBJECT macro in '%s', moc'ing to '%s'" % (str(h), str(h_moc))) + else: + if moc_options['debug']: + print("scons: qt5: found no Q_OBJECT macro in '%s', but a moc'ed version '%s' gets included in '%s'" % (str(h), inc_h_moc, cpp.name)) + + if cpp and re.search(inc_cxx_moc, cpp_contents): + # cpp file with #include directive for a MOCed cxx file found -> add moc + if self.qo_search.search(cpp_contents): + moc = env.XMoc5(target=cxx_moc, source=cpp) + env.Ignore(moc, moc) + added = True + if moc_options['debug']: + print("scons: qt5: found Q_OBJECT macro in '%s', moc'ing to '%s'" % (str(cpp), str(moc))) + else: + if moc_options['debug']: + print("scons: qt5: found no Q_OBJECT macro in '%s', although a moc'ed version '%s' of itself gets included" % (cpp.name, inc_cxx_moc)) + + if not added: + # Fallback to default Automoc strategy (Q_OBJECT driven) + self.__automoc_strategy_simple(env, moc_options, cpp, + cpp_contents, out_sources) + + def __call__(self, target, source, env): + """ + Smart autoscan function. Gets the list of objects for the Program + or Lib. Adds objects and builders for the special qt5 files. + """ + moc_options = self.create_automoc_options(env) + + # some shortcuts used in the scanner + self.splitext = SCons.Util.splitext + self.objBuilder = getattr(env, self.objBuilderName) + + # The following is kind of hacky to get builders working properly (FIXME) + objBuilderEnv = self.objBuilder.env + self.objBuilder.env = env + mocBuilderEnv = env.Moc5.env + env.Moc5.env = env + xMocBuilderEnv = env.XMoc5.env + env.XMoc5.env = env + + # make a deep copy for the result; MocH objects will be appended + out_sources = source[:] + + for obj in source: + if not moc_options['auto_scan']: + break + if isinstance(obj,str): # big kludge! + print("scons: qt5: '%s' MAYBE USING AN OLD SCONS VERSION AND NOT CONVERTED TO 'File'. Discarded." % str(obj)) + continue + if not obj.has_builder(): + # binary obj file provided + if moc_options['debug']: + print("scons: qt5: '%s' seems to be a binary. Discarded." % str(obj)) + continue + cpp = obj.sources[0] + if not self.splitext(str(cpp))[1] in cxx_suffixes: + if moc_options['debug']: + print("scons: qt5: '%s' is no cxx file. Discarded." % str(cpp)) + # c or fortran source + continue + try: + cpp_contents = cpp.get_contents() + if moc_options['gobble_comments']: + cpp_contents = self.ccomment.sub('', cpp_contents) + cpp_contents = self.cxxcomment.sub('', cpp_contents) + cpp_contents = self.literal_qobject.sub('""', cpp_contents) + except: continue # may be an still not generated source + + if moc_options['auto_scan_strategy'] == 0: + # Default Automoc strategy (Q_OBJECT driven) + self.__automoc_strategy_simple(env, moc_options, + cpp, cpp_contents, out_sources) + else: + # Automoc strategy #1 (include driven) + self.__automoc_strategy_include_driven(env, moc_options, + cpp, cpp_contents, out_sources) + + # restore the original env attributes (FIXME) + self.objBuilder.env = objBuilderEnv + env.Moc5.env = mocBuilderEnv + env.XMoc5.env = xMocBuilderEnv + + # We return the set of source entries as sorted sequence, else + # the order might accidentally change from one build to another + # and trigger unwanted rebuilds. For proper sorting, a key function + # has to be specified...FS.Entry (and Base nodes in general) do not + # provide a __cmp__, for performance reasons. + return (target, sorted(set(out_sources), key=lambda entry : str(entry))) + +AutomocShared = _Automoc('SharedObject') +AutomocStatic = _Automoc('StaticObject') + +def _detect(env): + """Not really safe, but fast method to detect the Qt5 library""" + try: return env['QT5DIR'] + except KeyError: pass + + try: return env['QTDIR'] + except KeyError: pass + + try: return os.environ['QT5DIR'] + except KeyError: pass + + try: return os.environ['QTDIR'] + except KeyError: pass + + moc = env.WhereIs('moc-qt5') or env.WhereIs('moc5') or env.WhereIs('moc') + if moc: + vernumber = os.popen3('%s -v' % moc)[2].read() + vernumber = mocver_re.match(vernumber) + if vernumber: + vernumber = [ int(x) for x in vernumber.groups() ] + if vernumber < [5, 0, 0]: + vernumber = '.'.join([str(x) for x in vernumber]) + moc = None + SCons.Warnings.warn( + QtdirNotFound, + "QT5DIR variable not defined, and detected moc is for Qt %s" % vernumber) + + QT5DIR = os.path.dirname(os.path.dirname(moc)) + SCons.Warnings.warn( + QtdirNotFound, + "QT5DIR variable is not defined, using moc executable as a hint (QT5DIR=%s)" % QT5DIR) + return QT5DIR + + raise SCons.Errors.StopError( + QtdirNotFound, + "Could not detect Qt 5 installation") + return None + + +def __scanResources(node, env, path, arg): + # Helper function for scanning .qrc resource files + # I've been careful on providing names relative to the qrc file + # If that was not needed this code could be simplified a lot + def recursiveFiles(basepath, path) : + result = [] + for item in os.listdir(os.path.join(basepath, path)) : + itemPath = os.path.join(path, item) + if os.path.isdir(os.path.join(basepath, itemPath)) : + result += recursiveFiles(basepath, itemPath) + else: + result.append(itemPath) + return result + contents = node.get_contents() + if sys.version_info.major >= 3: + # we assume the default xml encoding (utf-8) here + contents = contents.decode('utf-8') + includes = qrcinclude_re.findall(contents) + qrcpath = os.path.dirname(node.path) + dirs = [included for included in includes if os.path.isdir(os.path.join(qrcpath,included))] + # dirs need to include files recursively + for dir in dirs : + includes.remove(dir) + includes+=recursiveFiles(qrcpath,dir) + return includes + +# +# Scanners +# +__qrcscanner = SCons.Scanner.Scanner(name = 'qrcfile', + function = __scanResources, + argument = None, + skeys = ['.qrc']) + +# +# Emitters +# +def __qrc_path(head, prefix, tail, suffix): + if head: + if tail: + return os.path.join(head, "%s%s%s" % (prefix, tail, suffix)) + else: + return "%s%s%s" % (prefix, head, suffix) + else: + return "%s%s%s" % (prefix, tail, suffix) +def __qrc_emitter(target, source, env): + sourceBase, sourceExt = os.path.splitext(SCons.Util.to_String(source[0])) + sHead = None + sTail = sourceBase + if sourceBase: + sHead, sTail = os.path.split(sourceBase) + + t = __qrc_path(sHead, env.subst('$QT5_QRCCXXPREFIX'), + sTail, env.subst('$QT5_QRCCXXSUFFIX')) + + return t, source + +# +# Action generators +# +def __moc_generator_from_h(source, target, env, for_signature): + pass_defines = False + try: + if int(env.subst('$QT5_CPPDEFINES_PASSTOMOC')) == 1: + pass_defines = True + except ValueError: + pass + + if pass_defines: + return '$QT5_MOC $QT5_MOCDEFINES $QT5_MOCFROMHFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE' + else: + return '$QT5_MOC $QT5_MOCFROMHFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE' + +def __moc_generator_from_cxx(source, target, env, for_signature): + pass_defines = False + try: + if int(env.subst('$QT5_CPPDEFINES_PASSTOMOC')) == 1: + pass_defines = True + except ValueError: + pass + + if pass_defines: + return ['$QT5_MOC $QT5_MOCDEFINES $QT5_MOCFROMCXXFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE', + SCons.Action.Action(checkMocIncluded,None)] + else: + return ['$QT5_MOC $QT5_MOCFROMCXXFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE', + SCons.Action.Action(checkMocIncluded,None)] + +def __mocx_generator_from_h(source, target, env, for_signature): + pass_defines = False + try: + if int(env.subst('$QT5_CPPDEFINES_PASSTOMOC')) == 1: + pass_defines = True + except ValueError: + pass + + if pass_defines: + return '$QT5_MOC $QT5_MOCDEFINES $QT5_MOCFROMHFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE' + else: + return '$QT5_MOC $QT5_MOCFROMHFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE' + +def __mocx_generator_from_cxx(source, target, env, for_signature): + pass_defines = False + try: + if int(env.subst('$QT5_CPPDEFINES_PASSTOMOC')) == 1: + pass_defines = True + except ValueError: + pass + + if pass_defines: + return ['$QT5_MOC $QT5_MOCDEFINES $QT5_MOCFROMCXXFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE', + SCons.Action.Action(checkMocIncluded,None)] + else: + return ['$QT5_MOC $QT5_MOCFROMCXXFLAGS $QT5_MOCINCFLAGS -o $TARGET $SOURCE', + SCons.Action.Action(checkMocIncluded,None)] + +def __qrc_generator(source, target, env, for_signature): + name_defined = False + try: + if env.subst('$QT5_QRCFLAGS').find('-name') >= 0: + name_defined = True + except ValueError: + pass + + if name_defined: + return '$QT5_RCC $QT5_QRCFLAGS $SOURCE -o $TARGET' + else: + qrc_suffix = env.subst('$QT5_QRCSUFFIX') + src = str(source[0]) + head, tail = os.path.split(src) + if tail: + src = tail + qrc_suffix = env.subst('$QT5_QRCSUFFIX') + if src.endswith(qrc_suffix): + qrc_stem = src[:-len(qrc_suffix)] + else: + qrc_stem = src + return '$QT5_RCC $QT5_QRCFLAGS -name %s $SOURCE -o $TARGET' % qrc_stem + +# +# Builders +# +__ts_builder = SCons.Builder.Builder( + action = SCons.Action.Action('$QT5_LUPDATECOM','$QT5_LUPDATECOMSTR'), + suffix = '.ts', + source_factory = SCons.Node.FS.Entry) +__qm_builder = SCons.Builder.Builder( + action = SCons.Action.Action('$QT5_LRELEASECOM','$QT5_LRELEASECOMSTR'), + src_suffix = '.ts', + suffix = '.qm') +__qrc_builder = SCons.Builder.Builder( + action = SCons.Action.CommandGeneratorAction(__qrc_generator, {'cmdstr':'$QT5_QRCCOMSTR'}), + source_scanner = __qrcscanner, + src_suffix = '$QT5_QRCSUFFIX', + suffix = '$QT5_QRCCXXSUFFIX', + prefix = '$QT5_QRCCXXPREFIX', + single_source = 1) +__ex_moc_builder = SCons.Builder.Builder( + action = SCons.Action.CommandGeneratorAction(__moc_generator_from_h, {'cmdstr':'$QT5_MOCCOMSTR'})) +__ex_uic_builder = SCons.Builder.Builder( + action = SCons.Action.Action('$QT5_UICCOM', '$QT5_UICCOMSTR'), + src_suffix = '.ui') + + +# +# Wrappers (pseudo-Builders) +# +def Ts5(env, target, source=None, *args, **kw): + """ + A pseudo-Builder wrapper around the LUPDATE executable of Qt5. + lupdate [options] [source-file|path]... -ts ts-files + """ + if not SCons.Util.is_List(target): + target = [target] + if not source: + source = target[:] + if not SCons.Util.is_List(source): + source = [source] + + # Check QT5_CLEAN_TS and use NoClean() function + clean_ts = False + try: + if int(env.subst('$QT5_CLEAN_TS')) == 1: + clean_ts = True + except ValueError: + pass + + result = [] + for t in target: + obj = __ts_builder.__call__(env, t, source, **kw) + # Prevent deletion of the .ts file, unless explicitly specified + if not clean_ts: + env.NoClean(obj) + # Always make our target "precious", such that it is not deleted + # prior to a rebuild + env.Precious(obj) + # Add to resulting target list + result.extend(obj) + + return result + +def Qm5(env, target, source=None, *args, **kw): + """ + A pseudo-Builder wrapper around the LRELEASE executable of Qt5. + lrelease [options] ts-files [-qm qm-file] + """ + if not SCons.Util.is_List(target): + target = [target] + if not source: + source = target[:] + if not SCons.Util.is_List(source): + source = [source] + + result = [] + for t in target: + result.extend(__qm_builder.__call__(env, t, source, **kw)) + + return result + +def Qrc5(env, target, source=None, *args, **kw): + """ + A pseudo-Builder wrapper around the RCC executable of Qt5. + rcc [options] qrc-files -o out-file + """ + if not SCons.Util.is_List(target): + target = [target] + if not source: + source = target[:] + if not SCons.Util.is_List(source): + source = [source] + + result = [] + for t, s in zip(target, source): + result.extend(__qrc_builder.__call__(env, t, s, **kw)) + + return result + +def ExplicitMoc5(env, target, source, *args, **kw): + """ + A pseudo-Builder wrapper around the MOC executable of Qt5. + moc [options] + """ + if not SCons.Util.is_List(target): + target = [target] + if not SCons.Util.is_List(source): + source = [source] + + result = [] + for t in target: + # Is it a header or a cxx file? + result.extend(__ex_moc_builder.__call__(env, t, source, **kw)) + + return result + +def ExplicitUic5(env, target, source, *args, **kw): + """ + A pseudo-Builder wrapper around the UIC executable of Qt5. + uic [options] + """ + if not SCons.Util.is_List(target): + target = [target] + if not SCons.Util.is_List(source): + source = [source] + + result = [] + for t in target: + result.extend(__ex_uic_builder.__call__(env, t, source, **kw)) + + return result + +def generate(env): + """Add Builders and construction variables for qt5 to an Environment.""" + + suffixes = [ + '-qt5', + '-qt5.exe', + '5', + '5.exe', + '', + '.exe', + ] + command_suffixes = ['-qt5', '5', ''] + + def locateQt5Command(env, command, qtdir) : + triedPaths = [] + for suffix in suffixes : + fullpath = os.path.join(qtdir,'bin',command + suffix) + if os.access(fullpath, os.X_OK) : + return fullpath + triedPaths.append(fullpath) + + fullpath = env.Detect([command+s for s in command_suffixes]) + if not (fullpath is None) : return fullpath + + raise Exception("Qt5 command '" + command + "' not found. Tried: " + ', '.join(triedPaths)) + + CLVar = SCons.Util.CLVar + Action = SCons.Action.Action + Builder = SCons.Builder.Builder + + env['QT5DIR'] = _detect(env) + # TODO: 'Replace' should be 'SetDefault' +# env.SetDefault( + env.Replace( + QT5DIR = _detect(env), + QT5_BINPATH = os.path.join('$QT5DIR', 'bin'), + # TODO: This is not reliable to QT5DIR value changes but needed in order to support '-qt5' variants + QT5_MOC = locateQt5Command(env,'moc', env['QT5DIR']), + QT5_UIC = locateQt5Command(env,'uic', env['QT5DIR']), + QT5_RCC = locateQt5Command(env,'rcc', env['QT5DIR']), + QT5_LUPDATE = locateQt5Command(env,'lupdate', env['QT5DIR']), + QT5_LRELEASE = locateQt5Command(env,'lrelease', env['QT5DIR']), + + QT5_AUTOSCAN = 1, # Should the qt5 tool try to figure out, which sources are to be moc'ed? + QT5_AUTOSCAN_STRATEGY = 0, # While scanning for files to moc, should we search for includes in qtsolutions style? + QT5_GOBBLECOMMENTS = 0, # If set to 1, comments are removed before scanning cxx/h files. + QT5_CPPDEFINES_PASSTOMOC = 1, # If set to 1, all CPPDEFINES get passed to the moc executable. + QT5_CLEAN_TS = 0, # If set to 1, translation files (.ts) get cleaned on 'scons -c' + QT5_AUTOMOC_SCANCPPPATH = 1, # If set to 1, the CPPPATHs (or QT5_AUTOMOC_CPPPATH) get scanned for moc'able files + QT5_AUTOMOC_CPPPATH = [], # Alternative paths that get scanned for moc files + + # Some Qt5 specific flags. I don't expect someone wants to + # manipulate those ... + QT5_UICFLAGS = CLVar(''), + QT5_MOCFROMHFLAGS = CLVar(''), + QT5_MOCFROMCXXFLAGS = CLVar('-i'), + QT5_QRCFLAGS = '', + QT5_LUPDATEFLAGS = '', + QT5_LRELEASEFLAGS = '', + + # suffixes/prefixes for the headers / sources to generate + QT5_UISUFFIX = '.ui', + QT5_UICDECLPREFIX = 'ui_', + QT5_UICDECLSUFFIX = '.h', + QT5_MOCINCPREFIX = '-I', + QT5_MOCHPREFIX = 'moc_', + QT5_MOCHSUFFIX = '$CXXFILESUFFIX', + QT5_MOCCXXPREFIX = '', + QT5_MOCCXXSUFFIX = '.moc', + QT5_QRCSUFFIX = '.qrc', + QT5_QRCCXXSUFFIX = '$CXXFILESUFFIX', + QT5_QRCCXXPREFIX = 'qrc_', + QT5_MOCDEFPREFIX = '-D', + QT5_MOCDEFSUFFIX = '', + QT5_MOCDEFINES = '${_defines(QT5_MOCDEFPREFIX, CPPDEFINES, QT5_MOCDEFSUFFIX, __env__)}', + QT5_MOCCPPPATH = [], + QT5_MOCINCFLAGS = '$( ${_concat(QT5_MOCINCPREFIX, QT5_MOCCPPPATH, INCSUFFIX, __env__, RDirs)} $)', + + # Commands for the qt5 support ... + QT5_UICCOM = '$QT5_UIC $QT5_UICFLAGS -o $TARGET $SOURCE', + QT5_LUPDATECOM = '$QT5_LUPDATE $QT5_LUPDATEFLAGS $SOURCES -ts $TARGET', + QT5_LRELEASECOM = '$QT5_LRELEASE $QT5_LRELEASEFLAGS -qm $TARGET $SOURCES', + + # Specialized variables for the Extended Automoc support + # (Strategy #1 for qtsolutions) + QT5_XMOCHPREFIX = 'moc_', + QT5_XMOCHSUFFIX = '.cpp', + QT5_XMOCCXXPREFIX = '', + QT5_XMOCCXXSUFFIX = '.moc', + + ) + + try: + env.AddMethod(Ts5, "Ts5") + env.AddMethod(Qm5, "Qm5") + env.AddMethod(Qrc5, "Qrc5") + env.AddMethod(ExplicitMoc5, "ExplicitMoc5") + env.AddMethod(ExplicitUic5, "ExplicitUic5") + except AttributeError: + # Looks like we use a pre-0.98 version of SCons... + from SCons.Script.SConscript import SConsEnvironment + SConsEnvironment.Ts5 = Ts5 + SConsEnvironment.Qm5 = Qm5 + SConsEnvironment.Qrc5 = Qrc5 + SConsEnvironment.ExplicitMoc5 = ExplicitMoc5 + SConsEnvironment.ExplicitUic5 = ExplicitUic5 + + # Interface builder + uic5builder = Builder( + action = SCons.Action.Action('$QT5_UICCOM', '$QT5_UICCOMSTR'), + src_suffix='$QT5_UISUFFIX', + suffix='$QT5_UICDECLSUFFIX', + prefix='$QT5_UICDECLPREFIX', + single_source = True + #TODO: Consider the uiscanner on new scons version + ) + env['BUILDERS']['Uic5'] = uic5builder + + # Metaobject builder + mocBld = Builder(action={}, prefix={}, suffix={}) + for h in header_extensions: + act = SCons.Action.CommandGeneratorAction(__moc_generator_from_h, {'cmdstr':'$QT5_MOCCOMSTR'}) + mocBld.add_action(h, act) + mocBld.prefix[h] = '$QT5_MOCHPREFIX' + mocBld.suffix[h] = '$QT5_MOCHSUFFIX' + for cxx in cxx_suffixes: + act = SCons.Action.CommandGeneratorAction(__moc_generator_from_cxx, {'cmdstr':'$QT5_MOCCOMSTR'}) + mocBld.add_action(cxx, act) + mocBld.prefix[cxx] = '$QT5_MOCCXXPREFIX' + mocBld.suffix[cxx] = '$QT5_MOCCXXSUFFIX' + env['BUILDERS']['Moc5'] = mocBld + + # Metaobject builder for the extended auto scan feature + # (Strategy #1 for qtsolutions) + xMocBld = Builder(action={}, prefix={}, suffix={}) + for h in header_extensions: + act = SCons.Action.CommandGeneratorAction(__mocx_generator_from_h, {'cmdstr':'$QT5_MOCCOMSTR'}) + xMocBld.add_action(h, act) + xMocBld.prefix[h] = '$QT5_XMOCHPREFIX' + xMocBld.suffix[h] = '$QT5_XMOCHSUFFIX' + for cxx in cxx_suffixes: + act = SCons.Action.CommandGeneratorAction(__mocx_generator_from_cxx, {'cmdstr':'$QT5_MOCCOMSTR'}) + xMocBld.add_action(cxx, act) + xMocBld.prefix[cxx] = '$QT5_XMOCCXXPREFIX' + xMocBld.suffix[cxx] = '$QT5_XMOCCXXSUFFIX' + env['BUILDERS']['XMoc5'] = xMocBld + + # Add the Qrc5 action to the CXX file builder (registers the + # *.qrc extension with the Environment) + cfile_builder, cxxfile_builder = SCons.Tool.createCFileBuilders(env) + qrc_act = SCons.Action.CommandGeneratorAction(__qrc_generator, {'cmdstr':'$QT5_QRCCOMSTR'}) + cxxfile_builder.add_action('$QT5_QRCSUFFIX', qrc_act) + cxxfile_builder.add_emitter('$QT5_QRCSUFFIX', __qrc_emitter) + + # We use the emitters of Program / StaticLibrary / SharedLibrary + # to scan for moc'able files + # We can't refer to the builders directly, we have to fetch them + # as Environment attributes because that sets them up to be called + # correctly later by our emitter. + env.AppendUnique(PROGEMITTER =[AutomocStatic], + SHLIBEMITTER=[AutomocShared], + LIBEMITTER =[AutomocStatic], + ) + + # TODO: Does dbusxml2cpp need an adapter + try: + env.AddMethod(enable_modules, "EnableQt5Modules") + except AttributeError: + # Looks like we use a pre-0.98 version of SCons... + from SCons.Script.SConscript import SConsEnvironment + SConsEnvironment.EnableQt5Modules = enable_modules + +def enable_modules(self, modules, debug=False, crosscompiling=False) : + import sys + + validModules = [ + # Qt Essentials + 'QtCore', + 'QtGui', + 'QtMultimedia', + 'QtMultimediaQuick_p', + 'QtMultimediaWidgets', + 'QtNetwork', + 'QtPlatformSupport', + 'QtQml', + 'QtQmlDevTools', + 'QtQuick', + 'QtQuickParticles', + 'QtSql', + 'QtQuickTest', + 'QtTest', + 'QtWebKit', + 'QtWebKitWidgets', + 'QtWidgets', + # Qt Add-Ons + 'QtConcurrent', + 'QtDBus', + 'QtOpenGL', + 'QtPrintSupport', + 'QtDeclarative', + 'QtScript', + 'QtScriptTools', + 'QtSvg', + 'QtUiTools', + 'QtXml', + 'QtXmlPatterns', + # Qt Tools + 'QtHelp', + 'QtDesigner', + 'QtDesignerComponents', + # Other + 'QtCLucene', + 'QtConcurrent', + 'QtV8' + ] + pclessModules = [ + ] + staticModules = [ + ] + invalidModules=[] + for module in modules: + if module not in validModules : + invalidModules.append(module) + if invalidModules : + raise Exception("Modules %s are not Qt5 modules. Valid Qt5 modules are: %s"% ( + str(invalidModules),str(validModules))) + + moduleDefines = { + 'QtScript' : ['QT_SCRIPT_LIB'], + 'QtSvg' : ['QT_SVG_LIB'], + 'QtSql' : ['QT_SQL_LIB'], + 'QtXml' : ['QT_XML_LIB'], + 'QtOpenGL' : ['QT_OPENGL_LIB'], + 'QtGui' : ['QT_GUI_LIB'], + 'QtNetwork' : ['QT_NETWORK_LIB'], + 'QtCore' : ['QT_CORE_LIB'], + 'QtWidgets' : ['QT_WIDGETS_LIB'], + } + for module in modules : + try : self.AppendUnique(CPPDEFINES=moduleDefines[module]) + except: pass + debugSuffix = '' + if sys.platform in ["darwin", "linux2", "linux"] and not crosscompiling : + if debug : debugSuffix = '_debug' + for module in modules : + if module not in pclessModules : continue + self.AppendUnique(LIBS=[module.replace('Qt','Qt5')+debugSuffix]) + self.AppendUnique(LIBPATH=[os.path.join("$QT5DIR","lib")]) + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include")]) + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include",module)]) + pcmodules = [module.replace('Qt','Qt5')+debugSuffix for module in modules if module not in pclessModules ] + if 'Qt5DBus' in pcmodules: + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include","Qt5DBus")]) + if "Qt5Assistant" in pcmodules: + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include","Qt5Assistant")]) + pcmodules.remove("Qt5Assistant") + pcmodules.append("Qt5AssistantClient") + self.AppendUnique(RPATH=[os.path.join("$QT5DIR","lib")]) + self.ParseConfig('pkg-config %s --libs --cflags'% ' '.join(pcmodules)) + self["QT5_MOCCPPPATH"] = self["CPPPATH"] + return + if sys.platform == "win32" or crosscompiling : + if crosscompiling: + transformedQtdir = transformToWinePath(self['QT5DIR']) + self['QT5_MOC'] = "QT5DIR=%s %s"%( transformedQtdir, self['QT5_MOC']) + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include")]) + try: modules.remove("QtDBus") + except: pass + if debug : debugSuffix = 'd' + if "QtAssistant" in modules: + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include","QtAssistant")]) + modules.remove("QtAssistant") + modules.append("QtAssistantClient") + self.AppendUnique(LIBS=['qtmain'+debugSuffix]) + self.AppendUnique(LIBS=[lib.replace("Qt","Qt5")+debugSuffix for lib in modules if lib not in staticModules]) + self.PrependUnique(LIBS=[lib+debugSuffix for lib in modules if lib in staticModules]) + if 'QtOpenGL' in modules: + self.AppendUnique(LIBS=['opengl32']) + self.AppendUnique(CPPPATH=[ '$QT5DIR/include/']) + self.AppendUnique(CPPPATH=[ '$QT5DIR/include/'+module for module in modules]) + if crosscompiling : + self["QT5_MOCCPPPATH"] = [ + path.replace('$QT5DIR', transformedQtdir) + for path in self['CPPPATH'] ] + else : + self["QT5_MOCCPPPATH"] = self["CPPPATH"] + self.AppendUnique(LIBPATH=[os.path.join('$QT5DIR','lib')]) + return + + """ + if sys.platform=="darwin" : + # TODO: Test debug version on Mac + self.AppendUnique(LIBPATH=[os.path.join('$QT5DIR','lib')]) + self.AppendUnique(LINKFLAGS="-F$QT5DIR/lib") + self.AppendUnique(LINKFLAGS="-L$QT5DIR/lib") #TODO clean! + if debug : debugSuffix = 'd' + for module in modules : +# self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include")]) +# self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include",module)]) +# port qt5-mac: + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include", "qt5")]) + self.AppendUnique(CPPPATH=[os.path.join("$QT5DIR","include", "qt5", module)]) + if module in staticModules : + self.AppendUnique(LIBS=[module+debugSuffix]) # TODO: Add the debug suffix + self.AppendUnique(LIBPATH=[os.path.join("$QT5DIR","lib")]) + else : +# self.Append(LINKFLAGS=['-framework', module]) +# port qt5-mac: + self.Append(LIBS=module) + if 'QtOpenGL' in modules: + self.AppendUnique(LINKFLAGS="-F/System/Library/Frameworks") + self.Append(LINKFLAGS=['-framework', 'AGL']) #TODO ughly kludge to avoid quotes + self.Append(LINKFLAGS=['-framework', 'OpenGL']) + self["QT5_MOCCPPPATH"] = self["CPPPATH"] + return +# This should work for mac but doesn't +# env.AppendUnique(FRAMEWORKPATH=[os.path.join(env['QT5DIR'],'lib')]) +# env.AppendUnique(FRAMEWORKS=['QtCore','QtGui','QtOpenGL', 'AGL']) + """ + +def exists(env): + return _detect(env) diff --git a/workspace/sconstools3/qt5/docs/SConstruct b/workspace/sconstools3/qt5/docs/SConstruct new file mode 100644 index 0000000..e4afdf7 --- /dev/null +++ b/workspace/sconstools3/qt5/docs/SConstruct @@ -0,0 +1,34 @@ +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +import os + +env = Environment(ENV = os.environ, + tools = ['docbook']) + +env.DocbookPdf('manual', DOCBOOK_XSL='pdf.xsl') +env.DocbookPdf('reference', DOCBOOK_XSL='pdf.xsl') + +env.DocbookHtml('manual', DOCBOOK_XSL='html.xsl') +env.DocbookHtml('reference', DOCBOOK_XSL='html.xsl') + diff --git a/workspace/sconstools3/qt5/docs/html.xsl b/workspace/sconstools3/qt5/docs/html.xsl new file mode 100644 index 0000000..b4e9796 --- /dev/null +++ b/workspace/sconstools3/qt5/docs/html.xsl @@ -0,0 +1,55 @@ + + + + + + + + + + +/appendix toc,title +article/appendix nop +/article toc,title +book toc,title,figure,table,example,equation +/chapter toc,title +part toc,title +/preface toc,title +reference toc,title +/sect1 toc +/sect2 toc +/sect3 toc +/sect4 toc +/sect5 toc +/section toc +set toc,title + + + + diff --git a/workspace/sconstools3/qt5/docs/manual.xml b/workspace/sconstools3/qt5/docs/manual.xml new file mode 100644 index 0000000..490a410 --- /dev/null +++ b/workspace/sconstools3/qt5/docs/manual.xml @@ -0,0 +1,388 @@ + + + +
+ The SCons qt5 tool + + + + Dirk Baechle + + + 2012-12-13 + + +
+ Basics + + This tool can be used to compile Qt projects, designed for versions + 5.x.y and higher. It is not usable for Qt3 and older versions, since some + of the helper tools (moc, uic) + behave different. + +
+ Install + + Installing it, requires you to copy (or, even better: checkout) + the contents of the package's qt5 folder to + + + + /path_to_your_project/site_scons/site_tools/qt5, + if you need the Qt5 Tool in one project only, or + + + + ~/.scons/site_scons/site_tools/qt5, + for a system-wide installation under your current login. + + + + For more infos about this, please refer to + + + + the SCons User's Guide, sect. "Where to put your custom + Builders and Tools" and + + + + the SCons Tools Wiki page at http://scons.org/wiki/ToolsIndex. + + +
+ +
+ How to activate + + For activating the tool "qt5", you have to add its name to the + Environment constructor, like this + + env = Environment(tools=['default','qt5']) + + + On its startup, the Qt5 tool tries to read the variable + QT5DIR from the current Environment and + os.environ. If it is not set, the value of + QTDIR (in Environment/os.environ) + is used as a fallback. + + So, you either have to explicitly give the path of your Qt5 + installation to the Environment with + + env['QT5DIR'] = '/usr/local/Trolltech/Qt-5.2.3' + + + or set the QT5DIR as environment variable in + your shell. +
+ +
+ Requirements + + Under Linux, "qt5" uses the system tool + pkg-config for automatically setting the required + compile and link flags of the single Qt5 modules (like QtCore, + QtGui,...). This means that + + + + you should have pkg-config installed, + and + + + + you additionally have to set + PKG_CONFIG_PATH in your shell environment, such + that it points to $QT5DIR/lib/pkgconfig (or + $QT5DIR/lib for some older versions). + + + + Based on these two environment variables + (QT5DIR and PKG_CONFIG_PATH), the + "qt5" tool initializes all QT5_* construction + variables listed in the Reference manual. This happens when the tool is + "detected" during Environment construction. As a consequence, the setup + of the tool gets a two-stage process, if you want to override the values + provided by your current shell settings: + + # Stage 1: create plain environment +qtEnv = Environment() +# Set new vars +qtEnv['QT5DIR'] = '/usr/local/Trolltech/Qt-5.2.3 +qtEnv['ENV']['PKG_CONFIG_PATH'] = '/usr/local/Trolltech/Qt-5.2.3/lib/pkgconfig' +# Stage 2: add qt5 tool +qtEnv.Tool('qt5') + +
+
+ +
+ Suggested boilerplate + + Based on the requirements above, we suggest a simple ready-to-go + setup as follows: + + SConstruct + + # Detect Qt version +qtdir = detectLatestQtDir() + +# Create base environment +baseEnv = Environment() +#...further customization of base env + +# Clone Qt environment +qtEnv = baseEnv.Clone() +# Set QT5DIR and PKG_CONFIG_PATH +qtEnv['ENV']['PKG_CONFIG_PATH'] = os.path.join(qtdir, 'lib/pkgconfig') +qtEnv['QT5DIR'] = qtdir +# Add qt5 tool +qtEnv.Tool('qt5') +#...further customization of qt env + +# Export environments +Export('baseEnv qtEnv') + +# Your other stuff... +# ...including the call to your SConscripts + + + In a SConscript + + # Get the Qt5 environment +Import('qtEnv') +# Clone it +env = qtEnv.clone() +# Patch it +env.Append(CCFLAGS=['-m32']) # or whatever +# Use it +env.StaticLibrary('foo', Glob('*.cpp')) + + + The detection of the Qt directory could be as simple as directly + assigning a fixed path + + def detectLatestQtDir(): + return "/usr/local/qt5.3.2" + + + or a little more sophisticated + + # Tries to detect the path to the installation of Qt with +# the highest version number +def detectLatestQtDir(): + if sys.platform.startswith("linux"): + # Simple check: inspect only '/usr/local/Trolltech' + paths = glob.glob('/usr/local/Trolltech/*') + if len(paths): + paths.sort() + return paths[-1] + else: + return "" + else: + # Simple check: inspect only 'C:\Qt' + paths = glob.glob('C:\\Qt\\*') + if len(paths): + paths.sort() + return paths[-1] + else: + return os.environ.get("QTDIR","") + +
+ +
+ A first project + + The following SConscript is for a simple project with some cxx + files, using the QtCore, QtGui and QtNetwork modules: + + Import('qtEnv') +env = qtEnv.Clone() +env.EnableQt5Modules([ + 'QtGui', + 'QtCore', + 'QtNetwork' + ]) +# Add your CCFLAGS and CPPPATHs to env here... + +env.Program('foo', Glob('*.cpp')) + +
+ +
+ MOC it up + + For the basic support of automocing, nothing needs to be done by the + user. The tool usually detects the Q_OBJECT macro and + calls the moc executable + accordingly. + + If you don't want this, you can switch off the automocing by + a + + env['QT5_AUTOSCAN'] = 0 + + + in your SConscript file. Then, you have to moc your files + explicitly, using the Moc5 builder. + + You can also switch to an extended automoc strategy with + + env['QT5_AUTOSCAN_STRATEGY'] = 1 + + + Please read the description of the + QT5_AUTOSCAN_STRATEGY variable in the Reference manual + for details. + + For debugging purposes, you can set the variable + QT5_DEBUG with + + env['QT5_DEBUG'] = 1 + + + which outputs a lot of messages during automocing. +
+ +
+ Forms (.ui) + + The header files with setup code for your GUI classes, are not + compiled automatically from your .ui files. You always + have to call the Uic5 builder explicitly like + + env.Uic5(Glob('*.ui')) +env.Program('foo', Glob('*.cpp')) + +
+ +
+ Resource files (.qrc) + + Resource files are not built automatically, you always have to add + the names of the .qrc files to the source list for your + program or library: + + env.Program('foo', Glob('*.cpp')+Glob('*.qrc')) + + + For each of the Resource input files, its prefix defines the name of + the resulting resource. An appropriate + -name option is added to the call of the + rcc executable by default. + + You can also call the Qrc5 builder explicitly as + + qrccc = env.Qrc5('foo') # ['foo.qrc'] -> ['qrc_foo.cc'] + + + or (overriding the default suffix) + + qrccc = env.Qrc5('myprefix_foo.cxx','foo.qrc') # -> ['qrc_myprefix_foo.cxx'] + + + and then add the resulting cxx file to the sources of your + Program/Library: + + env.Program('foo', Glob('*.cpp') + qrccc) + +
+ +
+ Translation files + + The update of the .ts files and the conversion to + binary .qm files is not done automatically. You have to + call the corresponding builders on your own. + + Example for updating a translation file: + + env.Ts5('foo.ts','.') # -> ['foo.ts'] + + + By default, the .ts files are treated as + precious targets. This means that they are not + removed prior to a rebuild, but simply get updated. Additionally, they do + not get cleaned on a scons -c. If you + want to delete the translation files on the + -c SCons command, you can set the + variable QT5_CLEAN_TS like this + + env['QT5_CLEAN_TS']=1 + + + Example for releasing a translation file, i.e. compiling it to a + .qm binary file: + + env.Qm5('foo') # ['foo.ts'] -> ['foo.qm'] + + + or (overriding the output prefix) + + env.Qm5('myprefix','foo') # ['foo.ts'] -> ['myprefix.qm'] + + + As an extension both, the Ts5() and Qm5 builder, support the + definition of multiple targets. So, calling + + env.Ts5(['app_en','app_de'], Glob('*.cpp')) + + + and + + env.Qm5(['app','copy'], Glob('*.ts')) + + + should work fine. + + Finally, two short notes about the support of directories for the + Ts5() builder. You can pass an arbitrary mix of cxx files and subdirs to + it, as in + + env.Ts5('app_en',['sub1','appwindow.cpp','main.cpp'])) + + + where sub1 is a folder that gets scanned + recursively for cxx files by lupdate. But like this, + you lose all dependency information for the subdir, i.e. if a file inside + the folder changes, the .ts file is not updated automatically! In this + case you should tell SCons to always update the target: + + ts = env.Ts5('app_en',['sub1','appwindow.cpp','main.cpp']) +env.AlwaysBuild(ts) + + + Last note: specifying the current folder + . as input to Ts5() and storing the + resulting .ts file in the same directory, leads to a dependency cycle! You + then have to store the .ts and .qm files outside of the current folder, or + use Glob('*.cpp')) instead. +
+
diff --git a/workspace/sconstools3/qt5/docs/pdf.xsl b/workspace/sconstools3/qt5/docs/pdf.xsl new file mode 100644 index 0000000..c7495da --- /dev/null +++ b/workspace/sconstools3/qt5/docs/pdf.xsl @@ -0,0 +1,62 @@ + + + + + + + + + + +0pt + + +/appendix toc,title +article/appendix nop +/article toc,title +book toc,title,figure,table,example,equation +/chapter toc,title +part toc,title +/preface toc,title +reference toc,title +/sect1 toc +/sect2 toc +/sect3 toc +/sect4 toc +/sect5 toc +/section toc +set toc,title + + + + + + + + diff --git a/workspace/sconstools3/qt5/docs/qt5.xml b/workspace/sconstools3/qt5/docs/qt5.xml new file mode 100644 index 0000000..d067cad --- /dev/null +++ b/workspace/sconstools3/qt5/docs/qt5.xml @@ -0,0 +1,600 @@ + + + +Sets construction variables for building Qt5 applications. + + +QT5DIR +QT5_BINPATH +QT5_MOC +QT5_UIC +QT5_RCC +QT5_AUTOSCAN +QT5_AUTOSCAN_STRATEGY +QT5_AUTOMOC_CPPPATH +QT5_AUTOMOC_SCANCPPPATH +QT5_UICFLAGS +QT5_MOCFROMHFLAGS +QT5_MOCFROMCXXFLAGS +QT5_UICDECLPREFIX +QT5_UICDECLSUFFIX +QT5_MOCHPREFIX +QT5_MOCHSUFFIX +QT5_MOCCXXPREFIX +QT5_MOCCXXSUFFIX +QT5_MOCDEFPREFIX +QT5_MOCDEFSUFFIX +QT5_UISUFFIX +QT5_UICCOM +QT5_GOBBLECOMMENTS +QT5_CPPDEFINES_PASSTOMOC +QT5_CLEAN_TS +QT5_DEBUG +QT5_XMOCHPREFIX +QT5_XMOCHSUFFIX +QT5_XMOCCXXPREFIX +QT5_XMOCCXXSUFFIX +QT5_LUPDATE +QT5_LRELEASE +QT5_LUPDATEFLAGS +QT5_LRELEASEFLAGS +QT5_QRCFLAGS +QT5_UICDECLPREFIX +QT5_UICDECLSUFFIX +QT5_MOCINCPREFIX +QT5_QRCSUFFIX +QT5_QRCCXXSUFFIX +QT5_QRCCXXPREFIX +QT5_MOCDEFINES +QT5_MOCCPPPATH +QT5_MOCINCFLAGS +QT5_LUPDATECOM +QT5_LRELEASECOM + + + + + + + +Builds an output file from a moc input file. Moc input files are either +header files or cxx files. This builder is only available after using the +tool 'qt5'. It should be your first choice when manually Mocing files +and is the builder for the Q_OBJECT driven Automoc strategy (#0, the default). +It can can be controlled via the QT5_MOC* variables. See the &cv-link-QT5DIR; and +&cv-link-QT5_AUTOSCAN_STRATEGY; variables for more information. +Example: + + +env.Moc5('foo.h') # generates moc_foo.cc +env.Moc5('foo.cpp') # generates foo.moc + + + + + + +Just like the &b-Moc5; builder, it builds an output file from a moc input file. +Moc input files are either header files or cxx files. This builder is only available after using the +tool 'qt5'. It is defined separately for the include driven Automoc strategy (#1) +and can be controlled via the QT5_XMOC* variables. See the &cv-link-QT5DIR; and +&cv-link-QT5_AUTOSCAN_STRATEGY; variables for more information. +Example: + + +env.XMoc5('foo.h') # generates moc_foo.cpp +env.XMoc5('foo.cpp') # generates foo.moc + + + + + + +Just like the &b-Moc5; builder, it builds an output file from a moc input file. +However, it does not use any default prefix or suffix for the filenames. +You can, and have to, specify the full source and target names explicitly. +This builder is only available after using the +tool 'qt5'. It can be your last resort, when you have to moc single files +from/to exotic filenames. +Example: + + +env.ExplicitMoc5('moced_foo.cxx','foo.h') # generates moced_foo.cxx + + + + + + + +Builds a header file from an .ui file, where the former +contains the setup code for a GUI class. +This builder is only available after using the tool 'qt5'. +Using this builder lets you override the standard +naming conventions (be careful: prefixes are always prepended to names of +built files; if you don't want prefixes, you may set them to ``). +See the &cv-link-QT5DIR; variable for more information. +Example: + + +env.Uic5('foo.ui') # -> 'ui_foo.h' + + + + + + +Just like the &b-Uic5; builder, it builds a header file from a .ui input file. +However, it does not use any default prefix or suffix for the filenames. +You can, and have to, specify the full source and target names explicitly. +This builder is only available after using the +tool 'qt5'. It can be your last resort, when you have to convert .ui +files to exotic filenames. +Example: + + +env.ExplicitUic5('uiced_foo.hpp','foo.ui') # generates uiced_foo.hpp + + + + + + +Builds a cxx file, containing all resources from the given +.qrc file. +This builder is only available after using the tool 'qt5'. + +Example: + + +env.Qrc5('foo.qrc') # -> ['qrc_foo.cc'] + + + + + + +Scans the source files in the given path for tr() marked strings, +that should get translated. Writes a .ts file for the Qt Linguist. +This builder is only available after using the tool 'qt5'. + +Example: + + +env.Ts5('foo.ts','.') # -> ['foo.ts'] + + + + + + + +Compiles a given .ts file (Qt Linguist) into a binary .qm file. +This builder is only available after using the tool 'qt5'. + +Example: + + +env.Qm5('foo.ts') # -> ['foo.qm'] + + + + + + + +The Qt5 tool tries to read this from the current Environment and then from os.environ. +If it is not set and found, the value of QTDIR (in the Environment and os.environ) is +used as a fallback. +It also initializes all QT5_* +construction variables listed below. +(Note that all paths are constructed +with python's os.path.join() method, +but are listed here with the '/' separator +for easier reading.) +In addition, the construction environment +variables &cv-link-CPPPATH;, +&cv-link-LIBPATH; and +&cv-link-LIBS; may be modified +and the variables +PROGEMITTER, SHLIBEMITTER and LIBEMITTER +are modified. Because the build-performance is affected when using this tool, +you have to explicitly specify it at Environment creation: + + +Environment(tools=['default','qt5']) + + +The Qt5 tool supports the following operations: + +Automatic moc file generation from header files. +You do not have to specify moc files explicitly, the tool does it for you. +However, there are a few preconditions to do so: Your header file must have +the same filebase as your implementation file and must stay in the same +directory. It must have one of the suffixes .h, .hpp, .H, .hxx, .hh. You +can turn off automatic moc file generation by setting &cv-link-QT5_AUTOSCAN; to 0. +See also the corresponding builder method +&b-Moc5;(). + +Automatic moc file generation from cxx files. +As stated in the Qt documentation, include the moc file at the end of +the cxx file. Note that you have to include the file, which is generated +by the transformation ${QT5_MOCCXXPREFIX}<basename>${QT5_MOCCXXSUFFIX}, by default +<basename>.moc. A warning is generated after building the moc file, if you +do not include the correct file. If you are using VariantDir, you may +need to specify duplicate=1. You can turn off automatic moc file generation +by setting &cv-link-QT5_AUTOSCAN; to 0. See also the corresponding +&b-Moc5; +builder method. + +Handling of .ui files. +TODO: describe a little +See also the corresponding +&b-Uic5; +builder method. + +Handling translation files (.ts and .qm). +TODO: describe a little +See also the corresponding +builder methods &b-Ts5; and &b-Qm5;. + +Compiling resource files (.qrc). +TODO: describe a little +See also the corresponding +&b-Qrc5; builder method. + + + + + + +The default is '1', which means that the tool is automatically +scanning for mocable files (see also &cv-link-QT5_AUTOSCAN_STRATEGY;). +You can set this variable to '0' to +switch it off, and then use the &b-Moc5; Builder to explicitly +specify files to run moc on. + + + + + +The path where the Qt5 binaries are installed. +The default value is '&cv-link-QT5DIR;/bin'. + + + + + +The path where the Qt5 header files are installed. +The default value is '&cv-link-QT5DIR;/include'. +Note: If you set this variable to None, +the tool won't change the &cv-link-CPPPATH; +construction variable. + + + + + +Prints lots of debugging information while scanning for moc files. + + + + + +The path to the Qt5 moc executable. +Default value is '&cv-link-QT5_BINPATH;/moc'. + + + + + +Default value is ''. Prefix for moc output files, when source is a cxx file and the +Automoc strategy #0 (Q_OBJECT driven) is used. + + + + + +Default value is '.moc'. Suffix for moc output files, when source is a cxx file and the +Automoc strategy #0 (Q_OBJECT driven) is used. + + + + + +Default value is '-i'. These flags are passed to moc, when moccing a +C++ file. + + + + + +Default value is ''. These flags are passed to moc, when moccing a header +file. + + + + + +Default value is 'moc_'. Prefix for moc output files, when the source file is a header +and the Automoc strategy #0 (Q_OBJECT driven) is used. + + + + + +Default value is '&cv-link-CXXFILESUFFIX;'. Suffix for moc output files, when +the source file is a header and the Automoc strategy #0 (Q_OBJECT driven) is used. + + + + + +Default value is '&cv-link-QT5_BINPATH;/uic'. The path to the Qt5 uic executable. + + + + + +Command to generate the required header and source files from .ui form files. +Is compiled from &cv-link-QT5_UIC; and &cv-link-QT5_UICFLAGS;. + + + + + +The string displayed when generating header and source files from .ui form files. +If this is not set, then &cv-link-QT5_UICCOM; (the command line) is displayed. + + + + + +Default value is ''. These flags are passed to the Qt5 uic executable, when creating header +and source files from a .ui form file. + + + + + +Default value is 'ui_'. Prefix for uic generated header files. + + + + + +Default value is '.h'. Suffix for uic generated header files. + + + + + +Default value is '.ui'. Suffix of designer input files (form files) in Qt5. + + + + + +Default value is '0'. When using the Automoc feature of the Qt5 tool, you +can select different strategies for detecting which files should get moced. +The simple approach ('0' as the default) scans header and source files for +the Q_OBJECT macro, so the trigger 'moc or not' is Q_OBJECT driven. If it is +found, the corresponding file gets moced with +the &b-Moc5; builder. This results in the files 'moc_foo.cc' and 'foo.moc' for header +and source file, respectively. They get added to the list of sources, for compiling +the current library or program. + +In older Qt manuals, a different technique for mocing is recommended. A cxx file +includes the moced output of itself or its header at the end. This approach is somewhat +deprecated, but the 'qtsolutions' by Qt are still based on it, for example. You also +might have to switch older Qt sources to a new version 5.x.y. Then you can set +this variable to '1', for 'include driven' mocing. This means that the tool searches +for '#include' statements in all cxx files, containing a file pattern 'moc_foo.cpp' +and 'foo.moc' for header and source file, respectively. If the file 'foo.h/foo.cpp' +then contains a Q_OBJECT macro, it gets moced but is NOT added to the list of sources. +This is the important difference between the two strategies. If no matching +include patterns are found for the current cxx file, the Q_OBJECT driven method (#0) +is tried as fallback. + + + + + +The default is '1', meaning that the tool scans +for mocable files not only in the current directory, but +also in all CPPPATH folders (see also &cv-link-QT5_AUTOMOC_CPPPATH;). +You can set this variable to '0' to +switch it off on rare occasions, e.g. when too many search +folders give you a bad performance in large projects. + + + + + +The list of paths to scan for mocable files +(see also &cv-link-QT5_AUTOMOC_SCANCPPPATH;), it is empty by default +which means that the CPPPATH variable is used. +You can set this variable to a subset of CPPPATH in order +to improve performance, i.e. to minimize the search space. + + + + + +Default value is '0' (disabled). When you set this variable to '1', +you enable the automatic removal of C/C++ comments, while searching for +the Q_OBJECT keyword during the Automoc process. This can be helpful if you +have the string Q_OBJECT in one of your comments, but don't want this +file to get moced. + + + + + +Default value is '1' (enabled). When you set this variable to '1', +all currently set CPPDEFINES get passed to the moc executable. It does not matter +which strategy you selected with &cv-link-QT5_AUTOSCAN_STRATEGY; or whether you +call the &b-Moc5; builder directly. + + + + + +Default value is '0' (disabled). When you set this variable to '1', +the &b-Ts5; builder will delete your .ts files on a 'scons -c'. Normally, +these files for the QtLinguist are treated as 'precious' (they are not removed +prior to a rebuild) and do not get cleaned. + + + + + +Default value is 'moc_'. +Like &cv-link-QT5_MOCHPREFIX;, this is the prefix for moc output files, when +the source file is a header +and the Automoc strategy #1 (include driven) is used. + + + + + +Default value is '.cpp'. +Like &cv-link-QT5_MOCHSUFFIX;, this is the suffix for moc output files, when +the source file is a header and the Automoc strategy #1 +(include driven) is used. + + + + + +Default value is ''. +Like &cv-link-QT5_MOCCXXPREFIX;, this is the +prefix for moc output files, when source is a cxx file and the +Automoc strategy #1 (include driven) is used. + + + + + +Default value is '.moc'. +Like &cv-link-QT5_MOCCXXSUFFIX;, this is the +suffix for moc output files, when source is a cxx file and the +Automoc strategy #1 (include driven) is used. + + + + + +Default value is '&cv-link-QT5_BINPATH;/rcc'. The path to the Qt5 rcc +executable (resource file compiler). + + + + + +Default value is '&cv-link-QT5_BINPATH;/lupdate'. The path to the Qt5 lupdate +executable (updates the .ts files from sources). + + + + + +Default value is '&cv-link-QT5_BINPATH;/lrelease'. The path to the Qt5 lrelease +executable (converts .ts files to binary .qm files). + + + + + +Default value is ''. These flags are passed to the Qt5 rcc executable, +when compiling a resource file. + + + + + +Default value is ''. These flags are passed to the Qt5 lupdate executable, +when updating .ts files from sources. + + + + + +Default value is ''. These flags are passed to the Qt5 lrelease executable, +when compiling .ts files into binary .qm files. + + + + + +Default value is '-I'. The prefix for specifying include directories to the +Qt5 moc executable. + + + + + +Default value is '.qrc'. Suffix of Qt5 resource files. + + + + + +Default value is '$CXXFILESUFFIX'. +This is the suffix for compiled .qrc resource files. + + + + + +Default value is 'qrc_'. +This is the prefix for compiled .qrc resource files. + + + + + +List of include paths for the Qt5 moc executable, is compiled from +&cv-link-QT5_MOCINCPREFIX;, &cv-link-QT5_MOCCPPPATH; and &cv-link-INCSUFFIX;. + + + + + +List of CPP defines that are passed to the Qt5 moc executable, +is compiled from &cv-link-QT5_MOCDEFPREFIX;, &cv-link-CPPDEFINES; and &cv-link-QT5_MOCDEFSUFFIX;. + + + + + +Command to update .ts files for translation from the sources. + + + + + +The string displayed when updating .ts files from the sources. +If this is not set, then &cv-link-QT5_LUPDATECOM; (the command line) is displayed. + + + + + +Command to convert .ts files to binary .qm files. + + + + + +The string displayed when converting .ts files to binary .qm files. +If this is not set, then &cv-link-QT5_RCC; (the command line) is displayed. + + + + diff --git a/workspace/sconstools3/qt5/docs/reference.xml b/workspace/sconstools3/qt5/docs/reference.xml new file mode 100644 index 0000000..a9b459a --- /dev/null +++ b/workspace/sconstools3/qt5/docs/reference.xml @@ -0,0 +1,717 @@ + + + +
+ + SCons tool <quote>qt5</quote> - Reference + + + Dirk + + Baechle + + + 2012-12-13 + + + + This reference lists all the variables that are used within the + qt5 tool, and the available builders. It is intended for + SCons tool developers and core programmers, as a normal user you should + read the manual instead. + + +
+ What it does + + The qt5 tool sets construction variables and + registers builders for creating applications and libraries that use the + Qt5 framework by Trolltech/Nokia. + + It supports the following operations: + +
+ Automatic moc file generation from header files + + You do not have to specify moc files explicitly, the tool does it + for you. However, there are a few preconditions to do so: Your header + file must have the same filebase as your implementation file. It must + have one of the suffixes .h, .hpp, + .H, .hxx, .hh. + You can turn off automatic moc file generation by setting + QT5_AUTOSCAN to 0. See also the corresponding builder + method Moc5(). +
+ +
+ Automatic moc file generation from cxx files + + As stated in the Qt documentation, include the moc file at the end + of the cxx file. Note that you have to include the file, which is + generated by the transformation + + ${QT5_MOCCXXPREFIX}<basename>${QT5_MOCCXXSUFFIX}, + by default <basename>.moc. A warning is + generated after building the moc file, if you do not include the correct + file. If you are using VariantDir, you may need to specify + duplicate=1. You can turn off automatic moc file + generation by setting QT5_AUTOSCAN to 0. See also the + corresponding Moc5 builder method. +
+ +
+ Handling of .ui files + + TODO: describe in a little more detail. + + See also the corresponding Uic5 builder + method. +
+ +
+ Handling translation files (.ts and .qm) + + TODO: describe in a little more detail. + + See also the corresponding builder methods Ts5 and Qm5. +
+ +
+ Compiling resource files (.qrc) + + TODO: describe in a little more detail. + + See also the corresponding Qrc5 builder method. +
+
+ +
+ Builders + + + +
+ Moc5 + + Builds an output file from a moc input file. Moc input files are + either header files or cxx files. This builder is only available after + using the tool 'qt5'. + + Example: + + env.Moc5('foo.h') # generates moc_foo.cc +env.Moc5('foo.cpp') # generates foo.moc + +
+ +
+ XMoc5 + + Just like the Moc5 builder, it builds an output file from a moc + input file. Moc input files are either header files or cxx files. This + builder is only available after using the tool 'qt5'. It is defined + separately for the include driven Automoc strategy (#1) and can be + controlled via the QT5_XMOC* variables. + + Example: + + env.XMoc5('foo.h') # generates moc_foo.cpp +env.XMoc5('foo.cpp') # generates foo.moc + +
+ +
+ ExplicitMoc5 + + Just like the Moc5 builder, it builds an output + file from a moc input file. However, it does not use any default prefix + or suffix for the filenames. You can, and have to, specify the full + source and target names explicitly. This builder is only available after + using the tool 'qt5'. It can be your last resort, when you have to moc + single files from/to exotic filenames. + + Example: + + env.ExplicitMoc5('moced_foo.cxx','foo.h') # generates moced_foo.cxx + +
+ +
+ Uic5 + + Builds a header file from an .ui file, where the former contains + the setup code for a GUI class. This builder is only available after + using the tool 'qt5'. Using this builder lets you override the standard + naming conventions (be careful: prefixes are always prepended to names + of built files; if you don't want prefixes, you may set them to + ``). + + Example: + + env.Uic5('foo.ui') # -> 'ui_foo.h' + +
+ +
+ ExplicitUic5 + + Just like the Uic5 builder, it builds a header + file from a .ui input file. However, it does not use any default prefix + or suffix for the filenames. You can, and have to, specify the full + source and target names explicitly. This builder is only available after + using the tool 'qt5'. It can be your last resort, when you have to + convert .ui files to exotic filenames. + + Example: + + env.ExplicitUic5('uiced_foo.hpp','foo.ui') # generates uiced_foo.hpp + +
+ +
+ Qrc5 + + Builds a cxx file, containing all resources from the given + .qrc file. This builder is only available after using + the tool 'qt5'. + + Example: + + env.Qrc5('foo.qrc') # -> ['qrc_foo.cc'] + +
+ +
+ Ts5 + + Scans the source files in the given path for tr() marked strings, + that should get translated. Writes a .ts file for the + Qt Linguist. This builder is only available after using the tool + 'qt5'. + + Example: + + env.Ts5('foo.ts','.') # -> ['foo.ts'] + +
+ +
+ Qm5 + + Compiles a given .ts file (Qt Linguist) into a + binary .qm file. This builder is only available after + using the tool 'qt5'. + + Example: + + env.Qm5('foo.ts') # -> ['foo.qm'] + +
+
+ +
+ Variables + + + + QT5DIR + + + The Qt5 tool tries to read this from the current Environment + and os.environ. If it is not set and found, the + value of QTDIR (in Environment/os.environ) is + used as a fallback. It is used to initialize all QT5_* + construction variables listed below. + + + + + QT5_AUTOSCAN + + + The default is '1', which means that the tool is + automatically scanning for mocable files (see also + QT5_AUTOSCAN_STRATEGY). You can set this + variable to '0' to switch it off, and then use the + Moc5 Builder to explicitly specify files to run + moc on. + + + + + QT5_BINPATH + + + The path where the Qt5 binaries are installed. The default + value is 'QT5DIR/bin'. + + + + + QT5_MOCCPPPATH + + + The path where the Qt5 header files are installed. The + default value is 'QT5DIR/include'. Note: If you + set this variable to None, the tool won't change the + CPPPATH construction variable. + + + + + QT5_DEBUG + + + Prints lots of debugging information while scanning for moc + files. + + + + + QT5_MOC + + + The path to the Qt5 moc executable. Default value is + 'QT5_BINPATH/moc'. + + + + + QT5_MOCCXXPREFIX + + + Default value is ''. Prefix for moc output files, when + source is a cxx file and the Automoc strategy #0 (Q_OBJECT driven) + is used. + + + + + QT5_MOCCXXSUFFIX + + + Default value is '.moc'. Suffix for moc output files, when + source is a cxx file and the Automoc strategy #0 (Q_OBJECT driven) + is used. + + + + + QT5_MOCFROMCXXFLAGS + + + Default value is '-i'. These flags are passed to moc, when + moccing a C++ file. + + + + + QT5_MOCFROMHFLAGS + + + Default value is ''. These flags are passed to moc, when + moccing a header file. + + + + + QT5_MOCHPREFIX + + + Default value is 'moc_'. Prefix for moc output files, when + the source file is a header and the Automoc strategy #0 (Q_OBJECT + driven) is used. + + + + + QT5_MOCHSUFFIX + + + Default value is 'CXXFILESUFFIX'. Suffix + for moc output files, when the source file is a header and the + Automoc strategy #0 (Q_OBJECT driven) is used. + + + + + QT5_UIC + + + Default value is 'QT5_BINPATH/uic'. The + path to the Qt5 uic executable. + + + + + QT5_UICCOM + + + Command to generate the required header and source files + from .ui form files. Is compiled from QT5_UIC + and QT5_UICFLAGS. + + + + + QT5_UICCOMSTR + + + The string displayed when generating header and source files + from .ui form files. If this is not set, then + QT5_UICCOM (the command line) is + displayed. + + + + + QT5_UICFLAGS + + + Default value is ''. These flags are passed to the Qt5 uic + executable, when creating header and source files from a .ui form + file. + + + + + QT5_UICDECLPREFIX + + + Default value is 'ui_'. Prefix for uic generated header + files. + + + + + QT5_UICDECLSUFFIX + + + Default value is '.h'. Suffix for uic generated header + files. + + + + + QT5_UISUFFIX + + + Default value is '.ui'. Suffix of designer input files (form + files) in Qt5. + + + + + QT5_AUTOSCAN_STRATEGY + + + Default value is '0'. When using the Automoc feature of the + Qt5 tool, you can select different strategies for detecting which + files should get moced. The simple approach ('0' as the default) + scans header and source files for the Q_OBJECT macro, so the + trigger 'moc or not' is Q_OBJECT driven. If it is found, the + corresponding file gets moced with the Moc5 + builder. This results in the files 'moc_foo.cc' and 'foo.moc' for + header and source file, respectively. They get added to the list + of sources, for compiling the current library or program. In older + Qt manuals, a different technique for mocing is recommended. A cxx + file includes the moced output of itself or its header at the end. + This approach is somewhat deprecated, but the 'qtsolutions' by Qt + are still based on it, for example. You also might have to switch + older Qt sources to a new version 5.x.y. Then you can set this + variable to '1', for 'include driven' mocing. This means that the + tool searches for '#include' statements in all cxx files, + containing a file pattern 'moc_foo.cpp' and 'foo.moc' for header + and source file, respectively. If the file 'foo.h/foo.cpp' then + contains a Q_OBJECT macro, it gets moced but is NOT added to the + list of sources. This is the important difference between the two + strategies. If no matching include patterns are found for the + current cxx file, the Q_OBJECT driven method (#0) is tried as + fallback. + + + + + QT5_AUTOMOC_SCANCPPPATH + + + The default is '1', meaning that the tool scans for mocable + files not only in the current directory, but also in all CPPPATH + folders (see also QT5_AUTOMOC_CPPPATH). You can + set this variable to '0' to switch it off on rare occasions, e.g. + when too many search folders give you a bad performance in large + projects. + + + + + QT5_AUTOMOC_CPPPATH + + + The list of paths to scan for mocable files (see also + QT5_AUTOMOC_SCANCPPPATH), it is empty by + default which means that the CPPPATH variable is used. You can set + this variable to a subset of CPPPATH in order to improve + performance, i.e. to minimize the search space. + + + + + QT5_GOBBLECOMMENTS + + + Default value is '0' (disabled). When you set this variable + to '1', you enable the automatic removal of C/C++ comments, while + searching for the Q_OBJECT keyword during the Automoc process. + This can be helpful if you have the string Q_OBJECT in one of your + comments, but don't want this file to get moced. + + + + + QT5_CPPDEFINES_PASSTOMOC + + + Default value is '1' (enabled). When you set this variable + to '1', all currently set CPPDEFINES get passed to the moc + executable. It does not matter which strategy you selected with + QT5_AUTOSCAN_STRATEGY or whether you call the + Moc5 builder directly. + + + + + QT5_CLEAN_TS + + + Default value is '0' (disabled). When you set this variable + to '1', the Ts5 builder will delete your .ts + files on a 'scons -c'. Normally, these files for the QtLinguist + are treated as 'precious' (they are not removed prior to a + rebuild) and do not get cleaned. + + + + + QT5_XMOCHPREFIX + + + Default value is 'moc_'. Like + QT5_MOCHPREFIX, this is the prefix for moc + output files, when the source file is a header and the Automoc + strategy #1 (include driven) is used. + + + + + QT5_XMOCHSUFFIX + + + Default value is '.cpp'. Like + QT5_MOCHSUFFIX, this is the suffix for moc + output files, when the source file is a header and the Automoc + strategy #1 (include driven) is used. + + + + + QT5_XMOCCXXPREFIX + + + Default value is ''. Like + QT5_MOCCXXPREFIX, this is the prefix for moc + output files, when source is a cxx file and the Automoc strategy + #1 (include driven) is used. + + + + + QT5_XMOCCXXSUFFIX + + + Default value is '.moc'. Like + QT5_MOCCXXSUFFIX, this is the suffix for moc + output files, when source is a cxx file and the Automoc strategy + #1 (include driven) is used. + + + + + QT5_RCC + + + Default value is 'QT5_BINPATH/rcc'. The + path to the Qt5 rcc executable (resource file compiler). + + + + + QT5_LUPDATE + + + Default value is 'QT5_BINPATH/lupdate'. + The path to the Qt5 lupdate executable (updates the .ts files from + sources). + + + + + QT5_LRELEASE + + + Default value is 'QT5_BINPATH/lrelease'. + The path to the Qt5 lrelease executable (converts .ts files to + binary .qm files). + + + + + QT5_QRCFLAGS + + + Default value is ''. These flags are passed to the Qt5 rcc + executable, when compiling a resource file. + + + + + QT5_LUPDATEFLAGS + + + Default value is ''. These flags are passed to the Qt5 + lupdate executable, when updating .ts files from sources. + + + + + QT5_LRELEASEFLAGS + + + Default value is ''. These flags are passed to the Qt5 + lrelease executable, when compiling .ts files into binary .qm + files. + + + + + QT5_MOCINCPREFIX + + + Default value is '-I'. The prefix for specifying include + directories to the Qt5 moc executable. + + + + + QT5_QRCSUFFIX + + + Default value is '.qrc'. Suffix of Qt5 resource + files. + + + + + QT5_QRCCXXSUFFIX + + + Default value is '$CXXFILESUFFIX'. This is the suffix for + compiled .qrc resource files. + + + + + QT5_QRCCXXPREFIX + + + Default value is 'qrc_'. This is the prefix for compiled + .qrc resource files. + + + + + QT5_MOCINCFLAGS + + + List of include paths for the Qt5 moc executable, is + compiled from QT5_MOCINCPREFIX, + QT5_MOCCPPPATH and + INCSUFFIX. + + + + + QT5_MOCDEFINES + + + List of CPP defines that are passed to the Qt5 moc + executable, is compiled from QT5_MOCDEFPREFIX, + CPPDEFINES and + QT5_MOCDEFSUFFIX. + + + + + QT5_LUPDATECOM + + + Command to update .ts files for translation from the + sources. + + + + + QT5_LUPDATECOMSTR + + + The string displayed when updating .ts files from the + sources. If this is not set, then + QT5_LUPDATECOM (the command line) is + displayed. + + + + + QT5_LRELEASECOM + + + Command to convert .ts files to binary .qm files. + + + + + QT5_LRELEASECOMSTR + + + The string displayed when converting .ts files to binary .qm + files. If this is not set, then QT5_RCC (the + command line) is displayed. + + + +
+
\ No newline at end of file diff --git a/workspace/sconstools3/qt5/docs/scons.css b/workspace/sconstools3/qt5/docs/scons.css new file mode 100644 index 0000000..6941abb --- /dev/null +++ b/workspace/sconstools3/qt5/docs/scons.css @@ -0,0 +1,263 @@ +body { + background: #ffffff; + margin: 10px; + padding: 0; + font-family:palatino, georgia, verdana, arial, sans-serif; + } + + +a { + color: #80572a; + } + +a:hover { + color: #d72816; + text-decoration: none; + } + +tt { + color: #a14447; + } + +pre { + background: #e0e0e0; + } + +#main { + border: 1px solid; + border-color: black; + background-color: white; + background-image: url(../images/sconsback.png); + background-repeat: repeat-y 50% 0; + background-position: right top; + margin: 30px auto; + width: 750px; + } + +#banner { + background-image: url(../images/scons-banner.jpg); + border-bottom: 1px solid; + height: 95px; + } + +#menu { + font-family: sans-serif; + font-size: small; + line-height: 0.9em; + float: right; + width: 220px; + clear: both; + margin-top: 10px; + } + +#menu li { + margin-bottom: 7px; + } + +#menu li li { + margin-bottom: 2px; + } + +#menu li.submenuitems { + margin-bottom: 2px; + } + +#menu a { + text-decoration: none; + } + +#footer { + border-top: 1px solid black; + text-align: center; + font-size: small; + color: #822; + margin-top: 4px; + background: #eee; + } + +ul.hack { + list-style-position:inside; + } + +ul.menuitems { + list-style-type: none; + } + +ul.submenuitems { + list-style-type: none; + font-size: smaller; + margin-left: 0; + padding-left: 16px; + } + +ul.subsubmenuitems { + list-style-type: none; + font-size: smaller; + margin-left: 0; + padding-left: 16px; + } + +ol.upper-roman { + list-style-type: upper-roman; + } + +ol.decimal { + list-style-type: decimal; + } + +#currentpage { + font-weight: bold; + } + +#bodycontent { + margin: 15px; + width: 520px; + font-size: small; + line-height: 1.5em; + } + +#bodycontent li { + margin-bottom: 6px; + list-style-type: square; + } + +#sconsdownloadtable downloadtable { + display: table; + margin-left: 5%; + border-spacing: 12px 3px; + } + +#sconsdownloadtable downloadrow { + display: table-row; + } + +#sconsdownloadtable downloadentry { + display: table-cell; + text-align: center; + vertical-align: bottom; + } + +#sconsdownloadtable downloaddescription { + display: table-cell; + font-weight: bold; + text-align: left; + } + +#sconsdownloadtable downloadversion { + display: table-cell; + font-weight: bold; + text-align: center; + } + +#sconsdocversiontable sconsversiontable { + display: table; + margin-left: 10%; + border-spacing: 12px 3px; + } + +#sconsdocversiontable sconsversionrow { + display: table-row; + } + +#sconsdocversiontable docformat { + display: table-cell; + font-weight: bold; + text-align: center; + vertical-align: bottom; + } + +#sconsdocversiontable sconsversion { + display: table-cell; + font-weight: bold; + text-align: left; + } + +#sconsdocversiontable docversion { + display: table-cell; + font-weight: bold; + text-align: center; + } + +#osrating { + margin-left: 35px; + } + + +h2 { + color: #272; + color: #c01714; + font-family: sans-serif; + font-weight: normal; + } + +h2.pagetitle { + font-size: xx-large; + } +h3 { + margin-bottom: 10px; + } + +.date { + font-size: small; + color: gray; + } + +.link { + margin-bottom: 22px; + } + +.linkname { + } + +.linkdesc { + margin: 10px; + margin-top: 0; + } + +.quote { + margin-top: 20px; + margin-bottom: 10px; + background: #f8f8f8; + border: 1px solid; + border-color: #ddd; + } + +.quotetitle { + font-weight: bold; + font-size: large; + margin: 10px; + } + +.quotedesc { + margin-left: 20px; + margin-right: 10px; + margin-bottom: 15px; + } + +.quotetext { + margin-top: 20px; + margin-left: 20px; + margin-right: 10px; + font-style: italic; + } + +.quoteauthor { + font-size: small; + text-align: right; + margin-top: 10px; + margin-right: 7px; + } + +.sconslogo { + font-style: normal; + font-weight: bold; + color: #822; + } + +.downloadlink { + } + +.downloaddescription { + margin-left: 1em; + margin-bottom: 0.4em; + } diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-after b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-after new file mode 100644 index 0000000..9a0eb95 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-after @@ -0,0 +1,6 @@ + +Import("qtEnv") +qtEnv.Append(CPPPATH=['./local_include']) +qtEnv.EnableQt5Modules(['QtCore']) +qtEnv.Program(target = 'aaa', source = 'aaa.cpp') + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-before b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-before new file mode 100644 index 0000000..d5677bb --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/SConscript-before @@ -0,0 +1,5 @@ + +Import("qtEnv") +qtEnv.EnableQt5Modules(['QtCore']) +qtEnv.Program(target = 'aaa', source = 'aaa.cpp') + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConscript b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConscript new file mode 100644 index 0000000..f392a40 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConscript @@ -0,0 +1,2 @@ + +SConscript('sub/SConscript') diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConstruct b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.cpp b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.cpp new file mode 100644 index 0000000..36dc229 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.cpp @@ -0,0 +1,13 @@ +#include "aaa.h" + +aaa::aaa() +{ + ; +} + +int main() +{ + aaa a; + return 0; +} + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.h b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.h new file mode 100644 index 0000000..f397b48 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/aaa.h @@ -0,0 +1,12 @@ +#include +#include "local_include.h" + +class aaa : public QObject +{ + Q_OBJECT +public: + + aaa(); +}; + + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/local_include/local_include.h b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/local_include/local_include.h new file mode 100644 index 0000000..b427f9a --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/image/sub/local_include/local_include.h @@ -0,0 +1,3 @@ + +/* empty; just needs to be found */ + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended-fail.py b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended-fail.py new file mode 100644 index 0000000..8c236b5 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended-fail.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Test that an appended relative CPPPATH works with generated files. + +This is basically the same as CPPPATH.py, but the include path +is env.Append-ed and everything goes into sub directory "sub". +The SConscript does not add the necessary path, such that +the compile run actually fails. Together with the second +test, this demonstrates that the CPPPATH has an effect. + +""" + +import os.path + +import TestSCons + +test = TestSCons.TestSCons() + +test.dir_fixture('image') +test.file_fixture('SConscript-before','sub/SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(status=2, stderr=None) + +test.pass_test() + + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended.py b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended.py new file mode 100644 index 0000000..3d3dd7c --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH-appended/sconstest-CPPPATH-appended.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Test that an appended relative CPPPATH works with generated files. + +This is basically the same as CPPPATH.py, but the include path +is env.Append-ed and everything goes into sub directory "sub". +In the SConscript we really add the necessary path, such that +the compile run is successful. See also the accompanying test +that is supposed to fail. + +""" + +import TestSCons + +test = TestSCons.TestSCons() + +test.dir_fixture('image') +test.file_fixture('SConscript-after','sub/SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(stderr=None) + +test.pass_test() + + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-after b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-after new file mode 100644 index 0000000..43b0b91 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-after @@ -0,0 +1,3 @@ +Import("qtEnv") +qtEnv.EnableQt5Modules(['QtCore']) +qtEnv.Program(target = 'aaa', source = 'aaa.cpp', CPPPATH=['$CPPPATH', './local_include']) diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-before b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-before new file mode 100644 index 0000000..95ff8a3 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/SConscript-before @@ -0,0 +1,3 @@ +Import("qtEnv") +qtEnv.EnableQt5Modules(['QtCore']) +qtEnv.Program(target = 'aaa', source = 'aaa.cpp') diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/SConstruct b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.cpp b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.cpp new file mode 100644 index 0000000..36dc229 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.cpp @@ -0,0 +1,13 @@ +#include "aaa.h" + +aaa::aaa() +{ + ; +} + +int main() +{ + aaa a; + return 0; +} + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.h b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.h new file mode 100644 index 0000000..f397b48 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/aaa.h @@ -0,0 +1,12 @@ +#include +#include "local_include.h" + +class aaa : public QObject +{ + Q_OBJECT +public: + + aaa(); +}; + + diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/local_include/local_include.h b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/local_include/local_include.h new file mode 100644 index 0000000..ad85bb2 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/image/local_include/local_include.h @@ -0,0 +1,2 @@ + +/* empty; just needs to be found */ diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH-fail.py b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH-fail.py new file mode 100644 index 0000000..ad0ca4a --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH-fail.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Test that CPPPATH works with generated files. + +The SConscript does not add the necessary path, such that +the compile run actually fails. Together with the second +test, this demonstrates that the CPPPATH has an effect. + +""" + +import os.path + +import TestSCons + +test = TestSCons.TestSCons() + +test.dir_fixture('image') +test.file_fixture('SConscript-before','SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(status=2, stderr=None) + +test.pass_test() + + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH.py b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH.py new file mode 100644 index 0000000..293933f --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/CPPPATH/CPPPATH/sconstest-CPPPATH.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Test that CPPPATH works with generated files. + +In the SConscript we really add the necessary path, such that +the compile run is successful. See also the accompanying test +that is supposed to fail. +""" + +import os.path + +import TestSCons + +test = TestSCons.TestSCons() + +test.dir_fixture('image') +test.file_fixture('SConscript-after','SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(stderr=None) + +test.pass_test() + + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.cpp b/workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.cpp new file mode 100644 index 0000000..b5f3f8f --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.cpp @@ -0,0 +1,5 @@ + +#include "MyFile.h" +void useit() { + aaa(); +} diff --git a/workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.h b/workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.h new file mode 100644 index 0000000..8b2a3f8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/copied-env/image/MyFile.h @@ -0,0 +1,2 @@ + +void aaa(void) {}; diff --git a/workspace/sconstools3/qt5/test/basic/copied-env/image/SConscript b/workspace/sconstools3/qt5/test/basic/copied-env/image/SConscript new file mode 100644 index 0000000..fc7668b --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/copied-env/image/SConscript @@ -0,0 +1,11 @@ +Import("qtEnv") +qtEnv.Append(CPPDEFINES = ['FOOBAZ']) + +copy = qtEnv.Clone() +copy.Append(CPPDEFINES = ['MYLIB_IMPL']) +copy.EnableQt5Modules(['QtCore']) + +copy.SharedLibrary( + target = 'MyLib', + source = ['MyFile.cpp'] +) diff --git a/workspace/sconstools3/qt5/test/basic/copied-env/image/SConstruct b/workspace/sconstools3/qt5/test/basic/copied-env/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/copied-env/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/basic/copied-env/sconstest-copied-env.py b/workspace/sconstools3/qt5/test/basic/copied-env/sconstest-copied-env.py new file mode 100644 index 0000000..54ebcf8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/copied-env/sconstest-copied-env.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Test Qt with a copied construction environment. +""" +from __future__ import print_function +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +cpp_MyFile = [x for x in test.stdout().split('\n') if x.find('MyFile.cpp') != -1] + +for x in cpp_MyFile: + if ((x.find('MYLIB_IMPL') < 0) or (x.find('FOOBAZ') < 0)): + print("Did not find MYLIB_IMPL and FOOBAZ on MyFile.cpp compilation line:") + print(test.stdout()) + test.fail_test() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/empty-env/image/SConstruct b/workspace/sconstools3/qt5/test/basic/empty-env/image/SConstruct new file mode 100644 index 0000000..f7c3e64 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/empty-env/image/SConstruct @@ -0,0 +1,4 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +qtEnv.Program('main', 'main.cpp', CPPDEFINES=['FOO'], LIBS=[]) diff --git a/workspace/sconstools3/qt5/test/basic/empty-env/image/foo6.h b/workspace/sconstools3/qt5/test/basic/empty-env/image/foo6.h new file mode 100644 index 0000000..734d7c7 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/empty-env/image/foo6.h @@ -0,0 +1,8 @@ +#include +void +foo6(void) +{ +#ifdef FOO + printf("qt/include/foo6.h\n"); +#endif +} diff --git a/workspace/sconstools3/qt5/test/basic/empty-env/image/main.cpp b/workspace/sconstools3/qt5/test/basic/empty-env/image/main.cpp new file mode 100644 index 0000000..cedf3cb --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/empty-env/image/main.cpp @@ -0,0 +1,3 @@ + +#include "foo6.h" +int main() { foo6(); return 0; } diff --git a/workspace/sconstools3/qt5/test/basic/empty-env/sconstest-empty-env.py b/workspace/sconstools3/qt5/test/basic/empty-env/sconstest-empty-env.py new file mode 100644 index 0000000..bc894b3 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/empty-env/sconstest-empty-env.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Test Qt creation from a copied empty environment. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.run(program = test.workpath('main' + TestSCons._exe), + stderr = None, + stdout = 'qt/include/foo6.h\n') + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/SConscript b/workspace/sconstools3/qt5/test/basic/manual/image/SConscript new file mode 100644 index 0000000..085947e --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/SConscript @@ -0,0 +1,20 @@ +Import("qtEnv") +sources = ['aaa.cpp', 'bbb.cpp', 'ddd.cpp', 'eee.cpp', 'main.cpp'] + +qtEnv.EnableQt5Modules(['QtCore','QtGui']) + +# normal invocation +sources.append(qtEnv.Moc5('include/aaa.h')) +sources.append(qtEnv.Moc5('ui/ccc.h')) +qtEnv.Moc5('bbb.cpp') +qtEnv.Uic5('ui/ccc.ui') + +# manual target specification +sources.append(qtEnv.ExplicitMoc5('moc-ddd.cpp','include/ddd.h')) +qtEnv.ExplicitMoc5('moc_eee.cpp','eee.cpp') +qtEnv.ExplicitUic5('include/uic_fff.hpp','ui/fff.ui') + +qtEnv.Program(target='aaa', + source=sources, + CPPPATH=['$CPPPATH', './include'], + QT5_AUTOSCAN=0) diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/SConstruct b/workspace/sconstools3/qt5/test/basic/manual/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/aaa.cpp b/workspace/sconstools3/qt5/test/basic/manual/image/aaa.cpp new file mode 100644 index 0000000..cbd37c1 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/aaa.cpp @@ -0,0 +1,2 @@ + +#include "aaa.h" diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/bbb.cpp b/workspace/sconstools3/qt5/test/basic/manual/image/bbb.cpp new file mode 100644 index 0000000..159cc07 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/bbb.cpp @@ -0,0 +1,18 @@ +#include "bbb.h" + +#include + +class bbb : public QObject +{ + Q_OBJECT + +public: + bbb() {}; +}; + +#include "bbb.moc" + +void b_dummy() +{ + bbb b; +} diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/bbb.h b/workspace/sconstools3/qt5/test/basic/manual/image/bbb.h new file mode 100644 index 0000000..c3ec38e --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/bbb.h @@ -0,0 +1,2 @@ + +extern void b_dummy(void); diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/ddd.cpp b/workspace/sconstools3/qt5/test/basic/manual/image/ddd.cpp new file mode 100644 index 0000000..cd59f26 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/ddd.cpp @@ -0,0 +1,2 @@ + +#include "ddd.h" diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/eee.cpp b/workspace/sconstools3/qt5/test/basic/manual/image/eee.cpp new file mode 100644 index 0000000..38cb3e3 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/eee.cpp @@ -0,0 +1,16 @@ +#include + +class eee : public QObject +{ + Q_OBJECT + +public: + eee() {}; +}; + +#include "moc_eee.cpp" + +void e_dummy() +{ + eee e; +} diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/eee.h b/workspace/sconstools3/qt5/test/basic/manual/image/eee.h new file mode 100644 index 0000000..cb2fe43 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/eee.h @@ -0,0 +1,2 @@ + +extern void e_dummy(void); diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/fff.cpp b/workspace/sconstools3/qt5/test/basic/manual/image/fff.cpp new file mode 100644 index 0000000..9e5f80d --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/fff.cpp @@ -0,0 +1 @@ +#include "uic_fff.hpp" diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/main.cpp b/workspace/sconstools3/qt5/test/basic/manual/image/main.cpp new file mode 100644 index 0000000..74af94a --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/main.cpp @@ -0,0 +1,17 @@ + +#include "aaa.h" +#include "bbb.h" +#include "ui/ccc.h" +#include "ddd.h" +#include "eee.h" +#include "uic_fff.hpp" + +int main() { + aaa a; + b_dummy(); + ccc c; + ddd d; + e_dummy(); + + return 0; +} diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.h b/workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.h new file mode 100644 index 0000000..073dd1a --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.h @@ -0,0 +1,9 @@ +#include + +class ccc : public QObject +{ + Q_OBJECT + +public: + ccc() {}; +}; diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.ui b/workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.ui new file mode 100644 index 0000000..87fffd4 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/ui/ccc.ui @@ -0,0 +1,19 @@ + + + Form + + + + 0 + 0 + 288 + 108 + + + + Form + + + + + diff --git a/workspace/sconstools3/qt5/test/basic/manual/image/ui/fff.ui b/workspace/sconstools3/qt5/test/basic/manual/image/ui/fff.ui new file mode 100644 index 0000000..180d91f --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/image/ui/fff.ui @@ -0,0 +1,19 @@ + + + Form2 + + + + 0 + 0 + 288 + 108 + + + + Form2 + + + + + diff --git a/workspace/sconstools3/qt5/test/basic/manual/sconstest-manual.py b/workspace/sconstools3/qt5/test/basic/manual/sconstest-manual.py new file mode 100644 index 0000000..8b30365 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/manual/sconstest-manual.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Test the manual QT builder calls. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() + +# normal invocation +test.must_exist(test.workpath('include', 'moc_aaa.cc')) +test.must_exist(test.workpath('bbb.moc')) +test.must_exist(test.workpath('ui', 'ccc.h')) +test.must_exist(test.workpath('ui', 'moc_ccc.cc')) + +# manual target spec. +test.must_exist(test.workpath('moc-ddd.cpp')) +test.must_exist(test.workpath('moc_eee.cpp')) +test.must_exist(test.workpath('include', 'uic_fff.hpp')) +test.must_exist(test.workpath('fff.cpp')) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConscript b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConscript new file mode 100644 index 0000000..571778e --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConscript @@ -0,0 +1,7 @@ +Import("qtEnv dup") + +qtEnv.EnableQt5Modules(['QtCore','QtGui']) + +if dup == 0: qtEnv.Append(CPPPATH=['.']) +aaa_lib = qtEnv.StaticLibrary('aaa',['aaa.cpp','useit.cpp']) +qtEnv.Alias('aaa_lib', aaa_lib) diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConstruct b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConstruct new file mode 100644 index 0000000..acb6a99 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/SConstruct @@ -0,0 +1,25 @@ +from __future__ import print_function +import qtenv + +qtEnv = qtenv.createQtEnvironment() + +dup = 1 +if ARGUMENTS.get('variant_dir', 0): + if ARGUMENTS.get('chdir', 0): + SConscriptChdir(1) + else: + SConscriptChdir(0) + dup=int(ARGUMENTS.get('dup', 1)) + if dup == 0: + builddir = 'build_dup0' + qtEnv['QT5_DEBUG'] = 1 + else: + builddir = 'build' + VariantDir(builddir, '.', duplicate=dup) + print(builddir, dup) + sconscript = Dir(builddir).File('SConscript') +else: + sconscript = File('SConscript') + +Export("qtEnv dup") +SConscript( sconscript ) diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.cpp b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.cpp new file mode 100644 index 0000000..07a28ef --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.cpp @@ -0,0 +1,18 @@ +#include "aaa.h" + +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa() {}; +}; + +#include "aaa.moc" + +void dummy_a() +{ + aaa a; +} diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.h b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.h new file mode 100644 index 0000000..8626f9f --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/aaa.h @@ -0,0 +1,2 @@ + +extern void dummy_a(void); diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/useit.cpp b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/useit.cpp new file mode 100644 index 0000000..0942ec3 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/image/useit.cpp @@ -0,0 +1,4 @@ +#include "aaa.h" +void useit() { + dummy_a(); +} diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-cpp/sconstest-moc-from-cpp.py b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/sconstest-moc-from-cpp.py new file mode 100644 index 0000000..feee87c --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-cpp/sconstest-moc-from-cpp.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Create a moc file from a cpp file. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +lib_aaa = 'aaa_lib' # Alias for the Library +moc = 'aaa.moc' + +test.run(arguments=lib_aaa, + stderr=TestSCons.noisy_ar, + match=TestSCons.match_re_dotall) + +test.up_to_date(options = '-n', arguments = lib_aaa) + +test.write('aaa.cpp', r""" +#include "aaa.h" + +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa() {}; +}; + +#include "%s" + +// Using the class +void dummy_a() +{ + aaa a; +} +""" % moc) + +test.not_up_to_date(options = '-n', arguments = moc) + +test.run(options = '-c', arguments = lib_aaa) + +test.run(arguments = "variant_dir=1 " + lib_aaa, + stderr=TestSCons.noisy_ar, + match=TestSCons.match_re_dotall) + +test.run(arguments = "variant_dir=1 chdir=1 " + lib_aaa) + +test.must_exist(test.workpath('build', moc)) + +test.run(arguments = "variant_dir=1 dup=0 " + lib_aaa, + stderr=TestSCons.noisy_ar, + match=TestSCons.match_re_dotall) + +test.must_exist(test.workpath('build_dup0', moc)) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConscript b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConscript new file mode 100644 index 0000000..4307a75 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConscript @@ -0,0 +1,8 @@ +Import("qtEnv dup") + +qtEnv.EnableQt5Modules(['QtCore','QtGui']) + +if dup == 0: qtEnv.Append(CPPPATH=['.', '#build_dup0']) +qtEnv.Program('aaa', 'aaa.cpp', + QT5_MOCHPREFIX = 'moc_', + QT5_MOCHSUFFIX = '.cc') diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConstruct b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConstruct new file mode 100644 index 0000000..acb6a99 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/SConstruct @@ -0,0 +1,25 @@ +from __future__ import print_function +import qtenv + +qtEnv = qtenv.createQtEnvironment() + +dup = 1 +if ARGUMENTS.get('variant_dir', 0): + if ARGUMENTS.get('chdir', 0): + SConscriptChdir(1) + else: + SConscriptChdir(0) + dup=int(ARGUMENTS.get('dup', 1)) + if dup == 0: + builddir = 'build_dup0' + qtEnv['QT5_DEBUG'] = 1 + else: + builddir = 'build' + VariantDir(builddir, '.', duplicate=dup) + print(builddir, dup) + sconscript = Dir(builddir).File('SConscript') +else: + sconscript = File('SConscript') + +Export("qtEnv dup") +SConscript( sconscript ) diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.cpp b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.cpp new file mode 100644 index 0000000..d8b45aa --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.cpp @@ -0,0 +1,7 @@ +#include "moc_aaa.cc" + +int main() +{ + aaa a; + return 0; +} diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.h b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.h new file mode 100644 index 0000000..3438edd --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/image/aaa.h @@ -0,0 +1,9 @@ +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa() {}; +}; diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/sconstest-moc-from-header-nocompile.py b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/sconstest-moc-from-header-nocompile.py new file mode 100644 index 0000000..3e905a7 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header-nocompile/sconstest-moc-from-header-nocompile.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Create a moc file from a header file, but don't +compile it to an Object because the moc'ed output gets directly +included to the CXX file. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +aaa_exe = 'aaa' + TestSCons._exe +build_aaa_exe = test.workpath('build', aaa_exe) +moc = 'moc_aaa.cc' +moc_obj = 'moc_aaa' + TestSCons._obj + +test.run() + +# Ensure that the object file for the MOC output wasn't generated +test.must_not_exist(moc_obj) + +test.up_to_date(options = '-n', arguments=aaa_exe) +test.up_to_date(options = '-n', arguments=aaa_exe) + +test.write('aaa.h', r""" +#include + +// Introducing a change... +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa() {}; +}; +""") + +test.not_up_to_date(options='-n', arguments = moc) + +test.run(arguments = "variant_dir=1 " + build_aaa_exe) + +test.run(arguments = "variant_dir=1 chdir=1 " + build_aaa_exe) + +test.must_exist(test.workpath('build', moc)) +test.must_not_exist(test.workpath('build', moc_obj)) + +test.run(options='-c') + +test.run(arguments = "variant_dir=1 chdir=1 dup=0 " + + test.workpath('build_dup0', aaa_exe) ) + +test.must_exist(test.workpath('build_dup0', moc)) +test.must_not_exist(moc_obj) +test.must_not_exist(test.workpath('build_dup0', moc_obj)) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConscript b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConscript new file mode 100644 index 0000000..4486ee8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConscript @@ -0,0 +1,6 @@ +Import("qtEnv dup") + +qtEnv.EnableQt5Modules(['QtCore','QtGui']) + +if dup == 0: qtEnv.Append(CPPPATH=['.']) +qtEnv.Program('aaa','aaa.cpp') diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConstruct b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConstruct new file mode 100644 index 0000000..acb6a99 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/SConstruct @@ -0,0 +1,25 @@ +from __future__ import print_function +import qtenv + +qtEnv = qtenv.createQtEnvironment() + +dup = 1 +if ARGUMENTS.get('variant_dir', 0): + if ARGUMENTS.get('chdir', 0): + SConscriptChdir(1) + else: + SConscriptChdir(0) + dup=int(ARGUMENTS.get('dup', 1)) + if dup == 0: + builddir = 'build_dup0' + qtEnv['QT5_DEBUG'] = 1 + else: + builddir = 'build' + VariantDir(builddir, '.', duplicate=dup) + print(builddir, dup) + sconscript = Dir(builddir).File('SConscript') +else: + sconscript = File('SConscript') + +Export("qtEnv dup") +SConscript( sconscript ) diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.cpp b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.cpp new file mode 100644 index 0000000..054d582 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.cpp @@ -0,0 +1,7 @@ +#include "aaa.h" + +int main() +{ + aaa a; + return 0; +} diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.h b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.h new file mode 100644 index 0000000..3438edd --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header/image/aaa.h @@ -0,0 +1,9 @@ +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa() {}; +}; diff --git a/workspace/sconstools3/qt5/test/basic/moc-from-header/sconstest-moc-from-header.py b/workspace/sconstools3/qt5/test/basic/moc-from-header/sconstest-moc-from-header.py new file mode 100644 index 0000000..ab5a0de --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/moc-from-header/sconstest-moc-from-header.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Create a moc file from a header file. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +aaa_exe = 'aaa' + TestSCons._exe +build_aaa_exe = test.workpath('build', aaa_exe) +moc = 'moc_aaa.cc' + +test.run() + +test.up_to_date(options = '-n', arguments=aaa_exe) +test.up_to_date(options = '-n', arguments=aaa_exe) + +test.write('aaa.h', r""" +#include + +// Introducing a change... +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa() {}; +}; +""") + +test.not_up_to_date(options='-n', arguments = moc) + +test.run(arguments = "variant_dir=1 " + build_aaa_exe) + +test.run(arguments = "variant_dir=1 chdir=1 " + build_aaa_exe) + +test.must_exist(test.workpath('build', moc)) + +test.run(arguments = "variant_dir=1 chdir=1 dup=0 " + + test.workpath('build_dup0', aaa_exe) ) + +test.must_exist(['build_dup0', moc]) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/reentrant/image/SConscript b/workspace/sconstools3/qt5/test/basic/reentrant/image/SConscript new file mode 100644 index 0000000..163e252 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/reentrant/image/SConscript @@ -0,0 +1,4 @@ +Import("qtEnv") +env = qtEnv.Clone(tool='qt5') + +env.Program('main', 'main.cpp', CPPDEFINES=['FOO'], LIBS=[]) diff --git a/workspace/sconstools3/qt5/test/basic/reentrant/image/SConstruct b/workspace/sconstools3/qt5/test/basic/reentrant/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/reentrant/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/basic/reentrant/image/foo5.h b/workspace/sconstools3/qt5/test/basic/reentrant/image/foo5.h new file mode 100644 index 0000000..5ac79c8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/reentrant/image/foo5.h @@ -0,0 +1,8 @@ +#include + +#ifdef FOO +void foo5(void) +{ + printf("qt/include/foo5.h\\n"); +} +#endif diff --git a/workspace/sconstools3/qt5/test/basic/reentrant/image/main.cpp b/workspace/sconstools3/qt5/test/basic/reentrant/image/main.cpp new file mode 100644 index 0000000..0ea9300 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/reentrant/image/main.cpp @@ -0,0 +1,7 @@ +#include "foo5.h" + +int main() +{ + foo5(); + return 0; +} diff --git a/workspace/sconstools3/qt5/test/basic/reentrant/sconstest-reentrant.py b/workspace/sconstools3/qt5/test/basic/reentrant/sconstest-reentrant.py new file mode 100644 index 0000000..97487f5 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/reentrant/sconstest-reentrant.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Test creation from a copied environment that already has QT variables. +This makes sure the tool initialization is re-entrant. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.cpp b/workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.cpp new file mode 100644 index 0000000..cdeb4a6 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.cpp @@ -0,0 +1,14 @@ +#include "MyForm.h" + +#include + +MyForm::MyForm(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); +} + +void MyForm::testSlot() +{ + printf("Hello World\n"); +} + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.h b/workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.h new file mode 100644 index 0000000..fa2d119 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/MyForm.h @@ -0,0 +1,18 @@ +#include "ui_anUiFile.h" + +#include + +class MyForm : public QWidget +{ + Q_OBJECT + + public: + MyForm(QWidget *parent = 0); + + public slots: + void testSlot(); + + private: + Ui::Form ui; +}; + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/SConstruct b/workspace/sconstools3/qt5/test/basic/variantdir/image/SConstruct new file mode 100644 index 0000000..56d7a18 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/SConstruct @@ -0,0 +1,12 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +qtEnv.EnableQt5Modules(['QtCore','QtWidgets']) + +qtEnv.VariantDir('bld', '.') +qtEnv.Uic5('bld/anUiFile.ui') +qtEnv.Program('bld/test_realqt', ['bld/mocFromCpp.cpp', + 'bld/mocFromH.cpp', + 'bld/MyForm.cpp', + 'bld/main.cpp']) + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/anUiFile.ui b/workspace/sconstools3/qt5/test/basic/variantdir/image/anUiFile.ui new file mode 100644 index 0000000..87fffd4 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/anUiFile.ui @@ -0,0 +1,19 @@ + + + Form + + + + 0 + 0 + 288 + 108 + + + + Form + + + + + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/main.cpp b/workspace/sconstools3/qt5/test/basic/variantdir/image/main.cpp new file mode 100644 index 0000000..5212903 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/main.cpp @@ -0,0 +1,17 @@ +#include "mocFromCpp.h" +#include "mocFromH.h" +#include "MyForm.h" + +#include +#include + +int main(int argc, char **argv) { + QApplication app(argc, argv); + mocFromCpp(); + mocFromH(); + MyForm mywidget; + mywidget.testSlot(); + printf("Hello World\n"); + return 0; +} + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.cpp b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.cpp new file mode 100644 index 0000000..1d5b560 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.cpp @@ -0,0 +1,16 @@ +#include + +#include "mocFromCpp.h" + +class MyClass1 : public QObject { + Q_OBJECT + public: + MyClass1() : QObject() {}; + public slots: + void myslot() {}; +}; +void mocFromCpp() { + MyClass1 myclass; +} +#include "mocFromCpp.moc" + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.h b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.h new file mode 100644 index 0000000..fc1e04c --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromCpp.h @@ -0,0 +1,2 @@ +void mocFromCpp(); + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.cpp b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.cpp new file mode 100644 index 0000000..586aafb --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.cpp @@ -0,0 +1,8 @@ +#include "mocFromH.h" + +MyClass2::MyClass2() : QObject() {} +void MyClass2::myslot() {} +void mocFromH() { + MyClass2 myclass; +} + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.h b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.h new file mode 100644 index 0000000..48fdd7e --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/image/mocFromH.h @@ -0,0 +1,11 @@ +#include + +class MyClass2 : public QObject { + Q_OBJECT; + public: + MyClass2(); + public slots: + void myslot(); +}; +void mocFromH(); + diff --git a/workspace/sconstools3/qt5/test/basic/variantdir/sconstest-installed.py b/workspace/sconstools3/qt5/test/basic/variantdir/sconstest-installed.py new file mode 100644 index 0000000..7cef5c4 --- /dev/null +++ b/workspace/sconstools3/qt5/test/basic/variantdir/sconstest-installed.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +A simple test for VariantDir, in connection with basic +automocing from cxx and header files and the Uic5 builder. +""" + +import sys + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run(arguments="bld/test_realqt" + TestSCons._exe) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConscript b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConscript new file mode 100644 index 0000000..d560985 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConscript @@ -0,0 +1,9 @@ +Import("qtEnv") + +env = qtEnv.Clone() +env['QT5_GOBBLECOMMENTS']=1 +env['QT5_DEBUG']=1 +env.EnableQt5Modules(['QtCore','QtWidgets']) + +env.Program('main', Glob('*.cpp')) + diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConstruct b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConstruct new file mode 100644 index 0000000..9754f10 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') + +SConscript('SConscript') diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/main.cpp b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/main.cpp new file mode 100644 index 0000000..6d8b713 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/main.cpp @@ -0,0 +1,14 @@ +#include "mocFromCpp.h" +#include "mocFromH.h" + +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + mocFromCpp(); + mocFromH(); + printf("Hello World\n"); + return 0; +} diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.cpp b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.cpp new file mode 100644 index 0000000..b4a24ec --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.cpp @@ -0,0 +1,41 @@ +#include + +#include "mocFromCpp.h" + + +/* + +class MyClass1 : public QObject { + Q_OBJECT + public: + MyClass1() : QObject() {}; + public slots: + void myslot() {}; +}; +void mocFromCpp() { + MyClass1 myclass; +} +#include "mocFromCpp.moc" + +*/ + +// class MyClass1 : public QObject { Q_OBJECT; }; + +class MyClass1 +{ + // Q_OBJECT + + /* and another Q_OBJECT but in + * a C comment, + * ... next Q_OBJECT + */ + +public: + MyClass1() {}; + void myslot() {}; +}; + +void mocFromCpp() +{ + MyClass1 myclass; +} diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.h b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.h new file mode 100644 index 0000000..fc1e04c --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromCpp.h @@ -0,0 +1,2 @@ +void mocFromCpp(); + diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.cpp b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.cpp new file mode 100644 index 0000000..7cfb2be --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.cpp @@ -0,0 +1,10 @@ +#include "mocFromH.h" + +MyClass2::MyClass2(){} + +void MyClass2::myslot() {} + +void mocFromH() { + MyClass2 myclass; +} + diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.h b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.h new file mode 100644 index 0000000..6a96ca2 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/image/mocFromH.h @@ -0,0 +1,32 @@ +#include + +/* +class MyClass2 : public QObject { + // Here we define a Q_OBJECT + Q_OBJECT; + public: + MyClass2(); + public slots: + void myslot(); +}; +void mocFromH(); + +*/ + +// class MyClass2 : public QObject { Q_OBJECT; }; + +class MyClass2 +{ + // Q_OBJECT + + /* and another Q_OBJECT but in + * a C comment, + * ... next Q_OBJECT + */ + +public: + MyClass2(); + void myslot(); +}; + +void mocFromH(); diff --git a/workspace/sconstools3/qt5/test/moc/auto/ccomment/sconstest-ccomment.py b/workspace/sconstools3/qt5/test/moc/auto/ccomment/sconstest-ccomment.py new file mode 100644 index 0000000..842909c --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/ccomment/sconstest-ccomment.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +A simple test for a few C/C++ comments, containing the +string Q_OBJECT. With the QT5_GOBBLECOMMENTS variable set, +no automocing should get triggered. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConscript b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConscript new file mode 100644 index 0000000..1366183 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConscript @@ -0,0 +1,6 @@ +Import("qtEnv") + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +env.Program('main', Glob('*.cpp')) diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConstruct b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConstruct new file mode 100644 index 0000000..9754f10 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') + +SConscript('SConscript') diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/main.cpp b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/main.cpp new file mode 100644 index 0000000..6d8b713 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/main.cpp @@ -0,0 +1,14 @@ +#include "mocFromCpp.h" +#include "mocFromH.h" + +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + mocFromCpp(); + mocFromH(); + printf("Hello World\n"); + return 0; +} diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.cpp b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.cpp new file mode 100644 index 0000000..407cc41 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.cpp @@ -0,0 +1,27 @@ +#include "mocFromCpp.h" + +#include +#include + +class MyClass1 +{ +public: + MyClass1() {}; + void myslot() {}; + void printme() + { + printf("It's a Q_OBJECT!"); + printf("Concatenating " + "a Q_OBJECT" + " with another " + "Q_OBJECT" + "and a " + "Q_OBJECT, finally"); + }; + +}; + +void mocFromCpp() +{ + MyClass1 myclass; +} diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.h b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.h new file mode 100644 index 0000000..fc1e04c --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromCpp.h @@ -0,0 +1,2 @@ +void mocFromCpp(); + diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.cpp b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.cpp new file mode 100644 index 0000000..7cfb2be --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.cpp @@ -0,0 +1,10 @@ +#include "mocFromH.h" + +MyClass2::MyClass2(){} + +void MyClass2::myslot() {} + +void mocFromH() { + MyClass2 myclass; +} + diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.h b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.h new file mode 100644 index 0000000..6868c89 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/image/mocFromH.h @@ -0,0 +1,21 @@ +#include +#include + +class MyClass2 +{ +public: + MyClass2(); + void myslot(); + void printme() + { + printf("It's a Q_OBJECT!"); + printf("Concatenating " + "a Q_OBJECT" + " with another " + "Q_OBJECT" + "and a " + "Q_OBJECT, finally"); + }; +}; + +void mocFromH(); diff --git a/workspace/sconstools3/qt5/test/moc/auto/literalstring/sconstest-literalstring.py b/workspace/sconstools3/qt5/test/moc/auto/literalstring/sconstest-literalstring.py new file mode 100644 index 0000000..3d63113 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/auto/literalstring/sconstest-literalstring.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +A simple test, ensuring that the Q_OBJECT keyword in a +literal C/C++ string does not trigger automocing. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript b/workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript new file mode 100644 index 0000000..81a9851 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript @@ -0,0 +1,8 @@ +Import("qtEnv") + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) +env.Append(CPPPATH=['include']) + +env.Program('main', Glob('src/*.cpp')) + diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript-fails b/workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript-fails new file mode 100644 index 0000000..8e3fcc6 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/SConscript-fails @@ -0,0 +1,9 @@ +Import("qtEnv") + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) +env.Append(CPPPATH=['include']) +env['QT5_AUTOMOC_SCANCPPPATH']='0' + +env.Program('main', Glob('src/*.cpp')) + diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/image/SConstruct b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/SConstruct new file mode 100644 index 0000000..9754f10 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') + +SConscript('SConscript') diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/main.cpp b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/main.cpp new file mode 100644 index 0000000..6d8b713 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/main.cpp @@ -0,0 +1,14 @@ +#include "mocFromCpp.h" +#include "mocFromH.h" + +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + mocFromCpp(); + mocFromH(); + printf("Hello World\n"); + return 0; +} diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromCpp.cpp b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromCpp.cpp new file mode 100644 index 0000000..e18d1b8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromCpp.cpp @@ -0,0 +1,15 @@ +#include + +#include "mocFromCpp.h" + +class MyClass1 : public QObject { + Q_OBJECT + public: + MyClass1() : QObject() {}; + public slots: + void myslot() {}; +}; +void mocFromCpp() { + MyClass1 myclass; +} +#include "mocFromCpp.moc" diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromH.cpp b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromH.cpp new file mode 100644 index 0000000..7cfb2be --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/image/src/mocFromH.cpp @@ -0,0 +1,10 @@ +#include "mocFromH.h" + +MyClass2::MyClass2(){} + +void MyClass2::myslot() {} + +void mocFromH() { + MyClass2 myclass; +} + diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default-fails.py b/workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default-fails.py new file mode 100644 index 0000000..469c0b5 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default-fails.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Disabled the CPPPATH support of the Automoc feature: +uses the CPPPATH list of the current environment, but still fails. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('SConscript-fails','SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run(status=2, stderr=None) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default.py b/workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default.py new file mode 100644 index 0000000..586deb3 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/default/sconstest-default.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Default settings for the CPPPATH support of the Automoc feature: +it is enabled and uses the CPPPATH list of the current environment. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('SConscript','SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript b/workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript new file mode 100644 index 0000000..ea6157e --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript @@ -0,0 +1,9 @@ +Import("qtEnv") + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) +env.Append(CPPPATH=['include']) +env['QT5_AUTOMOC_CPPPATH']=['include'] + +env.Program('main', Glob('src/*.cpp')) + diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript-fails b/workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript-fails new file mode 100644 index 0000000..90689ee --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/SConscript-fails @@ -0,0 +1,9 @@ +Import("qtEnv") + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) +env.Append(CPPPATH=['include']) +env['QT5_AUTOMOC_CPPPATH']=['wrongdir'] + +env.Program('main', Glob('src/*.cpp')) + diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/SConstruct b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/SConstruct new file mode 100644 index 0000000..9754f10 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') + +SConscript('SConscript') diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/main.cpp b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/main.cpp new file mode 100644 index 0000000..6d8b713 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/main.cpp @@ -0,0 +1,14 @@ +#include "mocFromCpp.h" +#include "mocFromH.h" + +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + mocFromCpp(); + mocFromH(); + printf("Hello World\n"); + return 0; +} diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromCpp.cpp b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromCpp.cpp new file mode 100644 index 0000000..e18d1b8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromCpp.cpp @@ -0,0 +1,15 @@ +#include + +#include "mocFromCpp.h" + +class MyClass1 : public QObject { + Q_OBJECT + public: + MyClass1() : QObject() {}; + public slots: + void myslot() {}; +}; +void mocFromCpp() { + MyClass1 myclass; +} +#include "mocFromCpp.moc" diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromH.cpp b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromH.cpp new file mode 100644 index 0000000..7cfb2be --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/image/src/mocFromH.cpp @@ -0,0 +1,10 @@ +#include "mocFromH.h" + +MyClass2::MyClass2(){} + +void MyClass2::myslot() {} + +void mocFromH() { + MyClass2 myclass; +} + diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific-fails.py b/workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific-fails.py new file mode 100644 index 0000000..b301e80 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific-fails.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Explicitly sets the path list to scan for mocable files, +but to a wrong folder. +Although the correct CPPPATH is used, this lets the test +fail...and proves that the option QT5_AUTOMOC_CPPPATH +really has an effect. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('SConscript-fails','SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run(status=2, stderr=None) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific.py b/workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific.py new file mode 100644 index 0000000..a9ba087 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/cpppath/specific/sconstest-specific.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Explicitly sets the path list to scan for moc'able files. +The correct CPPPATH is used too, but gets only searched for +normal implicit header dependencies (as is proved by the +the accompanying test that fails). +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('SConscript','SConscript') +test.file_fixture('../../../qtenv.py') +test.file_fixture('../../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/explicit/image/SConscript b/workspace/sconstools3/qt5/test/moc/explicit/image/SConscript new file mode 100644 index 0000000..f224ec9 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/image/SConscript @@ -0,0 +1,10 @@ +Import("qtEnv") + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) +env['QT5_AUTOSCAN']=0 + +env.ExplicitMoc5('explicitly_moced_FromHeader.cpp','mocFromH.h') +env.ExplicitMoc5('explicitly_moced_FromCpp.strange_cpp_moc_prefix','mocFromCpp.cpp') + +env.Program('main', Glob('*.cpp')) diff --git a/workspace/sconstools3/qt5/test/moc/explicit/image/SConstruct b/workspace/sconstools3/qt5/test/moc/explicit/image/SConstruct new file mode 100644 index 0000000..9754f10 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') + +SConscript('SConscript') diff --git a/workspace/sconstools3/qt5/test/moc/explicit/image/main.cpp b/workspace/sconstools3/qt5/test/moc/explicit/image/main.cpp new file mode 100644 index 0000000..6d8b713 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/image/main.cpp @@ -0,0 +1,14 @@ +#include "mocFromCpp.h" +#include "mocFromH.h" + +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + mocFromCpp(); + mocFromH(); + printf("Hello World\n"); + return 0; +} diff --git a/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.cpp b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.cpp new file mode 100644 index 0000000..669d7a1 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.cpp @@ -0,0 +1,16 @@ +#include + +#include "mocFromCpp.h" + +class MyClass1 : public QObject { + Q_OBJECT + public: + MyClass1() : QObject() {}; + public slots: + void myslot() {}; +}; +void mocFromCpp() { + MyClass1 myclass; +} +#include "explicitly_moced_FromCpp.strange_cpp_moc_prefix" + diff --git a/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.h b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.h new file mode 100644 index 0000000..fc1e04c --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromCpp.h @@ -0,0 +1,2 @@ +void mocFromCpp(); + diff --git a/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.cpp b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.cpp new file mode 100644 index 0000000..586aafb --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.cpp @@ -0,0 +1,8 @@ +#include "mocFromH.h" + +MyClass2::MyClass2() : QObject() {} +void MyClass2::myslot() {} +void mocFromH() { + MyClass2 myclass; +} + diff --git a/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.h b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.h new file mode 100644 index 0000000..48fdd7e --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/image/mocFromH.h @@ -0,0 +1,11 @@ +#include + +class MyClass2 : public QObject { + Q_OBJECT; + public: + MyClass2(); + public slots: + void myslot(); +}; +void mocFromH(); + diff --git a/workspace/sconstools3/qt5/test/moc/explicit/sconstest-explicit.py b/workspace/sconstools3/qt5/test/moc/explicit/sconstest-explicit.py new file mode 100644 index 0000000..f5e78fe --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/explicit/sconstest-explicit.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Test of the explicit Moc5 builder, with arbitrary filenames +for the created files. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/moc/order_independent/image/SConscript b/workspace/sconstools3/qt5/test/moc/order_independent/image/SConscript new file mode 100644 index 0000000..a8b791c --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/order_independent/image/SConscript @@ -0,0 +1,12 @@ +Import("qtEnv dup reverted") + +qtEnv.EnableQt5Modules(['QtCore','QtGui']) + +if dup == 0: + qtEnv.Append(CPPPATH=['.']) + +if reverted == 0: + qtEnv.Program('aaa',['aaa.cpp','bbb.cpp']) +else: + qtEnv.Program('aaa',['bbb.cpp','aaa.cpp']) + diff --git a/workspace/sconstools3/qt5/test/moc/order_independent/image/SConstruct b/workspace/sconstools3/qt5/test/moc/order_independent/image/SConstruct new file mode 100644 index 0000000..5490ee2 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/order_independent/image/SConstruct @@ -0,0 +1,26 @@ +from __future__ import print_function +import qtenv + +qtEnv = qtenv.createQtEnvironment() + +reverted=int(ARGUMENTS.get('reverted', 0)) +dup = 1 +if ARGUMENTS.get('variant_dir', 0): + if ARGUMENTS.get('chdir', 0): + SConscriptChdir(1) + else: + SConscriptChdir(0) + dup=int(ARGUMENTS.get('dup', 1)) + if dup == 0: + builddir = 'build_dup0' + qtEnv['QT5_DEBUG'] = 1 + else: + builddir = 'build' + VariantDir(builddir, '.', duplicate=dup) + print(builddir, dup) + sconscript = Dir(builddir).File('SConscript') +else: + sconscript = File('SConscript') + +Export("qtEnv dup reverted") +SConscript( sconscript ) diff --git a/workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.cpp b/workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.cpp new file mode 100644 index 0000000..054d582 --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.cpp @@ -0,0 +1,7 @@ +#include "aaa.h" + +int main() +{ + aaa a; + return 0; +} diff --git a/workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.h b/workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.h new file mode 100644 index 0000000..3438edd --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/order_independent/image/aaa.h @@ -0,0 +1,9 @@ +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa() {}; +}; diff --git a/workspace/sconstools3/qt5/test/moc/order_independent/image/bbb.cpp b/workspace/sconstools3/qt5/test/moc/order_independent/image/bbb.cpp new file mode 100644 index 0000000..6fc20da --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/order_independent/image/bbb.cpp @@ -0,0 +1,4 @@ +int bbb() +{ + return 0; +} diff --git a/workspace/sconstools3/qt5/test/moc/order_independent/sconstest-order-independent.py b/workspace/sconstools3/qt5/test/moc/order_independent/sconstest-order-independent.py new file mode 100644 index 0000000..083bd7b --- /dev/null +++ b/workspace/sconstools3/qt5/test/moc/order_independent/sconstest-order-independent.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Ensures that no rebuilds get triggered when the order of the source +list changes in the Automoc routine(s). +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture('image') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') + +test.run() +test.up_to_date(options="-n", arguments=".") +test.up_to_date(options="-n reverted=1", arguments=".") +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/basic/SConscript b/workspace/sconstools3/qt5/test/qrc/basic/SConscript new file mode 100644 index 0000000..1bcb156 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/basic/SConscript @@ -0,0 +1,8 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp')+Glob('*.qrc') + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/basic/SConscript-wflags b/workspace/sconstools3/qt5/test/qrc/basic/SConscript-wflags new file mode 100644 index 0000000..bf987fc --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/basic/SConscript-wflags @@ -0,0 +1,9 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp')+Glob('*.qrc') +env['QT5_QRCFLAGS'] = ['-name', 'icons'] + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/basic/image/SConstruct b/workspace/sconstools3/qt5/test/qrc/basic/image/SConstruct new file mode 100644 index 0000000..00e5705 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/basic/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + \ No newline at end of file diff --git a/workspace/sconstools3/qt5/test/qrc/basic/image/icons.qrc b/workspace/sconstools3/qt5/test/qrc/basic/image/icons.qrc new file mode 100644 index 0000000..86fe71b --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/basic/image/icons.qrc @@ -0,0 +1,5 @@ + + + icons/scons.png + + diff --git a/workspace/sconstools3/qt5/test/qrc/basic/image/icons/scons.png b/workspace/sconstools3/qt5/test/qrc/basic/image/icons/scons.png new file mode 100644 index 0000000000000000000000000000000000000000..7d85f1f8c28e010a122c55baf3de8185609c95a4 GIT binary patch literal 1613 zcmV-T2D15yP)$y00004b3#c}2nYxW zd`K zafCm^j`0LMfo9-CV1%t0RMo2BrvP0sII2~{%XM165)FvBfCqd9jI$Ak*m|TLk%NPR zfpJxLmpi7#R&&*5V{M#Gb#b@RQ%7L8!g5vJ-Cc255iBATfjvvhBUDC8+TD+P9$(fq zu(2lQ*14Wz;IId80$WtoyQ#6*5Nt%Y_jAl6+eZv?dWqbyU9&3RUT$DrP0T%$avcLF zJXj8FP}Sy}5lgoV7LhT)Z}ZCfGAkPP<>LW6>l3WK6!VTJQ^vq~4^{ymsA{ZB@mLW2 zdC17qJ4XyMrN(sbkbz&~O?+Gv)5D3R7^wGP4e++AUd$_=?i4H{A%WP)-a%*OkSIff zfxP>wZfs_4O-%R38bu)K!AHOs7l)KMWeW+9JLI}P_E)oD{ z7+CDVJ!JvMeJ&byruHi#Wc&%=b|t~mGqnJZsp`&7ISVQHS`Z=vJYnE94<>{i<31B9 zb!PN0rLWWRQdW8LynCuSb@ZyIqkVA|c!Wz-wIRcHzrYfO$weaGv`C4W**{Eq&}rxY zQ@n|nPhSFKZA?jEn5v%X zZtmREmo7p?9AIjOdA$v>PyoTeM4&9g_L(j6LI|$R_`f`DXee^de-*_G2Lcmo9Rz^yw?3cM8xG^qXbz6&P zLJAi7^N5AcE1Ak~n)v~)_bC#Y@dSc2% z6FH7RwxM*j|Sh@PEP0SR7rjELc^K0h>t+^?-w_y5D9D z0Z4yN@M&$_x6>83OBI>Lq^p)GU}3(UY`vW5^q{RRsvBEA0t$*8Adf}dKm#x)&qDkn zd=}ob_>O7e&N=3w$N?ce%Kmp9*U_x1i9GzejFtoSUU|62d&K#$O$aa=m|&4}tE!#@ z5Rr0Vibd}I7^o-zu7{;lQLL^9IG6d7M! z#PY%UWO}(_eNCJVwXtvjpx6k}95Ro55zO+1PEhP{7q@&QQ;0%*L2+B~t&ZD*Z*>F! z4kePjf3d#X$^J=Q&1$p3eJGJk{}B|~h26z}S(}Z=GMn5%kzM}*w<<2%`cNe700000 LNkvXXu0mjfGb--F literal 0 HcmV?d00001 diff --git a/workspace/sconstools3/qt5/test/qrc/basic/image/main.cpp b/workspace/sconstools3/qt5/test/qrc/basic/image/main.cpp new file mode 100644 index 0000000..304dc3a --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/basic/image/main.cpp @@ -0,0 +1,11 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + Q_INIT_RESOURCE(icons); + + return 0; +} diff --git a/workspace/sconstools3/qt5/test/qrc/basic/sconstest-basic.py b/workspace/sconstools3/qt5/test/qrc/basic/sconstest-basic.py new file mode 100644 index 0000000..e88cc9e --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/basic/sconstest-basic.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Basic test for the Qrc() builder. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('SConscript') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/basic/sconstest-basicwflags.py b/workspace/sconstools3/qt5/test/qrc/basic/sconstest-basicwflags.py new file mode 100644 index 0000000..4d1b92d --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/basic/sconstest-basicwflags.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Basic test for the Qrc() builder, with '-name' flag set. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('SConscript-wflags','SConscript') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/manual/SConscript b/workspace/sconstools3/qt5/test/qrc/manual/SConscript new file mode 100644 index 0000000..8445a06 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/manual/SConscript @@ -0,0 +1,9 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp') +source_files.append(env.Qrc5('icons')) + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/manual/SConscript-wflags b/workspace/sconstools3/qt5/test/qrc/manual/SConscript-wflags new file mode 100644 index 0000000..961c00b --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/manual/SConscript-wflags @@ -0,0 +1,11 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp') +source_files.append(env.Qrc5('icons')) + +env['QT5_QRCFLAGS'] = ['-name', 'icons'] + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/manual/image/SConstruct b/workspace/sconstools3/qt5/test/qrc/manual/image/SConstruct new file mode 100644 index 0000000..00e5705 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/manual/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + \ No newline at end of file diff --git a/workspace/sconstools3/qt5/test/qrc/manual/image/icons.qrc b/workspace/sconstools3/qt5/test/qrc/manual/image/icons.qrc new file mode 100644 index 0000000..86fe71b --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/manual/image/icons.qrc @@ -0,0 +1,5 @@ + + + icons/scons.png + + diff --git a/workspace/sconstools3/qt5/test/qrc/manual/image/icons/scons.png b/workspace/sconstools3/qt5/test/qrc/manual/image/icons/scons.png new file mode 100644 index 0000000000000000000000000000000000000000..7d85f1f8c28e010a122c55baf3de8185609c95a4 GIT binary patch literal 1613 zcmV-T2D15yP)$y00004b3#c}2nYxW zd`K zafCm^j`0LMfo9-CV1%t0RMo2BrvP0sII2~{%XM165)FvBfCqd9jI$Ak*m|TLk%NPR zfpJxLmpi7#R&&*5V{M#Gb#b@RQ%7L8!g5vJ-Cc255iBATfjvvhBUDC8+TD+P9$(fq zu(2lQ*14Wz;IId80$WtoyQ#6*5Nt%Y_jAl6+eZv?dWqbyU9&3RUT$DrP0T%$avcLF zJXj8FP}Sy}5lgoV7LhT)Z}ZCfGAkPP<>LW6>l3WK6!VTJQ^vq~4^{ymsA{ZB@mLW2 zdC17qJ4XyMrN(sbkbz&~O?+Gv)5D3R7^wGP4e++AUd$_=?i4H{A%WP)-a%*OkSIff zfxP>wZfs_4O-%R38bu)K!AHOs7l)KMWeW+9JLI}P_E)oD{ z7+CDVJ!JvMeJ&byruHi#Wc&%=b|t~mGqnJZsp`&7ISVQHS`Z=vJYnE94<>{i<31B9 zb!PN0rLWWRQdW8LynCuSb@ZyIqkVA|c!Wz-wIRcHzrYfO$weaGv`C4W**{Eq&}rxY zQ@n|nPhSFKZA?jEn5v%X zZtmREmo7p?9AIjOdA$v>PyoTeM4&9g_L(j6LI|$R_`f`DXee^de-*_G2Lcmo9Rz^yw?3cM8xG^qXbz6&P zLJAi7^N5AcE1Ak~n)v~)_bC#Y@dSc2% z6FH7RwxM*j|Sh@PEP0SR7rjELc^K0h>t+^?-w_y5D9D z0Z4yN@M&$_x6>83OBI>Lq^p)GU}3(UY`vW5^q{RRsvBEA0t$*8Adf}dKm#x)&qDkn zd=}ob_>O7e&N=3w$N?ce%Kmp9*U_x1i9GzejFtoSUU|62d&K#$O$aa=m|&4}tE!#@ z5Rr0Vibd}I7^o-zu7{;lQLL^9IG6d7M! z#PY%UWO}(_eNCJVwXtvjpx6k}95Ro55zO+1PEhP{7q@&QQ;0%*L2+B~t&ZD*Z*>F! z4kePjf3d#X$^J=Q&1$p3eJGJk{}B|~h26z}S(}Z=GMn5%kzM}*w<<2%`cNe700000 LNkvXXu0mjfGb--F literal 0 HcmV?d00001 diff --git a/workspace/sconstools3/qt5/test/qrc/manual/image/main.cpp b/workspace/sconstools3/qt5/test/qrc/manual/image/main.cpp new file mode 100644 index 0000000..304dc3a --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/manual/image/main.cpp @@ -0,0 +1,11 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + Q_INIT_RESOURCE(icons); + + return 0; +} diff --git a/workspace/sconstools3/qt5/test/qrc/manual/sconstest-manual.py b/workspace/sconstools3/qt5/test/qrc/manual/sconstest-manual.py new file mode 100644 index 0000000..0a8fac9 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/manual/sconstest-manual.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Basic test for the Qrc() builder, called explicitly. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('SConscript') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/manual/sconstest-manualwflags.py b/workspace/sconstools3/qt5/test/qrc/manual/sconstest-manualwflags.py new file mode 100644 index 0000000..aef66c7 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/manual/sconstest-manualwflags.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Basic test for the Qrc() builder, called explicitly with '-name' flag set. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('SConscript-wflags','SConscript') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/SConscript b/workspace/sconstools3/qt5/test/qrc/multifiles/SConscript new file mode 100644 index 0000000..1bcb156 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/SConscript @@ -0,0 +1,8 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp')+Glob('*.qrc') + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/SConscript-manual b/workspace/sconstools3/qt5/test/qrc/multifiles/SConscript-manual new file mode 100644 index 0000000..d77c2f8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/SConscript-manual @@ -0,0 +1,9 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp') +qrc_files = env.Qrc5(['icons','other']) + +env.Program('main', source_files+qrc_files) diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/image/SConstruct b/workspace/sconstools3/qt5/test/qrc/multifiles/image/SConstruct new file mode 100644 index 0000000..00e5705 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + \ No newline at end of file diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/image/icons.qrc b/workspace/sconstools3/qt5/test/qrc/multifiles/image/icons.qrc new file mode 100644 index 0000000..86fe71b --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/image/icons.qrc @@ -0,0 +1,5 @@ + + + icons/scons.png + + diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/image/icons/scons.png b/workspace/sconstools3/qt5/test/qrc/multifiles/image/icons/scons.png new file mode 100644 index 0000000000000000000000000000000000000000..7d85f1f8c28e010a122c55baf3de8185609c95a4 GIT binary patch literal 1613 zcmV-T2D15yP)$y00004b3#c}2nYxW zd`K zafCm^j`0LMfo9-CV1%t0RMo2BrvP0sII2~{%XM165)FvBfCqd9jI$Ak*m|TLk%NPR zfpJxLmpi7#R&&*5V{M#Gb#b@RQ%7L8!g5vJ-Cc255iBATfjvvhBUDC8+TD+P9$(fq zu(2lQ*14Wz;IId80$WtoyQ#6*5Nt%Y_jAl6+eZv?dWqbyU9&3RUT$DrP0T%$avcLF zJXj8FP}Sy}5lgoV7LhT)Z}ZCfGAkPP<>LW6>l3WK6!VTJQ^vq~4^{ymsA{ZB@mLW2 zdC17qJ4XyMrN(sbkbz&~O?+Gv)5D3R7^wGP4e++AUd$_=?i4H{A%WP)-a%*OkSIff zfxP>wZfs_4O-%R38bu)K!AHOs7l)KMWeW+9JLI}P_E)oD{ z7+CDVJ!JvMeJ&byruHi#Wc&%=b|t~mGqnJZsp`&7ISVQHS`Z=vJYnE94<>{i<31B9 zb!PN0rLWWRQdW8LynCuSb@ZyIqkVA|c!Wz-wIRcHzrYfO$weaGv`C4W**{Eq&}rxY zQ@n|nPhSFKZA?jEn5v%X zZtmREmo7p?9AIjOdA$v>PyoTeM4&9g_L(j6LI|$R_`f`DXee^de-*_G2Lcmo9Rz^yw?3cM8xG^qXbz6&P zLJAi7^N5AcE1Ak~n)v~)_bC#Y@dSc2% z6FH7RwxM*j|Sh@PEP0SR7rjELc^K0h>t+^?-w_y5D9D z0Z4yN@M&$_x6>83OBI>Lq^p)GU}3(UY`vW5^q{RRsvBEA0t$*8Adf}dKm#x)&qDkn zd=}ob_>O7e&N=3w$N?ce%Kmp9*U_x1i9GzejFtoSUU|62d&K#$O$aa=m|&4}tE!#@ z5Rr0Vibd}I7^o-zu7{;lQLL^9IG6d7M! z#PY%UWO}(_eNCJVwXtvjpx6k}95Ro55zO+1PEhP{7q@&QQ;0%*L2+B~t&ZD*Z*>F! z4kePjf3d#X$^J=Q&1$p3eJGJk{}B|~h26z}S(}Z=GMn5%kzM}*w<<2%`cNe700000 LNkvXXu0mjfGb--F literal 0 HcmV?d00001 diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/image/main.cpp b/workspace/sconstools3/qt5/test/qrc/multifiles/image/main.cpp new file mode 100644 index 0000000..325478d --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/image/main.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + Q_INIT_RESOURCE(icons); + Q_INIT_RESOURCE(other); + + return 0; +} diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/image/other.qrc b/workspace/sconstools3/qt5/test/qrc/multifiles/image/other.qrc new file mode 100644 index 0000000..12ed5e8 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/image/other.qrc @@ -0,0 +1,5 @@ + + + other/rocks.png + + diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/image/other/rocks.png b/workspace/sconstools3/qt5/test/qrc/multifiles/image/other/rocks.png new file mode 100644 index 0000000000000000000000000000000000000000..7d85f1f8c28e010a122c55baf3de8185609c95a4 GIT binary patch literal 1613 zcmV-T2D15yP)$y00004b3#c}2nYxW zd`K zafCm^j`0LMfo9-CV1%t0RMo2BrvP0sII2~{%XM165)FvBfCqd9jI$Ak*m|TLk%NPR zfpJxLmpi7#R&&*5V{M#Gb#b@RQ%7L8!g5vJ-Cc255iBATfjvvhBUDC8+TD+P9$(fq zu(2lQ*14Wz;IId80$WtoyQ#6*5Nt%Y_jAl6+eZv?dWqbyU9&3RUT$DrP0T%$avcLF zJXj8FP}Sy}5lgoV7LhT)Z}ZCfGAkPP<>LW6>l3WK6!VTJQ^vq~4^{ymsA{ZB@mLW2 zdC17qJ4XyMrN(sbkbz&~O?+Gv)5D3R7^wGP4e++AUd$_=?i4H{A%WP)-a%*OkSIff zfxP>wZfs_4O-%R38bu)K!AHOs7l)KMWeW+9JLI}P_E)oD{ z7+CDVJ!JvMeJ&byruHi#Wc&%=b|t~mGqnJZsp`&7ISVQHS`Z=vJYnE94<>{i<31B9 zb!PN0rLWWRQdW8LynCuSb@ZyIqkVA|c!Wz-wIRcHzrYfO$weaGv`C4W**{Eq&}rxY zQ@n|nPhSFKZA?jEn5v%X zZtmREmo7p?9AIjOdA$v>PyoTeM4&9g_L(j6LI|$R_`f`DXee^de-*_G2Lcmo9Rz^yw?3cM8xG^qXbz6&P zLJAi7^N5AcE1Ak~n)v~)_bC#Y@dSc2% z6FH7RwxM*j|Sh@PEP0SR7rjELc^K0h>t+^?-w_y5D9D z0Z4yN@M&$_x6>83OBI>Lq^p)GU}3(UY`vW5^q{RRsvBEA0t$*8Adf}dKm#x)&qDkn zd=}ob_>O7e&N=3w$N?ce%Kmp9*U_x1i9GzejFtoSUU|62d&K#$O$aa=m|&4}tE!#@ z5Rr0Vibd}I7^o-zu7{;lQLL^9IG6d7M! z#PY%UWO}(_eNCJVwXtvjpx6k}95Ro55zO+1PEhP{7q@&QQ;0%*L2+B~t&ZD*Z*>F! z4kePjf3d#X$^J=Q&1$p3eJGJk{}B|~h26z}S(}Z=GMn5%kzM}*w<<2%`cNe700000 LNkvXXu0mjfGb--F literal 0 HcmV?d00001 diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles-manual.py b/workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles-manual.py new file mode 100644 index 0000000..ad54686 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles-manual.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Tests the Qrc() builder, when more than one .qrc file is given. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('SConscript-manual','SConscript') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles.py b/workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles.py new file mode 100644 index 0000000..ca41695 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/multifiles/sconstest-multifiles.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Tests the Qrc() builder, when more than one .qrc file is given to the +Program/Library builders directly. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('SConscript') +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/othername/image/SConscript b/workspace/sconstools3/qt5/test/qrc/othername/image/SConscript new file mode 100644 index 0000000..560f0cc --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/othername/image/SConscript @@ -0,0 +1,11 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp') +source_files.append(env.Qrc5('icons')) + +env['QT5_QRCFLAGS'] = ['-name', 'othername'] + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/othername/image/SConstruct b/workspace/sconstools3/qt5/test/qrc/othername/image/SConstruct new file mode 100644 index 0000000..00e5705 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/othername/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + \ No newline at end of file diff --git a/workspace/sconstools3/qt5/test/qrc/othername/image/icons.qrc b/workspace/sconstools3/qt5/test/qrc/othername/image/icons.qrc new file mode 100644 index 0000000..86fe71b --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/othername/image/icons.qrc @@ -0,0 +1,5 @@ + + + icons/scons.png + + diff --git a/workspace/sconstools3/qt5/test/qrc/othername/image/icons/scons.png b/workspace/sconstools3/qt5/test/qrc/othername/image/icons/scons.png new file mode 100644 index 0000000000000000000000000000000000000000..7d85f1f8c28e010a122c55baf3de8185609c95a4 GIT binary patch literal 1613 zcmV-T2D15yP)$y00004b3#c}2nYxW zd`K zafCm^j`0LMfo9-CV1%t0RMo2BrvP0sII2~{%XM165)FvBfCqd9jI$Ak*m|TLk%NPR zfpJxLmpi7#R&&*5V{M#Gb#b@RQ%7L8!g5vJ-Cc255iBATfjvvhBUDC8+TD+P9$(fq zu(2lQ*14Wz;IId80$WtoyQ#6*5Nt%Y_jAl6+eZv?dWqbyU9&3RUT$DrP0T%$avcLF zJXj8FP}Sy}5lgoV7LhT)Z}ZCfGAkPP<>LW6>l3WK6!VTJQ^vq~4^{ymsA{ZB@mLW2 zdC17qJ4XyMrN(sbkbz&~O?+Gv)5D3R7^wGP4e++AUd$_=?i4H{A%WP)-a%*OkSIff zfxP>wZfs_4O-%R38bu)K!AHOs7l)KMWeW+9JLI}P_E)oD{ z7+CDVJ!JvMeJ&byruHi#Wc&%=b|t~mGqnJZsp`&7ISVQHS`Z=vJYnE94<>{i<31B9 zb!PN0rLWWRQdW8LynCuSb@ZyIqkVA|c!Wz-wIRcHzrYfO$weaGv`C4W**{Eq&}rxY zQ@n|nPhSFKZA?jEn5v%X zZtmREmo7p?9AIjOdA$v>PyoTeM4&9g_L(j6LI|$R_`f`DXee^de-*_G2Lcmo9Rz^yw?3cM8xG^qXbz6&P zLJAi7^N5AcE1Ak~n)v~)_bC#Y@dSc2% z6FH7RwxM*j|Sh@PEP0SR7rjELc^K0h>t+^?-w_y5D9D z0Z4yN@M&$_x6>83OBI>Lq^p)GU}3(UY`vW5^q{RRsvBEA0t$*8Adf}dKm#x)&qDkn zd=}ob_>O7e&N=3w$N?ce%Kmp9*U_x1i9GzejFtoSUU|62d&K#$O$aa=m|&4}tE!#@ z5Rr0Vibd}I7^o-zu7{;lQLL^9IG6d7M! z#PY%UWO}(_eNCJVwXtvjpx6k}95Ro55zO+1PEhP{7q@&QQ;0%*L2+B~t&ZD*Z*>F! z4kePjf3d#X$^J=Q&1$p3eJGJk{}B|~h26z}S(}Z=GMn5%kzM}*w<<2%`cNe700000 LNkvXXu0mjfGb--F literal 0 HcmV?d00001 diff --git a/workspace/sconstools3/qt5/test/qrc/othername/image/main.cpp b/workspace/sconstools3/qt5/test/qrc/othername/image/main.cpp new file mode 100644 index 0000000..0457bcd --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/othername/image/main.cpp @@ -0,0 +1,11 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + Q_INIT_RESOURCE(othername); + + return 0; +} diff --git a/workspace/sconstools3/qt5/test/qrc/othername/sconstest-othername.py b/workspace/sconstools3/qt5/test/qrc/othername/sconstest-othername.py new file mode 100644 index 0000000..a62937d --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/othername/sconstest-othername.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Rename test for the Qrc() builder, uses the '-name' flag to set a different +name for the resulting .cxx file. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/samefilename/image/SConscript b/workspace/sconstools3/qt5/test/qrc/samefilename/image/SConscript new file mode 100644 index 0000000..1bcb156 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/samefilename/image/SConscript @@ -0,0 +1,8 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp')+Glob('*.qrc') + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/samefilename/image/SConstruct b/workspace/sconstools3/qt5/test/qrc/samefilename/image/SConstruct new file mode 100644 index 0000000..00e5705 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/samefilename/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + \ No newline at end of file diff --git a/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.cpp b/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.cpp new file mode 100644 index 0000000..304dc3a --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.cpp @@ -0,0 +1,11 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + Q_INIT_RESOURCE(icons); + + return 0; +} diff --git a/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.qrc b/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.qrc new file mode 100644 index 0000000..86fe71b --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons.qrc @@ -0,0 +1,5 @@ + + + icons/scons.png + + diff --git a/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons/scons.png b/workspace/sconstools3/qt5/test/qrc/samefilename/image/icons/scons.png new file mode 100644 index 0000000000000000000000000000000000000000..7d85f1f8c28e010a122c55baf3de8185609c95a4 GIT binary patch literal 1613 zcmV-T2D15yP)$y00004b3#c}2nYxW zd`K zafCm^j`0LMfo9-CV1%t0RMo2BrvP0sII2~{%XM165)FvBfCqd9jI$Ak*m|TLk%NPR zfpJxLmpi7#R&&*5V{M#Gb#b@RQ%7L8!g5vJ-Cc255iBATfjvvhBUDC8+TD+P9$(fq zu(2lQ*14Wz;IId80$WtoyQ#6*5Nt%Y_jAl6+eZv?dWqbyU9&3RUT$DrP0T%$avcLF zJXj8FP}Sy}5lgoV7LhT)Z}ZCfGAkPP<>LW6>l3WK6!VTJQ^vq~4^{ymsA{ZB@mLW2 zdC17qJ4XyMrN(sbkbz&~O?+Gv)5D3R7^wGP4e++AUd$_=?i4H{A%WP)-a%*OkSIff zfxP>wZfs_4O-%R38bu)K!AHOs7l)KMWeW+9JLI}P_E)oD{ z7+CDVJ!JvMeJ&byruHi#Wc&%=b|t~mGqnJZsp`&7ISVQHS`Z=vJYnE94<>{i<31B9 zb!PN0rLWWRQdW8LynCuSb@ZyIqkVA|c!Wz-wIRcHzrYfO$weaGv`C4W**{Eq&}rxY zQ@n|nPhSFKZA?jEn5v%X zZtmREmo7p?9AIjOdA$v>PyoTeM4&9g_L(j6LI|$R_`f`DXee^de-*_G2Lcmo9Rz^yw?3cM8xG^qXbz6&P zLJAi7^N5AcE1Ak~n)v~)_bC#Y@dSc2% z6FH7RwxM*j|Sh@PEP0SR7rjELc^K0h>t+^?-w_y5D9D z0Z4yN@M&$_x6>83OBI>Lq^p)GU}3(UY`vW5^q{RRsvBEA0t$*8Adf}dKm#x)&qDkn zd=}ob_>O7e&N=3w$N?ce%Kmp9*U_x1i9GzejFtoSUU|62d&K#$O$aa=m|&4}tE!#@ z5Rr0Vibd}I7^o-zu7{;lQLL^9IG6d7M! z#PY%UWO}(_eNCJVwXtvjpx6k}95Ro55zO+1PEhP{7q@&QQ;0%*L2+B~t&ZD*Z*>F! z4kePjf3d#X$^J=Q&1$p3eJGJk{}B|~h26z}S(}Z=GMn5%kzM}*w<<2%`cNe700000 LNkvXXu0mjfGb--F literal 0 HcmV?d00001 diff --git a/workspace/sconstools3/qt5/test/qrc/samefilename/sconstest-samefilename.py b/workspace/sconstools3/qt5/test/qrc/samefilename/sconstest-samefilename.py new file mode 100644 index 0000000..91178ac --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/samefilename/sconstest-samefilename.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Check that the correct prefixes/suffixes are appended to the target of +the Qrc() builder. Else, we could not add .qrc and CXX files with the +same prefix to the Program/Library builders. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qrc/subdir/image/SConscript b/workspace/sconstools3/qt5/test/qrc/subdir/image/SConscript new file mode 100644 index 0000000..53ef724 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/subdir/image/SConscript @@ -0,0 +1,8 @@ +Import('qtEnv') + +env = qtEnv.Clone() +env.EnableQt5Modules(['QtCore','QtWidgets']) + +source_files = Glob('*.cpp')+['qrc/icons.qrc'] + +env.Program('main', source_files) diff --git a/workspace/sconstools3/qt5/test/qrc/subdir/image/SConstruct b/workspace/sconstools3/qt5/test/qrc/subdir/image/SConstruct new file mode 100644 index 0000000..00e5705 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/subdir/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + \ No newline at end of file diff --git a/workspace/sconstools3/qt5/test/qrc/subdir/image/main.cpp b/workspace/sconstools3/qt5/test/qrc/subdir/image/main.cpp new file mode 100644 index 0000000..304dc3a --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/subdir/image/main.cpp @@ -0,0 +1,11 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + Q_INIT_RESOURCE(icons); + + return 0; +} diff --git a/workspace/sconstools3/qt5/test/qrc/subdir/image/qrc/icons.qrc b/workspace/sconstools3/qt5/test/qrc/subdir/image/qrc/icons.qrc new file mode 100644 index 0000000..86fe71b --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/subdir/image/qrc/icons.qrc @@ -0,0 +1,5 @@ + + + icons/scons.png + + diff --git a/workspace/sconstools3/qt5/test/qrc/subdir/image/qrc/icons/scons.png b/workspace/sconstools3/qt5/test/qrc/subdir/image/qrc/icons/scons.png new file mode 100644 index 0000000000000000000000000000000000000000..7d85f1f8c28e010a122c55baf3de8185609c95a4 GIT binary patch literal 1613 zcmV-T2D15yP)$y00004b3#c}2nYxW zd`K zafCm^j`0LMfo9-CV1%t0RMo2BrvP0sII2~{%XM165)FvBfCqd9jI$Ak*m|TLk%NPR zfpJxLmpi7#R&&*5V{M#Gb#b@RQ%7L8!g5vJ-Cc255iBATfjvvhBUDC8+TD+P9$(fq zu(2lQ*14Wz;IId80$WtoyQ#6*5Nt%Y_jAl6+eZv?dWqbyU9&3RUT$DrP0T%$avcLF zJXj8FP}Sy}5lgoV7LhT)Z}ZCfGAkPP<>LW6>l3WK6!VTJQ^vq~4^{ymsA{ZB@mLW2 zdC17qJ4XyMrN(sbkbz&~O?+Gv)5D3R7^wGP4e++AUd$_=?i4H{A%WP)-a%*OkSIff zfxP>wZfs_4O-%R38bu)K!AHOs7l)KMWeW+9JLI}P_E)oD{ z7+CDVJ!JvMeJ&byruHi#Wc&%=b|t~mGqnJZsp`&7ISVQHS`Z=vJYnE94<>{i<31B9 zb!PN0rLWWRQdW8LynCuSb@ZyIqkVA|c!Wz-wIRcHzrYfO$weaGv`C4W**{Eq&}rxY zQ@n|nPhSFKZA?jEn5v%X zZtmREmo7p?9AIjOdA$v>PyoTeM4&9g_L(j6LI|$R_`f`DXee^de-*_G2Lcmo9Rz^yw?3cM8xG^qXbz6&P zLJAi7^N5AcE1Ak~n)v~)_bC#Y@dSc2% z6FH7RwxM*j|Sh@PEP0SR7rjELc^K0h>t+^?-w_y5D9D z0Z4yN@M&$_x6>83OBI>Lq^p)GU}3(UY`vW5^q{RRsvBEA0t$*8Adf}dKm#x)&qDkn zd=}ob_>O7e&N=3w$N?ce%Kmp9*U_x1i9GzejFtoSUU|62d&K#$O$aa=m|&4}tE!#@ z5Rr0Vibd}I7^o-zu7{;lQLL^9IG6d7M! z#PY%UWO}(_eNCJVwXtvjpx6k}95Ro55zO+1PEhP{7q@&QQ;0%*L2+B~t&ZD*Z*>F! z4kePjf3d#X$^J=Q&1$p3eJGJk{}B|~h26z}S(}Z=GMn5%kzM}*w<<2%`cNe700000 LNkvXXu0mjfGb--F literal 0 HcmV?d00001 diff --git a/workspace/sconstools3/qt5/test/qrc/subdir/sconstest-subdir.py b/workspace/sconstools3/qt5/test/qrc/subdir/sconstest-subdir.py new file mode 100644 index 0000000..bd03a96 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qrc/subdir/sconstest-subdir.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +In this test the QRC file is placed in a subfolder (qrc/icons.qrc). The +Qrc5() builder should correctly strip the leading path and set the "-name" +option for the RCC executable to "icons" only. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/qt_examples/create_scons_tests.py b/workspace/sconstools3/qt5/test/qt_examples/create_scons_tests.py new file mode 100644 index 0000000..0ffbf02 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qt_examples/create_scons_tests.py @@ -0,0 +1,748 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# +# create_scons_test.py +# +# Populates a Qt "examples" directory with files for the +# SCons test framework. +# +# +# +# Usage: +# +# Step 1: Copy the "examples" folder of your Qt source tree into +# this directory. +# Step 2: Run "python create_scons_tests.py" to create all files +# for the SCons test framework. +# Step 3: Execute "python runtest.py -a" to run all tests +# +# Additional options for this script are: +# +# -local Creates the test files in the local directory, +# also copies qtenv.py and qt5.py to their correct +# places. +# -clean Removes all intermediate test files. +# +# + +import os, sys, re, glob, shutil + +# cxx file extensions +cxx_ext = ['.c', '.cpp', '.cxx', '.cc'] +# header file extensions +h_ext = ['.h', '.hpp', '.hxx'] +# regex for includes +inc_re = re.compile(r'#include\s+<([^>]+)>') +# regex for qtLibraryTarget function +qtlib_re = re.compile(r'\$\$qtLibraryTarget\(([^\)]+)\)') +qrcinit_re = re.compile('Q_INIT_RESOURCE\(([^\)]+)\)') +localvar_re = re.compile("\\$\\$([^/\s]+)") +qthave_re = re.compile("qtHaveModule\([^\)]+\)\s*:\s") + +updir = '..'+os.sep + +# we currently skip all .pro files that use these config values +complicated_configs = ['qdbus','phonon','plugin'] +# for the following CONFIG values we have to provide default qt modules +config_modules = {'designer' : ['QtCore','QtGui','QtXml','QtWidgets','QtDesigner','QtDesignerComponents'], + 'uitools' : ['QtCore','QtGui','QtUiTools'], + 'assistant' : ['QtCore','QtGui','QtXml','QtScript','QtAssistant'], + 'core' : ['QtCore'], + 'gui' : ['QtCore', 'QtGui'], + 'concurrent' : ['QtCore', 'QtConcurrent'], + 'dbus' : ['QtCore', 'QtDBus'], + 'declarative' : ['QtCore', 'QtGui', 'QtScript', 'QtSql', 'QtNetwork', 'QtWidgets', 'QtXmlPatterns', 'QtDeclarative'], + 'printsupport' : ['QtCore', 'QtGui', 'QtWidgets', 'QtPrintSupport'], + 'mediawidgets' : ['QtCore', 'QtGui', 'QtOpenGL', 'QtNetwork', 'QtWidgets', 'QtMultimedia', 'QtMultimediaWidgets'], + 'webkitwidgets' : ['QtCore', 'QtGui', 'QtOpenGL', 'QtNetwork', 'QtWidgets', 'QtPrintSupport', 'QtWebKit', 'QtQuick', 'QtQml', 'QtSql', 'QtV8', 'QtWebKitWidgets'], + 'qml' : ['QtCore', 'QtGui', 'QtNetwork', 'QtV8', 'QtQml'], + 'quick' : ['QtCore', 'QtGui', 'QtNetwork', 'QtV8', 'QtQml', 'QtQuick'], + 'axcontainer' : [], + 'axserver' : [], + 'testlib' : ['QtCore', 'QtTest'], + 'xmlpatterns' : ['QtCore', 'QtNetwork', 'QtXmlPatterns'], + 'qt' : ['QtCore','QtGui'], + 'xml' : ['QtCore','QtGui','QtXml'], + 'webkit' : ['QtCore','QtGui','QtQuick','QtQml','QtNetwork','QtSql','QtV8','QtWebKit'], + 'network' : ['QtCore','QtNetwork'], + 'svg' : ['QtCore','QtGui','QtWidgets','QtSvg'], + 'script' : ['QtCore','QtScript'], + 'scripttools' : ['QtCore','QtGui','QtWidgets','QtScript','QtScriptTools'], + 'multimedia' : ['QtCore','QtGui','QtNetwork','QtMultimedia'], + 'script' : ['QtCore','QtScript'], + 'help' : ['QtCore','QtGui','QtWidgets','QtCLucene','QtSql','QtNetwork','QtHelp'], + 'qtestlib' : ['QtCore','QtTest'], + 'opengl' : ['QtCore','QtGui','QtOpenGL'], + 'widgets' : ['QtCore','QtGui','QtWidgets'] + } +# for the following CONFIG values we have to provide additional CPP defines +config_defines = {'plugin' : ['QT_PLUGIN'], + 'designer' : ['QDESIGNER_EXPORT_WIDGETS'] + } + +# dictionary of special Qt Environment settings for all single tests/pro files +qtenv_flags = {'QT5_GOBBLECOMMENTS' : '1' + } + +# available qt modules +validModules = [ + # Qt Essentials + 'QtCore', + 'QtGui', + 'QtMultimedia', + 'QtMultimediaQuick_p', + 'QtMultimediaWidgets', + 'QtNetwork', + 'QtPlatformSupport', + 'QtQml', + 'QtQmlDevTools', + 'QtQuick', + 'QtQuickParticles', + 'QtSql', + 'QtQuickTest', + 'QtTest', + 'QtWebKit', + 'QtWebKitWidgets', + 'QtWidgets', + # Qt Add-Ons + 'QtConcurrent', + 'QtDBus', + 'QtOpenGL', + 'QtPrintSupport', + 'QtDeclarative', + 'QtScript', + 'QtScriptTools', + 'QtSvg', + 'QtUiTools', + 'QtXml', + 'QtXmlPatterns', + # Qt Tools + 'QtHelp', + 'QtDesigner', + 'QtDesignerComponents', + # Other + 'QtCLucene', + 'QtConcurrent', + 'QtV8' + ] + +def findQtBinParentPath(qtpath): + """ Within the given 'qtpath', search for a bin directory + containing the 'lupdate' executable and return + its parent path. + """ + for path, dirs, files in os.walk(qtpath): + for d in dirs: + if d == 'bin': + if sys.platform.startswith("linux"): + lpath = os.path.join(path, d, 'lupdate') + else: + lpath = os.path.join(path, d, 'lupdate.exe') + if os.path.isfile(lpath): + return path + + return "" + +def findMostRecentQtPath(dpath): + paths = glob.glob(dpath) + if len(paths): + paths.sort() + return findQtBinParentPath(paths[-1]) + + return "" + +def detectLatestQtVersion(): + if sys.platform.startswith("linux"): + # Inspect '/usr/local/Qt' first... + p = findMostRecentQtPath(os.path.join('/usr','local','Qt-*')) + if not p: + # ... then inspect '/usr/local/Trolltech'... + p = findMostRecentQtPath(os.path.join('/usr','local','Trolltech','*')) + if not p: + # ...then try to find a binary install... + p = findMostRecentQtPath(os.path.join('/opt','Qt*')) + if not p: + # ...then try to find a binary SDK. + p = findMostRecentQtPath(os.path.join('/opt','qtsdk*')) + + else: + # Simple check for Windows: inspect only 'C:\Qt' + paths = glob.glob(os.path.join('C:\\', 'Qt', 'Qt*')) + p = "" + if len(paths): + paths.sort() + # Select version with highest release number + paths = glob.glob(os.path.join(paths[-1], '5*')) + if len(paths): + paths.sort() + # Is it a MinGW or VS installation? + p = findMostRecentQtPath(os.path.join(paths[-1], 'mingw*')) + if not p: + # No MinGW, so try VS... + p = findMostRecentQtPath(os.path.join(paths[-1], 'msvc*')) + + return os.environ.get("QT5DIR", p) + +def detectPkgconfigPath(qtdir): + pkgpath = os.path.join(qtdir, 'lib', 'pkgconfig') + if os.path.exists(os.path.join(pkgpath,'Qt5Core.pc')): + return pkgpath + pkgpath = os.path.join(qtdir, 'lib') + if os.path.exists(os.path.join(pkgpath,'Qt5Core.pc')): + return pkgpath + + return "" + +def expandProFile(fpath): + """ Read the given file into a list of single lines, + while expanding included files (mainly *.pri) + recursively. + """ + lines = [] + f = open(fpath,'r') + content = f.readlines() + f.close() + pwdhead, tail = os.path.split(fpath) + head = pwdhead + if pwdhead: + pwdhead = os.path.abspath(pwdhead) + else: + pwdhead = os.path.abspath(os.getcwd()) + for idx, l in enumerate(content): + l = l.rstrip('\n') + l = l.rstrip() + if '$$PWD' in l: + l = l.replace("$$PWD", pwdhead) + if l.startswith('include('): + # Expand include file + l = l.rstrip(')') + l = l.replace('include(','') + while '$$' in l: + # Try to replace the used local variable by + # searching back in the content + m = localvar_re.search(l) + if m: + key = m.group(1) + tidx = idx-1 + skey = "%s = " % key + while tidx >= 0: + if content[tidx].startswith(skey): + # Key found + l = l.replace('$$%s' % key, content[tidx][len(skey):].rstrip('\n')) + if os.path.join('..','..','qtbase','examples') in l: + # Quick hack to cope with erroneous *.pro files + l = l.replace(os.path.join('..','..','qtbase','examples'), os.path.join('..','examples')) + break + tidx -= 1 + if tidx < 0: + print "Error: variable %s could not be found during parsing!" % key + break + ipath = l + if head: + ipath = os.path.join(head, l) + # Does the file exist? + if not os.path.isfile(ipath): + # Then search for it the hard way + ihead, tail = os.path.split(ipath) + for spath, dirs, files in os.walk('.'): + for f in files: + if f == tail: + ipath = os.path.join(spath, f) + lines.extend(expandProFile(ipath)) + else: + lines.append(l) + + return lines + +def parseProFile(fpath): + """ Parse the .pro file fpath and return the defined + variables in a dictionary. + """ + keys = {} + curkey = None + curlist = [] + for l in expandProFile(fpath): + # Strip off qtHaveModule part + m = qthave_re.search(l) + if m: + l = qthave_re.sub("", l) + kl = l.split('=') + if len(kl) > 1: + # Close old key + if curkey: + if keys.has_key(curkey): + keys[curkey].extend(curlist) + else: + keys[curkey] = curlist + + # Split off trailing + + nkey = kl[0].rstrip('+') + nkey = nkey.rstrip() + # Split off optional leading part with "contains():" + cl = nkey.split(':') + if ('lesock' not in l) and ((len(cl) < 2) or ('msvc' in cl[0])): + nkey = cl[-1] + # Open new key + curkey = nkey.split()[0] + value = kl[1].lstrip() + if value.endswith('\\'): + # Key is continued on next line + value = value[:-1] + curlist = value.split() + else: + # Single line key + if keys.has_key(curkey): + keys[curkey].extend(value.split()) + else: + keys[curkey] = value.split() + curkey = None + curlist = [] + else: + if l.endswith('\\'): + # Continue current key + curlist.extend(l[:-1].split()) + else: + # Unknown, so go back to VOID state + if curkey: + # Append last item for current key... + curlist.extend(l.split()) + if keys.has_key(curkey): + keys[curkey].extend(curlist) + else: + keys[curkey] = curlist + + # ... and reset parse state. + curkey = None + curlist = [] + + return keys + +def writeSConstruct(dirpath): + """ Create a SConstruct file in dirpath. + """ + sc = open(os.path.join(dirpath,'SConstruct'),'w') + sc.write("""import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + """) + sc.close() + +def collectModulesFromFiles(fpath): + """ Scan source files in dirpath for included + Qt5 modules, and return the used modules in a list. + """ + mods = [] + try: + f = open(fpath,'r') + content = f.read() + f.close() + except: + return mods + + for m in inc_re.finditer(content): + mod = m.group(1) + if (mod in validModules) and (mod not in mods): + mods.append(mod) + return mods + +def findQResourceName(fpath): + """ Scan the source file fpath and return the name + of the QRC instance that gets initialized via + QRC_INIT_RESOURCE. + """ + name = "" + + try: + f = open(fpath,'r') + content = f.read() + f.close() + except: + return name + + m = qrcinit_re.search(content) + if m: + name = m.group(1) + + return name + +def validKey(key, pkeys): + """ Helper function + """ + if pkeys.has_key(key) and len(pkeys[key]) > 0: + return True + + return False + +def collectModules(dirpath, pkeys): + """ Scan source files in dirpath for included + Qt5 modules, and return the used modules in a list. + """ + mods = [] + defines = [] + # Scan subdirs + if validKey('SUBDIRS', pkeys): + for s in pkeys['SUBDIRS']: + flist = glob.glob(os.path.join(dirpath, s, '*.*')) + for f in flist: + root, ext = os.path.splitext(f) + if (ext and ((ext in cxx_ext) or + (ext in h_ext))): + mods.extend(collectModulesFromFiles(f)) + + # Scan sources + if validKey('SOURCES', pkeys): + for s in pkeys['SOURCES']: + f = os.path.join(dirpath,s) + mods.extend(collectModulesFromFiles(f)) + + # Scan headers + if validKey('HEADERS', pkeys): + for s in pkeys['HEADERS']: + f = os.path.join(dirpath,s) + mods.extend(collectModulesFromFiles(f)) + + # Check CONFIG keyword + if validKey('CONFIG', pkeys): + for k in pkeys['CONFIG']: + if config_modules.has_key(k): + mods.extend(config_modules[k]) + if config_defines.has_key(k): + defines.extend(config_defines[k]) + + # Check QT keyword + if validKey('QT', pkeys): + for k in pkeys['QT']: + if config_modules.has_key(k): + mods.extend(config_modules[k]) + + # Make lists unique + unique_mods = [] + for m in mods: + if m not in unique_mods: + unique_mods.append(m) + unique_defines = [] + for d in defines: + if d not in unique_defines: + unique_defines.append(d) + + # Safety hack, if no modules are found so far + # assume that this is a normal Qt GUI application... + if len(unique_mods) == 0: + unique_mods = ['QtCore','QtGui'] + + return (unique_mods, unique_defines) + +def relOrAbsPath(dirpath, rpath): + if rpath.startswith('..'): + return os.path.abspath(os.path.normpath(os.path.join(dirpath, rpath))) + + return rpath + +def writeSConscript(dirpath, profile, pkeys): + """ Create a SConscript file in dirpath. + """ + + # Activate modules + mods, defines = collectModules(dirpath, pkeys) + if validKey('CONFIG', pkeys) and isComplicated(pkeys['CONFIG'][0]): + return False + + qrcname = "" + if not validKey('SOURCES', pkeys): + # No SOURCES specified, try to find CPP files + slist = glob.glob(os.path.join(dirpath,'*.cpp')) + if len(slist) == 0: + # Nothing to build here + return False + else: + # Scan for Q_INIT_RESOURCE + for s in slist: + qrcname = findQResourceName(s) + if qrcname: + break + + allmods = True + for m in mods: + if m not in pkeys['qtmodules']: + print " no module %s" % m + allmods = False + if not allmods: + return False + + sc = open(os.path.join(dirpath,'SConscript'),'w') + sc.write("""Import('qtEnv') + +env = qtEnv.Clone() +""") + + if len(mods): + sc.write('env.EnableQt5Modules([\n') + for m in mods[:-1]: + sc.write("'%s',\n" % m) + sc.write("'%s'\n" % mods[-1]) + sc.write('])\n\n') + + # Add CPPDEFINEs + if len(defines): + sc.write('env.AppendUnique(CPPDEFINES=[\n') + for d in defines[:-1]: + sc.write("'%s',\n" % d) + sc.write("'%s'\n" % defines[-1]) + sc.write('])\n\n') + + # Add LIBS + if validKey('LIBS', pkeys): + sc.write('env.AppendUnique(LIBS=[\n') + for d in pkeys['LIBS'][:-1]: + sc.write("'%s',\n" % d) + sc.write("'%s'\n" % pkeys['LIBS'][-1]) + sc.write('])\n\n') + + # Collect INCLUDEPATHs + incpaths = [] + if validKey('INCLUDEPATH', pkeys): + incpaths = pkeys['INCLUDEPATH'] + if validKey('FORMS', pkeys): + for s in pkeys['FORMS']: + head, tail = os.path.split(s) + if head and head not in incpaths: + incpaths.append(head) + if incpaths: + sc.write('env.Append(CPPPATH=[\n') + for d in incpaths[:-1]: + sc.write("'%s',\n" % relOrAbsPath(dirpath, d)) + sc.write("'%s'\n" % relOrAbsPath(dirpath, incpaths[-1])) + sc.write('])\n\n') + + # Add special environment flags + if len(qtenv_flags): + for key, value in qtenv_flags.iteritems(): + sc.write("env['%s']=%s\n" % (key, value)) + + + # Write source files + if validKey('SOURCES', pkeys): + sc.write('source_files = [\n') + for s in pkeys['SOURCES'][:-1]: + sc.write("'%s',\n" % relOrAbsPath(dirpath, s)) + if not qrcname: + qrcname = findQResourceName(os.path.join(dirpath,s)) + + sc.write("'%s'\n" % relOrAbsPath(dirpath, pkeys['SOURCES'][-1])) + if not qrcname: + qrcname = findQResourceName(os.path.join(dirpath,pkeys['SOURCES'][-1])) + sc.write(']\n\n') + + # Write .ui files + if validKey('FORMS', pkeys): + sc.write('ui_files = [\n') + for s in pkeys['FORMS'][:-1]: + sc.write("'%s',\n" % relOrAbsPath(dirpath, s)) + sc.write("'%s'\n" % relOrAbsPath(dirpath, pkeys['FORMS'][-1])) + sc.write(']\n') + sc.write('env.Uic5(ui_files)\n\n') + + # Write .qrc files + if validKey('RESOURCES', pkeys): + qrc_name = pkeys['RESOURCES'][0] + if qrcname: + if qrc_name.endswith('.qrc'): + qrc_name = qrc_name[:-4] + sc.write("qrc_out = env.Qrc5('%s')\nsource_files.append(qrc_out)\nenv['QT5_QRCFLAGS'] = ['-name', '%s']\n" % (qrc_name, qrcname)) + else: + if not qrc_name.endswith('.qrc'): + qrc_name += '.qrc' + sc.write("source_files.append('%s')\n" % qrc_name) + + # Select module + type = 'Program' + if validKey('TEMPLATE', pkeys): + if pkeys['TEMPLATE'][0] == 'lib': + type = 'StaticLibrary' + if pkeys['TEMPLATE'][0] == 'dll': + type = 'SharedLibrary' + + # TARGET may be wrapped by qtLibraryTarget function... + target = profile + if validKey('TARGET', pkeys): + t = pkeys['TARGET'][0] + m = qtlib_re.search(t) + if m: + t = "Qt" + m.group(1) + target = t.replace("$$TARGET", profile) + + # Create program/lib/dll + else: + if validKey('SOURCES', pkeys): + sc.write("env.%s('%s', source_files)\n\n" % (type, target)) + else: + sc.write("env.%s('%s', Glob('*.cpp'))\n\n" % (type, target)) + + sc.close() + + return True + +def writeSConsTestFile(dirpath, folder): + dirnums = dirpath.count(os.sep)+1 + f = open(os.path.join(dirpath, "sconstest-%s.py" % folder),'w') + f.write(""" +import os +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("%s") +test.file_fixture('%sqtenv.py') +test.file_fixture('%s__init__.py', os.path.join('site_scons','site_tools','qt5','__init__.py')) +test.run() + +test.pass_test() + """ % (folder, updir*dirnums, updir*(dirnums+1))) + f.close() + +def installLocalFiles(dirpath): + dirnums = dirpath.count(os.sep)+1 + shutil.copy(os.path.join(dirpath,updir*dirnums+'qtenv.py'), + os.path.join(dirpath,'qtenv.py')) + toolpath = os.path.join(dirpath,'site_scons','site_tools','qt5') + if not os.path.exists(toolpath): + os.makedirs(toolpath) + shutil.copy(os.path.join(dirpath,updir*(dirnums+1)+'__init__.py'), + os.path.join(dirpath,'site_scons','site_tools','qt5','__init__.py')) + +def isComplicated(keyvalues): + for s in keyvalues: + if s in complicated_configs: + return True + + return False + +# Folders that should get skipped while creating the SConscripts +skip_folders = ['activeqt', 'declarative', 'dbus'] + +def createSConsTest(dirpath, profile, options): + """ Create files for the SCons test framework in dirpath. + """ + pkeys = parseProFile(os.path.join(dirpath, profile)) + if validKey('TEMPLATE', pkeys) and pkeys['TEMPLATE'][0] == 'subdirs': + return + if validKey('CONFIG', pkeys) and isComplicated(pkeys['CONFIG']): + return + if validKey('QT', pkeys) and isComplicated(pkeys['QT']): + return + + head, tail = os.path.split(dirpath) + if head and tail: + print os.path.join(dirpath, profile) + for s in skip_folders: + if s in dirpath: + return + pkeys['qtmodules'] = options['qtmodules'] + if not writeSConscript(dirpath, profile[:-4], pkeys): + return + writeSConstruct(dirpath) + writeSConsTestFile(head, tail) + if options['local']: + installLocalFiles(dirpath) + +def cleanSConsTest(dirpath, profile, options): + """ Remove files for the SCons test framework in dirpath. + """ + try: + os.remove(os.path.join(dirpath,'SConstruct')) + except: + pass + try: + os.remove(os.path.join(dirpath,'SConscript')) + except: + pass + try: + os.remove(os.path.join(dirpath,'qtenv.py')) + except: + pass + try: + shutil.rmtree(os.path.join(dirpath,'site_scons'), + ignore_errors=True) + except: + pass + head, tail = os.path.split(dirpath) + if head and tail: + try: + os.remove(os.path.join(head, "sconstest-%s.py" % tail)) + except: + pass + +def main(): + """ The main program. + """ + + # Parse command line options + options = {'local' : False, # Install qtenv.py and qt5.py in local folder + 'qtpath' : None, + 'pkgconfig' : None + } + clean = False + qtpath = None + for o in sys.argv[1:]: + if o == "-local": + options['local'] = True + elif o == "-clean": + clean = True + else: + options['qtpath'] = o + + if not options['qtpath']: + qtpath = detectLatestQtVersion() + if qtpath == "": + print "No Qt installation found!" + sys.exit(1) + + is_win = sys.platform.startswith('win') + if not is_win: + # Use pkgconfig to detect the available modules + options['pkgconfig'] = detectPkgconfigPath(qtpath) + if options['pkgconfig'] == "": + print "No pkgconfig files found!" + sys.exit(1) + + options['qtpath'] = qtpath + options['qtmodules'] = [] + for v in validModules: + if is_win or os.path.exists(os.path.join(options['pkgconfig'],v.replace('Qt','Qt5')+'.pc')): + options['qtmodules'].append(v) + + if not clean: + doWork = createSConsTest + else: + doWork = cleanSConsTest + + # Detect .pro files + for path, dirs, files in os.walk('.'): + for f in files: + if f.endswith('.pro'): + doWork(path, f, options) + +if __name__ == "__main__": + main() diff --git a/workspace/sconstools3/qt5/test/qt_examples/sconstest.skip b/workspace/sconstools3/qt5/test/qt_examples/sconstest.skip new file mode 100644 index 0000000..e69de29 diff --git a/workspace/sconstools3/qt5/test/qtenv.py b/workspace/sconstools3/qt5/test/qtenv.py new file mode 100644 index 0000000..f37faf2 --- /dev/null +++ b/workspace/sconstools3/qt5/test/qtenv.py @@ -0,0 +1,107 @@ +# +# Copyright (c) 2001-2010 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +import os, sys, glob +from SCons import Environment + +def findQtBinParentPath(qtpath): + """ Within the given 'qtpath', search for a bin directory + containing the 'lupdate' executable and return + its parent path. + """ + for path, dirs, files in os.walk(qtpath): + for d in dirs: + if d == 'bin': + if sys.platform.startswith("linux"): + lpath = os.path.join(path, d, 'lupdate') + else: + lpath = os.path.join(path, d, 'lupdate.exe') + if os.path.isfile(lpath): + return path + + return "" + +def findMostRecentQtPath(dpath): + paths = glob.glob(dpath) + if len(paths): + paths.sort() + return findQtBinParentPath(paths[-1]) + + return "" + +def detectLatestQtVersion(): + if sys.platform.startswith("linux"): + # Inspect '/usr/local/Qt' first... + p = findMostRecentQtPath('/usr/local/Qt-*') + if not p: + # ... then inspect '/usr/local/Trolltech'... + p = findMostRecentQtPath('/usr/local/Trolltech/*') + if not p: + # ...then try to find a binary install... + p = findMostRecentQtPath('/opt/Qt*') + if not p: + # ...then try to find a binary SDK. + p = findMostRecentQtPath('/opt/qtsdk*') + + else: + # Simple check for Windows: inspect only 'C:\Qt' + paths = glob.glob(os.path.join('C:\\', 'Qt', 'Qt*')) + p = "" + if len(paths): + paths.sort() + # Select version with highest release number + paths = glob.glob(os.path.join(paths[-1], '5*')) + if len(paths): + paths.sort() + # Is it a MinGW or VS installation? + p = findMostRecentQtPath(os.path.join(paths[-1], 'mingw*')) + if not p: + # No MinGW, so try VS... + p = findMostRecentQtPath(os.path.join(paths[-1], 'msvc*')) + + return os.environ.get("QT5DIR", p) + +def detectPkgconfigPath(qtdir): + pkgpath = os.path.join(qtdir, 'lib', 'pkgconfig') + if os.path.exists(os.path.join(pkgpath,'Qt5Core.pc')): + return pkgpath + pkgpath = os.path.join(qtdir, 'lib') + if os.path.exists(os.path.join(pkgpath,'Qt5Core.pc')): + return pkgpath + + return "" + +def createQtEnvironment(qtdir=None, env=None): + if not env: + env = Environment.Environment(tools=['default']) + if not qtdir: + qtdir = detectLatestQtVersion() + env['QT5DIR'] = qtdir + if sys.platform.startswith("linux"): + env['ENV']['PKG_CONFIG_PATH'] = detectPkgconfigPath(qtdir) + env.Tool('qt5') + env.Append(CXXFLAGS=['-fPIC']) + + return env + diff --git a/workspace/sconstools3/qt5/test/sconstest.skip b/workspace/sconstools3/qt5/test/sconstest.skip new file mode 100644 index 0000000..e69de29 diff --git a/workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.cpp b/workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.cpp new file mode 100644 index 0000000..09e27c6 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.cpp @@ -0,0 +1,7 @@ +#include "MyFile.h" + +aaa::aaa() : my_s(tr("SCons rocks!")) +{ + ; +} + diff --git a/workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.h b/workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.h new file mode 100644 index 0000000..10311dd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/clean/image/MyFile.h @@ -0,0 +1,13 @@ +#include +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa(); + +private: + QString my_s; +}; diff --git a/workspace/sconstools3/qt5/test/ts_qm/clean/image/SConscript b/workspace/sconstools3/qt5/test/ts_qm/clean/image/SConscript new file mode 100644 index 0000000..915e1cd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/clean/image/SConscript @@ -0,0 +1,6 @@ +Import("qtEnv") + +qtEnv['QT5_CLEAN_TS']=1 + +qtEnv.Ts5('my_en.ts', Glob('*.cpp')) +qtEnv.Qm5('my_en','my_en') diff --git a/workspace/sconstools3/qt5/test/ts_qm/clean/image/SConstruct b/workspace/sconstools3/qt5/test/ts_qm/clean/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/clean/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/ts_qm/clean/sconstest-clean.py b/workspace/sconstools3/qt5/test/ts_qm/clean/sconstest-clean.py new file mode 100644 index 0000000..40cb442 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/clean/sconstest-clean.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Test the QT5_CLEAN_TS option, which removes .ts files +on a 'scons -c'. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(stderr=None) + +test.must_exist(test.workpath('my_en.ts')) +test.must_contain(test.workpath('my_en.ts'),'SCons rocks!') +test.must_exist(test.workpath('my_en.qm')) + +test.run(options = '-c') + +test.must_not_exist(test.workpath('my_en.ts')) +test.must_not_exist(test.workpath('my_en.qm')) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.cpp b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.cpp new file mode 100644 index 0000000..09e27c6 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.cpp @@ -0,0 +1,7 @@ +#include "MyFile.h" + +aaa::aaa() : my_s(tr("SCons rocks!")) +{ + ; +} + diff --git a/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.h b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.h new file mode 100644 index 0000000..10311dd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/MyFile.h @@ -0,0 +1,13 @@ +#include +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa(); + +private: + QString my_s; +}; diff --git a/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConscript b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConscript new file mode 100644 index 0000000..4b68448 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConscript @@ -0,0 +1,3 @@ +Import("qtEnv") + +qtEnv.Ts5('my_en', ['MyFile.cpp','subdir']) diff --git a/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConstruct b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.cpp b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.cpp new file mode 100644 index 0000000..687c0cd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.cpp @@ -0,0 +1,6 @@ +#include "bbb.h" + +bbb::bbb() : my_s(tr("And Qt5 too!")) +{ + ; +} diff --git a/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.h b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.h new file mode 100644 index 0000000..0b60846 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/mixdir/image/subdir/bbb.h @@ -0,0 +1,13 @@ +#include +#include + +class bbb : public QObject +{ + Q_OBJECT + +public: + bbb(); + +private: + QString my_s; +}; diff --git a/workspace/sconstools3/qt5/test/ts_qm/mixdir/sconstest-mixdir.py b/workspace/sconstools3/qt5/test/ts_qm/mixdir/sconstest-mixdir.py new file mode 100644 index 0000000..df16877 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/mixdir/sconstest-mixdir.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Runs the Ts() builder with a mix of files and dirs as +input (list of sources). +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(stderr=None) + +test.must_exist(test.workpath('my_en.ts')) +test.must_contain(test.workpath('my_en.ts'),'SCons rocks!') +test.must_contain(test.workpath('my_en.ts'),'And Qt5 too!') + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.cpp b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.cpp new file mode 100644 index 0000000..09e27c6 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.cpp @@ -0,0 +1,7 @@ +#include "MyFile.h" + +aaa::aaa() : my_s(tr("SCons rocks!")) +{ + ; +} + diff --git a/workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.h b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.h new file mode 100644 index 0000000..10311dd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/MyFile.h @@ -0,0 +1,13 @@ +#include +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa(); + +private: + QString my_s; +}; diff --git a/workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConscript b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConscript new file mode 100644 index 0000000..677bd35 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConscript @@ -0,0 +1,6 @@ +Import("qtEnv") + +qtEnv.Ts5('my_en', Glob('*.cpp')) +qtEnv.Ts5('a','MyFile.cpp') +qtEnv.Ts5('b','bbb.cpp') +qtEnv.Qm5('my_en',['a','b']) diff --git a/workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConstruct b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.cpp b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.cpp new file mode 100644 index 0000000..687c0cd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.cpp @@ -0,0 +1,6 @@ +#include "bbb.h" + +bbb::bbb() : my_s(tr("And Qt5 too!")) +{ + ; +} diff --git a/workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.h b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.h new file mode 100644 index 0000000..0b60846 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multisource/image/bbb.h @@ -0,0 +1,13 @@ +#include +#include + +class bbb : public QObject +{ + Q_OBJECT + +public: + bbb(); + +private: + QString my_s; +}; diff --git a/workspace/sconstools3/qt5/test/ts_qm/multisource/sconstest-multisource.py b/workspace/sconstools3/qt5/test/ts_qm/multisource/sconstest-multisource.py new file mode 100644 index 0000000..5b66c9a --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multisource/sconstest-multisource.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Tests that the Ts() and Qm() builders accept and +process multiple sources correctly. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(stderr=None) + +test.must_exist(test.workpath('my_en.ts')) +test.must_exist(test.workpath('a.ts')) +test.must_exist(test.workpath('b.ts')) +test.must_contain(test.workpath('my_en.ts'),'SCons rocks!') +test.must_contain(test.workpath('my_en.ts'),'And Qt5 too!') +test.must_contain(test.workpath('a.ts'),'SCons rocks!') +test.must_contain(test.workpath('b.ts'),'And Qt5 too!') +test.must_exist(test.workpath('my_en.qm')) + +test.run(options = '-c') + +test.must_exist(test.workpath('my_en.ts')) +test.must_exist(test.workpath('a.ts')) +test.must_exist(test.workpath('b.ts')) +test.must_not_exist(test.workpath('my_en.qm')) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.cpp b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.cpp new file mode 100644 index 0000000..09e27c6 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.cpp @@ -0,0 +1,7 @@ +#include "MyFile.h" + +aaa::aaa() : my_s(tr("SCons rocks!")) +{ + ; +} + diff --git a/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.h b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.h new file mode 100644 index 0000000..10311dd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/MyFile.h @@ -0,0 +1,13 @@ +#include +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa(); + +private: + QString my_s; +}; diff --git a/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConscript b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConscript new file mode 100644 index 0000000..0d6a666 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConscript @@ -0,0 +1,4 @@ +Import("qtEnv") + +qtEnv.Ts5(['my_en','my_de'], Glob('*.cpp')) +qtEnv.Qm5(['my_en','my_de'],'my_en') diff --git a/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConstruct b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multitarget/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/ts_qm/multitarget/sconstest-multitarget.py b/workspace/sconstools3/qt5/test/ts_qm/multitarget/sconstest-multitarget.py new file mode 100644 index 0000000..2a900fe --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/multitarget/sconstest-multitarget.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Tests that the Ts() and Qm() builders accept and +process multiple targets correctly. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(stderr=None) + +test.must_exist(test.workpath('my_en.ts')) +test.must_exist(test.workpath('my_de.ts')) +test.must_contain(test.workpath('my_en.ts'),'SCons rocks!') +test.must_contain(test.workpath('my_de.ts'),'SCons rocks!') +test.must_exist(test.workpath('my_en.qm')) +test.must_exist(test.workpath('my_de.qm')) + +test.run(options = '-c') + +test.must_exist(test.workpath('my_en.ts')) +test.must_exist(test.workpath('my_de.ts')) +test.must_not_exist(test.workpath('my_en.qm')) +test.must_not_exist(test.workpath('my_de.qm')) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.cpp b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.cpp new file mode 100644 index 0000000..09e27c6 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.cpp @@ -0,0 +1,7 @@ +#include "MyFile.h" + +aaa::aaa() : my_s(tr("SCons rocks!")) +{ + ; +} + diff --git a/workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.h b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.h new file mode 100644 index 0000000..10311dd --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/MyFile.h @@ -0,0 +1,13 @@ +#include +#include + +class aaa : public QObject +{ + Q_OBJECT + +public: + aaa(); + +private: + QString my_s; +}; diff --git a/workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConscript b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConscript new file mode 100644 index 0000000..932ec0f --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConscript @@ -0,0 +1,4 @@ +Import("qtEnv") + +qtEnv.Ts5('my_en.ts', Glob('*.cpp')) +qtEnv.Qm5('my_en','my_en') diff --git a/workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConstruct b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConstruct new file mode 100644 index 0000000..d9d897d --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/noclean/image/SConstruct @@ -0,0 +1,6 @@ +import qtenv + +qtEnv = qtenv.createQtEnvironment() +Export('qtEnv') +SConscript('SConscript') + diff --git a/workspace/sconstools3/qt5/test/ts_qm/noclean/sconstest-noclean.py b/workspace/sconstools3/qt5/test/ts_qm/noclean/sconstest-noclean.py new file mode 100644 index 0000000..69320b2 --- /dev/null +++ b/workspace/sconstools3/qt5/test/ts_qm/noclean/sconstest-noclean.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# Copyright (c) 2001-2010,2011,2012 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +""" +Tests that .ts files are not removed by default, +on a 'scons -c'. +""" + +import TestSCons + +test = TestSCons.TestSCons() +test.dir_fixture("image") +test.file_fixture('../../qtenv.py') +test.file_fixture('../../../__init__.py','site_scons/site_tools/qt5/__init__.py') +test.run(stderr=None) + +test.must_exist(test.workpath('my_en.ts')) +test.must_contain(test.workpath('my_en.ts'),'SCons rocks!') +test.must_exist(test.workpath('my_en.qm')) + +test.run(options = '-c') + +test.must_exist(test.workpath('my_en.ts')) +test.must_not_exist(test.workpath('my_en.qm')) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: From 26e0c45e6c12c3fefe4b3509fe669b64172501cf Mon Sep 17 00:00:00 2001 From: cwiede <62332054+cwiede@users.noreply.github.com> Date: Sat, 21 Mar 2020 16:03:20 +0100 Subject: [PATCH 05/16] build is working now --- .gitignore | 7 + nexxT/tests/core/test_except_constr.json | 60 +++ workspace/BUILDING.txt | 34 ++ workspace/SConstruct | 6 +- workspace/pylint.rc | 582 +++++++++++++++++++++++ workspace/pylint_nexxT.bat | 2 + workspace/pylint_nexxT.sh | 3 + workspace/pytest_nexxT.bat | 11 + workspace/pytest_nexxT.sh | 5 + workspace/requirements.txt | 8 + 10 files changed, 716 insertions(+), 2 deletions(-) create mode 100644 nexxT/tests/core/test_except_constr.json create mode 100644 workspace/BUILDING.txt create mode 100644 workspace/pylint.rc create mode 100644 workspace/pylint_nexxT.bat create mode 100644 workspace/pylint_nexxT.sh create mode 100644 workspace/pytest_nexxT.bat create mode 100644 workspace/pytest_nexxT.sh create mode 100644 workspace/requirements.txt diff --git a/.gitignore b/.gitignore index 1e3795a..60a3be2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,13 @@ include/ .sconsign.dblite workspace/build +# files generated during tests +nexxT/tests/core/test1.saved.json +nexxT/tests/core/test1.saved.json.guistate +nexxT/tests/core/test_except_constr_tmp.json +nexxT/tests/core/test_except_constr_tmp.json.guistate + + # Distribution / packaging .Python build/ diff --git a/nexxT/tests/core/test_except_constr.json b/nexxT/tests/core/test_except_constr.json new file mode 100644 index 0000000..2a188ad --- /dev/null +++ b/nexxT/tests/core/test_except_constr.json @@ -0,0 +1,60 @@ +{ + "composite_filters": [ + { + "name": "subGraph", + "nodes": [ + { + "name":"CompositeInput", + "library":"composite://port", + "factoryFunction": "CompositeInput", + "dynamicOutputPorts": ["graph_in"] + }, + { + "name":"CompositeOutput", + "library":"composite://port", + "factoryFunction": "CompositeOutput", + "dynamicInputPorts": [] + }, + { + "name": "filter", + "library": "pyfile://../interface/TestExceptionFilter.py", + "factoryFunction": "TestExceptionFilter", + "thread": "main", + "properties": { + "whereToThrow": "nowhere" + } + } + ], + "connections": [ + "CompositeInput.graph_in -> filter.port" + ] + } + ], + "applications": [ + { + "name": "testApp", + "nodes": [ + { + "name": "source", + "library": "pyfile://../interface/SimpleStaticFilter.py", + "factoryFunction": "SimpleSource", + "thread": "thread-source", + "staticOutputPorts": [ + "outPort" + ], + "properties": { + "frequency": 2 + } + }, + { + "name": "filter", + "library": "composite://ref", + "factoryFunction": "subGraph" + } + ], + "connections": [ + "source.outPort -> filter.graph_in" + ] + } + ] +} diff --git a/workspace/BUILDING.txt b/workspace/BUILDING.txt new file mode 100644 index 0000000..c32a4ef --- /dev/null +++ b/workspace/BUILDING.txt @@ -0,0 +1,34 @@ +It's best to set up a new virtual environment for nexxT builds and development: + + python3 -m venv venv + +It is assumed that this environment is activated for the following commands. + +Build dependencies: +- QT5: set QTDIR for identifying the QT version +- shiboken2_generator: this is referenced in requirements.txt, but you have to + build or download this manually. The easiest option is to download it from + downloaded from + https://download.qt.io/official_releases/QtForPython/shiboken2-generator/ + This is working for windows and debian builds and probably also for more, + even though the PySide2 project doesn't recommend to use the binary + distribution + +You can use pip to install the required dependencies like that + + pip install -r requirements.txt --find-links /path/of/shiboken2_generator + +Afterwards you should be able to use "scons -j 8 .." to build and install nexxT +binaries and c extensions. + +Note that the project uses pylint for coding style checks. Use + + pylint_nexxT[.bat|.sh] + +for generating a report. + +For running tests, use + + pytest_nexxT[.bat|.sh] + +scripts. \ No newline at end of file diff --git a/workspace/SConstruct b/workspace/SConstruct index b76b1ce..3ce6e48 100644 --- a/workspace/SConstruct +++ b/workspace/SConstruct @@ -36,5 +36,7 @@ else: LINKFLAGS=Split("/nologo /DEBUG"), variant="release") -SConscript('../nexxT/src/SConscript.py', variant_dir="build/nonopt", exports=dict(env=dbg_env), duplicate=0) -SConscript('../nexxT/src/SConscript.py', variant_dir="build/release", exports=dict(env=rel_env), duplicate=0) +SConscript('../nexxT/src/SConscript.py', variant_dir=dbg_env.subst("build/${target_platform}_${variant}/nexxT/src"), exports=dict(env=dbg_env), duplicate=0) +SConscript('../nexxT/src/SConscript.py', variant_dir=rel_env.subst("build/${target_platform}_${variant}/nexxT/src"), exports=dict(env=rel_env), duplicate=0) +SConscript('../nexxT/tests/src/SConscript.py', variant_dir=dbg_env.subst("build/${target_platform}_${variant}/nexxT/tests/src"), exports=dict(env=dbg_env), duplicate=0) +SConscript('../nexxT/tests/src/SConscript.py', variant_dir=rel_env.subst("build/${target_platform}_${variant}/nexxT/tests/src"), exports=dict(env=rel_env), duplicate=0) diff --git a/workspace/pylint.rc b/workspace/pylint.rc new file mode 100644 index 0000000..1f30666 --- /dev/null +++ b/workspace/pylint.rc @@ -0,0 +1,582 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist=PySide2 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=camelCase + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=camelCase + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=camelCase + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=camelCase + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=PascalCase + +# Regular expression matching correct module names. Overrides module-naming- +# style. +module-rgx=([A-Z_][a-zA-Z0-9]+)|(core)|(interface)$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=camelCase + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +variable-rgx=[a-z_][a-zA-Z0-9_]{0,30}$ + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +# CW: increased from 5 to 8 +max-args=8 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +# CW: there are too many cases where this may be wanted; set to 0 (from 2) +min-public-methods=0 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/workspace/pylint_nexxT.bat b/workspace/pylint_nexxT.bat new file mode 100644 index 0000000..4a93597 --- /dev/null +++ b/workspace/pylint_nexxT.bat @@ -0,0 +1,2 @@ +set NEXT_DISABLE_CIMPL=1 +pylint --rcfile=pylint.rc nexxT.core nexxT.interface nexxT.services diff --git a/workspace/pylint_nexxT.sh b/workspace/pylint_nexxT.sh new file mode 100644 index 0000000..2eaf575 --- /dev/null +++ b/workspace/pylint_nexxT.sh @@ -0,0 +1,3 @@ +#!/bin/sh +export NEXT_DISABLE_CIMPL=1 +pylint --rcfile=pylint.rc nexxT.core nexxT.interface nexxT.services diff --git a/workspace/pytest_nexxT.bat b/workspace/pytest_nexxT.bat new file mode 100644 index 0000000..f00dcee --- /dev/null +++ b/workspace/pytest_nexxT.bat @@ -0,0 +1,11 @@ +CALL scons .. || exit /b 1 +set NEXT_CEXT_PATH=%cd%\build\msvc_x86_64_nonopt\nexxT\src +set NEXT_CPLUGIN_PATH=%cd%\build\msvc_x86_64_nonopt\nexxT\tests\src +pytest --cov=nexxT.core --cov=nexxT.interface --cov-report html ../nexxT/tests || exit /b 1 + +set NEXT_CEXT_PATH=%cd%\build\msvc_x86_64_release\nexxT\src +set NEXT_CPLUGIN_PATH=%cd%\build\msvc_x86_64_release\nexxT\tests\src +pytest --cov-append --cov=nexxT.core --cov=nexxT.interface --cov-report html ../nexxT/tests || exit /b 1 + +set NEXT_DISABLE_CIMPL=1 +pytest --cov-append --cov=nexxT.core --cov=nexxT.interface --cov-report html ../nexxT/tests || exit /b 1 diff --git a/workspace/pytest_nexxT.sh b/workspace/pytest_nexxT.sh new file mode 100644 index 0000000..d857fb6 --- /dev/null +++ b/workspace/pytest_nexxT.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +pytest --cov=nexT.core --cov=nexT.interface --cov-report html ../nexxT/tests +NEXT_DISABLE_CIMPL=1 pytest --cov-append --cov=nexT.core --cov=nexT.interface --cov-report html ../nexT/tests + diff --git a/workspace/requirements.txt b/workspace/requirements.txt new file mode 100644 index 0000000..363ac08 --- /dev/null +++ b/workspace/requirements.txt @@ -0,0 +1,8 @@ +PySide2==5.14.1 +scons==3.1.2 +shiboken2==5.14.1 +shiboken2-generator==5.14.1 +pytest==4.3.0 +pytest-cov==2.8.1 +pylint==2.4.4 +-e .. From d10ed9e20362629e8e0aeef63d166623e7514684 Mon Sep 17 00:00:00 2001 From: cwiede <62332054+cwiede@users.noreply.github.com> Date: Sun, 22 Mar 2020 13:15:56 +0100 Subject: [PATCH 06/16] introduce new filter state OPENED between INITIALIZED and ACTIVE - rename left over "next" spellings with "nexxt" - fix exception handling in receiveAsync slot and adapt changed behaviour to receiveSync - introduce new environment variables NEXXT_PLATFORM and NEXXT_VARIANT - fix sorting of drive letters on windows --- .gitignore | 3 +- nexxT/__init__.py | 9 ++-- nexxT/core/ActiveApplication.py | 47 +++++++++++++++++++- nexxT/core/FilterEnvironment.py | 42 +++++++++++++---- nexxT/core/PortImpl.py | 27 ++++++----- nexxT/core/PropertyCollectionImpl.py | 10 ++++- nexxT/core/Thread.py | 8 ++-- nexxT/core/Utils.py | 8 ++++ nexxT/interface/Filters.py | 33 ++++++++++---- nexxT/src/FilterEnvironment.cpp | 2 +- nexxT/src/Filters.cpp | 16 +++++++ nexxT/src/Filters.hpp | 17 ++++--- nexxT/src/Ports.cpp | 26 ++++++++--- nexxT/tests/core/test2.json | 2 +- nexxT/tests/core/test_ActiveApplication.py | 3 ++ nexxT/tests/core/test_CompositeFilter.py | 6 +++ nexxT/tests/core/test_ConfigFiles.py | 3 ++ nexxT/tests/core/test_FilterEnvironment.py | 41 ++++++++++++++--- nexxT/tests/core/test_FilterExceptions.py | 7 ++- nexxT/tests/interface/QImageDisplay.py | 4 +- nexxT/tests/interface/TestExceptionFilter.py | 10 ++++- nexxT/tests/src/AviFilePlayback.cpp | 12 +++-- nexxT/tests/src/AviFilePlayback.hpp | 2 + nexxT/tests/src/SConscript.py | 11 +++-- nexxT/tests/src/TestExceptionFilter.cpp | 16 +++++++ nexxT/tests/src/TestExceptionFilter.hpp | 2 + workspace/pytest_nexxT.bat | 12 ++--- 27 files changed, 300 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index 60a3be2..9fef41e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,8 @@ workspace/build # files generated during tests nexxT/tests/core/test1.saved.json -nexxT/tests/core/test1.saved.json.guistate nexxT/tests/core/test_except_constr_tmp.json -nexxT/tests/core/test_except_constr_tmp.json.guistate +*.guistate # Distribution / packaging diff --git a/nexxT/__init__.py b/nexxT/__init__.py index c957491..0cfeb97 100644 --- a/nexxT/__init__.py +++ b/nexxT/__init__.py @@ -30,15 +30,16 @@ def internal(self, message, *args, **kws): logger.setLevel(logging.INFO) global useCImpl - useCImpl = not bool(int(os.environ.get("NEXT_DISABLE_CIMPL", "0"))) + useCImpl = not bool(int(os.environ.get("NEXXT_DISABLE_CIMPL", "0"))) if useCImpl: # make sure to import PySide2 before loading the cnexxT extension module because # there is a link-time dependency which would be impossible to resolve otherwise import PySide2.QtCore - p = os.environ.get("NEXT_CEXT_PATH", None) + p = os.environ.get("NEXXT_CEXT_PATH", None) if p is None: - p = [p for p in [Path(__file__).parent / "binary" / "msvc_x86_64" / "release", - Path(__file__).parent / "binary" / "linux_x86_64" / "release"] if p.exists()] + variant = os.environ.get("NEXXT_VARIANT", "release") + p = [p for p in [Path(__file__).parent / "binary" / "msvc_x86_64" / variant, + Path(__file__).parent / "binary" / "linux_x86_64" / variant] if p.exists()] if len(p) > 0: p = p[0].absolute() else: diff --git a/nexxT/core/ActiveApplication.py b/nexxT/core/ActiveApplication.py index b54aedf..72e188a 100644 --- a/nexxT/core/ActiveApplication.py +++ b/nexxT/core/ActiveApplication.py @@ -261,9 +261,13 @@ def _operationFinished(self): self._state = FilterState.CONSTRUCTED elif self._state == FilterState.INITIALIZING: self._state = FilterState.INITIALIZED + elif self._state == FilterState.OPENING: + self._state = FilterState.OPENED elif self._state == FilterState.STARTING: self._state = FilterState.ACTIVE elif self._state == FilterState.STOPPING: + self._state = FilterState.OPENED + elif self._state == FilterState.CLOSING: self._state = FilterState.INITIALIZED elif self._state == FilterState.DEINITIALIZING: self._state = FilterState.CONSTRUCTED @@ -309,6 +313,26 @@ def init(self): self._operationInProgress = False logger.internal("leaving operation done, new state %s", FilterState.state2str(self._state)) + @Slot() + def open(self): + """ + Perform open operation + :return: None + """ + logger.internal("entering setup operation, old state %s", FilterState.state2str(self._state)) + assertMainThread() + while self._operationInProgress and self._state != FilterState.INITIALIZED: + QCoreApplication.processEvents() + if self._state != FilterState.INITIALIZED: + raise FilterStateMachineError(self._state, FilterState.OPENING) + self._operationInProgress = True + self._state = FilterState.OPENING + self.performOperation.emit("open", Barrier(len(self._threads))) + while self._state == FilterState.OPENING: + QCoreApplication.processEvents() + self._operationInProgress = False + logger.internal("leaving operation done, new state %s", FilterState.state2str(self._state)) + @Slot() def start(self): """ @@ -317,9 +341,9 @@ def start(self): """ logger.internal("entering start operation, old state %s", FilterState.state2str(self._state)) assertMainThread() - while self._operationInProgress and self._state != FilterState.INITIALIZED: + while self._operationInProgress and self._state != FilterState.OPENED: QCoreApplication.processEvents() - if self._state != FilterState.INITIALIZED: + if self._state != FilterState.OPENED: raise FilterStateMachineError(self._state, FilterState.STARTING) self._operationInProgress = True self._state = FilterState.STARTING @@ -351,6 +375,25 @@ def stop(self): self._operationInProgress = False logger.internal("leaving stop operation, new state %s", FilterState.state2str(self._state)) + @Slot() + def close(self): + """ + Perform close operation + :return: None + """ + logger.internal("entering close operation, old state %s", FilterState.state2str(self._state)) + assertMainThread() + while self._operationInProgress and self._state != FilterState.OPENED: + QCoreApplication.processEvents() + if self._state != FilterState.OPENED: + raise FilterStateMachineError(self._state, FilterState.CLOSING) + self._operationInProgress = True + self._state = FilterState.CLOSING + self.performOperation.emit("close", Barrier(len(self._threads))) + while self._state == FilterState.CLOSING: + QCoreApplication.processEvents() + self._operationInProgress = False + logger.internal("leaving operation done, new state %s", FilterState.state2str(self._state)) @Slot() def deinit(self): diff --git a/nexxT/core/FilterEnvironment.py b/nexxT/core/FilterEnvironment.py index 5551cdd..be74902 100644 --- a/nexxT/core/FilterEnvironment.py +++ b/nexxT/core/FilterEnvironment.py @@ -116,7 +116,7 @@ def portDataChanged(self, inputPort): """ self._assertMyThread() if self._state != FilterState.ACTIVE: - if self._state != FilterState.INITIALIZED: + if self._state != FilterState.OPENED: raise UnexpectedFilterState(self._state, "portDataChanged") logger.info("DataSample discarded because application has been stopped already.") return @@ -159,7 +159,7 @@ def getMockup(self): """ return self._mockup - def close(self): + def destroy(self): """ Deinitialize filter if necessary. :return: None @@ -167,6 +167,8 @@ def close(self): if not (self.getPlugin() is None or (useCImpl and self.getPlugin().data() is None)): if self._state == FilterState.ACTIVE: self.stop() + if self._state == FilterState.OPENED: + self.close() if self._state == FilterState.INITIALIZED: self.deinit() if not self._state in [FilterState.CONSTRUCTED, FilterState.DESTRUCTING]: @@ -178,7 +180,7 @@ def __enter__(self): return self def __exit__(self, *args): #exctype, value, traceback - self.close() + self.destroy() def guiState(self): """ @@ -339,14 +341,17 @@ def preStateTransition(self, operation): :param operation: The FilterState operation. :return: None """ + logger.internal("Performing pre-state transition: %s", FilterState.state2str(operation)) self._assertMyThread() if self.getPlugin() is None or (useCImpl and self.getPlugin().data() is None): raise NexTInternalError("Cannot perform state transitions on uninitialized plugin") operations = { FilterState.CONSTRUCTING: (None, FilterState.CONSTRUCTED, None), FilterState.INITIALIZING: (FilterState.CONSTRUCTED, FilterState.INITIALIZED, self.getPlugin().onInit), - FilterState.STARTING: (FilterState.INITIALIZED, FilterState.ACTIVE, self.getPlugin().onStart), - FilterState.STOPPING: (FilterState.ACTIVE, FilterState.INITIALIZED, self.getPlugin().onStop), + FilterState.OPENING: (FilterState.INITIALIZED, FilterState.OPENED, self.getPlugin().onOpen), + FilterState.STARTING: (FilterState.OPENED, FilterState.ACTIVE, self.getPlugin().onStart), + FilterState.STOPPING: (FilterState.ACTIVE, FilterState.OPENED, self.getPlugin().onStop), + FilterState.CLOSING: (FilterState.OPENED, FilterState.INITIALIZED, self.getPlugin().onClose), FilterState.DEINITIALIZING: (FilterState.INITIALIZED, FilterState.CONSTRUCTED, self.getPlugin().onDeinit), FilterState.DESTRUCTING: (FilterState.CONSTRUCTED, None, None), } @@ -361,14 +366,17 @@ def _stateTransition(self, operation): :param operation: The FilterState operation. :return: None """ + logger.internal("Performing state transition: %s", FilterState.state2str(operation)) self._assertMyThread() if self.getPlugin() is None or (useCImpl and self.getPlugin().data() is None): raise NexTInternalError("Cannot perform state transitions on uninitialized plugin") operations = { FilterState.CONSTRUCTING: (None, FilterState.CONSTRUCTED, None), FilterState.INITIALIZING: (FilterState.CONSTRUCTED, FilterState.INITIALIZED, self.getPlugin().onInit), - FilterState.STARTING: (FilterState.INITIALIZED, FilterState.ACTIVE, self.getPlugin().onStart), - FilterState.STOPPING: (FilterState.ACTIVE, FilterState.INITIALIZED, self.getPlugin().onStop), + FilterState.OPENING: (FilterState.INITIALIZED, FilterState.OPENED, self.getPlugin().onOpen), + FilterState.STARTING: (FilterState.OPENED, FilterState.ACTIVE, self.getPlugin().onStart), + FilterState.STOPPING: (FilterState.ACTIVE, FilterState.OPENED, self.getPlugin().onStop), + FilterState.CLOSING: (FilterState.OPENED, FilterState.INITIALIZED, self.getPlugin().onClose), FilterState.DEINITIALIZING: (FilterState.INITIALIZED, FilterState.CONSTRUCTED, self.getPlugin().onDeinit), FilterState.DESTRUCTING: (FilterState.CONSTRUCTED, None, None), } @@ -404,9 +412,17 @@ def init(self): self._assertMyThread() self._stateTransition(FilterState.INITIALIZING) + def open(self): + """ + Perform filter opening (state transition INITIALIZED -> OPENING -> OPENED + :return: + """ + self._assertMyThread() + self._stateTransition(FilterState.OPENING) + def start(self): """ - Perform filter start (state transition INITIALIZED -> STARTING -> ACTIVE) + Perform filter start (state transition OPENED -> STARTING -> ACTIVE) :return: None """ self._assertMyThread() @@ -414,12 +430,20 @@ def start(self): def stop(self): """ - Perform filter stop (state transition ACTIVE -> STOPPING -> INITIALIZED) + Perform filter stop (state transition ACTIVE -> STOPPING -> OPENED) :return: None """ self._assertMyThread() self._stateTransition(FilterState.STOPPING) + def close(self): + """ + Perform filter stop (state transition OPENED -> CLOSING -> INITIALIZED) + :return: None + """ + self._assertMyThread() + self._stateTransition(FilterState.CLOSING) + def deinit(self): """ Perform filter deinitialization (state transition INITIALIZED -> DEINITIALIZING -> CONSTRUCTED) diff --git a/nexxT/core/PortImpl.py b/nexxT/core/PortImpl.py index 1ca964c..b5bd199 100644 --- a/nexxT/core/PortImpl.py +++ b/nexxT/core/PortImpl.py @@ -9,6 +9,7 @@ """ import logging +import sys from PySide2.QtCore import QThread, QSemaphore, Signal, QObject, Qt from nexxT.interface.Ports import InputPortInterface, OutputPortInterface from nexxT.interface.DataSamples import DataSample @@ -145,26 +146,32 @@ def _addToQueue(self, dataSample): def receiveAsync(self, dataSample, semaphore): """ - Called from framework only and implements the asynchronous receive mechanism using a semaphore. TODO implement + Called from framework only and implements the asynchronous receive mechanism using a semaphore. :param dataSample: the transmitted DataSample instance :param semaphore: a QSemaphore instance :return: None """ - if not QThread.currentThread() is self.thread(): - raise NexTInternalError("InputPort.receiveAsync has been called from an unexpected thread.") - semaphore.release(1) - self._addToQueue(dataSample) + try: + if not QThread.currentThread() is self.thread(): + raise NexTInternalError("InputPort.receiveAsync has been called from an unexpected thread.") + semaphore.release(1) + self._addToQueue(dataSample) + except Exception: + sys.excepthook(*sys.exc_info()) def receiveSync(self, dataSample): """ - Called from framework only and implements the synchronous receive mechanism. TODO implement + Called from framework only and implements the synchronous receive mechanism. :param dataSample: the transmitted DataSample instance :return: None """ - if not QThread.currentThread() is self.thread(): - raise NexTInternalError("InputPort.receiveSync has been called from an unexpected thread.") - self._addToQueue(dataSample) - + try: + if not QThread.currentThread() is self.thread(): + raise NexTInternalError("InputPort.receiveSync has been called from an unexpected thread.") + self._addToQueue(dataSample) + except Exception: + sys.excepthook(*sys.exc_info()) + def clone(self, newEnvironment): """ Return a copy of this port attached to a new environment. diff --git a/nexxT/core/PropertyCollectionImpl.py b/nexxT/core/PropertyCollectionImpl.py index f1575d5..91c51f3 100644 --- a/nexxT/core/PropertyCollectionImpl.py +++ b/nexxT/core/PropertyCollectionImpl.py @@ -11,6 +11,7 @@ from collections import OrderedDict from pathlib import Path import logging +import platform import string import os import shiboken2 @@ -362,7 +363,14 @@ def evalpath(self, path): while root_prop.parent() is not None: root_prop = root_prop.parent() # substitute ${VAR} with environment variables - path = string.Template(path).safe_substitute(os.environ) + default_environ = dict( + NEXXT_VARIANT="release" + ) + if platform.system() == "Windows": + default_environ["NEXXT_PLATFORM"] = "msvc_x86%s" % ("_64" if platform.architecture()[0] == "64bit" else "") + else: + default_environ["NEXXT_PLATFORM"] = "linux_x86%s" % ("_64" if platform.architecture()[0] == "64bit" else "") + path = string.Template(path).safe_substitute({**default_environ, **os.environ}) if Path(path).is_absolute(): return path try: diff --git a/nexxT/core/Thread.py b/nexxT/core/Thread.py index 0f04775..e0b2b8b 100644 --- a/nexxT/core/Thread.py +++ b/nexxT/core/Thread.py @@ -23,8 +23,10 @@ class NexTThread(QObject): _operations = dict( init=FilterState.INITIALIZING, + open=FilterState.OPENING, start=FilterState.STARTING, stop=FilterState.STOPPING, + close=FilterState.CLOSING, deinit=FilterState.DEINITIALIZING, ) @@ -70,7 +72,7 @@ def cleanup(self): self._qthread = None logger.internal("cleanup filters") for name in self._filters: - self._filters[name].close() + self._filters[name].destroy() self._filters.clear() self._filter2name.clear() logger.internal("cleanup mockups") @@ -120,7 +122,7 @@ def qthread(self): def performOperation(self, operation, barrier): """ Perform the given operation on all filters. - :param operation: one of "create", "destruct", "init", "start", "stop", "deinit" + :param operation: one of "create", "destruct", "init", "open", "start", "stop", "close", "deinit" :param barrier: a barrier object to synchronize threads :return: None """ @@ -143,7 +145,7 @@ def performOperation(self, operation, barrier): self._filter2name[res] = name logger.internal("Created filter %s in thread %s", name, self._name) elif operation == "destruct": - self._filters[name].close() + self._filters[name].destroy() logging.getLogger(__name__).internal("deleting filter...") del self._filters[name] logging.getLogger(__name__).internal("filter deleted") diff --git a/nexxT/core/Utils.py b/nexxT/core/Utils.py index 1e4cddc..8b3ffe3 100644 --- a/nexxT/core/Utils.py +++ b/nexxT/core/Utils.py @@ -13,6 +13,7 @@ import sys import logging import datetime +import platform import os.path import sqlite3 from PySide2.QtCore import (QObject, Signal, Slot, QMutex, QWaitCondition, QCoreApplication, QThread, @@ -182,6 +183,13 @@ def lessThan(self, left, right): return not asc if left_fi.isDir() and not right_fi.isDir(): return asc + left_fp = left_fi.filePath() + right_fp = right_fi.filePath() + if (platform.system() == "Windows" and + left_fi.isAbsolute() and len(left_fp) == 3 and left_fp[1:] == ":/" and + right_fi.isAbsolute() and len(right_fp) == 3 and right_fp[1:] == ":/"): + res = (asc and left_fp < right_fp) or ((not asc) and right_fp < left_fp) + return res return QSortFilterProxyModel.lessThan(self, left, right) class QByteArrayBuffer(io.IOBase): diff --git a/nexxT/interface/Filters.py b/nexxT/interface/Filters.py index 6f575fd..d44cad0 100644 --- a/nexxT/interface/Filters.py +++ b/nexxT/interface/Filters.py @@ -18,12 +18,15 @@ class FilterState: CONSTRUCTED = 1 INITIALIZING = 2 INITIALIZED = 3 - STARTING = 4 - ACTIVE = 5 - STOPPING = 6 - DEINITIALIZING = 7 - DESTRUCTING = 8 - DESTRUCTED = 9 + OPENING = 4 + OPENED = 5 + STARTING = 6 + ACTIVE = 7 + STOPPING = 8 + CLOSING = 9 + DEINITIALIZING = 10 + DESTRUCTING = 11 + DESTRUCTED = 12 @staticmethod def state2str(state): @@ -130,10 +133,16 @@ def onInit(self): :return: None """ - def onStart(self): + def onOpen(self): """ This function can be overwritten for general initialization tasks (e.g. acquire resources needed to - run the filter, open files, etc.). + run the filter, open files, connecting to services etc.). + :return: + """ + + def onStart(self): + """ + This function can be overwritten to reset internal filter state. It is called before loading a new sequence. :return: None """ @@ -146,9 +155,15 @@ def onPortDataChanged(self, inputPort): """ def onStop(self): + """ + Opposite of onStart. + :return: None + """ + + def onClose(self): """ This function can be overwritten for general de-initialization tasks (e.g. release resources needed to - run the filter, close files, etc.). It is the opoosite to onStart(...). + run the filter, close files, etc.). It is the opoosite to onOpen(...). :return: None """ diff --git a/nexxT/src/FilterEnvironment.cpp b/nexxT/src/FilterEnvironment.cpp index df76693..39745ec 100644 --- a/nexxT/src/FilterEnvironment.cpp +++ b/nexxT/src/FilterEnvironment.cpp @@ -89,7 +89,7 @@ void BaseFilterEnvironment::portDataChanged(const InputPortInterface &port) assertMyThread(); if( state() != FilterState::ACTIVE ) { - if( state() != FilterState::INITIALIZED ) + if( state() != FilterState::OPENED ) { throw std::runtime_error(QString("Unexpected filter state %1, expected ACTIVE or INITIALIZED.").arg(FilterState::state2str(state())).toStdString()); } diff --git a/nexxT/src/Filters.cpp b/nexxT/src/Filters.cpp index e7f1409..7ba2e9c 100644 --- a/nexxT/src/Filters.cpp +++ b/nexxT/src/Filters.cpp @@ -17,9 +17,12 @@ const int FilterState::CONSTRUCTING; const int FilterState::CONSTRUCTED; const int FilterState::INITIALIZING; const int FilterState::INITIALIZED; +const int FilterState::OPENING; +const int FilterState::OPENED; const int FilterState::STARTING; const int FilterState::ACTIVE; const int FilterState::STOPPING; +const int FilterState::CLOSING; const int FilterState::DEINITIALIZING; const int FilterState::DESTRUCTING; const int FilterState::DESTRUCTED; @@ -32,9 +35,12 @@ QString FilterState::state2str(int state) case FilterState::CONSTRUCTED: return "CONSTRUCTED"; case FilterState::INITIALIZING: return "INITIALIZING"; case FilterState::INITIALIZED: return "INITIALIZED"; + case FilterState::OPENING: return "OPENING"; + case FilterState::OPENED: return "OPENED"; case FilterState::STARTING: return "STARTING"; case FilterState::ACTIVE: return "ACTIVE"; case FilterState::STOPPING: return "STOPPING"; + case FilterState::CLOSING: return "CLOSING"; case FilterState::DEINITIALIZING: return "DEINITIALIZING"; case FilterState::DESTRUCTING: return "DESTRUCTING"; case FilterState::DESTRUCTED: return "DESTRUCTED"; @@ -106,6 +112,11 @@ void Filter::onInit() /* intentionally empty */ } +void Filter::onOpen() +{ + /* intentionally empty */ +} + void Filter::onStart() { /* intentionally empty */ @@ -121,6 +132,11 @@ void Filter::onStop() /* intentionally empty */ } +void Filter::onClose() +{ + /* intentionally empty */ +} + void Filter::onDeinit() { /* intentionally empty */ diff --git a/nexxT/src/Filters.hpp b/nexxT/src/Filters.hpp index a7cff56..2d201c1 100644 --- a/nexxT/src/Filters.hpp +++ b/nexxT/src/Filters.hpp @@ -25,12 +25,15 @@ START_NAMESPACE static const int CONSTRUCTED = 1; static const int INITIALIZING = 2; static const int INITIALIZED = 3; - static const int STARTING = 4; - static const int ACTIVE = 5; - static const int STOPPING = 6; - static const int DEINITIALIZING = 7; - static const int DESTRUCTING = 8; - static const int DESTRUCTED = 9; + static const int OPENING = 4; + static const int OPENED = 5; + static const int STARTING = 6; + static const int ACTIVE = 7; + static const int STOPPING = 8; + static const int CLOSING = 9; + static const int DEINITIALIZING = 10; + static const int DESTRUCTING = 11; + static const int DESTRUCTED = 12; static QString state2str(int state); }; @@ -61,9 +64,11 @@ START_NAMESPACE public: virtual ~Filter(); virtual void onInit(); + virtual void onOpen(); virtual void onStart(); virtual void onPortDataChanged(const InputPortInterface &inputPort); virtual void onStop(); + virtual void onClose(); virtual void onDeinit(); BaseFilterEnvironment *environment() const; diff --git a/nexxT/src/Ports.cpp b/nexxT/src/Ports.cpp index df7d931..b768d51 100644 --- a/nexxT/src/Ports.cpp +++ b/nexxT/src/Ports.cpp @@ -208,21 +208,33 @@ void InputPortInterface::addToQueue(const SharedDataSamplePtr &sample) void InputPortInterface::receiveAsync(const QSharedPointer &sample, QSemaphore *semaphore) { - if( QThread::currentThread() != thread() ) + try { - throw std::runtime_error("InputPort.getData has been called from an unexpected thread."); + if( QThread::currentThread() != thread() ) + { + throw std::runtime_error("InputPort.getData has been called from an unexpected thread."); + } + semaphore->release(1); + addToQueue(sample); + } catch(std::exception &e) + { + NEXT_LOG_ERROR(QString("Unhandled exception in port data changed: %1").arg(e.what())); } - semaphore->release(1); - addToQueue(sample); } void InputPortInterface::receiveSync (const QSharedPointer &sample) { - if( QThread::currentThread() != thread() ) + try { - throw std::runtime_error("InputPort.getData has been called from an unexpected thread."); + if( QThread::currentThread() != thread() ) + { + throw std::runtime_error("InputPort.getData has been called from an unexpected thread."); + } + addToQueue(sample); + } catch(std::exception &e) + { + NEXT_LOG_ERROR(QString("Unhandled exception in port data changed: %1").arg(e.what())); } - addToQueue(sample); } InterThreadConnection::InterThreadConnection(QThread *from_thread) diff --git a/nexxT/tests/core/test2.json b/nexxT/tests/core/test2.json index f74e84c..9f81995 100644 --- a/nexxT/tests/core/test2.json +++ b/nexxT/tests/core/test2.json @@ -34,7 +34,7 @@ "nodes": [ { "name": "source", - "library": "binary://$NEXT_CPLUGIN_PATH/test_plugins", + "library": "binary://../binary/${NEXXT_PLATFORM}/${NEXXT_VARIANT}/test_plugins", "factoryFunction": "SimpleSource", "thread": "thread-source", "staticOutputPorts": [ diff --git a/nexxT/tests/core/test_ActiveApplication.py b/nexxT/tests/core/test_ActiveApplication.py index 38b8b8c..30123c7 100644 --- a/nexxT/tests/core/test_ActiveApplication.py +++ b/nexxT/tests/core/test_ActiveApplication.py @@ -63,6 +63,7 @@ def timeout(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(0) @@ -73,6 +74,7 @@ def timeout2(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(1) @@ -104,6 +106,7 @@ def state_changed(state): f2.afterTransmit = lambda: logger(object="SimpleStaticFilter", function="afterTransmit", datasample=None) aa.init() + aa.open() aa.start() app.exec_() diff --git a/nexxT/tests/core/test_CompositeFilter.py b/nexxT/tests/core/test_CompositeFilter.py index fe6c69d..b79bf88 100644 --- a/nexxT/tests/core/test_CompositeFilter.py +++ b/nexxT/tests/core/test_CompositeFilter.py @@ -93,6 +93,7 @@ def timeout(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(0) @@ -103,6 +104,7 @@ def timeout2(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(1) @@ -134,6 +136,7 @@ def state_changed(state): f2.afterTransmit = lambda: logger(object="SimpleStaticFilter", function="afterTransmit", datasample=None) aa.init() + aa.open() aa.start() app.exec_() @@ -251,6 +254,7 @@ def timeout(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(0) @@ -261,6 +265,7 @@ def timeout2(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(1) @@ -292,6 +297,7 @@ def state_changed(state): f2.afterTransmit = lambda: logger(object="SimpleStaticFilter", function="afterTransmit", datasample=None) aa.init() + aa.open() aa.start() app.exec_() diff --git a/nexxT/tests/core/test_ConfigFiles.py b/nexxT/tests/core/test_ConfigFiles.py index bd7df4c..f434a19 100644 --- a/nexxT/tests/core/test_ConfigFiles.py +++ b/nexxT/tests/core/test_ConfigFiles.py @@ -44,6 +44,7 @@ def timeout(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(0) #logging.INTERNAL = INTERNAL @@ -55,6 +56,7 @@ def timeout2(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(1) @@ -69,6 +71,7 @@ def state_changed(state): aa.stateChanged.connect(state_changed) aa.init() + aa.open() aa.start() app.exec_() diff --git a/nexxT/tests/core/test_FilterEnvironment.py b/nexxT/tests/core/test_FilterEnvironment.py index a85a439..e23cb21 100644 --- a/nexxT/tests/core/test_FilterEnvironment.py +++ b/nexxT/tests/core/test_FilterEnvironment.py @@ -24,7 +24,7 @@ def test_static_filter(): PropertyCollectionImpl("root", None) ) as staticPyFilter: f = staticPyFilter.getPlugin() - origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + origCallbacks = dict(onInit=f.onInit, onOpen=f.onOpen, onStart=f.onStart, onStop=f.onStop, onClose=f.onClose, onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) for callback in origCallbacks: setattr(f, callback, @@ -49,7 +49,9 @@ def exceptionCallback(*args): assert len(function_calls) == 0 expect_exception(staticPyFilter.start) + expect_exception(staticPyFilter.open) expect_exception(staticPyFilter.stop) + expect_exception(staticPyFilter.close) expect_exception(staticPyFilter.deinit) staticPyFilter.init() assert function_calls == [("onInit", None)] @@ -58,6 +60,17 @@ def exceptionCallback(*args): expect_exception(staticPyFilter.init) expect_exception(staticPyFilter.stop) + expect_exception(staticPyFilter.start) + expect_exception(staticPyFilter.close) + staticPyFilter.open() + assert function_calls == [("onOpen", None)] + assert staticPyFilter.state() == FilterState.OPENED + function_calls.clear() + + expect_exception(staticPyFilter.init) + expect_exception(staticPyFilter.open) + expect_exception(staticPyFilter.stop) + expect_exception(staticPyFilter.deinit) staticPyFilter.start() assert function_calls == [("onStart", None)] assert staticPyFilter.state() == FilterState.ACTIVE @@ -65,10 +78,22 @@ def exceptionCallback(*args): assert len(function_calls) == 0 expect_exception(staticPyFilter.init) + expect_exception(staticPyFilter.open) expect_exception(staticPyFilter.start) + expect_exception(staticPyFilter.close) expect_exception(staticPyFilter.deinit) staticPyFilter.stop() assert function_calls == [("onStop", None)] + assert staticPyFilter.state() == FilterState.OPENED + function_calls.clear() + + assert len(function_calls) == 0 + expect_exception(staticPyFilter.init) + expect_exception(staticPyFilter.open) + expect_exception(staticPyFilter.stop) + expect_exception(staticPyFilter.deinit) + staticPyFilter.close() + assert function_calls == [("onClose", None)] assert staticPyFilter.state() == FilterState.INITIALIZED function_calls.clear() @@ -89,7 +114,7 @@ def exceptionCallback(*args): with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter", PropertyCollectionImpl("root", None) ) as staticPyFilter: f = staticPyFilter.getPlugin() - origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + origCallbacks = dict(onInit=f.onInit, onOpen=f.onOpen, onStart=f.onStart, onStop=f.onStop, onClose=f.onClose, onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) for callback in origCallbacks: setattr(f, callback, @@ -100,7 +125,7 @@ def exceptionCallback(*args): with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter", PropertyCollectionImpl("root", None) ) as staticPyFilter: f = staticPyFilter.getPlugin() - origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + origCallbacks = dict(onInit=f.onInit, onOpen=f.onOpen, onStart=f.onStart, onStop=f.onStop, onClose=f.onClose, onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) for callback in origCallbacks: setattr(f, callback, @@ -113,15 +138,17 @@ def exceptionCallback(*args): with FilterEnvironment("pyfile://" + os.path.dirname(__file__) + "/../interface/SimpleStaticFilter.py", "SimpleStaticFilter", PropertyCollectionImpl("root", None) ) as staticPyFilter: f = staticPyFilter.getPlugin() - origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onStop=f.onStop, + origCallbacks = dict(onInit=f.onInit, onStart=f.onStart, onOpen=f.onOpen, onStop=f.onStop, onClose=f.onClose, onDeinit=f.onDeinit, onPortDataChanged=f.onPortDataChanged) for callback in origCallbacks: setattr(f, callback, lambda *args, callback=callback: function_calls.append((callback, origCallbacks[callback](*args)))) staticPyFilter.init() + staticPyFilter.open() staticPyFilter.start() - assert function_calls == [("onInit", None), ("onStart", None), ("onStop", None), ("onDeinit", None)] + assert function_calls == [("onInit", None), ("onOpen", None), ("onStart", None), + ("onStop", None), ("onClose", None), ("onDeinit", None)] function_calls.clear() expect_exception(FilterEnvironment, "weird.plugin.extension", "factory", PropertyCollectionImpl("root", None) ) @@ -154,7 +181,7 @@ def test_dynamic_in_filter(): dip2 = InputPort(True, "dynInPort2", dynInPyFilter) expect_exception(dynInPyFilter.addPort, dip2) - dynInPyFilter.start() + dynInPyFilter.open() expect_exception(dynInPyFilter.addPort, dip2) def test_dynamic_out_filter(): @@ -187,7 +214,7 @@ def test_dynamic_out_filter(): dop2 = OutputPort(True, "dynOutPort2", dynOutPyFilter) expect_exception(dynOutPyFilter.addPort, dop2) - dynOutPyFilter.start() + dynOutPyFilter.open() expect_exception(dynOutPyFilter.addPort, dop2) if __name__ == "__main__": diff --git a/nexxT/tests/core/test_FilterExceptions.py b/nexxT/tests/core/test_FilterExceptions.py index 20f0e27..63472de 100644 --- a/nexxT/tests/core/test_FilterExceptions.py +++ b/nexxT/tests/core/test_FilterExceptions.py @@ -44,7 +44,7 @@ def emit(self, record): with test_json.open("r", encoding='utf-8') as fp: cfg = json.load(fp) if nexxT.useCImpl and not python: - cfg["composite_filters"][0]["nodes"][2]["library"] = "binary://$NEXT_CPLUGIN_PATH/test_plugins" + cfg["composite_filters"][0]["nodes"][2]["library"] = "binary://../binary/${NEXXT_PLATFORM}/${NEXXT_VARIANT}/test_plugins" cfg["composite_filters"][0]["nodes"][2]["thread"] = thread cfg["composite_filters"][0]["nodes"][2]["properties"]["whereToThrow"] = where mod_json = Path(__file__).parent / "test_except_constr_tmp.json" @@ -64,6 +64,7 @@ def timeout(): if init: init = False aa.stop() + aa.close() aa.deinit() else: app.exit(0) @@ -74,6 +75,7 @@ def timeout2(): if init: init = False aa.stop() + aa.close() aa.deinit() else: print("application exit!") @@ -89,6 +91,7 @@ def state_changed(state): aa.stateChanged.connect(state_changed) aa.init() + aa.open() aa.start() app.exec_() @@ -373,3 +376,5 @@ def test_exception_c_compute_constr(): exception = True assert exception +if __name__ == "__main__": + test_exception_c_source_init() \ No newline at end of file diff --git a/nexxT/tests/interface/QImageDisplay.py b/nexxT/tests/interface/QImageDisplay.py index 8d48562..77e65d0 100644 --- a/nexxT/tests/interface/QImageDisplay.py +++ b/nexxT/tests/interface/QImageDisplay.py @@ -22,13 +22,13 @@ def __init__(self, environment): self.propertyCollection().defineProperty("SubplotID", "ImageDisplay", "The parent subplot.") self.lastSize = None - def onStart(self): + def onOpen(self): srv = Services.getService("MainWindow") self.display = QLabel() self.subplotID = self.propertyCollection().getProperty("SubplotID") srv.subplot(self.subplotID, self, self.display) - def onStop(self): + def onClose(self): srv = Services.getService("MainWindow") srv.releaseSubplot(self.subplotID) diff --git a/nexxT/tests/interface/TestExceptionFilter.py b/nexxT/tests/interface/TestExceptionFilter.py index 07fc40d..bfcfa4d 100644 --- a/nexxT/tests/interface/TestExceptionFilter.py +++ b/nexxT/tests/interface/TestExceptionFilter.py @@ -10,7 +10,7 @@ class TestExceptionFilter(Filter): def __init__(self, env): super().__init__(False, False, env) self.propertyCollection().defineProperty("whereToThrow", "nowhere", - "one of nowhere,constructor,init,start,port,stop,deinit") + "one of nowhere,constructor,init,open,start,port,stop,close,deinit") if self.propertyCollection().getProperty("whereToThrow") == "constructor": raise RuntimeError("exception in constructor") self.port = InputPort(False, "port", env) @@ -20,6 +20,10 @@ def onInit(self): if self.propertyCollection().getProperty("whereToThrow") == "init": raise RuntimeError("exception in init") + def onOpen(self): + if self.propertyCollection().getProperty("whereToThrow") == "open": + raise RuntimeError("exception in open") + def onStart(self): if self.propertyCollection().getProperty("whereToThrow") == "start": raise RuntimeError("exception in start") @@ -28,6 +32,10 @@ def onStop(self): if self.propertyCollection().getProperty("whereToThrow") == "stop": raise RuntimeError("exception in stop") + def onClose(self): + if self.propertyCollection().getProperty("whereToThrow") == "close": + raise RuntimeError("exception in close") + def onDeinit(self): if self.propertyCollection().getProperty("whereToThrow") == "deinit": raise RuntimeError("exception in deinit") diff --git a/nexxT/tests/src/AviFilePlayback.cpp b/nexxT/tests/src/AviFilePlayback.cpp index 967273a..9e9cf48 100644 --- a/nexxT/tests/src/AviFilePlayback.cpp +++ b/nexxT/tests/src/AviFilePlayback.cpp @@ -169,7 +169,6 @@ VideoPlaybackDevice::VideoPlaybackDevice(BaseFilterEnvironment *env) : playbackRate = 1.0; video_out = SharedOutputPortPtr(new OutputPortInterface(false, "video_out", env)); addStaticPort(video_out); - propertyCollection()->defineProperty("filename", "", "video file name"); } VideoPlaybackDevice::~VideoPlaybackDevice() @@ -296,7 +295,7 @@ void VideoPlaybackDevice::setTimeFactor(double factor) if(player) player->setPlaybackRate(factor); } -void VideoPlaybackDevice::onStart() +void VideoPlaybackDevice::onOpen() { QStringList filters; filters << "*.avi" << "*.mp4" << "*.wmv"; @@ -306,13 +305,20 @@ void VideoPlaybackDevice::onStart() Qt::DirectConnection, Q_ARG(QObject*, this), Q_ARG(const QStringList &, filters)); - filename = propertyCollection()->getProperty("filename").toString(); +} + +void VideoPlaybackDevice::onStart() +{ openVideo(); } void VideoPlaybackDevice::onStop() { closeVideo(); +} + +void VideoPlaybackDevice::onClose() +{ SharedQObjectPtr ctrlSrv = Services::getService("PlaybackControl"); QMetaObject::invokeMethod(ctrlSrv.data(), "removeConnections", diff --git a/nexxT/tests/src/AviFilePlayback.hpp b/nexxT/tests/src/AviFilePlayback.hpp index 74a270a..00c10ea 100644 --- a/nexxT/tests/src/AviFilePlayback.hpp +++ b/nexxT/tests/src/AviFilePlayback.hpp @@ -67,8 +67,10 @@ public slots: void setSequence(const QString &_filename); void setTimeFactor(double factor); protected: + void onOpen(); void onStart(); void onStop(); + void onClose(); }; diff --git a/nexxT/tests/src/SConscript.py b/nexxT/tests/src/SConscript.py index ac6bb55..ed4add2 100644 --- a/nexxT/tests/src/SConscript.py +++ b/nexxT/tests/src/SConscript.py @@ -10,16 +10,19 @@ env = env.Clone() env.EnableQt5Modules(['QtCore', "QtMultimedia", "QtGui"]) +srcDir = Dir(".").srcnode() env.Append(CPPPATH=["../../src", "."], LIBPATH=["../../src"], LIBS=["nexxT"]) -env['QT5_DEBUG'] = 1 - -plugin = env.RegisterTargets(env.SharedLibrary("test_plugins", env.RegisterSources(Split(""" +plugin = env.SharedLibrary("test_plugins", env.RegisterSources(Split(""" SimpleSource.cpp AviFilePlayback.cpp TestExceptionFilter.cpp Plugins.cpp -""")))) +"""))) +env.RegisterTargets(plugin) + +installed = env.Install(srcDir.Dir("..").Dir("binary").Dir(env.subst("$target_platform")).Dir(env.subst("$variant")).abspath, plugin) +env.RegisterTargets(installed) diff --git a/nexxT/tests/src/TestExceptionFilter.cpp b/nexxT/tests/src/TestExceptionFilter.cpp index dc822ec..cea3446 100644 --- a/nexxT/tests/src/TestExceptionFilter.cpp +++ b/nexxT/tests/src/TestExceptionFilter.cpp @@ -37,6 +37,14 @@ void TestExceptionFilter::onInit() } } +void TestExceptionFilter::onOpen() +{ + if( propertyCollection()->getProperty("whereToThrow") == "open" ) + { + throw std::runtime_error("exception in open"); + } +} + void TestExceptionFilter::onStart() { if( propertyCollection()->getProperty("whereToThrow") == "start" ) @@ -61,6 +69,14 @@ void TestExceptionFilter::onStop() } } +void TestExceptionFilter::onClose() +{ + if( propertyCollection()->getProperty("whereToThrow") == "close" ) + { + throw std::runtime_error("exception in close"); + } +} + void TestExceptionFilter::onDeinit() { if( propertyCollection()->getProperty("whereToThrow") == "deinit" ) diff --git a/nexxT/tests/src/TestExceptionFilter.hpp b/nexxT/tests/src/TestExceptionFilter.hpp index 60fdfc3..3943647 100644 --- a/nexxT/tests/src/TestExceptionFilter.hpp +++ b/nexxT/tests/src/TestExceptionFilter.hpp @@ -24,9 +24,11 @@ class TestExceptionFilter : public Filter NEXT_PLUGIN_DECLARE_FILTER(TestExceptionFilter) void onInit(); + void onOpen(); void onStart(); void onPortDataChanged(const InputPortInterface &inputPort); void onStop(); + void onClose(); void onDeinit(); }; diff --git a/workspace/pytest_nexxT.bat b/workspace/pytest_nexxT.bat index f00dcee..b92d522 100644 --- a/workspace/pytest_nexxT.bat +++ b/workspace/pytest_nexxT.bat @@ -1,11 +1,11 @@ -CALL scons .. || exit /b 1 -set NEXT_CEXT_PATH=%cd%\build\msvc_x86_64_nonopt\nexxT\src -set NEXT_CPLUGIN_PATH=%cd%\build\msvc_x86_64_nonopt\nexxT\tests\src +CALL scons -j8 .. || exit /b 1 +REM set NEXXT_CEXT_PATH=%cd%\build\msvc_x86_64_nonopt\nexxT\src +set NEXXT_VARIANT=nonopt pytest --cov=nexxT.core --cov=nexxT.interface --cov-report html ../nexxT/tests || exit /b 1 -set NEXT_CEXT_PATH=%cd%\build\msvc_x86_64_release\nexxT\src -set NEXT_CPLUGIN_PATH=%cd%\build\msvc_x86_64_release\nexxT\tests\src +REM set NEXXT_CEXT_PATH=%cd%\build\msvc_x86_64_release\nexxT\src +set NEXXT_VARIANT=release pytest --cov-append --cov=nexxT.core --cov=nexxT.interface --cov-report html ../nexxT/tests || exit /b 1 -set NEXT_DISABLE_CIMPL=1 +set NEXXT_DISABLE_CIMPL=1 pytest --cov-append --cov=nexxT.core --cov=nexxT.interface --cov-report html ../nexxT/tests || exit /b 1 From 3f1259cf599e5d7caecdc5305990a53375b9369c Mon Sep 17 00:00:00 2001 From: cwiede <62332054+cwiede@users.noreply.github.com> Date: Sun, 22 Mar 2020 14:18:42 +0100 Subject: [PATCH 07/16] Applications are now stopped and started on sequence change - contains also a fix for MethodInvoker queued connections - some fixes for new state OPENED --- nexxT/core/ActiveApplication.py | 2 ++ nexxT/core/Application.py | 8 +++++++- nexxT/core/Utils.py | 10 +++++++++- nexxT/services/gui/PlaybackControl.py | 20 ++++++++++++++++---- nexxT/tests/src/AviFilePlayback.cpp | 5 +++++ 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/nexxT/core/ActiveApplication.py b/nexxT/core/ActiveApplication.py index 72e188a..a223629 100644 --- a/nexxT/core/ActiveApplication.py +++ b/nexxT/core/ActiveApplication.py @@ -121,6 +121,8 @@ def shutdown(self): assertMainThread() if self._state == FilterState.ACTIVE: self.stop() + if self._state == FilterState.OPENED: + self.close() if self._state == FilterState.INITIALIZED: self.deinit() if self._state == FilterState.CONSTRUCTED: diff --git a/nexxT/core/Application.py b/nexxT/core/Application.py index daa076c..69d32d9 100644 --- a/nexxT/core/Application.py +++ b/nexxT/core/Application.py @@ -15,7 +15,7 @@ from nexxT.core.SubConfiguration import SubConfiguration from nexxT.core.ActiveApplication import ActiveApplication from nexxT.core.Exceptions import NexTRuntimeError, PropertyCollectionChildNotFound -from nexxT.core.Utils import MethodInvoker, assertMainThread +from nexxT.core.Utils import MethodInvoker, assertMainThread, handle_exception logger = logging.getLogger(__name__) @@ -48,6 +48,7 @@ def guiState(self, name): @staticmethod + @handle_exception def unactivate(): """ if an active application exists, close it. @@ -61,6 +62,7 @@ def unactivate(): Application.activeApplication = None logger.internal("leaving unactivate") + @handle_exception def activate(self): """ puts this application to active @@ -73,6 +75,7 @@ def activate(self): logger.internal("leaving activate") @staticmethod + @handle_exception def initialize(): """ Initialize the active application such that the filters are active. @@ -82,9 +85,11 @@ def initialize(): if Application.activeApplication is None: raise NexTRuntimeError("No active application to initialize") MethodInvoker(Application.activeApplication.init, Qt.DirectConnection) + MethodInvoker(Application.activeApplication.open, Qt.DirectConnection) MethodInvoker(Application.activeApplication.start, Qt.DirectConnection) @staticmethod + @handle_exception def deInitialize(): """ Deinitialize the active application such that the filters are in CONSTRUCTED state @@ -94,4 +99,5 @@ def deInitialize(): if Application.activeApplication is None: raise NexTRuntimeError("No active application to initialize") MethodInvoker(Application.activeApplication.stop, Qt.DirectConnection) + MethodInvoker(Application.activeApplication.close, Qt.DirectConnection) MethodInvoker(Application.activeApplication.deinit, Qt.DirectConnection) diff --git a/nexxT/core/Utils.py b/nexxT/core/Utils.py index 8b3ffe3..72ea271 100644 --- a/nexxT/core/Utils.py +++ b/nexxT/core/Utils.py @@ -33,7 +33,15 @@ class MethodInvoker(QObject): def __init__(self, callback, connectiontype, *args): super().__init__() self.args = args - self.callback = callback + if isinstance(callback, dict): + obj = callback["object"] + method = callback["method"] + self.callback = getattr(obj, method) + self.moveToThread(obj.thread()) + else: + self.callback = callback + if connectiontype != Qt.DirectConnection: + logging.getLogger(__name__).warning("Using old style API, wrong thread might be used!") if connectiontype is self.IDLE_TASK: QTimer.singleShot(0, self.callbackWrapper) else: diff --git a/nexxT/services/gui/PlaybackControl.py b/nexxT/services/gui/PlaybackControl.py index 124fa0b..06decf6 100644 --- a/nexxT/services/gui/PlaybackControl.py +++ b/nexxT/services/gui/PlaybackControl.py @@ -16,8 +16,10 @@ from PySide2.QtWidgets import (QWidget, QGridLayout, QLabel, QBoxLayout, QSlider, QToolBar, QAction, QApplication, QStyle, QLineEdit, QFileSystemModel, QTreeView, QHeaderView) from nexxT.interface import Services +from nexxT.interface import FilterState from nexxT.core.Exceptions import NexTRuntimeError, PropertyCollectionPropertyNotFound -from nexxT.core.Utils import FileSystemModelSortProxy, assertMainThread, MethodInvoker +from nexxT.core.Application import Application +from nexxT.core.Utils import FileSystemModelSortProxy, assertMainThread, MethodInvoker, handle_exception logger = logging.getLogger(__name__) @@ -95,16 +97,26 @@ def setupConnections(self, playbackDevice, nameFilters): featureset.add(feature) connections.append((signal, slot)) - #@Slot(str) + @handle_exception def setSequenceWrapper(filename): + assertMainThread() + if Application.activeApplication is None: + return + if Application.activeApplication.getState() not in [FilterState.ACTIVE, FilterState.OPENED]: + return if QDir.match(nameFilters, pathlib.Path(filename).name): logger.debug("setSequence %s", filename) - setSequenceWrapper.invoke = MethodInvoker(playbackDevice.setSequence, Qt.QueuedConnection, filename) + if Application.activeApplication.getState() == FilterState.ACTIVE: + Application.activeApplication.stop() + setSequenceWrapper.invoke = MethodInvoker(dict(object=playbackDevice,method="setSequence"), + Qt.QueuedConnection, filename) + Application.activeApplication.start() + logger.debug("setSequence done") else: logger.debug("%s does not match filters: %s", filename, nameFilters) # setSequence is called only if filename matches the given filters - if self.setSequence.connect(setSequenceWrapper): + if self.setSequence.connect(setSequenceWrapper, Qt.DirectConnection): featureset.add("setSequence") connections.append((self.setSequence, setSequenceWrapper)) for feature in ["sequenceOpened", "currentTimestampChanged", "playbackStarted", "playbackPaused", diff --git a/nexxT/tests/src/AviFilePlayback.cpp b/nexxT/tests/src/AviFilePlayback.cpp index 9e9cf48..4481537 100644 --- a/nexxT/tests/src/AviFilePlayback.cpp +++ b/nexxT/tests/src/AviFilePlayback.cpp @@ -10,6 +10,7 @@ #include "PropertyCollection.hpp" #include "Logger.hpp" #include +#include #include #include #include @@ -116,6 +117,10 @@ class DummyVideoSurface : public QAbstractVideoSurface void VideoPlaybackDevice::openVideo() { + if( QThread::currentThread() != thread() ) + { + throw std::runtime_error("unexpected thread."); + } NEXT_LOG_DEBUG("entering openVideo"); pauseOnNextImage = false; player = new QMediaPlayer(this, QMediaPlayer::VideoSurface); From 1aec056263621f22e478405a5ce856396ae06440 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Sun, 22 Mar 2020 14:25:36 +0100 Subject: [PATCH 08/16] remove autogenerated MANIFEST.in --- MANIFEST.in | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 3f057d0..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,19 +0,0 @@ -include nexxT/include\DataSamples.hpp -include nexxT/include\FilterEnvironment.hpp -include nexxT/include\Filters.hpp -include nexxT/include\Logger.hpp -include nexxT/include\NexTConfig.hpp -include nexxT/include\NexTLinkage.hpp -include nexxT/include\NexTPlugins.hpp -include nexxT/include\Ports.hpp -include nexxT/include\PropertyCollection.hpp -include nexxT/include\Services.hpp -include nexxT/binary/msvc_x86_64/nonopt/cnexxT.pyd -include nexxT/binary/msvc_x86_64/nonopt/nexxT.dll -include nexxT/binary/msvc_x86_64/nonopt/nexxT.exp -include nexxT/binary/msvc_x86_64/nonopt/nexxT.lib -include nexxT/binary/msvc_x86_64/release/cnexxT.pyd -include nexxT/binary/msvc_x86_64/release/nexxT.dll -include nexxT/binary/msvc_x86_64/release/nexxT.exp -include nexxT/binary/msvc_x86_64/release/nexxT.lib -include nexxT/core/ConfigFileSchema.json \ No newline at end of file From 6c067b5128e807742fc1e4bc490ea9e105d390f1 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Sun, 22 Mar 2020 14:57:47 +0100 Subject: [PATCH 09/16] add stream selection for single step mode --- nexxT/services/gui/PlaybackControl.py | 31 ++++++++++++++++++++++----- nexxT/tests/src/AviFilePlayback.cpp | 14 ++++++------ nexxT/tests/src/AviFilePlayback.hpp | 4 ++-- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/nexxT/services/gui/PlaybackControl.py b/nexxT/services/gui/PlaybackControl.py index 06decf6..382e14d 100644 --- a/nexxT/services/gui/PlaybackControl.py +++ b/nexxT/services/gui/PlaybackControl.py @@ -14,7 +14,7 @@ import os from PySide2.QtCore import QObject, Signal, Slot, QDateTime, Qt, QDir, QTimer, QMutex, QMutexLocker from PySide2.QtWidgets import (QWidget, QGridLayout, QLabel, QBoxLayout, QSlider, QToolBar, QAction, QApplication, - QStyle, QLineEdit, QFileSystemModel, QTreeView, QHeaderView) + QStyle, QLineEdit, QFileSystemModel, QTreeView, QHeaderView, QActionGroup) from nexxT.interface import Services from nexxT.interface import FilterState from nexxT.core.Exceptions import NexTRuntimeError, PropertyCollectionPropertyNotFound @@ -30,8 +30,8 @@ class MVCPlaybackControlBase(QObject): """ startPlayback = Signal() pausePlayback = Signal() - stepForward = Signal() - stepBackward = Signal() + stepForward = Signal(str) + stepBackward = Signal(str) seekBeginning = Signal() seekEnd = Signal() seekTime = Signal(QDateTime) @@ -255,8 +255,8 @@ def __init__(self, config): # let's stay on the safe side and do not use emit as a slot... self.actStart.triggered.connect(lambda: self.startPlayback.emit()) self.actPause.triggered.connect(lambda: self.pausePlayback.emit()) - self.actStepFwd.triggered.connect(lambda: self.stepForward.emit()) - self.actStepBwd.triggered.connect(lambda: self.stepBackward.emit()) + self.actStepFwd.triggered.connect(lambda: self.stepForward.emit(self.selectedStream())) + self.actStepBwd.triggered.connect(lambda: self.stepBackward.emit(self.selectedStream())) self.actSeekEnd.triggered.connect(lambda: self.seekEnd.emit()) self.actSeekBegin.triggered.connect(lambda: self.seekBeginning.emit()) # pylint: enable=unnecessary-lambda @@ -359,6 +359,12 @@ def setTimeFactor(newFactor): playbackMenu.addAction(self.actShowAllFiles) playbackMenu.addAction(self.actRefreshBrowser) + self.actGroupStream = QActionGroup(self) + self.actGroupStream.setExclusionPolicy(QActionGroup.ExclusionPolicy.ExclusiveOptional) + playbackMenu.addSeparator() + self.actGroupStreamMenu = playbackMenu.addMenu("Step Stream") + self._selectedStream = None + self.recentSeqs = [QAction() for i in range(10)] playbackMenu.addSeparator() recentMenu = playbackMenu.addMenu("Recent") @@ -462,6 +468,15 @@ def sequenceOpened(self, filename, begin, end, streams): idx = self.proxyFileSystemModel.mapFromSource(idx) self.browser.setCurrentIndex(idx) self.browser.scrollTo(idx) + self._selectedStream = None + for a in self.actGroupStream.actions(): + self.actGroupStream.removeAction(a) + for stream in streams: + act = QAction(stream, self.actGroupStream) + act.triggered.connect(lambda: self.setSelectedStream(stream)) + act.setCheckable(True) + act.setChecked(False) + self.actGroupStreamMenu.addAction(act) QTimer.singleShot(250, self.scrollToCurrent) def currentTimestampChanged(self, currentTime): @@ -585,6 +600,12 @@ def timeRatioChanged(self, newRatio): return self.timeRatioLabel.setText("%.2f" % newRatio) + def selectedStream(self): + return self._selectedStream + + def setSelectedStream(self, stream): + self._selectedStream = stream + def saveState(self): """ Saves the state of the playback control diff --git a/nexxT/tests/src/AviFilePlayback.cpp b/nexxT/tests/src/AviFilePlayback.cpp index 4481537..1c5ee38 100644 --- a/nexxT/tests/src/AviFilePlayback.cpp +++ b/nexxT/tests/src/AviFilePlayback.cpp @@ -122,7 +122,7 @@ void VideoPlaybackDevice::openVideo() throw std::runtime_error("unexpected thread."); } NEXT_LOG_DEBUG("entering openVideo"); - pauseOnNextImage = false; + pauseOnStream = QString(); player = new QMediaPlayer(this, QMediaPlayer::VideoSurface); player->setMuted(true); videoSurface = new DummyVideoSurface(this); @@ -170,7 +170,7 @@ VideoPlaybackDevice::VideoPlaybackDevice(BaseFilterEnvironment *env) : player(nullptr), videoSurface(nullptr) { - pauseOnNextImage = false; + pauseOnStream = QString(); playbackRate = 1.0; video_out = SharedOutputPortPtr(new OutputPortInterface(false, "video_out", env)); addStaticPort(video_out); @@ -183,9 +183,9 @@ VideoPlaybackDevice::~VideoPlaybackDevice() void VideoPlaybackDevice::newImage(const QImage &img) { - if(pauseOnNextImage) + if(!pauseOnStream.isNull()) { - pauseOnNextImage = false; + pauseOnStream = QString(); QMetaObject::invokeMethod(this, "pausePlayback", Qt::QueuedConnection); } QByteArray a; @@ -257,10 +257,10 @@ void VideoPlaybackDevice::pausePlayback() if(player) player->pause(); } -void VideoPlaybackDevice::stepForward() +void VideoPlaybackDevice::stepForward(const QString &stream) { - NEXT_LOG_DEBUG("stepForward called"); - pauseOnNextImage = true; + NEXT_LOG_DEBUG(QString("stepForward(%1) called").arg(stream)); + pauseOnStream = "video"; if( player && player->state() != QMediaPlayer::PlayingState ) { NEXT_LOG_DEBUG("calling play"); diff --git a/nexxT/tests/src/AviFilePlayback.hpp b/nexxT/tests/src/AviFilePlayback.hpp index 00c10ea..35bf428 100644 --- a/nexxT/tests/src/AviFilePlayback.hpp +++ b/nexxT/tests/src/AviFilePlayback.hpp @@ -26,7 +26,7 @@ class VideoPlaybackDevice : public Filter SharedOutputPortPtr video_out; QString filename; double playbackRate; - bool pauseOnNextImage; + QString pauseOnStream; QMediaPlayer *player; DummyVideoSurface *videoSurface; @@ -60,7 +60,7 @@ public slots: void currentMediaChanged(const QMediaContent &); void startPlayback(); void pausePlayback(); - void stepForward(); + void stepForward(const QString &stream); void seekBeginning(); void seekEnd(); void seekTime(const QDateTime &pos); From 6f5173cb52789dce057878fc5372b4f7f40727db Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Sun, 22 Mar 2020 16:45:19 +0100 Subject: [PATCH 10/16] add examples --- nexxT/examples/videoplayback/QImageDisplay.py | 54 +++++++++++++++++++ nexxT/examples/videoplayback/config.json | 51 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 nexxT/examples/videoplayback/QImageDisplay.py create mode 100644 nexxT/examples/videoplayback/config.json diff --git a/nexxT/examples/videoplayback/QImageDisplay.py b/nexxT/examples/videoplayback/QImageDisplay.py new file mode 100644 index 0000000..77e65d0 --- /dev/null +++ b/nexxT/examples/videoplayback/QImageDisplay.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2020 ifm electronic gmbh +# +# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +# + +import logging +import shiboken2 +from PySide2.QtCore import QBuffer +from PySide2.QtGui import QImageReader, QPixmap +from PySide2.QtWidgets import QLabel, QWidget, QMdiSubWindow +from nexxT.interface import Filter, InputPort, Services + +logger = logging.getLogger(__name__) + +class QImageDisplay(Filter): + + def __init__(self, environment): + Filter.__init__(self, False, False, environment) + self.inPort = InputPort(False, "inPort", environment) + self.addStaticPort(self.inPort) + self.propertyCollection().defineProperty("SubplotID", "ImageDisplay", "The parent subplot.") + self.lastSize = None + + def onOpen(self): + srv = Services.getService("MainWindow") + self.display = QLabel() + self.subplotID = self.propertyCollection().getProperty("SubplotID") + srv.subplot(self.subplotID, self, self.display) + + def onClose(self): + srv = Services.getService("MainWindow") + srv.releaseSubplot(self.subplotID) + + def onPortDataChanged(self, inputPort): + dataSample = inputPort.getData() + c = dataSample.getContent() + b = QBuffer(c) + r = QImageReader() + r.setDevice(b) + img = r.read() + self.display.setPixmap(QPixmap.fromImage(img)) + logger.debug("got image, size %d %d", img.size().width(), img.size().height()) + if self.lastSize != img.size(): + self.lastSize = img.size() + self.display.setMinimumSize(img.size()) + # propagate the size change to the parents + w = self.display + while w is not None: + if isinstance(w, QWidget): + w.adjustSize() + if isinstance(w, QMdiSubWindow): + break + w = w.parent() \ No newline at end of file diff --git a/nexxT/examples/videoplayback/config.json b/nexxT/examples/videoplayback/config.json new file mode 100644 index 0000000..925ed24 --- /dev/null +++ b/nexxT/examples/videoplayback/config.json @@ -0,0 +1,51 @@ +{ + "composite_filters": [ + ], + "applications": [ + { + "name": "videoplayback", + "_guiState": { + "filters__display": { + "mdi_mdi_MainWindow_MDI_ImageDisplay_geom": "AdnQywABAAAAAAAAAAAAVQAAAAAAAAAPAAAAAAAABRoAAAAAAAAC/gAAAAAAAABVAAAAAAAAAA8AAAAAAAAFGgAAAAAAAAL+AAAAAAAAAAA=", + "mdi_mdi_MainWindow_MDI_ImageDisplay_visible": 1 + } + }, + "nodes": [ + { + "name": "source", + "library": "binary://../../tests/binary/${NEXXT_PLATFORM}/${NEXXT_VARIANT}/test_plugins", + "factoryFunction": "VideoPlaybackDevice", + "dynamicInputPorts": [], + "staticInputPorts": [], + "dynamicOutputPorts": [], + "staticOutputPorts": [ + "video_out" + ], + "thread": "thread-source", + "properties": { + "filename": "/tmp/testLLLLLLLLLLLLLLLLLLLLOOOOOOOOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNNNNNNNGGGGGGGGGGGGGG.avi" + } + }, + { + "name": "display", + "library": "pyfile://./QImageDisplay.py", + "factoryFunction": "QImageDisplay", + "dynamicInputPorts": [], + "staticInputPorts": [ + "inPort" + ], + "dynamicOutputPorts": [], + "staticOutputPorts": [], + "thread": "main", + "properties": { + "SubplotID": "ImageDisplay" + } + } + ], + "connections": [ + "source.video_out -> display.inPort" + ] + } + ], + "_guiState": {} +} \ No newline at end of file From 9027b366dbbcde130e6f8f21f7368c8f3d2b8706 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Sun, 22 Mar 2020 17:23:16 +0100 Subject: [PATCH 11/16] provide recent config file list --- nexxT/core/AppConsole.py | 6 ++- nexxT/services/gui/Configuration.py | 70 ++++++++++++++++++++++++++++- nexxT/services/gui/MainWindow.py | 2 + 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/nexxT/core/AppConsole.py b/nexxT/core/AppConsole.py index 82b80d1..9008b17 100644 --- a/nexxT/core/AppConsole.py +++ b/nexxT/core/AppConsole.py @@ -59,13 +59,15 @@ def startNexT(cfgfile, active, withGui): config = Configuration() if withGui: app = QApplication() + app.setOrganizationName("nexxT") + app.setApplicationName("nexxT") setupGuiServices(config) else: app = QCoreApplication() + app.setOrganizationName("nexxT") + app.setApplicationName("nexxT") setupConsoleServices(config) - app.setOrganizationName("nexxT") - app.setApplicationName("nexxT") ConfigFileLoader.load(config, cfgfile) if withGui: mainWindow = Services.getService("MainWindow") diff --git a/nexxT/services/gui/Configuration.py b/nexxT/services/gui/Configuration.py index db2e9e4..345d92f 100644 --- a/nexxT/services/gui/Configuration.py +++ b/nexxT/services/gui/Configuration.py @@ -9,7 +9,8 @@ """ import logging -from PySide2.QtCore import QObject, Signal, Slot, Qt, QAbstractItemModel, QModelIndex +from PySide2.QtCore import (QObject, Signal, Slot, Qt, QAbstractItemModel, QModelIndex, QSettings, QByteArray, + QDataStream, QIODevice) from PySide2.QtGui import QFont, QPainter from PySide2.QtWidgets import (QTreeView, QAction, QStyle, QApplication, QFileDialog, QAbstractItemView, QMessageBox, QHeaderView, QMenu, QDockWidget, QGraphicsView) @@ -565,7 +566,8 @@ def __init__(self, configuration): srv = Services.getService("MainWindow") confMenu = srv.menuBar().addMenu("&Configuration") toolBar = srv.getToolBar() - + self._configuration = configuration + self.actLoad = QAction(QApplication.style().standardIcon(QStyle.SP_DialogOpenButton), "Open", self) self.actLoad.triggered.connect(lambda *args: self._execLoad(configuration, *args)) self.actSave = QAction(QApplication.style().standardIcon(QStyle.SP_DialogSaveButton), "Save", self) @@ -592,6 +594,14 @@ def __init__(self, configuration): toolBar.addAction(self.actActivate) toolBar.addAction(self.actDeactivate) + self.recentConfigs = [QAction() for i in range(10)] + confMenu.addSeparator() + recentMenu = confMenu.addMenu("Recent") + for a in self.recentConfigs: + a.setVisible(False) + a.triggered.connect(self._openRecent) + recentMenu.addAction(a) + self.mainWidget = srv.newDockWidget("Configuration", None, Qt.LeftDockWidgetArea) self.model = ConfigurationModel(configuration, self.mainWidget) self.treeView = QTreeView(self.mainWidget) @@ -614,6 +624,8 @@ def __init__(self, configuration): configuration.appActivated.connect(self.appActivated) self.activeAppChanged.connect(configuration.activate) + self.restoreState() + srv.aboutToClose.connect(self.saveState) def _execLoad(self, configuration): assertMainThread() @@ -626,6 +638,19 @@ def _execLoad(self, configuration): logger.exception("Error while loading configuration %s: %s", fn, str(e)) QMessageBox.warning(self.mainWidget, "Error while loading configuration", str(e)) + def _openRecent(self): + """ + Called when the user clicks on a recent sequence. + :return: + """ + action = self.sender() + fn = action.data() + try: + ConfigFileLoader.load(self._configuration, fn) + except Exception as e: + logger.exception("Error while loading configuration %s: %s", fn, str(e)) + QMessageBox.warning(self.mainWidget, "Error while loading configuration", str(e)) + @staticmethod def _execSave(configuration): assertMainThread() @@ -668,6 +693,19 @@ def _configNameChanged(self, cfgfile): srv.setWindowTitle("nexxT") else: srv.setWindowTitle("nexxT: " + cfgfile) + foundIdx = None + for i, a in enumerate(self.recentConfigs): + if a.data() == cfgfile: + foundIdx = i + if foundIdx is None: + foundIdx = len(self.recentConfigs)-1 + for i in range(foundIdx, 0, -1): + self.recentConfigs[i].setText(self.recentConfigs[i-1].text()) + self.recentConfigs[i].setData(self.recentConfigs[i-1].data()) + self.recentConfigs[i].setVisible(self.recentConfigs[i-1].data() is not None) + self.recentConfigs[0].setText(cfgfile) + self.recentConfigs[0].setData(cfgfile) + self.recentConfigs[0].setVisible(True) def _onItemDoubleClicked(self, index): assertMainThread() @@ -705,3 +743,31 @@ def activeAppStateChange(self, newState): self.actDeactivate.setEnabled(True) else: self.actDeactivate.setEnabled(False) + + def restoreState(self): + logger.debug("restoring config state ...") + settings = QSettings() + v = settings.value("ConfigurationRecentFiles") + if v is not None and isinstance(v, QByteArray): + ds = QDataStream(v) + recentFiles = ds.readQStringList() + idx = 0 + for f in recentFiles: + if f != "" and f is not None: + self.recentConfigs[idx].setData(f) + self.recentConfigs[idx].setText(f) + self.recentConfigs[idx].setVisible(True) + idx += 1 + if idx >= len(self.recentConfigs): + break + logger.debug("restoring config state done") + + def saveState(self): + logger.debug("saving config state ...") + settings = QSettings() + b = QByteArray() + ds = QDataStream(b, QIODevice.WriteOnly) + l = [rc.data() for rc in self.recentConfigs if rc.isVisible() and rc.data() is not None and rc.data() != ""] + ds.writeQStringList(l) + settings.setValue("ConfigurationRecentFiles", b) + logger.debug("saving config state done (%s)", l) \ No newline at end of file diff --git a/nexxT/services/gui/MainWindow.py b/nexxT/services/gui/MainWindow.py index ced0d01..7a377b4 100644 --- a/nexxT/services/gui/MainWindow.py +++ b/nexxT/services/gui/MainWindow.py @@ -144,6 +144,7 @@ class MainWindow(QMainWindow): subplot functionality to create grid-layouted views. """ mdiSubWindowCreated = Signal(QMdiSubWindow) # TODO: remove, is not necessary anymore with subplot feature + aboutToClose = Signal() def __init__(self, config): super().__init__() @@ -168,6 +169,7 @@ def closeEvent(self, closeEvent): """ self.saveState() self.saveMdiState() + self.aboutToClose.emit() return super().closeEvent(closeEvent) def restoreState(self): From 482f628361d64ad3d92225c8b94173e5d8e01408 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Sun, 22 Mar 2020 17:26:39 +0100 Subject: [PATCH 12/16] avoid loosing gui state at exception during load --- nexxT/core/Configuration.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nexxT/core/Configuration.py b/nexxT/core/Configuration.py index 596e169..57765dc 100644 --- a/nexxT/core/Configuration.py +++ b/nexxT/core/Configuration.py @@ -58,13 +58,14 @@ def __init__(self): self._guiState = PropertyCollectionImpl("_guiState", self._propertyCollection) @Slot() - def close(self): + def close(self, avoidSave=False): """ Closing the configuration instance and free allocated resources. :return: """ logger.internal("entering Configuration.close") - ConfigFileLoader.saveGuiState(self) + if not avoidSave: + ConfigFileLoader.saveGuiState(self) Application.unactivate() for sc in self._compositeFilters + self._applications: sc.cleanup() @@ -122,7 +123,7 @@ def compositeLookup(name): self.configNameChanged.emit(cfg["CFGFILE"]) self.configLoaded.emit() except RuntimeError as e: - self.close() + self.close(avoidSave=True) raise e def save(self): From 54366e57e8b8be9a4eced47ea7b42fb9f1d05ed3 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Wed, 25 Mar 2020 09:55:21 +0100 Subject: [PATCH 13/16] fix saving guistate after introducing new filter state "opened" possibility for starting with empty config possibility for adding new applications and composite filters add possibility to specify execution thread in MethodInvoker PyCharm debugging fixes (regarding excepthook, unfortunately pycharm exits on uncaught exceptions :() --- nexxT/core/ActiveApplication.py | 4 +- nexxT/core/AppConsole.py | 12 ++++-- nexxT/core/Configuration.py | 16 +++++++ nexxT/core/FilterMockup.py | 3 +- nexxT/core/PluginManager.py | 4 +- nexxT/core/PropertyCollectionImpl.py | 8 +++- nexxT/core/Utils.py | 22 ++++++++-- nexxT/services/ConsoleLogger.py | 17 +------- nexxT/services/gui/Configuration.py | 60 +++++++++++++++++++++++++-- nexxT/services/gui/MainWindow.py | 2 +- nexxT/services/gui/PlaybackControl.py | 2 + 11 files changed, 117 insertions(+), 33 deletions(-) diff --git a/nexxT/core/ActiveApplication.py b/nexxT/core/ActiveApplication.py index a223629..a0c7de0 100644 --- a/nexxT/core/ActiveApplication.py +++ b/nexxT/core/ActiveApplication.py @@ -26,7 +26,7 @@ class ActiveApplication(QObject): performOperation = Signal(str, object) # Signal is connected to all the threads (operation, barrier) stateChanged = Signal(int) # Signal is emitted after the state of the graph has been changed - aboutToStop = Signal() # Signal is emitted before stop operation takes place + aboutToClose = Signal() # Signal is emitted before stop operation takes place def __init__(self, graph): super().__init__() @@ -363,7 +363,6 @@ def stop(self): :return: None """ logger.internal("entering stop operation, old state %s", FilterState.state2str(self._state)) - self.aboutToStop.emit() assertMainThread() while self._operationInProgress and self._state != FilterState.ACTIVE: QCoreApplication.processEvents() @@ -385,6 +384,7 @@ def close(self): """ logger.internal("entering close operation, old state %s", FilterState.state2str(self._state)) assertMainThread() + self.aboutToClose.emit() while self._operationInProgress and self._state != FilterState.OPENED: QCoreApplication.processEvents() if self._state != FilterState.OPENED: diff --git a/nexxT/core/AppConsole.py b/nexxT/core/AppConsole.py index 9008b17..0041408 100644 --- a/nexxT/core/AppConsole.py +++ b/nexxT/core/AppConsole.py @@ -68,7 +68,8 @@ def startNexT(cfgfile, active, withGui): app.setApplicationName("nexxT") setupConsoleServices(config) - ConfigFileLoader.load(config, cfgfile) + if cfgfile is not None: + ConfigFileLoader.load(config, cfgfile) if withGui: mainWindow = Services.getService("MainWindow") mainWindow.restoreState() @@ -76,7 +77,8 @@ def startNexT(cfgfile, active, withGui): if active is not None: config.activate(active) # need the reference of this - i2 = MethodInvoker(Application.initialize, MethodInvoker.IDLE_TASK) # pylint: disable=unused-variable + i2 = MethodInvoker(dict(object=Application, method="initialize", thread=app.thread()), + MethodInvoker.IDLE_TASK) # pylint: disable=unused-variable def cleanup(): logger.debug("cleaning up loaded services") @@ -103,7 +105,7 @@ def main(withGui): :return: None """ parser = ArgumentParser(description="nexxT console application") - parser.add_argument("cfg", nargs=1, help=".json configuration file of the project to be loaded.") + parser.add_argument("cfg", nargs='?', help=".json configuration file of the project to be loaded.") parser.add_argument("-a", "--active", default=None, type=str, help="active application; default: first application in config file") parser.add_argument("-l", "--logfile", default=None, type=str, @@ -113,6 +115,8 @@ def main(withGui): help="sets the log verbosity") parser.add_argument("-q", "--quiet", action="store_true", default=False, help="disble logging to stderr") args = parser.parse_args() + if args.cfg is None and args.active is not None: + parser.error("Active application set, but no config given.") nexT_logger = logging.getLogger() nexT_logger.setLevel(args.verbosity) @@ -128,7 +132,7 @@ def main(withGui): handler = logging.FileHandler(args.logfile) handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) nexT_logger.addHandler(handler) - startNexT(args.cfg[0], args.active, withGui=withGui) + startNexT(args.cfg, args.active, withGui=withGui) def mainConsole(): """ diff --git a/nexxT/core/Configuration.py b/nexxT/core/Configuration.py index 57765dc..1226e66 100644 --- a/nexxT/core/Configuration.py +++ b/nexxT/core/Configuration.py @@ -241,6 +241,22 @@ def addApplication(self, app): self._applications.append(app) self.subConfigAdded.emit(app) + def addNewApplication(self): + name = "application" + idx = 1 + while len([a for a in self._applications if a.getName() == name]) > 0: + idx += 1 + name = "application_%d" % idx + Application(name, self) + + def addNewCompositeFilter(self): + name = "composite" + idx = 1 + while len([c for c in self._compositeFilters if c.getName() == name]) > 0: + idx += 1 + name = "composite_%d" % idx + CompositeFilter(name, self) + def getApplicationNames(self): """ Return list of application names diff --git a/nexxT/core/FilterMockup.py b/nexxT/core/FilterMockup.py index 779e991..3b8e602 100644 --- a/nexxT/core/FilterMockup.py +++ b/nexxT/core/FilterMockup.py @@ -72,7 +72,8 @@ def createFilterAndUpdate(self, immediate=True): self._createFilterAndUpdate() self._createFilterAndUpdatePending = None elif self._createFilterAndUpdatePending is None: - self._createFilterAndUpdatePending = MethodInvoker(self._createFilterAndUpdate, Qt.QueuedConnection) + self._createFilterAndUpdatePending = MethodInvoker(dict(object=self,method="_createFilterAndUpdate"), + Qt.QueuedConnection) def _createFilterAndUpdate(self): self._createFilterAndUpdatePending = None diff --git a/nexxT/core/PluginManager.py b/nexxT/core/PluginManager.py index c899024..7ee678b 100644 --- a/nexxT/core/PluginManager.py +++ b/nexxT/core/PluginManager.py @@ -229,5 +229,7 @@ def _loadBinary(library, prop=None): raise UnknownPluginType("binary plugins can only be loaded with c extension enabled.") if prop is not None: library = prop.evalpath(library) - logging.getLogger(__name__).debug("loading binary plugin from file '%s'", library) + else: + logger.warning("no property collection instance, string interpolation skipped.") + logger.debug("loading binary plugin from file '%s'", library) return BinaryLibrary(library) diff --git a/nexxT/core/PropertyCollectionImpl.py b/nexxT/core/PropertyCollectionImpl.py index 91c51f3..bdf4e28 100644 --- a/nexxT/core/PropertyCollectionImpl.py +++ b/nexxT/core/PropertyCollectionImpl.py @@ -23,6 +23,8 @@ from nexxT.core.Utils import assertMainThread, checkIdentifier from nexxT.interface import PropertyCollection +logger = logging.getLogger(__name__) + class Property: """ This class represents a specific property. @@ -195,7 +197,7 @@ def defineProperty(self, name, defaultVal, helpstr, stringConverter=None, valida try: p.value = stringConverter(str(l)) except PropertyParsingError: - logging.getLogger(__name__).warning("Property %s: can't convert value '%s'.", name, str(l)) + logger.warning("Property %s: can't convert value '%s'.", name, str(l)) self.propertyAdded.emit(self, name) else: # the arguments to getProperty shall be consistent among calls @@ -317,7 +319,7 @@ def addChild(self, name, propColl): pass propColl.setObjectName(name) propColl.setParent(self) - logging.getLogger(__name__).internal("Propcoll %s: add child %s", self.objectName(), name) + logger.internal("Propcoll %s: add child %s", self.objectName(), name) def renameChild(self, oldName, newName): """ @@ -370,7 +372,9 @@ def evalpath(self, path): default_environ["NEXXT_PLATFORM"] = "msvc_x86%s" % ("_64" if platform.architecture()[0] == "64bit" else "") else: default_environ["NEXXT_PLATFORM"] = "linux_x86%s" % ("_64" if platform.architecture()[0] == "64bit" else "") + origpath = path path = string.Template(path).safe_substitute({**default_environ, **os.environ}) + logger.debug("interpolated path %s -> %s", origpath, path) if Path(path).is_absolute(): return path try: diff --git a/nexxT/core/Utils.py b/nexxT/core/Utils.py index 72ea271..9228043 100644 --- a/nexxT/core/Utils.py +++ b/nexxT/core/Utils.py @@ -20,6 +20,8 @@ QMutexLocker, QRecursiveMutex, QTimer, QSortFilterProxyModel, Qt) from nexxT.core.Exceptions import NexTInternalError, InvalidIdentifierException +logger = logging.getLogger(__name__) + class MethodInvoker(QObject): """ a workaround for broken QMetaObject.invokeMethod wrapper. See also @@ -37,11 +39,12 @@ def __init__(self, callback, connectiontype, *args): obj = callback["object"] method = callback["method"] self.callback = getattr(obj, method) - self.moveToThread(obj.thread()) + thread = callback["thread"] if "thread" in callback else obj.thread() + self.moveToThread(thread) else: self.callback = callback if connectiontype != Qt.DirectConnection: - logging.getLogger(__name__).warning("Using old style API, wrong thread might be used!") + logger.warning("Using old style API, wrong thread might be used!") if connectiontype is self.IDLE_TASK: QTimer.singleShot(0, self.callbackWrapper) else: @@ -237,12 +240,25 @@ def seek(self, offset, whence): elif self.p > self.ba.size(): self.p = self.ba.size() +# https://stackoverflow.com/questions/6234405/logging-uncaught-exceptions-in-python +def excepthook(*args): + """ + Generic exception handler for logging uncaught exceptions in plugin code. + :param args: + :return: + """ + exc_type = args[0] + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(*args) + return + logger.error("Uncaught exception", exc_info=args) + def handle_exception(func): def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception: - sys.excepthook(*sys.exc_info()) + excepthook(*sys.exc_info()) return wrapper if __name__ == "__main__": # pragma: no cover diff --git a/nexxT/services/ConsoleLogger.py b/nexxT/services/ConsoleLogger.py index fe90fd2..83bce4f 100644 --- a/nexxT/services/ConsoleLogger.py +++ b/nexxT/services/ConsoleLogger.py @@ -13,6 +13,7 @@ import os.path import sys from PySide2.QtCore import QObject, Slot, qInstallMessageHandler, QtMsgType +from nexxT.core.Utils import excepthook logger = logging.getLogger(__name__) @@ -66,19 +67,5 @@ def qtMessageHandler(qtMsgType, qMessageLogContext, msg): logger.log(typeMap[qtMsgType], msg, extra=(qMessageLogContext.file if qMessageLogContext.file is not None else "", qMessageLogContext.line)) -# https://stackoverflow.com/questions/6234405/logging-uncaught-exceptions-in-python -def handleException(*args): - """ - Generic exception handler for logging uncaught exceptions in plugin code. - :param args: - :return: - """ - exc_type = args[0] - if issubclass(exc_type, KeyboardInterrupt): - sys.__excepthook__(*args) - return - logger.error("Uncaught exception", exc_info=args) - - qInstallMessageHandler(ConsoleLogger.qtMessageHandler) -sys.excepthook = handleException +sys.excepthook = excepthook diff --git a/nexxT/services/gui/Configuration.py b/nexxT/services/gui/Configuration.py index 345d92f..a423761 100644 --- a/nexxT/services/gui/Configuration.py +++ b/nexxT/services/gui/Configuration.py @@ -10,7 +10,7 @@ import logging from PySide2.QtCore import (QObject, Signal, Slot, Qt, QAbstractItemModel, QModelIndex, QSettings, QByteArray, - QDataStream, QIODevice) + QDataStream, QIODevice, QMimeData) from PySide2.QtGui import QFont, QPainter from PySide2.QtWidgets import (QTreeView, QAction, QStyle, QApplication, QFileDialog, QAbstractItemView, QMessageBox, QHeaderView, QMenu, QDockWidget, QGraphicsView) @@ -19,6 +19,7 @@ from nexxT.core.ConfigFiles import ConfigFileLoader from nexxT.core.Configuration import Configuration from nexxT.core.Application import Application +from nexxT.core.CompositeFilter import CompositeFilter from nexxT.core.Utils import assertMainThread from nexxT.services.gui.PropertyDelegate import PropertyDelegate from nexxT.services.gui.GraphEditor import GraphScene @@ -88,6 +89,15 @@ def __init__(self, configuration, parent): configuration.subConfigRemoved.connect(self.subConfigRemoved) configuration.appActivated.connect(self.appActivated) + def isSubConfigParent(self, index): + item = self.data(index, ITEM_ROLE) + if index.isValid() and not index.parent().isValid(): + if item == "composite": + return Configuration.CONFIG_TYPE_COMPOSITE + if item == "apps": + return Configuration.CONFIG_TYPE_APPLICATION + return None + @staticmethod def isApplication(index): """ @@ -459,6 +469,10 @@ def data(self, index, role): # pylint: disable=too-many-return-statements,too-ma if item.subConfig.getName() == self.activeApp: font.setBold(True) return font + if role == Qt.ToolTipRole: + if isinstance(item, self.PropertyContent): + p = item.property.getPropertyDetails(item.name) + return p.helpstr if role == ITEM_ROLE: return item return None @@ -475,6 +489,8 @@ def flags(self, index): # pylint: disable=too-many-return-statements,too-many-br if isinstance(item, str): return Qt.ItemIsEnabled if isinstance(item, self.SubConfigContent): + if isinstance(item.subConfig, CompositeFilter): + return Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsDragEnabled return Qt.ItemIsEnabled | Qt.ItemIsEditable if isinstance(item, self.NodeContent): return Qt.ItemIsEnabled | Qt.ItemIsEditable @@ -553,6 +569,21 @@ def headerDate(self, section, orientation, role): # pylint: disable=no-self-use return "Value" return None + def mimeTypes(self): + logger.debug("mimeTypes") + return ["application/x-nexxT-compositefilter"] + + def mimeData(self, indices): + logger.debug("mimeData") + if len(indices) == 1 and indices[0].isValid(): + index = indices[0] + item = index.internalPointer().content + if isinstance(item, self.SubConfigContent) and isinstance(item.subConfig, CompositeFilter): + res = QMimeData() + res.setData(self.mimeTypes()[0], item.subConfig.getName().encode("utf8")) + return res + return None + class MVCConfigurationGUI(QObject): """ GUI implementation of MVCConfigurationBase @@ -568,11 +599,11 @@ def __init__(self, configuration): toolBar = srv.getToolBar() self._configuration = configuration - self.actLoad = QAction(QApplication.style().standardIcon(QStyle.SP_DialogOpenButton), "Open", self) + self.actLoad = QAction(QApplication.style().standardIcon(QStyle.SP_DialogOpenButton), "Open config", self) self.actLoad.triggered.connect(lambda *args: self._execLoad(configuration, *args)) - self.actSave = QAction(QApplication.style().standardIcon(QStyle.SP_DialogSaveButton), "Save", self) + self.actSave = QAction(QApplication.style().standardIcon(QStyle.SP_DialogSaveButton), "Save config", self) self.actSave.triggered.connect(lambda *args: self._execSave(configuration, *args)) - self.actNew = QAction(QApplication.style().standardIcon(QStyle.SP_FileIcon), "New", self) + self.actNew = QAction(QApplication.style().standardIcon(QStyle.SP_FileIcon), "New config", self) self.actNew.triggered.connect(lambda *args: self._execNew(configuration, *args)) self.cfgfile = None @@ -610,6 +641,9 @@ def __init__(self, configuration): self.treeView.setEditTriggers(self.treeView.EditKeyPressed|self.treeView.AnyKeyPressed) self.treeView.setAllColumnsShowFocus(True) self.treeView.setExpandsOnDoubleClick(False) + self.treeView.setDragEnabled(True) + self.treeView.setDropIndicatorShown(True) + self.treeView.setDragDropMode(QAbstractItemView.DragOnly) self.mainWidget.setWidget(self.treeView) self.treeView.setModel(self.model) self.treeView.header().setStretchLastSection(True) @@ -684,6 +718,24 @@ def _execTreeViewContextMenu(self, point): graphView.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) graphView.setScene(GraphScene(item.subConfig.getGraph(), graphDw)) graphDw.setWidget(graphView) + return + if self.model.isSubConfigParent(index) == Configuration.CONFIG_TYPE_APPLICATION: + m = QMenu() + a = QAction("Add application ...") + m.addAction(a) + a = m.exec_(self.treeView.mapToGlobal(point)) + if a is not None: + self._configuration.addNewApplication() + return + if self.model.isSubConfigParent(index) == Configuration.CONFIG_TYPE_COMPOSITE: + m = QMenu() + a = QAction("Add composite filter ...") + m.addAction(a) + a = m.exec_(self.treeView.mapToGlobal(point)) + if a is not None: + self._configuration.addNewCompositeFilter() + return + def _configNameChanged(self, cfgfile): assertMainThread() diff --git a/nexxT/services/gui/MainWindow.py b/nexxT/services/gui/MainWindow.py index 7a377b4..b4f2d49 100644 --- a/nexxT/services/gui/MainWindow.py +++ b/nexxT/services/gui/MainWindow.py @@ -377,6 +377,6 @@ def _windowDestroyed(self, obj): def _appActivated(self, name, app): if app is not None: self.activeApp = name - app.aboutToStop.connect(self.saveMdiState, Qt.UniqueConnection) + app.aboutToClose.connect(self.saveMdiState, Qt.UniqueConnection) else: self.activeApp = None diff --git a/nexxT/services/gui/PlaybackControl.py b/nexxT/services/gui/PlaybackControl.py index 382e14d..3fda553 100644 --- a/nexxT/services/gui/PlaybackControl.py +++ b/nexxT/services/gui/PlaybackControl.py @@ -470,12 +470,14 @@ def sequenceOpened(self, filename, begin, end, streams): self.browser.scrollTo(idx) self._selectedStream = None for a in self.actGroupStream.actions(): + logger.debug("Remove stream group action: %s", a.data()) self.actGroupStream.removeAction(a) for stream in streams: act = QAction(stream, self.actGroupStream) act.triggered.connect(lambda: self.setSelectedStream(stream)) act.setCheckable(True) act.setChecked(False) + logger.debug("Add stream group action: %s", act.data()) self.actGroupStreamMenu.addAction(act) QTimer.singleShot(250, self.scrollToCurrent) From 7998312f80070ad1e4d65d2d2fbe6c2370ab3753 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Thu, 26 Mar 2020 09:03:58 +0100 Subject: [PATCH 14/16] make pytests work under linux --- nexxT/__init__.py | 6 ++++-- nexxT/src/SConscript.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/nexxT/__init__.py b/nexxT/__init__.py index 0cfeb97..ed33a39 100644 --- a/nexxT/__init__.py +++ b/nexxT/__init__.py @@ -13,6 +13,7 @@ def setup(): from pathlib import Path import os import sys + import platform logger = logging.getLogger() INTERNAL = 5 # setup log level for internal messages @@ -38,8 +39,9 @@ def internal(self, message, *args, **kws): p = os.environ.get("NEXXT_CEXT_PATH", None) if p is None: variant = os.environ.get("NEXXT_VARIANT", "release") - p = [p for p in [Path(__file__).parent / "binary" / "msvc_x86_64" / variant, - Path(__file__).parent / "binary" / "linux_x86_64" / variant] if p.exists()] + cplatform = "linux_x86_64" if platform.system() == "Linux" else "msvc_x86_64" + p = [p for p in [Path(__file__).parent / "binary" / cplatform / variant, + Path(__file__).parent / "binary" / cplatform / variant] if p.exists()] if len(p) > 0: p = p[0].absolute() else: diff --git a/nexxT/src/SConscript.py b/nexxT/src/SConscript.py index f8ea46e..c4efdf9 100644 --- a/nexxT/src/SConscript.py +++ b/nexxT/src/SConscript.py @@ -78,8 +78,9 @@ env = env.Clone() env.Append(LIBS=["nexxT"]) if "linux" in env["target_platform"]: - env.Append(SHLINKFLAGS=["-l:libpyside2.abi3.so.$QT5VERSION", - "-l:libshiboken2.abi3.so.$QT5VERSION"]) + # the : notation is for the linker and enables to use lib names which are not + # ending with .so + env.Append(LIBS=[":libpyside2.abi3.so.$QT5VERSION",":libshiboken2.abi3.so.$QT5VERSION"]) else: env.Append(LIBS=["shiboken2.abi3", "pyside2.abi3"]) dummy = env.Command(targets, env.RegisterSources(Split("cnexxT.h cnexxT.xml")), From e14cfce2b3693a8c91cff139b245d76cfa60c018 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Thu, 26 Mar 2020 11:32:34 +0100 Subject: [PATCH 15/16] use abi3 tag on linux --- nexxT/src/SConscript.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nexxT/src/SConscript.py b/nexxT/src/SConscript.py index c4efdf9..9db14ec 100644 --- a/nexxT/src/SConscript.py +++ b/nexxT/src/SConscript.py @@ -5,6 +5,7 @@ # import sysconfig +import platform Import("env") @@ -93,7 +94,8 @@ pyext = env.SharedLibrary("cnexxT", dummy, SHLIBPREFIX=sysconfig.get_config_var("EXT_PREFIX"), - SHLIBSUFFIX=sysconfig.get_config_var("EXT_SUFFIX"), no_import_lib=True) + SHLIBSUFFIX=sysconfig.get_config_var("EXT_SUFFIX") if platform.system() == "Windows" else ".abi3.so", + no_import_lib=True) env.RegisterTargets(pyext) Depends(dummy, apilib) From 136f196d675318069edbd7c8927862653c97ed27 Mon Sep 17 00:00:00 2001 From: Christoph Wiedemann <62332054+cwiede@users.noreply.github.com> Date: Thu, 26 Mar 2020 14:50:18 +0100 Subject: [PATCH 16/16] trying to fix the install process --- setup.py | 74 +++++++++++++++++++++++++++++++++++++------- workspace/SConstruct | 14 ++++++--- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/setup.py b/setup.py index bac8719..39ae218 100644 --- a/setup.py +++ b/setup.py @@ -9,14 +9,25 @@ import glob import os import sys +import pathlib import platform import sysconfig import setuptools import subprocess +import shutil +import multiprocessing from setuptools.command.build_ext import build_ext from distutils.core import setup from distutils.command.install import INSTALL_SCHEMES +# remove build results +for p in ["nexxT/binary", "nexxT/include", "nexxT/tests/binary"]: + if os.path.exists(p): + shutil.rmtree(p, ignore_errors=True) + if os.path.exists(p): + shutil.rmtree(p, ignore_errors=True) + if os.path.exists(p): + shutil.rmtree(p) # create platform specific wheel try: from wheel.bdist_wheel import bdist_wheel as _bdist_wheel @@ -29,11 +40,17 @@ def finalize_options(self): def get_tag(self): python, abi, plat = _bdist_wheel.get_tag(self) # uncomment for non-python extensions - #python, abi = 'py3', 'none' + if platform.system() == "Linux": + abi = "abi3" + else: + abi = "none" + python = "cp37.cp38.cp39" + print("plat=", plat) return python, abi, plat + except ImportError: bdist_wheel = None - + if platform.system() == "Linux": p = "linux_x86_64" presuf = [("lib", ".so")] @@ -43,28 +60,61 @@ def get_tag(self): cv = sysconfig.get_config_vars() -cnexT = cv.get("EXT_PREFIX", "") + "cnexxT" + cv.get("EXT_SUFFIX", "") +if platform.system() == "Windows": + cnexT = cv.get("EXT_PREFIX", "") + "cnexxT" + cv.get("EXT_SUFFIX", "") +elif platform.system() == "Linux": + cnexT = cv.get("EXT_PREFIX", "") + "cnexxT.abi3.so" build_files = [] for variant in ["nonopt", "release"]: build_files.append('nexxT/binary/' + p + '/' + variant + "/" + cnexT) for prefix,suffix in presuf: build_files.append('nexxT/binary/' + p + '/' + variant + "/" + prefix + "nexxT" + suffix) + build_files.append('nexxT/tests/binary/' + p + '/' + variant + "/" + prefix + "test_plugins" + suffix) # generate MANIFEST.in to add build files and include files with open("MANIFEST.in", "w") as manifest: - for fn in glob.glob('nexxT/include/*.hpp'): - manifest.write("include " + fn + "\n") - for bf in build_files: - manifest.write("include " + bf + "\n") - # json schema - manifest.write("include nexxT/core/ConfigFileSchema.json") + manifest.write("include nexxT/examples/*/*.json\n") + manifest.write("include nexxT/examples/*/*.py\n") + manifest.write("include nexxT/core/*.json\n") + manifest.write("include nexxT/src/*.*\n") + manifest.write("include nexxT/tests/src/*.*\n") + manifest.write("include nexxT/tests/core/*.json\n") + manifest.write("include workspace/*.*\n") + manifest.write("include workspace/SConstruct\n") + manifest.write("include workspace/sconstools3/qt5/__init__.py\n") + manifest.write("include LICENSE\n") + manifest.write("include NOTICE\n") + # ok, this is hacky but it is a way to easily include build artefacts into the wheels and this is + # the intention here, drawback is that sdist needs to be generated by a seperate setup.py call + build_required = False + if "bdist_wheel" in sys.argv: + if "sdist" in sys.argv: + raise RuntimeError("cannot build sdist and bdist_wheel with one call.") + build_required = True + for fn in glob.glob('nexxT/include/*.hpp'): + manifest.write("include " + fn + "\n") + for bf in build_files: + manifest.write("include " + bf + "\n") +if build_required: + try: + import PySide2 + except ImportError: + raise RuntimeError("PySide2 must be installed for building the extension module.") + cwd = pathlib.Path().absolute() + os.chdir("workspace") + subprocess.run([sys.executable, os.path.dirname(sys.executable) + "/scons", "-j%d" % multiprocessing.cpu_count(), ".."], check=True) + os.chdir(str(cwd)) + setup(name='nexxT', install_requires=["PySide2 >=5.14.0, <5.15", "shiboken2 >=5.14.0, <5.15", "jsonschema>=3.2.0"], - version='0.0.0', - description='nexxT extensible framework', - author='pca', + version=os.environ.get("NEXXT_VERSION", "0.0.0"), + description='An extensible framework.', + author='Christoph Wiedemann', + author_email='62332054+cwiede@users.noreply.github.com', + url='https://github.com/ifm/nexxT', + license="Apache-2", include_package_data = True, packages=['nexxT', 'nexxT.interface', 'nexxT.tests', 'nexxT.services', 'nexxT.services.gui', 'nexxT.tests.interface', 'nexxT.core', 'nexxT.tests.core'], cmdclass={ diff --git a/workspace/SConstruct b/workspace/SConstruct index 3ce6e48..a4e07ec 100644 --- a/workspace/SConstruct +++ b/workspace/SConstruct @@ -1,17 +1,23 @@ import platform +import PySide2 + +qtversion = ".".join(PySide2.__version__.split(".")[:2]) if platform.system() == "Linux": env = Environment(target_platform="linux_x86_64", + QT5VERSION=qtversion, toolpath=["#/sconstools3"], variant="unknown") + env.Tool("qt5") + env['ENV']['PKG_CONFIG_PATH'] = env.subst("$QT5DIR") + '/lib/pkgconfig' env.PrependENVPath('LD_LIBRARY_PATH', env.subst("$QT5DIR") + "/lib") - env.Tool("qt5") else: # windows environment env = Environment(MSVC_VERSION="14.0", + QT5VERSION=qtversion, tools=["default", "qt5"], toolpath=["#/sconstools3"], target_platform="msvc_x86_64", @@ -20,18 +26,18 @@ else: env.EnableQt5Modules(['QtCore']) env.AddMethod(lambda env, args: args, "RegisterSources") env.AddMethod(lambda env, args: None, "RegisterTargets") +env.Append(CPPDEFINES=["Py_LIMITED_API"]) if platform.system() == "Linux": dbg_env = env.Clone(CCFLAGS=Split("-g -std=c++14 -O0"), #-fvisibility=hidden LINKFLAGS=Split("-g"), - variant="debug") + variant="nonopt") rel_env = env.Clone(CCFLAGS=Split("-std=c++14 -O3"), #-fvisibility=hidden variant="release") else: dbg_env = env.Clone(CCFLAGS=Split("/nologo /EHsc /TP /W3 /Od /Ob2 /Z7 /MD /std:c++14"), LINKFLAGS=Split("/nologo /DEBUG"), - variant="nonopt", - CPPDEFINES=["Py_LIMITED_API"]) # not exactly sure why we need this in debug mode and not in release mode... + variant="nonopt") # not exactly sure why we need this in debug mode and not in release mode... rel_env = env.Clone(CCFLAGS=Split("/nologo /EHsc /TP /W3 /Ox /Z7 /MD /std:c++14"), LINKFLAGS=Split("/nologo /DEBUG"), variant="release")