[CCSDK-28] populated the seed code for dgbuilder
[ccsdk/distribution.git] / dgbuilder / public / red / ui / view.js.orig
1 /**
2  * Copyright 2013, 2014 IBM Corp.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  **/
16
17
18 RED.view = (function() {
19     var space_width = 5000,
20         space_height = 5000,
21         lineCurveScale = 0.75,
22         scaleFactor = 1,
23         node_width = 100,
24         node_height = 30;
25     
26     var touchLongPressTimeout = 1000,
27         startTouchDistance = 0,
28         startTouchCenter = [],
29         moveTouchCenter = [],
30         touchStartTime = 0;
31
32
33     var activeWorkspace = 0;
34     var workspaceScrollPositions = {};
35
36     var selected_link = null,
37         mousedown_link = null,
38         mousedown_node = null,
39         mousedown_port_type = null,
40         mousedown_port_index = 0,
41         mouseup_node = null,
42         mouse_offset = [0,0],
43         mouse_position = null,
44         mouse_mode = 0,
45         moving_set = [],
46         dirty = false,
47         lasso = null,
48         showStatus = false,
49         lastClickNode = null,
50         dblClickPrimed = null,
51         clickTime = 0,
52         clickElapsed = 0;
53
54     var clipboard = "";
55
56     var status_colours = {
57         "red":    "#c00",
58         "green":  "#5a8",
59         "yellow": "#F9DF31",
60         "blue":   "#53A3F3",
61         "grey":   "#d3d3d3"
62     }
63
64     var outer = d3.select("#chart")
65         .append("svg:svg")
66         .attr("width", space_width)
67         .attr("height", space_height)
68         .attr("pointer-events", "all")
69         .style("cursor","crosshair");
70
71      var vis = outer
72         .append('svg:g')
73         .on("dblclick.zoom", null)
74         .append('svg:g')
75         .on("mousemove", canvasMouseMove)
76         .on("mousedown", canvasMouseDown)
77         .on("mouseup", canvasMouseUp)
78         .on("touchend", function() {
79             clearTimeout(touchStartTime);
80             touchStartTime = null;
81             if  (RED.touch.radialMenu.active()) {
82                 return;
83             }
84             if (lasso) {
85                 outer_background.attr("fill","#fff");
86             }
87             canvasMouseUp.call(this);
88         })
89         .on("touchcancel", canvasMouseUp)
90         .on("touchstart", function() {
91             var touch0;
92             if (d3.event.touches.length>1) {
93                 clearTimeout(touchStartTime);
94                 touchStartTime = null;
95                 d3.event.preventDefault();
96                 touch0 = d3.event.touches.item(0);
97                 var touch1 = d3.event.touches.item(1);
98                 var a = touch0['pageY']-touch1['pageY'];
99                 var b = touch0['pageX']-touch1['pageX'];
100
101                 var offset = $("#chart").offset();
102                 var scrollPos = [$("#chart").scrollLeft(),$("#chart").scrollTop()];
103                 startTouchCenter = [
104                     (touch1['pageX']+(b/2)-offset.left+scrollPos[0])/scaleFactor,
105                     (touch1['pageY']+(a/2)-offset.top+scrollPos[1])/scaleFactor
106                 ];
107                 moveTouchCenter = [
108                     touch1['pageX']+(b/2),
109                     touch1['pageY']+(a/2)
110                 ]
111                 startTouchDistance = Math.sqrt((a*a)+(b*b));
112             } else {
113                 var obj = d3.select(document.body);
114                 touch0 = d3.event.touches.item(0);
115                 var pos = [touch0.pageX,touch0.pageY];
116                 startTouchCenter = [touch0.pageX,touch0.pageY];
117                 startTouchDistance = 0;
118                 var point = d3.touches(this)[0];
119                 touchStartTime = setTimeout(function() {
120                     touchStartTime = null;
121                     showTouchMenu(obj,pos);
122                     //lasso = vis.append('rect')
123                     //    .attr("ox",point[0])
124                     //    .attr("oy",point[1])
125                     //    .attr("rx",2)
126                     //    .attr("ry",2)
127                     //    .attr("x",point[0])
128                     //    .attr("y",point[1])
129                     //    .attr("width",0)
130                     //    .attr("height",0)
131                     //    .attr("class","lasso");
132                     //outer_background.attr("fill","#e3e3f3");
133                 },touchLongPressTimeout);
134             }
135         })
136         .on("touchmove", function(){
137                 if  (RED.touch.radialMenu.active()) {
138                     d3.event.preventDefault();
139                     return;
140                 }
141                 var touch0;
142                 if (d3.event.touches.length<2) {
143                     if (touchStartTime) {
144                         touch0 = d3.event.touches.item(0);
145                         var dx = (touch0.pageX-startTouchCenter[0]);
146                         var dy = (touch0.pageY-startTouchCenter[1]);
147                         var d = Math.abs(dx*dx+dy*dy);
148                         if (d > 64) {
149                             clearTimeout(touchStartTime);
150                             touchStartTime = null;
151                         }
152                     } else if (lasso) {
153                         d3.event.preventDefault();
154                     }
155                     canvasMouseMove.call(this);
156                 } else {
157                     touch0 = d3.event.touches.item(0);
158                     var touch1 = d3.event.touches.item(1);
159                     var a = touch0['pageY']-touch1['pageY'];
160                     var b = touch0['pageX']-touch1['pageX'];
161                     var offset = $("#chart").offset();
162                     var scrollPos = [$("#chart").scrollLeft(),$("#chart").scrollTop()];
163                     var moveTouchDistance = Math.sqrt((a*a)+(b*b));
164                     var touchCenter = [
165                         touch1['pageX']+(b/2),
166                         touch1['pageY']+(a/2)
167                     ];
168
169                     if (!isNaN(moveTouchDistance)) {
170                         oldScaleFactor = scaleFactor;
171                         scaleFactor = Math.min(2,Math.max(0.3, scaleFactor + (Math.floor(((moveTouchDistance*100)-(startTouchDistance*100)))/10000)));
172
173                         var deltaTouchCenter = [                             // Try to pan whilst zooming - not 100%
174                             startTouchCenter[0]*(scaleFactor-oldScaleFactor),//-(touchCenter[0]-moveTouchCenter[0]),
175                             startTouchCenter[1]*(scaleFactor-oldScaleFactor) //-(touchCenter[1]-moveTouchCenter[1])
176                         ];
177
178                         startTouchDistance = moveTouchDistance;
179                         moveTouchCenter = touchCenter;
180
181                         $("#chart").scrollLeft(scrollPos[0]+deltaTouchCenter[0]);
182                         $("#chart").scrollTop(scrollPos[1]+deltaTouchCenter[1]);
183                         redraw();
184                     }
185                 }
186         });
187
188     var outer_background = vis.append('svg:rect')
189         .attr('width', space_width)
190         .attr('height', space_height)
191         .attr('fill','#fff');
192
193     //var gridScale = d3.scale.linear().range([0,2000]).domain([0,2000]);
194     //var grid = vis.append('g');
195     //
196     //grid.selectAll("line.horizontal").data(gridScale.ticks(100)).enter()
197     //    .append("line")
198     //        .attr(
199     //        {
200     //            "class":"horizontal",
201     //            "x1" : 0,
202     //            "x2" : 2000,
203     //            "y1" : function(d){ return gridScale(d);},
204     //            "y2" : function(d){ return gridScale(d);},
205     //            "fill" : "none",
206     //            "shape-rendering" : "crispEdges",
207     //            "stroke" : "#eee",
208     //            "stroke-width" : "1px"
209     //        });
210     //grid.selectAll("line.vertical").data(gridScale.ticks(100)).enter()
211     //    .append("line")
212     //        .attr(
213     //        {
214     //            "class":"vertical",
215     //            "y1" : 0,
216     //            "y2" : 2000,
217     //            "x1" : function(d){ return gridScale(d);},
218     //            "x2" : function(d){ return gridScale(d);},
219     //            "fill" : "none",
220     //            "shape-rendering" : "crispEdges",
221     //            "stroke" : "#eee",
222     //            "stroke-width" : "1px"
223     //        });
224
225
226     var drag_line = vis.append("svg:path").attr("class", "drag_line");
227
228     var workspace_tabs = RED.tabs.create({
229         id: "workspace-tabs",
230         onchange: function(tab) {
231             if (tab.type == "subflow") {
232                 $("#workspace-toolbar").show();
233             } else {
234                 $("#workspace-toolbar").hide();
235             }
236             var chart = $("#chart");
237             if (activeWorkspace !== 0) {
238                 workspaceScrollPositions[activeWorkspace] = {
239                     left:chart.scrollLeft(),
240                     top:chart.scrollTop()
241                 };
242             }
243             var scrollStartLeft = chart.scrollLeft();
244             var scrollStartTop = chart.scrollTop();
245
246             activeWorkspace = tab.id;
247             if (workspaceScrollPositions[activeWorkspace]) {
248                 chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left);
249                 chart.scrollTop(workspaceScrollPositions[activeWorkspace].top);
250             } else {
251                 chart.scrollLeft(0);
252                 chart.scrollTop(0);
253             }
254             var scrollDeltaLeft = chart.scrollLeft() - scrollStartLeft;
255             var scrollDeltaTop = chart.scrollTop() - scrollStartTop;
256             if (mouse_position != null) {
257                 mouse_position[0] += scrollDeltaLeft;
258                 mouse_position[1] += scrollDeltaTop;
259             }
260
261             clearSelection();
262             RED.nodes.eachNode(function(n) {
263                     n.dirty = true;
264             });
265             redraw();
266         },
267         ondblclick: function(tab) {
268             showRenameWorkspaceDialog(tab.id);
269         },
270         onadd: function(tab) {
271             RED.menu.addItem("btn-workspace-menu",{
272                 id:"btn-workspace-menu-"+tab.id.replace(".","-"),
273                 label:tab.label,
274                 onselect:function() {
275                     workspace_tabs.activateTab(tab.id);
276                 }
277             });
278             RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1);
279         },
280         onremove: function(tab) {
281             RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1);
282             RED.menu.removeItem("btn-workspace-menu-"+tab.id.replace(".","-"));
283         }
284     });
285
286     var workspaceIndex = 0;
287
288     function addWorkspace() {
289         var tabId = RED.nodes.id();
290         do {
291             workspaceIndex += 1;
292         } while($("#workspace-tabs a[title='Sheet "+workspaceIndex+"']").size() !== 0);
293
294         var ws = {type:"tab",id:tabId,label:"Sheet "+workspaceIndex};
295         RED.nodes.addWorkspace(ws);
296         workspace_tabs.addTab(ws);
297         workspace_tabs.activateTab(tabId);
298         RED.history.push({t:'add',workspaces:[ws],dirty:dirty});
299         RED.view.dirty(true);
300     }
301     $(function() {
302         $('#btn-workspace-add-tab').on("click",addWorkspace);
303         $('#btn-workspace-add').on("click",addWorkspace);
304         $('#btn-workspace-edit').on("click",function() {
305             showRenameWorkspaceDialog(activeWorkspace);
306         });
307         $('#btn-workspace-delete').on("click",function() {
308             deleteWorkspace(activeWorkspace);
309         });
310     });
311
312     function deleteWorkspace(id) {
313         if (workspace_tabs.count() == 1) {
314             return;
315         }
316         var ws = RED.nodes.workspace(id);
317         $( "#node-dialog-delete-workspace" ).dialog('option','workspace',ws);
318         $( "#node-dialog-delete-workspace-name" ).text(ws.label);
319         $( "#node-dialog-delete-workspace" ).dialog('open');
320     }
321
322     function canvasMouseDown() {
323         if (!mousedown_node && !mousedown_link) {
324             selected_link = null;
325             updateSelection();
326         }
327         if (mouse_mode === 0) {
328             if (lasso) {
329                 lasso.remove();
330                 lasso = null;
331             }
332             
333             if (!touchStartTime) {
334                 var point = d3.mouse(this);
335                 lasso = vis.append('rect')
336                     .attr("ox",point[0])
337                     .attr("oy",point[1])
338                     .attr("rx",2)
339                     .attr("ry",2)
340                     .attr("x",point[0])
341                     .attr("y",point[1])
342                     .attr("width",0)
343                     .attr("height",0)
344                     .attr("class","lasso");
345                 d3.event.preventDefault();
346             }
347         }
348     }
349
350     function canvasMouseMove() {
351         mouse_position = d3.touches(this)[0]||d3.mouse(this);
352
353         // Prevent touch scrolling...
354         //if (d3.touches(this)[0]) {
355         //    d3.event.preventDefault();
356         //}
357
358         // TODO: auto scroll the container
359         //var point = d3.mouse(this);
360         //if (point[0]-container.scrollLeft < 30 && container.scrollLeft > 0) { container.scrollLeft -= 15; }
361         //console.log(d3.mouse(this),container.offsetWidth,container.offsetHeight,container.scrollLeft,container.scrollTop);
362
363         if (lasso) {
364             var ox = parseInt(lasso.attr("ox"));
365             var oy = parseInt(lasso.attr("oy"));
366             var x = parseInt(lasso.attr("x"));
367             var y = parseInt(lasso.attr("y"));
368             var w;
369             var h;
370             if (mouse_position[0] < ox) {
371                 x = mouse_position[0];
372                 w = ox-x;
373             } else {
374                 w = mouse_position[0]-x;
375             }
376             if (mouse_position[1] < oy) {
377                 y = mouse_position[1];
378                 h = oy-y;
379             } else {
380                 h = mouse_position[1]-y;
381             }
382             lasso
383                 .attr("x",x)
384                 .attr("y",y)
385                 .attr("width",w)
386                 .attr("height",h)
387             ;
388             return;
389         }
390
391         if (mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && selected_link == null) {
392             return;
393         }
394
395         var mousePos;
396         if (mouse_mode == RED.state.JOINING) {
397             // update drag line
398             drag_line.attr("class", "drag_line");
399             mousePos = mouse_position;
400             var numOutputs = (mousedown_port_type === 0)?(mousedown_node.outputs || 1):1;
401             var sourcePort = mousedown_port_index;
402             var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
403
404             var sc = (mousedown_port_type === 0)?1:-1;
405
406             var dy = mousePos[1]-(mousedown_node.y+portY);
407             var dx = mousePos[0]-(mousedown_node.x+sc*mousedown_node.w/2);
408             var delta = Math.sqrt(dy*dy+dx*dx);
409             var scale = lineCurveScale;
410             var scaleY = 0;
411
412             if (delta < node_width) {
413                 scale = 0.75-0.75*((node_width-delta)/node_width);
414             }
415             if (dx*sc < 0) {
416                 scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
417                 if (Math.abs(dy) < 3*node_height) {
418                     scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
419                 }
420             }
421
422             drag_line.attr("d",
423                 "M "+(mousedown_node.x+sc*mousedown_node.w/2)+" "+(mousedown_node.y+portY)+
424                 " C "+(mousedown_node.x+sc*(mousedown_node.w/2+node_width*scale))+" "+(mousedown_node.y+portY+scaleY*node_height)+" "+
425                 (mousePos[0]-sc*(scale)*node_width)+" "+(mousePos[1]-scaleY*node_height)+" "+
426                 mousePos[0]+" "+mousePos[1]
427                 );
428             d3.event.preventDefault();
429         } else if (mouse_mode == RED.state.MOVING) {
430             mousePos = mouse_position;
431             var d = (mouse_offset[0]-mousePos[0])*(mouse_offset[0]-mousePos[0]) + (mouse_offset[1]-mousePos[1])*(mouse_offset[1]-mousePos[1]);
432             if (d > 2) {
433                 mouse_mode = RED.state.MOVING_ACTIVE;
434                 clickElapsed = 0;
435             }
436         } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) {
437             mousePos = mouse_position;
438             var node;
439             var i;
440             var minX = 0;
441             var minY = 0;
442             for (var n = 0; n<moving_set.length; n++) {
443                 node = moving_set[n];
444                 if (d3.event.shiftKey) {
445                     node.n.ox = node.n.x;
446                     node.n.oy = node.n.y;
447                 }
448                 node.n.x = mousePos[0]+node.dx;
449                 node.n.y = mousePos[1]+node.dy;
450                 node.n.dirty = true;
451                 minX = Math.min(node.n.x-node.n.w/2-5,minX);
452                 minY = Math.min(node.n.y-node.n.h/2-5,minY);
453             }
454             if (minX !== 0 || minY !== 0) {
455                 for (i = 0; i<moving_set.length; i++) {
456                     node = moving_set[i];
457                     node.n.x -= minX;
458                     node.n.y -= minY;
459                 }
460             }
461             if (d3.event.shiftKey && moving_set.length > 0) {
462                 var gridOffset =  [0,0];
463                 node = moving_set[0];
464                 gridOffset[0] = node.n.x-(20*Math.floor((node.n.x-node.n.w/2)/20)+node.n.w/2);
465                 gridOffset[1] = node.n.y-(20*Math.floor(node.n.y/20));
466                 if (gridOffset[0] !== 0 || gridOffset[1] !== 0) {
467                     for (i = 0; i<moving_set.length; i++) {
468                         node = moving_set[i];
469                         node.n.x -= gridOffset[0];
470                         node.n.y -= gridOffset[1];
471                         if (node.n.x == node.n.ox && node.n.y == node.n.oy) {
472                             node.dirty = false;
473                         }
474                     }
475                 }
476             }
477         }
478         redraw();
479     }
480
481     function canvasMouseUp() {
482         if (mousedown_node && mouse_mode == RED.state.JOINING) {
483             drag_line.attr("class", "drag_line_hidden");
484         }
485         if (lasso) {
486             var x = parseInt(lasso.attr("x"));
487             var y = parseInt(lasso.attr("y"));
488             var x2 = x+parseInt(lasso.attr("width"));
489             var y2 = y+parseInt(lasso.attr("height"));
490             if (!d3.event.ctrlKey) {
491                 clearSelection();
492             }
493             RED.nodes.eachNode(function(n) {
494                 if (n.z == activeWorkspace && !n.selected) {
495                     n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2);
496                     if (n.selected) {
497                         n.dirty = true;
498                         moving_set.push({n:n});
499                     }
500                 }
501             });
502             updateSelection();
503             lasso.remove();
504             lasso = null;
505         } else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey ) {
506             clearSelection();
507             updateSelection();
508         }
509         if (mouse_mode == RED.state.MOVING_ACTIVE) {
510             if (moving_set.length > 0) {
511                 var ns = [];
512                 for (var j=0;j<moving_set.length;j++) {
513                     ns.push({n:moving_set[j].n,ox:moving_set[j].ox,oy:moving_set[j].oy});
514                 }
515                 RED.history.push({t:'move',nodes:ns,dirty:dirty});
516             }
517         }
518         if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) {
519             for (var i=0;i<moving_set.length;i++) {
520                 delete moving_set[i].ox;
521                 delete moving_set[i].oy;
522             }
523         }
524         if (mouse_mode == RED.state.IMPORT_DRAGGING) {
525             RED.keyboard.remove(/* ESCAPE */ 27);
526             setDirty(true);
527         }
528         redraw();
529         // clear mouse event vars
530         resetMouseVars();
531     }
532
533     $('#btn-zoom-out').click(function() {zoomOut();});
534     $('#btn-zoom-zero').click(function() {zoomZero();});
535     $('#btn-zoom-in').click(function() {zoomIn();});
536     $("#chart").on('DOMMouseScroll mousewheel', function (evt) {
537         if ( evt.altKey ) {
538             evt.preventDefault();
539             evt.stopPropagation();
540             var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta;
541             if (move <= 0) { zoomOut(); }
542             else { zoomIn(); }
543         }
544     });
545     $("#chart").droppable({
546             accept:".palette_node",
547             drop: function( event, ui ) {
548                 d3.event = event;
549                 var selected_tool = ui.draggable[0].type;
550                 var mousePos = d3.touches(this)[0]||d3.mouse(this);
551                 mousePos[1] += this.scrollTop;
552                 mousePos[0] += this.scrollLeft;
553                 mousePos[1] /= scaleFactor;
554                 mousePos[0] /= scaleFactor;
555
556                 var nn = { id:(1+Math.random()*4294967295).toString(16),x: mousePos[0],y:mousePos[1],w:node_width,z:activeWorkspace};
557
558                 nn.type = selected_tool;
559                 nn._def = RED.nodes.getType(nn.type);
560                 nn.outputs = nn._def.outputs;
561                 nn.changed = true;
562
563                 for (var d in nn._def.defaults) {
564                     if (nn._def.defaults.hasOwnProperty(d)) {
565                         nn[d] = nn._def.defaults[d].value;
566                     }
567                 }
568
569                 if (nn._def.onadd) {
570                     nn._def.onadd.call(nn);
571                 }
572
573                 nn.h = Math.max(node_height,(nn.outputs||0) * 15);
574                 RED.history.push({t:'add',nodes:[nn.id],dirty:dirty});
575                 RED.nodes.add(nn);
576                 RED.editor.validateNode(nn);
577                 setDirty(true);
578                 // auto select dropped node - so info shows (if visible)
579                 clearSelection();
580                 nn.selected = true;
581                 moving_set.push({n:nn});
582                 updateSelection();
583                 redraw();
584
585                 if (nn._def.autoedit) {
586                     RED.editor.edit(nn);
587                 }
588             }
589     });
590
591     function zoomIn() {
592         if (scaleFactor < 2) {
593             scaleFactor += 0.1;
594             redraw();
595         }
596     }
597     function zoomOut() {
598         if (scaleFactor > 0.3) {
599             scaleFactor -= 0.1;
600             redraw();
601         }
602     }
603     function zoomZero() {
604         scaleFactor = 1;
605         redraw();
606     }
607
608     function selectAll() {
609         RED.nodes.eachNode(function(n) {
610             if (n.z == activeWorkspace) {
611                 if (!n.selected) {
612                     n.selected = true;
613                     n.dirty = true;
614                     moving_set.push({n:n});
615                 }
616             }
617         });
618         selected_link = null;
619         updateSelection();
620         redraw();
621     }
622
623     function clearSelection() {
624         for (var i=0;i<moving_set.length;i++) {
625             var n = moving_set[i];
626             n.n.dirty = true;
627             n.n.selected = false;
628         }
629         moving_set = [];
630         selected_link = null;
631     }
632
633     function updateSelection() {
634         if (moving_set.length === 0) {
635             RED.menu.setDisabled("btn-export-menu",true);
636             RED.menu.setDisabled("btn-export-clipboard",true);
637             RED.menu.setDisabled("btn-export-library",true);
638         } else {
639             RED.menu.setDisabled("btn-export-menu",false);
640             RED.menu.setDisabled("btn-export-clipboard",false);
641             RED.menu.setDisabled("btn-export-library",false);
642         }
643         if (moving_set.length === 0 && selected_link == null) {
644             RED.keyboard.remove(/* backspace */ 8);
645             RED.keyboard.remove(/* delete */ 46);
646             RED.keyboard.remove(/* c */ 67);
647             RED.keyboard.remove(/* x */ 88);
648         } else {
649             RED.keyboard.add(/* backspace */ 8,function(){deleteSelection();d3.event.preventDefault();});
650             RED.keyboard.add(/* delete */ 46,function(){deleteSelection();d3.event.preventDefault();});
651             RED.keyboard.add(/* c */ 67,{ctrl:true},function(){copySelection();d3.event.preventDefault();});
652             RED.keyboard.add(/* x */ 88,{ctrl:true},function(){copySelection();deleteSelection();d3.event.preventDefault();});
653         }
654         if (moving_set.length === 0) {
655             RED.keyboard.remove(/* up   */ 38);
656             RED.keyboard.remove(/* down */ 40);
657             RED.keyboard.remove(/* left */ 37);
658             RED.keyboard.remove(/* right*/ 39);
659         } else {
660             RED.keyboard.add(/* up   */ 38, function() { if(d3.event.shiftKey){moveSelection(  0,-20)}else{moveSelection( 0,-1);}d3.event.preventDefault();},endKeyboardMove);
661             RED.keyboard.add(/* down */ 40, function() { if(d3.event.shiftKey){moveSelection(  0, 20)}else{moveSelection( 0, 1);}d3.event.preventDefault();},endKeyboardMove);
662             RED.keyboard.add(/* left */ 37, function() { if(d3.event.shiftKey){moveSelection(-20,  0)}else{moveSelection(-1, 0);}d3.event.preventDefault();},endKeyboardMove);
663             RED.keyboard.add(/* right*/ 39, function() { if(d3.event.shiftKey){moveSelection( 20,  0)}else{moveSelection( 1, 0);}d3.event.preventDefault();},endKeyboardMove);
664         }
665         if (moving_set.length == 1) {
666             RED.sidebar.info.refresh(moving_set[0].n);
667         } else {
668             RED.sidebar.info.clear();
669         }
670     }
671     function endKeyboardMove() {
672         var ns = [];
673         for (var i=0;i<moving_set.length;i++) {
674             ns.push({n:moving_set[i].n,ox:moving_set[i].ox,oy:moving_set[i].oy});
675             delete moving_set[i].ox;
676             delete moving_set[i].oy;
677         }
678         RED.history.push({t:'move',nodes:ns,dirty:dirty});
679     }
680     function moveSelection(dx,dy) {
681         var minX = 0;
682         var minY = 0;
683         var node;
684         
685         for (var i=0;i<moving_set.length;i++) {
686             node = moving_set[i];
687             if (node.ox == null && node.oy == null) {
688                 node.ox = node.n.x;
689                 node.oy = node.n.y;
690             }
691             node.n.x += dx;
692             node.n.y += dy;
693             node.n.dirty = true;
694             minX = Math.min(node.n.x-node.n.w/2-5,minX);
695             minY = Math.min(node.n.y-node.n.h/2-5,minY);
696         }
697
698         if (minX !== 0 || minY !== 0) {
699             for (var n = 0; n<moving_set.length; n++) {
700                 node = moving_set[n];
701                 node.n.x -= minX;
702                 node.n.y -= minY;
703             }
704         }
705
706         redraw();
707     }
708     function deleteSelection() {
709         var removedNodes = [];
710         var removedLinks = [];
711         var startDirty = dirty;
712         if (moving_set.length > 0) {
713             for (var i=0;i<moving_set.length;i++) {
714                 var node = moving_set[i].n;
715                 node.selected = false;
716                 if (node.x < 0) {
717                     node.x = 25
718                 }
719                 var rmlinks = RED.nodes.remove(node.id);
720                 removedNodes.push(node);
721                 removedLinks = removedLinks.concat(rmlinks);
722             }
723             moving_set = [];
724             setDirty(true);
725         }
726         if (selected_link) {
727             RED.nodes.removeLink(selected_link);
728             removedLinks.push(selected_link);
729             setDirty(true);
730         }
731         RED.history.push({t:'delete',nodes:removedNodes,links:removedLinks,dirty:startDirty});
732
733         selected_link = null;
734         updateSelection();
735         redraw();
736     }
737
738     function copySelection() {
739         if (moving_set.length > 0) {
740             var nns = [];
741             for (var n=0;n<moving_set.length;n++) {
742                 var node = moving_set[n].n;
743                 nns.push(RED.nodes.convertNode(node));
744             }
745             clipboard = JSON.stringify(nns);
746             RED.notify(moving_set.length+" node"+(moving_set.length>1?"s":"")+" copied");
747         }
748     }
749
750
751     function calculateTextWidth(str) {
752         var sp = document.createElement("span");
753         sp.className = "node_label";
754         sp.style.position = "absolute";
755         sp.style.top = "-1000px";
756         sp.innerHTML = (str||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
757         document.body.appendChild(sp);
758         var w = sp.offsetWidth;
759         document.body.removeChild(sp);
760         return 50+w;
761     }
762
763     function resetMouseVars() {
764         mousedown_node = null;
765         mouseup_node = null;
766         mousedown_link = null;
767         mouse_mode = 0;
768         mousedown_port_type = 0;
769     }
770
771     function portMouseDown(d,portType,portIndex) {
772         // disable zoom
773         //vis.call(d3.behavior.zoom().on("zoom"), null);
774         mousedown_node = d;
775         selected_link = null;
776         mouse_mode = RED.state.JOINING;
777         mousedown_port_type = portType;
778         mousedown_port_index = portIndex || 0;
779         document.body.style.cursor = "crosshair";
780         d3.event.preventDefault();
781     }
782
783     function portMouseUp(d,portType,portIndex) {
784         document.body.style.cursor = "";
785         if (mouse_mode == RED.state.JOINING && mousedown_node) {
786             if (typeof TouchEvent != "undefined" && d3.event instanceof TouchEvent) {
787                 RED.nodes.eachNode(function(n) {
788                         if (n.z == activeWorkspace) {
789                             var hw = n.w/2;
790                             var hh = n.h/2;
791                             if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
792                                 n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
793                                     mouseup_node = n;
794                                     portType = mouseup_node._def.inputs>0?1:0;
795                                     portIndex = 0;
796                             }
797                         }
798                 });
799             } else {
800                 mouseup_node = d;
801             }
802             if (portType == mousedown_port_type || mouseup_node === mousedown_node) {
803                 drag_line.attr("class", "drag_line_hidden");
804                 resetMouseVars();
805                 return;
806             }
807             var src,dst,src_port;
808             if (mousedown_port_type === 0) {
809                 src = mousedown_node;
810                 src_port = mousedown_port_index;
811                 dst = mouseup_node;
812             } else if (mousedown_port_type == 1) {
813                 src = mouseup_node;
814                 dst = mousedown_node;
815                 src_port = portIndex;
816             }
817
818             var existingLink = false;
819             RED.nodes.eachLink(function(d) {
820                     existingLink = existingLink || (d.source === src && d.target === dst && d.sourcePort == src_port);
821             });
822             if (!existingLink) {
823                 var link = {source: src, sourcePort:src_port, target: dst};
824                 RED.nodes.addLink(link);
825                 RED.history.push({t:'add',links:[link],dirty:dirty});
826                 setDirty(true);
827             }
828             selected_link = null;
829             redraw();
830         }
831     }
832
833     function nodeMouseUp(d) {
834         if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
835             RED.editor.edit(d);
836             clickElapsed = 0;
837             d3.event.stopPropagation();
838             return;
839         }
840         portMouseUp(d, d._def.inputs > 0 ? 1 : 0, 0);
841     }
842
843     function nodeMouseDown(d) {
844         //var touch0 = d3.event;
845         //var pos = [touch0.pageX,touch0.pageY];
846         //RED.touch.radialMenu.show(d3.select(this),pos);
847         if (mouse_mode == RED.state.IMPORT_DRAGGING) {
848             RED.keyboard.remove(/* ESCAPE */ 27);
849             updateSelection();
850             setDirty(true);
851             redraw();
852             resetMouseVars();
853             d3.event.stopPropagation();
854             return;
855         }
856         mousedown_node = d;
857         var now = Date.now();
858         clickElapsed = now-clickTime;
859         clickTime = now;
860
861         dblClickPrimed = (lastClickNode == mousedown_node);
862         lastClickNode = mousedown_node;
863         
864         var i;
865         
866         if (d.selected && d3.event.ctrlKey) {
867             d.selected = false;
868             for (i=0;i<moving_set.length;i+=1) {
869                 if (moving_set[i].n === d) {
870                     moving_set.splice(i,1);
871                     break;
872                 }
873             }
874         } else {
875             if (d3.event.shiftKey) {
876                 clearSelection();
877                 var cnodes = RED.nodes.getAllFlowNodes(mousedown_node);
878                 for (var n=0;n<cnodes.length;n++) {
879                     cnodes[n].selected = true;
880                     cnodes[n].dirty = true;
881                     moving_set.push({n:cnodes[n]});
882                 }
883             } else if (!d.selected) {
884                 if (!d3.event.ctrlKey) {
885                     clearSelection();
886                 }
887                 mousedown_node.selected = true;
888                 moving_set.push({n:mousedown_node});
889             }
890             selected_link = null;
891             if (d3.event.button != 2) {
892                 mouse_mode = RED.state.MOVING;
893                 var mouse = d3.touches(this)[0]||d3.mouse(this);
894                 mouse[0] += d.x-d.w/2;
895                 mouse[1] += d.y-d.h/2;
896                 for (i=0;i<moving_set.length;i++) {
897                     moving_set[i].ox = moving_set[i].n.x;
898                     moving_set[i].oy = moving_set[i].n.y;
899                     moving_set[i].dx = moving_set[i].n.x-mouse[0];
900                     moving_set[i].dy = moving_set[i].n.y-mouse[1];
901                 }
902                 mouse_offset = d3.mouse(document.body);
903                 if (isNaN(mouse_offset[0])) {
904                     mouse_offset = d3.touches(document.body)[0];
905                 }
906             }
907         }
908         d.dirty = true;
909         updateSelection();
910         redraw();
911         d3.event.stopPropagation();
912     }
913
914     function nodeButtonClicked(d) {
915         if (d._def.button.toggle) {
916             d[d._def.button.toggle] = !d[d._def.button.toggle];
917             d.dirty = true;
918         }
919         if (d._def.button.onclick) {
920             d._def.button.onclick.call(d);
921         }
922         if (d.dirty) {
923             redraw();
924         }
925         d3.event.preventDefault();
926     }
927
928     function showTouchMenu(obj,pos) {
929         var mdn = mousedown_node;
930         var options = [];
931         options.push({name:"delete",disabled:(moving_set.length===0),onselect:function() {deleteSelection();}});
932         options.push({name:"cut",disabled:(moving_set.length===0),onselect:function() {copySelection();deleteSelection();}});
933         options.push({name:"copy",disabled:(moving_set.length===0),onselect:function() {copySelection();}});
934         options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard,true);}});
935         options.push({name:"edit",disabled:(moving_set.length != 1),onselect:function() { RED.editor.edit(mdn);}});
936         options.push({name:"select",onselect:function() {selectAll();}});
937         options.push({name:"undo",disabled:(RED.history.depth() === 0),onselect:function() {RED.history.pop();}});
938         
939         RED.touch.radialMenu.show(obj,pos,options);
940         resetMouseVars();
941     }
942     function redraw() {
943         vis.attr("transform","scale("+scaleFactor+")");
944         outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor);
945
946         if (mouse_mode != RED.state.JOINING) {
947             // Don't bother redrawing nodes if we're drawing links
948
949             var node = vis.selectAll(".nodegroup").data(RED.nodes.nodes.filter(function(d) { return d.z == activeWorkspace }),function(d){return d.id});
950             node.exit().remove();
951
952             var nodeEnter = node.enter().insert("svg:g").attr("class", "node nodegroup");
953             nodeEnter.each(function(d,i) {
954                     var node = d3.select(this);
955                     node.attr("id",d.id);
956                     var l = d._def.label;
957                     l = (typeof l === "function" ? l.call(d) : l)||"";
958                     d.w = Math.max(node_width,calculateTextWidth(l)+(d._def.inputs>0?7:0) );
959                     d.h = Math.max(node_height,(d.outputs||0) * 15);
960
961                     if (d._def.badge) {
962                         var badge = node.append("svg:g").attr("class","node_badge_group");
963                         var badgeRect = badge.append("rect").attr("class","node_badge").attr("rx",5).attr("ry",5).attr("width",40).attr("height",15);
964                         badge.append("svg:text").attr("class","node_badge_label").attr("x",35).attr("y",11).attr('text-anchor','end').text(d._def.badge());
965                         if (d._def.onbadgeclick) {
966                             badgeRect.attr("cursor","pointer")
967                                 .on("click",function(d) { d._def.onbadgeclick.call(d);d3.event.preventDefault();});
968                         }
969                     }
970
971                     if (d._def.button) {
972                         var nodeButtonGroup = node.append('svg:g')
973                             .attr("transform",function(d) { return "translate("+((d._def.align == "right") ? 94 : -25)+",2)"; })
974                             .attr("class",function(d) { return "node_button "+((d._def.align == "right") ? "node_right_button" : "node_left_button"); });
975                         nodeButtonGroup.append('rect')
976                             .attr("rx",8)
977                             .attr("ry",8)
978                             .attr("width",32)
979                             .attr("height",node_height-4)
980                             .attr("fill","#eee");//function(d) { return d._def.color;})
981                         nodeButtonGroup.append('rect')
982                             .attr("x",function(d) { return d._def.align == "right"? 10:5})
983                             .attr("y",4)
984                             .attr("rx",5)
985                             .attr("ry",5)
986                             .attr("width",16)
987                             .attr("height",node_height-12)
988                             .attr("fill",function(d) { return d._def.color;})
989                             .attr("cursor","pointer")
990                             .on("mousedown",function(d) {if (!lasso) { d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
991                             .on("mouseup",function(d) {if (!lasso) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
992                             .on("mouseover",function(d) {if (!lasso) { d3.select(this).attr("fill-opacity",0.4);}})
993                             .on("mouseout",function(d) {if (!lasso) {
994                                 var op = 1;
995                                 if (d._def.button.toggle) {
996                                     op = d[d._def.button.toggle]?1:0.2;
997                                 }
998                                 d3.select(this).attr("fill-opacity",op);
999                             }})
1000                             .on("click",nodeButtonClicked)
1001                             .on("touchstart",nodeButtonClicked)
1002                     }
1003
1004                     var mainRect = node.append("rect")
1005                         .attr("class", "node")
1006                         .classed("node_unknown",function(d) { return d.type == "unknown"; })
1007                         .attr("rx", 6)
1008                         .attr("ry", 6)
1009                         .attr("fill",function(d) { return d._def.color;})
1010                         .on("mouseup",nodeMouseUp)
1011                         .on("mousedown",nodeMouseDown)
1012                         .on("touchstart",function(d) {
1013                             var obj = d3.select(this);
1014                             var touch0 = d3.event.touches.item(0);
1015                             var pos = [touch0.pageX,touch0.pageY];
1016                             startTouchCenter = [touch0.pageX,touch0.pageY];
1017                             startTouchDistance = 0;
1018                             touchStartTime = setTimeout(function() {
1019                                 showTouchMenu(obj,pos);
1020                             },touchLongPressTimeout);
1021                             nodeMouseDown.call(this,d)       
1022                         })
1023                         .on("touchend", function(d) {
1024                             clearTimeout(touchStartTime);
1025                             touchStartTime = null;
1026                             if  (RED.touch.radialMenu.active()) {
1027                                 d3.event.stopPropagation();
1028                                 return;
1029                             }
1030                             nodeMouseUp.call(this,d);
1031                         })
1032                         .on("mouseover",function(d) {
1033                                 if (mouse_mode === 0) {
1034                                     var node = d3.select(this);
1035                                     node.classed("node_hovered",true);
1036                                 }
1037                         })
1038                         .on("mouseout",function(d) {
1039                                 var node = d3.select(this);
1040                                 node.classed("node_hovered",false);
1041                         });
1042
1043                    //node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none");
1044                    //node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none");
1045
1046                     if (d._def.icon) {
1047                         
1048                         var icon_group = node.append("g")
1049                             .attr("class","node_icon_group")
1050                             .attr("x",0).attr("y",0);
1051                         
1052                         var icon_shade = icon_group.append("rect")
1053                             .attr("x",0).attr("y",0)
1054                             .attr("class","node_icon_shade")
1055                             .attr("width","30")
1056                             .attr("stroke","none")
1057                             .attr("fill","#000")
1058                             .attr("fill-opacity","0.05")
1059                             .attr("height",function(d){return Math.min(50,d.h-4);});
1060                             
1061                         var icon = icon_group.append("image")
1062                             .attr("xlink:href","icons/"+d._def.icon)
1063                             .attr("class","node_icon")
1064                             .attr("x",0)
1065                             .attr("width","30")
1066                             .attr("height","30");
1067                             
1068                         var icon_shade_border = icon_group.append("path")
1069                             .attr("d",function(d) { return "M 30 1 l 0 "+(d.h-2)})
1070                             .attr("class","node_icon_shade_border")
1071                             .attr("stroke-opacity","0.1")
1072                             .attr("stroke","#000")
1073                             .attr("stroke-width","2");
1074
1075                         if ("right" == d._def.align) {
1076                             icon_group.attr('class','node_icon_group node_icon_group_'+d._def.align);
1077                             icon_shade_border.attr("d",function(d) { return "M 0 1 l 0 "+(d.h-2)})
1078                             //icon.attr('class','node_icon node_icon_'+d._def.align);
1079                             //icon.attr('class','node_icon_shade node_icon_shade_'+d._def.align);
1080                             //icon.attr('class','node_icon_shade_border node_icon_shade_border_'+d._def.align);
1081                         }
1082                         
1083                         //if (d._def.inputs > 0 && d._def.align == null) {
1084                         //    icon_shade.attr("width",35);
1085                         //    icon.attr("transform","translate(5,0)");
1086                         //    icon_shade_border.attr("transform","translate(5,0)");
1087                         //}
1088                         //if (d._def.outputs > 0 && "right" == d._def.align) {
1089                         //    icon_shade.attr("width",35); //icon.attr("x",5);
1090                         //}
1091                         
1092                         var img = new Image();
1093                         img.src = "icons/"+d._def.icon;
1094                         img.onload = function() {
1095                             icon.attr("width",Math.min(img.width,30));
1096                             icon.attr("height",Math.min(img.height,30));
1097                             icon.attr("x",15-Math.min(img.width,30)/2);
1098                             //if ("right" == d._def.align) {
1099                             //    icon.attr("x",function(d){return d.w-img.width-1-(d.outputs>0?5:0);});
1100                             //    icon_shade.attr("x",function(d){return d.w-30});
1101                             //    icon_shade_border.attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2);});
1102                             //}
1103                         }
1104                         
1105                         //icon.style("pointer-events","none");
1106                         icon_group.style("pointer-events","none");
1107                     }
1108                     var text = node.append('svg:text').attr('class','node_label').attr('x', 38).attr('dy', '.35em').attr('text-anchor','start');
1109                     if (d._def.align) {
1110                         text.attr('class','node_label node_label_'+d._def.align);
1111                         text.attr('text-anchor','end');
1112                     }
1113
1114                     var status = node.append("svg:g").attr("class","node_status_group").style("display","none");
1115
1116                     var statusRect = status.append("rect").attr("class","node_status")
1117                                         .attr("x",6).attr("y",1).attr("width",9).attr("height",9)
1118                                         .attr("rx",2).attr("ry",2).attr("stroke-width","3");
1119
1120                     var statusLabel = status.append("svg:text")
1121                         .attr("class","node_status_label")
1122                         .attr('x',20).attr('y',9)
1123                         .style({
1124                                 'stroke-width': 0,
1125                                 'fill': '#888',
1126                                 'font-size':'9pt',
1127                                 'stroke':'#000',
1128                                 'text-anchor':'start'
1129                         });
1130
1131                     //node.append("circle").attr({"class":"centerDot","cx":0,"cy":0,"r":5});
1132
1133                     if (d._def.inputs > 0) {
1134                         text.attr("x",38);
1135                         node.append("rect").attr("class","port port_input").attr("rx",3).attr("ry",3).attr("x",-5).attr("width",10).attr("height",10)
1136                             .on("mousedown",function(d){portMouseDown(d,1,0);})
1137                             .on("touchstart",function(d){portMouseDown(d,1,0);})
1138                             .on("mouseup",function(d){portMouseUp(d,1,0);} )
1139                             .on("touchend",function(d){portMouseUp(d,1,0);} )
1140                             .on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type != 1 ));})
1141                             .on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
1142                     }
1143
1144                     //node.append("path").attr("class","node_error").attr("d","M 3,-3 l 10,0 l -5,-8 z");
1145                     node.append("image").attr("class","node_error hidden").attr("xlink:href","icons/node-error.png").attr("x",0).attr("y",-6).attr("width",10).attr("height",9);
1146                     node.append("image").attr("class","node_changed hidden").attr("xlink:href","icons/node-changed.png").attr("x",12).attr("y",-6).attr("width",10).attr("height",10);
1147             });
1148
1149             node.each(function(d,i) {
1150                     if (d.dirty) {
1151                         //if (d.x < -50) deleteSelection();  // Delete nodes if dragged back to palette
1152                         if (d.resize) {
1153                             var l = d._def.label;
1154                             l = (typeof l === "function" ? l.call(d) : l)||"";
1155                             d.w = Math.max(node_width,calculateTextWidth(l)+(d._def.inputs>0?7:0) );
1156                             d.h = Math.max(node_height,(d.outputs||0) * 15);
1157                         }
1158                         var thisNode = d3.select(this);
1159                         //thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
1160                         thisNode.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
1161                         thisNode.selectAll(".node")
1162                             .attr("width",function(d){return d.w})
1163                             .attr("height",function(d){return d.h})
1164                             .classed("node_selected",function(d) { return d.selected; })
1165                             .classed("node_highlighted",function(d) { return d.highlighted; })
1166                         ;
1167                         //thisNode.selectAll(".node-gradient-top").attr("width",function(d){return d.w});
1168                         //thisNode.selectAll(".node-gradient-bottom").attr("width",function(d){return d.w}).attr("y",function(d){return d.h-30});
1169
1170                         thisNode.selectAll(".node_icon_group_right").attr('transform', function(d){return "translate("+(d.w-30)+",0)"});
1171                         thisNode.selectAll(".node_label_right").attr('x', function(d){return d.w-38});
1172                         //thisNode.selectAll(".node_icon_right").attr("x",function(d){return d.w-d3.select(this).attr("width")-1-(d.outputs>0?5:0);});
1173                         //thisNode.selectAll(".node_icon_shade_right").attr("x",function(d){return d.w-30;});
1174                         //thisNode.selectAll(".node_icon_shade_border_right").attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2)});
1175
1176                         
1177                         var numOutputs = d.outputs;
1178                         var y = (d.h/2)-((numOutputs-1)/2)*13;
1179                         d.ports = d.ports || d3.range(numOutputs);
1180                         d._ports = thisNode.selectAll(".port_output").data(d.ports);
1181                         d._ports.enter().append("rect").attr("class","port port_output").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
1182                             .on("mousedown",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
1183                             .on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
1184                             .on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
1185                             .on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
1186                             .on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
1187                             .on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
1188                         d._ports.exit().remove();
1189                         if (d._ports) {
1190                             numOutputs = d.outputs || 1;
1191                             y = (d.h/2)-((numOutputs-1)/2)*13;
1192                             var x = d.w - 5;
1193                             d._ports.each(function(d,i) {
1194                                     var port = d3.select(this);
1195                                     port.attr("y",(y+13*i)-5).attr("x",x);
1196                             });
1197                         }
1198                         thisNode.selectAll('text.node_label').text(function(d,i){
1199                                 if (d._def.label) {
1200                                     if (typeof d._def.label == "function") {
1201                                         return d._def.label.call(d);
1202                                     } else {
1203                                         return d._def.label;
1204                                     }
1205                                 }
1206                                 return "";
1207                         })
1208                             .attr('y', function(d){return (d.h/2)-1;})
1209                             .attr('class',function(d){
1210                                 return 'node_label'+
1211                                 (d._def.align?' node_label_'+d._def.align:'')+
1212                                 (d._def.labelStyle?' '+(typeof d._def.labelStyle == "function" ? d._def.labelStyle.call(d):d._def.labelStyle):'') ;
1213                         });
1214                         thisNode.selectAll(".node_tools").attr("x",function(d){return d.w-35;}).attr("y",function(d){return d.h-20;});
1215
1216                         thisNode.selectAll(".node_changed")
1217                             .attr("x",function(d){return d.w-10})
1218                             .classed("hidden",function(d) { return !d.changed; });
1219
1220                         thisNode.selectAll(".node_error")
1221                             .attr("x",function(d){return d.w-10-(d.changed?13:0)})
1222                             .classed("hidden",function(d) { return d.valid; });
1223
1224                         thisNode.selectAll(".port_input").each(function(d,i) {
1225                                 var port = d3.select(this);
1226                                 port.attr("y",function(d){return (d.h/2)-5;})
1227                         });
1228
1229                         thisNode.selectAll(".node_icon").attr("y",function(d){return (d.h-d3.select(this).attr("height"))/2;});
1230                         thisNode.selectAll(".node_icon_shade").attr("height",function(d){return d.h;});
1231                         thisNode.selectAll(".node_icon_shade_border").attr("d",function(d){ return "M "+(("right" == d._def.align) ?0:30)+" 1 l 0 "+(d.h-2)});
1232
1233                         
1234                         thisNode.selectAll('.node_right_button').attr("transform",function(d){
1235                                 var x = d.w-6;
1236                                 if (d._def.button.toggle && !d[d._def.button.toggle]) {
1237                                     x = x - 8;
1238                                 }
1239                                 return "translate("+x+",2)";
1240                         });
1241                         thisNode.selectAll('.node_right_button rect').attr("fill-opacity",function(d){
1242                                 if (d._def.button.toggle) {
1243                                     return d[d._def.button.toggle]?1:0.2;
1244                                 }
1245                                 return 1;
1246                         });
1247
1248                         //thisNode.selectAll('.node_right_button').attr("transform",function(d){return "translate("+(d.w - d._def.button.width.call(d))+","+0+")";}).attr("fill",function(d) {
1249                         //         return typeof d._def.button.color  === "function" ? d._def.button.color.call(d):(d._def.button.color != null ? d._def.button.color : d._def.color)
1250                         //});
1251
1252                         thisNode.selectAll('.node_badge_group').attr("transform",function(d){return "translate("+(d.w-40)+","+(d.h+3)+")";});
1253                         thisNode.selectAll('text.node_badge_label').text(function(d,i) {
1254                             if (d._def.badge) {
1255                                 if (typeof d._def.badge == "function") {
1256                                     return d._def.badge.call(d);
1257                                 } else {
1258                                     return d._def.badge;
1259                                 }
1260                             }
1261                             return "";
1262                         });
1263                         if (!showStatus || !d.status) {
1264                             thisNode.selectAll('.node_status_group').style("display","none");
1265                         } else {
1266                             thisNode.selectAll('.node_status_group').style("display","inline").attr("transform","translate(3,"+(d.h+3)+")");
1267                             var fill = status_colours[d.status.fill]; // Only allow our colours for now
1268                             if (d.status.shape == null && fill == null) {
1269                                 thisNode.selectAll('.node_status').style("display","none");
1270                             } else {
1271                                 var style;
1272                                 if (d.status.shape == null || d.status.shape == "dot") {
1273                                     style = {
1274                                         display: "inline",
1275                                         fill: fill,
1276                                         stroke: fill
1277                                     };
1278                                 } else if (d.status.shape == "ring" ){
1279                                     style = {
1280                                         display: "inline",
1281                                         fill: '#fff',
1282                                         stroke: fill
1283                                     }
1284                                 }
1285                                 thisNode.selectAll('.node_status').style(style);
1286                             }
1287                             if (d.status.text) {
1288                                 thisNode.selectAll('.node_status_label').text(d.status.text);
1289                             } else {
1290                                 thisNode.selectAll('.node_status_label').text("");
1291                             }
1292                         }
1293
1294                         d.dirty = false;
1295                     }
1296             });
1297         }
1298
1299         var link = vis.selectAll(".link").data(RED.nodes.links.filter(function(d) { return d.source.z == activeWorkspace && d.target.z == activeWorkspace }),function(d) { return d.source.id+":"+d.sourcePort+":"+d.target.id;});
1300
1301         var linkEnter = link.enter().insert("g",".node").attr("class","link");
1302         
1303         linkEnter.each(function(d,i) {
1304             var l = d3.select(this);
1305             l.append("svg:path").attr("class","link_background link_path")
1306                .on("mousedown",function(d) {
1307                     mousedown_link = d;
1308                     clearSelection();
1309                     selected_link = mousedown_link;
1310                     updateSelection();
1311                     redraw();
1312                     d3.event.stopPropagation();
1313                 })
1314                 .on("touchstart",function(d) {
1315                     mousedown_link = d;
1316                     clearSelection();
1317                     selected_link = mousedown_link;
1318                     updateSelection();
1319                     redraw();
1320                     d3.event.stopPropagation();
1321                 });
1322             l.append("svg:path").attr("class","link_outline link_path");
1323             l.append("svg:path").attr("class","link_line link_path");
1324         });
1325
1326         link.exit().remove();
1327
1328         var links = vis.selectAll(".link_path")
1329         links.attr("d",function(d){
1330                 var numOutputs = d.source.outputs || 1;
1331                 var sourcePort = d.sourcePort || 0;
1332                 var y = -((numOutputs-1)/2)*13 +13*sourcePort;
1333
1334                 var dy = d.target.y-(d.source.y+y);
1335                 var dx = (d.target.x-d.target.w/2)-(d.source.x+d.source.w/2);
1336                 var delta = Math.sqrt(dy*dy+dx*dx);
1337                 var scale = lineCurveScale;
1338                 var scaleY = 0;
1339                 if (delta < node_width) {
1340                     scale = 0.75-0.75*((node_width-delta)/node_width);
1341                 }
1342
1343                 if (dx < 0) {
1344                     scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
1345                     if (Math.abs(dy) < 3*node_height) {
1346                         scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
1347                     }
1348                 }
1349
1350                 d.x1 = d.source.x+d.source.w/2;
1351                 d.y1 = d.source.y+y;
1352                 d.x2 = d.target.x-d.target.w/2;
1353                 d.y2 = d.target.y;
1354
1355                 return "M "+(d.source.x+d.source.w/2)+" "+(d.source.y+y)+
1356                     " C "+(d.source.x+d.source.w/2+scale*node_width)+" "+(d.source.y+y+scaleY*node_height)+" "+
1357                     (d.target.x-d.target.w/2-scale*node_width)+" "+(d.target.y-scaleY*node_height)+" "+
1358                     (d.target.x-d.target.w/2)+" "+d.target.y;
1359         })
1360
1361         link.classed("link_selected", function(d) { return d === selected_link || d.selected; });
1362         link.classed("link_unknown",function(d) { return d.target.type == "unknown" || d.source.type == "unknown"});
1363
1364         if (d3.event) {
1365             d3.event.preventDefault();
1366         }
1367     }
1368
1369     RED.keyboard.add(/* z */ 90,{ctrl:true},function(){RED.history.pop();});
1370     RED.keyboard.add(/* a */ 65,{ctrl:true},function(){selectAll();d3.event.preventDefault();});
1371     RED.keyboard.add(/* = */ 187,{ctrl:true},function(){zoomIn();d3.event.preventDefault();});
1372     RED.keyboard.add(/* - */ 189,{ctrl:true},function(){zoomOut();d3.event.preventDefault();});
1373     RED.keyboard.add(/* 0 */ 48,{ctrl:true},function(){zoomZero();d3.event.preventDefault();});
1374     RED.keyboard.add(/* v */ 86,{ctrl:true},function(){importNodes(clipboard);d3.event.preventDefault();});
1375     RED.keyboard.add(/* e */ 69,{ctrl:true},function(){showExportNodesDialog();d3.event.preventDefault();});
1376     RED.keyboard.add(/* i */ 73,{ctrl:true},function(){showImportNodesDialog();d3.event.preventDefault();});
1377
1378     // TODO: 'dirty' should be a property of RED.nodes - with an event callback for ui hooks
1379     function setDirty(d) {
1380         dirty = d;
1381         if (dirty) {
1382             $("#btn-deploy").removeClass("disabled");
1383         } else {
1384             $("#btn-deploy").addClass("disabled");
1385         }
1386     }
1387
1388     /**
1389      * Imports a new collection of nodes from a JSON String.
1390      *  - all get new IDs assigned
1391      *  - all 'selected'
1392      *  - attached to mouse for placing - 'IMPORT_DRAGGING'
1393      */
1394     function importNodes(newNodesStr,touchImport) {
1395         try {
1396             var result = RED.nodes.import(newNodesStr,true);
1397             if (result) {
1398                 var new_nodes = result[0];
1399                 var new_links = result[1];
1400                 var new_workspaces = result[2];
1401                 
1402                 var new_ms = new_nodes.filter(function(n) { return n.z == activeWorkspace }).map(function(n) { return {n:n};});
1403                 var new_node_ids = new_nodes.map(function(n){ return n.id; });
1404                 
1405                 // TODO: pick a more sensible root node
1406                 if (new_ms.length > 0) {
1407                     var root_node = new_ms[0].n;
1408                     var dx = root_node.x;
1409                     var dy = root_node.y;
1410     
1411                     if (mouse_position == null) {
1412                         mouse_position = [0,0];
1413                     }
1414     
1415                     var minX = 0;
1416                     var minY = 0;
1417                     var i;
1418                     var node;
1419                     
1420                     for (i=0;i<new_ms.length;i++) {
1421                         node = new_ms[i];
1422                         node.n.selected = true;
1423                         node.n.changed = true;
1424                         node.n.x -= dx - mouse_position[0];
1425                         node.n.y -= dy - mouse_position[1];
1426                         node.dx = node.n.x - mouse_position[0];
1427                         node.dy = node.n.y - mouse_position[1];
1428                         minX = Math.min(node.n.x-node_width/2-5,minX);
1429                         minY = Math.min(node.n.y-node_height/2-5,minY);
1430                     }
1431                     for (i=0;i<new_ms.length;i++) {
1432                         node = new_ms[i];
1433                         node.n.x -= minX;
1434                         node.n.y -= minY;
1435                         node.dx -= minX;
1436                         node.dy -= minY;
1437                     }
1438                     if (!touchImport) {
1439                         mouse_mode = RED.state.IMPORT_DRAGGING;
1440                     }
1441     
1442                     RED.keyboard.add(/* ESCAPE */ 27,function(){
1443                             RED.keyboard.remove(/* ESCAPE */ 27);
1444                             clearSelection();
1445                             RED.history.pop();
1446                             mouse_mode = 0;
1447                     });
1448                     clearSelection();
1449                     moving_set = new_ms;
1450                 }
1451
1452                 RED.history.push({t:'add',nodes:new_node_ids,links:new_links,workspaces:new_workspaces,dirty:RED.view.dirty()});
1453
1454
1455                 redraw();
1456             }
1457         } catch(error) {
1458             console.log(error.stack);
1459             RED.notify("<strong>Error</strong>: "+error,"error");
1460         }
1461     }
1462
1463     function showExportNodesDialog() {
1464         mouse_mode = RED.state.EXPORT;
1465         var nns = RED.nodes.createExportableNodeSet(moving_set);
1466         $("#dialog-form").html($("script[data-template-name='export-clipboard-dialog']").html());
1467         $("#node-input-export").val(JSON.stringify(nns));
1468         $("#node-input-export").focus(function() {
1469                 var textarea = $(this);
1470                 textarea.select();
1471                 textarea.mouseup(function() {
1472                         textarea.unbind("mouseup");
1473                         return false;
1474                 });
1475         });
1476         $( "#dialog" ).dialog("option","title","Export nodes to clipboard").dialog( "open" );
1477         $("#node-input-export").focus();
1478     }
1479
1480     function showExportNodesLibraryDialog() {
1481         mouse_mode = RED.state.EXPORT;
1482         var nns = RED.nodes.createExportableNodeSet(moving_set);
1483         $("#dialog-form").html($("script[data-template-name='export-library-dialog']").html());
1484         $("#node-input-filename").attr('nodes',JSON.stringify(nns));
1485         $( "#dialog" ).dialog("option","title","Export nodes to library").dialog( "open" );
1486     }
1487
1488     function showImportNodesDialog() {
1489         mouse_mode = RED.state.IMPORT;
1490         $("#dialog-form").html($("script[data-template-name='import-dialog']").html());
1491         $("#node-input-import").val("");
1492         $( "#dialog" ).dialog("option","title","Import nodes").dialog( "open" );
1493     }
1494
1495     function showRenameWorkspaceDialog(id) {
1496         var ws = RED.nodes.workspace(id);
1497         $( "#node-dialog-rename-workspace" ).dialog("option","workspace",ws);
1498
1499         if (workspace_tabs.count() == 1) {
1500             $( "#node-dialog-rename-workspace").next().find(".leftButton")
1501                 .prop('disabled',true)
1502                 .addClass("ui-state-disabled");
1503         } else {
1504             $( "#node-dialog-rename-workspace").next().find(".leftButton")
1505                 .prop('disabled',false)
1506                 .removeClass("ui-state-disabled");
1507         }
1508
1509         $( "#node-input-workspace-name" ).val(ws.label);
1510         $( "#node-dialog-rename-workspace" ).dialog("open");
1511     }
1512
1513     $("#node-dialog-rename-workspace form" ).submit(function(e) { e.preventDefault();});
1514     $( "#node-dialog-rename-workspace" ).dialog({
1515         modal: true,
1516         autoOpen: false,
1517         width: 500,
1518         title: "Rename sheet",
1519         buttons: [
1520             {
1521                 class: 'leftButton',
1522                 text: "Delete",
1523                 click: function() {
1524                     var workspace = $(this).dialog('option','workspace');
1525                     $( this ).dialog( "close" );
1526                     deleteWorkspace(workspace.id);
1527                 }
1528             },
1529             {
1530                 text: "Ok",
1531                 click: function() {
1532                     var workspace = $(this).dialog('option','workspace');
1533                     var label = $( "#node-input-workspace-name" ).val();
1534                     if (workspace.label != label) {
1535                         workspace.label = label;
1536                         var link = $("#workspace-tabs a[href='#"+workspace.id+"']");
1537                         link.attr("title",label);
1538                         link.text(label);
1539                         RED.view.dirty(true);
1540                     }
1541                     $( this ).dialog( "close" );
1542                 }
1543             },
1544             {
1545                 text: "Cancel",
1546                 click: function() {
1547                     $( this ).dialog( "close" );
1548                 }
1549             }
1550         ],
1551         open: function(e) {
1552             RED.keyboard.disable();
1553         },
1554         close: function(e) {
1555             RED.keyboard.enable();
1556         }
1557     });
1558     $( "#node-dialog-delete-workspace" ).dialog({
1559         modal: true,
1560         autoOpen: false,
1561         width: 500,
1562         title: "Confirm delete",
1563         buttons: [
1564             {
1565                 text: "Ok",
1566                 click: function() {
1567                     var workspace = $(this).dialog('option','workspace');
1568                     RED.view.removeWorkspace(workspace);
1569                     var historyEvent = RED.nodes.removeWorkspace(workspace.id);
1570                     historyEvent.t = 'delete';
1571                     historyEvent.dirty = dirty;
1572                     historyEvent.workspaces = [workspace];
1573                     RED.history.push(historyEvent);
1574                     RED.view.dirty(true);
1575                     $( this ).dialog( "close" );
1576                 }
1577             },
1578             {
1579                 text: "Cancel",
1580                 click: function() {
1581                     $( this ).dialog( "close" );
1582                 }
1583             }
1584         ],
1585         open: function(e) {
1586             RED.keyboard.disable();
1587         },
1588         close: function(e) {
1589             RED.keyboard.enable();
1590         }
1591
1592     });
1593
1594     return {
1595         state:function(state) {
1596             if (state == null) {
1597                 return mouse_mode
1598             } else {
1599                 mouse_mode = state;
1600             }
1601         },
1602         addWorkspace: function(ws) {
1603             workspace_tabs.addTab(ws);
1604             workspace_tabs.resize();
1605         },
1606         removeWorkspace: function(ws) {
1607             workspace_tabs.removeTab(ws.id);
1608         },
1609         getWorkspace: function() {
1610             return activeWorkspace;
1611         },
1612         showWorkspace: function(id) {
1613             workspace_tabs.activateTab(id);
1614         },
1615         redraw:redraw,
1616         dirty: function(d) {
1617             if (d == null) {
1618                 return dirty;
1619             } else {
1620                 setDirty(d);
1621             }
1622         },
1623         importNodes: importNodes,
1624         resize: function() {
1625             workspace_tabs.resize();
1626         },
1627         status: function(s) {
1628             showStatus = s;
1629             RED.nodes.eachNode(function(n) { n.dirty = true;});
1630             //TODO: subscribe/unsubscribe here
1631             redraw();
1632         },
1633         
1634         //TODO: should these move to an import/export module?
1635         showImportNodesDialog: showImportNodesDialog,
1636         showExportNodesDialog: showExportNodesDialog,
1637         showExportNodesLibraryDialog: showExportNodesLibraryDialog
1638     };
1639 })();