diff --git a/data/qmltoolbox/qml/QmlToolbox/Base/StyleDefault.qml b/data/qmltoolbox/qml/QmlToolbox/Base/StyleDefault.qml index 1b0353d..6f89299 100644 --- a/data/qmltoolbox/qml/QmlToolbox/Base/StyleDefault.qml +++ b/data/qmltoolbox/qml/QmlToolbox/Base/StyleDefault.qml @@ -68,9 +68,11 @@ Item property real pipelineSlotSize: 20 // Diameter of slots property color pipelineSlotColorIn: '#ffffff' // Color of input slots - property color pipelineSlotColorOut: '#cafd00' // Color of output slots + property color pipelineSlotColorOut: '#ffffff' // Color of output slots + property color pipelineSlotColorOutRequired: '#cafd00' // Color of required output slots property color pipelineLineColorDefault: '#000000' // Color of connections property color pipelineLineColorHighlighted: '#6688c8' // Color of connections when highlighted property color pipelineLineColorSelected: '#c83366' // Color of connections when selected + property color pipelineLineColorFeedback: '#afafaf' // Color of feedback connections } diff --git a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Connectors.qml b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Connectors.qml index ffc0f97..d2736d0 100644 --- a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Connectors.qml +++ b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Connectors.qml @@ -62,20 +62,24 @@ Item var splitPath = path.split('.'); return splitPath[splitPath.length - 1]; }; + var isEqual = function (path, slot, combinedPath) { + return path == getPath(combinedPath) && slot == getSlot(combinedPath); + } // Get all stages of the pipeline and the pipeline itself var stages = []; stages.push(path); - var pipeline = properties.getStage(path); + var pipelineInfo = properties.getStage(path); - for (var i in pipeline.stages) + for (var i in pipelineInfo.stages) { - stages.push(path + '.' + pipeline.stages[i]); + stages.push(path + '.' + pipelineInfo.stages[i]); } // Get connectors + var pipeline = connectors.pipeline; for (var i in stages) { // Get stage @@ -87,17 +91,24 @@ Item { // Get connection var connection = connections[j]; - var from = connection.from; - var to = connection.to; + + var from = connection.from; + var to = connection.to; + var feedback = connection.feedback || false; // Draw connection - var p0 = connectors.pipeline.getSlotPos(getPath(from), getSlot(from)); - var p1 = connectors.pipeline.getSlotPos(getPath(to), getSlot(to)); + var p0 = pipeline.getSlotPos(getPath(from), getSlot(from)); + var p1 = pipeline.getSlotPos(getPath(to), getSlot(to)); if (p0 != null && p1 != null) { // Highlight the connection if its input or output slot is selected - var status = (connectors.pipeline.hoveredElement == from || connectors.pipeline.hoveredElement == to) ? 1 : 0; + var status = feedback ? 3 : 0; + if (isEqual(pipeline.hoveredPath, pipeline.hoveredSlot, from) || + isEqual(pipeline.hoveredPath, pipeline.hoveredSlot, to)) + { + status = 1; + } drawConnector(ctx, p0, p1, status); } @@ -105,17 +116,17 @@ Item } // Draw interactive connector - if (connectors.pipeline.selectedOutput != '') + if (pipeline.selectedOutput != '') { - var p0 = connectors.pipeline.getSlotPos(connectors.pipeline.selectedPath, connectors.pipeline.selectedOutput); - var p1 = { x: connectors.pipeline.mouseX, y: connectors.pipeline.mouseY }; + var p0 = pipeline.getSlotPos(pipeline.selectedPath, pipeline.selectedOutput); + var p1 = { x: pipeline.mouseX, y: pipeline.mouseY }; drawConnector(ctx, p0, p1, 2); } - if (connectors.pipeline.selectedInput != '') + if (pipeline.selectedInput != '') { - var p0 = { x: connectors.pipeline.mouseX, y: connectors.pipeline.mouseY }; - var p1 = connectors.pipeline.getSlotPos(connectors.pipeline.selectedPath, connectors.pipeline.selectedInput); + var p0 = { x: pipeline.mouseX, y: pipeline.mouseY }; + var p1 = pipeline.getSlotPos(pipeline.selectedPath, pipeline.selectedInput); drawConnector(ctx, p0, p1, 2); } } @@ -142,6 +153,7 @@ Item var color = Ui.style.pipelineLineColorDefault; if (status == 1) color = Ui.style.pipelineLineColorHighlighted; if (status == 2) color = Ui.style.pipelineLineColorSelected; + if (status == 3) color = Ui.style.pipelineLineColorFeedback; ctx.strokeStyle = color; ctx.fillStyle = color; diff --git a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/OutputSlot.qml b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/OutputSlot.qml index 38a7fc2..8751409 100644 --- a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/OutputSlot.qml +++ b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/OutputSlot.qml @@ -82,7 +82,7 @@ Item height: item.radius radius: width / 2.0 - color: item.selected ? Ui.style.pipelineLineColorSelected : (item.hovered ? Ui.style.pipelineLineColorHighlighted : item.color) + color: item.selected ? Ui.style.pipelineLineColorSelected : (item.hovered ? Ui.style.pipelineLineColorHighlighted : (status !== null && status.required ? Ui.style.pipelineSlotColorOutRequired : item.color)) border.color: item.borderColor border.width: item.borderWidth diff --git a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Pipeline.qml b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Pipeline.qml index 11b75df..916c54e 100644 --- a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Pipeline.qml +++ b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Pipeline.qml @@ -15,6 +15,8 @@ Item { id: pipeline + signal stageCreated(string path) ///< Signals creation of a new stage item after the pipeline was loaded + // Options property var properties: null ///< Interface for accessing the actual properties property string path: '' ///< Path in the pipeline hierarchy (e.g., 'pipeline') @@ -103,7 +105,7 @@ Item { return function() { - pipeline.createStage(type, type); + pipeline.createStage(type, type, menu.x, menu.y); }; }; @@ -200,11 +202,23 @@ Item } // Add pseudo stages for inputs and outputs of the pipeline itself - addInputStageItem (pipeline.path, 'Inputs', 20, 150); - addOutputStageItem(pipeline.path, 'Outputs', 1200, 150); + addInputStageItem (pipeline.path, 'Inputs', 20, 150); + addOutputStageItem(pipeline.path, 'Outputs', x, 150); - // Do the layout - computeLayout(); + // Hack: layout after 5 ms to ensure loading of stages is finished + function Timer() { + return Qt.createQmlObject("import QtQuick 2.0; Timer {}", pipeline); + } + function delay(delayTime, cb) { + var timer = new Timer(); + timer.interval = delayTime; + timer.repeat = false; + timer.triggered.connect(cb); + timer.start(); + } + delay(10, function() { + computeLayout(); + }); // Redraw connections connectors.requestPaint(); @@ -360,13 +374,15 @@ Item * @param[in] name * Name of stage */ - function createStage(className, name) + function createStage(className, name, x, y) { // Create stage var realName = properties.createStage(pipeline.path, className, name); // Create stage item - addStageItem(pipeline.path + '.' + realName, realName, 100, 100); + addStageItem(pipeline.path + '.' + realName, realName, x, y); + + stageCreated(pipeline.path + '.' + realName); } /** @@ -390,12 +406,143 @@ Item connectors.requestPaint(); } + /** + * Compute ranks in half ordered graph + */ + function computeRanks() + { + var getPath = function (path) { + var splitPath = path.split('.'); + splitPath.splice(splitPath.length - 1, 1); + return splitPath.join('.'); + }; + + // build the empty graph + var graph = {}; + var inverseGraph = {}; + var pathToName = {}; + for (var name in stageItems) + { + graph[stageItems[name].path] = []; + inverseGraph[stageItems[name].path] = []; + pathToName[stageItems[name].path] = name; + } + pathToName["root"] = "Outputs"; + + // insert edges + for (var name in stageItems) + { + var connections = properties.getConnections(stageItems[name].path); + for (var i in connections) + { + var fromPath = getPath(connections[i].from); + var toPath = getPath(connections[i].to); + + if (fromPath == "root" || connections[i].feedback) + { + // ignore connections from root (inputs will be leftmost) + // and feedback connections + // this lets the algorithm start at the output node + continue; + } + + if (graph[fromPath].indexOf(toPath) < 0) + { + graph[fromPath].push(toPath); + } + + if (inverseGraph[toPath].indexOf(fromPath) < 0) + { + inverseGraph[toPath].push(fromPath); + } + } + } + + var ranks = []; + var toRemove = []; + while (Object.keys(graph).length > 0) + { + for (var path in graph) + { + if (graph[path].length === 0) + { + toRemove.push(path); + } + } + + if (toRemove.length === 0) + { + console.log("circular graph detected, abort layout process."); + break; + } + + var names = []; + for (var i in toRemove) + { + names.push(pathToName[toRemove[i]]); + + } + ranks.push(names); + + for (var i in toRemove) + { + var current = toRemove[i]; + for (var j in inverseGraph[current]) + { + var predecessor = inverseGraph[current][j]; + var edgeNum = graph[predecessor].indexOf(current); + graph[predecessor].splice(edgeNum, 1); + } + delete graph[current]; + } + toRemove = []; + } + ranks.push(["Inputs"]); + + return ranks; + } + /** * Compute automatic layout for stages */ function computeLayout() { - // [TODO] + var startX = 120; + var marginX = 100; + var startY = 120; + var marginY = 100; + + var x = startX; + + var ranks = computeRanks(); + + for (var i in ranks) + { + // go through the computed ranks in reverse order + var stages = ranks[ranks.length - i - 1]; + + var maxWidth = 0; + for (var j in stages) + { + var current = stageItems[stages[j]]; + + maxWidth = Math.max(maxWidth, current.width); + } + + var y = startY; + for (var j in stages) + { + var current = stageItems[stages[j]]; + + current.x = x + (maxWidth - current.width) / 2; + current.y = y; + + y += current.height + marginY; + } + x += maxWidth + marginX; + } + + connectors.requestPaint(); } /** diff --git a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/PipelineEditor.qml b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/PipelineEditor.qml index 4166013..63131b0 100644 --- a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/PipelineEditor.qml +++ b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/PipelineEditor.qml @@ -15,7 +15,8 @@ Rectangle id: panel // Options - property var properties: null ///< Interface for communicating with the actual properties + property var properties: null ///< Interface for communicating with the actual properties + property var pipeline: pipeline ///< Interface for communication from root // Internals property bool loaded: false @@ -61,4 +62,13 @@ Rectangle loaded = true; } } + + /** + * Update pipeline (reload on different model) + */ + function update() + { + pipeline.path = ""; + pipeline.path = "root"; + } } diff --git a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Stage.qml b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Stage.qml index 8229f54..be94100 100644 --- a/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Stage.qml +++ b/data/qmltoolbox/qml/QmlToolbox/PipelineEditor/Stage.qml @@ -57,7 +57,7 @@ Item MenuItem { - text: 'Add Input' + text: 'Add Input...' onTriggered: { @@ -69,7 +69,7 @@ Item MenuItem { - text: 'Add Output' + text: 'Add Output...' onTriggered: { @@ -78,6 +78,16 @@ Item dialog.open(); } } + + MenuItem + { + text: 'Delete Stage' + + onTriggered: + { + stage.closed(); + } + } } // Stage body @@ -368,6 +378,19 @@ Item return null; } + /** + * Add custom component to stage body + * + * @param[in] component + * The component from which one instance should be added + * @param[in] options + * Options for component creation + */ + function addComponent(component, options) + { + return component.createObject(inputs, options); + } + /** * Load pipeline */ diff --git a/data/qmltoolbox/qml/examples/uiconcept/DemoPropertyInterface.qml b/data/qmltoolbox/qml/examples/uiconcept/DemoPropertyInterface.qml index 972e093..6ac4cb1 100644 --- a/data/qmltoolbox/qml/examples/uiconcept/DemoPropertyInterface.qml +++ b/data/qmltoolbox/qml/examples/uiconcept/DemoPropertyInterface.qml @@ -40,6 +40,7 @@ QtObject { from: 'root.Number', to: 'root.Stage2.Number' }, { from: 'root.Stage1.Ok', to: 'root.Stage3.Ok1' }, { from: 'root.Stage2.Ok', to: 'root.Stage3.Ok2' }, + { from: 'root.Stage3.OkFeedback', to: 'root.Stage2.FeedbackIn', feedback : true }, { from: 'root.Stage3.Ok', to: 'root.Ok' } ]; } @@ -199,19 +200,26 @@ QtObject // Internals function getSlotInfo(path, slot) { - for (var i=0; i