Browse Source

Make everything lint free.

main
Ben Niemann 6 months ago
parent
commit
077c1bc842
  1. 2
      etc/pylintrc
  2. 9
      noisicaa/model/base.py
  3. 4
      noisicaa/model/project.py
  4. 13
      noisicaa/node_lib/oscilloscope/ui.py
  5. 38
      noisicaa/node_lib/sink/ui.py
  6. 28
      noisicaa/node_lib/ui_registry.py
  7. 5
      noisicaa/node_lib/vumeter/ui.py
  8. 36
      noisicaa/ui/App.py
  9. 5
      noisicaa/ui/CreateNodeDialog.py
  10. 33
      noisicaa/ui/DeviceDB.py
  11. 11
      noisicaa/ui/GraphConnection.py
  12. 101
      noisicaa/ui/GraphNode.py
  13. 68
      noisicaa/ui/GraphView.py
  14. 52
      noisicaa/ui/Project.py
  15. 34
      noisicaa/ui/Project_test.py
  16. 20
      noisicaa/ui/SettingsDialog.py
  17. 27
      noisicaa/ui/main.py
  18. 17
      noisicaa/ui/node_db.py
  19. 23
      noisicaa/ui/qt_helper.pyi
  20. 38
      noisicaa/ui/ui_base.py
  21. 4
      typeshed/PySide2/QtCore.pyi

2
etc/pylintrc

@ -10,7 +10,7 @@ output-format=text
msg-template={path}:{line}:{obj} {msg_id}({symbol}) {msg}
[MESSAGES CONTROL]
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,too-few-public-methods,no-self-use,duplicate-code,invalid-name,too-many-arguments,too-many-branches,too-many-instance-attributes,too-many-statements,typecheck,redefined-builtin
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,too-few-public-methods,no-self-use,duplicate-code,invalid-name,too-many-arguments,too-many-branches,too-many-instance-attributes,too-many-statements,typecheck,redefined-builtin,too-many-public-methods,consider-using-enumerate
[FORMAT]
max-line-length=120

9
noisicaa/model/base.py

