An open source DAW for GNU/Linux, inspired by modular synths. http://noisicaa.odahoda.de/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

350 lines
13 KiB

# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import functools
import logging
import os.path
import random
from typing import Any
from PySide2.QtCore import Qt
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
from PySide2 import QtQml
from noisicaa import model
from noisicaa import engine
from noisicaa.ui import ui_base
from .GraphNode import GraphNodeWrapper, GraphNodePort
from .GraphConnection import GraphConnectionWrapper
logger = logging.getLogger(__name__)
class ObjectList(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super().__init__(parent)
self.__list = []
def __iter__(self):
yield from self.__list
def __len__(self):
return len(self.__list)
def __getitem__(self, idx):
return self.__list[idx]
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__list)
def data(self, index, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return None
if not index.isValid():
return None
if index.row() < 0 or index.row() >= len(self.__list):
return None
return self.__list[index.row()]
def insertObject(self, row, obj):
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.__list.insert(row, obj)
self.endInsertRows()
def removeObject(self, row):
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
obj = self.__list.pop(row)
self.endRemoveRows()
return obj
class GraphView(ui_base.ProjectMixin, ui_base.PropertyContainer, QtCore.QObject):
highlightedConnection, highlightedConnectionChanged = ui_base.Property('highlightedConnection', GraphConnectionWrapper)
def __init__(self, **kwargs):
super().__init__(**kwargs)
with self.app.settingsGroup('graphView') as settings:
self.__zoom = float(settings.value('zoom', 1.0))
self.__offset = QtCore.QPointF(
float(settings.value('offsetx', 0.0)),
float(settings.value('offsety', 0.0)))
self.__nodes = ObjectList(self)
self.__nodeMap = {}
for idx, node in enumerate(self.project.nodes):
self.__addNode(idx, node)
self.project.nodesChanged.connect(self.__nodesChanged)
self.__connections = ObjectList(self)
for idx, connection in enumerate(self.project.connections):
self.__addConnection(idx, connection)
self.project.connectionsChanged.connect(self.__connectionsChanged)
self.__contentRect = QtCore.QRectF(0, 0, 1000, 1000)
self.__updateContentRect()
self.__hasSelection = False
self.__selection = QtCore.QItemSelectionModel(self.__nodes, self)
self.__selection.selectionChanged.connect(self.__selectionChanged)
def cleanup(self):
with self.app.settingsGroup('graphView') as settings:
settings.setValue("zoom", self.__zoom)
settings.setValue("offsetx", self.__offset.x())
settings.setValue("offsety", self.__offset.y())
def selection(self) -> QtCore.QItemSelectionModel:
return self.__selection
selection = QtCore.Property(QtCore.QItemSelectionModel, fget=selection, constant=True)
def hasSelection(self) -> bool:
return self.__hasSelection
hasSelectionChanged = QtCore.Signal(bool)
hasSelection = QtCore.Property(bool, fget=hasSelection, notify=hasSelectionChanged)
def nodes(self):
return self.__nodes
nodes = QtCore.Property(QtCore.QObject, fget=nodes, constant=True)
def connections(self):
return self.__connections
connections = QtCore.Property(QtCore.QObject, fget=connections, constant=True)
def getZoom(self):
return self.__zoom
def setZoom(self, v):
if v == self.__zoom:
return
self.__zoom = v
self.zoomChanged.emit(v)
zoomChanged = QtCore.Signal(float)
zoom = QtCore.Property(float, fget=getZoom, fset=setZoom, notify=zoomChanged)
def getOffset(self):
return self.__offset
def setOffset(self, v):
if v == self.__offset:
return
self.__offset = v
self.offsetChanged.emit(v)
offsetChanged = QtCore.Signal(QtCore.QPointF)
offset = QtCore.Property(QtCore.QPointF, fget=getOffset, fset=setOffset, notify=offsetChanged)
def mapFromGlobal(self, pos: QtCore.QPointF) -> QtCore.QPointF:
return (self.__item.mapFromGlobal(pos) - self.offset) / self.zoom
def getContentRect(self):
return self.__contentRect
def setContentRect(self, r):
if r == self.__contentRect:
return
self.__contentRect = r
self.contentRectChanged.emit(r)
contentRectChanged = QtCore.Signal(QtCore.QRectF)
contentRect = QtCore.Property(QtCore.QRectF, fget=getContentRect, fset=setContentRect, notify=contentRectChanged)
@QtCore.Slot()
def selectAll(self):
self.__selection.select(
QtCore.QItemSelection(self.__nodes.index(0), self.__nodes.index(self.__nodes.rowCount() - 1)),
QtCore.QItemSelectionModel.Select)
@QtCore.Slot()
def clearSelection(self) -> None:
self.__selection.select(
QtCore.QItemSelection(self.__nodes.index(0), self.__nodes.index(self.__nodes.rowCount() - 1)),
QtCore.QItemSelectionModel.Deselect)
@QtCore.Slot(QtCore.QRectF)
def updateSelection(self, rect: QtCore.QRectF) -> None:
for row, node in enumerate(self.__nodes):
if node.rect.intersects(rect):
mode = QtCore.QItemSelectionModel.Select
else:
mode = QtCore.QItemSelectionModel.Deselect
self.__selection.select(
QtCore.QItemSelection(self.__nodes.index(row), self.__nodes.index(row)),
mode)
@QtCore.Slot(QtCore.QPointF)
def moveSelectedNodes(self, delta: QtCore.QPointF) -> None:
for index in self.__selection.selectedIndexes():
node = self.__nodes.data(index)
node.rect = node.rect.translated(delta)
@QtCore.Slot(GraphNodePort, result=list)
def getTargetPorts(self, src: GraphNodePort) -> None:
if src.portDesc.direction == engine.PortDirection.OUTPUT:
reqDirection = engine.PortDirection.INPUT
else:
reqDirection = engine.PortDirection.OUTPUT
existingConns = set()
for conn in self.project.connections:
if conn.srcNodeId == src.node.node.id and conn.srcPort == src.portDesc.name:
existingConns.add((conn.destNodeId, conn.destPort))
elif conn.destNodeId == src.node.node.id and conn.destPort == src.portDesc.name:
existingConns.add((conn.srcNodeId, conn.srcPort))
ports = []
for wrapper in self.__nodes:
if src.node is wrapper:
continue
for port in wrapper.ports:
if (wrapper.node.id, port.portDesc.name) in existingConns:
continue
if port.portDesc.direction == reqDirection:
ports.append(port)
return ports
@QtCore.Slot(GraphConnectionWrapper)
def highlightConnection(self, conn: GraphConnectionWrapper) -> None:
for c in self.__connections:
c.highlighted = c is conn
self.highlightedConnection = conn
@QtCore.Slot(GraphConnectionWrapper)
def unhighlightConnection(self, conn: GraphConnectionWrapper) -> None:
if conn is self.highlightedConnection:
self.highlightedConnection = None
@QtCore.Slot(GraphNodePort, GraphNodePort)
def connectPorts(self, src: GraphNodePort, dest: GraphNodePort) -> None:
if src.portDesc.direction != engine.PortDirection.OUTPUT:
src, dest = dest, src
assert src.portDesc.direction == engine.PortDirection.OUTPUT
assert dest.portDesc.direction == engine.PortDirection.INPUT
srcNode = src.node.node
srcPort = src.portDesc.name
destNode = dest.node.node
destPort = dest.portDesc.name
with self.project.captureChanges("Connect '{}:{}' to '{}:{}'".format(srcNode.title, srcPort, destNode.title, destPort)):
conn = model.Connection.create(
parent=self.project,
srcNodeId=srcNode.id, srcPort=srcPort,
destNodeId=destNode.id, destPort=destPort)
self.project.connections.append(conn)
@QtCore.Slot(GraphConnectionWrapper)
def disconnectPorts(self, wrapper: GraphConnectionWrapper):
conn = wrapper.connection
with self.project.captureChanges("Disconnect '{}:{}' from '{}:{}'".format(
wrapper.srcNode.node.title, conn.srcPort, wrapper.destNode.node.title, conn.destPort)):
for pidx, p in reversed(list(enumerate(self.project.connections))):
if (p.srcNodeId == conn.srcNodeId and p.srcPort == conn.srcPort
and p.destNodeId == conn.destNodeId and p.destPort == conn.destPort):
self.project.connections.pop(pidx)
def __updateContentRect(self) -> None:
contentRect = QtCore.QRectF()
for node in self.__nodes:
contentRect |= node.node.rect
size = max(contentRect.width(), contentRect.height())
if size < 1000:
m = (1000 - size) / 2
contentRect = contentRect.marginsAdded(QtCore.QMarginsF(m, m, m, m))
self.setContentRect(contentRect)
def __nodesChanged(self, change: model.PropertyListChange) -> None:
if isinstance(change, model.PropertyListInsert):
self.__addNode(change.index, change.new_value)
elif isinstance(change, model.PropertyListDelete):
self.__removeNode(change.index)
else:
raise TypeError(type(change))
self.__updateContentRect()
def __addNode(self, index, node):
wrapper = GraphNodeWrapper(
parent=self,
node=node,
context=self.context)
wrapper.activeChanged.connect(functools.partial(self.__activeNodeChanged, wrapper))
wrapper.setZoom(self.zoom)
self.zoomChanged.connect(wrapper.setZoom)
node.rectChanged.connect(lambda _: self.__updateContentRect())
self.__nodeMap[node.id] = wrapper
self.__nodes.insertObject(index, wrapper)
def __removeNode(self, index):
wrapper = self.__nodes.removeObject(index)
self.__nodeMap.pop(wrapper.node.id)
@QtCore.Slot(QtCore.QPointF, str)
def insertNode(self, pos: QtCore.QPointF, uri: str):
desc = self.app.nodeDB.nodeDescription(uri)
title = desc.display_name or desc.uri
with self.project.captureChanges("Add node '{}'".format(title)):
node = model.Node.create(
parent=self.project,
id=random.getrandbits(63),
uri=uri,
title=title,
rect=QtCore.QRectF(pos, QtCore.QSizeF(100, 100)))
self.project.nodes.append(node)
def __activeNodeChanged(self, node, active):
if active:
for n in self.__nodes:
if n is not node:
n.active = False
def __selectionChanged(self, selected, deselected):
for index in selected.indexes():
node = self.__nodes.data(index)
node.selected = True
for index in deselected.indexes():
node = self.__nodes.data(index)
node.selected = False
if self.__selection.hasSelection() != self.__hasSelection:
self.__hasSelection = self.__selection.hasSelection()
self.hasSelectionChanged.emit(self.__hasSelection)
def __connectionsChanged(self, change: model.PropertyListChange) -> None:
if isinstance(change, model.PropertyListInsert):
self.__addConnection(change.index, change.new_value)
elif isinstance(change, model.PropertyListDelete):
self.__removeConnection(change.index)
else:
raise TypeError(type(change))
def __addConnection(self, index, connection):
wrapper = GraphConnectionWrapper(
parent=self,
connection=connection,
nodeMap=self.__nodeMap,
context=self.context)
self.__connections.insertObject(index, wrapper)
def __removeConnection(self, index):
connection = self.__connections.removeObject(index)