1/**
2 * Copyright (c) 2006-2012, JGraph Ltd
3 */
4/**
5 * Constructs a new graph editor
6 */
7EditorUi = function(editor, container, lightbox)
8{
9	mxEventSource.call(this);
10
11	this.destroyFunctions = [];
12	this.editor = editor || new Editor();
13	this.container = container || document.body;
14
15	var graph = this.editor.graph;
16	graph.lightbox = lightbox;
17
18	// Overrides graph bounds to include background pages
19	var graphGetGraphBounds = graph.getGraphBounds;
20
21	graph.getGraphBounds = function(img)
22	{
23		var bounds = graphGetGraphBounds.apply(this, arguments);
24		var img = this.backgroundImage;
25
26		if (img != null)
27		{
28			var t = this.view.translate;
29			var s = this.view.scale;
30
31			bounds = mxRectangle.fromRectangle(bounds);
32			bounds.add(new mxRectangle(
33				(t.x + img.x) * s, (t.y + img.y) * s,
34				img.width * s, img.height * s));
35		}
36
37		return bounds;
38	};
39
40	// Faster scrollwheel zoom is possible with CSS transforms
41	if (graph.useCssTransforms)
42	{
43		this.lazyZoomDelay = 0;
44	}
45
46	// Pre-fetches submenu image or replaces with embedded image if supported
47	if (mxClient.IS_SVG)
48	{
49		mxPopupMenu.prototype.submenuImage = 'data:image/gif;base64,R0lGODlhCQAJAIAAAP///zMzMyH5BAEAAAAALAAAAAAJAAkAAAIPhI8WebHsHopSOVgb26AAADs=';
50	}
51	else
52	{
53		new Image().src = mxPopupMenu.prototype.submenuImage;
54	}
55
56	// Pre-fetches connect image
57	if (!mxClient.IS_SVG && mxConnectionHandler.prototype.connectImage != null)
58	{
59		new Image().src = mxConnectionHandler.prototype.connectImage.src;
60	}
61
62	// Disables graph and forced panning in chromeless mode
63	if (this.editor.chromeless && !this.editor.editable)
64	{
65		this.footerHeight = 0;
66		graph.isEnabled = function() { return false; };
67		graph.panningHandler.isForcePanningEvent = function(me)
68		{
69			return !mxEvent.isPopupTrigger(me.getEvent());
70		};
71	}
72
73    // Creates the user interface
74	this.actions = new Actions(this);
75	this.menus = this.createMenus();
76
77	if (!graph.standalone)
78	{
79		// Stores the current style and assigns it to new cells
80		var styles = ['rounded', 'shadow', 'glass', 'dashed', 'dashPattern', 'labelBackgroundColor',
81			'labelBorderColor', 'comic', 'sketch', 'fillWeight', 'hachureGap', 'hachureAngle', 'jiggle',
82			'disableMultiStroke', 'disableMultiStrokeFill', 'fillStyle', 'curveFitting',
83			'simplification', 'sketchStyle', 'pointerEvents'];
84		var connectStyles = ['shape', 'edgeStyle', 'curved', 'rounded', 'elbow', 'jumpStyle', 'jumpSize',
85			'comic', 'sketch', 'fillWeight', 'hachureGap', 'hachureAngle', 'jiggle',
86			'disableMultiStroke', 'disableMultiStrokeFill', 'fillStyle', 'curveFitting',
87			'simplification', 'sketchStyle'];
88		// Styles to be ignored if applyAll is false
89		var ignoredEdgeStyles = ['curved', 'sourcePerimeterSpacing', 'targetPerimeterSpacing',
90			'startArrow', 'startFill', 'startSize', 'endArrow', 'endFill', 'endSize'];
91
92		// Note: Everything that is not in styles is ignored (styles is augmented below)
93		this.setDefaultStyle = function(cell)
94		{
95			try
96			{
97				var state = graph.view.getState(cell);
98
99				if (state != null)
100				{
101					// Ignores default styles
102					var clone = cell.clone();
103					clone.style = ''
104					var defaultStyle = graph.getCellStyle(clone);
105					var values = [];
106					var keys = [];
107
108					for (var key in state.style)
109					{
110						if (defaultStyle[key] != state.style[key])
111						{
112							values.push(state.style[key]);
113							keys.push(key);
114						}
115					}
116
117					// Handles special case for value "none"
118					var cellStyle = graph.getModel().getStyle(state.cell);
119					var tokens = (cellStyle != null) ? cellStyle.split(';') : [];
120
121					for (var i = 0; i < tokens.length; i++)
122					{
123						var tmp = tokens[i];
124				 		var pos = tmp.indexOf('=');
125
126				 		if (pos >= 0)
127				 		{
128				 			var key = tmp.substring(0, pos);
129				 			var value = tmp.substring(pos + 1);
130
131				 			if (defaultStyle[key] != null && value == 'none')
132				 			{
133				 				values.push(value);
134				 				keys.push(key);
135				 			}
136				 		}
137					}
138
139					// Resets current style
140					if (graph.getModel().isEdge(state.cell))
141					{
142						graph.currentEdgeStyle = {};
143					}
144					else
145					{
146						graph.currentVertexStyle = {}
147					}
148
149					this.fireEvent(new mxEventObject('styleChanged', 'keys', keys, 'values', values, 'cells', [state.cell]));
150				}
151			}
152			catch (e)
153			{
154				this.handleError(e);
155			}
156		};
157
158		this.clearDefaultStyle = function()
159		{
160			graph.currentEdgeStyle = mxUtils.clone(graph.defaultEdgeStyle);
161			graph.currentVertexStyle = mxUtils.clone(graph.defaultVertexStyle);
162
163			// Updates UI
164			this.fireEvent(new mxEventObject('styleChanged', 'keys', [], 'values', [], 'cells', []));
165		};
166
167		// Keys that should be ignored if the cell has a value (known: new default for all cells is html=1 so
168	    // for the html key this effecticely only works for edges inserted via the connection handler)
169		var valueStyles = ['fontFamily', 'fontSource', 'fontSize', 'fontColor'];
170
171		for (var i = 0; i < valueStyles.length; i++)
172		{
173			if (mxUtils.indexOf(styles, valueStyles[i]) < 0)
174			{
175				styles.push(valueStyles[i]);
176			}
177		}
178
179		// Keys that always update the current edge style regardless of selection
180		var alwaysEdgeStyles = ['edgeStyle', 'startArrow', 'startFill', 'startSize', 'endArrow',
181			'endFill', 'endSize'];
182
183		// Keys that are ignored together (if one appears all are ignored)
184		var keyGroups = [['startArrow', 'startFill', 'endArrow', 'endFill'],
185						 ['startSize', 'endSize'],
186						 ['sourcePerimeterSpacing', 'targetPerimeterSpacing'],
187		                 ['strokeColor', 'strokeWidth'],
188		                 ['fillColor', 'gradientColor', 'gradientDirection'],
189		                 ['align', 'verticalAlign'],
190		                 ['opacity'],
191		                 ['html']];
192
193		// Adds all keys used above to the styles array
194		for (var i = 0; i < keyGroups.length; i++)
195		{
196			for (var j = 0; j < keyGroups[i].length; j++)
197			{
198				styles.push(keyGroups[i][j]);
199			}
200		}
201
202		for (var i = 0; i < connectStyles.length; i++)
203		{
204			if (mxUtils.indexOf(styles, connectStyles[i]) < 0)
205			{
206				styles.push(connectStyles[i]);
207			}
208		}
209
210		// Implements a global current style for edges and vertices that is applied to new cells
211		var insertHandler = function(cells, asText, model, vertexStyle, edgeStyle, applyAll, recurse)
212		{
213			vertexStyle = (vertexStyle != null) ? vertexStyle : graph.currentVertexStyle;
214			edgeStyle = (edgeStyle != null) ? edgeStyle : graph.currentEdgeStyle;
215			applyAll = (applyAll != null) ? applyAll : true;
216
217			model = (model != null) ? model : graph.getModel();
218
219			if (recurse)
220			{
221				var temp = [];
222
223				for (var i = 0; i < cells.length; i++)
224				{
225					temp = temp.concat(model.getDescendants(cells[i]));
226				}
227
228				cells = temp;
229			}
230
231			model.beginUpdate();
232			try
233			{
234				for (var i = 0; i < cells.length; i++)
235				{
236					var cell = cells[i];
237
238					var appliedStyles;
239
240					if (asText)
241					{
242						// Applies only basic text styles
243						appliedStyles = ['fontSize', 'fontFamily', 'fontColor'];
244					}
245					else
246					{
247						// Removes styles defined in the cell style from the styles to be applied
248						var cellStyle = model.getStyle(cell);
249						var tokens = (cellStyle != null) ? cellStyle.split(';') : [];
250						appliedStyles = styles.slice();
251
252						for (var j = 0; j < tokens.length; j++)
253						{
254							var tmp = tokens[j];
255					 		var pos = tmp.indexOf('=');
256
257					 		if (pos >= 0)
258					 		{
259					 			var key = tmp.substring(0, pos);
260					 			var index = mxUtils.indexOf(appliedStyles, key);
261
262					 			if (index >= 0)
263					 			{
264					 				appliedStyles.splice(index, 1);
265					 			}
266
267					 			// Handles special cases where one defined style ignores other styles
268					 			for (var k = 0; k < keyGroups.length; k++)
269					 			{
270					 				var group = keyGroups[k];
271
272					 				if (mxUtils.indexOf(group, key) >= 0)
273					 				{
274					 					for (var l = 0; l < group.length; l++)
275					 					{
276								 			var index2 = mxUtils.indexOf(appliedStyles, group[l]);
277
278								 			if (index2 >= 0)
279								 			{
280								 				appliedStyles.splice(index2, 1);
281								 			}
282					 					}
283					 				}
284					 			}
285					 		}
286						}
287					}
288
289					// Applies the current style to the cell
290					var edge = model.isEdge(cell);
291					var current = (edge) ? edgeStyle : vertexStyle;
292					var newStyle = model.getStyle(cell);
293
294					for (var j = 0; j < appliedStyles.length; j++)
295					{
296						var key = appliedStyles[j];
297						var styleValue = current[key];
298
299						if (styleValue != null && key != 'edgeStyle' && (key != 'shape' || edge))
300						{
301							// Special case: Connect styles are not applied here but in the connection handler
302							if (!edge || applyAll || mxUtils.indexOf(ignoredEdgeStyles, key) < 0)
303							{
304								newStyle = mxUtils.setStyle(newStyle, key, styleValue);
305							}
306						}
307					}
308
309					if (Editor.simpleLabels)
310					{
311						newStyle = mxUtils.setStyle(mxUtils.setStyle(
312							newStyle, 'html', null), 'whiteSpace', null);
313					}
314
315					model.setStyle(cell, newStyle);
316				}
317			}
318			finally
319			{
320				model.endUpdate();
321			}
322
323			return cells;
324		};
325
326		graph.addListener('cellsInserted', function(sender, evt)
327		{
328			insertHandler(evt.getProperty('cells'), null, null, null, null, true, true);
329		});
330
331		graph.addListener('textInserted', function(sender, evt)
332		{
333			insertHandler(evt.getProperty('cells'), true);
334		});
335
336		this.insertHandler = insertHandler;
337
338		this.createDivs();
339		this.createUi();
340		this.refresh();
341
342		// Disables HTML and text selection
343		var textEditing =  mxUtils.bind(this, function(evt)
344		{
345			if (evt == null)
346			{
347				evt = window.event;
348			}
349
350			return graph.isEditing() || (evt != null && this.isSelectionAllowed(evt));
351		});
352
353		// Disables text selection while not editing and no dialog visible
354		if (this.container == document.body)
355		{
356			this.menubarContainer.onselectstart = textEditing;
357			this.menubarContainer.onmousedown = textEditing;
358			this.toolbarContainer.onselectstart = textEditing;
359			this.toolbarContainer.onmousedown = textEditing;
360			this.diagramContainer.onselectstart = textEditing;
361			this.diagramContainer.onmousedown = textEditing;
362			this.sidebarContainer.onselectstart = textEditing;
363			this.sidebarContainer.onmousedown = textEditing;
364			this.formatContainer.onselectstart = textEditing;
365			this.formatContainer.onmousedown = textEditing;
366			this.footerContainer.onselectstart = textEditing;
367			this.footerContainer.onmousedown = textEditing;
368
369			if (this.tabContainer != null)
370			{
371				// Mouse down is needed for drag and drop
372				this.tabContainer.onselectstart = textEditing;
373			}
374		}
375
376		// And uses built-in context menu while editing
377		if (!this.editor.chromeless || this.editor.editable)
378		{
379			// Allows context menu for links in hints
380			var linkHandler = function(evt)
381			{
382				if (evt != null)
383				{
384					var source = mxEvent.getSource(evt);
385
386					if (source.nodeName == 'A')
387					{
388						while (source != null)
389						{
390							if (source.className == 'geHint')
391							{
392								return true;
393							}
394
395							source = source.parentNode;
396						}
397					}
398				}
399
400				return textEditing(evt);
401			};
402
403			if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9))
404			{
405				mxEvent.addListener(this.diagramContainer, 'contextmenu', linkHandler);
406			}
407			else
408			{
409				// Allows browser context menu outside of diagram and sidebar
410				this.diagramContainer.oncontextmenu = linkHandler;
411			}
412		}
413		else
414		{
415			graph.panningHandler.usePopupTrigger = false;
416		}
417
418		// Contains the main graph instance inside the given panel
419		graph.init(this.diagramContainer);
420
421	    // Improves line wrapping for in-place editor
422	    if (mxClient.IS_SVG && graph.view.getDrawPane() != null)
423	    {
424	        var root = graph.view.getDrawPane().ownerSVGElement;
425
426	        if (root != null)
427	        {
428	            root.style.position = 'absolute';
429	        }
430	    }
431
432		// Creates hover icons
433		this.hoverIcons = this.createHoverIcons();
434
435		// Hides hover icons when cells are moved
436		if (graph.graphHandler != null)
437		{
438			var graphHandlerStart = graph.graphHandler.start;
439
440			graph.graphHandler.start = function()
441			{
442				if (ui.hoverIcons != null)
443				{
444					ui.hoverIcons.reset();
445				}
446
447				graphHandlerStart.apply(this, arguments);
448			};
449		}
450
451		// Adds tooltip when mouse is over scrollbars to show space-drag panning option
452		mxEvent.addListener(this.diagramContainer, 'mousemove', mxUtils.bind(this, function(evt)
453		{
454			var off = mxUtils.getOffset(this.diagramContainer);
455
456			if (mxEvent.getClientX(evt) - off.x - this.diagramContainer.clientWidth > 0 ||
457				mxEvent.getClientY(evt) - off.y - this.diagramContainer.clientHeight > 0)
458			{
459				this.diagramContainer.setAttribute('title', mxResources.get('panTooltip'));
460			}
461			else
462			{
463				this.diagramContainer.removeAttribute('title');
464			}
465		}));
466
467	   	// Escape key hides dialogs, adds space+drag panning
468		var spaceKeyPressed = false;
469
470		// Overrides hovericons to disable while space key is pressed
471		var hoverIconsIsResetEvent = this.hoverIcons.isResetEvent;
472
473		this.hoverIcons.isResetEvent = function(evt, allowShift)
474		{
475			return spaceKeyPressed || hoverIconsIsResetEvent.apply(this, arguments);
476		};
477
478		this.keydownHandler = mxUtils.bind(this, function(evt)
479		{
480			if (evt.which == 32 /* Space */ && !graph.isEditing())
481			{
482				spaceKeyPressed = true;
483				this.hoverIcons.reset();
484				graph.container.style.cursor = 'move';
485
486				// Disables scroll after space keystroke with scrollbars
487				if (!graph.isEditing() && mxEvent.getSource(evt) == graph.container)
488				{
489					mxEvent.consume(evt);
490				}
491			}
492			else if (!mxEvent.isConsumed(evt) && evt.keyCode == 27 /* Escape */)
493			{
494				this.hideDialog(null, true);
495			}
496		});
497
498		mxEvent.addListener(document, 'keydown', this.keydownHandler);
499
500		this.keyupHandler = mxUtils.bind(this, function(evt)
501		{
502			graph.container.style.cursor = '';
503			spaceKeyPressed = false;
504		});
505
506		mxEvent.addListener(document, 'keyup', this.keyupHandler);
507
508	    // Forces panning for middle and right mouse buttons
509		var panningHandlerIsForcePanningEvent = graph.panningHandler.isForcePanningEvent;
510		graph.panningHandler.isForcePanningEvent = function(me)
511		{
512			// Ctrl+left button is reported as right button in FF on Mac
513			return panningHandlerIsForcePanningEvent.apply(this, arguments) ||
514				spaceKeyPressed || (mxEvent.isMouseEvent(me.getEvent()) &&
515				(this.usePopupTrigger || !mxEvent.isPopupTrigger(me.getEvent())) &&
516				((!mxEvent.isControlDown(me.getEvent()) &&
517				mxEvent.isRightMouseButton(me.getEvent())) ||
518				mxEvent.isMiddleMouseButton(me.getEvent())));
519		};
520
521		// Ctrl/Cmd+Enter applies editing value except in Safari where Ctrl+Enter creates
522		// a new line (while Enter creates a new paragraph and Shift+Enter stops)
523		var cellEditorIsStopEditingEvent = graph.cellEditor.isStopEditingEvent;
524		graph.cellEditor.isStopEditingEvent = function(evt)
525		{
526			return cellEditorIsStopEditingEvent.apply(this, arguments) ||
527				(evt.keyCode == 13 && ((!mxClient.IS_SF && mxEvent.isControlDown(evt)) ||
528				(mxClient.IS_MAC && mxEvent.isMetaDown(evt)) ||
529				(mxClient.IS_SF && mxEvent.isShiftDown(evt))));
530		};
531
532		// Adds space+wheel for zoom
533		var graphIsZoomWheelEvent = graph.isZoomWheelEvent;
534
535		graph.isZoomWheelEvent = function()
536		{
537			return spaceKeyPressed || graphIsZoomWheelEvent.apply(this, arguments);
538		};
539
540		// Switches toolbar for text editing
541		var textMode = false;
542		var fontMenu = null;
543		var sizeMenu = null;
544		var nodes = null;
545
546		var updateToolbar = mxUtils.bind(this, function()
547		{
548			if (this.toolbar != null && textMode != graph.cellEditor.isContentEditing())
549			{
550				var node = this.toolbar.container.firstChild;
551				var newNodes = [];
552
553				while (node != null)
554				{
555					var tmp = node.nextSibling;
556
557					if (mxUtils.indexOf(this.toolbar.staticElements, node) < 0)
558					{
559						node.parentNode.removeChild(node);
560						newNodes.push(node);
561					}
562
563					node = tmp;
564				}
565
566				// Saves references to special items
567				var tmp1 = this.toolbar.fontMenu;
568				var tmp2 = this.toolbar.sizeMenu;
569
570				if (nodes == null)
571				{
572					this.toolbar.createTextToolbar();
573				}
574				else
575				{
576					for (var i = 0; i < nodes.length; i++)
577					{
578						this.toolbar.container.appendChild(nodes[i]);
579					}
580
581					// Restores references to special items
582					this.toolbar.fontMenu = fontMenu;
583					this.toolbar.sizeMenu = sizeMenu;
584				}
585
586				textMode = graph.cellEditor.isContentEditing();
587				fontMenu = tmp1;
588				sizeMenu = tmp2;
589				nodes = newNodes;
590			}
591		});
592
593		var ui = this;
594
595		// Overrides cell editor to update toolbar
596		var cellEditorStartEditing = graph.cellEditor.startEditing;
597		graph.cellEditor.startEditing = function()
598		{
599			cellEditorStartEditing.apply(this, arguments);
600			updateToolbar();
601
602			if (graph.cellEditor.isContentEditing())
603			{
604				var updating = false;
605
606				var updateCssHandler = function()
607				{
608					if (!updating)
609					{
610						updating = true;
611
612						window.setTimeout(function()
613						{
614							var node = graph.getSelectedEditingElement();
615
616							if (node != null)
617							{
618								var css = mxUtils.getCurrentStyle(node);
619
620								if (css != null && ui.toolbar != null)
621								{
622									ui.toolbar.setFontName(Graph.stripQuotes(css.fontFamily));
623									ui.toolbar.setFontSize(parseInt(css.fontSize));
624								}
625							}
626
627							updating = false;
628						}, 0);
629					}
630				};
631
632				mxEvent.addListener(graph.cellEditor.textarea, 'input', updateCssHandler)
633				mxEvent.addListener(graph.cellEditor.textarea, 'touchend', updateCssHandler);
634				mxEvent.addListener(graph.cellEditor.textarea, 'mouseup', updateCssHandler);
635				mxEvent.addListener(graph.cellEditor.textarea, 'keyup', updateCssHandler);
636				updateCssHandler();
637			}
638		};
639
640		// Updates toolbar and handles possible errors
641		var cellEditorStopEditing = graph.cellEditor.stopEditing;
642		graph.cellEditor.stopEditing = function(cell, trigger)
643		{
644			try
645			{
646				cellEditorStopEditing.apply(this, arguments);
647				updateToolbar();
648			}
649			catch (e)
650			{
651				ui.handleError(e);
652			}
653		};
654
655	    // Enables scrollbars and sets cursor style for the container
656		graph.container.setAttribute('tabindex', '0');
657	   	graph.container.style.cursor = 'default';
658
659		// Workaround for page scroll if embedded via iframe
660		if (window.self === window.top && graph.container.parentNode != null)
661		{
662			try
663			{
664				graph.container.focus();
665			}
666			catch (e)
667			{
668				// ignores error in old versions of IE
669			}
670		}
671
672	   	// Keeps graph container focused on mouse down
673	   	var graphFireMouseEvent = graph.fireMouseEvent;
674	   	graph.fireMouseEvent = function(evtName, me, sender)
675	   	{
676	   		if (evtName == mxEvent.MOUSE_DOWN)
677	   		{
678	   			this.container.focus();
679	   		}
680
681	   		graphFireMouseEvent.apply(this, arguments);
682	   	};
683
684	   	// Configures automatic expand on mouseover
685		graph.popupMenuHandler.autoExpand = true;
686
687	    // Installs context menu
688		if (this.menus != null)
689		{
690			graph.popupMenuHandler.factoryMethod = mxUtils.bind(this, function(menu, cell, evt)
691			{
692				this.menus.createPopupMenu(menu, cell, evt);
693			});
694		}
695
696		// Hides context menu
697		mxEvent.addGestureListeners(document, mxUtils.bind(this, function(evt)
698		{
699			graph.popupMenuHandler.hideMenu();
700		}));
701
702	    // Create handler for key events
703		this.keyHandler = this.createKeyHandler(editor);
704
705		// Getter for key handler
706		this.getKeyHandler = function()
707		{
708			return keyHandler;
709		};
710
711		graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt)
712		{
713			var cells = [evt.getProperty('cell')];
714
715			if (evt.getProperty('terminalInserted'))
716			{
717				cells.push(evt.getProperty('terminal'));
718
719				window.setTimeout(function()
720				{
721					if (ui.hoverIcons != null)
722					{
723						ui.hoverIcons.update(graph.view.getState(cells[cells.length - 1]));
724					}
725				}, 0);
726			}
727
728			insertHandler(cells);
729		});
730
731		this.addListener('styleChanged', mxUtils.bind(this, function(sender, evt)
732		{
733			// Checks if edges and/or vertices were modified
734			var cells = evt.getProperty('cells');
735			var vertex = false;
736			var edge = false;
737
738			if (cells.length > 0)
739			{
740				for (var i = 0; i < cells.length; i++)
741				{
742					vertex = graph.getModel().isVertex(cells[i]) || vertex;
743					edge = graph.getModel().isEdge(cells[i]) || edge;
744
745					if (edge && vertex)
746					{
747						break;
748					}
749				}
750			}
751			else
752			{
753				vertex = true;
754				edge = true;
755			}
756
757			var keys = evt.getProperty('keys');
758			var values = evt.getProperty('values');
759
760			for (var i = 0; i < keys.length; i++)
761			{
762				var common = mxUtils.indexOf(valueStyles, keys[i]) >= 0;
763
764				// Ignores transparent stroke colors
765				if (keys[i] != 'strokeColor' || values[i] != null && values[i] != 'none')
766				{
767					// Special case: Edge style and shape
768					if (mxUtils.indexOf(connectStyles, keys[i]) >= 0)
769					{
770						if (edge || mxUtils.indexOf(alwaysEdgeStyles, keys[i]) >= 0)
771						{
772							if (values[i] == null)
773							{
774								delete graph.currentEdgeStyle[keys[i]];
775							}
776							else
777							{
778								graph.currentEdgeStyle[keys[i]] = values[i];
779							}
780						}
781						// Uses style for vertex if defined in styles
782						else if (vertex && mxUtils.indexOf(styles, keys[i]) >= 0)
783						{
784							if (values[i] == null)
785							{
786								delete graph.currentVertexStyle[keys[i]];
787							}
788							else
789							{
790								graph.currentVertexStyle[keys[i]] = values[i];
791							}
792						}
793					}
794					else if (mxUtils.indexOf(styles, keys[i]) >= 0)
795					{
796						if (vertex || common)
797						{
798							if (values[i] == null)
799							{
800								delete graph.currentVertexStyle[keys[i]];
801							}
802							else
803							{
804								graph.currentVertexStyle[keys[i]] = values[i];
805							}
806						}
807
808						if (edge || common || mxUtils.indexOf(alwaysEdgeStyles, keys[i]) >= 0)
809						{
810							if (values[i] == null)
811							{
812								delete graph.currentEdgeStyle[keys[i]];
813							}
814							else
815							{
816								graph.currentEdgeStyle[keys[i]] = values[i];
817							}
818						}
819					}
820				}
821			}
822
823			if (this.toolbar != null)
824			{
825				this.toolbar.setFontName(graph.currentVertexStyle['fontFamily'] || Menus.prototype.defaultFont);
826				this.toolbar.setFontSize(graph.currentVertexStyle['fontSize'] || Menus.prototype.defaultFontSize);
827
828				if (this.toolbar.edgeStyleMenu != null)
829				{
830					// Updates toolbar icon for edge style
831					var edgeStyleDiv = this.toolbar.edgeStyleMenu.getElementsByTagName('div')[0];
832
833					if (graph.currentEdgeStyle['edgeStyle'] == 'orthogonalEdgeStyle' && graph.currentEdgeStyle['curved'] == '1')
834					{
835						edgeStyleDiv.className = 'geSprite geSprite-curved';
836					}
837					else if (graph.currentEdgeStyle['edgeStyle'] == 'straight' || graph.currentEdgeStyle['edgeStyle'] == 'none' ||
838							graph.currentEdgeStyle['edgeStyle'] == null)
839					{
840						edgeStyleDiv.className = 'geSprite geSprite-straight';
841					}
842					else if (graph.currentEdgeStyle['edgeStyle'] == 'entityRelationEdgeStyle')
843					{
844						edgeStyleDiv.className = 'geSprite geSprite-entity';
845					}
846					else if (graph.currentEdgeStyle['edgeStyle'] == 'elbowEdgeStyle')
847					{
848						edgeStyleDiv.className = 'geSprite geSprite-' + ((graph.currentEdgeStyle['elbow'] == 'vertical') ?
849							'verticalelbow' : 'horizontalelbow');
850					}
851					else if (graph.currentEdgeStyle['edgeStyle'] == 'isometricEdgeStyle')
852					{
853						edgeStyleDiv.className = 'geSprite geSprite-' + ((graph.currentEdgeStyle['elbow'] == 'vertical') ?
854							'verticalisometric' : 'horizontalisometric');
855					}
856					else
857					{
858						edgeStyleDiv.className = 'geSprite geSprite-orthogonal';
859					}
860				}
861
862				if (this.toolbar.edgeShapeMenu != null)
863				{
864					// Updates icon for edge shape
865					var edgeShapeDiv = this.toolbar.edgeShapeMenu.getElementsByTagName('div')[0];
866
867					if (graph.currentEdgeStyle['shape'] == 'link')
868					{
869						edgeShapeDiv.className = 'geSprite geSprite-linkedge';
870					}
871					else if (graph.currentEdgeStyle['shape'] == 'flexArrow')
872					{
873						edgeShapeDiv.className = 'geSprite geSprite-arrow';
874					}
875					else if (graph.currentEdgeStyle['shape'] == 'arrow')
876					{
877						edgeShapeDiv.className = 'geSprite geSprite-simplearrow';
878					}
879					else
880					{
881						edgeShapeDiv.className = 'geSprite geSprite-connection';
882					}
883				}
884
885				// Updates icon for optinal line start shape
886				if (this.toolbar.lineStartMenu != null)
887				{
888					var lineStartDiv = this.toolbar.lineStartMenu.getElementsByTagName('div')[0];
889
890					lineStartDiv.className = this.getCssClassForMarker('start',
891							graph.currentEdgeStyle['shape'], graph.currentEdgeStyle[mxConstants.STYLE_STARTARROW],
892							mxUtils.getValue(graph.currentEdgeStyle, 'startFill', '1'));
893				}
894
895				// Updates icon for optinal line end shape
896				if (this.toolbar.lineEndMenu != null)
897				{
898					var lineEndDiv = this.toolbar.lineEndMenu.getElementsByTagName('div')[0];
899
900					lineEndDiv.className = this.getCssClassForMarker('end',
901							graph.currentEdgeStyle['shape'], graph.currentEdgeStyle[mxConstants.STYLE_ENDARROW],
902							mxUtils.getValue(graph.currentEdgeStyle, 'endFill', '1'));
903				}
904			}
905		}));
906
907		// Update font size and font family labels
908		if (this.toolbar != null)
909		{
910			var update = mxUtils.bind(this, function()
911			{
912				var ff = graph.currentVertexStyle['fontFamily'] || 'Helvetica';
913				var fs = String(graph.currentVertexStyle['fontSize'] || '12');
914			    	var state = graph.getView().getState(graph.getSelectionCell());
915
916			    	if (state != null)
917			    	{
918			    		ff = state.style[mxConstants.STYLE_FONTFAMILY] || ff;
919			    		fs = state.style[mxConstants.STYLE_FONTSIZE] || fs;
920
921			    		if (ff.length > 10)
922			    		{
923			    			ff = ff.substring(0, 8) + '...';
924			    		}
925			    	}
926
927			    	this.toolbar.setFontName(ff);
928			    	this.toolbar.setFontSize(fs);
929			});
930
931		    graph.getSelectionModel().addListener(mxEvent.CHANGE, update);
932		    graph.getModel().addListener(mxEvent.CHANGE, update);
933		}
934
935		// Makes sure the current layer is visible when cells are added
936		graph.addListener(mxEvent.CELLS_ADDED, function(sender, evt)
937		{
938			var cells = evt.getProperty('cells');
939			var parent = evt.getProperty('parent');
940
941			if (parent != null && graph.getModel().isLayer(parent) &&
942				!graph.isCellVisible(parent) && cells != null &&
943				cells.length > 0)
944			{
945				graph.getModel().setVisible(parent, true);
946			}
947		});
948
949		// Global handler to hide the current menu
950		this.gestureHandler = mxUtils.bind(this, function(evt)
951		{
952			if (this.currentMenu != null && mxEvent.getSource(evt) != this.currentMenu.div)
953			{
954				this.hideCurrentMenu();
955			}
956		});
957
958		mxEvent.addGestureListeners(document, this.gestureHandler);
959
960		// Updates the editor UI after the window has been resized or the orientation changes
961		// Timeout is workaround for old IE versions which have a delay for DOM client sizes.
962		// Should not use delay > 0 to avoid handle multiple repaints during window resize
963		this.resizeHandler = mxUtils.bind(this, function()
964	   	{
965	   		window.setTimeout(mxUtils.bind(this, function()
966	   		{
967	   			if (this.editor.graph != null)
968	   			{
969	   				this.refresh();
970	   			}
971	   		}), 0);
972	   	});
973
974	   	mxEvent.addListener(window, 'resize', this.resizeHandler);
975
976	   	this.orientationChangeHandler = mxUtils.bind(this, function()
977	   	{
978	   		this.refresh();
979	   	});
980
981	   	mxEvent.addListener(window, 'orientationchange', this.orientationChangeHandler);
982
983		// Workaround for bug on iOS see
984		// http://stackoverflow.com/questions/19012135/ios-7-ipad-safari-landscape-innerheight-outerheight-layout-issue
985		if (mxClient.IS_IOS && !window.navigator.standalone)
986		{
987			this.scrollHandler = mxUtils.bind(this, function()
988		   	{
989		   		window.scrollTo(0, 0);
990		   	});
991
992		   	mxEvent.addListener(window, 'scroll', this.scrollHandler);
993		}
994
995		/**
996		 * Sets the initial scrollbar locations after a file was loaded.
997		 */
998		this.editor.addListener('resetGraphView', mxUtils.bind(this, function()
999		{
1000			this.resetScrollbars();
1001		}));
1002
1003		/**
1004		 * Repaints the grid.
1005		 */
1006		this.addListener('gridEnabledChanged', mxUtils.bind(this, function()
1007		{
1008			graph.view.validateBackground();
1009		}));
1010
1011		this.addListener('backgroundColorChanged', mxUtils.bind(this, function()
1012		{
1013			graph.view.validateBackground();
1014		}));
1015
1016		/**
1017		 * Repaints the grid.
1018		 */
1019		graph.addListener('gridSizeChanged', mxUtils.bind(this, function()
1020		{
1021			if (graph.isGridEnabled())
1022			{
1023				graph.view.validateBackground();
1024			}
1025		}));
1026
1027	   	// Resets UI, updates action and menu states
1028	   	this.editor.resetGraph();
1029	}
1030
1031	this.init();
1032
1033	if (!graph.standalone)
1034	{
1035		this.open();
1036	}
1037};
1038
1039// Extends mxEventSource
1040mxUtils.extend(EditorUi, mxEventSource);
1041
1042/**
1043 * Global config that specifies if the compact UI elements should be used.
1044 */
1045EditorUi.compactUi = true;
1046
1047/**
1048 * Specifies the size of the split bar.
1049 */
1050EditorUi.prototype.splitSize = (mxClient.IS_TOUCH || mxClient.IS_POINTER) ? 12 : 8;
1051
1052/**
1053 * Specifies the height of the menubar. Default is 30.
1054 */
1055EditorUi.prototype.menubarHeight = 30;
1056
1057/**
1058 * Specifies the width of the format panel should be enabled. Default is true.
1059 */
1060EditorUi.prototype.formatEnabled = true;
1061
1062/**
1063 * Specifies the width of the format panel. Default is 240.
1064 */
1065EditorUi.prototype.formatWidth = 240;
1066
1067/**
1068 * Specifies the height of the toolbar. Default is 38.
1069 */
1070EditorUi.prototype.toolbarHeight = 38;
1071
1072/**
1073 * Specifies the height of the footer. Default is 28.
1074 */
1075EditorUi.prototype.footerHeight = 28;
1076
1077/**
1078 * Specifies the height of the optional sidebarFooterContainer. Default is 34.
1079 */
1080EditorUi.prototype.sidebarFooterHeight = 34;
1081
1082/**
1083 * Specifies the position of the horizontal split bar. Default is 240 or 118 for
1084 * screen widths <= 640px.
1085 */
1086EditorUi.prototype.hsplitPosition = (screen.width <= 640) ? 118 : ((urlParams['sidebar-entries'] != 'large') ? 212 : 240);
1087
1088/**
1089 * Specifies if animations are allowed in <executeLayout>. Default is true.
1090 */
1091EditorUi.prototype.allowAnimation = true;
1092
1093/**
1094 * Default is 2.
1095 */
1096EditorUi.prototype.lightboxMaxFitScale = 2;
1097
1098/**
1099 * Default is 4.
1100 */
1101EditorUi.prototype.lightboxVerticalDivider = 4;
1102
1103/**
1104 * Specifies if single click on horizontal split should collapse sidebar. Default is false.
1105 */
1106EditorUi.prototype.hsplitClickEnabled = false;
1107
1108/**
1109 * Installs the listeners to update the action states.
1110 */
1111EditorUi.prototype.init = function()
1112{
1113	var graph = this.editor.graph;
1114
1115	if (!graph.standalone)
1116	{
1117		if (urlParams['shape-picker'] != '0')
1118		{
1119			this.installShapePicker();
1120		}
1121
1122		// Hides tooltips and connection points when scrolling
1123		mxEvent.addListener(graph.container, 'scroll', mxUtils.bind(this, function()
1124		{
1125			graph.tooltipHandler.hide();
1126
1127			if (graph.connectionHandler != null && graph.connectionHandler.constraintHandler != null)
1128			{
1129				graph.connectionHandler.constraintHandler.reset();
1130			}
1131		}));
1132
1133		// Hides tooltip on escape
1134		graph.addListener(mxEvent.ESCAPE, mxUtils.bind(this, function()
1135		{
1136			graph.tooltipHandler.hide();
1137			var rb = graph.getRubberband();
1138
1139			if (rb != null)
1140			{
1141				rb.cancel();
1142			}
1143		}));
1144
1145		mxEvent.addListener(graph.container, 'keydown', mxUtils.bind(this, function(evt)
1146		{
1147			this.onKeyDown(evt);
1148		}));
1149
1150		mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, function(evt)
1151		{
1152			this.onKeyPress(evt);
1153		}));
1154
1155		// Updates action states
1156		this.addUndoListener();
1157		this.addBeforeUnloadListener();
1158
1159		graph.getSelectionModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function()
1160		{
1161			this.updateActionStates();
1162		}));
1163
1164		graph.getModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function()
1165		{
1166			this.updateActionStates();
1167		}));
1168
1169		// Changes action states after change of default parent
1170		var graphSetDefaultParent = graph.setDefaultParent;
1171		var ui = this;
1172
1173		this.editor.graph.setDefaultParent = function()
1174		{
1175			graphSetDefaultParent.apply(this, arguments);
1176			ui.updateActionStates();
1177		};
1178
1179		// Hack to make editLink available in vertex handler
1180		graph.editLink = ui.actions.get('editLink').funct;
1181
1182		this.updateActionStates();
1183		this.initClipboard();
1184		this.initCanvas();
1185
1186		if (this.format != null)
1187		{
1188			this.format.init();
1189		}
1190	}
1191};
1192
1193/**
1194 * Returns true if the given event should start editing. This implementation returns true.
1195 */
1196EditorUi.prototype.installShapePicker = function()
1197{
1198	var graph = this.editor.graph;
1199	var ui = this;
1200
1201	// Uses this event to process mouseDown to check the selection state before it is changed
1202	graph.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt)
1203	{
1204		if (evt.getProperty('eventName') == 'mouseDown')
1205		{
1206			ui.hideShapePicker();
1207		}
1208	}));
1209
1210	var hidePicker = mxUtils.bind(this, function()
1211	{
1212		ui.hideShapePicker(true);
1213	});
1214
1215	graph.addListener('wheel', hidePicker);
1216	graph.addListener(mxEvent.ESCAPE, hidePicker);
1217	graph.view.addListener(mxEvent.SCALE, hidePicker);
1218	graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, hidePicker);
1219	graph.getSelectionModel().addListener(mxEvent.CHANGE, hidePicker);
1220	graph.getModel().addListener(mxEvent.CHANGE, hidePicker);
1221
1222	// Counts as popup menu
1223	var popupMenuHandlerIsMenuShowing = graph.popupMenuHandler.isMenuShowing;
1224
1225	graph.popupMenuHandler.isMenuShowing = function()
1226	{
1227		return popupMenuHandlerIsMenuShowing.apply(this, arguments) || ui.shapePicker != null;
1228	};
1229
1230	// Adds dbl click dialog for inserting shapes
1231	var graphDblClick = graph.dblClick;
1232
1233	graph.dblClick = function(evt, cell)
1234	{
1235		if (this.isEnabled())
1236		{
1237			if (cell == null && ui.sidebar != null && !mxEvent.isShiftDown(evt) &&
1238				!graph.isCellLocked(graph.getDefaultParent()))
1239			{
1240				var pt = mxUtils.convertPoint(this.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt));
1241				mxEvent.consume(evt);
1242
1243				// Asynchronous to avoid direct insert after double tap
1244				window.setTimeout(mxUtils.bind(this, function()
1245				{
1246					ui.showShapePicker(pt.x, pt.y);
1247				}), 30);
1248			}
1249			else
1250			{
1251				graphDblClick.apply(this, arguments);
1252			}
1253		}
1254	};
1255
1256	if (this.hoverIcons != null)
1257	{
1258		this.hoverIcons.addListener('reset', hidePicker);
1259		var hoverIconsDrag = this.hoverIcons.drag;
1260
1261		this.hoverIcons.drag = function()
1262		{
1263			ui.hideShapePicker();
1264			hoverIconsDrag.apply(this, arguments);
1265		};
1266
1267		var hoverIconsExecute = this.hoverIcons.execute;
1268
1269		this.hoverIcons.execute = function(state, dir, me)
1270		{
1271			var evt = me.getEvent();
1272
1273			if (!this.graph.isCloneEvent(evt) && !mxEvent.isShiftDown(evt))
1274			{
1275				this.graph.connectVertex(state.cell, dir, this.graph.defaultEdgeLength, evt, null, null, mxUtils.bind(this, function(x, y, execute)
1276				{
1277					var temp = graph.getCompositeParent(state.cell);
1278					var geo = graph.getCellGeometry(temp);
1279					me.consume();
1280
1281					while (temp != null && graph.model.isVertex(temp) && geo != null && geo.relative)
1282					{
1283						cell = temp;
1284						temp = graph.model.getParent(cell)
1285						geo = graph.getCellGeometry(temp);
1286					}
1287
1288					// Asynchronous to avoid direct insert after double tap
1289					window.setTimeout(mxUtils.bind(this, function()
1290					{
1291						ui.showShapePicker(me.getGraphX(), me.getGraphY(), temp, mxUtils.bind(this, function(cell)
1292						{
1293							execute(cell);
1294
1295							if (ui.hoverIcons != null)
1296							{
1297								ui.hoverIcons.update(graph.view.getState(cell));
1298							}
1299						}), dir);
1300					}), 30);
1301				}), mxUtils.bind(this, function(result)
1302				{
1303					this.graph.selectCellsForConnectVertex(result, evt, this);
1304				}));
1305			}
1306			else
1307			{
1308				hoverIconsExecute.apply(this, arguments);
1309			}
1310		};
1311
1312		var thread = null;
1313
1314		this.hoverIcons.addListener('focus', mxUtils.bind(this, function(sender, evt)
1315		{
1316			if (thread != null)
1317			{
1318				window.clearTimeout(thread);
1319			}
1320
1321			thread = window.setTimeout(mxUtils.bind(this, function()
1322			{
1323				var arrow = evt.getProperty('arrow');
1324				var dir = evt.getProperty('direction');
1325				var mouseEvent = evt.getProperty('event');
1326
1327				var rect = arrow.getBoundingClientRect();
1328				var offset = mxUtils.getOffset(graph.container);
1329				var x = graph.container.scrollLeft + rect.x - offset.x;
1330				var y = graph.container.scrollTop + rect.y - offset.y;
1331
1332				var temp = graph.getCompositeParent((this.hoverIcons.currentState != null) ?
1333					this.hoverIcons.currentState.cell : null);
1334				var div = ui.showShapePicker(x, y, temp, mxUtils.bind(this, function(cell)
1335				{
1336					if (cell != null)
1337					{
1338						graph.connectVertex(temp, dir, graph.defaultEdgeLength, mouseEvent, true, true, function(x, y, execute)
1339						{
1340							execute(cell);
1341
1342							if (ui.hoverIcons != null)
1343							{
1344								ui.hoverIcons.update(graph.view.getState(cell));
1345							}
1346						}, function(cells)
1347						{
1348							graph.selectCellsForConnectVertex(cells);
1349						}, mouseEvent, this.hoverIcons);
1350					}
1351				}), dir, true);
1352
1353				this.centerShapePicker(div, rect, x, y, dir);
1354				mxUtils.setOpacity(div, 30);
1355
1356				mxEvent.addListener(div, 'mouseenter', function()
1357				{
1358					mxUtils.setOpacity(div, 100);
1359				});
1360
1361				mxEvent.addListener(div, 'mouseleave', function()
1362				{
1363					ui.hideShapePicker();
1364				});
1365			}), Editor.shapePickerHoverDelay);
1366		}));
1367
1368		this.hoverIcons.addListener('blur', mxUtils.bind(this, function(sender, evt)
1369		{
1370			if (thread != null)
1371			{
1372				window.clearTimeout(thread);
1373			}
1374		}));
1375	}
1376};
1377
1378/**
1379 * Creates a temporary graph instance for rendering off-screen content.
1380 */
1381EditorUi.prototype.centerShapePicker = function(div, rect, x, y, dir)
1382{
1383	if (dir == mxConstants.DIRECTION_EAST || dir == mxConstants.DIRECTION_WEST)
1384	{
1385		div.style.width = '40px';
1386	}
1387
1388	var r2 = div.getBoundingClientRect();
1389
1390	if (dir == mxConstants.DIRECTION_NORTH)
1391	{
1392		x -= r2.width / 2 - 10;
1393		y -= r2.height + 6;
1394	}
1395	else if (dir == mxConstants.DIRECTION_SOUTH)
1396	{
1397		x -= r2.width / 2 - 10;
1398		y += rect.height + 6;
1399	}
1400	else if (dir == mxConstants.DIRECTION_WEST)
1401	{
1402		x -= r2.width + 6;
1403		y -= r2.height / 2 - 10;
1404	}
1405	else if (dir == mxConstants.DIRECTION_EAST)
1406	{
1407		x += rect.width + 6;
1408		y -= r2.height / 2 - 10;
1409	}
1410
1411	div.style.left = x + 'px';
1412	div.style.top = y + 'px';
1413};
1414
1415/**
1416 * Creates a temporary graph instance for rendering off-screen content.
1417 */
1418EditorUi.prototype.showShapePicker = function(x, y, source, callback, direction, hovering)
1419{
1420	var div = this.createShapePicker(x, y, source, callback, direction, mxUtils.bind(this, function()
1421	{
1422		this.hideShapePicker();
1423	}), this.getCellsForShapePicker(source, hovering), hovering);
1424
1425	if (div != null)
1426	{
1427		if (this.hoverIcons != null && !hovering)
1428		{
1429			this.hoverIcons.reset();
1430		}
1431
1432		var graph = this.editor.graph;
1433		graph.popupMenuHandler.hideMenu();
1434		graph.tooltipHandler.hideTooltip();
1435		this.hideCurrentMenu();
1436		this.hideShapePicker();
1437
1438		this.shapePickerCallback = callback;
1439		this.shapePicker = div;
1440	}
1441
1442	return div;
1443};
1444
1445/**
1446 * Creates a temporary graph instance for rendering off-screen content.
1447 */
1448EditorUi.prototype.createShapePicker = function(x, y, source, callback, direction, afterClick, cells, hovering)
1449{
1450	var div = null;
1451
1452	if (cells != null && cells.length > 0)
1453	{
1454		var ui = this;
1455		var graph = this.editor.graph;
1456		div = document.createElement('div');
1457		var sourceState = graph.view.getState(source);
1458		var style = (source != null && (sourceState == null ||
1459			!graph.isTransparentState(sourceState))) ?
1460			graph.copyStyle(source) : null;
1461
1462		// Do not place entry under pointer for touch devices
1463		var w = (cells.length < 6) ? cells.length * 35 : 140;
1464		div.className = 'geToolbarContainer geSidebarContainer';
1465		div.style.cssText = 'position:absolute;left:' + x + 'px;top:' + y +
1466			'px;width:' + w + 'px;border-radius:10px;padding:4px;text-align:center;' +
1467			'box-shadow:0px 0px 3px 1px #d1d1d1;padding: 6px 0 8px 0;' +
1468			'z-index: ' + mxPopupMenu.prototype.zIndex + 1 + ';';
1469
1470		if (!hovering)
1471		{
1472			mxUtils.setPrefixedStyle(div.style, 'transform', 'translate(-22px,-22px)');
1473		}
1474
1475		if (graph.background != null && graph.background != mxConstants.NONE)
1476		{
1477			div.style.backgroundColor = graph.background;
1478		}
1479
1480		graph.container.appendChild(div);
1481
1482		var addCell = mxUtils.bind(this, function(cell)
1483		{
1484			// Wrapper needed to catch events
1485			var node = document.createElement('a');
1486			node.className = 'geItem';
1487			node.style.cssText = 'position:relative;display:inline-block;position:relative;' +
1488				'width:30px;height:30px;cursor:pointer;overflow:hidden;padding:3px 0 0 3px;';
1489			div.appendChild(node);
1490
1491			if (style != null && urlParams['sketch'] != '1')
1492			{
1493				this.sidebar.graph.pasteStyle(style, [cell]);
1494			}
1495			else
1496			{
1497				ui.insertHandler([cell], cell.value != '' && urlParams['sketch'] != '1', this.sidebar.graph.model);
1498			}
1499
1500			this.sidebar.createThumb([cell], 25, 25, node, null, true, false, cell.geometry.width, cell.geometry.height);
1501
1502			mxEvent.addListener(node, 'click', function()
1503			{
1504				var clone = graph.cloneCell(cell);
1505
1506				if (callback != null)
1507				{
1508					callback(clone);
1509				}
1510				else
1511				{
1512					clone.geometry.x = graph.snap(Math.round(x / graph.view.scale) -
1513						graph.view.translate.x - cell.geometry.width / 2);
1514					clone.geometry.y = graph.snap(Math.round(y / graph.view.scale) -
1515						graph.view.translate.y - cell.geometry.height / 2);
1516
1517					graph.model.beginUpdate();
1518					try
1519					{
1520						graph.addCell(clone);
1521					}
1522					finally
1523					{
1524						graph.model.endUpdate();
1525					}
1526
1527					graph.setSelectionCell(clone);
1528					graph.scrollCellToVisible(clone);
1529					graph.startEditingAtCell(clone);
1530
1531					if (ui.hoverIcons != null)
1532					{
1533						ui.hoverIcons.update(graph.view.getState(clone));
1534					}
1535				}
1536
1537				if (afterClick != null)
1538				{
1539					afterClick();
1540				}
1541			});
1542		});
1543
1544		for (var i = 0; i < (hovering ? Math.min(cells.length, 4) : cells.length); i++)
1545		{
1546			addCell(cells[i]);
1547		}
1548
1549		var b = graph.container.scrollTop + graph.container.offsetHeight;
1550		var dy = div.offsetTop + div.clientHeight - b;
1551
1552		if (dy > 0)
1553		{
1554			div.style.top = Math.max(graph.container.scrollTop + 22, y - dy) + 'px';
1555		}
1556
1557		var r = graph.container.scrollLeft + graph.container.offsetWidth;
1558		var dx = div.offsetLeft + div.clientWidth - r;
1559
1560		if (dx > 0)
1561		{
1562			div.style.left = Math.max(graph.container.scrollLeft + 22, x - dx) + 'px';
1563		}
1564	}
1565
1566	return div;
1567};
1568
1569/**
1570 * Creates a temporary graph instance for rendering off-screen content.
1571 */
1572EditorUi.prototype.getCellsForShapePicker = function(cell, hovering)
1573{
1574	var createVertex = mxUtils.bind(this, function(style, w, h, value)
1575	{
1576		return this.editor.graph.createVertex(null, null, value || '', 0, 0, w || 120, h || 60, style, false);
1577	});
1578
1579	return [(cell != null) ? this.editor.graph.cloneCell(cell) :
1580			createVertex('text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;', 40, 20, 'Text'),
1581		createVertex('whiteSpace=wrap;html=1;'),
1582		createVertex('ellipse;whiteSpace=wrap;html=1;'),
1583		createVertex('rhombus;whiteSpace=wrap;html=1;', 80, 80),
1584		createVertex('rounded=1;whiteSpace=wrap;html=1;'),
1585		createVertex('shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;'),
1586		createVertex('shape=trapezoid;perimeter=trapezoidPerimeter;whiteSpace=wrap;html=1;fixedSize=1;', 120, 60),
1587		createVertex('shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;', 120, 80),
1588		createVertex('shape=step;perimeter=stepPerimeter;whiteSpace=wrap;html=1;fixedSize=1;', 120, 80),
1589		createVertex('shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;'),
1590		createVertex('triangle;whiteSpace=wrap;html=1;', 60, 80),
1591		createVertex('shape=document;whiteSpace=wrap;html=1;boundedLbl=1;', 120, 80),
1592		createVertex('shape=tape;whiteSpace=wrap;html=1;', 120, 100),
1593		createVertex('ellipse;shape=cloud;whiteSpace=wrap;html=1;', 120, 80),
1594		createVertex('shape=singleArrow;whiteSpace=wrap;html=1;arrowWidth=0.4;arrowSize=0.4;', 80, 60),
1595		createVertex('shape=waypoint;sketch=0;size=6;pointerEvents=1;points=[];fillColor=none;resizable=0;rotatable=0;perimeter=centerPerimeter;snapToPoint=1;', 40, 40)];
1596};
1597
1598/**
1599 * Creates a temporary graph instance for rendering off-screen content.
1600 */
1601EditorUi.prototype.hideShapePicker = function(cancel)
1602{
1603	if (this.shapePicker != null)
1604	{
1605		this.shapePicker.parentNode.removeChild(this.shapePicker);
1606		this.shapePicker = null;
1607
1608		if (!cancel && this.shapePickerCallback != null)
1609		{
1610			this.shapePickerCallback();
1611		}
1612
1613		this.shapePickerCallback = null;
1614	}
1615};
1616
1617/**
1618 * Returns true if the given event should start editing. This implementation returns true.
1619 */
1620EditorUi.prototype.onKeyDown = function(evt)
1621{
1622	var graph = this.editor.graph;
1623
1624	// Alt+tab for task switcher in Windows, ctrl+tab for tab control in Chrome
1625	if (evt.which == 9 && graph.isEnabled() && !mxEvent.isControlDown(evt))
1626	{
1627		if (graph.isEditing())
1628		{
1629			if (mxEvent.isAltDown(evt))
1630			{
1631				graph.stopEditing(false);
1632			}
1633			else
1634			{
1635				try
1636				{
1637					if (graph.cellEditor.isContentEditing() && graph.cellEditor.isTextSelected())
1638					{
1639						// (Shift+)tab indents/outdents with text selection
1640						document.execCommand(mxEvent.isShiftDown(evt) ? 'outdent' : 'indent', false, null);
1641					}
1642					// Shift+tab applies value with cursor
1643					else if (mxEvent.isShiftDown(evt))
1644					{
1645						graph.stopEditing(false);
1646					}
1647					else
1648					{
1649						// Inserts tab character
1650						graph.cellEditor.insertTab(!graph.cellEditor.isContentEditing() ? 4 : null);
1651					}
1652				}
1653				catch (e)
1654				{
1655					// ignore
1656				}
1657			}
1658		}
1659		else if (mxEvent.isAltDown(evt))
1660		{
1661			graph.selectParentCell();
1662		}
1663		else
1664		{
1665			graph.selectCell(!mxEvent.isShiftDown(evt));
1666		}
1667
1668		mxEvent.consume(evt);
1669	}
1670};
1671
1672/**
1673 * Returns true if the given event should start editing. This implementation returns true.
1674 */
1675EditorUi.prototype.onKeyPress = function(evt)
1676{
1677	var graph = this.editor.graph;
1678
1679	// KNOWN: Focus does not work if label is empty in quirks mode
1680	if (this.isImmediateEditingEvent(evt) && !graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 &&
1681		evt.which !== 27 && !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt))
1682	{
1683		graph.escape();
1684		graph.startEditing();
1685
1686		// Workaround for FF where char is lost if cursor is placed before char
1687		if (mxClient.IS_FF)
1688		{
1689			var ce = graph.cellEditor;
1690
1691			if (ce.textarea != null)
1692			{
1693				ce.textarea.innerHTML = String.fromCharCode(evt.which);
1694
1695				// Moves cursor to end of textarea
1696				var range = document.createRange();
1697				range.selectNodeContents(ce.textarea);
1698				range.collapse(false);
1699				var sel = window.getSelection();
1700				sel.removeAllRanges();
1701				sel.addRange(range);
1702			}
1703		}
1704	}
1705};
1706
1707/**
1708 * Returns true if the given event should start editing. This implementation returns true.
1709 */
1710EditorUi.prototype.isImmediateEditingEvent = function(evt)
1711{
1712	return true;
1713};
1714
1715/**
1716 * Private helper method.
1717 */
1718EditorUi.prototype.getCssClassForMarker = function(prefix, shape, marker, fill)
1719{
1720	var result = '';
1721
1722	if (shape == 'flexArrow')
1723	{
1724		result = (marker != null && marker != mxConstants.NONE) ?
1725			'geSprite geSprite-' + prefix + 'blocktrans' : 'geSprite geSprite-noarrow';
1726	}
1727	else
1728	{
1729		// SVG marker sprites
1730		if (marker == 'box' || marker == 'halfCircle')
1731		{
1732			result = 'geSprite geSvgSprite geSprite-' + marker + ((prefix == 'end') ? ' geFlipSprite' : '');
1733		}
1734		else if (marker == mxConstants.ARROW_CLASSIC)
1735		{
1736			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'classic' : 'geSprite geSprite-' + prefix + 'classictrans';
1737		}
1738		else if (marker == mxConstants.ARROW_CLASSIC_THIN)
1739		{
1740			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'classicthin' : 'geSprite geSprite-' + prefix + 'classicthintrans';
1741		}
1742		else if (marker == mxConstants.ARROW_OPEN)
1743		{
1744			result = 'geSprite geSprite-' + prefix + 'open';
1745		}
1746		else if (marker == mxConstants.ARROW_OPEN_THIN)
1747		{
1748			result = 'geSprite geSprite-' + prefix + 'openthin';
1749		}
1750		else if (marker == mxConstants.ARROW_BLOCK)
1751		{
1752			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'block' : 'geSprite geSprite-' + prefix + 'blocktrans';
1753		}
1754		else if (marker == mxConstants.ARROW_BLOCK_THIN)
1755		{
1756			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'blockthin' : 'geSprite geSprite-' + prefix + 'blockthintrans';
1757		}
1758		else if (marker == mxConstants.ARROW_OVAL)
1759		{
1760			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'oval' : 'geSprite geSprite-' + prefix + 'ovaltrans';
1761		}
1762		else if (marker == mxConstants.ARROW_DIAMOND)
1763		{
1764			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'diamond' : 'geSprite geSprite-' + prefix + 'diamondtrans';
1765		}
1766		else if (marker == mxConstants.ARROW_DIAMOND_THIN)
1767		{
1768			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'thindiamond' : 'geSprite geSprite-' + prefix + 'thindiamondtrans';
1769		}
1770		else if (marker == 'openAsync')
1771		{
1772			result = 'geSprite geSprite-' + prefix + 'openasync';
1773		}
1774		else if (marker == 'dash')
1775		{
1776			result = 'geSprite geSprite-' + prefix + 'dash';
1777		}
1778		else if (marker == 'cross')
1779		{
1780			result = 'geSprite geSprite-' + prefix + 'cross';
1781		}
1782		else if (marker == 'async')
1783		{
1784			result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'async' : 'geSprite geSprite-' + prefix + 'asynctrans';
1785		}
1786		else if (marker == 'circle' || marker == 'circlePlus')
1787		{
1788			result = (fill == '1' || marker == 'circle') ? 'geSprite geSprite-' + prefix + 'circle' : 'geSprite geSprite-' + prefix + 'circleplus';
1789		}
1790		else if (marker == 'ERone')
1791		{
1792			result = 'geSprite geSprite-' + prefix + 'erone';
1793		}
1794		else if (marker == 'ERmandOne')
1795		{
1796			result = 'geSprite geSprite-' + prefix + 'eronetoone';
1797		}
1798		else if (marker == 'ERmany')
1799		{
1800			result = 'geSprite geSprite-' + prefix + 'ermany';
1801		}
1802		else if (marker == 'ERoneToMany')
1803		{
1804			result = 'geSprite geSprite-' + prefix + 'eronetomany';
1805		}
1806		else if (marker == 'ERzeroToOne')
1807		{
1808			result = 'geSprite geSprite-' + prefix + 'eroneopt';
1809		}
1810		else if (marker == 'ERzeroToMany')
1811		{
1812			result = 'geSprite geSprite-' + prefix + 'ermanyopt';
1813		}
1814		else
1815		{
1816			result = 'geSprite geSprite-noarrow';
1817		}
1818	}
1819
1820	return result;
1821};
1822
1823/**
1824 * Overridden in Menus.js
1825 */
1826EditorUi.prototype.createMenus = function()
1827{
1828	return null;
1829};
1830
1831/**
1832 * Hook for allowing selection and context menu for certain events.
1833 */
1834EditorUi.prototype.updatePasteActionStates = function()
1835{
1836	var graph = this.editor.graph;
1837	var paste = this.actions.get('paste');
1838	var pasteHere = this.actions.get('pasteHere');
1839
1840	paste.setEnabled(this.editor.graph.cellEditor.isContentEditing() ||
1841		(((!mxClient.IS_FF && navigator.clipboard != null) || !mxClipboard.isEmpty()) &&
1842		graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())));
1843	pasteHere.setEnabled(paste.isEnabled());
1844};
1845
1846/**
1847 * Hook for allowing selection and context menu for certain events.
1848 */
1849EditorUi.prototype.initClipboard = function()
1850{
1851	var ui = this;
1852
1853	var mxClipboardCut = mxClipboard.cut;
1854	mxClipboard.cut = function(graph)
1855	{
1856		if (graph.cellEditor.isContentEditing())
1857		{
1858			document.execCommand('cut', false, null);
1859		}
1860		else
1861		{
1862			mxClipboardCut.apply(this, arguments);
1863		}
1864
1865		ui.updatePasteActionStates();
1866	};
1867
1868	var mxClipboardCopy = mxClipboard.copy;
1869	mxClipboard.copy = function(graph)
1870	{
1871		var result = null;
1872
1873		if (graph.cellEditor.isContentEditing())
1874		{
1875			document.execCommand('copy', false, null);
1876		}
1877		else
1878		{
1879			result = result || graph.getSelectionCells();
1880			result = graph.getExportableCells(graph.model.getTopmostCells(result));
1881
1882			var cloneMap = new Object();
1883			var lookup = graph.createCellLookup(result);
1884			var clones = graph.cloneCells(result, null, cloneMap);
1885
1886			// Uses temporary model to force new IDs to be assigned
1887			// to avoid having to carry over the mapping from object
1888			// ID to cell ID to the paste operation
1889			var model = new mxGraphModel();
1890			var parent = model.getChildAt(model.getRoot(), 0);
1891
1892			for (var i = 0; i < clones.length; i++)
1893			{
1894				model.add(parent, clones[i]);
1895
1896				// Checks for orphaned relative children and makes absolute
1897				var state = graph.view.getState(result[i]);
1898
1899				if (state != null)
1900				{
1901					var geo = graph.getCellGeometry(clones[i]);
1902
1903					if (geo != null && geo.relative && !model.isEdge(result[i]) &&
1904						lookup[mxObjectIdentity.get(model.getParent(result[i]))] == null)
1905					{
1906						geo.offset = null;
1907						geo.relative = false;
1908						geo.x = state.x / state.view.scale - state.view.translate.x;
1909						geo.y = state.y / state.view.scale - state.view.translate.y;
1910					}
1911				}
1912			}
1913
1914			graph.updateCustomLinks(graph.createCellMapping(cloneMap, lookup), clones);
1915
1916			mxClipboard.insertCount = 1;
1917			mxClipboard.setCells(clones);
1918		}
1919
1920		ui.updatePasteActionStates();
1921
1922		return result;
1923	};
1924
1925	var mxClipboardPaste = mxClipboard.paste;
1926	mxClipboard.paste = function(graph)
1927	{
1928		var result = null;
1929
1930		if (graph.cellEditor.isContentEditing())
1931		{
1932			document.execCommand('paste', false, null);
1933		}
1934		else
1935		{
1936			result = mxClipboardPaste.apply(this, arguments);
1937		}
1938
1939		ui.updatePasteActionStates();
1940
1941		return result;
1942	};
1943
1944	// Overrides cell editor to update paste action state
1945	var cellEditorStartEditing = this.editor.graph.cellEditor.startEditing;
1946
1947	this.editor.graph.cellEditor.startEditing = function()
1948	{
1949		cellEditorStartEditing.apply(this, arguments);
1950		ui.updatePasteActionStates();
1951	};
1952
1953	var cellEditorStopEditing = this.editor.graph.cellEditor.stopEditing;
1954
1955	this.editor.graph.cellEditor.stopEditing = function(cell, trigger)
1956	{
1957		cellEditorStopEditing.apply(this, arguments);
1958		ui.updatePasteActionStates();
1959	};
1960
1961	this.updatePasteActionStates();
1962};
1963
1964/**
1965 * Delay between zoom steps when not using preview.
1966 */
1967EditorUi.prototype.lazyZoomDelay = 20;
1968
1969/**
1970 * Delay before update of DOM when using preview.
1971 */
1972EditorUi.prototype.wheelZoomDelay = 400;
1973
1974/**
1975 * Delay before update of DOM when using preview.
1976 */
1977EditorUi.prototype.buttonZoomDelay = 600;
1978
1979/**
1980 * Initializes the infinite canvas.
1981 */
1982EditorUi.prototype.initCanvas = function()
1983{
1984	// Initial page layout view, scrollBuffer and timer-based scrolling
1985	var graph = this.editor.graph;
1986	graph.timerAutoScroll = true;
1987
1988	/**
1989	 * Returns the padding for pages in page view with scrollbars.
1990	 */
1991	graph.getPagePadding = function()
1992	{
1993		return new mxPoint(Math.max(0, Math.round((graph.container.offsetWidth - 34) / graph.view.scale)),
1994				Math.max(0, Math.round((graph.container.offsetHeight - 34) / graph.view.scale)));
1995	};
1996
1997	// Fits the number of background pages to the graph
1998	graph.view.getBackgroundPageBounds = function()
1999	{
2000		var layout = this.graph.getPageLayout();
2001		var page = this.graph.getPageSize();
2002
2003		return new mxRectangle(this.scale * (this.translate.x + layout.x * page.width),
2004				this.scale * (this.translate.y + layout.y * page.height),
2005				this.scale * layout.width * page.width,
2006				this.scale * layout.height * page.height);
2007	};
2008
2009	graph.getPreferredPageSize = function(bounds, width, height)
2010	{
2011		var pages = this.getPageLayout();
2012		var size = this.getPageSize();
2013
2014		return new mxRectangle(0, 0, pages.width * size.width, pages.height * size.height);
2015	};
2016
2017	// Scales pages/graph to fit available size
2018	var resize = null;
2019	var ui = this;
2020
2021	if (this.editor.isChromelessView())
2022	{
2023        resize = mxUtils.bind(this, function(autoscale, maxScale, cx, cy)
2024        {
2025            if (graph.container != null && !graph.isViewer())
2026            {
2027                cx = (cx != null) ? cx : 0;
2028                cy = (cy != null) ? cy : 0;
2029
2030                var bds = (graph.pageVisible) ? graph.view.getBackgroundPageBounds() : graph.getGraphBounds();
2031                var scroll = mxUtils.hasScrollbars(graph.container);
2032                var tr = graph.view.translate;
2033                var s = graph.view.scale;
2034
2035                // Normalizes the bounds
2036                var b = mxRectangle.fromRectangle(bds);
2037                b.x = b.x / s - tr.x;
2038                b.y = b.y / s - tr.y;
2039                b.width /= s;
2040                b.height /= s;
2041
2042                var st = graph.container.scrollTop;
2043                var sl = graph.container.scrollLeft;
2044                var sb = (document.documentMode >= 8) ? 20 : 14;
2045
2046                if (document.documentMode == 8 || document.documentMode == 9)
2047                {
2048                    sb += 3;
2049                }
2050
2051                var cw = graph.container.offsetWidth - sb;
2052                var ch = graph.container.offsetHeight - sb;
2053
2054                var ns = (autoscale) ? Math.max(0.3, Math.min(maxScale || 1, cw / b.width)) : s;
2055                var dx = ((cw - ns * b.width) / 2) / ns;
2056                var dy = (this.lightboxVerticalDivider == 0) ? 0 : ((ch - ns * b.height) / this.lightboxVerticalDivider) / ns;
2057
2058                if (scroll)
2059                {
2060                    dx = Math.max(dx, 0);
2061                    dy = Math.max(dy, 0);
2062                }
2063
2064                if (scroll || bds.width < cw || bds.height < ch)
2065                {
2066                    graph.view.scaleAndTranslate(ns, Math.floor(dx - b.x), Math.floor(dy - b.y));
2067                    graph.container.scrollTop = st * ns / s;
2068                    graph.container.scrollLeft = sl * ns / s;
2069                }
2070                else if (cx != 0 || cy != 0)
2071                {
2072                    var t = graph.view.translate;
2073                    graph.view.setTranslate(Math.floor(t.x + cx / s), Math.floor(t.y + cy / s));
2074                }
2075            }
2076        });
2077
2078		// Hack to make function available to subclassers
2079		this.chromelessResize = resize;
2080
2081		// Hook for subclassers for override
2082		this.chromelessWindowResize = mxUtils.bind(this, function()
2083	   	{
2084			this.chromelessResize(false);
2085	   	});
2086
2087		// Removable resize listener
2088		var autoscaleResize = mxUtils.bind(this, function()
2089	   	{
2090			this.chromelessWindowResize(false);
2091	   	});
2092
2093	   	mxEvent.addListener(window, 'resize', autoscaleResize);
2094
2095	   	this.destroyFunctions.push(function()
2096	   	{
2097	   		mxEvent.removeListener(window, 'resize', autoscaleResize);
2098	   	});
2099
2100		this.editor.addListener('resetGraphView', mxUtils.bind(this, function()
2101		{
2102			this.chromelessResize(true);
2103		}));
2104
2105		this.actions.get('zoomIn').funct = mxUtils.bind(this, function(evt)
2106		{
2107			graph.zoomIn();
2108			this.chromelessResize(false);
2109		});
2110		this.actions.get('zoomOut').funct = mxUtils.bind(this, function(evt)
2111		{
2112			graph.zoomOut();
2113			this.chromelessResize(false);
2114		});
2115
2116		// Creates toolbar for viewer - do not use CSS here
2117		// as this may be used in a viewer that has no CSS
2118		if (urlParams['toolbar'] != '0')
2119		{
2120			var toolbarConfig = JSON.parse(decodeURIComponent(urlParams['toolbar-config'] || '{}'));
2121
2122			this.chromelessToolbar = document.createElement('div');
2123			this.chromelessToolbar.style.position = 'fixed';
2124			this.chromelessToolbar.style.overflow = 'hidden';
2125			this.chromelessToolbar.style.boxSizing = 'border-box';
2126			this.chromelessToolbar.style.whiteSpace = 'nowrap';
2127			this.chromelessToolbar.style.padding = '10px 10px 8px 10px';
2128			this.chromelessToolbar.style.left = (graph.isViewer()) ? '0' : '50%';
2129
2130			if (!mxClient.IS_IE && !mxClient.IS_IE11)
2131			{
2132				this.chromelessToolbar.style.backgroundColor = '#000000';
2133			}
2134			else
2135			{
2136				this.chromelessToolbar.style.backgroundColor = '#ffffff';
2137				this.chromelessToolbar.style.border = '3px solid black';
2138			}
2139
2140			mxUtils.setPrefixedStyle(this.chromelessToolbar.style, 'borderRadius', '16px');
2141			mxUtils.setPrefixedStyle(this.chromelessToolbar.style, 'transition', 'opacity 600ms ease-in-out');
2142
2143			var updateChromelessToolbarPosition = mxUtils.bind(this, function()
2144			{
2145				var css = mxUtils.getCurrentStyle(graph.container);
2146
2147				if (graph.isViewer())
2148				{
2149					this.chromelessToolbar.style.top = '0';
2150				}
2151				else
2152				{
2153				 	this.chromelessToolbar.style.bottom = ((css != null) ? parseInt(css['margin-bottom'] || 0) : 0) +
2154				 		((this.tabContainer != null) ? (20 + parseInt(this.tabContainer.style.height)) : 20) + 'px';
2155				}
2156			});
2157
2158			this.editor.addListener('resetGraphView', updateChromelessToolbarPosition);
2159			updateChromelessToolbarPosition();
2160
2161			var btnCount = 0;
2162
2163			var addButton = mxUtils.bind(this, function(fn, imgSrc, tip)
2164			{
2165				btnCount++;
2166
2167				var a = document.createElement('span');
2168				a.style.paddingLeft = '8px';
2169				a.style.paddingRight = '8px';
2170				a.style.cursor = 'pointer';
2171				mxEvent.addListener(a, 'click', fn);
2172
2173				if (tip != null)
2174				{
2175					a.setAttribute('title', tip);
2176				}
2177
2178				var img = document.createElement('img');
2179				img.setAttribute('border', '0');
2180				img.setAttribute('src', imgSrc);
2181				img.style.width = '36px';
2182				img.style.filter = 'invert(100%)';
2183
2184				a.appendChild(img);
2185				this.chromelessToolbar.appendChild(a);
2186
2187				return a;
2188			});
2189
2190			if (toolbarConfig.backBtn != null)
2191			{
2192				addButton(mxUtils.bind(this, function(evt)
2193				{
2194					window.location.href = toolbarConfig.backBtn.url;
2195					mxEvent.consume(evt);
2196				}), Editor.backImage, mxResources.get('back', null, 'Back'));
2197			}
2198
2199			if (this.isPagesEnabled())
2200			{
2201				var prevButton = addButton(mxUtils.bind(this, function(evt)
2202				{
2203					this.actions.get('previousPage').funct();
2204					mxEvent.consume(evt);
2205				}), Editor.previousImage, mxResources.get('previousPage'));
2206
2207				var pageInfo = document.createElement('div');
2208				pageInfo.style.fontFamily = Editor.defaultHtmlFont;
2209				pageInfo.style.display = 'inline-block';
2210				pageInfo.style.verticalAlign = 'top';
2211				pageInfo.style.fontWeight = 'bold';
2212				pageInfo.style.marginTop = '8px';
2213				pageInfo.style.fontSize = '14px';
2214
2215				if (!mxClient.IS_IE && !mxClient.IS_IE11)
2216				{
2217					pageInfo.style.color = '#ffffff';
2218				}
2219				else
2220				{
2221					pageInfo.style.color = '#000000';
2222				}
2223
2224				this.chromelessToolbar.appendChild(pageInfo);
2225
2226				var nextButton = addButton(mxUtils.bind(this, function(evt)
2227				{
2228					this.actions.get('nextPage').funct();
2229					mxEvent.consume(evt);
2230				}), Editor.nextImage, mxResources.get('nextPage'));
2231
2232				var updatePageInfo = mxUtils.bind(this, function()
2233				{
2234					if (this.pages != null && this.pages.length > 1 && this.currentPage != null)
2235					{
2236						pageInfo.innerHTML = '';
2237						mxUtils.write(pageInfo, (mxUtils.indexOf(this.pages, this.currentPage) + 1) + ' / ' + this.pages.length);
2238					}
2239				});
2240
2241				prevButton.style.paddingLeft = '0px';
2242				prevButton.style.paddingRight = '4px';
2243				nextButton.style.paddingLeft = '4px';
2244				nextButton.style.paddingRight = '0px';
2245
2246				var updatePageButtons = mxUtils.bind(this, function()
2247				{
2248					if (this.pages != null && this.pages.length > 1 && this.currentPage != null)
2249					{
2250						nextButton.style.display = '';
2251						prevButton.style.display = '';
2252						pageInfo.style.display = 'inline-block';
2253					}
2254					else
2255					{
2256						nextButton.style.display = 'none';
2257						prevButton.style.display = 'none';
2258						pageInfo.style.display = 'none';
2259					}
2260
2261					updatePageInfo();
2262				});
2263
2264				this.editor.addListener('resetGraphView', updatePageButtons);
2265				this.editor.addListener('pageSelected', updatePageInfo);
2266			}
2267
2268			addButton(mxUtils.bind(this, function(evt)
2269			{
2270				this.actions.get('zoomOut').funct();
2271				mxEvent.consume(evt);
2272			}), Editor.zoomOutImage, mxResources.get('zoomOut') + ' (Alt+Mousewheel)');
2273
2274			addButton(mxUtils.bind(this, function(evt)
2275			{
2276				this.actions.get('zoomIn').funct();
2277				mxEvent.consume(evt);
2278			}), Editor.zoomInImage, mxResources.get('zoomIn') + ' (Alt+Mousewheel)');
2279
2280			addButton(mxUtils.bind(this, function(evt)
2281			{
2282				if (graph.isLightboxView())
2283				{
2284					if (graph.view.scale == 1)
2285					{
2286						this.lightboxFit();
2287					}
2288					else
2289					{
2290						graph.zoomTo(1);
2291					}
2292
2293					this.chromelessResize(false);
2294				}
2295				else
2296				{
2297					this.chromelessResize(true);
2298				}
2299
2300				mxEvent.consume(evt);
2301			}), Editor.zoomFitImage, mxResources.get('fit'));
2302
2303			// Changes toolbar opacity on hover
2304			var fadeThread = null;
2305			var fadeThread2 = null;
2306
2307			var fadeOut = mxUtils.bind(this, function(delay)
2308			{
2309				if (fadeThread != null)
2310				{
2311					window.clearTimeout(fadeThread);
2312					fadeThread = null;
2313				}
2314
2315				if (fadeThread2 != null)
2316				{
2317					window.clearTimeout(fadeThread2);
2318					fadeThread2 = null;
2319				}
2320
2321				fadeThread = window.setTimeout(mxUtils.bind(this, function()
2322				{
2323				 	mxUtils.setOpacity(this.chromelessToolbar, 0);
2324					fadeThread = null;
2325
2326					fadeThread2 = window.setTimeout(mxUtils.bind(this, function()
2327					{
2328						this.chromelessToolbar.style.display = 'none';
2329						fadeThread2 = null;
2330					}), 600);
2331				}), delay || 200);
2332			});
2333
2334			var fadeIn = mxUtils.bind(this, function(opacity)
2335			{
2336				if (fadeThread != null)
2337				{
2338					window.clearTimeout(fadeThread);
2339					fadeThread = null;
2340				}
2341
2342				if (fadeThread2 != null)
2343				{
2344					window.clearTimeout(fadeThread2);
2345					fadeThread2 = null;
2346				}
2347
2348				this.chromelessToolbar.style.display = '';
2349				mxUtils.setOpacity(this.chromelessToolbar, opacity || 30);
2350			});
2351
2352			if (urlParams['layers'] == '1')
2353			{
2354				this.layersDialog = null;
2355
2356				var layersButton = addButton(mxUtils.bind(this, function(evt)
2357				{
2358					if (this.layersDialog != null)
2359					{
2360						this.layersDialog.parentNode.removeChild(this.layersDialog);
2361						this.layersDialog = null;
2362					}
2363					else
2364					{
2365						this.layersDialog = graph.createLayersDialog(null, true);
2366
2367						mxEvent.addListener(this.layersDialog, 'mouseleave', mxUtils.bind(this, function()
2368						{
2369							this.layersDialog.parentNode.removeChild(this.layersDialog);
2370							this.layersDialog = null;
2371						}));
2372
2373						var r = layersButton.getBoundingClientRect();
2374
2375						mxUtils.setPrefixedStyle(this.layersDialog.style, 'borderRadius', '5px');
2376						this.layersDialog.style.position = 'fixed';
2377						this.layersDialog.style.fontFamily = Editor.defaultHtmlFont;
2378						this.layersDialog.style.width = '160px';
2379						this.layersDialog.style.padding = '4px 2px 4px 2px';
2380						this.layersDialog.style.left = r.left + 'px';
2381						this.layersDialog.style.bottom = parseInt(this.chromelessToolbar.style.bottom) +
2382							this.chromelessToolbar.offsetHeight + 4 + 'px';
2383
2384						if (!mxClient.IS_IE && !mxClient.IS_IE11)
2385						{
2386							this.layersDialog.style.backgroundColor = '#000000';
2387							this.layersDialog.style.color = '#ffffff';
2388							mxUtils.setOpacity(this.layersDialog, 80);
2389						}
2390						else
2391						{
2392							this.layersDialog.style.backgroundColor = '#ffffff';
2393							this.layersDialog.style.border = '2px solid black';
2394							this.layersDialog.style.color = '#000000';
2395						}
2396
2397						// Puts the dialog on top of the container z-index
2398						var style = mxUtils.getCurrentStyle(this.editor.graph.container);
2399						this.layersDialog.style.zIndex = style.zIndex;
2400
2401						document.body.appendChild(this.layersDialog);
2402						this.editor.fireEvent(new mxEventObject('layersDialogShown'));
2403					}
2404
2405					mxEvent.consume(evt);
2406				}), Editor.layersImage, mxResources.get('layers'));
2407
2408				// Shows/hides layers button depending on content
2409				var model = graph.getModel();
2410
2411				model.addListener(mxEvent.CHANGE, function()
2412				{
2413					layersButton.style.display = (model.getChildCount(model.root) > 1) ? '' : 'none';
2414				});
2415			}
2416
2417			if (urlParams['openInSameWin'] != '1' || navigator.standalone)
2418			{
2419				this.addChromelessToolbarItems(addButton);
2420			}
2421
2422			if (this.editor.editButtonLink != null || this.editor.editButtonFunc != null)
2423			{
2424				addButton(mxUtils.bind(this, function(evt)
2425				{
2426					if (this.editor.editButtonFunc != null)
2427					{
2428						this.editor.editButtonFunc();
2429					}
2430					else if (this.editor.editButtonLink == '_blank')
2431					{
2432						this.editor.editAsNew(this.getEditBlankXml());
2433					}
2434					else
2435					{
2436						graph.openLink(this.editor.editButtonLink, 'editWindow');
2437					}
2438
2439					mxEvent.consume(evt);
2440				}), Editor.editImage, mxResources.get('edit'));
2441			}
2442
2443			if (this.lightboxToolbarActions != null)
2444			{
2445				for (var i = 0; i < this.lightboxToolbarActions.length; i++)
2446				{
2447					var lbAction = this.lightboxToolbarActions[i];
2448					lbAction.elem = addButton(lbAction.fn, lbAction.icon, lbAction.tooltip);
2449				}
2450			}
2451
2452			if (toolbarConfig.refreshBtn != null)
2453			{
2454				addButton(mxUtils.bind(this, function(evt)
2455				{
2456					if (toolbarConfig.refreshBtn.url)
2457					{
2458						window.location.href = toolbarConfig.refreshBtn.url;
2459					}
2460					else
2461					{
2462						window.location.reload();
2463					}
2464
2465					mxEvent.consume(evt);
2466				}), Editor.refreshImage, mxResources.get('refresh', null, 'Refresh'));
2467			}
2468
2469			if (toolbarConfig.fullscreenBtn != null && window.self !== window.top)
2470			{
2471				addButton(mxUtils.bind(this, function(evt)
2472				{
2473					if (toolbarConfig.fullscreenBtn.url)
2474					{
2475						graph.openLink(toolbarConfig.fullscreenBtn.url);
2476					}
2477					else
2478					{
2479						graph.openLink(window.location.href);
2480					}
2481
2482					mxEvent.consume(evt);
2483				}), Editor.fullscreenImage, mxResources.get('openInNewWindow', null, 'Open in New Window'));
2484			}
2485
2486			if ((toolbarConfig.closeBtn && window.self === window.top) ||
2487				(graph.lightbox && (urlParams['close'] == '1' || this.container != document.body)))
2488			{
2489				addButton(mxUtils.bind(this, function(evt)
2490				{
2491					if (urlParams['close'] == '1' || toolbarConfig.closeBtn)
2492					{
2493						window.close();
2494					}
2495					else
2496					{
2497						this.destroy();
2498						mxEvent.consume(evt);
2499					}
2500				}), Editor.closeImage, mxResources.get('close') + ' (Escape)');
2501			}
2502
2503			// Initial state invisible
2504			this.chromelessToolbar.style.display = 'none';
2505
2506			if (!graph.isViewer())
2507			{
2508				mxUtils.setPrefixedStyle(this.chromelessToolbar.style, 'transform', 'translate(-50%,0)');
2509			}
2510
2511			graph.container.appendChild(this.chromelessToolbar);
2512
2513			mxEvent.addListener(graph.container, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', mxUtils.bind(this, function(evt)
2514			{
2515				if (!mxEvent.isTouchEvent(evt))
2516				{
2517					if (!mxEvent.isShiftDown(evt))
2518					{
2519						fadeIn(30);
2520					}
2521
2522					fadeOut();
2523				}
2524			}));
2525
2526			mxEvent.addListener(this.chromelessToolbar, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', function(evt)
2527			{
2528				mxEvent.consume(evt);
2529			});
2530
2531			mxEvent.addListener(this.chromelessToolbar, 'mouseenter', mxUtils.bind(this, function(evt)
2532			{
2533				graph.tooltipHandler.resetTimer();
2534				graph.tooltipHandler.hideTooltip();
2535
2536				if (!mxEvent.isShiftDown(evt))
2537				{
2538					fadeIn(100);
2539				}
2540				else
2541				{
2542					fadeOut();
2543				}
2544			}));
2545
2546			mxEvent.addListener(this.chromelessToolbar, 'mousemove',  mxUtils.bind(this, function(evt)
2547			{
2548				if (!mxEvent.isShiftDown(evt))
2549				{
2550					fadeIn(100);
2551				}
2552				else
2553				{
2554					fadeOut();
2555				}
2556
2557				mxEvent.consume(evt);
2558			}));
2559
2560			mxEvent.addListener(this.chromelessToolbar, 'mouseleave',  mxUtils.bind(this, function(evt)
2561			{
2562				if (!mxEvent.isTouchEvent(evt))
2563				{
2564					fadeIn(30);
2565				}
2566			}));
2567
2568			// Shows/hides toolbar for touch devices
2569			var tol = graph.getTolerance();
2570
2571			graph.addMouseListener(
2572			{
2573			    startX: 0,
2574			    startY: 0,
2575			    scrollLeft: 0,
2576			    scrollTop: 0,
2577			    mouseDown: function(sender, me)
2578			    {
2579			    	this.startX = me.getGraphX();
2580			    	this.startY = me.getGraphY();
2581				    this.scrollLeft = graph.container.scrollLeft;
2582				    this.scrollTop = graph.container.scrollTop;
2583			    },
2584			    mouseMove: function(sender, me) {},
2585			    mouseUp: function(sender, me)
2586			    {
2587			    	if (mxEvent.isTouchEvent(me.getEvent()))
2588			    	{
2589				    	if ((Math.abs(this.scrollLeft - graph.container.scrollLeft) < tol &&
2590				    		Math.abs(this.scrollTop - graph.container.scrollTop) < tol) &&
2591				    		(Math.abs(this.startX - me.getGraphX()) < tol &&
2592				    		Math.abs(this.startY - me.getGraphY()) < tol))
2593				    	{
2594				    		if (parseFloat(ui.chromelessToolbar.style.opacity || 0) > 0)
2595				    		{
2596				    			fadeOut();
2597				    		}
2598				    		else
2599				    		{
2600				    			fadeIn(30);
2601				    		}
2602						}
2603			    	}
2604			    }
2605			});
2606		} // end if toolbar
2607
2608		// Installs handling of highlight and handling links to relative links and anchors
2609		if (!this.editor.editable)
2610		{
2611			this.addChromelessClickHandler();
2612		}
2613	}
2614	else if (this.editor.extendCanvas)
2615	{
2616		/**
2617		 * Guesses autoTranslate to avoid another repaint (see below).
2618		 * Works if only the scale of the graph changes or if pages
2619		 * are visible and the visible pages do not change.
2620		 */
2621		var graphViewValidate = graph.view.validate;
2622		graph.view.validate = function()
2623		{
2624			if (this.graph.container != null && mxUtils.hasScrollbars(this.graph.container))
2625			{
2626				var pad = this.graph.getPagePadding();
2627				var size = this.graph.getPageSize();
2628
2629				// Updating scrollbars here causes flickering in quirks and is not needed
2630				// if zoom method is always used to set the current scale on the graph.
2631				var tx = this.translate.x;
2632				var ty = this.translate.y;
2633				this.translate.x = pad.x - (this.x0 || 0) * size.width;
2634				this.translate.y = pad.y - (this.y0 || 0) * size.height;
2635			}
2636
2637			graphViewValidate.apply(this, arguments);
2638		};
2639
2640		if (!graph.isViewer())
2641		{
2642			var graphSizeDidChange = graph.sizeDidChange;
2643
2644			graph.sizeDidChange = function()
2645			{
2646				if (this.container != null && mxUtils.hasScrollbars(this.container))
2647				{
2648					var pages = this.getPageLayout();
2649					var pad = this.getPagePadding();
2650					var size = this.getPageSize();
2651
2652					// Updates the minimum graph size
2653					var minw = Math.ceil(2 * pad.x + pages.width * size.width);
2654					var minh = Math.ceil(2 * pad.y + pages.height * size.height);
2655
2656					var min = graph.minimumGraphSize;
2657
2658					// LATER: Fix flicker of scrollbar size in IE quirks mode
2659					// after delayed call in window.resize event handler
2660					if (min == null || min.width != minw || min.height != minh)
2661					{
2662						graph.minimumGraphSize = new mxRectangle(0, 0, minw, minh);
2663					}
2664
2665					// Updates auto-translate to include padding and graph size
2666					var dx = pad.x - pages.x * size.width;
2667					var dy = pad.y - pages.y * size.height;
2668
2669					if (!this.autoTranslate && (this.view.translate.x != dx || this.view.translate.y != dy))
2670					{
2671						this.autoTranslate = true;
2672						this.view.x0 = pages.x;
2673						this.view.y0 = pages.y;
2674
2675						// NOTE: THIS INVOKES THIS METHOD AGAIN. UNFORTUNATELY THERE IS NO WAY AROUND THIS SINCE THE
2676						// BOUNDS ARE KNOWN AFTER THE VALIDATION AND SETTING THE TRANSLATE TRIGGERS A REVALIDATION.
2677						// SHOULD MOVE TRANSLATE/SCALE TO VIEW.
2678						var tx = graph.view.translate.x;
2679						var ty = graph.view.translate.y;
2680						graph.view.setTranslate(dx, dy);
2681
2682						// LATER: Fix rounding errors for small zoom
2683						graph.container.scrollLeft += Math.round((dx - tx) * graph.view.scale);
2684						graph.container.scrollTop += Math.round((dy - ty) * graph.view.scale);
2685
2686						this.autoTranslate = false;
2687
2688						return;
2689					}
2690
2691					graphSizeDidChange.apply(this, arguments);
2692				}
2693				else
2694				{
2695					// Fires event but does not invoke superclass
2696					this.fireEvent(new mxEventObject(mxEvent.SIZE, 'bounds', this.getGraphBounds()));
2697				}
2698			};
2699		}
2700	}
2701
2702	// Accumulates the zoom factor while the rendering is taking place
2703	// so that not the complete sequence of zoom steps must be painted
2704	var bgGroup = graph.view.getBackgroundPane();
2705	var mainGroup = graph.view.getDrawPane();
2706	graph.cumulativeZoomFactor = 1;
2707	var updateZoomTimeout = null;
2708	var cursorPosition = null;
2709	var scrollPosition = null;
2710	var forcedZoom = null;
2711	var filter = null;
2712
2713	var scheduleZoom = function(delay)
2714	{
2715		if (updateZoomTimeout != null)
2716		{
2717			window.clearTimeout(updateZoomTimeout);
2718		}
2719
2720		window.setTimeout(function()
2721		{
2722			if (!graph.isMouseDown || forcedZoom)
2723			{
2724				updateZoomTimeout = window.setTimeout(mxUtils.bind(this, function()
2725		        {
2726		        	if (graph.isFastZoomEnabled())
2727		    		{
2728		            	// Transforms background page
2729		  				if (graph.view.backgroundPageShape != null && graph.view.backgroundPageShape.node != null)
2730		  				{
2731		  					mxUtils.setPrefixedStyle(graph.view.backgroundPageShape.node.style, 'transform-origin', null);
2732		  					mxUtils.setPrefixedStyle(graph.view.backgroundPageShape.node.style, 'transform', null);
2733		  				}
2734
2735		  				// Transforms graph and background image
2736		  				mainGroup.style.transformOrigin = '';
2737		  				bgGroup.style.transformOrigin = '';
2738
2739		  				// Workaround for no reset of transform in Safari
2740		  				if (mxClient.IS_SF)
2741		  				{
2742			  				mainGroup.style.transform = 'scale(1)';
2743			  				bgGroup.style.transform = 'scale(1)';
2744
2745			  				window.setTimeout(function()
2746	  						{
2747			  					mainGroup.style.transform = '';
2748	  							bgGroup.style.transform = '';
2749	  						}, 0)
2750		  				}
2751		  				else
2752		  				{
2753			  				mainGroup.style.transform = '';
2754			  				bgGroup.style.transform = '';
2755		  				}
2756
2757		            	// Shows interactive elements
2758		            	graph.view.getDecoratorPane().style.opacity = '';
2759		            	graph.view.getOverlayPane().style.opacity = '';
2760		    		}
2761
2762		        	var sp = new mxPoint(graph.container.scrollLeft, graph.container.scrollTop);
2763		            var offset = mxUtils.getOffset(graph.container);
2764		        	var prev = graph.view.scale;
2765		            var dx = 0;
2766		            var dy = 0;
2767
2768		            if (cursorPosition != null)
2769		            {
2770		                dx = graph.container.offsetWidth / 2 - cursorPosition.x + offset.x;
2771		                dy = graph.container.offsetHeight / 2 - cursorPosition.y + offset.y;
2772		            }
2773
2774		            graph.zoom(graph.cumulativeZoomFactor);
2775		            var s = graph.view.scale;
2776
2777		            if (s != prev)
2778		            {
2779			            if (scrollPosition != null)
2780			            {
2781			            	dx += sp.x - scrollPosition.x;
2782			            	dy += sp.y - scrollPosition.y;
2783			            }
2784
2785		                if (resize != null)
2786		                {
2787		                	ui.chromelessResize(false, null, dx * (graph.cumulativeZoomFactor - 1),
2788		                		dy * (graph.cumulativeZoomFactor - 1));
2789		                }
2790
2791		                if (mxUtils.hasScrollbars(graph.container) && (dx != 0 || dy != 0))
2792		                {
2793		                    graph.container.scrollLeft -= dx * (graph.cumulativeZoomFactor - 1);
2794		                    graph.container.scrollTop -= dy * (graph.cumulativeZoomFactor - 1);
2795		                }
2796		            }
2797
2798					if (filter != null)
2799					{
2800						mainGroup.setAttribute('filter', filter);
2801					}
2802
2803		            graph.cumulativeZoomFactor = 1;
2804		            updateZoomTimeout = null;
2805		            scrollPosition = null;
2806		            cursorPosition = null;
2807		            forcedZoom = null;
2808		            filter = null;
2809		        }), (delay != null) ? delay : ((graph.isFastZoomEnabled()) ? ui.wheelZoomDelay : ui.lazyZoomDelay));
2810			}
2811		}, 0);
2812	};
2813
2814	var lastZoomEvent = Date.now();
2815
2816	graph.lazyZoom = function(zoomIn, ignoreCursorPosition, delay)
2817	{
2818		// TODO: Fix ignored cursor position if scrollbars are disabled
2819		ignoreCursorPosition = ignoreCursorPosition || !graph.scrollbars;
2820
2821		if (ignoreCursorPosition)
2822		{
2823			cursorPosition = new mxPoint(
2824				graph.container.offsetLeft + graph.container.clientWidth / 2,
2825				graph.container.offsetTop + graph.container.clientHeight / 2);
2826		}
2827
2828		// Ignores events to reduce touchpad and magic mouse zoom speed
2829		if (!mxClient.IS_IOS && Date.now() - lastZoomEvent < 15)
2830		{
2831			return;
2832		}
2833
2834		lastZoomEvent = Date.now();
2835
2836		// Switches to 5% zoom steps below 15%
2837		if (zoomIn)
2838		{
2839			if (this.view.scale * this.cumulativeZoomFactor <= 0.15)
2840			{
2841				this.cumulativeZoomFactor *= (this.view.scale + 0.05) / this.view.scale;
2842			}
2843			else
2844			{
2845				// Uses to 5% zoom steps for better grid rendering in webkit
2846				// and to avoid rounding errors for zoom steps
2847				this.cumulativeZoomFactor *= this.zoomFactor;
2848				this.cumulativeZoomFactor = Math.round(this.view.scale * this.cumulativeZoomFactor * 20) / 20 / this.view.scale;
2849			}
2850		}
2851		else
2852		{
2853			if (this.view.scale * this.cumulativeZoomFactor <= 0.15)
2854			{
2855				this.cumulativeZoomFactor *= (this.view.scale - 0.05) / this.view.scale;
2856			}
2857			else
2858			{
2859				// Uses to 5% zoom steps for better grid rendering in webkit
2860				// and to avoid rounding errors for zoom steps
2861				this.cumulativeZoomFactor /= this.zoomFactor;
2862				this.cumulativeZoomFactor = Math.round(this.view.scale * this.cumulativeZoomFactor * 20) / 20 / this.view.scale;
2863			}
2864		}
2865
2866		this.cumulativeZoomFactor = Math.max(0.05, Math.min(this.view.scale * this.cumulativeZoomFactor, 160)) / this.view.scale;
2867
2868		if (graph.isFastZoomEnabled())
2869		{
2870			if (filter == null && mainGroup.getAttribute('filter') != '')
2871			{
2872				filter = mainGroup.getAttribute('filter');
2873				mainGroup.removeAttribute('filter');
2874			}
2875
2876			scrollPosition = new mxPoint(graph.container.scrollLeft, graph.container.scrollTop);
2877
2878			var cx = (ignoreCursorPosition) ? graph.container.scrollLeft + graph.container.clientWidth / 2 :
2879				cursorPosition.x + graph.container.scrollLeft - graph.container.offsetLeft;
2880			var cy = (ignoreCursorPosition) ? graph.container.scrollTop + graph.container.clientHeight / 2 :
2881				cursorPosition.y + graph.container.scrollTop - graph.container.offsetTop;
2882			mainGroup.style.transformOrigin = cx + 'px ' + cy + 'px';
2883			mainGroup.style.transform = 'scale(' + this.cumulativeZoomFactor + ')';
2884			bgGroup.style.transformOrigin = cx + 'px ' + cy + 'px';
2885			bgGroup.style.transform = 'scale(' + this.cumulativeZoomFactor + ')';
2886
2887			if (graph.view.backgroundPageShape != null && graph.view.backgroundPageShape.node != null)
2888			{
2889				var page = graph.view.backgroundPageShape.node;
2890
2891				mxUtils.setPrefixedStyle(page.style, 'transform-origin',
2892					((ignoreCursorPosition) ? ((graph.container.clientWidth / 2 + graph.container.scrollLeft -
2893						page.offsetLeft) + 'px') : ((cursorPosition.x + graph.container.scrollLeft -
2894						page.offsetLeft - graph.container.offsetLeft) + 'px')) + ' ' +
2895					((ignoreCursorPosition) ? ((graph.container.clientHeight / 2 + graph.container.scrollTop -
2896						page.offsetTop) + 'px') : ((cursorPosition.y + graph.container.scrollTop -
2897						page.offsetTop - graph.container.offsetTop) + 'px')));
2898				mxUtils.setPrefixedStyle(page.style, 'transform',
2899					'scale(' + this.cumulativeZoomFactor + ')');
2900			}
2901
2902			graph.view.getDecoratorPane().style.opacity = '0';
2903			graph.view.getOverlayPane().style.opacity = '0';
2904
2905			if (ui.hoverIcons != null)
2906			{
2907				ui.hoverIcons.reset();
2908			}
2909		}
2910
2911		scheduleZoom(delay);
2912	};
2913
2914	// Holds back repaint until after mouse gestures
2915	mxEvent.addGestureListeners(graph.container, function(evt)
2916	{
2917		if (updateZoomTimeout != null)
2918		{
2919			window.clearTimeout(updateZoomTimeout);
2920		}
2921	}, null, function(evt)
2922	{
2923		if (graph.cumulativeZoomFactor != 1)
2924		{
2925			scheduleZoom(0);
2926		}
2927	});
2928
2929	// Holds back repaint until scroll ends
2930	mxEvent.addListener(graph.container, 'scroll', function(evt)
2931	{
2932		if (updateZoomTimeout != null && !graph.isMouseDown && graph.cumulativeZoomFactor != 1)
2933		{
2934			scheduleZoom(0);
2935		}
2936	});
2937
2938	mxEvent.addMouseWheelListener(mxUtils.bind(this, function(evt, up, force, cx, cy)
2939	{
2940		graph.fireEvent(new mxEventObject('wheel'));
2941
2942		if (this.dialogs == null || this.dialogs.length == 0)
2943		{
2944			// Scrolls with scrollbars turned off
2945			if (!graph.scrollbars && !force && graph.isScrollWheelEvent(evt))
2946            {
2947                var t = graph.view.getTranslate();
2948                var step = 40 / graph.view.scale;
2949
2950                if (!mxEvent.isShiftDown(evt))
2951                {
2952                    graph.view.setTranslate(t.x, t.y + ((up) ? step : -step));
2953                }
2954                else
2955                {
2956                    graph.view.setTranslate(t.x + ((up) ? -step : step), t.y);
2957                }
2958            }
2959			else if (force || graph.isZoomWheelEvent(evt))
2960			{
2961				var source = mxEvent.getSource(evt);
2962
2963				while (source != null)
2964				{
2965					if (source == graph.container)
2966					{
2967						graph.tooltipHandler.hideTooltip();
2968						cursorPosition = (cx != null && cy!= null) ? new mxPoint(cx, cy) :
2969							new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));
2970						forcedZoom = force;
2971						graph.lazyZoom(up);
2972						mxEvent.consume(evt);
2973
2974						return false;
2975					}
2976
2977					source = source.parentNode;
2978				}
2979			}
2980		}
2981	}), graph.container);
2982
2983	// Uses fast zoom for pinch gestures on iOS
2984	graph.panningHandler.zoomGraph = function(evt)
2985	{
2986		graph.cumulativeZoomFactor = evt.scale;
2987		graph.lazyZoom(evt.scale > 0, true);
2988		mxEvent.consume(evt);
2989	};
2990};
2991
2992/**
2993 * Creates a temporary graph instance for rendering off-screen content.
2994 */
2995EditorUi.prototype.addChromelessToolbarItems = function(addButton)
2996{
2997	addButton(mxUtils.bind(this, function(evt)
2998	{
2999		this.actions.get('print').funct();
3000		mxEvent.consume(evt);
3001	}), Editor.printImage, mxResources.get('print'));
3002};
3003
3004/**
3005 * Creates a temporary graph instance for rendering off-screen content.
3006 */
3007EditorUi.prototype.isPagesEnabled = function()
3008{
3009	return this.editor.editable || urlParams['hide-pages'] != '1';
3010};
3011
3012/**
3013 * Creates a temporary graph instance for rendering off-screen content.
3014 */
3015EditorUi.prototype.createTemporaryGraph = function(stylesheet)
3016{
3017	return Graph.createOffscreenGraph(stylesheet);
3018};
3019
3020/**
3021 *
3022 */
3023EditorUi.prototype.addChromelessClickHandler = function()
3024{
3025	var hl = urlParams['highlight'];
3026
3027	// Adds leading # for highlight color code
3028	if (hl != null && hl.length > 0)
3029	{
3030		hl = '#' + hl;
3031	}
3032
3033	this.editor.graph.addClickHandler(hl);
3034};
3035
3036/**
3037 *
3038 */
3039EditorUi.prototype.toggleFormatPanel = function(visible)
3040{
3041	visible = (visible != null) ? visible : this.formatWidth == 0;
3042
3043	if (this.format != null)
3044	{
3045		this.formatWidth = (visible) ? 240 : 0;
3046		this.formatContainer.style.display = (visible) ? '' : 'none';
3047		this.refresh();
3048		this.format.refresh();
3049		this.fireEvent(new mxEventObject('formatWidthChanged'));
3050	}
3051};
3052
3053/**
3054 * Adds support for placeholders in labels.
3055 */
3056EditorUi.prototype.lightboxFit = function(maxHeight)
3057{
3058	if (this.isDiagramEmpty())
3059	{
3060		this.editor.graph.view.setScale(1);
3061	}
3062	else
3063	{
3064		var p = urlParams['border'];
3065		var border = 60;
3066
3067		if (p != null)
3068		{
3069			border = parseInt(p);
3070		}
3071
3072		// LATER: Use initial graph bounds to avoid rounding errors
3073		this.editor.graph.maxFitScale = this.lightboxMaxFitScale;
3074		this.editor.graph.fit(border, null, null, null, null, null, maxHeight);
3075		this.editor.graph.maxFitScale = null;
3076	}
3077};
3078
3079/**
3080 * Translates this point by the given vector.
3081 *
3082 * @param {number} dx X-coordinate of the translation.
3083 * @param {number} dy Y-coordinate of the translation.
3084 */
3085EditorUi.prototype.isDiagramEmpty = function()
3086{
3087	var model = this.editor.graph.getModel();
3088
3089	return model.getChildCount(model.root) == 1 && model.getChildCount(model.getChildAt(model.root, 0)) == 0;
3090};
3091
3092/**
3093 * Hook for allowing selection and context menu for certain events.
3094 */
3095EditorUi.prototype.isSelectionAllowed = function(evt)
3096{
3097	return mxEvent.getSource(evt).nodeName == 'SELECT' || (mxEvent.getSource(evt).nodeName == 'INPUT' &&
3098		mxUtils.isAncestorNode(this.formatContainer, mxEvent.getSource(evt)));
3099};
3100
3101/**
3102 * Installs dialog if browser window is closed without saving
3103 * This must be disabled during save and image export.
3104 */
3105EditorUi.prototype.addBeforeUnloadListener = function()
3106{
3107	// Installs dialog if browser window is closed without saving
3108	// This must be disabled during save and image export
3109	window.onbeforeunload = mxUtils.bind(this, function()
3110	{
3111		if (!this.editor.isChromelessView())
3112		{
3113			return this.onBeforeUnload();
3114		}
3115	});
3116};
3117
3118/**
3119 * Sets the onbeforeunload for the application
3120 */
3121EditorUi.prototype.onBeforeUnload = function()
3122{
3123	if (this.editor.modified)
3124	{
3125		return mxResources.get('allChangesLost');
3126	}
3127};
3128
3129/**
3130 * Opens the current diagram via the window.opener if one exists.
3131 */
3132EditorUi.prototype.open = function()
3133{
3134	// Cross-domain window access is not allowed in FF, so if we
3135	// were opened from another domain then this will fail.
3136	try
3137	{
3138		if (window.opener != null && window.opener.openFile != null)
3139		{
3140			window.opener.openFile.setConsumer(mxUtils.bind(this, function(xml, filename)
3141			{
3142				try
3143				{
3144					var doc = mxUtils.parseXml(xml);
3145					this.editor.setGraphXml(doc.documentElement);
3146					this.editor.setModified(false);
3147					this.editor.undoManager.clear();
3148
3149					if (filename != null)
3150					{
3151						this.editor.setFilename(filename);
3152						this.updateDocumentTitle();
3153					}
3154
3155					return;
3156				}
3157				catch (e)
3158				{
3159					mxUtils.alert(mxResources.get('invalidOrMissingFile') + ': ' + e.message);
3160				}
3161			}));
3162		}
3163	}
3164	catch(e)
3165	{
3166		// ignore
3167	}
3168
3169	// Fires as the last step if no file was loaded
3170	this.editor.graph.view.validate();
3171
3172	// Required only in special cases where an initial file is opened
3173	// and the minimumGraphSize changes and CSS must be updated.
3174	this.editor.graph.sizeDidChange();
3175	this.editor.fireEvent(new mxEventObject('resetGraphView'));
3176};
3177
3178/**
3179 * Shows the given popup menu.
3180 */
3181EditorUi.prototype.showPopupMenu = function(fn, x, y, evt)
3182{
3183	this.editor.graph.popupMenuHandler.hideMenu();
3184
3185	var menu = new mxPopupMenu(fn);
3186	menu.div.className += ' geMenubarMenu';
3187	menu.smartSeparators = true;
3188	menu.showDisabled = true;
3189	menu.autoExpand = true;
3190
3191	// Disables autoexpand and destroys menu when hidden
3192	menu.hideMenu = mxUtils.bind(this, function()
3193	{
3194		mxPopupMenu.prototype.hideMenu.apply(menu, arguments);
3195		menu.destroy();
3196	});
3197
3198	menu.popup(x, y, null, evt);
3199
3200	// Allows hiding by clicking on document
3201	this.setCurrentMenu(menu);
3202};
3203
3204/**
3205 * Sets the current menu and element.
3206 */
3207EditorUi.prototype.setCurrentMenu = function(menu, elt)
3208{
3209	this.currentMenuElt = elt;
3210	this.currentMenu = menu;
3211};
3212
3213/**
3214 * Resets the current menu and element.
3215 */
3216EditorUi.prototype.resetCurrentMenu = function()
3217{
3218	this.currentMenuElt = null;
3219	this.currentMenu = null;
3220};
3221
3222/**
3223 * Hides and destroys the current menu.
3224 */
3225EditorUi.prototype.hideCurrentMenu = function()
3226{
3227	if (this.currentMenu != null)
3228	{
3229		this.currentMenu.hideMenu();
3230		this.resetCurrentMenu();
3231	}
3232};
3233
3234/**
3235 * Updates the document title.
3236 */
3237EditorUi.prototype.updateDocumentTitle = function()
3238{
3239	var title = this.editor.getOrCreateFilename();
3240
3241	if (this.editor.appName != null)
3242	{
3243		title += ' - ' + this.editor.appName;
3244	}
3245
3246	document.title = title;
3247};
3248
3249/**
3250 * Updates the document title.
3251 */
3252EditorUi.prototype.createHoverIcons = function()
3253{
3254	return new HoverIcons(this.editor.graph);
3255};
3256
3257/**
3258 * Returns the URL for a copy of this editor with no state.
3259 */
3260EditorUi.prototype.redo = function()
3261{
3262	try
3263	{
3264		var graph = this.editor.graph;
3265
3266		if (graph.isEditing())
3267		{
3268			document.execCommand('redo', false, null);
3269		}
3270		else
3271		{
3272			this.editor.undoManager.redo();
3273		}
3274	}
3275	catch (e)
3276	{
3277		// ignore all errors
3278	}
3279};
3280
3281/**
3282 * Returns the URL for a copy of this editor with no state.
3283 */
3284EditorUi.prototype.undo = function()
3285{
3286	try
3287	{
3288		var graph = this.editor.graph;
3289
3290		if (graph.isEditing())
3291		{
3292			// Stops editing and executes undo on graph if native undo
3293			// does not affect current editing value
3294			var value = graph.cellEditor.textarea.innerHTML;
3295			document.execCommand('undo', false, null);
3296
3297			if (value == graph.cellEditor.textarea.innerHTML)
3298			{
3299				graph.stopEditing(true);
3300				this.editor.undoManager.undo();
3301			}
3302		}
3303		else
3304		{
3305			this.editor.undoManager.undo();
3306		}
3307	}
3308	catch (e)
3309	{
3310		// ignore all errors
3311	}
3312};
3313
3314/**
3315 * Returns the URL for a copy of this editor with no state.
3316 */
3317EditorUi.prototype.canRedo = function()
3318{
3319	return this.editor.graph.isEditing() || this.editor.undoManager.canRedo();
3320};
3321
3322/**
3323 * Returns the URL for a copy of this editor with no state.
3324 */
3325EditorUi.prototype.canUndo = function()
3326{
3327	return this.editor.graph.isEditing() || this.editor.undoManager.canUndo();
3328};
3329
3330/**
3331 *
3332 */
3333EditorUi.prototype.getEditBlankXml = function()
3334{
3335	return mxUtils.getXml(this.editor.getGraphXml());
3336};
3337
3338/**
3339 * Returns the URL for a copy of this editor with no state.
3340 */
3341EditorUi.prototype.getUrl = function(pathname)
3342{
3343	var href = (pathname != null) ? pathname : window.location.pathname;
3344	var parms = (href.indexOf('?') > 0) ? 1 : 0;
3345
3346	// Removes template URL parameter for new blank diagram
3347	for (var key in urlParams)
3348	{
3349		if (parms == 0)
3350		{
3351			href += '?';
3352		}
3353		else
3354		{
3355			href += '&';
3356		}
3357
3358		href += key + '=' + urlParams[key];
3359		parms++;
3360	}
3361
3362	return href;
3363};
3364
3365/**
3366 * Specifies if the graph has scrollbars.
3367 */
3368EditorUi.prototype.setScrollbars = function(value)
3369{
3370	var graph = this.editor.graph;
3371	var prev = graph.container.style.overflow;
3372	graph.scrollbars = value;
3373	this.editor.updateGraphComponents();
3374
3375	if (prev != graph.container.style.overflow)
3376	{
3377		graph.container.scrollTop = 0;
3378		graph.container.scrollLeft = 0;
3379		graph.view.scaleAndTranslate(1, 0, 0);
3380		this.resetScrollbars();
3381	}
3382
3383	this.fireEvent(new mxEventObject('scrollbarsChanged'));
3384};
3385
3386/**
3387 * Returns true if the graph has scrollbars.
3388 */
3389EditorUi.prototype.hasScrollbars = function()
3390{
3391	return this.editor.graph.scrollbars;
3392};
3393
3394/**
3395 * Resets the state of the scrollbars.
3396 */
3397EditorUi.prototype.resetScrollbars = function()
3398{
3399	var graph = this.editor.graph;
3400
3401	if (!this.editor.extendCanvas)
3402	{
3403		graph.container.scrollTop = 0;
3404		graph.container.scrollLeft = 0;
3405
3406		if (!mxUtils.hasScrollbars(graph.container))
3407		{
3408			graph.view.setTranslate(0, 0);
3409		}
3410	}
3411	else if (!this.editor.isChromelessView())
3412	{
3413		if (mxUtils.hasScrollbars(graph.container))
3414		{
3415			if (graph.pageVisible)
3416			{
3417				var pad = graph.getPagePadding();
3418				graph.container.scrollTop = Math.floor(pad.y - this.editor.initialTopSpacing) - 1;
3419				graph.container.scrollLeft = Math.floor(Math.min(pad.x,
3420					(graph.container.scrollWidth - graph.container.clientWidth) / 2)) - 1;
3421
3422				// Scrolls graph to visible area
3423				var bounds = graph.getGraphBounds();
3424
3425				if (bounds.width > 0 && bounds.height > 0)
3426				{
3427					if (bounds.x > graph.container.scrollLeft + graph.container.clientWidth * 0.9)
3428					{
3429						graph.container.scrollLeft = Math.min(bounds.x + bounds.width - graph.container.clientWidth, bounds.x - 10);
3430					}
3431
3432					if (bounds.y > graph.container.scrollTop + graph.container.clientHeight * 0.9)
3433					{
3434						graph.container.scrollTop = Math.min(bounds.y + bounds.height - graph.container.clientHeight, bounds.y - 10);
3435					}
3436				}
3437			}
3438			else
3439			{
3440				var bounds = graph.getGraphBounds();
3441				var width = Math.max(bounds.width, graph.scrollTileSize.width * graph.view.scale);
3442				var height = Math.max(bounds.height, graph.scrollTileSize.height * graph.view.scale);
3443				graph.container.scrollTop = Math.floor(Math.max(0, bounds.y - Math.max(20, (graph.container.clientHeight - height) / 4)));
3444				graph.container.scrollLeft = Math.floor(Math.max(0, bounds.x - Math.max(0, (graph.container.clientWidth - width) / 2)));
3445			}
3446		}
3447		else
3448		{
3449			var b = mxRectangle.fromRectangle((graph.pageVisible) ? graph.view.getBackgroundPageBounds() : graph.getGraphBounds())
3450			var tr = graph.view.translate;
3451			var s = graph.view.scale;
3452            b.x = b.x / s - tr.x;
3453            b.y = b.y / s - tr.y;
3454            b.width /= s;
3455            b.height /= s;
3456
3457            var dy = (graph.pageVisible) ? 0 : Math.max(0, (graph.container.clientHeight - b.height) / 4);
3458
3459			graph.view.setTranslate(Math.floor(Math.max(0,
3460				(graph.container.clientWidth - b.width) / 2) - b.x + 2),
3461				Math.floor(dy - b.y + 1));
3462		}
3463	}
3464};
3465
3466/**
3467 * Loads the stylesheet for this graph.
3468 */
3469EditorUi.prototype.setPageVisible = function(value)
3470{
3471	var graph = this.editor.graph;
3472	var hasScrollbars = mxUtils.hasScrollbars(graph.container);
3473	var tx = 0;
3474	var ty = 0;
3475
3476	if (hasScrollbars)
3477	{
3478		tx = graph.view.translate.x * graph.view.scale - graph.container.scrollLeft;
3479		ty = graph.view.translate.y * graph.view.scale - graph.container.scrollTop;
3480	}
3481
3482	graph.pageVisible = value;
3483	graph.pageBreaksVisible = value;
3484	graph.preferPageSize = value;
3485	graph.view.validateBackground();
3486
3487	// Workaround for possible handle offset
3488	if (hasScrollbars)
3489	{
3490		var cells = graph.getSelectionCells();
3491		graph.clearSelection();
3492		graph.setSelectionCells(cells);
3493	}
3494
3495	// Calls updatePageBreaks
3496	graph.sizeDidChange();
3497
3498	if (hasScrollbars)
3499	{
3500		graph.container.scrollLeft = graph.view.translate.x * graph.view.scale - tx;
3501		graph.container.scrollTop = graph.view.translate.y * graph.view.scale - ty;
3502	}
3503
3504	graph.defaultPageVisible = value;
3505	this.fireEvent(new mxEventObject('pageViewChanged'));
3506};
3507
3508/**
3509 * Class: ChangeGridColor
3510 *
3511 * Undoable change to grid color.
3512 */
3513function ChangeGridColor(ui, color)
3514{
3515	this.ui = ui;
3516	this.color = color;
3517};
3518
3519/**
3520 * Executes selection of a new page.
3521 */
3522ChangeGridColor.prototype.execute = function()
3523{
3524	var temp = this.ui.editor.graph.view.gridColor;
3525	this.ui.setGridColor(this.color);
3526	this.color = temp;
3527};
3528
3529// Registers codec for ChangePageSetup
3530(function()
3531{
3532	var codec = new mxObjectCodec(new ChangeGridColor(), ['ui']);
3533
3534	mxCodecRegistry.register(codec);
3535})();
3536
3537/**
3538 * Change types
3539 */
3540function ChangePageSetup(ui, color, image, format, pageScale)
3541{
3542	this.ui = ui;
3543	this.color = color;
3544	this.previousColor = color;
3545	this.image = image;
3546	this.previousImage = image;
3547	this.format = format;
3548	this.previousFormat = format;
3549	this.pageScale = pageScale;
3550	this.previousPageScale = pageScale;
3551
3552	// Needed since null are valid values for color and image
3553	this.ignoreColor = false;
3554	this.ignoreImage = false;
3555}
3556
3557/**
3558 * Implementation of the undoable page rename.
3559 */
3560ChangePageSetup.prototype.execute = function()
3561{
3562	var graph = this.ui.editor.graph;
3563
3564	if (!this.ignoreColor)
3565	{
3566		this.color = this.previousColor;
3567		var tmp = graph.background;
3568		this.ui.setBackgroundColor(this.previousColor);
3569		this.previousColor = tmp;
3570	}
3571
3572	if (!this.ignoreImage)
3573	{
3574		this.image = this.previousImage;
3575		var tmp = graph.backgroundImage;
3576		var img = this.previousImage;
3577
3578		if (img != null && img.src != null && img.src.substring(0, 13) == 'data:page/id,')
3579		{
3580			img = this.ui.createImageForPageLink(img.src, this.ui.currentPage);
3581		}
3582
3583		this.ui.setBackgroundImage(img);
3584		this.previousImage = tmp;
3585	}
3586
3587	if (this.previousFormat != null)
3588	{
3589		this.format = this.previousFormat;
3590		var tmp = graph.pageFormat;
3591
3592		if (this.previousFormat.width != tmp.width ||
3593			this.previousFormat.height != tmp.height)
3594		{
3595			this.ui.setPageFormat(this.previousFormat);
3596			this.previousFormat = tmp;
3597		}
3598	}
3599
3600    if (this.foldingEnabled != null && this.foldingEnabled != this.ui.editor.graph.foldingEnabled)
3601    {
3602    	this.ui.setFoldingEnabled(this.foldingEnabled);
3603        this.foldingEnabled = !this.foldingEnabled;
3604    }
3605
3606    if (this.previousPageScale != null)
3607    {
3608	    var currentPageScale = this.ui.editor.graph.pageScale;
3609
3610	    if (this.previousPageScale != currentPageScale)
3611	    {
3612	    	this.ui.setPageScale(this.previousPageScale);
3613	        this.previousPageScale = currentPageScale;
3614	    }
3615    }
3616};
3617
3618// Registers codec for ChangePageSetup
3619(function()
3620{
3621	var codec = new mxObjectCodec(new ChangePageSetup(),  ['ui', 'previousColor', 'previousImage', 'previousFormat', 'previousPageScale']);
3622
3623	codec.afterDecode = function(dec, node, obj)
3624	{
3625		obj.previousColor = obj.color;
3626		obj.previousImage = obj.image;
3627		obj.previousFormat = obj.format;
3628		obj.previousPageScale = obj.pageScale;
3629
3630        if (obj.foldingEnabled != null)
3631        {
3632        	obj.foldingEnabled = !obj.foldingEnabled;
3633        }
3634
3635		return obj;
3636	};
3637
3638	mxCodecRegistry.register(codec);
3639})();
3640
3641/**
3642 * Loads the stylesheet for this graph.
3643 */
3644EditorUi.prototype.setBackgroundColor = function(value)
3645{
3646	this.editor.graph.background = value;
3647	this.editor.graph.view.validateBackground();
3648
3649	this.fireEvent(new mxEventObject('backgroundColorChanged'));
3650};
3651
3652/**
3653 * Loads the stylesheet for this graph.
3654 */
3655EditorUi.prototype.setFoldingEnabled = function(value)
3656{
3657	this.editor.graph.foldingEnabled = value;
3658	this.editor.graph.view.revalidate();
3659
3660	this.fireEvent(new mxEventObject('foldingEnabledChanged'));
3661};
3662
3663/**
3664 * Loads the stylesheet for this graph.
3665 */
3666EditorUi.prototype.setPageFormat = function(value, ignorePageVisible)
3667{
3668	ignorePageVisible = (ignorePageVisible != null) ? ignorePageVisible : urlParams['sketch'] == '1';
3669	this.editor.graph.pageFormat = value;
3670
3671	if (!ignorePageVisible)
3672	{
3673		if (!this.editor.graph.pageVisible)
3674		{
3675			this.actions.get('pageView').funct();
3676		}
3677		else
3678		{
3679			this.editor.graph.view.validateBackground();
3680			this.editor.graph.sizeDidChange();
3681		}
3682	}
3683
3684	this.fireEvent(new mxEventObject('pageFormatChanged'));
3685};
3686
3687/**
3688 * Loads the stylesheet for this graph.
3689 */
3690EditorUi.prototype.setPageScale = function(value)
3691{
3692	this.editor.graph.pageScale = value;
3693
3694	if (!this.editor.graph.pageVisible)
3695	{
3696		this.actions.get('pageView').funct();
3697	}
3698	else
3699	{
3700		this.editor.graph.view.validateBackground();
3701		this.editor.graph.sizeDidChange();
3702	}
3703
3704	this.fireEvent(new mxEventObject('pageScaleChanged'));
3705};
3706
3707/**
3708 * Loads the stylesheet for this graph.
3709 */
3710EditorUi.prototype.setGridColor = function(value)
3711{
3712	this.editor.graph.view.gridColor = value;
3713	this.editor.graph.view.validateBackground();
3714	this.fireEvent(new mxEventObject('gridColorChanged'));
3715};
3716
3717/**
3718 * Updates the states of the given undo/redo items.
3719 */
3720EditorUi.prototype.addUndoListener = function()
3721{
3722	var undo = this.actions.get('undo');
3723	var redo = this.actions.get('redo');
3724
3725	var undoMgr = this.editor.undoManager;
3726
3727    var undoListener = mxUtils.bind(this, function()
3728    {
3729    	undo.setEnabled(this.canUndo());
3730    	redo.setEnabled(this.canRedo());
3731    });
3732
3733    undoMgr.addListener(mxEvent.ADD, undoListener);
3734    undoMgr.addListener(mxEvent.UNDO, undoListener);
3735    undoMgr.addListener(mxEvent.REDO, undoListener);
3736    undoMgr.addListener(mxEvent.CLEAR, undoListener);
3737
3738	// Overrides cell editor to update action states
3739	var cellEditorStartEditing = this.editor.graph.cellEditor.startEditing;
3740
3741	this.editor.graph.cellEditor.startEditing = function()
3742	{
3743		cellEditorStartEditing.apply(this, arguments);
3744		undoListener();
3745	};
3746
3747	var cellEditorStopEditing = this.editor.graph.cellEditor.stopEditing;
3748
3749	this.editor.graph.cellEditor.stopEditing = function(cell, trigger)
3750	{
3751		cellEditorStopEditing.apply(this, arguments);
3752		undoListener();
3753	};
3754
3755	// Updates the button states once
3756    undoListener();
3757};
3758
3759/**
3760* Updates the states of the given toolbar items based on the selection.
3761*/
3762EditorUi.prototype.updateActionStates = function()
3763{
3764	var graph = this.editor.graph;
3765	var vertexSelected = false;
3766	var groupSelected = false;
3767	var edgeSelected = false;
3768	var selected = false;
3769	var editable = [];
3770
3771	var cells = graph.getSelectionCells();
3772
3773	if (cells != null)
3774	{
3775    	for (var i = 0; i < cells.length; i++)
3776    	{
3777    		var cell = cells[i];
3778
3779			if (graph.isCellEditable(cell))
3780			{
3781				editable.push(cell);
3782				selected = true;
3783
3784	    		if (graph.getModel().isEdge(cell))
3785	    		{
3786	    			edgeSelected = true;
3787	    		}
3788
3789	    		if (graph.getModel().isVertex(cell))
3790	    		{
3791	    			vertexSelected = true;
3792
3793		    		if (graph.getModel().getChildCount(cell) > 0 ||
3794		    			graph.isContainer(cell))
3795		    		{
3796		    			groupSelected = true;
3797		    		}
3798	    		}
3799			}
3800		}
3801	}
3802
3803	// Updates action states
3804	var actions = ['cut', 'copy', 'bold', 'italic', 'underline', 'delete', 'duplicate',
3805	               'editStyle', 'editTooltip', 'editLink', 'backgroundColor', 'borderColor',
3806	               'edit', 'toFront', 'toBack', 'solid', 'dashed', 'pasteSize',
3807	               'dotted', 'fillColor', 'gradientColor', 'shadow', 'fontColor',
3808	               'formattedText', 'rounded', 'toggleRounded', 'sharp', 'strokeColor'];
3809
3810	for (var i = 0; i < actions.length; i++)
3811	{
3812		this.actions.get(actions[i]).setEnabled(selected);
3813	}
3814
3815	this.actions.get('lockUnlock').setEnabled(!graph.isSelectionEmpty());
3816	this.actions.get('setAsDefaultStyle').setEnabled(graph.getSelectionCount() == 1);
3817	this.actions.get('clearWaypoints').setEnabled(selected);
3818	this.actions.get('copySize').setEnabled(graph.getSelectionCount() == 1);
3819	this.actions.get('bringForward').setEnabled(editable.length == 1);
3820	this.actions.get('sendBackward').setEnabled(editable.length == 1);
3821	this.actions.get('turn').setEnabled(graph.getResizableCells(graph.getSelectionCells()).length > 0);
3822	this.actions.get('curved').setEnabled(edgeSelected);
3823	this.actions.get('rotation').setEnabled(vertexSelected);
3824	this.actions.get('wordWrap').setEnabled(vertexSelected);
3825	this.actions.get('autosize').setEnabled(vertexSelected);
3826   	var oneVertexSelected = vertexSelected && graph.getSelectionCount() == 1;
3827	this.actions.get('group').setEnabled(graph.getSelectionCount() > 1 ||
3828		(oneVertexSelected && !graph.isContainer(graph.getSelectionCell())));
3829	this.actions.get('ungroup').setEnabled(groupSelected);
3830   	this.actions.get('removeFromGroup').setEnabled(oneVertexSelected &&
3831   		graph.getModel().isVertex(graph.getModel().getParent(editable[0])));
3832
3833	// Updates menu states
3834   	var state = graph.view.getState(graph.getSelectionCell());
3835    this.menus.get('navigation').setEnabled(selected || graph.view.currentRoot != null);
3836    this.actions.get('collapsible').setEnabled(vertexSelected &&
3837    	(graph.isContainer(graph.getSelectionCell()) || graph.model.getChildCount(graph.getSelectionCell()) > 0));
3838    this.actions.get('home').setEnabled(graph.view.currentRoot != null);
3839    this.actions.get('exitGroup').setEnabled(graph.view.currentRoot != null);
3840    this.actions.get('enterGroup').setEnabled(graph.getSelectionCount() == 1 && graph.isValidRoot(graph.getSelectionCell()));
3841    var foldable = graph.getSelectionCount() == 1 && graph.isCellFoldable(graph.getSelectionCell()); // TODO
3842    this.actions.get('expand').setEnabled(foldable);
3843    this.actions.get('collapse').setEnabled(foldable);
3844    this.actions.get('editLink').setEnabled(editable.length == 1);
3845    this.actions.get('openLink').setEnabled(graph.getSelectionCount() == 1 &&
3846    	graph.getLinkForCell(graph.getSelectionCell()) != null);
3847    this.actions.get('guides').setEnabled(graph.isEnabled());
3848    this.actions.get('grid').setEnabled(!this.editor.chromeless || this.editor.editable);
3849
3850    var unlocked = graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent());
3851    this.menus.get('layout').setEnabled(unlocked);
3852    this.menus.get('insert').setEnabled(unlocked);
3853    this.menus.get('direction').setEnabled(unlocked && vertexSelected);
3854    this.menus.get('align').setEnabled(unlocked && vertexSelected && graph.getSelectionCount() > 1);
3855    this.menus.get('distribute').setEnabled(unlocked && vertexSelected && graph.getSelectionCount() > 1);
3856    this.actions.get('selectVertices').setEnabled(unlocked);
3857    this.actions.get('selectEdges').setEnabled(unlocked);
3858    this.actions.get('selectAll').setEnabled(unlocked);
3859    this.actions.get('selectNone').setEnabled(unlocked);
3860
3861    this.updatePasteActionStates();
3862};
3863
3864EditorUi.prototype.zeroOffset = new mxPoint(0, 0);
3865
3866EditorUi.prototype.getDiagramContainerOffset = function()
3867{
3868	return this.zeroOffset;
3869};
3870
3871/**
3872 * Refreshes the viewport.
3873 */
3874EditorUi.prototype.refresh = function(sizeDidChange)
3875{
3876	sizeDidChange = (sizeDidChange != null) ? sizeDidChange : true;
3877
3878	var w = this.container.clientWidth;
3879	var h = this.container.clientHeight;
3880
3881	if (this.container == document.body)
3882	{
3883		w = document.body.clientWidth || document.documentElement.clientWidth;
3884		h = document.documentElement.clientHeight;
3885	}
3886
3887	// Workaround for bug on iOS see
3888	// http://stackoverflow.com/questions/19012135/ios-7-ipad-safari-landscape-innerheight-outerheight-layout-issue
3889	// FIXME: Fix if footer visible
3890	var off = 0;
3891
3892	if (mxClient.IS_IOS && !window.navigator.standalone)
3893	{
3894		if (window.innerHeight != document.documentElement.clientHeight)
3895		{
3896			off = document.documentElement.clientHeight - window.innerHeight;
3897			window.scrollTo(0, 0);
3898		}
3899	}
3900
3901	var effHsplitPosition = Math.max(0, Math.min(this.hsplitPosition, w - this.splitSize - 20));
3902	var tmp = 0;
3903
3904	if (this.menubar != null)
3905	{
3906		this.menubarContainer.style.height = this.menubarHeight + 'px';
3907		tmp += this.menubarHeight;
3908	}
3909
3910	if (this.toolbar != null)
3911	{
3912		this.toolbarContainer.style.top = this.menubarHeight + 'px';
3913		this.toolbarContainer.style.height = this.toolbarHeight + 'px';
3914		tmp += this.toolbarHeight;
3915	}
3916
3917	if (tmp > 0)
3918	{
3919		tmp += 1;
3920	}
3921
3922	var sidebarFooterHeight = 0;
3923
3924	if (this.sidebarFooterContainer != null)
3925	{
3926		var bottom = this.footerHeight + off;
3927		sidebarFooterHeight = Math.max(0, Math.min(h - tmp - bottom, this.sidebarFooterHeight));
3928		this.sidebarFooterContainer.style.width = effHsplitPosition + 'px';
3929		this.sidebarFooterContainer.style.height = sidebarFooterHeight + 'px';
3930		this.sidebarFooterContainer.style.bottom = bottom + 'px';
3931	}
3932
3933	var fw = (this.format != null) ? this.formatWidth : 0;
3934	this.sidebarContainer.style.top = tmp + 'px';
3935	this.sidebarContainer.style.width = effHsplitPosition + 'px';
3936	this.formatContainer.style.top = tmp + 'px';
3937	this.formatContainer.style.width = fw + 'px';
3938	this.formatContainer.style.display = (this.format != null) ? '' : 'none';
3939
3940	var diagContOffset = this.getDiagramContainerOffset();
3941	var contLeft = (this.hsplit.parentNode != null) ? (effHsplitPosition + this.splitSize) : 0;
3942	this.footerContainer.style.height = this.footerHeight + 'px';
3943	this.hsplit.style.top = this.sidebarContainer.style.top;
3944	this.hsplit.style.bottom = (this.footerHeight + off) + 'px';
3945	this.hsplit.style.left = effHsplitPosition + 'px';
3946	this.footerContainer.style.display = (this.footerHeight == 0) ? 'none' : '';
3947
3948	if (this.tabContainer != null)
3949	{
3950		this.tabContainer.style.left = contLeft + 'px';
3951	}
3952
3953	if (this.footerHeight > 0)
3954	{
3955		this.footerContainer.style.bottom = off + 'px';
3956	}
3957
3958	var th = 0;
3959
3960	if (this.tabContainer != null)
3961	{
3962		this.tabContainer.style.bottom = (this.footerHeight + off) + 'px';
3963		this.tabContainer.style.right = this.diagramContainer.style.right;
3964		th = this.tabContainer.clientHeight;
3965	}
3966
3967	this.sidebarContainer.style.bottom = (this.footerHeight + sidebarFooterHeight + off) + 'px';
3968	this.formatContainer.style.bottom = (this.footerHeight + off) + 'px';
3969
3970	if (urlParams['embedInline'] != '1')
3971	{
3972		this.diagramContainer.style.left =  (contLeft + diagContOffset.x) + 'px';
3973		this.diagramContainer.style.top = (tmp + diagContOffset.y) + 'px';
3974		this.diagramContainer.style.right = fw + 'px';
3975		this.diagramContainer.style.bottom = (this.footerHeight + off + th) + 'px';
3976	}
3977
3978	if (sizeDidChange)
3979	{
3980		this.editor.graph.sizeDidChange();
3981	}
3982};
3983
3984/**
3985 * Creates the required containers.
3986 */
3987EditorUi.prototype.createTabContainer = function()
3988{
3989	return null;
3990};
3991
3992/**
3993 * Creates the required containers.
3994 */
3995EditorUi.prototype.createDivs = function()
3996{
3997	this.menubarContainer = this.createDiv('geMenubarContainer');
3998	this.toolbarContainer = this.createDiv('geToolbarContainer');
3999	this.sidebarContainer = this.createDiv('geSidebarContainer');
4000	this.formatContainer = this.createDiv('geSidebarContainer geFormatContainer');
4001	this.diagramContainer = this.createDiv('geDiagramContainer');
4002	this.footerContainer = this.createDiv('geFooterContainer');
4003	this.hsplit = this.createDiv('geHsplit');
4004	this.hsplit.setAttribute('title', mxResources.get('collapseExpand'));
4005
4006	// Sets static style for containers
4007	this.menubarContainer.style.top = '0px';
4008	this.menubarContainer.style.left = '0px';
4009	this.menubarContainer.style.right = '0px';
4010	this.toolbarContainer.style.left = '0px';
4011	this.toolbarContainer.style.right = '0px';
4012	this.sidebarContainer.style.left = '0px';
4013	this.formatContainer.style.right = '0px';
4014	this.formatContainer.style.zIndex = '1';
4015	this.diagramContainer.style.right = ((this.format != null) ? this.formatWidth : 0) + 'px';
4016	this.footerContainer.style.left = '0px';
4017	this.footerContainer.style.right = '0px';
4018	this.footerContainer.style.bottom = '0px';
4019	this.footerContainer.style.zIndex = mxPopupMenu.prototype.zIndex - 3;
4020	this.hsplit.style.width = this.splitSize + 'px';
4021	this.sidebarFooterContainer = this.createSidebarFooterContainer();
4022
4023	if (this.sidebarFooterContainer)
4024	{
4025		this.sidebarFooterContainer.style.left = '0px';
4026	}
4027
4028	if (!this.editor.chromeless)
4029	{
4030		this.tabContainer = this.createTabContainer();
4031	}
4032	else
4033	{
4034		this.diagramContainer.style.border = 'none';
4035	}
4036};
4037
4038/**
4039 * Hook for sidebar footer container. This implementation returns null.
4040 */
4041EditorUi.prototype.createSidebarFooterContainer = function()
4042{
4043	return null;
4044};
4045
4046/**
4047 * Creates the required containers.
4048 */
4049EditorUi.prototype.createUi = function()
4050{
4051	// Creates menubar
4052	this.menubar = (this.editor.chromeless) ? null : this.menus.createMenubar(this.createDiv('geMenubar'));
4053
4054	if (this.menubar != null)
4055	{
4056		this.menubarContainer.appendChild(this.menubar.container);
4057	}
4058
4059	// Adds status bar in menubar
4060	if (this.menubar != null)
4061	{
4062		this.statusContainer = this.createStatusContainer();
4063
4064		// Connects the status bar to the editor status
4065		this.editor.addListener('statusChanged', mxUtils.bind(this, function()
4066		{
4067			this.setStatusText(this.editor.getStatus());
4068		}));
4069
4070		this.setStatusText(this.editor.getStatus());
4071		this.menubar.container.appendChild(this.statusContainer);
4072
4073		// Inserts into DOM
4074		this.container.appendChild(this.menubarContainer);
4075	}
4076
4077	// Creates the sidebar
4078	this.sidebar = (this.editor.chromeless) ? null : this.createSidebar(this.sidebarContainer);
4079
4080	if (this.sidebar != null)
4081	{
4082		this.container.appendChild(this.sidebarContainer);
4083	}
4084
4085	// Creates the format sidebar
4086	this.format = (this.editor.chromeless || !this.formatEnabled) ? null : this.createFormat(this.formatContainer);
4087
4088	if (this.format != null)
4089	{
4090		this.container.appendChild(this.formatContainer);
4091	}
4092
4093	// Creates the footer
4094	var footer = (this.editor.chromeless) ? null : this.createFooter();
4095
4096	if (footer != null)
4097	{
4098		this.footerContainer.appendChild(footer);
4099		this.container.appendChild(this.footerContainer);
4100	}
4101
4102	if (this.sidebar != null && this.sidebarFooterContainer)
4103	{
4104		this.container.appendChild(this.sidebarFooterContainer);
4105	}
4106
4107	this.container.appendChild(this.diagramContainer);
4108
4109	if (this.container != null && this.tabContainer != null)
4110	{
4111		this.container.appendChild(this.tabContainer);
4112	}
4113
4114	// Creates toolbar
4115	this.toolbar = (this.editor.chromeless) ? null : this.createToolbar(this.createDiv('geToolbar'));
4116
4117	if (this.toolbar != null)
4118	{
4119		this.toolbarContainer.appendChild(this.toolbar.container);
4120		this.container.appendChild(this.toolbarContainer);
4121	}
4122
4123	// HSplit
4124	if (this.sidebar != null)
4125	{
4126		this.container.appendChild(this.hsplit);
4127
4128		this.addSplitHandler(this.hsplit, true, 0, mxUtils.bind(this, function(value)
4129		{
4130			this.hsplitPosition = value;
4131			this.refresh();
4132		}));
4133	}
4134};
4135
4136/**
4137 * Creates a new toolbar for the given container.
4138 */
4139EditorUi.prototype.createStatusContainer = function()
4140{
4141	var container = document.createElement('a');
4142	container.className = 'geItem geStatus';
4143
4144	return container;
4145};
4146
4147/**
4148 * Creates a new toolbar for the given container.
4149 */
4150EditorUi.prototype.setStatusText = function(value)
4151{
4152	this.statusContainer.innerHTML = value;
4153};
4154
4155/**
4156 * Creates a new toolbar for the given container.
4157 */
4158EditorUi.prototype.createToolbar = function(container)
4159{
4160	return new Toolbar(this, container);
4161};
4162
4163/**
4164 * Creates a new sidebar for the given container.
4165 */
4166EditorUi.prototype.createSidebar = function(container)
4167{
4168	return new Sidebar(this, container);
4169};
4170
4171/**
4172 * Creates a new sidebar for the given container.
4173 */
4174EditorUi.prototype.createFormat = function(container)
4175{
4176	return new Format(this, container);
4177};
4178
4179/**
4180 * Creates and returns a new footer.
4181 */
4182EditorUi.prototype.createFooter = function()
4183{
4184	return this.createDiv('geFooter');
4185};
4186
4187/**
4188 * Creates the actual toolbar for the toolbar container.
4189 */
4190EditorUi.prototype.createDiv = function(classname)
4191{
4192	var elt = document.createElement('div');
4193	elt.className = classname;
4194
4195	return elt;
4196};
4197
4198/**
4199 * Updates the states of the given undo/redo items.
4200 */
4201EditorUi.prototype.addSplitHandler = function(elt, horizontal, dx, onChange)
4202{
4203	var start = null;
4204	var initial = null;
4205	var ignoreClick = true;
4206	var last = null;
4207
4208	// Disables built-in pan and zoom in IE10 and later
4209	if (mxClient.IS_POINTER)
4210	{
4211		elt.style.touchAction = 'none';
4212	}
4213
4214	var getValue = mxUtils.bind(this, function()
4215	{
4216		var result = parseInt(((horizontal) ? elt.style.left : elt.style.bottom));
4217
4218		// Takes into account hidden footer
4219		if (!horizontal)
4220		{
4221			result = result + dx - this.footerHeight;
4222		}
4223
4224		return result;
4225	});
4226
4227	function moveHandler(evt)
4228	{
4229		if (start != null)
4230		{
4231			var pt = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));
4232			onChange(Math.max(0, initial + ((horizontal) ? (pt.x - start.x) : (start.y - pt.y)) - dx));
4233			mxEvent.consume(evt);
4234
4235			if (initial != getValue())
4236			{
4237				ignoreClick = true;
4238				last = null;
4239			}
4240		}
4241	};
4242
4243	function dropHandler(evt)
4244	{
4245		moveHandler(evt);
4246		initial = null;
4247		start = null;
4248	};
4249
4250	mxEvent.addGestureListeners(elt, function(evt)
4251	{
4252		start = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));
4253		initial = getValue();
4254		ignoreClick = false;
4255		mxEvent.consume(evt);
4256	});
4257
4258	mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt)
4259	{
4260		if (!ignoreClick && this.hsplitClickEnabled)
4261		{
4262			var next = (last != null) ? last - dx : 0;
4263			last = getValue();
4264			onChange(next);
4265			mxEvent.consume(evt);
4266		}
4267	}));
4268
4269	mxEvent.addGestureListeners(document, null, moveHandler, dropHandler);
4270
4271	this.destroyFunctions.push(function()
4272	{
4273		mxEvent.removeGestureListeners(document, null, moveHandler, dropHandler);
4274	});
4275};
4276
4277/**
4278 * Translates this point by the given vector.
4279 *
4280 * @param {number} dx X-coordinate of the translation.
4281 * @param {number} dy Y-coordinate of the translation.
4282 */
4283EditorUi.prototype.handleError = function(resp, title, fn, invokeFnOnClose, notFoundMessage)
4284{
4285	var e = (resp != null && resp.error != null) ? resp.error : resp;
4286
4287	if (e != null || title != null)
4288	{
4289		var msg = mxUtils.htmlEntities(mxResources.get('unknownError'));
4290		var btn = mxResources.get('ok');
4291		title = (title != null) ? title : mxResources.get('error');
4292
4293		if (e != null && e.message != null)
4294		{
4295			msg = mxUtils.htmlEntities(e.message);
4296		}
4297
4298		this.showError(title, msg, btn, fn, null, null, null, null, null,
4299			null, null, null, (invokeFnOnClose) ? fn : null);
4300	}
4301	else if (fn != null)
4302	{
4303		fn();
4304	}
4305};
4306
4307/**
4308 * Translates this point by the given vector.
4309 *
4310 * @param {number} dx X-coordinate of the translation.
4311 * @param {number} dy Y-coordinate of the translation.
4312 */
4313EditorUi.prototype.showError = function(title, msg, btn, fn, retry, btn2, fn2, btn3, fn3, w, h, hide, onClose)
4314{
4315	var dlg = new ErrorDialog(this, title, msg, btn || mxResources.get('ok'),
4316		fn, retry, btn2, fn2, hide, btn3, fn3);
4317	var lines = Math.ceil((msg != null) ? msg.length / 50 : 1);
4318	this.showDialog(dlg.container, w || 340, h || (100 + lines * 20), true, false, onClose);
4319	dlg.init();
4320};
4321
4322/**
4323 * Displays a print dialog.
4324 */
4325EditorUi.prototype.showDialog = function(elt, w, h, modal, closable, onClose, noScroll, transparent, onResize, ignoreBgClick)
4326{
4327	this.editor.graph.tooltipHandler.resetTimer();
4328	this.editor.graph.tooltipHandler.hideTooltip();
4329
4330	if (this.dialogs == null)
4331	{
4332		this.dialogs = [];
4333	}
4334
4335	this.dialog = new Dialog(this, elt, w, h, modal, closable, onClose, noScroll, transparent, onResize, ignoreBgClick);
4336	this.dialogs.push(this.dialog);
4337};
4338
4339/**
4340 * Displays a print dialog.
4341 */
4342EditorUi.prototype.hideDialog = function(cancel, isEsc, matchContainer)
4343{
4344	if (this.dialogs != null && this.dialogs.length > 0)
4345	{
4346		if (matchContainer != null && matchContainer != this.dialog.container.firstChild)
4347		{
4348			return;
4349		}
4350
4351		var dlg = this.dialogs.pop();
4352
4353		if (dlg.close(cancel, isEsc) == false)
4354		{
4355			//add the dialog back if dialog closing is cancelled
4356			this.dialogs.push(dlg);
4357			return;
4358		}
4359
4360		this.dialog = (this.dialogs.length > 0) ? this.dialogs[this.dialogs.length - 1] : null;
4361		this.editor.fireEvent(new mxEventObject('hideDialog'));
4362
4363		if (this.dialog == null && this.editor.graph.container.style.visibility != 'hidden')
4364		{
4365			window.setTimeout(mxUtils.bind(this, function()
4366			{
4367				if (this.editor.graph.isEditing() && this.editor.graph.cellEditor.textarea != null)
4368				{
4369					this.editor.graph.cellEditor.textarea.focus();
4370				}
4371				else
4372				{
4373					mxUtils.clearSelection();
4374					this.editor.graph.container.focus();
4375				}
4376			}), 0);
4377		}
4378	}
4379};
4380
4381/**
4382 * Handles ctrl+enter keystroke to clone cells.
4383 */
4384EditorUi.prototype.ctrlEnter = function()
4385{
4386	var graph = this.editor.graph;
4387
4388	if (graph.isEnabled())
4389	{
4390		try
4391		{
4392			var cells = graph.getSelectionCells();
4393		    var lookup = new mxDictionary();
4394		    var newCells = [];
4395
4396		    for (var i = 0; i < cells.length; i++)
4397		    {
4398		    	// Clones table rows instead of cells
4399		    	var cell = (graph.isTableCell(cells[i])) ? graph.model.getParent(cells[i]) : cells[i];
4400
4401		    	if (cell != null && !lookup.get(cell))
4402		    	{
4403		    		lookup.put(cell, true);
4404		            newCells.push(cell);
4405		        }
4406		    }
4407
4408			graph.setSelectionCells(graph.duplicateCells(newCells, false));
4409		}
4410		catch (e)
4411		{
4412			this.handleError(e);
4413		}
4414	}
4415};
4416
4417/**
4418 * Display a color dialog.
4419 */
4420EditorUi.prototype.pickColor = function(color, apply)
4421{
4422	var graph = this.editor.graph;
4423	var selState = graph.cellEditor.saveSelection();
4424	var h = 230 + ((Math.ceil(ColorDialog.prototype.presetColors.length / 12) +
4425		Math.ceil(ColorDialog.prototype.defaultColors.length / 12)) * 17);
4426
4427	var dlg = new ColorDialog(this, color || 'none', function(color)
4428	{
4429		graph.cellEditor.restoreSelection(selState);
4430		apply(color);
4431	}, function()
4432	{
4433		graph.cellEditor.restoreSelection(selState);
4434	});
4435	this.showDialog(dlg.container, 230, h, true, false);
4436	dlg.init();
4437};
4438
4439/**
4440 * Adds the label menu items to the given menu and parent.
4441 */
4442EditorUi.prototype.openFile = function()
4443{
4444	// Closes dialog after open
4445	window.openFile = new OpenFile(mxUtils.bind(this, function(cancel)
4446	{
4447		this.hideDialog(cancel);
4448	}));
4449
4450	// Removes openFile if dialog is closed
4451	this.showDialog(new OpenDialog(this).container, (Editor.useLocalStorage) ? 640 : 320,
4452			(Editor.useLocalStorage) ? 480 : 220, true, true, function()
4453	{
4454		window.openFile = null;
4455	});
4456};
4457
4458/**
4459 * Extracs the graph model from the given HTML data from a data transfer event.
4460 */
4461EditorUi.prototype.extractGraphModelFromHtml = function(data)
4462{
4463	var result = null;
4464
4465	try
4466	{
4467    	var idx = data.indexOf('&lt;mxGraphModel ');
4468
4469    	if (idx >= 0)
4470    	{
4471    		var idx2 = data.lastIndexOf('&lt;/mxGraphModel&gt;');
4472
4473    		if (idx2 > idx)
4474    		{
4475    			result = data.substring(idx, idx2 + 21).replace(/&gt;/g, '>').
4476    				replace(/&lt;/g, '<').replace(/\\&quot;/g, '"').replace(/\n/g, '');
4477    		}
4478    	}
4479	}
4480	catch (e)
4481	{
4482		// ignore
4483	}
4484
4485	return result;
4486};
4487
4488/**
4489 * Opens the given files in the editor.
4490 */
4491EditorUi.prototype.readGraphModelFromClipboard = function(fn)
4492{
4493	this.readGraphModelFromClipboardWithType(mxUtils.bind(this, function(xml)
4494	{
4495		if (xml != null)
4496		{
4497			fn(xml);
4498		}
4499		else
4500		{
4501			this.readGraphModelFromClipboardWithType(mxUtils.bind(this, function(xml)
4502			{
4503				if (xml != null)
4504				{
4505					var tmp = decodeURIComponent(xml);
4506
4507					if (this.isCompatibleString(tmp))
4508					{
4509						xml = tmp;
4510					}
4511				}
4512
4513				fn(xml);
4514			}), 'text');
4515		}
4516	}), 'html');
4517};
4518
4519/**
4520 * Opens the given files in the editor.
4521 */
4522EditorUi.prototype.readGraphModelFromClipboardWithType = function(fn, type)
4523{
4524	navigator.clipboard.read().then(mxUtils.bind(this, function(data)
4525	{
4526		if (data != null && data.length > 0 && type == 'html' &&
4527			mxUtils.indexOf(data[0].types, 'text/html') >= 0)
4528		{
4529			data[0].getType('text/html').then(mxUtils.bind(this, function(blob)
4530			{
4531				blob.text().then(mxUtils.bind(this, function(value)
4532				{
4533					try
4534					{
4535						var elt = this.parseHtmlData(value);
4536						var asHtml = elt.getAttribute('data-type') != 'text/plain';
4537
4538						// KNOWN: Paste from IE11 to other browsers on Windows
4539						// seems to paste the contents of index.html
4540						var xml = (asHtml) ? elt.innerHTML :
4541							mxUtils.trim((elt.innerText == null) ?
4542							mxUtils.getTextContent(elt) : elt.innerText);
4543
4544						// Workaround for junk after XML in VM
4545						try
4546						{
4547							var idx = xml.lastIndexOf('%3E');
4548
4549							if (idx >= 0 && idx < xml.length - 3)
4550							{
4551								xml = xml.substring(0, idx + 3);
4552							}
4553						}
4554						catch (e)
4555						{
4556							// ignore
4557						}
4558
4559						// Checks for embedded XML content
4560						try
4561						{
4562							var spans = elt.getElementsByTagName('span');
4563							var tmp = (spans != null && spans.length > 0) ?
4564								mxUtils.trim(decodeURIComponent(spans[0].textContent)) :
4565								decodeURIComponent(xml);
4566
4567							if (this.isCompatibleString(tmp))
4568							{
4569								xml = tmp;
4570							}
4571						}
4572						catch (e)
4573						{
4574							// ignore
4575						}
4576					}
4577					catch (e)
4578					{
4579						// ignore
4580					}
4581
4582					fn(this.isCompatibleString(xml) ? xml : null);
4583				}))['catch'](function(data)
4584				{
4585					fn(null);
4586				});
4587			}))['catch'](function(data)
4588			{
4589				fn(null);
4590			});
4591		}
4592		else if (data != null && data.length > 0 && type == 'text' &&
4593				mxUtils.indexOf(data[0].types, 'text/plain') >= 0)
4594		{
4595			data[0].getType('text/plain').then(function(blob)
4596			{
4597				blob.text().then(function(value)
4598				{
4599					fn(value);
4600				})['catch'](function()
4601				{
4602					fn(null);
4603				});
4604			})['catch'](function()
4605			{
4606				fn(null);
4607			});
4608		}
4609		else
4610		{
4611			fn(null);
4612		}
4613	}))['catch'](function(data)
4614	{
4615		fn(null);
4616	});
4617};
4618
4619/**
4620 * Parses the given HTML data and returns a DIV.
4621 */
4622EditorUi.prototype.parseHtmlData = function(data)
4623{
4624	var elt = null;
4625
4626	if (data != null && data.length > 0)
4627	{
4628		var hasMeta = data.substring(0, 6) == '<meta ';
4629		elt = document.createElement('div');
4630		elt.innerHTML = ((hasMeta) ? '<meta charset="utf-8">' : '') +
4631			this.editor.graph.sanitizeHtml(data);
4632		asHtml = true;
4633
4634		// Workaround for innerText not ignoring style elements in Chrome
4635		var styles = elt.getElementsByTagName('style');
4636
4637		if (styles != null)
4638		{
4639			while (styles.length > 0)
4640			{
4641				styles[0].parentNode.removeChild(styles[0]);
4642			}
4643		}
4644
4645		// Special case of link pasting from Chrome
4646		if (elt.firstChild != null && elt.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT &&
4647			elt.firstChild.nextSibling != null && elt.firstChild.nextSibling.nodeType == mxConstants.NODETYPE_ELEMENT &&
4648			elt.firstChild.nodeName == 'META' && elt.firstChild.nextSibling.nodeName == 'A' &&
4649			elt.firstChild.nextSibling.nextSibling == null)
4650		{
4651			var temp = (elt.firstChild.nextSibling.innerText == null) ?
4652				mxUtils.getTextContent(elt.firstChild.nextSibling) :
4653				elt.firstChild.nextSibling.innerText;
4654
4655			if (temp == elt.firstChild.nextSibling.getAttribute('href'))
4656			{
4657				mxUtils.setTextContent(elt, temp);
4658				asHtml = false;
4659			}
4660		}
4661
4662		// Extracts single image source address with meta tag in markup
4663		var img = (hasMeta && elt.firstChild != null) ? elt.firstChild.nextSibling : elt.firstChild;
4664
4665		if (img != null && img.nextSibling == null &&
4666			img.nodeType == mxConstants.NODETYPE_ELEMENT &&
4667			img.nodeName == 'IMG')
4668		{
4669			var temp = img.getAttribute('src');
4670
4671			if (temp != null)
4672			{
4673				if (temp.substring(0, 22) == 'data:image/png;base64,')
4674				{
4675					var xml = this.extractGraphModelFromPng(temp);
4676
4677					if (xml != null && xml.length > 0)
4678					{
4679						temp = xml;
4680					}
4681				}
4682
4683				mxUtils.setTextContent(elt, temp);
4684				asHtml = false;
4685			}
4686		}
4687		else
4688		{
4689			// Extracts embedded XML or image source address from single PNG image
4690			var images = elt.getElementsByTagName('img');
4691
4692			if (images.length == 1)
4693			{
4694				var img = images[0];
4695				var temp = img.getAttribute('src');
4696
4697				if (temp != null && img.parentNode == elt && elt.children.length == 1)
4698				{
4699					if (temp.substring(0, 22) == 'data:image/png;base64,')
4700					{
4701						var xml = this.extractGraphModelFromPng(temp);
4702
4703						if (xml != null && xml.length > 0)
4704						{
4705							temp = xml;
4706						}
4707					}
4708
4709					mxUtils.setTextContent(elt, temp);
4710					asHtml = false;
4711				}
4712			}
4713		}
4714
4715		if (asHtml)
4716		{
4717			Graph.removePasteFormatting(elt);
4718		}
4719	}
4720
4721	if (!asHtml)
4722	{
4723		elt.setAttribute('data-type', 'text/plain');
4724	}
4725
4726	return elt;
4727};
4728
4729/**
4730 * Opens the given files in the editor.
4731 */
4732EditorUi.prototype.extractGraphModelFromEvent = function(evt)
4733{
4734	var result = null;
4735	var data = null;
4736
4737	if (evt != null)
4738	{
4739		var provider = (evt.dataTransfer != null) ? evt.dataTransfer : evt.clipboardData;
4740
4741		if (provider != null)
4742		{
4743			if (document.documentMode == 10 || document.documentMode == 11)
4744			{
4745				data = provider.getData('Text');
4746			}
4747			else
4748			{
4749				data = (mxUtils.indexOf(provider.types, 'text/html') >= 0) ? provider.getData('text/html') : null;
4750
4751				if (mxUtils.indexOf(provider.types, 'text/plain' && (data == null || data.length == 0)))
4752				{
4753					data = provider.getData('text/plain');
4754				}
4755			}
4756
4757			if (data != null)
4758			{
4759				data = Graph.zapGremlins(mxUtils.trim(data));
4760
4761				// Tries parsing as HTML document with embedded XML
4762				var xml =  this.extractGraphModelFromHtml(data);
4763
4764				if (xml != null)
4765				{
4766					data = xml;
4767				}
4768			}
4769		}
4770	}
4771
4772	if (data != null && this.isCompatibleString(data))
4773	{
4774		result = data;
4775	}
4776
4777	return result;
4778};
4779
4780/**
4781 * Hook for subclassers to return true if event data is a supported format.
4782 * This implementation always returns false.
4783 */
4784EditorUi.prototype.isCompatibleString = function(data)
4785{
4786	return false;
4787};
4788
4789/**
4790 * Adds the label menu items to the given menu and parent.
4791 */
4792EditorUi.prototype.saveFile = function(forceDialog)
4793{
4794	if (!forceDialog && this.editor.filename != null)
4795	{
4796		this.save(this.editor.getOrCreateFilename());
4797	}
4798	else
4799	{
4800		var dlg = new FilenameDialog(this, this.editor.getOrCreateFilename(), mxResources.get('save'), mxUtils.bind(this, function(name)
4801		{
4802			this.save(name);
4803		}), null, mxUtils.bind(this, function(name)
4804		{
4805			if (name != null && name.length > 0)
4806			{
4807				return true;
4808			}
4809
4810			mxUtils.confirm(mxResources.get('invalidName'));
4811
4812			return false;
4813		}));
4814		this.showDialog(dlg.container, 300, 100, true, true);
4815		dlg.init();
4816	}
4817};
4818
4819/**
4820 * Saves the current graph under the given filename.
4821 */
4822EditorUi.prototype.save = function(name)
4823{
4824	if (name != null)
4825	{
4826		if (this.editor.graph.isEditing())
4827		{
4828			this.editor.graph.stopEditing();
4829		}
4830
4831		var xml = mxUtils.getXml(this.editor.getGraphXml());
4832
4833		try
4834		{
4835			if (Editor.useLocalStorage)
4836			{
4837				if (localStorage.getItem(name) != null &&
4838					!mxUtils.confirm(mxResources.get('replaceIt', [name])))
4839				{
4840					return;
4841				}
4842
4843				localStorage.setItem(name, xml);
4844				this.editor.setStatus(mxUtils.htmlEntities(mxResources.get('saved')) + ' ' + new Date());
4845			}
4846			else
4847			{
4848				if (xml.length < MAX_REQUEST_SIZE)
4849				{
4850					new mxXmlRequest(SAVE_URL, 'filename=' + encodeURIComponent(name) +
4851						'&xml=' + encodeURIComponent(xml)).simulate(document, '_blank');
4852				}
4853				else
4854				{
4855					mxUtils.alert(mxResources.get('drawingTooLarge'));
4856					mxUtils.popup(xml);
4857
4858					return;
4859				}
4860			}
4861
4862			this.editor.setModified(false);
4863			this.editor.setFilename(name);
4864			this.updateDocumentTitle();
4865		}
4866		catch (e)
4867		{
4868			this.editor.setStatus(mxUtils.htmlEntities(mxResources.get('errorSavingFile')));
4869		}
4870	}
4871};
4872
4873/**
4874 * Executes the given layout.
4875 */
4876EditorUi.prototype.executeLayout = function(exec, animate, post)
4877{
4878	var graph = this.editor.graph;
4879
4880	if (graph.isEnabled())
4881	{
4882		graph.getModel().beginUpdate();
4883		try
4884		{
4885			exec();
4886		}
4887		catch (e)
4888		{
4889			throw e;
4890		}
4891		finally
4892		{
4893			// Animates the changes in the graph model except
4894			// for Camino, where animation is too slow
4895			if (this.allowAnimation && animate && (navigator.userAgent == null ||
4896				navigator.userAgent.indexOf('Camino') < 0))
4897			{
4898				// New API for animating graph layout results asynchronously
4899				var morph = new mxMorphing(graph);
4900				morph.addListener(mxEvent.DONE, mxUtils.bind(this, function()
4901				{
4902					graph.getModel().endUpdate();
4903
4904					if (post != null)
4905					{
4906						post();
4907					}
4908				}));
4909
4910				morph.startAnimation();
4911			}
4912			else
4913			{
4914				graph.getModel().endUpdate();
4915
4916				if (post != null)
4917				{
4918					post();
4919				}
4920			}
4921		}
4922	}
4923};
4924
4925/**
4926 * Hides the current menu.
4927 */
4928EditorUi.prototype.showImageDialog = function(title, value, fn, ignoreExisting)
4929{
4930	var cellEditor = this.editor.graph.cellEditor;
4931	var selState = cellEditor.saveSelection();
4932	var newValue = mxUtils.prompt(title, value);
4933	cellEditor.restoreSelection(selState);
4934
4935	if (newValue != null && newValue.length > 0)
4936	{
4937		var img = new Image();
4938
4939		img.onload = function()
4940		{
4941			fn(newValue, img.width, img.height);
4942		};
4943		img.onerror = function()
4944		{
4945			fn(null);
4946			mxUtils.alert(mxResources.get('fileNotFound'));
4947		};
4948
4949		img.src = newValue;
4950	}
4951	else
4952	{
4953		fn(null);
4954	}
4955};
4956
4957/**
4958 * Hides the current menu.
4959 */
4960EditorUi.prototype.showLinkDialog = function(value, btnLabel, fn)
4961{
4962	var dlg = new LinkDialog(this, value, btnLabel, fn);
4963	this.showDialog(dlg.container, 420, 90, true, true);
4964	dlg.init();
4965};
4966
4967/**
4968 * Hides the current menu.
4969 */
4970EditorUi.prototype.showDataDialog = function(cell)
4971{
4972	if (cell != null)
4973	{
4974		var dlg = new EditDataDialog(this, cell);
4975		this.showDialog(dlg.container, 480, 420, true, false, null, false);
4976		dlg.init();
4977	}
4978};
4979
4980/**
4981 * Hides the current menu.
4982 */
4983EditorUi.prototype.showBackgroundImageDialog = function(apply, img)
4984{
4985	apply = (apply != null) ? apply : mxUtils.bind(this, function(image)
4986	{
4987		var change = new ChangePageSetup(this, null, image);
4988		change.ignoreColor = true;
4989
4990		this.editor.graph.model.execute(change);
4991	});
4992
4993	var newValue = mxUtils.prompt(mxResources.get('backgroundImage'), (img != null) ? img.src : '');
4994
4995	if (newValue != null && newValue.length > 0)
4996	{
4997		var img = new Image();
4998
4999		img.onload = function()
5000		{
5001			apply(new mxImage(newValue, img.width, img.height), false);
5002		};
5003		img.onerror = function()
5004		{
5005			apply(null, true);
5006			mxUtils.alert(mxResources.get('fileNotFound'));
5007		};
5008
5009		img.src = newValue;
5010	}
5011	else
5012	{
5013		apply(null);
5014	}
5015};
5016
5017/**
5018 * Loads the stylesheet for this graph.
5019 */
5020EditorUi.prototype.setBackgroundImage = function(image)
5021{
5022	this.editor.graph.setBackgroundImage(image);
5023	this.editor.graph.view.validateBackgroundImage();
5024
5025	this.fireEvent(new mxEventObject('backgroundImageChanged'));
5026};
5027
5028/**
5029 * Creates the keyboard event handler for the current graph and history.
5030 */
5031EditorUi.prototype.confirm = function(msg, okFn, cancelFn)
5032{
5033	if (mxUtils.confirm(msg))
5034	{
5035		if (okFn != null)
5036		{
5037			okFn();
5038		}
5039	}
5040	else if (cancelFn != null)
5041	{
5042		cancelFn();
5043	}
5044};
5045
5046/**
5047 * Creates the keyboard event handler for the current graph and history.
5048 */
5049EditorUi.prototype.createOutline = function(wnd)
5050{
5051	var outline = new mxOutline(this.editor.graph);
5052
5053	mxEvent.addListener(window, 'resize', function()
5054	{
5055		outline.update(false);
5056	});
5057
5058	return outline;
5059};
5060
5061// Alt+Shift+Keycode mapping to action
5062EditorUi.prototype.altShiftActions = {67: 'clearWaypoints', // Alt+Shift+C
5063  65: 'connectionArrows', // Alt+Shift+A
5064  76: 'editLink', // Alt+Shift+L
5065  80: 'connectionPoints', // Alt+Shift+P
5066  84: 'editTooltip', // Alt+Shift+T
5067  86: 'pasteSize', // Alt+Shift+V
5068  88: 'copySize', // Alt+Shift+X
5069  66: 'copyData', // Alt+Shift+B
5070  69: 'pasteData' // Alt+Shift+E
5071};
5072
5073/**
5074 * Creates the keyboard event handler for the current graph and history.
5075 */
5076EditorUi.prototype.createKeyHandler = function(editor)
5077{
5078	var editorUi = this;
5079	var graph = this.editor.graph;
5080	var keyHandler = new mxKeyHandler(graph);
5081
5082	var isEventIgnored = keyHandler.isEventIgnored;
5083	keyHandler.isEventIgnored = function(evt)
5084	{
5085		// Handles undo/redo/ctrl+./,/u via action and allows ctrl+b/i
5086		// only if editing value is HTML (except for FF and Safari)
5087		return !(mxEvent.isShiftDown(evt) && evt.keyCode == 9) &&
5088			((!this.isControlDown(evt) || mxEvent.isShiftDown(evt) ||
5089			(evt.keyCode != 90 && evt.keyCode != 89 && evt.keyCode != 188 &&
5090			evt.keyCode != 190 && evt.keyCode != 85)) && ((evt.keyCode != 66 && evt.keyCode != 73) ||
5091			!this.isControlDown(evt) ||  (this.graph.cellEditor.isContentEditing() &&
5092			!mxClient.IS_FF && !mxClient.IS_SF)) && isEventIgnored.apply(this, arguments));
5093	};
5094
5095	// Ignores graph enabled state but not chromeless state
5096	keyHandler.isEnabledForEvent = function(evt)
5097	{
5098		return (!mxEvent.isConsumed(evt) && this.isGraphEvent(evt) && this.isEnabled() &&
5099			(editorUi.dialogs == null || editorUi.dialogs.length == 0));
5100	};
5101
5102	// Routes command-key to control-key on Mac
5103	keyHandler.isControlDown = function(evt)
5104	{
5105		return mxEvent.isControlDown(evt) || (mxClient.IS_MAC && evt.metaKey);
5106	};
5107
5108	var thread = null;
5109
5110	// Helper function to move cells with the cursor keys
5111	function nudge(keyCode, stepSize, resize)
5112	{
5113		if (!graph.isSelectionEmpty() && graph.isEnabled())
5114		{
5115			stepSize = (stepSize != null) ? stepSize : 1;
5116
5117			if (resize)
5118			{
5119				// Resizes all selected vertices
5120				graph.getModel().beginUpdate();
5121				try
5122				{
5123					var cells = graph.getSelectionCells();
5124
5125					for (var i = 0; i < cells.length; i++)
5126					{
5127						if (graph.getModel().isVertex(cells[i]) && graph.isCellResizable(cells[i]))
5128						{
5129							var geo = graph.getCellGeometry(cells[i]);
5130
5131							if (geo != null)
5132							{
5133								geo = geo.clone();
5134
5135								if (keyCode == 37)
5136								{
5137									geo.width = Math.max(0, geo.width - stepSize);
5138								}
5139								else if (keyCode == 38)
5140								{
5141									geo.height = Math.max(0, geo.height - stepSize);
5142								}
5143								else if (keyCode == 39)
5144								{
5145									geo.width += stepSize;
5146								}
5147								else if (keyCode == 40)
5148								{
5149									geo.height += stepSize;
5150								}
5151
5152								graph.getModel().setGeometry(cells[i], geo);
5153							}
5154						}
5155					}
5156				}
5157				finally
5158				{
5159					graph.getModel().endUpdate();
5160				}
5161			}
5162			else
5163			{
5164				// Moves vertices up/down in a stack layout
5165				var cell = graph.getSelectionCell();
5166				var parent = graph.model.getParent(cell);
5167				var scale = graph.getView().scale;
5168				var layout = null;
5169
5170				if (graph.getSelectionCount() == 1 && graph.model.isVertex(cell) &&
5171					graph.layoutManager != null && !graph.isCellLocked(cell))
5172				{
5173					layout = graph.layoutManager.getLayout(parent);
5174				}
5175
5176				if (layout != null && layout.constructor == mxStackLayout)
5177				{
5178					var index = parent.getIndex(cell);
5179
5180					if (keyCode == 37 || keyCode == 38)
5181					{
5182						graph.model.add(parent, cell, Math.max(0, index - 1));
5183					}
5184					else if (keyCode == 39 ||keyCode == 40)
5185					{
5186						graph.model.add(parent, cell, Math.min(graph.model.getChildCount(parent), index + 1));
5187					}
5188				}
5189				else
5190				{
5191					var handler = graph.graphHandler;
5192
5193					if (handler != null)
5194					{
5195						if (handler.first == null)
5196						{
5197							handler.start(graph.getSelectionCell(),
5198								0, 0, graph.getSelectionCells());
5199						}
5200
5201						if (handler.first != null)
5202						{
5203							var dx = 0;
5204							var dy = 0;
5205
5206							if (keyCode == 37)
5207							{
5208								dx = -stepSize;
5209							}
5210							else if (keyCode == 38)
5211							{
5212								dy = -stepSize;
5213							}
5214							else if (keyCode == 39)
5215							{
5216								dx = stepSize;
5217							}
5218							else if (keyCode == 40)
5219							{
5220								dy = stepSize;
5221							}
5222
5223							handler.currentDx += dx * scale;
5224							handler.currentDy += dy * scale;
5225							handler.checkPreview();
5226							handler.updatePreview();
5227						}
5228
5229						// Groups move steps in undoable change
5230						if (thread != null)
5231						{
5232							window.clearTimeout(thread);
5233						}
5234
5235						thread = window.setTimeout(function()
5236						{
5237							if (handler.first != null)
5238							{
5239								var dx = handler.roundLength(handler.currentDx / scale);
5240								var dy = handler.roundLength(handler.currentDy / scale);
5241								handler.moveCells(handler.cells, dx, dy);
5242								handler.reset();
5243							}
5244						}, 400);
5245					}
5246				}
5247			}
5248		}
5249	};
5250
5251	// Overridden to handle special alt+shift+cursor keyboard shortcuts
5252	var directions = {37: mxConstants.DIRECTION_WEST, 38: mxConstants.DIRECTION_NORTH,
5253			39: mxConstants.DIRECTION_EAST, 40: mxConstants.DIRECTION_SOUTH};
5254
5255	var keyHandlerGetFunction = keyHandler.getFunction;
5256
5257	mxKeyHandler.prototype.getFunction = function(evt)
5258	{
5259		if (graph.isEnabled())
5260		{
5261			// TODO: Add alt modified state in core API, here are some specific cases
5262			if (mxEvent.isShiftDown(evt) && mxEvent.isAltDown(evt))
5263			{
5264				var action = editorUi.actions.get(editorUi.altShiftActions[evt.keyCode]);
5265
5266				if (action != null)
5267				{
5268					return action.funct;
5269				}
5270			}
5271
5272			if (directions[evt.keyCode] != null && !graph.isSelectionEmpty())
5273			{
5274				// On macOS, Control+Cursor is used by Expose so allow for Alt+Control to resize
5275				if (!this.isControlDown(evt) && mxEvent.isShiftDown(evt) && mxEvent.isAltDown(evt))
5276				{
5277					if (graph.model.isVertex(graph.getSelectionCell()))
5278					{
5279						return function()
5280						{
5281							var cells = graph.connectVertex(graph.getSelectionCell(), directions[evt.keyCode],
5282								graph.defaultEdgeLength, evt, true);
5283
5284							if (cells != null && cells.length > 0)
5285							{
5286								if (cells.length == 1 && graph.model.isEdge(cells[0]))
5287								{
5288									graph.setSelectionCell(graph.model.getTerminal(cells[0], false));
5289								}
5290								else
5291								{
5292									graph.setSelectionCell(cells[cells.length - 1]);
5293								}
5294
5295								graph.scrollCellToVisible(graph.getSelectionCell());
5296
5297								if (editorUi.hoverIcons != null)
5298								{
5299									editorUi.hoverIcons.update(graph.view.getState(graph.getSelectionCell()));
5300								}
5301							}
5302						};
5303					}
5304				}
5305				else
5306				{
5307					// Avoids consuming event if no vertex is selected by returning null below
5308					// Cursor keys move and resize (ctrl) cells
5309					if (this.isControlDown(evt))
5310					{
5311						return function()
5312						{
5313							nudge(evt.keyCode, (mxEvent.isShiftDown(evt)) ? graph.gridSize : null, true);
5314						};
5315					}
5316					else
5317					{
5318						return function()
5319						{
5320							nudge(evt.keyCode, (mxEvent.isShiftDown(evt)) ? graph.gridSize : null);
5321						};
5322					}
5323				}
5324			}
5325		}
5326
5327		return keyHandlerGetFunction.apply(this, arguments);
5328	};
5329
5330	// Binds keystrokes to actions
5331	keyHandler.bindAction = mxUtils.bind(this, function(code, control, key, shift)
5332	{
5333		var action = this.actions.get(key);
5334
5335		if (action != null)
5336		{
5337			var f = function()
5338			{
5339				if (action.isEnabled())
5340				{
5341					action.funct();
5342				}
5343			};
5344
5345			if (control)
5346			{
5347				if (shift)
5348				{
5349					keyHandler.bindControlShiftKey(code, f);
5350				}
5351				else
5352				{
5353					keyHandler.bindControlKey(code, f);
5354				}
5355			}
5356			else
5357			{
5358				if (shift)
5359				{
5360					keyHandler.bindShiftKey(code, f);
5361				}
5362				else
5363				{
5364					keyHandler.bindKey(code, f);
5365				}
5366			}
5367		}
5368	});
5369
5370	var ui = this;
5371	var keyHandlerEscape = keyHandler.escape;
5372	keyHandler.escape = function(evt)
5373	{
5374		keyHandlerEscape.apply(this, arguments);
5375	};
5376
5377	// Ignores enter keystroke. Remove this line if you want the
5378	// enter keystroke to stop editing. N, W, T are reserved.
5379	keyHandler.enter = function() {};
5380
5381	keyHandler.bindControlShiftKey(36, function() { graph.exitGroup(); }); // Ctrl+Shift+Home
5382	keyHandler.bindControlShiftKey(35, function() { graph.enterGroup(); }); // Ctrl+Shift+End
5383	keyHandler.bindShiftKey(36, function() { graph.home(); }); // Ctrl+Shift+Home
5384	keyHandler.bindKey(35, function() { graph.refresh(); }); // End
5385	keyHandler.bindAction(107, true, 'zoomIn'); // Ctrl+Plus
5386	keyHandler.bindAction(109, true, 'zoomOut'); // Ctrl+Minus
5387	keyHandler.bindAction(80, true, 'print'); // Ctrl+P
5388	keyHandler.bindAction(79, true, 'outline', true); // Ctrl+Shift+O
5389
5390	if (!this.editor.chromeless || this.editor.editable)
5391	{
5392		keyHandler.bindControlKey(36, function() { if (graph.isEnabled()) { graph.foldCells(true); }}); // Ctrl+Home
5393		keyHandler.bindControlKey(35, function() { if (graph.isEnabled()) { graph.foldCells(false); }}); // Ctrl+End
5394		keyHandler.bindControlKey(13, function() { ui.ctrlEnter(); }); // Ctrl+Enter
5395		keyHandler.bindAction(8, false, 'delete'); // Backspace
5396		keyHandler.bindAction(8, true, 'deleteAll'); // Ctrl+Backspace
5397		keyHandler.bindAction(8, false, 'deleteLabels', true); // Shift+Backspace
5398		keyHandler.bindAction(46, false, 'delete'); // Delete
5399		keyHandler.bindAction(46, true, 'deleteAll'); // Ctrl+Delete
5400		keyHandler.bindAction(46, false, 'deleteLabels', true); // Shift+Delete
5401		keyHandler.bindAction(36, false, 'resetView'); // Home
5402		keyHandler.bindAction(72, true, 'fitWindow', true); // Ctrl+Shift+H
5403		keyHandler.bindAction(74, true, 'fitPage'); // Ctrl+J
5404		keyHandler.bindAction(74, true, 'fitTwoPages', true); // Ctrl+Shift+J
5405		keyHandler.bindAction(48, true, 'customZoom'); // Ctrl+0
5406		keyHandler.bindAction(82, true, 'turn'); // Ctrl+R
5407		keyHandler.bindAction(82, true, 'clearDefaultStyle', true); // Ctrl+Shift+R
5408		keyHandler.bindAction(83, true, 'save'); // Ctrl+S
5409		keyHandler.bindAction(83, true, 'saveAs', true); // Ctrl+Shift+S
5410		keyHandler.bindAction(65, true, 'selectAll'); // Ctrl+A
5411		keyHandler.bindAction(65, true, 'selectNone', true); // Ctrl+A
5412		keyHandler.bindAction(73, true, 'selectVertices', true); // Ctrl+Shift+I
5413		keyHandler.bindAction(69, true, 'selectEdges', true); // Ctrl+Shift+E
5414		keyHandler.bindAction(69, true, 'editStyle'); // Ctrl+E
5415		keyHandler.bindAction(66, true, 'bold'); // Ctrl+B
5416		keyHandler.bindAction(66, true, 'toBack', true); // Ctrl+Shift+B
5417		keyHandler.bindAction(70, true, 'toFront', true); // Ctrl+Shift+F
5418		keyHandler.bindAction(68, true, 'duplicate'); // Ctrl+D
5419		keyHandler.bindAction(68, true, 'setAsDefaultStyle', true); // Ctrl+Shift+D
5420		keyHandler.bindAction(90, true, 'undo'); // Ctrl+Z
5421		keyHandler.bindAction(89, true, 'autosize', true); // Ctrl+Shift+Y
5422		keyHandler.bindAction(88, true, 'cut'); // Ctrl+X
5423		keyHandler.bindAction(67, true, 'copy'); // Ctrl+C
5424		keyHandler.bindAction(86, true, 'paste'); // Ctrl+V
5425		keyHandler.bindAction(71, true, 'group'); // Ctrl+G
5426		keyHandler.bindAction(77, true, 'editData'); // Ctrl+M
5427		keyHandler.bindAction(71, true, 'grid', true); // Ctrl+Shift+G
5428		keyHandler.bindAction(73, true, 'italic'); // Ctrl+I
5429		keyHandler.bindAction(76, true, 'lockUnlock'); // Ctrl+L
5430		keyHandler.bindAction(76, true, 'layers', true); // Ctrl+Shift+L
5431		keyHandler.bindAction(80, true, 'formatPanel', true); // Ctrl+Shift+P
5432		keyHandler.bindAction(85, true, 'underline'); // Ctrl+U
5433		keyHandler.bindAction(85, true, 'ungroup', true); // Ctrl+Shift+U
5434		keyHandler.bindAction(190, true, 'superscript'); // Ctrl+.
5435		keyHandler.bindAction(188, true, 'subscript'); // Ctrl+,
5436		keyHandler.bindAction(13, false, 'keyPressEnter'); // Enter
5437		keyHandler.bindKey(113, function() { if (graph.isEnabled()) { graph.startEditingAtCell(); }}); // F2
5438	}
5439
5440	if (!mxClient.IS_WIN)
5441	{
5442		keyHandler.bindAction(90, true, 'redo', true); // Ctrl+Shift+Z
5443	}
5444	else
5445	{
5446		keyHandler.bindAction(89, true, 'redo'); // Ctrl+Y
5447	}
5448
5449	return keyHandler;
5450};
5451
5452/**
5453 * Creates the keyboard event handler for the current graph and history.
5454 */
5455EditorUi.prototype.destroy = function()
5456{
5457	if (this.editor != null)
5458	{
5459		this.editor.destroy();
5460		this.editor = null;
5461	}
5462
5463	if (this.menubar != null)
5464	{
5465		this.menubar.destroy();
5466		this.menubar = null;
5467	}
5468
5469	if (this.toolbar != null)
5470	{
5471		this.toolbar.destroy();
5472		this.toolbar = null;
5473	}
5474
5475	if (this.sidebar != null)
5476	{
5477		this.sidebar.destroy();
5478		this.sidebar = null;
5479	}
5480
5481	if (this.keyHandler != null)
5482	{
5483		this.keyHandler.destroy();
5484		this.keyHandler = null;
5485	}
5486
5487	if (this.keydownHandler != null)
5488	{
5489		mxEvent.removeListener(document, 'keydown', this.keydownHandler);
5490		this.keydownHandler = null;
5491	}
5492
5493	if (this.keyupHandler != null)
5494	{
5495		mxEvent.removeListener(document, 'keyup', this.keyupHandler);
5496		this.keyupHandler = null;
5497	}
5498
5499	if (this.resizeHandler != null)
5500	{
5501		mxEvent.removeListener(window, 'resize', this.resizeHandler);
5502		this.resizeHandler = null;
5503	}
5504
5505	if (this.gestureHandler != null)
5506	{
5507		mxEvent.removeGestureListeners(document, this.gestureHandler);
5508		this.gestureHandler = null;
5509	}
5510
5511	if (this.orientationChangeHandler != null)
5512	{
5513		mxEvent.removeListener(window, 'orientationchange', this.orientationChangeHandler);
5514		this.orientationChangeHandler = null;
5515	}
5516
5517	if (this.scrollHandler != null)
5518	{
5519		mxEvent.removeListener(window, 'scroll', this.scrollHandler);
5520		this.scrollHandler = null;
5521	}
5522
5523	if (this.destroyFunctions != null)
5524	{
5525		for (var i = 0; i < this.destroyFunctions.length; i++)
5526		{
5527			this.destroyFunctions[i]();
5528		}
5529
5530		this.destroyFunctions = null;
5531	}
5532
5533	var c = [this.menubarContainer, this.toolbarContainer, this.sidebarContainer,
5534	         this.formatContainer, this.diagramContainer, this.footerContainer,
5535	         this.chromelessToolbar, this.hsplit, this.sidebarFooterContainer,
5536	         this.layersDialog];
5537
5538	for (var i = 0; i < c.length; i++)
5539	{
5540		if (c[i] != null && c[i].parentNode != null)
5541		{
5542			c[i].parentNode.removeChild(c[i]);
5543		}
5544	}
5545};
5546