@ -18,6 +18,7 @@
#
# @end:license
import enum
import logging
import pprint
import sys
@ -248,6 +249,14 @@ def ScalarProperty(
return prop, sig
ENUM = TypeVar('ENUM', bound=enum.Enum)
def EnumProperty(
name: str, pytype: Type[ENUM], qtype: Union[type, str] = None, *,
default: ENUM = None,
validate: Callable[['BaseObject', ENUM], None] = None
) -> Tuple['QtCore.Property[ENUM]', QtCore.Signal]:
return ScalarProperty(name, pytype, qtype, default=default, validate=validate) # type: ignore
_qobj_types = (QtCore.QRectF, QtGui.QColor)
QOBJ = TypeVar('QOBJ', QtCore.QRectF, QtGui.QColor)

4
noisicaa/model/project.py

@ -48,9 +48,9 @@ class ConnectIOBuffer(EngineChange):
class Port(base.BaseObject):
name, nameChanged = base.ScalarProperty('uri', str)
direction, directionChanged = base.ScalarProperty(
direction, directionChanged = base.EnumProperty(
'direction', engine.PortDirection, qtype=int, default=engine.PortDirection.INPUT)
type, typeChanged = base.ScalarProperty('type', engine.PortType, qtype=int, default=engine.PortType.NOT_SET)
type, typeChanged = base.EnumProperty('type', engine.PortType, qtype=int, default=engine.PortType.NOT_SET)
hasControlValue, hasControlValueChanged = base.ScalarProperty('hasControlValue', bool)
controlValue, controlValueChanged = base.ScalarProperty('controlValue', float)

13
noisicaa/node_lib/oscilloscope/ui.py

@ -19,6 +19,7 @@
# @end:license
import logging
from typing import Any
from PySide2 import QtCore
@ -31,38 +32,38 @@ logger = logging.getLogger(__name__)
class Node(GraphNode):
addSamples = QtCore.Signal(int, list)
def __init__(self, **kwargs):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.setNodeMessageHandler(self.__handleNodeMessage)
def __handleNodeMessage(self, msg_serialized: bytes):
def __handleNodeMessage(self, msg_serialized: bytes) -> None:
msg = node_message_fb.NodeMessage(msg_serialized)
self.addSamples.emit(msg.sample_rate, msg.samples)
@QtCore.Slot(int)
def setTimeScale(self, v):
def setTimeScale(self, v: int) -> None:
if v == self.node.timeScale:
return
with self.project.captureChanges("{}: Set time scale".format(self.node.title)):
self.node.timeScale = v
@QtCore.Slot(int)
def setYScale(self, v):
def setYScale(self, v: int) -> None:
if v == self.node.yScale:
return
with self.project.captureChanges("{}: Set Y scale".format(self.node.title)):
self.node.yScale = v
@QtCore.Slot(float)
def setYOffset(self, v):
def setYOffset(self, v: float) -> None:
if v == self.node.yOffset:
return
with self.project.captureChanges("{}: Set Y offset".format(self.node.title)):
self.node.yOffset = v
@QtCore.Slot(int)
def setHoldTime(self, v):
def setHoldTime(self, v: int) -> None:
if v == self.node.holdTime:
return
with self.project.captureChanges("{}: Set hold time".format(self.node.title)):

38
noisicaa/node_lib/sink/ui.py

@ -19,12 +19,14 @@
# @end:license
import logging
from typing import Any, Dict, List
from PySide2.QtCore import Qt
from PySide2 import QtCore
from noisicaa import engine
from noisicaa.ui import ui_base
from noisicaa.ui.DeviceDB import DeviceDB, Device
from noisicaa.ui.GraphNode import GraphNode
logger = logging.getLogger(__name__)
@ -33,18 +35,18 @@ logger = logging.getLogger(__name__)
class PortList(QtCore.QAbstractListModel):
changed = QtCore.Signal()
def __init__(self, parent=None):
def __init__(self, parent: QtCore.QObject = None) -> None:
super().__init__(parent)
self.__ports = []
self.__ports = [] # type: List[str]
def clear(self):
def clear(self) -> None:
if self.__ports:
self.beginRemoveRows(QtCore.QModelIndex(), 0, len(self.__ports) - 1)
self.__ports.clear()
self.endRemoveRows()
def setFromDevice(self, device):
def setFromDevice(self, device: Device) -> None:
self.clear()
ports = [
@ -58,15 +60,15 @@ class PortList(QtCore.QAbstractListModel):
self.changed.emit()
def roleNames(self):
def roleNames(self) -> Dict[int, bytes]:
return {
Qt.DisplayRole: b'portName',
}
def rowCount(self, parent=QtCore.QModelIndex()):
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: # pylint: disable=unused-argument
return len(self.__ports)
def data(self, index, role=Qt.DisplayRole):
def data(self, index: QtCore.QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
return None
if index.row() < 0 or index.row() >= len(self.__ports):
@ -78,11 +80,15 @@ class PortList(QtCore.QAbstractListModel):
class SinkDevices(QtCore.QSortFilterProxyModel):
def __init__(self, deviceDB, parent=None):
def __init__(self, deviceDB: DeviceDB, parent: QtCore.QObject = None) -> None:
super().__init__(parent)
self.setSourceModel(deviceDB)
def filterAcceptsRow(self, source_row, source_parent):
def filterAcceptsRow(
self,
source_row: int,
source_parent: QtCore.QModelIndex # pylint: disable=unused-argument
) -> bool:
device = self.sourceModel()[source_row]
return any(
port.type == engine.PortType.AUDIO and port.direction == engine.PortDirection.INPUT
@ -92,7 +98,7 @@ class SinkDevices(QtCore.QSortFilterProxyModel):
class Node(GraphNode):
isConnected, isConnectedChanged = ui_base.Property('isConnected', bool)
def __init__(self, **kwargs):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__devices = SinkDevices(self.app.deviceDB, self)
@ -104,15 +110,15 @@ class Node(GraphNode):
self.node.deviceChanged.connect(lambda _: self.__updateIsConnected())
@ui_base.ConstProperty(QtCore.QObject)
def devices(self):
def devices(self) -> QtCore.QAbstractItemModel:
return self.__devices
@ui_base.ConstProperty(QtCore.QObject)
def devicePorts(self):
def devicePorts(self) -> QtCore.QAbstractItemModel:
return self.__devicePorts
@QtCore.Slot(str)
def setDevice(self, v):
def setDevice(self, v: str) -> None:
if v == self.node.device:
return
@ -128,14 +134,14 @@ class Node(GraphNode):
self.node.devicePorts[i] = ports[i] if i < len(ports) else ''
@QtCore.Slot(int, str)
def setDevicePort(self, idx, v):
def setDevicePort(self, idx: int, v: str) -> None:
if v == self.node.devicePorts[idx]:
return
with self.project.captureChanges("{}: Change device port".format(self.node.title)):
self.node.devicePorts[idx] = v
def __updateDevicePorts(self):
def __updateDevicePorts(self) -> None:
try:
device = self.app.deviceDB.getDevice(self.node.device)
except KeyError:
@ -143,7 +149,7 @@ class Node(GraphNode):
else:
self.__devicePorts.setFromDevice(device)
def __updateIsConnected(self):
def __updateIsConnected(self) -> None:
for r in range(self.__devices.rowCount()):
if self.__devices.data(self.__devices.index(r, 0)) == self.node.device:
self.isConnected = True

28
noisicaa/node_lib/ui_registry.py

@ -22,13 +22,18 @@ import importlib
import logging
import os
import os.path
import typing
from typing import Dict, Type
if typing.TYPE_CHECKING:
from noisicaa.ui.GraphNode import GraphNode
logger = logging.getLogger(__name__)
class UIRegistry:
def __init__(self):
self.classes = {}
def __init__(self) -> None:
self.classes = {} # type: Dict[str, Type[GraphNode]]
base = os.path.dirname(__file__)
for node in os.listdir(base):
@ -43,22 +48,27 @@ class UIRegistry:
logger.info("Importing UI module for node '%s'...", node)
ui_mod_name = 'noisicaa.node_lib.{}.{}'.format(node, 'ui')
ui_mod_name = 'noisicaa.node_lib.{}.ui'.format(node)
ui_mod = importlib.import_module(ui_mod_name)
cls = getattr(ui_mod, 'CLASS')
assert cls is not None, ui_mod_name
uri = getattr(ui_mod, 'URI')
assert uri is not None, ui_mod_name
seen = set()
for c in ui_mod.CLASS.__mro__:
for c in cls.__mro__:
# https://bugreports.qt.io/browse/PYSIDE-983
assert c.__name__ not in seen, (
"Duplicate class name {} in MRO of class {}.{}".format(
c.__name__, ui_mod.CLASS.__module__, ui_mod.CLASS.__qualname__))
c.__name__, cls.__module__, cls.__qualname__))
seen.add(c.__name__)
assert ui_mod.URI not in self.classes, (
"Duplicate URI {} in {}".format(ui_mod.URI, ui_mod_name))
self.classes[ui_mod.URI] = ui_mod.CLASS
assert uri not in self.classes, (
"Duplicate URI {} in {}".format(uri, ui_mod_name))
self.classes[uri] = cls
def getClass(self, uri):
def getClass(self, uri: str) -> 'Type[GraphNode]':
try:
return self.classes[uri]
except KeyError:

5
noisicaa/node_lib/vumeter/ui.py

@ -19,6 +19,7 @@
# @end:license
import logging
from typing import Any
from noisicaa.ui import ui_base
from noisicaa.ui.GraphNode import GraphNode
@ -31,12 +32,12 @@ class Node(GraphNode):
current, currentChanged = ui_base.Property('current', float, default=-80.0)
peak, peakChanged = ui_base.Property('peak', float, default=-80.0)
def __init__(self, **kwargs):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.setNodeMessageHandler(self.__handleNodeMessage)
def __handleNodeMessage(self, msg_serialized: bytes):
def __handleNodeMessage(self, msg_serialized: bytes) -> None:
msg = node_message_fb.NodeMessage(msg_serialized)
self.current = msg.current
self.peak = msg.peak

36
noisicaa/ui/App.py

@ -21,11 +21,13 @@
import contextlib
import os
import os.path
from typing import Dict, Iterator, List
from PySide2.QtCore import Qt
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtQml
from PySide2 import QtQuick
import flatbuffers
import noisicaa
@ -44,24 +46,24 @@ logger = logging.getLogger(__name__)
class RecentProjects(QtCore.QStringListModel):
def __init__(self, parent):
def __init__(self, parent: QtCore.QObject) -> None:
super().__init__(parent)
self.__paths = []
self.__paths = [] # type: List[str]
def paths(self):
def paths(self) -> List[str]:
return list(self.__paths)
def setPaths(self, lst):
def setPaths(self, lst: List[str]) -> None:
self.__paths = list(lst)
self.setStringList(self.__paths)
def roleNames(self):
def roleNames(self) -> Dict[int, bytes]:
return {
Qt.DisplayRole: b'path',
}
def addPath(self, path):
def addPath(self, path: str) -> None:
for idx, p in reversed(list(enumerate(self.__paths))):
if p == path:
del self.__paths[idx]
@ -98,17 +100,17 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
self._project = None # type: Project
self._qmlEngine = None # type: QtQml.QQmlApplicationEngine
self._projectWindowComponent = None # type: QtQml.QQmlComponent
self._window = None # type: Window
self._window = None # type: QtQuick.QQuickItem
@ui_base.ConstProperty(str)
def appVersion(self) -> str:
return noisicaa.__version__
@ui_base.ConstProperty(QtCore.QObject)
def recentProjects(self):
def recentProjects(self) -> RecentProjects:
return self._recentProjects
def addRecentProject(self, path):
def addRecentProject(self, path: str) -> None:
self._recentProjects.addPath(path)
self.settings.setValue('recentProjects', self._recentProjects.paths())
@ -145,7 +147,7 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
raise NotImplementedError
@contextlib.contextmanager
def settingsGroup(self, group) -> QtCore.QSettings:
def settingsGroup(self, group: str) -> Iterator[QtCore.QSettings]:
self.settings.beginGroup(group)
try:
yield self.settings
@ -156,7 +158,7 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
def engine(self) -> engine_lib.Engine:
return self._engine
def createQmlComponent(self, path):
def createQmlComponent(self, path: str) -> QtQml.QQmlComponent:
path = os.path.join(os.path.dirname(os.path.dirname(noisicaa.__file__)), path)
url = QtCore.QUrl.fromLocalFile(path)
component = QtQml.QQmlComponent(self.qmlEngine)
@ -167,9 +169,9 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
raise RuntimeError("Failed to load component '{}'".format(path))
return component
def updateAudioBackend(self):
def updateAudioBackend(self) -> None:
with self.settingsGroup('audio') as settings:
backend = settings.value('backend', 'jack')
backend = str(settings.value('backend', 'jack'))
builder = flatbuffers.Builder(1024)
builder.Finish(engine_lib.CreateBackendSettings(
@ -182,7 +184,7 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
return SettingsDialog(parent=self, context=self.__context)
@QtCore.Slot()
def newProject(self):
def newProject(self) -> None:
logger.info("New project...")
if self._project is not None:
self._project.cleanup()
@ -194,7 +196,7 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
self._window.setProperty('d', self._project)
@QtCore.Slot(str)
def openProject(self, path):
def openProject(self, path: str) -> None:
if path.startswith('file://'):
path = path[7:]
logger.info("Open project '%s'...", path)
@ -211,7 +213,7 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
self._window.setProperty('d', self._project)
@QtCore.Slot()
def showEngineProgramWindow(self):
def showEngineProgramWindow(self) -> None:
if self.__engineProgramWindow is None:
self.__engineProgramComponent = self.createQmlComponent(
os.path.join(os.path.dirname(__file__), 'EngineProgramWindow.qml'))
@ -222,5 +224,5 @@ class App(ui_base.PropertyContainer, QtCore.QObject):
self.__engineProgramWindow.show()
self.__engineProgramWindow.requestActivate()
def __programChanged(self):
def __programChanged(self, unused_: object=None) -> None:
self.engineProgram = self._engine.program_code()

5
noisicaa/ui/CreateNodeDialog.py

@ -19,6 +19,7 @@
# @end:license
import logging
from typing import Any
from PySide2.QtCore import Qt
from PySide2 import QtCore
@ -29,15 +30,15 @@ logger = logging.getLogger(__name__)
class CreateNodeDialog(ui_base.ProjectMixin, QtCore.QObject):
def __init__(self, **kwargs) -> None:
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__model = QtCore.QSortFilterProxyModel(self)
self.__model.setSourceModel(self.app.nodeDB)
@ui_base.ConstProperty(QtCore.QObject)
def nodeDB(self) -> QtCore.QAbstractItemModel:
return self.__model
nodeDB = QtCore.Property(QtCore.QObject, fget=nodeDB, constant=True)
@QtCore.Slot(str)
def setFilter(self, filter: str) -> None:

33
noisicaa/ui/DeviceDB.py

@ -20,29 +20,32 @@
import bisect
import logging
from typing import Any, Dict, Iterator, List
from PySide2.QtCore import Qt
from PySide2 import QtCore
from noisicaa import engine
logger = logging.getLogger(__name__)
class Port:
def __init__(self, name, direction, type):
def __init__(self, name: str, direction: engine.PortDirection, type: engine.PortType) -> None:
self.name = name
self.direction = direction
self.type = type
class Device:
def __init__(self, name):
def __init__(self, name: str) -> None:
self.name = name
self.ports = []
self.ports = [] # type: List[Port]
def addPort(self, name, direction, type):
def addPort(self, name: str, direction: engine.PortDirection, type: engine.PortType) -> None:
self.ports.append(Port(name, direction, type))
def __lt__(self, other):
def __lt__(self, other: object) -> bool:
if isinstance(other, str):
return self.name < other
if isinstance(other, Device):
@ -51,29 +54,29 @@ class Device:
class DeviceDB(QtCore.QAbstractListModel):
def __init__(self, parent=None):
def __init__(self, parent: QtCore.QObject = None) -> None:
super().__init__(parent)
self.__devices = []
self.__devices = [] # type: List[Device]
def __iter__(self):
def __iter__(self) -> Iterator[Device]:
yield from self.__devices
def __getitem__(self, idx):
def __getitem__(self, idx: int) -> Device:
return self.__devices[idx]
def __len__(self):
def __len__(self) -> int:
return len(self.__devices)
def roleNames(self):
def roleNames(self) -> Dict[int, bytes]:
return {
Qt.DisplayRole: b'deviceName',
}
def rowCount(self, parent=QtCore.QModelIndex()):
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: # pylint: disable=unused-argument
return len(self.__devices)
def data(self, index, role=Qt.DisplayRole):
def data(self, index: QtCore.QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
return None
if index.row() < 0 or index.row() >= len(self.__devices):
@ -84,13 +87,13 @@ class DeviceDB(QtCore.QAbstractListModel):
return device.name
return None
def getDevice(self, name):
def getDevice(self, name: str) -> Device:
idx = bisect.bisect_left(self.__devices, name)
if idx < len(self.__devices) and self.__devices[idx].name == name:
return self.__devices[idx]
raise KeyError(name)
def addDevice(self, name):
def addDevice(self, name: str) -> Device:
idx = bisect.bisect_left(self.__devices, name)
assert idx >= len(self.__devices) or self.__devices[idx].name != name, "Device {} already exists".format(name)

11
noisicaa/ui/GraphConnection.py

@ -19,6 +19,7 @@
# @end:license
import logging
from typing import Any, Dict
from PySide2 import QtCore
@ -33,24 +34,24 @@ logger = logging.getLogger(__name__)
class GraphConnection(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject):
highlighted, highlightedChanged = ui_base.Property('highlighted', bool)
def __init__(self, *, connection, nodeMap, **kwargs):
def __init__(self, *, connection: model.Connection, nodeMap: Dict[int, GraphNode], **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__connection = connection
self.__srcNode = nodeMap[self.__connection.srcNodeId]
self.__destNode = nodeMap[self.__connection.destNodeId]
def __str__(self):
def __str__(self) -> str:
return '<{} {}>'.format(type(self).__name__, self.__connection)
@ui_base.ConstProperty(model.Connection)
def connection(self):
def connection(self) -> model.Connection:
return self.__connection
@ui_base.ConstProperty(GraphNode)
def srcNode(self):
def srcNode(self) -> GraphNode:
return self.__srcNode
@ui_base.ConstProperty(GraphNode)
def destNode(self):
def destNode(self) -> GraphNode:
return self.__destNode

101
noisicaa/ui/GraphNode.py

@ -21,11 +21,13 @@
import logging
import sys
import textwrap
from typing import Any, Callable, Dict, List
from PySide2.QtCore import Qt
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
from PySide2 import QtQml
from PySide2 import QtQuick
from noisicaa import model
from noisicaa import engine
@ -51,7 +53,7 @@ class GraphNodePort(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObj
glowing, glowingChanged = ui_base.Property('glowing', bool)
numConnections, numConnectionsChanged = ui_base.Property('numConnection', int)
def __init__(self, *, node, port, **kwargs):
def __init__(self, *, node: 'GraphNode', port: model.Port, **kwargs: Any) -> None:
super().__init__(parent=node, **kwargs)
self.__node = node
@ -62,11 +64,11 @@ class GraphNodePort(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObj
# Can't declare this as GraphNode, because this would cause a dependency cycle.
@ui_base.ConstProperty(QtCore.QObject)
def node(self):
def node(self) -> 'GraphNode':
return self.__node
@ui_base.ConstProperty(model.Port)
def port(self):
def port(self) -> model.Port:
return self.__port
@QtCore.Slot(float)
@ -81,17 +83,15 @@ class GraphNodePort(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObj
class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject):
def __init__(self, *, node, **kwargs):
def __init__(self, *, node: model.GraphNode, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__node_message_listener = None
self.__node = node
self.__description = self.app.nodeDB.nodeDescription(self.__node.uri)
self.__ports = {}
self.__in_ports = []
self.__out_ports = []
self.__ports = {} # type: Dict[str, GraphNodePort]
self.__in_ports = [] # type: List[GraphNodePort]
self.__out_ports = [] # type: List[GraphNodePort]
for port in self.__node.ports:
wrapper = GraphNodePort(node=self, port=port, context=self.context)
self.__ports[port.name] = wrapper
@ -107,41 +107,43 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
self.__active = False
self.__selected = False
self.__nodeMessageListener = None
self.__nodeMessageListener = None # type: int
self.__body = None
self.__bodyComponent = None
self.__body = None # type: QtQuick.QQuickItem
self.__bodyComponent = None # type: QtQml.QQmlComponent
def cleanup(self):
def cleanup(self) -> None:
if self.__nodeMessageListener is not None:
self.engine.remove_node_message_listener(self.__node.id, self.__nodeMessageListener)
self.__nodeMessageListener = None
def setNodeMessageHandler(self, handler):
def setNodeMessageHandler(self, handler: Callable[[bytes], None]) -> None:
assert self.__nodeMessageListener is None
self.__nodeMessageListener = self.engine.add_node_message_listener(self.__node.id, handler)
def __str__(self):
def __str__(self) -> str:
return '<{} {}>'.format(type(self).__name__, self.__node)
@ui_base.ConstProperty(model.GraphNode)
def node(self):
def node(self) -> model.GraphNode:
return self.__node
@ui_base.ConstProperty(engine.NodeDescription)
def description(self):
def description(self) -> engine.NodeDescription:
return self.__description
@ui_base.ConstProperty(QtCore.QObject)
def body(self):
def body(self) -> QtQuick.QQuickItem:
if self.__body is None and self.__description.ui is not None and self.__description.ui.body_qml is not None:
logger.info("%s#%016x: Loading body component '%s'...", self.__node.uri, self.__node.id, self.__description.ui.body_qml)
logger.info(
"%s#%016x: Loading body component '%s'...",
self.__node.uri, self.__node.id, self.__description.ui.body_qml)
self.__bodyComponent = self.app.createQmlComponent(self.__description.ui.body_qml)
self.__body = self.__bodyComponent.createWithInitialProperties({'d': self})
return self.__body
@QtCore.Slot()
def moveDone(self):
def moveDone(self) -> None:
with self.project.captureChanges("{}: Move node".format(self.__node.title)):
self.__node.rect = QtCore.QRectF(
int(self.__rect.x() / self.__zoom + 10) // 20 * 20,
@ -150,7 +152,7 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
int(self.__rect.height() / self.__zoom + 10) // 20 * 20)
@QtCore.Slot()
def resizeDone(self):
def resizeDone(self) -> None:
with self.project.captureChanges("{}: Resize node".format(self.__node.title)):
self.__node.rect = QtCore.QRectF(
self.__rect.x() / self.__zoom,
@ -159,34 +161,13 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
self.__rect.height() / self.__zoom)
@QtCore.Slot()
def showContextMenu(self):
menu = QtWidgets.QMenu(self.__view)
rename_action = menu.addAction("Rename...")
rename_action.triggered.connect(self.__rename)
color_menu = menu.addMenu("Set color")
color_action = SelectColorAction(color_menu)
color_action.colorSelected.connect(self.__setColor)
color_menu.addAction(color_action)
menu.addAction("Cut")
menu.addAction("Copy")
menu.addAction("Duplicate")
delete_action = menu.addAction("Delete")
delete_action.triggered.connect(self.delete)
if not menu.isEmpty():
menu.popup(QtGui.QCursor.pos())
@QtCore.Slot()
def delete(self):
def delete(self) -> None:
with self.project.captureChanges("{}: Delete node".format(self.__node.title)):
self.model.deleteNode(self.__node)
def getRect(self):
def getRect(self) -> QtCore.QRectF:
return self.__rect
def setRect(self, r):
def setRect(self, r: QtCore.QRectF) -> None:
r = QtCore.QRectF(
int(r.x() / self.__zoom + 10) // 20 * 20 * self.__zoom,
int(r.y() / self.__zoom + 10) // 20 * 20 * self.__zoom,
@ -223,7 +204,7 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
rectChanged = QtCore.Signal(QtCore.QRectF)
rect = QtCore.Property(QtCore.QRectF, fget=getRect, fset=setRect, notify=rectChanged)
def __nodeRectChanged(self, rect):
def __nodeRectChanged(self, rect: QtCore.QRectF) -> None:
self.setRect(QtCore.QRectF(
int(self.__zoom * rect.x()),
int(self.__zoom * rect.y()),
@ -231,22 +212,22 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
int(self.__zoom * rect.height())))
@QtCore.Slot(str, result=GraphNodePort)
def port(self, portName):
def port(self, portName: str) -> GraphNodePort:
return self.__ports.get(portName)
def ports(self):
@ui_base.ConstProperty(List[GraphNodePort], list)
def ports(self) -> List[GraphNodePort]:
return [self.__ports[port.name] for port in self.__node.ports]
ports = QtCore.Property(list, fget=ports, constant=True)
def setZoom(self, v):
def setZoom(self, v: float) -> None:
if v == self.__zoom:
return
self.__zoom = v
self.__nodeRectChanged(self.__node.rect)
def isActive(self):
def isActive(self) -> bool:
return self.__active
def setActive(self, v):
def setActive(self, v: bool) -> None:
if v == self.__active:
return
self.__active = v
@ -254,9 +235,9 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
activeChanged = QtCore.Signal(bool)
active = QtCore.Property(bool, fget=isActive, fset=setActive, notify=activeChanged)
def isSelected(self):
def isSelected(self) -> bool:
return self.__selected
def setSelected(self, v):
def setSelected(self, v: bool) -> None:
if v == self.__selected:
return
self.__selected = v
@ -270,7 +251,8 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
with self.project.captureChanges("{}: Rename node".format(self.__node.title)):
self.__node.title = title
def nodeColors(self):
@ui_base.ConstProperty(List[QtGui.QColor], list)
def nodeColors(self) -> List[QtGui.QColor]:
return [
QtGui.QColor(180, 180, 180),
QtGui.QColor(205, 205, 205),
@ -282,8 +264,8 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
QtGui.QColor(255, 205, 205),
QtGui.QColor(255, 230, 230),
QtGui.QColor(255, 155, 0.1),
QtGui.QColor(255, 180, 0.3),
QtGui.QColor(255, 155, 26),
QtGui.QColor(255, 180, 77),
QtGui.QColor(255, 205, 155),
QtGui.QColor(255, 230, 205),
@ -312,7 +294,6 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
QtGui.QColor(205, 255, 255),
QtGui.QColor(230, 255, 255),
]
nodeColors = QtCore.Property(list, fget=nodeColors, constant=True)
@QtCore.Slot(QtGui.QColor)
def setColor(self, color: QtGui.QColor) -> None:
@ -321,7 +302,7 @@ class GraphNode(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
self.__node.color = color
@QtCore.Slot(result=str)
def debugInfo(self):
def debugInfo(self) -> str:
info = []
info.append(str(self))
info.append("class: {}".format(type(self).__qualname__))

68
noisicaa/ui/GraphView.py

@ -20,6 +20,7 @@
import functools
import logging
from typing import Any, Dict, Generic, Iterator, List, Sequence, TypeVar
from PySide2.QtCore import Qt
from PySide2 import QtCore
@ -32,26 +33,26 @@ from .GraphConnection import GraphConnection
logger = logging.getLogger(__name__)
class ObjectList(QtCore.QAbstractListModel):
def __init__(self, parent=None):
OBJ = TypeVar('OBJ')
class ObjectList(Generic[OBJ], QtCore.QAbstractListModel):
def __init__(self, parent: QtCore.QObject = None) -> None:
super().__init__(parent)
self.__list = []
self.__list = [] # type: List[OBJ]
def __iter__(self):
def __iter__(self) -> Iterator[OBJ]:
yield from self.__list
def __len__(self):
def __len__(self) -> int:
return len(self.__list)
def __getitem__(self, idx):
def __getitem__(self, idx: int) -> OBJ:
return self.__list[idx]
def rowCount(self, parent=QtCore.QModelIndex()):
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: # pylint: disable=unused-argument
return len(self.__list)
def data(self, index, role=Qt.DisplayRole):
def data(self, index: QtCore.QModelIndex, role: int = Qt.DisplayRole) -> OBJ:
if role != Qt.DisplayRole:
return None
if not index.isValid():
@ -60,12 +61,12 @@ class ObjectList(QtCore.QAbstractListModel):
return None
return self.__list[index.row()]
def insertObject(self, row, obj):
def insertObject(self, row: int, obj: OBJ) -> None:
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.__list.insert(row, obj)
self.endInsertRows()
def removeObject(self, row):
def removeObject(self, row: int) -> OBJ:
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
obj = self.__list.pop(row)
self.endRemoveRows()
@ -78,16 +79,16 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
offset, offsetChanged = ui_base.Property('offset', QtCore.QPointF)
contentRect, contentRectChanged = ui_base.Property('contextRect', QtCore.QRectF)
def __init__(self, **kwargs):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__nodes = ObjectList(self)
self.__nodeMap = {}
self.__nodes = ObjectList[GraphNode](self)
self.__nodeMap = {} # type: Dict[int, GraphNode]
for idx, node in enumerate(self.model.nodes):
self.__addNode(idx, node)
self.model.nodesChanged.connect(self.__nodesChanged)
self.__connections = ObjectList(self)
self.__connections = ObjectList[GraphConnection](self)
for idx, connection in enumerate(self.model.connections):
self.__addConnection(idx, connection)
self.model.connectionsChanged.connect(self.__connectionsChanged)
@ -99,20 +100,20 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
self.__selection = QtCore.QItemSelectionModel(self.__nodes, self)
self.__selection.selectionChanged.connect(self.__selectionChanged)
def cleanup(self):
def cleanup(self) -> None:
while self.__connections:
self.__removeConnection(0)
while self.__nodes:
self.__removeNode(0)
def saveState(self):
def saveState(self) -> Dict[str, Any]:
return {
'x': self.offset.x(),
'y': self.offset.y(),
'zoom': self.zoom,
}
def restoreState(self, state):
def restoreState(self, state: Dict[str, Any]) -> None:
self.offset = QtCore.QPointF(
float(state.get('x', 0.0)), float(state.get('y', 0.0)))
self.zoom = float(state.get('zoom', 1.0))
@ -121,24 +122,24 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
def selection(self) -> QtCore.QItemSelectionModel:
return self.__selection
def hasSelection(self) -> bool:
def getHasSelection(self) -> bool:
return self.__hasSelection
hasSelectionChanged = QtCore.Signal(bool)
hasSelection = QtCore.Property(bool, fget=hasSelection, notify=hasSelectionChanged)
hasSelection = QtCore.Property(bool, fget=getHasSelection, notify=hasSelectionChanged)
@ui_base.ConstProperty(QtCore.QObject)
def nodes(self):
def nodes(self) -> ObjectList[GraphNode]:
return self.__nodes
@ui_base.ConstProperty(QtCore.QObject)
def connections(self):
def connections(self) -> ObjectList[GraphConnection]:
return self.__connections
def mapFromGlobal(self, pos: QtCore.QPointF) -> QtCore.QPointF:
return (self.__item.mapFromGlobal(pos) - self.offset) / self.zoom
@QtCore.Slot()
def selectAll(self):
def selectAll(self) -> None:
self.__selection.select(
QtCore.QItemSelection(self.__nodes.index(0), self.__nodes.index(self.__nodes.rowCount() - 1)),
QtCore.QItemSelectionModel.Select)
@ -167,7 +168,7 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
node.rect = node.rect.translated(delta)
@QtCore.Slot(GraphNodePort, result=list)
def getTargetPorts(self, src: GraphNodePort) -> None:
def getTargetPorts(self, src: GraphNodePort) -> Sequence[GraphNodePort]:
ports = []
for wrapper in self.__nodes:
for port in wrapper.ports:
@ -198,11 +199,12 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
srcPort = src.port.name
destNode = dest.node.node
destPort = dest.port.name
with self.project.captureChanges("Connect '{}:{}' to '{}:{}'".format(srcNode.title, srcPort, destNode.title, destPort)):
with self.project.captureChanges(
"Connect '{}:{}' to '{}:{}'".format(srcNode.title, srcPort, destNode.title, destPort)):
self.model.connectPorts(srcNode, srcPort, destNode, destPort)
@QtCore.Slot(GraphConnection)
def disconnectPorts(self, wrapper: GraphConnection):
def disconnectPorts(self, wrapper: GraphConnection) -> None:
self.unhighlightConnection(wrapper)
conn = wrapper.connection
with self.project.captureChanges("Disconnect '{}:{}' from '{}:{}'".format(
@ -231,7 +233,7 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
self.__updateContentRect()
def __addNode(self, index, node):
def __addNode(self, index: int, node: model.GraphNode) -> None:
nodeDesc = self.app.nodeDB.nodeDescription(node.uri)
if nodeDesc.ui is not None and nodeDesc.ui.uri is not None:
wrapperClass = self.app.uiRegistry.getClass(nodeDesc.ui.uri)
@ -249,13 +251,13 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
self.__nodeMap[node.id] = wrapper
self.__nodes.insertObject(index, wrapper)
def __removeNode(self, index):
def __removeNode(self, index: int) -> None:
wrapper = self.__nodes.removeObject(index)
wrapper.cleanup()
self.__nodeMap.pop(wrapper.node.id)
@QtCore.Slot(QtCore.QPointF, str)
def insertNode(self, pos: QtCore.QPointF, uri: str):
def insertNode(self, pos: QtCore.QPointF, uri: str) -> None:
desc = self.app.nodeDB.nodeDescription(uri)
title = desc.display_name or desc.uri
with self.project.captureChanges("Add node '{}'".format(title)):
@ -265,13 +267,13 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
title=title,
rect=QtCore.QRectF(pos, QtCore.QSizeF(100, 100)))
def __activeNodeChanged(self, node, active):
def __activeNodeChanged(self, node: GraphNode, active: bool) -> None:
if active:
for n in self.__nodes:
if n is not node:
n.active = False
def __selectionChanged(self, selected, deselected):
def __selectionChanged(self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection) -> None:
for index in selected.indexes():
node = self.__nodes.data(index)
node.selected = True
@ -292,7 +294,7 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
else:
raise TypeError(type(change))
def __addConnection(self, index, connection):
def __addConnection(self, index: int, connection: model.Connection) -> None:
srcPort = self.__nodeMap[connection.srcNodeId].port(connection.srcPort)
srcPort.numConnections += 1
destPort = self.__nodeMap[connection.destNodeId].port(connection.destPort)
@ -304,7 +306,7 @@ class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject)
context=self.context)
self.__connections.insertObject(index, wrapper)
def __removeConnection(self, index):
def __removeConnection(self, index: int) -> None:
wrapper = self.__connections.removeObject(index)
connection = wrapper.connection
srcPort = self.__nodeMap[connection.srcNodeId].port(connection.srcPort)

52
noisicaa/ui/Project.py

@ -24,7 +24,7 @@ import json
import logging
import os
import os.path
from typing import Generator
from typing import Any, Dict, Generator, List, Sequence, Tuple
from PySide2 import QtCore
from PySide2 import QtWidgets
@ -40,14 +40,14 @@ logger = logging.getLogger(__name__)
class Command(QtWidgets.QUndoCommand):
def __init__(self, title, project, changes):
def __init__(self, title: str, project: 'Project', changes: Sequence[model_lib.ModelChange]) -> None:
super().__init__(title)
self.__project = project
self.__changes = changes
self.__state = 'created'
def undo(self):
def undo(self) -> None:
logger.info("undo '%s'", self.text())
assert self.__state == 'done'
@ -69,7 +69,7 @@ class Command(QtWidgets.QUndoCommand):
self.__state = 'undone'
def redo(self):
def redo(self) -> None:
# QUndoStack calls Command.redo() when the command is pushed onto the stack. But we already
# executed the actions, so there's nothing to do here.
if self.__state == 'created':
@ -101,7 +101,14 @@ class Command(QtWidgets.QUndoCommand):
class Project(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject):
path, pathChanged = ui_base.Property('path', str)
def __init__(self, *, path, project, viewState, context: ui_base.CommonContext, **kwargs):
def __init__(
self, *,
path: str,
project: model_lib.Project,
viewState: Dict[str, Any],
context: ui_base.CommonContext,
**kwargs: Any
) -> None:
super().__init__(
context=ui_base.ProjectContext(
app=context.app,
@ -145,7 +152,10 @@ class Project(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject):
for node in self.__project.nodes:
self.__nodeAdded(node)
for conn in self.__project.connections:
self.engine.connect_ports(conn.srcNodeId, conn.srcPort, conn.destNodeId, conn.destPort, engine.PortType.AUDIO)
self.engine.connect_ports(
conn.srcNodeId, conn.srcPort,
conn.destNodeId, conn.destPort,
engine.PortType.AUDIO)
self.engine.compile_graph()
self.__nodesListener = self.__project.nodesChanged.connect(self.__nodesChanged)
self.__connectionsListener = self.__project.connectionsChanged.connect(self.__connectionsChanged)
@ -187,8 +197,8 @@ class Project(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject):
MAGIC = '# NOISICAA-PROJECT\n'
@classmethod
def load(cls, path: str, classRegistry) -> model_lib.Project:
with open(path, 'r') as fp:
def load(cls, path: str, classRegistry: model_lib.ClassRegistry) -> Tuple[model_lib.Project, Dict[str, Any]]:
with open(path, 'r', encoding='utf-8') as fp:
magic = fp.read(len(cls.MAGIC))
if magic != cls.MAGIC:
raise RuntimeError
@ -196,9 +206,10 @@ class Project(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject):
data = json</