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.
 
 
 
 
 
 

523 lines
17 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 QtQuick 2.12
import QtQuick.Shapes 1.15
import QtQuick.Controls 2.15
import "." as Noisicaa
import "matrix.js" as Transform
Control {
id: root
property QtObject d
property color bgColor: ApplicationWindow.window.color
Menu {
id: contextMenu
property point clickPos
function popupAt(pos) {
clickPos = pos;
popup();
}
Action {
text: "Select all"
onTriggered: d.selectAll();
}
Action {
text: "Select none"
enabled: d.hasSelection
onTriggered: d.clearSelection();
}
Action {
text: "Insert node..."
onTriggered: {
var canvasPos = canvas.canvasTransform.applyToPoint(contextMenu.clickPos.x, contextMenu.clickPos.y);
openCreateNodeDialog(Qt.point(canvasPos.x, canvasPos.y));
}
}
}
Menu {
id: connectionContextMenu
property QtObject conn
Action {
text: "Disconnect"
onTriggered: d.disconnectPorts(connectionContextMenu.conn);
}
}
function openCreateNodeDialog(pos) {
createNodeDialog.pos = pos;
createNodeDialog.open();
}
CreateNodeDialog {
id: createNodeDialog
d: root.d.project.createNodeDialog()
property point pos
onUriSelected: function (uri) {
root.d.insertNode(pos, uri);
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
cursorShape: moving ? Qt.OpenHandCursor : undefined
property bool moving: false
property real movex
property real movey
property bool selecting: false
property real selectx1
property real selecty1
onPressed: {
for (var i = 0 ; i < d.nodes.rowCount() ; ++i ) {
var node = d.nodes.data(d.nodes.index(i, 0));
node.active = false;
}
if (mouse.button == Qt.LeftButton) {
selecting = true;
selectx1 = mouse.x - d.offset.x;
selecty1 = mouse.y - d.offset.y;
selectionBox.visible = true;
selectionBox.rect = Qt.rect(selectx1, selecty1, 1, 1);
d.clearSelection();
}
else if (mouse.button == Qt.MiddleButton) {
moving = true;
movex = mouse.x - d.offset.x;
movey = mouse.y - d.offset.y;
} else if (mouse.button == Qt.RightButton) {
if (d.highlightedConnection != null) {
connectionContextMenu.conn = d.highlightedConnection;
connectionContextMenu.popup();
} else {
contextMenu.popupAt(mapToItem(root, Qt.point(mouse.x, mouse.y)));
}
}
}
onPositionChanged: {
if (selecting) {
var selectx2 = mouse.x - d.offset.x;
var selecty2 = mouse.y - d.offset.y;
var rect = Qt.rect(
selectx2 > selectx1 ? selectx1 : selectx2,
selecty2 > selecty1 ? selecty1 : selecty2,
Math.abs(selectx2 - selectx1) + 1,
Math.abs(selecty2 - selecty1) + 1)
selectionBox.rect = rect;
d.updateSelection(rect);
}
if (moving) {
d.offset = Qt.point(
mouse.x - movex,
mouse.y - movey,
);
}
}
onReleased: {
if (mouse.button == Qt.LeftButton && selecting) {
selectionBox.visible = false;
selecting = false;
}
if (mouse.button == Qt.MiddleButton && moving) {
moving = false;
}
}
onWheel: {
if (wheel.angleDelta.y > 0) {
d.offset = Qt.point(
Math.floor(wheel.x + (d.offset.x - wheel.x) * 1.2),
Math.floor(wheel.y + (d.offset.y - wheel.y) * 1.2));
d.zoom *= 1.2;
} else {
d.offset = Qt.point(
Math.floor(wheel.x + (d.offset.x - wheel.x) / 1.2),
Math.floor(wheel.y + (d.offset.y - wheel.y) / 1.2));
d.zoom /= 1.2;
}
}
}
Item {
id: grid
anchors.fill: parent
property real step: 20 * d.zoom
property real offsetx: d.offset.x % step
property real offsety: d.offset.y % step
property color color: Qt.tint(root.bgColor, "#20808080")
visible: step > 5
Repeater {
model: Math.floor(grid.width / grid.step) + 1
Shape {
x: Math.floor(grid.step * index + grid.offsetx)
y: 0
ShapePath {
fillColor: "transparent"
strokeWidth: 1
strokeColor: grid.color
startX: 0
startY: 0
PathLine {
x: 0
y: grid.height
}
}
}
}
Repeater {
model: Math.floor(grid.height / grid.step) + 1
Shape {
x: 0
y: Math.floor(grid.step * index + grid.offsety)
ShapePath {
fillColor: "transparent"
strokeWidth: 1
strokeColor: grid.color
startX: 0
startY: 0
PathLine {
x: grid.width
y: 0
}
}
}
}
}
Item {
id: canvas
property var canvasTransform: new Transform.Matrix().scale(1/d.zoom, 1/d.zoom).translate(-d.offset.x, -d.offset.y)
x: d.offset.x
y: d.offset.y
Repeater {
model: d.connections
Noisicaa.GraphConnection {
d: display
onHoveredChanged: function (hovered) {
if (hovered) {
root.d.highlightConnection(d)
} else {
root.d.unhighlightConnection(d)
}
}
}
}
Item {
id: dragConnection
property QtObject port
property var targetPorts
property var targetPort
property point p1: port ? port.pos : Qt.point(0, 0)
property point p2: Qt.point(0, 0)
visible: false
Shape {
ShapePath {
fillColor: "transparent"
strokeWidth: 3
strokeColor: "#ffbb88"
startX: dragConnection.p1.x
startY: dragConnection.p1.y
PathCubic {
control1X: (dragConnection.p1.x + dragConnection.p2.x) / 2
control1Y: dragConnection.p1.y
control2X: (dragConnection.p1.x + dragConnection.p2.x) / 2
control2Y: dragConnection.p2.y
x: dragConnection.p2.x
y: dragConnection.p2.y
}
}
}
function dragBegin(port) {
dragConnection.port = port;
dragConnection.targetPorts = d.getTargetPorts(port);
dragConnection.targetPort = null;
dragConnection.p2 = port.pos;
dragConnection.visible = true;
}
function dragMove(pos) {
dragConnection.p2 = pos;
var closestTarget = null;
var closestDist = 10000;
for (var target of dragConnection.targetPorts) {
var dx = target.pos.x - pos.x;
var dy = target.pos.y - pos.y;
var d = Math.sqrt(dx*dx + dy*dy);
if (d > 50) continue;
if (closestTarget == null || d < closestDist) {
closestTarget = target;
closestDist = d;
}
}
if (closestTarget == dragConnection.targetPort) {
return;
}
if (dragConnection.targetPort != null) {
dragConnection.targetPort.highlighted = false;
dragConnection.targetPort = null;
}
if (closestTarget != null) {
dragConnection.targetPort = closestTarget;
dragConnection.targetPort.highlighted = true;
}
}
function dragEnd() {
if (dragConnection.targetPort != null) {
d.connectPorts(dragConnection.port, dragConnection.targetPort);
}
if (dragConnection.targetPort != null) {
dragConnection.targetPort.highlighted = false;
}
dragConnection.targetPorts = null;
dragConnection.targetPort = null;
dragConnection.visible = false;
}
}
Repeater {
model: d.nodes
Item {
z: node.d.active ? 1 : 0
Noisicaa.GraphNode {
id: node
d: display
onConnectionDragBegin: function (port) {
dragConnection.dragBegin(port);
}
onConnectionDragMove: function (x, y) {
dragConnection.dragMove(canvas.mapFromItem(node, Qt.point(x, y)));
}
onConnectionDragEnd: function () {
dragConnection.dragEnd();
}
}
MouseArea {
anchors.fill: node
enabled: d.hasSelection && display.selected
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: enabled ? Qt.OpenHandCursor : undefined
property bool moving: false
property real movex
property real movey
onPressed: {
if (mouse.button == Qt.LeftButton) {
moving = true;
movex = mouse.x;
movey = mouse.y;
} else if (mouse.button == Qt.RightButton) {
contextMenu.popupAt(mapToItem(root, Qt.point(mouse.x, mouse.y)));
}
}
onPositionChanged: {
if (moving) {
d.moveSelectedNodes(Qt.point(
mouse.x - movex,
mouse.y - movey,
));
}
}
onReleased: {
if (mouse.button == Qt.LeftButton && moving) {
moving = false;
}
}
}
}
}
Rectangle {
id: selectionBox
property rect rect
x: rect.x
y: rect.y
width: rect.width
height: rect.height
visible: false
color: "#aaaaff"
border { width: 1; color: "#444488" }
opacity: 0.3
}
}
Rectangle {
color: root.bgColor
border.width: 1
border.color: Qt.tint(root.bgColor, "#a0808080")
anchors {
left: parent.left
leftMargin: 20
bottom: parent.bottom
bottomMargin: 10
}
width: 200
height: 160
Rectangle {
id: minimap
anchors.fill: parent
anchors.margins: 2
clip: true
color: parent.color
property var complete: false
property rect contentRect: d.contentRect
property var mapTransform: new Transform.Matrix()
onContentRectChanged: recompute()
Component.onCompleted: recompute()
function recompute() {
var t = new Transform.Matrix();
if (d.nodes.rowCount() == 0) {
minimapView.visible = false;
return;
}
minimapView.visible = true;
var sx = width / contentRect.width;
var sy = height / contentRect.height;
var s = Math.min(1.0, sx, sy);
t.scale(s, s);
var inv = t.getInverse();
var mapP1 = inv.applyToPoint(0, 0);
var mapP2 = inv.applyToPoint(width, height);
t.translate(-contentRect.x, -contentRect.y);
if (sx < sy) {
t.translate(0, -(contentRect.height - (mapP2.y - mapP1.y)) / 2);
} else {
t.translate(-(contentRect.width - (mapP2.x - mapP1.x)) / 2, 0);
}
mapTransform = t;
}
Item {
id: minimapCanvas
Repeater {
model: d.nodes
Rectangle {
color: Qt.tint(display.node.color, "#80ffffff")
border {
width: 1
color: Qt.darker(display.node.color, 1.5)
}
property rect rect: display.node.rect
property var p1: minimap.mapTransform.applyToPoint(rect.left, rect.top)
property var p2: minimap.mapTransform.applyToPoint(rect.right, rect.bottom)
x: Math.floor(p1.x)
y: Math.floor(p1.y)
width: Math.floor(p2.x - p1.x)
height: Math.floor(p2.y - p1.y)
}
}
Rectangle {
id: minimapView
color: "#aaaaff"
border { width: 1; color: "#6666aa" }
opacity: 0.4
property var c1: canvas.canvasTransform.applyToPoint(0, 0)
property var c2: canvas.canvasTransform.applyToPoint(root.width, root.height)
property var p1: minimap.mapTransform.applyToPoint(c1.x, c1.y)
property var p2: minimap.mapTransform.applyToPoint(c2.x, c2.y)
x: Math.floor(p1.x)
y: Math.floor(p1.y)
width: Math.floor(p2.x - p1.x)
height: Math.floor(p2.y - p1.y)
}
}
MouseArea {
anchors.fill: parent
onPressed: {
var c = minimap.mapTransform.getInverse().applyToPoint(mouse.x, mouse.y);
c = canvas.canvasTransform.getInverse().applyToPoint(c.x, c.y);
d.offset = Qt.point(
d.offset.x + Math.floor(-c.x + root.width/2),
d.offset.y + Math.floor(-c.y + root.height/2));
}
onPositionChanged: {
var c = minimap.mapTransform.getInverse().applyToPoint(mouse.x, mouse.y);
c = canvas.canvasTransform.getInverse().applyToPoint(c.x, c.y);
d.offset = Qt.point(
d.offset.x + Math.floor(-c.x + root.width/2),
d.offset.y + Math.floor(-c.y + root.height/2));
}
onWheel: {
var c = {x: root.width / 2, y: root.height / 2};
if (wheel.angleDelta.y > 0) {
d.offset = Qt.point(
Math.floor(c.x + (d.offset.x - c.x) * 1.2),
Math.floor(c.y + (d.offset.y - c.y) * 1.2)),
d.zoom *= 1.2;
} else {
d.offset = Qt.point(
Math.floor(c.x + (d.offset.x - c.x) / 1.2),
Math.floor(c.y + (d.offset.y - c.y) / 1.2));
d.zoom /= 1.2;
}
}
}
}
}
}