From fdbf079896f46d6673475cb5bf8c12fc4b81c660 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 30 Jul 2015 11:03:37 +0100 Subject: [PATCH] Clean up subflow editor - new appearance of subflow tabs - input/output buttons now counters - allow multiple input wires to the same node when converting to subflow - ensure edit history is propagated properly to instance nodes --- editor/js/history.js | 54 ++++-- editor/js/ui/editor.js | 49 ++++- editor/js/ui/subflow.js | 296 +++++++++++++++++++++++++----- editor/js/ui/tabs.js | 12 +- editor/js/ui/view.js | 135 ++++++++------ editor/js/ui/workspaces.js | 11 +- editor/sass/header.scss | 31 ++-- editor/sass/tabs.scss | 20 +- editor/sass/workspaceToolbar.scss | 51 ++++- editor/templates/index.mst | 7 +- locales/en-US/editor.json | 5 +- 11 files changed, 493 insertions(+), 178 deletions(-) diff --git a/editor/js/history.js b/editor/js/history.js index 79c49ae1a..dd5956e38 100644 --- a/editor/js/history.js +++ b/editor/js/history.js @@ -15,7 +15,7 @@ **/ RED.history = (function() { var undo_history = []; - + return { //TODO: this function is a placeholder until there is a 'save' event that can be listened to markAllDirty: function() { @@ -62,6 +62,23 @@ RED.history = (function() { RED.workspaces.remove(ev.subflows[i]); } } + if (ev.subflow) { + if (ev.subflow.instances) { + ev.subflow.instances.forEach(function(n) { + var node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; + } + }); + } + if (ev.subflow.hasOwnProperty('changed')) { + var subflow = RED.nodes.subflow(ev.subflow.id); + if (subflow) { + subflow.changed = ev.subflow.changed; + } + } + } } else if (ev.t == "delete") { if (ev.workspaces) { for (i=0;i 0) { @@ -97,9 +114,17 @@ RED.history = (function() { }); } } + if (ev.subflow && ev.subflow.hasOwnProperty('instances')) { + ev.subflow.instances.forEach(function(n) { + var node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; + } + }); + } if (subflow) { RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) { - n.changed = true; n.inputs = subflow.in.length; n.outputs = subflow.out.length; while (n.outputs > n.ports.length) { @@ -148,13 +173,21 @@ RED.history = (function() { ev.node.out = ev.node.out.concat(ev.subflow.outputs); } } + if (ev.subflow.hasOwnProperty('instances')) { + ev.subflow.instances.forEach(function(n) { + var node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; + } + }); + } RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) { - n.changed = ev.changed; n.inputs = ev.node.in.length; n.outputs = ev.node.out.length; RED.editor.updateNodeProperties(n); }); - + RED.palette.refresh(); } else { RED.editor.updateNodeProperties(ev.node); @@ -182,10 +215,10 @@ RED.history = (function() { RED.nodes.removeLink(ev.links[i]); } } - - RED.nodes.removeSubflow(ev.subflow); - RED.workspaces.remove(ev.subflow); - + + RED.nodes.removeSubflow(ev.subflow.subflow); + RED.workspaces.remove(ev.subflow.subflow); + if (ev.removedLinks) { for (i=0;i output.i) { + subflowMovedLinks.push(l); + } + } + }); + subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)}); + subflowMovedLinks.forEach(function(l) { l.sourcePort--; }); + + removedLinks = removedLinks.concat(subflowRemovedLinks); + for (var j=output.i;j').appendTo(toolbar); + $(' '+ + '
'+ + '0'+ + '1'+ + '
').appendTo(toolbar); + + $('
'+ + ''+ + '
3
'+ + ''+ + '
').appendTo(toolbar); + + // $(' ').appendTo(toolbar); + // $(' ').appendTo(toolbar); + $(' ').appendTo(toolbar); + toolbar.i18n(); + + + $("#workspace-subflow-output-remove").click(function(event) { + event.preventDefault(); + var wasDirty = RED.nodes.dirty(); + var wasChanged = activeSubflow.changed; + var result = removeSubflowOutput(); + if (result) { + var inst = refresh(true); + RED.history.push({ + t:'delete', + links:result.links, + subflowOutputs: result.subflowOutputs, + changed: wasChanged, + dirty:wasDirty, + subflow: { + instances: inst.instances + } + }); + + RED.view.select(); + RED.nodes.dirty(true); + RED.view.redraw(true); + } + }); + $("#workspace-subflow-output-add").click(function(event) { + event.preventDefault(); + addSubflowOutput(); + }); + + $("#workspace-subflow-input-add").click(function(event) { + event.preventDefault(); + addSubflowInput(); + }); + $("#workspace-subflow-input-remove").click(function(event) { + event.preventDefault(); + var wasDirty = RED.nodes.dirty(); + var wasChanged = activeSubflow.changed; + activeSubflow.changed = true; + var result = removeSubflowInput(); + if (result) { + var inst = refresh(true); + RED.history.push({ + t:'delete', + links:result.links, + changed: wasChanged, + subflowInputs: result.subflowInputs, + dirty:wasDirty, + subflow: { + instances: inst.instances + } + }); + RED.view.select(); + RED.nodes.dirty(true); + RED.view.redraw(true); + } + }); + $("#workspace-subflow-edit").click(function(event) { RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active())); event.preventDefault(); }); - $("#workspace-subflow-add-input").click(function(event) { - event.preventDefault(); - if ($(this).hasClass("disabled")) { - return; - } - addSubflowInput(); - }); - $("#workspace-subflow-add-output").click(function(event) { - event.preventDefault(); - if ($(this).hasClass("disabled")) { - return; - } - addSubflowOutput(); - }); $("#workspace-subflow-delete").click(function(event) { event.preventDefault(); @@ -170,7 +336,9 @@ RED.subflow = (function() { t:'delete', nodes:removedNodes, links:removedLinks, - subflow: activeSubflow, + subflow: { + subflow: activeSubflow + }, dirty:startDirty }); @@ -179,6 +347,26 @@ RED.subflow = (function() { RED.view.redraw(); }); + refreshToolbar(activeSubflow); + + $("#chart").css({"margin-top": "40px"}); + $("#workspace-toolbar").show(); + } + function hideWorkspaceToolbar() { + $("#workspace-toolbar").hide().empty(); + $("#chart").css({"margin-top": "0"}); + } + + + function init() { + RED.events.on("workspace:change",function(event) { + var activeSubflow = RED.nodes.subflow(event.workspace); + if (activeSubflow) { + showWorkspaceToolbar(activeSubflow); + } else { + hideWorkspaceToolbar(); + } + }); RED.events.on("view:selection-changed",function(selection) { if (!selection.nodes) { RED.menu.setDisabled("menu-item-subflow-convert",true); @@ -211,7 +399,9 @@ RED.subflow = (function() { RED.nodes.addSubflow(subflow); RED.history.push({ t:'createSubflow', - subflow: subflow, + subflow: { + subflow:subflow + }, dirty:RED.nodes.dirty() }); RED.workspaces.show(subflowId); @@ -230,6 +420,8 @@ RED.subflow = (function() { var candidateInputs = []; var candidateOutputs = []; + var candidateInputNodes = {}; + var boundingBox = [selection.nodes[0].x, selection.nodes[0].y, @@ -262,6 +454,7 @@ RED.subflow = (function() { if (!nodes[link.source.id] && nodes[link.target.id]) { // An inbound link candidateInputs.push(link); + candidateInputNodes[link.target.id] = link.target; removedLinks.push(link); } }); @@ -279,15 +472,10 @@ RED.subflow = (function() { }); candidateOutputs.sort(function(a,b) { return a.source.y-b.source.y}); - if (candidateInputs.length > 1) { + if (Object.keys(candidateInputNodes).length > 1) { RED.notify(RED._("subflow.errors.multipleInputsToSelection"),"error"); return; } - //if (candidateInputs.length == 0) { - // RED.notify("Cannot create subflow: no input to selection","error"); - // return; - //} - var lastIndex = 0; RED.nodes.eachSubflow(function(sf) { @@ -304,15 +492,15 @@ RED.subflow = (function() { type:"subflow", id:subflowId, name:name, - in: candidateInputs.map(function(v,i) { var index = i; return { + in: Object.keys(candidateInputNodes).map(function(v,i) { var index = i; return { type:"subflow", direction:"in", - x:v.target.x-(v.target.w/2)-80, - y:v.target.y, + x:candidateInputNodes[v].x-(candidateInputNodes[v].w/2)-80, + y:candidateInputNodes[v].y, z:subflowId, i:index, id:RED.nodes.id(), - wires:[{id:v.target.id}] + wires:[{id:candidateInputNodes[v].id}] }}), out: candidateOutputs.map(function(v,i) { var index = i; return { type:"subflow", @@ -325,6 +513,7 @@ RED.subflow = (function() { wires:[{id:v.source.id,port:v.sourcePort}] }}) }; + RED.nodes.addSubflow(subflow); var subflowInstance = { @@ -383,7 +572,9 @@ RED.subflow = (function() { t:'createSubflow', nodes:[subflowInstance.id], links:new_links, - subflow: subflow, + subflow: { + subflow: subflow + }, activeWorkspace: RED.workspaces.active(), removedLinks: removedLinks, @@ -401,6 +592,9 @@ RED.subflow = (function() { return { init: init, createSubflow: createSubflow, - convertToSubflow: convertToSubflow + convertToSubflow: convertToSubflow, + refresh: refresh, + removeInput: removeSubflowInput, + removeOutput: removeSubflowOutput } })(); diff --git a/editor/js/ui/tabs.js b/editor/js/ui/tabs.js index b7add37bb..9faeb0e9c 100644 --- a/editor/js/ui/tabs.js +++ b/editor/js/ui/tabs.js @@ -80,12 +80,15 @@ RED.tabs = (function() { tabs.css({width:currentTabWidth+"%"}); if (tabWidth < 50) { ul.find(".red-ui-tab-close").hide(); + ul.find(".red-ui-tab-icon").hide(); } else { ul.find(".red-ui-tab-close").show(); + ul.find(".red-ui-tab-icon").show(); } if (currentActiveTabWidth !== 0) { ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth}); ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); + ul.find("li.red-ui-tab.active .red-ui-tab-icon").show(); } } @@ -116,13 +119,16 @@ RED.tabs = (function() { tabs[tab.id] = tab; var li = $("
  • ",{class:"red-ui-tab"}).appendTo(ul); var link = $("",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li); - link.html(tab.label); + if (tab.icon) { + $('').appendTo(link); + } + $('').text(tab.label).appendTo(link); link.on("click",onTabClick); link.on("dblclick",onTabDblClick); if (tab.closeable) { var closeLink = $("",{href:"#",class:"red-ui-tab-close"}).appendTo(li); - closeLink.html(''); + closeLink.append(''); closeLink.on("click",function(event) { removeTab(tab.id); @@ -150,7 +156,7 @@ RED.tabs = (function() { tabs[id].label = label; var tab = ul.find("a[href='#"+id+"']"); tab.attr("title",label); - tab.text(label); + tab.find("span").text(label); updateTabWidths(); } diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index a40472828..39b189270 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -255,9 +255,6 @@ RED.view = (function() { var scrollStartTop = chart.scrollTop(); activeSubflow = RED.nodes.subflow(event.workspace); - if (activeSubflow) { - $("#workspace-subflow-add-input").toggleClass("disabled",activeSubflow.in.length > 0); - } RED.menu.setDisabled("menu-item-workspace-edit", activeSubflow); RED.menu.setDisabled("menu-item-workspace-delete",RED.workspaces.count() == 1 || activeSubflow); @@ -351,7 +348,23 @@ RED.view = (function() { nn.changed = true; nn.h = Math.max(node_height,(nn.outputs||0) * 15); - RED.history.push({t:'add',nodes:[nn.id],dirty:RED.nodes.dirty()}); + var historyEvent = { + t:'add', + nodes:[nn.id], + dirty:RED.nodes.dirty() + } + if (activeSubflow) { + var subflowRefresh = RED.subflow.refresh(true); + if (subflowRefresh) { + historyEvent.subflow = { + id:activeSubflow.id, + changed: activeSubflow.changed, + instances: subflowRefresh.instances + } + } + } + + RED.history.push(historyEvent); RED.nodes.add(nn); RED.editor.validateNode(nn); RED.nodes.dirty(true); @@ -745,8 +758,10 @@ RED.view = (function() { var removedLinks = []; var removedSubflowOutputs = []; var removedSubflowInputs = []; + var subflowInstances = []; var startDirty = RED.nodes.dirty(); + var startChanged = false; if (moving_set.length > 0) { for (var i=0;i 0) { - removedSubflowOutputs.sort(function(a,b) { return b.i-a.i}); - for (i=0;i output.i) { - subflowMovedLinks.push(l); - } - } - }); - subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)}); - subflowMovedLinks.forEach(function(l) { l.sourcePort--; }); - - removedLinks = removedLinks.concat(subflowRemovedLinks); - for (var j=output.i;j 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0) { RED.nodes.dirty(true); @@ -838,7 +810,18 @@ RED.view = (function() { removedLinks.push(selected_link); RED.nodes.dirty(true); } - RED.history.push({t:'delete',nodes:removedNodes,links:removedLinks,subflowOutputs:removedSubflowOutputs,subflowInputs:removedSubflowInputs,dirty:startDirty}); + var historyEvent = { + t:'delete', + nodes:removedNodes, + links:removedLinks, + subflowOutputs:removedSubflowOutputs, + subflowInputs:removedSubflowInputs, + subflow: { + instances: subflowInstances + }, + dirty:startDirty + }; + RED.history.push(historyEvent); selected_link = null; updateActiveNodes(); @@ -945,7 +928,22 @@ RED.view = (function() { if (!existingLink) { var link = {source: src, sourcePort:src_port, target: dst}; RED.nodes.addLink(link); - RED.history.push({t:'add',links:[link],dirty:RED.nodes.dirty()}); + var historyEvent = { + t:'add', + links:[link], + dirty:RED.nodes.dirty() + }; + if (activeSubflow) { + var subflowRefresh = RED.subflow.refresh(true); + if (subflowRefresh) { + historyEvent.subflow = { + id:activeSubflow.id, + changed: activeSubflow.changed, + instances: subflowRefresh.instances + } + } + } + RED.history.push(historyEvent); updateActiveNodes(); RED.nodes.dirty(true); } else { @@ -1695,6 +1693,10 @@ RED.view = (function() { */ function importNodes(newNodesStr,touchImport) { try { + var activeSubflowChanged; + if (activeSubflow) { + activeSubflowChanged = activeSubflow.changed; + } var result = RED.nodes.import(newNodesStr,true); if (result) { var new_nodes = result[0]; @@ -1752,14 +1754,25 @@ RED.view = (function() { moving_set = new_ms; } - RED.history.push({ + var historyEvent = { t:'add', nodes:new_node_ids, links:new_links, workspaces:new_workspaces, subflows:new_subflows, dirty:RED.nodes.dirty() - }); + }; + if (activeSubflow) { + var subflowRefresh = RED.subflow.refresh(true); + if (subflowRefresh) { + historyEvent.subflow = { + id:activeSubflow.id, + changed: activeSubflowChanged, + instances: subflowRefresh.instances + } + } + } + RED.history.push(historyEvent); updateActiveNodes(); redraw(); diff --git a/editor/js/ui/workspaces.js b/editor/js/ui/workspaces.js index ae8196678..001d4b9b4 100644 --- a/editor/js/ui/workspaces.js +++ b/editor/js/ui/workspaces.js @@ -84,13 +84,6 @@ RED.workspaces = (function() { workspace_tabs = RED.tabs.create({ id: "workspace-tabs", onchange: function(tab) { - if (tab.type == "subflow") { - $("#chart").css({"margin-top": "40px"}); - $("#workspace-toolbar").show(); - } else { - $("#workspace-toolbar").hide(); - $("#chart").css({"margin-top": "0"}); - } var event = { old: activeWorkspace } @@ -242,7 +235,7 @@ RED.workspaces = (function() { if (!workspace_tabs.contains(id)) { var sf = RED.nodes.subflow(id); if (sf) { - addWorkspace({type:"subflow",id:id,label:RED._("subflow.tabLabel",{name:sf.name}), closeable: true}); + addWorkspace({type:"subflow",id:id,icon:"red/images/subflow_tab.png",label:sf.name, closeable: true}); } } workspace_tabs.activateTab(id); @@ -250,7 +243,7 @@ RED.workspaces = (function() { refresh: function() { RED.nodes.eachSubflow(function(sf) { if (workspace_tabs.contains(sf.id)) { - workspace_tabs.renameTab(sf.id,RED._("subflow.tabLabel",{name:sf.name})); + workspace_tabs.renameTab(sf.id,sf.name); } }); }, diff --git a/editor/sass/header.scss b/editor/sass/header.scss index 2f8534e9c..780c211c4 100644 --- a/editor/sass/header.scss +++ b/editor/sass/header.scss @@ -47,7 +47,7 @@ span.logo { font-size: 30px; line-height: 30px; text-decoration: none; - + span { vertical-align: middle; font-size: 16px !important; @@ -55,14 +55,14 @@ span.logo { img { height: 18px; } - + a { color: inherit; &:hover { text-decoration: none; } } - + } .header-toolbar { @@ -70,13 +70,13 @@ span.logo { margin: 0; list-style: none; float: right; - + > li { display: inline-block; padding: 0; margin: 0; position: relative; - + } } @@ -97,19 +97,19 @@ span.logo { vertical-align: middle; border-left: 2px solid #000; border-right: 2px solid #000; - + &:hover { border-color: $headerMenuItemHover; } } -.button-group { +#header .button-group { display: inline-block; margin: auto 15px; vertical-align: middle; clear: both; } -.button-group > a { +#header .button-group > a { display: inline-block; float: left; line-height: 22px; @@ -122,11 +122,11 @@ span.logo { .deploy-button { background: $deployButton; color: #eee !important; - + &:hover { background: $deployButtonHover; } - + &:active { background: $deployButtonActive; color: #ccc !important; @@ -134,18 +134,18 @@ span.logo { } #btn-deploy { - + padding: 4px 12px; - + &.disabled { cursor: default; background: $deployDisabledButton; color: #999 !important; - + img { opacity: 0.3; } - + &+ #btn-deploy-options { background: $deployDisabledButton; color: #ddd; @@ -157,7 +157,7 @@ span.logo { background: $deployDisabledButton; } } - + img { margin-right: 8px; } @@ -264,4 +264,3 @@ span.logo { font-size: 16px; color: #fff; } - diff --git a/editor/sass/tabs.scss b/editor/sass/tabs.scss index 34c24f687..a9a0afa83 100644 --- a/editor/sass/tabs.scss +++ b/editor/sass/tabs.scss @@ -21,13 +21,10 @@ ul.red-ui-tabs { display: block; height: 35px; box-sizing:border-box; + white-space: nowrap; border-bottom: 1px solid $primary-border-color; background: #fff; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + @include disable-selection; } ul.red-ui-tabs li { @@ -110,3 +107,16 @@ ul.red-ui-tabs li.active { ul.red-ui-tabs li.active a { color: #333; } + +ul.red-ui-tabs li img { + margin-left: -8px; + margin-right: 3px; + margin-top: -2px; + opacity: 0.1; + width: 20px; + height: 20px; + vertical-align: middle; +} +ul.red-ui-tabs li.active img { + opacity: 0.2; +} diff --git a/editor/sass/workspaceToolbar.scss b/editor/sass/workspaceToolbar.scss index 7698f4bf8..9b4ac5b4f 100644 --- a/editor/sass/workspaceToolbar.scss +++ b/editor/sass/workspaceToolbar.scss @@ -17,6 +17,9 @@ #workspace-toolbar { display: none; + color: $workspace-button-color; + font-size: 12px; + line-height: 18px; position: absolute; top: 35px; left:0; @@ -26,12 +29,46 @@ box-sizing: border-box; background: #fff; border-bottom: 1px solid $secondary-border-color; -} + white-space: nowrap; + + .button { + @include workspace-button; + margin-right: 10px; + padding: 2px 8px; + } + + .button-group { + @include disable-selection; + + .button:first-child { + margin-right: 0; + } + + &:not(.spinner-group) { + .button:not(:first-child) { + border-left: none; + } + } + .button.active { + background: $workspace-button-background-active; + cursor: default; + } + } + + .spinner-value { + width: 25px; + color: $workspace-button-color; + padding: 0 5px; + display: inline-block; + text-align: center; + border-top: 1px solid $secondary-border-color; + border-bottom: 1px solid $secondary-border-color; + margin: 0; + height: 24px; + font-size: 12px; + background: #f9f9f9; + line-height: 22px; + box-sizing: border-box; + } -#workspace-toolbar .button { - @include workspace-button; - line-height: 18px; - font-size: 12px; - margin-right: 5px; - padding: 2px 8px; } diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 2500b8608..a2efb250f 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -58,12 +58,7 @@
      -
      - - - - -
      +