Browse Source

11.1.5 release

Gaudenz Alder 6 years ago
parent
commit
5c95e2d973

+ 13 - 1
ChangeLog

@@ -1,3 +1,12 @@
+19-AUG-2019: 11.1.5
+
+- Fixes ignored background for multipage PDF export
+- Adds Lollipop Notation shape in UML section
+- Fixes export for global placeholders
+- Adds XML declaration for SVG export
+- Enables block alignment for text
+- Adds allowArrows style
+
 08-AUG-2019: 11.1.4
 
 - Fixes grid NPE in export dialog
@@ -12,7 +21,7 @@
 05-AUG-2019: 11.1.2
 
 - Fixes encoding of embedded PNG data
-- Ads custom Gitlab URL option
+- Adds custom Gitlab URL option
 
 02-AUG-2019: 11.1.1
 
@@ -49,6 +58,7 @@
 - Fixes incorrect zTXt header for PNG+XML files
 - Fixes rounding of ellipse positions in SVG
 - Fixes rounding for fixed connection points
+- Uses mxGraph 4.0.3 beta 4
 
 23-JUL-2019: 11.0.4
 
@@ -58,11 +68,13 @@
 
 - Fixes handling of repository paths in GitLab
 - Ignores hidden cells for selectAll
+- Uses mxGraph 4.0.3 beta 3
 
 22-JUL-2019: 11.0.2
 
 - Replaces alt+click with shift+click in ChromeOS
 - Uses draw.io Viewer app for lightbox mode
+- Uses mxGraph 4.0.3 beta 2
 
 20-JUL-2019: 11.0.1
 

+ 1 - 1
VERSION

@@ -1 +1 @@
-11.1.4
+11.1.5

+ 2 - 0
etc/build/build.xml

@@ -275,6 +275,7 @@
 				<file name="Trees.js" />
 				<file name="Minimal.js" />
 				<file name="DistanceGuides.js" />
+				<file name="mxRuler.js" />
 				<file name="mxFreehand.js" />
 				<file name="DrawioUser.js" />
 				<file name="DrawioComment.js" />
@@ -375,6 +376,7 @@
 				<file name="Trees.js" />
 				<file name="Minimal.js" />
 				<file name="DistanceGuides.js" />
+				<file name="mxRuler.js" />
 				<file name="mxFreehand.js" />
 			</sources>
 		</jscomp>

+ 1 - 1
src/main/webapp/cache.manifest

@@ -1,7 +1,7 @@
 CACHE MANIFEST
 
 # THIS FILE WAS GENERATED. DO NOT MODIFY!
-# 08/08/2019 04:41 PM
+# 08/19/2019 05:05 PM
 
 app.html
 index.html?offline=1

+ 2 - 1
src/main/webapp/electron.js

@@ -1076,7 +1076,8 @@ function exportDiagram(event, args, directFinalize)
 				}
 				else if (args.format == 'png' || args.format == 'jpg' || args.format == 'jpeg')
 				{
-					var newBounds = {width: Math.ceil(bounds.width + bounds.x), height: Math.ceil(bounds.height + bounds.y)};
+					//Adds an extra pixel to prevent scrollbars from showing
+					var newBounds = {width: Math.ceil(bounds.width + bounds.x) + 1, height: Math.ceil(bounds.height + bounds.y) + 1};
 					browser.setBounds(newBounds);
 					
 					//TODO The browser takes sometime to show the graph (also after resize it takes some time to render)

+ 9 - 2
src/main/webapp/export3.html

@@ -103,6 +103,12 @@
 				diagrams = xmlDoc.documentElement.getElementsByTagName('diagram');
 			}
 	        
+			//Add global variables to graph
+			if (extras != null && extras.globalVars != null)
+			{
+				graph.globalUrlVars = extras.globalVars;
+			}
+			
 	        /**
 			 * Implements %page% and %pagenumber% placeholders
 			 */
@@ -319,7 +325,8 @@
 				codec.decode(xmlDoc.documentElement, model);
 	
 				// Loads background color
-				var bg = (data.bg != null && data.bg.length > 0) ? data.bg : xmlDoc.documentElement.getAttribute('background');
+				var bg = (data.bg != null && data.bg.length > 0 && data.format != 'pdf') ?
+					data.bg : xmlDoc.documentElement.getAttribute('background');
 	
 				// Normalizes values for transparent backgrounds
 				if (bg == 'none' || bg == '')
@@ -334,7 +341,7 @@
 				}
 	
 				// Sets background color on page
-				if (bg != null)
+				if (bg != null && data.format != 'pdf')
 				{
 					document.body.style.backgroundColor = bg;
 				}

File diff suppressed because it is too large
+ 1402 - 1378
src/main/webapp/js/app.min.js


+ 1 - 1
src/main/webapp/js/diagramly/App.js

@@ -1266,7 +1266,7 @@ App.prototype.init = function()
 						{
 							if (this.drive.user != null && (!isLocalStorage || mxSettings.settings == null ||
 								mxSettings.settings.closeRealtimeWarning == null || mxSettings.settings.closeRealtimeWarning <
-								new Date().getTime() - (4 * 24 * 60 * 60 * 1000)) &&
+								new Date().getTime() - (2 * 24 * 60 * 60 * 1000)) &&
 								(!this.editor.chromeless || this.editor.editable))
 							{
 								this.drive.checkRealtimeFiles(mxUtils.bind(this, function()

+ 2 - 0
src/main/webapp/js/diagramly/DevTools.js

@@ -51,6 +51,8 @@ if (urlParams['dev'] == '1')
 				}
 				else
 				{
+//					tip += 'pos=' + this.view.formatUnitText(parseFloat(geo.x)) + ',' + this.view.formatUnitText(parseFloat(geo.y)) + '<br>' +
+//						'size=' + this.view.formatUnitText(parseFloat(geo.width)) + 'x' + this.view.formatUnitText(parseFloat(geo.height));
 					tip += 'pos=' + parseFloat(geo.x) + ',' + parseFloat(geo.y) + '<br>' +
 						'size=' + parseFloat(geo.width) + 'x' + parseFloat(geo.height);
 				}

+ 2 - 5
src/main/webapp/js/diagramly/Devel.js

@@ -126,6 +126,8 @@ mxscript(drawDevUrl + 'js/diagramly/Pages.js');
 mxscript(drawDevUrl + 'js/diagramly/Trees.js');
 mxscript(drawDevUrl + 'js/diagramly/Minimal.js');
 mxscript(drawDevUrl + 'js/diagramly/DistanceGuides.js');
+mxscript(drawDevUrl + 'js/diagramly/mxRuler.js');
+mxscript(drawDevUrl + 'js/diagramly/mxFreehand.js');
 mxscript(drawDevUrl + 'js/diagramly/DevTools.js');
 
 // Vsdx/vssx support
@@ -135,9 +137,6 @@ mxscript(drawDevUrl + 'js/diagramly/vsdx/bmpDecoder.js');
 mxscript(drawDevUrl + 'js/diagramly/vsdx/importer.js');
 mxscript(drawDevUrl + 'js/jszip/jszip.min.js');
 
-// mxRuler
-mxscript(drawDevUrl + 'js/diagramly/ruler/mxRuler.js');
-
 //GraphMl Import
 mxscript(drawDevUrl + 'js/diagramly/graphml/mxGraphMlCodec.js');
 
@@ -146,5 +145,3 @@ if (urlParams['tableLayout'] == '1')
 {
   mxscript(drawDevUrl + 'js/diagramly/mxTableLayout.js');
 }
-
-mxscript(drawDevUrl + 'js/diagramly/mxFreehand.js');

+ 50 - 35
src/main/webapp/js/diagramly/Editor.js

@@ -175,6 +175,7 @@
         },
         {name: 'portConstraintRotation', dispName: 'Rotate Constraint', type: 'bool', defVal: false},
         {name: 'connectable', dispName: 'Connectable', type: 'bool', defVal: true},
+        {name: 'allowArrows', dispName: 'Allow Arrows', type: 'bool', defVal: true},
         {name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
         {name: 'perimeter', dispName: 'Perimeter', defVal: 'none', type: 'enum',
         	enumList: [{val: 'none', dispName: 'None'},
@@ -3716,6 +3717,55 @@
 			
 			return layoutManagerGetLayout.apply(this, arguments);
 		}
+		
+		this.updateGlobalUrlVariables();
+	};
+	
+	/**
+	 * Updates the global variables from the vars URL parameter.
+	 */
+	Graph.prototype.updateGlobalUrlVariables = function()
+	{
+		if (urlParams['vars'] != null)
+		{
+			try
+			{
+				this.globalUrlVars = JSON.parse(decodeURIComponent(urlParams['vars']));
+			}
+			catch (e)
+			{
+				if (window.console != null)
+				{
+					console.log('Error in vars URL parameter: ' + e);
+				}
+			}
+		}
+	};
+
+	/**
+	 * Returns all global variables used for export. This function never returns null.
+	 * This can be overridden by plugins to return global variables for export.
+	 */
+	Graph.prototype.getExportVariables = function()
+	{
+		return (this.globalUrlVars != null) ? this.globalUrlVars : {};
+	};
+	
+	/**
+	 * Adds support for vars URL parameter.
+	 */
+	var graphGetGlobalVariable = Graph.prototype.getGlobalVariable;
+	
+	Graph.prototype.getGlobalVariable = function(name)
+	{
+		var val = graphGetGlobalVariable.apply(this, arguments);
+		
+		if (val == null && this.globalUrlVars != null)
+		{
+			val = this.globalUrlVars[name];
+		}
+		
+		return val;
 	};
 
 	/**
@@ -3771,41 +3821,6 @@
 		return graphIsCssTransformsSupported.apply(this, arguments) && !mxClient.IS_SF;
 	};
 
-	/**
-	 * Adds support for vars URL parameter.
-	 */
-	var graphGetGlobalVariable = Graph.prototype.getGlobalVariable;
-	
-	Graph.prototype.getGlobalVariable = function(name)
-	{
-		var val = graphGetGlobalVariable.apply(this, arguments);
-		
-		if (val == null)
-		{
-			if (this.globalUrlVars == null && urlParams['vars'] != null)
-			{
-				try
-				{
-					this.globalUrlVars = JSON.parse(decodeURIComponent(urlParams['vars']));
-				}
-				catch (e)
-				{
-					if (window.console != null)
-					{
-						console.log('Error in vars URL parameter: ' + e);
-					}
-				}
-			}
-			
-			if (this.globalUrlVars != null)
-			{
-				val = this.globalUrlVars[name];
-			}
-		}
-		
-		return val;
-	};
-
 	/**
 	 * Adds workaround for math rendering in Chrome.
 	 * 

+ 316 - 297
src/main/webapp/js/diagramly/EditorUi.js

@@ -931,6 +931,11 @@
 					fileNode.setAttribute('type', md);
 				}
 				
+				if (this.pages != null)
+				{
+					fileNode.setAttribute('pages', this.pages.length);
+				}
+				
 				if (nonCompressed)
 				{
 					fileNode.setAttribute('compressed', 'false');
@@ -1742,7 +1747,8 @@
 	 */
 	EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64, transparent, currentPage, scale, border, grid)
 	{
-		var bounds = this.editor.graph.getGraphBounds();
+		var graph = this.editor.graph;
+		var bounds = graph.getGraphBounds();
 		
 		// Exports only current page for images that does not contain file data, but for
 		// the other formats with XML included or pdf with all pages, we need to send the complete data and use
@@ -1782,7 +1788,7 @@
        		}
        	}
        	
-		var bg = this.editor.graph.background;
+		var bg = graph.background;
 		
 		if (format == 'png' && transparent)
 		{
@@ -1793,18 +1799,24 @@
 			bg = '#ffffff';
 		}
 		
+		var extras = {};
+		extras.globalVars = graph.getExportVariables();
+		
+		if (grid)
+		{
+			extras.grid = {
+				size: graph.gridSize,
+				steps: graph.view.gridSteps,
+				color: graph.view.gridColor
+			}
+		}		
+		
 		return new mxXmlRequest(EXPORT_URL, 'format=' + format + range + allPages +
 			'&bg=' + ((bg != null) ? bg : mxConstants.NONE) +
 			'&base64=' + base64 + '&embedXml=' + embed + '&xml=' +
 			encodeURIComponent(data) + ((filename != null) ?
 			'&filename=' + encodeURIComponent(filename) : '') +
-			(grid? '&extras=' + encodeURIComponent(JSON.stringify({
-				grid: {
-					size: this.editor.graph.gridSize,
-					steps: this.editor.graph.view.gridSteps,
-					color: this.editor.graph.view.gridColor
-				}
-			})) : '') +
+			'&extras=' + encodeURIComponent(JSON.stringify(extras)) +
 			(scale != null? '&scale=' + scale : '') +
 			(border != null? '&border=' + border : ''));
 	};
@@ -3827,7 +3839,7 @@
 	/**
 	 * 
 	 */
-	EditorUi.prototype.createImageDataUri = function(canvas, xml, format)
+	EditorUi.prototype.createImageDataUri = function(canvas, xml, format, dpi)
 	{
    	    var data = canvas.toDataURL('image/' + format);
    	    
@@ -3842,17 +3854,22 @@
    	    	data = this.writeGraphModelToPng(data, 'tEXt', 'mxfile', encodeURIComponent(xml));
    	    }
    	    
+   	    if (dpi > 0)
+    	{
+   	    	data = this.writeGraphModelToPng(data, 'pHYs', 'dpi', dpi);
+    	}
+   	    
    	    return data;
 	};
 	
 	/**
 	 * 
 	 */
-	EditorUi.prototype.saveCanvas = function(canvas, xml, format, ignorePageName)
+	EditorUi.prototype.saveCanvas = function(canvas, xml, format, ignorePageName, dpi)
 	{
 		var ext = ((format == 'jpeg') ? 'jpg' : format);
 		var filename = this.getBaseFilename(ignorePageName) + '.' + ext;
-   	    var data = this.createImageDataUri(canvas, xml, format);
+   	    var data = this.createImageDataUri(canvas, xml, format, dpi);
    	    this.saveData(filename, ext, data.substring(data.lastIndexOf(',') + 1), 'image/' + format, true);
 	};
 	
@@ -4476,7 +4493,8 @@
 					svgRoot.getElementsByTagName('defs')[0].appendChild(style);
 				}
 				
-				var svg = '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
+				var svg = '<?xml version="1.0" encoding="UTF-8"?>\n' +
+					'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
 					mxUtils.getXml(svgRoot);
 				
 		    		if (this.isLocalFileSave() || svg.length <= MAX_REQUEST_SIZE)
@@ -6016,7 +6034,7 @@
 	/**
 	 *
 	 */
-	EditorUi.prototype.exportImage = function(scale, transparentBackground, ignoreSelection, addShadow, editable, border, noCrop, currentPage, format, grid)
+	EditorUi.prototype.exportImage = function(scale, transparentBackground, ignoreSelection, addShadow, editable, border, noCrop, currentPage, format, grid, dpi)
 	{
 		format = (format != null) ? format : 'png';
 		
@@ -6041,7 +6059,7 @@
 			   		{
 			   			this.saveCanvas(canvas, (editable) ? this.getFileData(true, null,
 			   				null, null, ignoreSelection, currentPage) : null,
-			   				format, !currentPage);
+			   				format, !currentPage, dpi);
 			   		}
 			   		catch (e)
 			   		{
@@ -8280,9 +8298,17 @@
 			{
 				result = f.substring(0, pos - 8);
 				
-				var chunkData = key + String.fromCharCode(0) +
-					((type == 'zTXt') ? String.fromCharCode(0) : '') + 
-					value;
+				if (type == 'pHYs' && key == 'dpi')
+				{
+					var dpm = Math.round(value / 0.0254); //One inch is equal to exactly 0.0254 meters.
+					var chunkData = writeInt(dpm) + writeInt(dpm) + String.fromCharCode(1);
+				}
+				else
+				{
+					var chunkData = key + String.fromCharCode(0) +
+						((type == 'zTXt') ? String.fromCharCode(0) : '') + 
+						value;
+				}
 				
 				var crc = 0xffffffff;
 				crc = this.updateCRC(crc, type, 0, 4);
@@ -8301,7 +8327,7 @@
 		while (n);
 		
 		return 'data:image/png;base64,' + ((window.btoa) ? btoa(result) : Base64.encode(result, true));
-	}
+	};
 	
 	/**
 	 * Extracts the XML from the compressed or non-compressed text chunk.
@@ -8562,7 +8588,7 @@
 		printAction.setEnabled(!mxClient.IS_IOS || !navigator.standalone);
 		printAction.visible = printAction.isEnabled();
 		
-		// Scales pages/graph to fit available size
+		// Installs additional keyboard shortcuts for editor
 		if (!this.editor.chromeless || this.editor.editable)
 		{
 			// Defines additional hotkeys
@@ -8579,249 +8605,8 @@
 		    	this.altShiftActions[83] = 'synchronize'; // Alt+Shift+S
 			}
 		    
-			// Handles copy paste of images from clipboard
-			if (!mxClient.IS_IE)
-			{
-				graph.container.addEventListener('paste', mxUtils.bind(this, function(evt)
-				{
-					var graph = this.editor.graph;
-					
-					if (!mxEvent.isConsumed(evt))
-					{
-						try
-						{
-							var data = (evt.clipboardData || evt.originalEvent.clipboardData);
-							var containsText = false;
-							
-							// Workaround for asynchronous paste event processing in textInput
-							// is to ignore this event if it contains text/html/rtf (see below).
-							// NOTE: Image is not pasted into textInput so can't listen there.
-							for (var i = 0; i < data.types.length; i++)
-							{	
-								if (data.types[i].substring(0, 5) === 'text/')
-								{
-									containsText = true;
-									break;
-								}
-							}
-							
-							if (!containsText)
-							{
-								var items = data.items;
-								
-								for (index in items)
-								{
-									var item = items[index];
-									
-									if (item.kind === 'file')
-									{
-										if (graph.isEditing())
-										{
-										    	this.importFiles([item.getAsFile()], 0, 0, this.maxImageSize, function(data, mimeType, x, y, w, h)
-										    	{
-										    		// Inserts image into current text box
-										    		graph.insertImage(data, w, h);
-										    	}, function()
-										    	{
-										    		// No post processing
-										    	}, function(file)
-										    	{
-										    		// Handles only images
-										    		return file.type.substring(0, 6) == 'image/';
-										    	}, function(queue)
-										    	{
-										    		// Invokes elements of queue in order
-										    		for (var i = 0; i < queue.length; i++)
-										    		{
-										    			queue[i]();
-										    		}
-										    	});
-										}
-										else
-										{
-											var pt = this.editor.graph.getInsertPoint();
-											this.importFiles([item.getAsFile()], pt.x, pt.y, this.maxImageSize);
-											mxEvent.consume(evt);
-										}
-										
-										break;
-									}
-								}
-							}
-						}
-						catch (e)
-						{
-							// ignore
-						}
-					}
-				}), false);
-			}
-
-			// Focused but invisible textarea during control or meta key events
-			var textInput = document.createElement('div');
-			textInput.setAttribute('autocomplete', 'off');
-			textInput.setAttribute('autocorrect', 'off');
-			textInput.setAttribute('autocapitalize', 'off');
-			textInput.setAttribute('spellcheck', 'false');
-			textInput.style.position = 'absolute';
-			textInput.style.whiteSpace = 'nowrap';
-			textInput.style.overflow = 'hidden';
-			textInput.style.display = 'block';
-			textInput.contentEditable = true;
-			mxUtils.setOpacity(textInput, 0);
-			textInput.style.width = '1px';
-			textInput.style.height = '1px';
-			textInput.innerHTML = '&nbsp;';
-
-			var restoreFocus = false;
-			
-			// Disables built-in cut, copy and paste shortcuts
-			this.keyHandler.bindControlKey(88, null);
-			this.keyHandler.bindControlKey(67, null);
-			this.keyHandler.bindControlKey(86, null);
-
-			// Shows a textare when control/cmd is pressed to handle native clipboard actions
-			mxEvent.addListener(document, 'keydown', mxUtils.bind(this, function(evt)
-			{
-				// No dialog visible
-				var source = mxEvent.getSource(evt);
-				
-				if (graph.container != null && graph.isEnabled() && !graph.isMouseDown && !graph.isEditing() &&
-					this.dialog == null && source.nodeName != 'INPUT' && source.nodeName != 'TEXTAREA')
-				{
-					if (evt.keyCode == 224 /* FF */ || (!mxClient.IS_MAC && evt.keyCode == 17 /* Control */) ||
-						(mxClient.IS_MAC && evt.keyCode == 91 /* Meta */))
-					{
-						// Cannot use parentNode for check in IE
-						if (!restoreFocus)
-						{
-							// Avoid autoscroll but allow handling of all pass-through ctrl shortcuts
-							textInput.style.left = (graph.container.scrollLeft + 10) + 'px';
-							textInput.style.top = (graph.container.scrollTop + 10) + 'px';
-							
-							graph.container.appendChild(textInput);
-							restoreFocus = true;
-							
-							// Workaround for selected document content in quirks mode
-							if (mxClient.IS_QUIRKS)
-							{
-								window.setTimeout(function()
-								{
-									textInput.focus();
-									document.execCommand('selectAll', false, null);
-								}, 0);
-							}
-							else
-							{
-								textInput.focus();
-								document.execCommand('selectAll', false, null);
-							}
-						}
-					}
-				}
-			}));
-
-			// Clears input and restores focus and selection
-			function clearInput()
-			{
-				window.setTimeout(function()
-				{
-					textInput.innerHTML = '&nbsp;';
-					textInput.focus();
-					document.execCommand('selectAll', false, null);
-				}, 0);
-			};
-			
-			mxEvent.addListener(document, 'keyup', mxUtils.bind(this, function(evt)
-			{
-				// Workaround for asynchronous event read invalid in IE quirks mode
-				var keyCode = evt.keyCode;
-				
-				// Asynchronous workaround for scroll to origin after paste if the
-				// Ctrl-key is not pressed for long enough in FF on Windows
-				window.setTimeout(mxUtils.bind(this, function()
-				{
-					if (restoreFocus && (keyCode == 224 /* FF */ || keyCode == 17 /* Control */ ||
-						keyCode == 91 /* Meta */))
-					{
-						restoreFocus = false;
-						
-						if (!graph.isEditing() && this.dialog == null && graph.container != null)
-						{
-							graph.container.focus();
-						}
-						
-						textInput.parentNode.removeChild(textInput);
-						
-						// Workaround for lost cursor in focused element
-						if (this.dialog == null)
-						{
-							mxUtils.clearSelection();
-						}
-					}
-				}), 0);
-			}));
-
-			mxEvent.addListener(textInput, 'copy', mxUtils.bind(this, function(evt)
-			{
-				if (graph.isEnabled())
-				{
-					try
-					{
-						mxClipboard.copy(graph);
-						this.copyCells(textInput);
-						clearInput();
-					}
-					catch (e)
-					{
-						this.handleError(e);
-					}
-				}
-			}));
-			
-			mxEvent.addListener(textInput, 'cut', mxUtils.bind(this, function(evt)
-			{
-				if (graph.isEnabled())
-				{
-					try
-					{
-						mxClipboard.copy(graph);
-						this.copyCells(textInput, true);
-						clearInput();
-					}
-					catch (e)
-					{
-						this.handleError(e);
-					}
-				}
-			}));
-			
-			mxEvent.addListener(textInput, 'paste', mxUtils.bind(this, function(evt)
-			{
-				if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
-				{
-					textInput.innerHTML = '&nbsp;';
-					textInput.focus();
-					
-					window.setTimeout(mxUtils.bind(this, function()
-					{
-						this.pasteCells(evt, textInput);
-						textInput.innerHTML = '&nbsp;';
-					}), 0);
-				}
-			}), true);
-			
-			// Needed for IE11
-			var isSelectionAllowed2 = this.isSelectionAllowed;
-			this.isSelectionAllowed = function(evt)
-			{
-				if (mxEvent.getSource(evt) == textInput)
-				{
-					return true;
-				}
-
-				return isSelectionAllowed2.apply(this, arguments);
-			};
+		    this.installImagePasteHandler();
+		    this.installNativeClipboardHandler();
 		};
 
 		var y = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight || 0) / 2;
@@ -8936,39 +8721,10 @@
 			}));
 		}
 		
-		//Add ruler in test mode only
-		//TODO add the ruler containers correctly and make the vertical one dynamic as the side panel size can change
+		//Add ruler with url only
 		if (urlParams['ruler'] == '1' && typeof mxRuler !== 'undefined')
 		{
-			var hRulerDiv = document.createElement('div');
-			hRulerDiv.style.position = 'absolute';
-			hRulerDiv.style.top = '95px';
-			hRulerDiv.style.left = '250px';
-			hRulerDiv.style.width = '2000px';
-			hRulerDiv.style.height = '30px';
-			hRulerDiv.style.background = 'whiteSmoke';
-			document.body.appendChild(hRulerDiv);
-			
-			var vRulerDiv = document.createElement('div');
-			vRulerDiv.style.position = 'absolute';
-			vRulerDiv.style.top = '125px';
-			vRulerDiv.style.left = '220px';
-			vRulerDiv.style.width = '30px';
-			vRulerDiv.style.height = '1000px';
-			vRulerDiv.style.background = 'whiteSmoke';
-			document.body.appendChild(vRulerDiv);
-
-			var square = document.createElement('div');
-			square.style.position = 'absolute';
-			square.style.top = '95px';
-			square.style.left = '220px';
-			square.style.width = '30px';
-			square.style.height = '30px';
-			square.style.background = 'whiteSmoke';
-			document.body.appendChild(square);
-
-			this.vRuler = new mxRuler(this.editor.graph, vRulerDiv, true);
-			this.hRuler = new mxRuler(this.editor.graph, hRulerDiv, false);
+			this.ruler = new mxDualRuler(this, this.editor.graph.view.unit);
 		}
 		
 		// Adds an element to edit the style in the footer in test mode
@@ -9217,6 +8973,263 @@
 		
 		this.installSettings();
 	};
+	
+	/**
+	 * Installs handler for pasting image from clipboard.
+	 */
+	EditorUi.prototype.installImagePasteHandler = function()
+	{
+		if (!mxClient.IS_IE)
+		{
+			var graph = this.editor.graph;
+			
+			graph.container.addEventListener('paste', mxUtils.bind(this, function(evt)
+			{
+				if (!mxEvent.isConsumed(evt))
+				{
+					try
+					{
+						var data = (evt.clipboardData || evt.originalEvent.clipboardData);
+						var containsText = false;
+						
+						// Workaround for asynchronous paste event processing in textInput
+						// is to ignore this event if it contains text/html/rtf (see below).
+						// NOTE: Image is not pasted into textInput so can't listen there.
+						for (var i = 0; i < data.types.length; i++)
+						{	
+							if (data.types[i].substring(0, 5) === 'text/')
+							{
+								containsText = true;
+								break;
+							}
+						}
+						
+						if (!containsText)
+						{
+							var items = data.items;
+							
+							for (index in items)
+							{
+								var item = items[index];
+								
+								if (item.kind === 'file')
+								{
+									if (graph.isEditing())
+									{
+									    	this.importFiles([item.getAsFile()], 0, 0, this.maxImageSize, function(data, mimeType, x, y, w, h)
+									    	{
+									    		// Inserts image into current text box
+									    		graph.insertImage(data, w, h);
+									    	}, function()
+									    	{
+									    		// No post processing
+									    	}, function(file)
+									    	{
+									    		// Handles only images
+									    		return file.type.substring(0, 6) == 'image/';
+									    	}, function(queue)
+									    	{
+									    		// Invokes elements of queue in order
+									    		for (var i = 0; i < queue.length; i++)
+									    		{
+									    			queue[i]();
+									    		}
+									    	});
+									}
+									else
+									{
+										var pt = this.editor.graph.getInsertPoint();
+										this.importFiles([item.getAsFile()], pt.x, pt.y, this.maxImageSize);
+										mxEvent.consume(evt);
+									}
+									
+									break;
+								}
+							}
+						}
+					}
+					catch (e)
+					{
+						// ignore
+					}
+				}
+			}), false);
+		}
+	};
+	
+	/**
+	 * Installs the native clipboard support.
+	 */
+	EditorUi.prototype.installNativeClipboardHandler = function()
+	{
+		var graph = this.editor.graph;
+
+		// Focused but invisible textarea during control or meta key events
+		var textInput = document.createElement('div');
+		textInput.setAttribute('autocomplete', 'off');
+		textInput.setAttribute('autocorrect', 'off');
+		textInput.setAttribute('autocapitalize', 'off');
+		textInput.setAttribute('spellcheck', 'false');
+		textInput.style.position = 'absolute';
+		textInput.style.whiteSpace = 'nowrap';
+		textInput.style.overflow = 'hidden';
+		textInput.style.display = 'block';
+		textInput.contentEditable = true;
+		mxUtils.setOpacity(textInput, 0);
+		textInput.style.width = '1px';
+		textInput.style.height = '1px';
+		textInput.innerHTML = '&nbsp;';
+
+		var restoreFocus = false;
+		
+		// Disables built-in cut, copy and paste shortcuts
+		this.keyHandler.bindControlKey(88, null);
+		this.keyHandler.bindControlKey(67, null);
+		this.keyHandler.bindControlKey(86, null);
+
+		// Shows a textare when control/cmd is pressed to handle native clipboard actions
+		mxEvent.addListener(document, 'keydown', mxUtils.bind(this, function(evt)
+		{
+			// No dialog visible
+			var source = mxEvent.getSource(evt);
+			
+			if (graph.container != null && graph.isEnabled() && !graph.isMouseDown && !graph.isEditing() &&
+				this.dialog == null && source.nodeName != 'INPUT' && source.nodeName != 'TEXTAREA')
+			{
+				if (evt.keyCode == 224 /* FF */ || (!mxClient.IS_MAC && evt.keyCode == 17 /* Control */) ||
+					(mxClient.IS_MAC && evt.keyCode == 91 /* Meta */))
+				{
+					// Cannot use parentNode for check in IE
+					if (!restoreFocus)
+					{
+						// Avoid autoscroll but allow handling of all pass-through ctrl shortcuts
+						textInput.style.left = (graph.container.scrollLeft + 10) + 'px';
+						textInput.style.top = (graph.container.scrollTop + 10) + 'px';
+						
+						graph.container.appendChild(textInput);
+						restoreFocus = true;
+						
+						// Workaround for selected document content in quirks mode
+						if (mxClient.IS_QUIRKS)
+						{
+							window.setTimeout(function()
+							{
+								textInput.focus();
+								document.execCommand('selectAll', false, null);
+							}, 0);
+						}
+						else
+						{
+							textInput.focus();
+							document.execCommand('selectAll', false, null);
+						}
+					}
+				}
+			}
+		}));
+
+		// Clears input and restores focus and selection
+		function clearInput()
+		{
+			window.setTimeout(function()
+			{
+				textInput.innerHTML = '&nbsp;';
+				textInput.focus();
+				document.execCommand('selectAll', false, null);
+			}, 0);
+		};
+		
+		mxEvent.addListener(document, 'keyup', mxUtils.bind(this, function(evt)
+		{
+			// Workaround for asynchronous event read invalid in IE quirks mode
+			var keyCode = evt.keyCode;
+			
+			// Asynchronous workaround for scroll to origin after paste if the
+			// Ctrl-key is not pressed for long enough in FF on Windows
+			window.setTimeout(mxUtils.bind(this, function()
+			{
+				if (restoreFocus && (keyCode == 224 /* FF */ || keyCode == 17 /* Control */ ||
+					keyCode == 91 /* Meta */))
+				{
+					restoreFocus = false;
+					
+					if (!graph.isEditing() && this.dialog == null && graph.container != null)
+					{
+						graph.container.focus();
+					}
+					
+					textInput.parentNode.removeChild(textInput);
+					
+					// Workaround for lost cursor in focused element
+					if (this.dialog == null)
+					{
+						mxUtils.clearSelection();
+					}
+				}
+			}), 0);
+		}));
+
+		mxEvent.addListener(textInput, 'copy', mxUtils.bind(this, function(evt)
+		{
+			if (graph.isEnabled())
+			{
+				try
+				{
+					mxClipboard.copy(graph);
+					this.copyCells(textInput);
+					clearInput();
+				}
+				catch (e)
+				{
+					this.handleError(e);
+				}
+			}
+		}));
+		
+		mxEvent.addListener(textInput, 'cut', mxUtils.bind(this, function(evt)
+		{
+			if (graph.isEnabled())
+			{
+				try
+				{
+					mxClipboard.copy(graph);
+					this.copyCells(textInput, true);
+					clearInput();
+				}
+				catch (e)
+				{
+					this.handleError(e);
+				}
+			}
+		}));
+		
+		mxEvent.addListener(textInput, 'paste', mxUtils.bind(this, function(evt)
+		{
+			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
+			{
+				textInput.innerHTML = '&nbsp;';
+				textInput.focus();
+				
+				window.setTimeout(mxUtils.bind(this, function()
+				{
+					this.pasteCells(evt, textInput);
+					textInput.innerHTML = '&nbsp;';
+				}), 0);
+			}
+		}), true);
+		
+		// Needed for IE11
+		var isSelectionAllowed2 = this.isSelectionAllowed;
+		this.isSelectionAllowed = function(evt)
+		{
+			if (mxEvent.getSource(evt) == textInput)
+			{
+				return true;
+			}
+
+			return isSelectionAllowed2.apply(this, arguments);
+		};
+	};
 
 	/**
 	 * 
@@ -12177,7 +12190,7 @@
 		ExportDialog.showXmlOption = false;
 		ExportDialog.showGifOption = false;
 		
-		ExportDialog.exportFile = function(editorUi, name, format, bg, s, b)
+		ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi)
 		{
 			var graph = editorUi.editor.graph;
 			
@@ -12207,21 +12220,27 @@
 						if (format == 'png')
 						{
 							editorUi.exportImage(s, bg == null || bg == 'none', true,
-						   		false, false, b, true, false);
+						   		false, false, b, true, false, null, null, dpi);
 						}
 						else 
 						{
 							editorUi.exportImage(s, false, true,
-								false, false, b, true, false, 'jpeg');
+								false, false, b, true, false, 'jpeg', null, dpi);
 						}
 					}
 					else 
 					{
+						var extras = {
+							globalVars: graph.getExportVariables()
+						};
+						
 						editorUi.saveRequest(name, format,
 							function(newTitle, base64)
 							{
 								return new mxXmlRequest(EXPORT_URL, 'format=' + format + '&base64=' + (base64 || '0') +
 									((newTitle != null) ? '&filename=' + encodeURIComponent(newTitle) : '') +
+									'&extras=' + encodeURIComponent(JSON.stringify(extras)) +
+									(dpi > 0? '&dpi=' + dpi : '') +
 									'&bg=' + ((bg != null) ? bg : 'none') + '&w=' + w + '&h=' + h +
 									'&border=' + b + '&xml=' + encodeURIComponent(data));
 							});

+ 22 - 35
src/main/webapp/js/diagramly/Menus.js

@@ -981,41 +981,6 @@
 				menu.addSeparator(parent);
 				this.addSubmenu('testDevelop', menu, parent);
 			}
-			
-			if (urlParams['ruler'] == '1')
-			{
-				mxResources.parse('rulerInch=Ruler unit: Inches');
-
-				editorUi.actions.addAction('rulerInch', mxUtils.bind(this, function()
-				{
-					editorUi.vRuler.setUnit(mxRuler.prototype.INCHES);
-					editorUi.hRuler.setUnit(mxRuler.prototype.INCHES);
-					editorUi.vRuler.drawRuler(true);
-					editorUi.hRuler.drawRuler(true);
-				}));
-
-				mxResources.parse('rulerCM=Ruler unit: CMs');
-
-				editorUi.actions.addAction('rulerCM', mxUtils.bind(this, function()
-				{
-					editorUi.vRuler.setUnit(mxRuler.prototype.CENTIMETER);
-					editorUi.hRuler.setUnit(mxRuler.prototype.CENTIMETER);
-					editorUi.vRuler.drawRuler(true);
-					editorUi.hRuler.drawRuler(true);
-				}));
-
-				mxResources.parse('rulerPixel=Ruler unit: Pixels');
-
-				editorUi.actions.addAction('rulerPixel', mxUtils.bind(this, function()
-				{
-					editorUi.vRuler.setUnit(mxRuler.prototype.PIXELS);
-					editorUi.hRuler.setUnit(mxRuler.prototype.PIXELS);
-					editorUi.vRuler.drawRuler(true);
-					editorUi.hRuler.drawRuler(true);
-				}));
-
-				this.addMenuItems(menu, ['-', 'rulerInch', 'rulerCM', 'rulerPixel'], parent);
-			}
 		})));
 		
 		// Only visible in test mode
@@ -2991,6 +2956,28 @@
 			                         'scrollbars', 'tooltips', '-',
 			                         'grid', 'guides'], parent);
 			
+			if (urlParams['ruler'] == '1')
+			{
+				mxResources.parse('ruler=Ruler');
+				
+				var rulerAction = editorUi.actions.addAction('ruler', function()
+				{
+					if (editorUi.ruler != null)
+					{
+						editorUi.ruler.destroy();
+						editorUi.ruler = null;
+					}
+					else
+					{
+						editorUi.ruler = new mxDualRuler(editorUi, editorUi.editor.graph.view.unit);
+					}
+				});
+				rulerAction.setToggleAction(true);
+				rulerAction.setSelectedCallback(function() { return editorUi.ruler != null; });
+						
+				this.addMenuItem(menu, 'ruler', parent);
+			}
+			
 			if (mxClient.IS_SVG && (document.documentMode == null || document.documentMode > 9))
 			{
 				this.addMenuItem(menu, 'shadowVisible', parent);

+ 420 - 0
src/main/webapp/js/diagramly/mxRuler.js

@@ -0,0 +1,420 @@
+/**
+ * Copyright (c) 2017, CTI LOGIC
+ * Copyright (c) 2006-2017, JGraph Ltd
+ * Copyright (c) 2006-2017, Gaudenz Alder
+ * 
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 
+ * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+function mxRuler(editorUi, unit, isVertical, isSecondery) 
+{
+	var RULER_THIKNESS = 30;
+    var ruler = this;
+    this.unit = unit;
+    //create the container
+    var container = document.createElement('div');
+    container.style.position = 'absolute';
+    container.style.background = 'whiteSmoke';
+	document.body.appendChild(container);
+	mxEvent.disableContextMenu(container);
+    	
+	function resizeRulerContainer()
+	{
+	    container.style.top = editorUi.origContTop + 'px';
+	    container.style.left = editorUi.origContLeft + 'px';
+	    container.style.width = (isVertical? RULER_THIKNESS : editorUi.origContWidth) + 'px';
+	    container.style.height = (isVertical? editorUi.origContHeight : RULER_THIKNESS) + 'px';
+	};
+    
+	this.editorUiRefresh = editorUi.refresh;
+	
+	editorUi.refresh = function(minor)
+	{
+		//If it is called with true, then only our added code is executed
+		if (minor != true) 
+		{
+			ruler.editorUiRefresh.apply(editorUi, arguments);
+		}
+		
+		var cont = editorUi.diagramContainer;
+		
+		if (!isSecondery)
+		{
+			editorUi.origContTop = cont.offsetTop;
+			editorUi.origContLeft = cont.offsetLeft;
+			editorUi.origContWidth = cont.offsetWidth;
+			editorUi.origContHeight = cont.offsetHeight;
+		}
+		
+		resizeRulerContainer();
+		
+		if (isVertical)
+		{
+			cont.style.left = (cont.offsetLeft + RULER_THIKNESS) + 'px';
+		}
+		else
+		{
+			cont.style.top = (cont.offsetTop + RULER_THIKNESS) + 'px';
+		}
+	};
+
+	resizeRulerContainer();
+    	
+    var canvas = document.createElement("canvas");
+    //initial sizing which is corrected by the graph size event
+    canvas.width = container.offsetWidth;
+    canvas.height = container.offsetHeight;
+    container.style.overflow = 'hidden';
+    canvas.style.position = "relative";
+    container.appendChild(canvas);
+    //Disable alpha to improve performance as we don't need it
+    var ctx = canvas.getContext("2d", window.uiTheme == 'dark'? {alpha: false} : null);
+    this.ui = editorUi;
+    var graph = editorUi.editor.graph;
+    this.graph = graph;
+    this.container = container;
+    this.canvas = canvas;
+
+    var drawLine = function (x1, y1, x2, y2, text) 
+    {
+        //remove all fractions
+        x1 = Math.round(x1); y1 = Math.round(y1); x2 = Math.round(x2); y2 = Math.round(y2);
+        //adding the 0.5 is necessary to prevent anti-aliasing from making lines thicker!
+        ctx.beginPath();
+        ctx.moveTo(x1 + 0.5, y1 + 0.5);
+        ctx.lineTo(x2 + 0.5, y2 + 0.5);
+        ctx.stroke();
+        
+        if (text) 
+        {
+            if (isVertical) 
+            {
+                var x = x1;
+                var y = y1 - 3;
+                var metric = ctx.measureText(text);
+
+                ctx.save();
+
+                // We want to find the center of the text (or whatever point you want) and rotate about it
+                var tx = x + (metric.width / 2) + 8;
+                var ty = y + 5;
+
+                // Translate to near the center to rotate about the center
+                ctx.translate(tx, ty);
+                // Then rotate...
+                ctx.rotate(-Math.PI / 2);
+                // Then translate back to draw in the right place!
+                ctx.translate(-tx, -ty);
+                ctx.fillText(text, x, y);
+                ctx.restore();
+            }
+            else
+            {
+                ctx.fillText(text, isVertical? x1 : x1 + 3, isVertical? y1 - 3 : y1 + 9);
+            }
+        }
+    };
+    
+    var drawRuler = function() 
+    {
+    	ctx.clearRect(0, 0, canvas.width, canvas.height);
+    	
+        ctx.beginPath();
+        ctx.lineWidth = 0.7;
+        ctx.strokeStyle = "#BBBBBB";
+        ctx.setLineDash([]);
+        ctx.font = "9px Arial";
+        ctx.fillStyle = '#BBBBBB';
+
+        ctx.beginPath();
+        ctx.moveTo(0, 0);
+        ctx.lineTo(RULER_THIKNESS, RULER_THIKNESS);
+        ctx.stroke();
+        
+        var scale = graph.view.scale;
+        var bgPages = graph.view.getBackgroundPageBounds();
+        var t = graph.view.translate;
+        var bounds = graph.view.getGraphBounds();
+        
+        var rStart = (isVertical? bgPages.y -  graph.container.scrollTop : bgPages.x - graph.container.scrollLeft);
+
+        //handle negative pages
+        if (isVertical) 
+        {
+            var y = ((bounds.y) / scale - t.y);
+            
+            if (y <= -1) 
+            {
+                rStart += Math.ceil(Math.abs(y) / graph.pageFormat.height) * graph.pageFormat.height * scale;
+            }
+        }
+        else
+        {
+            var x = ((bounds.x) / scale - t.x);
+            
+            if (x <= -1)
+            {
+                rStart += Math.ceil(Math.abs(x) / graph.pageFormat.width) * graph.pageFormat.width * scale;
+            }
+        }
+        
+        rStart += isVertical? (graph.container.offsetTop - ruler.container.offsetTop) : (graph.container.offsetLeft - ruler.container.offsetLeft);
+
+        var tickStep, tickSize, len;
+
+        switch(ruler.unit) 
+        {
+            case mxConstants.PIXELS:
+                len = 10;
+                tickStep = 10;
+                tickSize = [25,5,5,5,5,10,5,5,5,5];
+                break;
+            case mxConstants.CENTIMETERS:
+                len = 10;
+                tickStep = mxConstants.PIXELS_PER_CM / len;
+                tickSize = [25,5,5,5,5,10,5,5,5,5];
+                break;
+            case mxConstants.INCHES:
+            	if (scale <=0.5 || scale >=4)
+                    len = 8;
+                else
+                    len = 16;
+                
+                tickStep = mxConstants.PIXELS_PER_INCH / len;
+                tickSize = [25,5,8,5,12,5,8,5,15,5,8,5,12,5,8,5];
+                break;
+        }
+        
+        var step = tickStep;
+        
+        if (ruler.unit != mxConstants.INCHES || (scale > 2 || scale < 0.25))
+            step = scale>= 1 ? (tickStep / Math.floor(scale)) : Math.floor(10 / scale / 10) * 10;
+
+        for (var i = RULER_THIKNESS + rStart % (step * scale); i <= (isVertical? canvas.height : canvas.width); i += step * scale) 
+        {
+            var current = Math.round((i - rStart) / scale / step);
+            var text = null;
+            
+            if (current % len == 0) 
+            {
+                text = ruler.formatText(current * step) + "";
+            }
+            
+            if (isVertical) 
+            {
+                drawLine(RULER_THIKNESS - tickSize[Math.abs(current) % len], i, RULER_THIKNESS, i, text);
+            }
+            else
+            {
+                drawLine(i, RULER_THIKNESS - tickSize[Math.abs(current) % len], i, RULER_THIKNESS, text);
+            }
+        }
+    };
+    
+	var sizeListener = function() 
+	{
+	    var div = graph.container;
+	    
+	    if (isVertical)
+    	{
+	    	var newH = div.offsetHeight + RULER_THIKNESS;
+	    	
+	    	if (canvas.height != newH)
+    		{
+	    		canvas.height = newH;
+	    		container.style.height = newH + 'px';
+	    		drawRuler();
+    		}
+    	}
+	    else 
+    	{
+	    	var newW = div.offsetWidth + RULER_THIKNESS;
+	    	
+	    	if (canvas.width != newW)
+    		{
+	    		canvas.width = newW;
+	    		container.style.width = newW + 'px';
+	    		drawRuler();
+    		}
+    	}
+    };
+
+    this.drawRuler = drawRuler;
+
+	var efficientSizeListener = debounce(sizeListener, 10);
+	this.sizeListener = efficientSizeListener;
+	
+	var efficientScrollListener = debounce(function()
+	{
+		var newScroll = isVertical? graph.container.scrollTop : graph.container.scrollLeft;
+		
+		if (ruler.lastScroll != newScroll)
+		{
+			ruler.lastScroll = newScroll;
+			drawRuler();
+		}
+	}, 10);
+
+	this.scrollListener = efficientScrollListener;
+	this.unitListener = function(sender, evt)
+    {
+    	ruler.setUnit(evt.getProperty('unit'));
+    };
+	
+    graph.addListener(mxEvent.SIZE, efficientSizeListener);
+    graph.container.addEventListener("scroll", efficientScrollListener);
+    graph.view.addListener("unitChanged", this.unitListener);
+
+    function debounce(func, wait, immediate) 
+    {
+        var timeout;
+        return function() {
+            var context = this, args = arguments;
+            var later = function() {
+                timeout = null;
+                if (!immediate) func.apply(context, args);
+            };
+            var callNow = immediate && !timeout;
+            clearTimeout(timeout);
+            timeout = setTimeout(later, wait);
+            if (callNow) func.apply(context, args);
+        };
+    };
+    
+    //Showing guides on cell move
+    this.origGuideMove = mxGuide.prototype.move;
+	
+	mxGuide.prototype.move = function (bounds, delta, gridEnabled, clone)
+	{
+		if (ruler.guidePart != null)
+		{
+			ctx.putImageData(ruler.guidePart.imgData, ruler.guidePart.x, ruler.guidePart.y);	
+		}
+		
+		var ret = ruler.origGuideMove.apply(this, arguments);
+
+		var x, y, imgData;
+		ctx.lineWidth = 0.5;
+        ctx.strokeStyle = "#0000BB";
+        ctx.setLineDash([2]);
+
+        if (isVertical)
+		{
+			y = bounds.y + ret.y + RULER_THIKNESS - this.graph.container.scrollTop;
+			x = 0;
+			imgData = ctx.getImageData(x, y, RULER_THIKNESS, 5);
+			drawLine(x, y, RULER_THIKNESS, y);
+		}
+		else
+		{
+			var y = 0;
+			var x = bounds.x + ret.x + RULER_THIKNESS - this.graph.container.scrollLeft;
+			imgData = ctx.getImageData(x , y, 5, RULER_THIKNESS);
+			drawLine(x, y, x, RULER_THIKNESS);
+		}
+		
+		if (ruler.guidePart == null || ruler.guidePart.x != x || ruler.guidePart.y != y)
+		{
+			ruler.guidePart = { 
+				imgData: imgData,
+				x: x,
+				y: y
+			}	
+		}
+		
+		return ret;
+	}
+	
+	this.origGuideDestroy = mxGuide.prototype.destroy;
+	
+	mxGuide.prototype.destroy = function()
+	{
+		var ret = ruler.origGuideDestroy.apply(this, arguments);
+		
+		if (ruler.guidePart != null)
+		{
+			ctx.putImageData(ruler.guidePart.imgData, ruler.guidePart.x, ruler.guidePart.y);	
+		}
+		
+		return ret;
+	};
+	
+	drawRuler();
+	editorUi.refresh(true);
+};
+
+mxRuler.prototype.unit = mxConstants.PIXELS;
+
+mxRuler.prototype.setUnit = function(unit) 
+{
+    this.unit = unit;
+    this.drawRuler();
+};
+
+mxRuler.prototype.formatText = function(pixels) 
+{
+    switch(this.unit) 
+    {
+        case mxConstants.PIXELS:
+            return Math.round(pixels);
+        case mxConstants.CENTIMETERS:
+            return (pixels / mxConstants.PIXELS_PER_CM).toFixed(2);
+        case mxConstants.INCHES:
+            return (pixels / mxConstants.PIXELS_PER_INCH).toFixed(2);
+    }
+};
+
+mxRuler.prototype.destroy = function() 
+{
+	this.ui.refresh = this.editorUiRefresh;
+	mxGuide.prototype.move = this.origGuideMove;
+	mxGuide.prototype.destroy = this.origGuideDestroy;
+    this.graph.removeListener(this.sizeListener);
+    this.graph.container.removeEventListener("scroll", this.scrollListener);
+    this.graph.view.removeListener("unitChanged", this.unitListener);
+    
+    if (this.container != null)
+    {
+    	this.container.parentNode.removeChild(this.container);
+    }
+    
+	this.ui.diagramContainer.style.left = this.ui.origContLeft + 'px';
+	this.ui.diagramContainer.style.top = this.ui.origContTop + 'px';
+};
+
+function mxDualRuler(editorUi, unit)
+{
+	this.editorUiRefresh = editorUi.refresh;
+	this.ui = editorUi;
+	this.origGuideMove = mxGuide.prototype.move;
+	this.origGuideDestroy = mxGuide.prototype.destroy;
+	
+	this.vRuler = new mxRuler(editorUi, unit, true);
+	this.hRuler = new mxRuler(editorUi, unit, false, true);
+};
+
+mxDualRuler.prototype.setUnit = function(unit) 
+{
+	this.vRuler.setUnit(unit);
+	this.hRuler.setUnit(unit);
+};
+
+mxDualRuler.prototype.destroy = function() 
+{
+	this.vRuler.destroy();
+	this.hRuler.destroy();
+	this.ui.refresh = this.editorUiRefresh;
+	mxGuide.prototype.move = this.origGuideMove;
+	mxGuide.prototype.destroy = this.origGuideDestroy;
+};

+ 1 - 1
src/main/webapp/js/diagramly/vsdx/VsdxExport.js

@@ -686,7 +686,7 @@ function VsdxExport(editorUi)
 
 	function writeXmlDoc2Zip(zip, name, xmlDoc, noHeader)
 	{
-		zip.file(name, (noHeader? "" : "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>") + mxUtils.getXml(xmlDoc));
+		zip.file(name, (noHeader? "" : "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>") + mxUtils.getXml(xmlDoc, '\n'));
 	};
 	
 	function addPagesXML(zip, pages, modelsAttr) 

+ 18 - 8
src/main/webapp/js/diagramly/vsdx/mxVsdxCanvas2D.js

@@ -877,7 +877,9 @@ mxVsdxCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, f
 			pStyle = pStyle || {};
 			for (var i=0; i<ch.length; i++) 
 			{
-				if (ch[i].nodeType == 3) 
+				var curCh = ch[i];
+				
+				if (curCh.nodeType == 3) 
 				{ //#text
 					var fontStyle = that.cellState.style["fontStyle"];
 					var styleMap = {
@@ -890,14 +892,22 @@ mxVsdxCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, f
 						underline: pStyle['underline'] || (fontStyle & 4)
 					};
 					
+					var brNext = false;
+					
+					if (i + 1 < ch.length && ch[i + 1].nodeName.toUpperCase() == 'BR')
+					{
+						brNext = true;
+						i++;
+					}
+					
 					//VSDX doesn't have numbered list!
-					createTextRow(styleMap, charSect, pSect, text, (pStyle['OL']? pStyle['LiIndex'] + '. ' : '') + ch[i].textContent);
+					createTextRow(styleMap, charSect, pSect, text, (pStyle['OL']? pStyle['LiIndex'] + '. ' : '') + curCh.textContent + (brNext? '\n' : ''));
 				} 
-				else if (ch[i].nodeType == 1) 
+				else if (curCh.nodeType == 1) 
 				{ //element
-					var nodeName = ch[i].nodeName.toUpperCase();
-					var chLen = ch[i].childNodes.length;
-					var style = window.getComputedStyle(ch[i], null);
+					var nodeName = curCh.nodeName.toUpperCase();
+					var chLen = curCh.childNodes.length;
+					var style = window.getComputedStyle(curCh, null);
 					var styleMap = {
 						bold: style.getPropertyValue('font-weight') == 'bold' || pStyle['bold'],
 						italic: style.getPropertyValue('font-style') == 'italic' || pStyle['italic'],
@@ -936,7 +946,7 @@ mxVsdxCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, f
 					
 					if (chLen > 0)
 					{
-						processNodeChildren(ch[i].childNodes, styleMap);
+						processNodeChildren(curCh.childNodes, styleMap);
 						
 						//Close the UL by adding another pp with no Vullets
 						if (nodeName == "UL")
@@ -957,7 +967,7 @@ mxVsdxCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, f
 					else
 					{
 						//VSDX doesn't have numbered list!
-						createTextRow(styleMap, charSect, pSect, text, (pStyle['OL']? pStyle['LiIndex'] + '. ' : '') + ch[i].textContent);
+						createTextRow(styleMap, charSect, pSect, text, (pStyle['OL']? pStyle['LiIndex'] + '. ' : '') + curCh.textContent);
 					}
 				}
 			}

File diff suppressed because it is too large
+ 158 - 158
src/main/webapp/js/extensions.min.js


+ 1 - 1
src/main/webapp/js/mxgraph/Actions.js

@@ -65,7 +65,7 @@ Actions.prototype.init = function()
 	}).isEnabled = isGraphEnabled;
 	this.addAction('save', function() { ui.saveFile(false); }, null, null, Editor.ctrlKey + '+S').isEnabled = isGraphEnabled;
 	this.addAction('saveAs...', function() { ui.saveFile(true); }, null, null, Editor.ctrlKey + '+Shift+S').isEnabled = isGraphEnabled;
-	this.addAction('export...', function() { ui.showDialog(new ExportDialog(ui).container, 300, 230, true, true); });
+	this.addAction('export...', function() { ui.showDialog(new ExportDialog(ui).container, 300, 260, true, true); });
 	this.addAction('editDiagram...', function()
 	{
 		var dlg = new EditDiagramDialog(ui);

+ 109 - 3
src/main/webapp/js/mxgraph/Dialogs.js

@@ -488,6 +488,7 @@ var FilenameDialog = function(editorUi, filename, buttonText, fn, label, validat
 	};
 
 	td = document.createElement('td');
+	td.style.whiteSpace = 'nowrap';
 	td.appendChild(nameInput);
 	row.appendChild(td);
 	
@@ -1077,6 +1078,97 @@ var ExportDialog = function(editorUi)
 	
 	row = document.createElement('tr');
 	
+	td = document.createElement('td');
+	td.style.fontSize = '10pt';
+	mxUtils.write(td, mxResources.get('dpi', null, 'DPI') + ':');
+	
+	row.appendChild(td);
+	
+	var dpiSelect = document.createElement('select');
+	dpiSelect.style.width = '180px';
+
+	var dpi100Option = document.createElement('option');
+	dpi100Option.setAttribute('value', '100');
+	mxUtils.write(dpi100Option, '100dpi');
+	dpiSelect.appendChild(dpi100Option);
+
+	var dpi200Option = document.createElement('option');
+	dpi200Option.setAttribute('value', '200');
+	mxUtils.write(dpi200Option, '200dpi');
+	dpiSelect.appendChild(dpi200Option);
+	
+	var dpi300Option = document.createElement('option');
+	dpi300Option.setAttribute('value', '300');
+	mxUtils.write(dpi300Option, '300dpi');
+	dpiSelect.appendChild(dpi300Option);
+	
+	var dpi400Option = document.createElement('option');
+	dpi400Option.setAttribute('value', '400');
+	mxUtils.write(dpi400Option, '400dpi');
+	dpiSelect.appendChild(dpi400Option);
+	
+	var dpiCustOption = document.createElement('option');
+	dpiCustOption.setAttribute('value', 'custom');
+	mxUtils.write(dpiCustOption, mxResources.get('custom'));
+	dpiSelect.appendChild(dpiCustOption);
+
+	var customDpi = document.createElement('input');
+	customDpi.style.width = '180px';
+	customDpi.style.display = 'none';
+	customDpi.setAttribute('value', '100');
+	customDpi.setAttribute('type', 'number');
+	customDpi.setAttribute('min', '50');
+	customDpi.setAttribute('step', '50');
+	
+	var zoomUserChanged = false;
+	
+	mxEvent.addListener(dpiSelect, 'change', function()
+	{
+		if (this.value == 'custom')
+		{
+			this.style.display = 'none';
+			customDpi.style.display = '';
+			customDpi.focus();
+		}
+		else
+		{
+			customDpi.value = this.value;
+			
+			if (!zoomUserChanged) 
+			{
+				zoomInput.value = this.value;
+			}
+		}
+	});
+	
+	mxEvent.addListener(customDpi, 'change', function()
+	{
+		var dpi = parseInt(customDpi.value);
+		
+		if (isNaN(dpi) || dpi <= 0)
+		{
+			customDpi.style.backgroundColor = 'red';
+		}
+		else
+		{
+			customDpi.style.backgroundColor = '';
+
+			if (!zoomUserChanged) 
+			{
+				zoomInput.value = dpi;
+			}
+		}	
+	});
+	
+	td = document.createElement('td');
+	td.appendChild(dpiSelect);
+	td.appendChild(customDpi);
+	row.appendChild(td);
+
+	tbody.appendChild(row);
+	
+	row = document.createElement('tr');
+	
 	td = document.createElement('td');
 	td.style.fontSize = '10pt';
 	mxUtils.write(td, mxResources.get('background') + ':');
@@ -1153,6 +1245,17 @@ var ExportDialog = function(editorUi)
 		{
 			transparentCheckbox.setAttribute('disabled', 'disabled');
 		}
+		
+		if (imageFormatSelect.value === 'png')
+		{
+			dpiSelect.removeAttribute('disabled');
+			customDpi.removeAttribute('disabled');
+		}
+		else
+		{
+			dpiSelect.setAttribute('disabled', 'disabled');
+			customDpi.setAttribute('disabled', 'disabled');
+		}
 	};
 	
 	mxEvent.addListener(imageFormatSelect, 'change', formatChanged);
@@ -1181,6 +1284,7 @@ var ExportDialog = function(editorUi)
 
 	mxEvent.addListener(zoomInput, 'change', function()
 	{
+		zoomUserChanged = true;
 		var s = Math.max(0, parseFloat(zoomInput.value) || 100) / 100;
 		zoomInput.value = parseFloat((s * 100).toFixed(2));
 		
@@ -1256,6 +1360,7 @@ var ExportDialog = function(editorUi)
 	    	var s = Math.max(0, parseFloat(zoomInput.value) || 100) / 100;
 			var b = Math.max(0, parseInt(borderInput.value));
 			var bg = graph.background;
+			var dpi = Math.max(1, parseInt(customDpi.value));
 			
 			if ((format == 'svg' || format == 'png') && transparentCheckbox.checked)
 			{
@@ -1267,7 +1372,7 @@ var ExportDialog = function(editorUi)
 			}
 			
 			ExportDialog.lastBorderValue = b;
-			ExportDialog.exportFile(editorUi, name, format, bg, s, b);
+			ExportDialog.exportFile(editorUi, name, format, bg, s, b, dpi);
 		}
 	}));
 	saveBtn.className = 'geBtn gePrimaryBtn';
@@ -1316,7 +1421,7 @@ ExportDialog.showXmlOption = true;
  * parameter and value to be used in the request in the form
  * key=value, where value should be URL encoded.
  */
-ExportDialog.exportFile = function(editorUi, name, format, bg, s, b)
+ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi)
 {
 	var graph = editorUi.editor.graph;
 	
@@ -1358,7 +1463,8 @@ ExportDialog.exportFile = function(editorUi, name, format, bg, s, b)
 			var req = new mxXmlRequest(EXPORT_URL, 'format=' + format +
 				'&filename=' + encodeURIComponent(name) +
 				'&bg=' + ((bg != null) ? bg : 'none') +
-				'&w=' + w + '&h=' + h + '&' + param);
+				'&w=' + w + '&h=' + h + '&' + param +
+				'&dpi=' + dpi);
 			req.simulate(document, '_blank');
 		}
 		else

+ 66 - 0
src/main/webapp/js/mxgraph/Editor.js

@@ -1429,6 +1429,27 @@ var PageSetupDialog = function(editorUi)
 	row.appendChild(td);
 	tbody.appendChild(row);
 	
+	if (urlParams['ruler'] == '1')
+	{
+		row = document.createElement('tr');
+		
+		td = document.createElement('td');
+		td.style.verticalAlign = 'top';
+		td.style.fontSize = '10pt';
+		mxUtils.write(td, mxResources.get('unit', null, 'Unit') + ':');
+		
+		row.appendChild(td);
+		
+		td = document.createElement('td');
+		td.style.verticalAlign = 'top';
+		td.style.fontSize = '10pt';
+		
+		var unitSelect = PageSetupDialog.addUnitPanel(td, graph.view.unit);
+		
+		row.appendChild(td);
+		tbody.appendChild(row);
+	}
+	
 	row = document.createElement('tr');
 	
 	td = document.createElement('td');
@@ -1589,6 +1610,11 @@ var PageSetupDialog = function(editorUi)
 		{
 			graph.model.execute(change);
 		}
+		
+		if (unitSelect != null)
+		{
+			graph.view.setUnit(parseInt(unitSelect.value));
+		}
 	});
 	applyBtn.className = 'geBtn gePrimaryBtn';
 	td.appendChild(applyBtn);
@@ -1899,6 +1925,46 @@ PageSetupDialog.getFormats = function()
 	        {key: 'custom', title: mxResources.get('custom'), format: null}];
 };
 
+PageSetupDialog.addUnitPanel = function(div, unit, unitListener)
+{
+	var unitSelect = document.createElement('select');
+	unitSelect.style.marginBottom = '8px';
+	unitSelect.style.width = '202px';
+
+	var units = PageSetupDialog.getUnits();
+	
+	for (var i = 0; i < units.length; i++)
+	{
+		var u = units[i];
+
+		var unitOption = document.createElement('option');
+		unitOption.setAttribute('value', u.unit);
+		mxUtils.write(unitOption, mxResources.get(u.key, null, u.title));
+		unitSelect.appendChild(unitOption);
+	}
+
+	div.appendChild(unitSelect);
+	
+	unitSelect.value = unit;
+	
+	if (unitListener != null)
+	{
+		mxEvent.addListener(unitSelect, 'change', function(evt)
+		{
+			unitListener(parseInt(unitSelect.value));
+		});
+	}
+	
+	return unitSelect;
+};
+
+PageSetupDialog.getUnits = function()
+{
+	return [{key: 'pixel', title: 'Pixel', unit: mxConstants.PIXELS},
+	        {key: 'inch', title: 'Inch', unit: mxConstants.INCHES},
+	        {key: 'cm', title: 'CM', unit: mxConstants.CENTIMETERS}];
+};
+
 /**
  * Static overrides
  */

+ 117 - 32
src/main/webapp/js/mxgraph/Format.js

@@ -725,7 +725,7 @@ BaseFormatPanel.prototype.createTitle = function(title)
 /**
  * 
  */
-BaseFormatPanel.prototype.createStepper = function(input, update, step, height, disableFocus, defaultValue)
+BaseFormatPanel.prototype.createStepper = function(input, update, step, height, disableFocus, defaultValue, isFloat)
 {
 	step = (step != null) ? step : 1;
 	height = (height != null) ? height : 8;
@@ -765,7 +765,7 @@ BaseFormatPanel.prototype.createStepper = function(input, update, step, height,
 			input.value = defaultValue || '2';
 		}
 		
-		var val = parseInt(input.value);
+		var val = isFloat? parseFloat(input.value) : parseInt(input.value);
 		
 		if (!isNaN(val))
 		{
@@ -787,7 +787,7 @@ BaseFormatPanel.prototype.createStepper = function(input, update, step, height,
 			input.value = defaultValue || '0';
 		}
 		
-		var val = parseInt(input.value);
+		var val = isFloat? parseFloat(input.value) : parseInt(input.value);
 		
 		if (!isNaN(val))
 		{
@@ -1264,7 +1264,7 @@ BaseFormatPanel.prototype.addArrow = function(elt, height)
 /**
  * 
  */
-BaseFormatPanel.prototype.addUnitInput = function(container, unit, right, width, update, step, marginTop, disableFocus)
+BaseFormatPanel.prototype.addUnitInput = function(container, unit, right, width, update, step, marginTop, disableFocus, isFloat)
 {
 	marginTop = (marginTop != null) ? marginTop : 0;
 	
@@ -1276,7 +1276,7 @@ BaseFormatPanel.prototype.addUnitInput = function(container, unit, right, width,
 	input.style.width = width + 'px';
 	container.appendChild(input);
 	
-	var stepper = this.createStepper(input, update, step, null, disableFocus);
+	var stepper = this.createStepper(input, update, step, null, disableFocus, null, isFloat);
 	stepper.style.marginTop = (marginTop - 2) + 'px';
 	stepper.style.right = right + 'px';
 	container.appendChild(stepper);
@@ -1909,11 +1909,67 @@ ArrangePanel.prototype.addAngle = function(div)
 	return div;
 };
 
+ArrangePanel.prototype.getUnit = function()
+{
+	var unit = this.editorUi.editor.graph.view.unit;
+	
+	switch(unit)
+	{
+		case mxConstants.PIXELS:
+			return 'pt';
+		case mxConstants.INCHES:
+			return '"';
+		case mxConstants.CENTIMETERS:
+			return 'cm';
+	}
+};
+
+ArrangePanel.prototype.inUnit = function(pixels)
+{
+	return this.editorUi.editor.graph.view.formatUnitText(pixels);
+};
+
+ArrangePanel.prototype.fromUnit = function(value)
+{
+	var unit = this.editorUi.editor.graph.view.unit;
+	
+	switch(unit)
+	{
+		case mxConstants.PIXELS:
+			return value;
+		case mxConstants.INCHES:
+			return value * mxConstants.PIXELS_PER_INCH;
+		case mxConstants.CENTIMETERS:
+			return value * mxConstants.PIXELS_PER_CM;
+	}
+};
+
+ArrangePanel.prototype.isFloatUnit = function()
+{
+	return this.editorUi.editor.graph.view.unit != mxConstants.PIXELS;
+};
+
+ArrangePanel.prototype.getUnitStep = function()
+{
+	var unit = this.editorUi.editor.graph.view.unit;
+	
+	switch(unit)
+	{
+		case mxConstants.PIXELS:
+			return 1;
+		case mxConstants.INCHES:
+			return 0.1;
+		case mxConstants.CENTIMETERS:
+			return 0.1;
+	}
+};
+
 /**
  * 
  */
 ArrangePanel.prototype.addGeometry = function(container)
 {
+	var panel = this;
 	var ui = this.editorUi;
 	var graph = ui.editor.graph;
 	var rect = this.format.getSelectionState();
@@ -1930,14 +1986,14 @@ ArrangePanel.prototype.addGeometry = function(container)
 	div.appendChild(span);
 
 	var widthUpdate, heightUpdate, leftUpdate, topUpdate;
-	var width = this.addUnitInput(div, 'pt', 84, 44, function()
+	var width = this.addUnitInput(div, this.getUnit(), 84, 44, function()
 	{
 		widthUpdate.apply(this, arguments);
-	});
-	var height = this.addUnitInput(div, 'pt', 20, 44, function()
+	}, this.getUnitStep(), null, null, this.isFloatUnit());
+	var height = this.addUnitInput(div, this.getUnit(), 20, 44, function()
 	{
 		heightUpdate.apply(this, arguments);
-	});
+	}, this.getUnitStep(), null, null, this.isFloatUnit());
 	
 	var autosizeBtn = document.createElement('div');
 	autosizeBtn.className = 'geSprite geSprite-fit';
@@ -1988,7 +2044,7 @@ ArrangePanel.prototype.addGeometry = function(container)
 	{
 		if (geo.width > 0)
 		{
-			var value = Math.max(1, value);
+			var value = Math.max(1, panel.fromUnit(value));
 			
 			if (constrainCheckbox.checked)
 			{
@@ -2002,7 +2058,7 @@ ArrangePanel.prototype.addGeometry = function(container)
 	{
 		if (geo.height > 0)
 		{
-			var value = Math.max(1, value);
+			var value = Math.max(1, panel.fromUnit(value));
 			
 			if (constrainCheckbox.checked)
 			{
@@ -2026,14 +2082,14 @@ ArrangePanel.prototype.addGeometry = function(container)
 	mxUtils.write(span, mxResources.get('position'));
 	div2.appendChild(span);
 	
-	var left = this.addUnitInput(div2, 'pt', 84, 44, function()
+	var left = this.addUnitInput(div2, this.getUnit(), 84, 44, function()
 	{
 		leftUpdate.apply(this, arguments);
-	});
-	var top = this.addUnitInput(div2, 'pt', 20, 44, function()
+	}, this.getUnitStep(), null, null, this.isFloatUnit());
+	var top = this.addUnitInput(div2, this.getUnit(), 20, 44, function()
 	{
 		topUpdate.apply(this, arguments);
-	});
+	}, this.getUnitStep(), null, null, this.isFloatUnit());
 
 	mxUtils.br(div2);
 	this.addLabel(div2, mxResources.get('left'), 84);
@@ -2050,12 +2106,12 @@ ArrangePanel.prototype.addGeometry = function(container)
 			
 			if (force || document.activeElement != width)
 			{
-				width.value = rect.width + ((rect.width == '') ? '' : ' pt');
+				width.value = this.inUnit(rect.width) + ((rect.width == '') ? '' : ' ' + this.getUnit());
 			}
 			
 			if (force || document.activeElement != height)
 			{
-				height.value = rect.height + ((rect.height == '') ? '' : ' pt');
+				height.value = this.inUnit(rect.height) + ((rect.height == '') ? '' : ' ' + this.getUnit());
 			}
 		}
 		else
@@ -2070,12 +2126,12 @@ ArrangePanel.prototype.addGeometry = function(container)
 			
 			if (force || document.activeElement != left)
 			{
-				left.value = rect.x  + ((rect.x == '') ? '' : ' pt');
+				left.value = this.inUnit(rect.x)  + ((rect.x == '') ? '' : ' ' + this.getUnit());
 			}
 			
 			if (force || document.activeElement != top)
 			{
-				top.value = rect.y + ((rect.y == '') ? '' : ' pt');
+				top.value = this.inUnit(rect.y) + ((rect.y == '') ? '' : ' ' + this.getUnit());
 			}
 		}
 		else
@@ -2093,6 +2149,8 @@ ArrangePanel.prototype.addGeometry = function(container)
 	
 	leftUpdate = this.addGeometryHandler(left, function(geo, value)
 	{
+		value = panel.fromUnit(value);
+		
 		if (geo.relative)
 		{
 			geo.offset.x = value;
@@ -2104,6 +2162,8 @@ ArrangePanel.prototype.addGeometry = function(container)
 	});
 	topUpdate = this.addGeometryHandler(top, function(geo, value)
 	{
+		value = panel.fromUnit(value);
+		
 		if (geo.relative)
 		{
 			geo.offset.y = value;
@@ -2125,6 +2185,7 @@ ArrangePanel.prototype.addGeometryHandler = function(input, fn)
 	var ui = this.editorUi;
 	var graph = ui.editor.graph;
 	var initialValue = null;
+	var panel = this;
 	
 	function update(evt)
 	{
@@ -2134,7 +2195,7 @@ ArrangePanel.prototype.addGeometryHandler = function(input, fn)
 
 			if (isNaN(value)) 
 			{
-				input.value = initialValue + ' pt';
+				input.value = initialValue + ' ' + panel.getUnit();
 			}
 			else if (value != initialValue)
 			{
@@ -2165,7 +2226,7 @@ ArrangePanel.prototype.addGeometryHandler = function(input, fn)
 				}
 				
 				initialValue = value;
-				input.value = value + ' pt';
+				input.value = value + ' ' + panel.getUnit();
 			}
 		}
 		
@@ -2633,6 +2694,7 @@ TextFormatPanel.prototype.addFont = function(container)
 			{
 				document.execCommand('superscript', false, null);
 			}, stylePanel3)]);
+		sub.style.marginLeft = '9px';
 		
 		var tmp = stylePanel3.cloneNode(false);
 		tmp.style.paddingTop = '4px';
@@ -3789,29 +3851,34 @@ TextFormatPanel.prototype.addFont = function(container)
 							setSelected(fontStyleItems[1], css.fontStyle == 'italic' ||
 								hasParentOrOnlyChild('I') || hasParentOrOnlyChild('EM'));
 							setSelected(fontStyleItems[2], hasParentOrOnlyChild('U'));
-							setSelected(full, isEqualOrPrefixed(css.textAlign, 'justify'));
 							setSelected(sup, hasParentOrOnlyChild('SUP'));
 							setSelected(sub, hasParentOrOnlyChild('SUB'));
 							
 							if (!graph.cellEditor.isTableSelected())
 							{
 								var align = graph.cellEditor.align || mxUtils.getValue(ss.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER);
-								setSelected(left, align == mxConstants.ALIGN_LEFT);
-								setSelected(center, align == mxConstants.ALIGN_CENTER);
-								setSelected(right, align == mxConstants.ALIGN_RIGHT);
-								
-								setSelected(full, false);
-								full.style.opacity = 0.2;
-								full.style.cursor = 'default';
+
+								if (isEqualOrPrefixed(css.textAlign, 'justify'))
+								{
+									setSelected(full, isEqualOrPrefixed(css.textAlign, 'justify'));
+									setSelected(left, false);
+									setSelected(center, false);
+									setSelected(right, false);
+								}
+								else
+								{
+									setSelected(full, false);
+									setSelected(left, align == mxConstants.ALIGN_LEFT);
+									setSelected(center, align == mxConstants.ALIGN_CENTER);
+									setSelected(right, align == mxConstants.ALIGN_RIGHT);
+								}
 							}
 							else
 							{
+								setSelected(full, isEqualOrPrefixed(css.textAlign, 'justify'));
 								setSelected(left, isEqualOrPrefixed(css.textAlign, 'left'));
 								setSelected(center, isEqualOrPrefixed(css.textAlign, 'center'));
 								setSelected(right, isEqualOrPrefixed(css.textAlign, 'right'));
-								
-								full.style.opacity = 1;
-								full.style.cursor = '';
 							}
 							
 							currentTable = graph.getParentByName(node, 'TABLE', graph.cellEditor.textarea);
@@ -5644,6 +5711,24 @@ DiagramFormatPanel.prototype.addPaperSize = function(div)
 	graph.getModel().addListener(mxEvent.CHANGE, listener);
 	this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }});
 	
+	if (urlParams['ruler'] == '1')
+	{
+		div.appendChild(this.createTitle(mxResources.get('unit', null, 'Unit')));
+		
+		var unitSelect = PageSetupDialog.addUnitPanel(div, graph.view.unit, function(unit)
+		{
+			graph.view.setUnit(unit);
+		});
+		
+		var unitChangeListener = function(sender, evt)
+		{
+			unitSelect.value = evt.getProperty('unit');
+		};
+		
+		graph.view.addListener('unitChanged', unitChangeListener);
+		this.listeners.push({destroy: function() { graph.view.removeListener(unitChangeListener); }});
+	}
+	
 	return div;
 };
 

+ 44 - 4
src/main/webapp/js/mxgraph/Graph.js

@@ -95,6 +95,19 @@ mxGraphView.prototype.minGridSize = 4;
 // UrlParams is null in embed mode
 mxGraphView.prototype.gridColor = '#e0e0e0';
 
+//Units
+mxGraphView.prototype.unit = mxConstants.PIXELS;
+
+mxGraphView.prototype.setUnit = function(unit) 
+{
+	if (this.unit != unit)
+	{
+	    this.unit = unit;
+	    
+	    this.fireEvent(new mxEventObject('unitChanged', 'unit', unit));
+	}
+};
+
 // Alternative text for unsupported foreignObjects
 mxSvgCanvas2D.prototype.foAltText = '[Not supported by viewer]';
 
@@ -3928,7 +3941,8 @@ HoverIcons.prototype.getState = function(state)
  */
 HoverIcons.prototype.update = function(state, x, y)
 {
-	if (!this.graph.connectionArrowsEnabled)
+	if (!this.graph.connectionArrowsEnabled || (state != null &&
+		mxUtils.getValue(state.style, 'allowArrows', '1') == '0'))
 	{
 		this.reset();
 	}
@@ -7714,6 +7728,28 @@ if (typeof mxVertexHandler != 'undefined')
 			return hint;
 		};
 		
+		/**
+		 * Format pixels in the given unit
+		 */
+		function formatHintText(pixels, unit) 
+		{
+		    switch(unit) 
+		    {
+		        case mxConstants.PIXELS:
+		            return pixels;
+		        case mxConstants.CENTIMETERS:
+		            return (pixels / mxConstants.PIXELS_PER_CM).toFixed(2);
+		        case mxConstants.INCHES:
+		            return (pixels / mxConstants.PIXELS_PER_INCH).toFixed(2);
+		    }
+		};
+		
+		
+		mxGraphView.prototype.formatUnitText = function(pixels) 
+		{
+			return pixels? formatHintText(pixels, this.unit) : pixels;
+		};
+		
 		/**
 		 * Updates the hint for the current operation.
 		 */
@@ -7731,8 +7767,9 @@ if (typeof mxVertexHandler != 'undefined')
 				var s = this.graph.view.scale;
 				var x = this.roundLength((this.bounds.x + this.currentDx) / s - t.x);
 				var y = this.roundLength((this.bounds.y + this.currentDy) / s - t.y);
+				var unit = this.graph.view.unit;
 				
-				this.hint.innerHTML = x + ', ' + y;
+				this.hint.innerHTML = formatHintText(x, unit) + ', ' + formatHintText(y, unit);
 	
 				this.hint.style.left = (this.shape.bounds.x + Math.round((this.shape.bounds.width - this.hint.clientWidth) / 2)) + 'px';
 				this.hint.style.top = (this.shape.bounds.y + this.shape.bounds.height + 12) + 'px';
@@ -7817,7 +7854,9 @@ if (typeof mxVertexHandler != 'undefined')
 				else
 				{
 					var s = this.state.view.scale;
-					this.hint.innerHTML = this.roundLength(this.bounds.width / s) + ' x ' + this.roundLength(this.bounds.height / s);
+					var unit = this.state.view.unit;
+					this.hint.innerHTML = formatHintText(this.roundLength(this.bounds.width / s), unit) + ' x ' + 
+											formatHintText(this.roundLength(this.bounds.height / s), unit);
 				}
 				
 				var rot = (this.currentAlpha != null) ? this.currentAlpha : this.state.style[mxConstants.STYLE_ROTATION] || '0';
@@ -7866,8 +7905,9 @@ if (typeof mxVertexHandler != 'undefined')
 			var s = this.graph.view.scale;
 			var x = this.roundLength(point.x / s - t.x);
 			var y = this.roundLength(point.y / s - t.y);
+			var unit = this.graph.view.unit;
 			
-			this.hint.innerHTML = x + ', ' + y;
+			this.hint.innerHTML = formatHintText(x, unit) + ', ' + formatHintText(y, unit);
 			this.hint.style.visibility = 'visible';
 			
 			if (this.isSource || this.isTarget)

+ 20 - 0
src/main/webapp/js/mxgraph/Shapes.js

@@ -2801,6 +2801,26 @@
 		};
 	});
 	
+	// Registers and defines the custom marker
+	mxMarker.addMarker('halfCircle', function(c, shape, type, pe, unitX, unitY, size, source, sw, filled)
+	{
+		var nx = unitX * (size + sw + 1);
+		var ny = unitY * (size + sw + 1);
+		var pt = pe.clone();
+		
+		pe.x -= nx;
+		pe.y -= ny;
+
+		return function()
+		{
+			c.begin();
+			c.moveTo(pt.x - ny, pt.y + nx);
+			c.quadTo(pe.x - ny, pe.y + nx, pe.x, pe.y);
+			c.quadTo(pe.x + ny, pe.y - nx, pt.x + ny, pt.y - nx);
+			c.stroke();
+		};
+	});
+
 	mxMarker.addMarker('async', function(c, shape, type, pe, unitX, unitY, size, source, sw, filled)
 	{
 		// The angle of the forward facing arrow sides against the x axis is

File diff suppressed because it is too large
+ 7 - 2
src/main/webapp/js/mxgraph/Sidebar.js


File diff suppressed because it is too large
+ 955 - 950
src/main/webapp/js/viewer.min.js


+ 3 - 2
src/main/webapp/plugins/cConf-1-4-8.js

@@ -369,7 +369,7 @@ Draw.loadPlugin(function(ui)
 	ui.showLinkDialog = function(value, btnLabel, fn)
 	{
 		var dlg = new LinkDialog(this, value, btnLabel, fn, true);
-		this.showDialog(dlg.container, 480, 165, true, true);
+		this.showDialog(dlg.container, 500, 180, true, true);
 		dlg.init();
 	};
 	
@@ -401,7 +401,8 @@ Draw.loadPlugin(function(ui)
 							url = url.substring(0, hash);
 						}
 						
-						newWin.location = url + '#' + encodeURI(info.title.replace(/\s/g, '') + '-' + anchor.replace(/\s/g, ''));
+						//When page title has a [ at the beginning, conf adds id- to anchor name
+						newWin.location = url + '#' + (info.title.indexOf('[') == 0? 'id-' : '') + encodeURI(info.title.replace(/\s/g, '') + '-' + anchor.replace(/\s/g, ''));
 					}
 				}, function()
 				{

+ 28 - 12
src/main/webapp/plugins/p1.js

@@ -20,26 +20,42 @@ Draw.loadPlugin(function(ui) {
 
     // Adds logo to footer
     ui.footerContainer.innerHTML = '<img width=50px height=17px align="right" style="margin-top:14px;margin-right:12px;" ' + 'src="http://download.esolia.net.s3.amazonaws.com/img/eSolia-Logo-Color.svg"/>';
-
-	// Adds variables in labels (%today, %filename%)
-	var superGetLabel = ui.editor.graph.getLabel;
 	
-	ui.editor.graph.getLabel = function(cell)
+	// Adds placeholder for %today% and %filename%
+    var graph = ui.editor.graph;
+	var graphGetGlobalVariable = graph.getGlobalVariable;
+	
+	graph.getGlobalVariable = function(name)
 	{
-		var result = superGetLabel.apply(this, arguments);
-		
-		if (result != null)
+		if (name == 'today')
+		{
+			return new Date().toLocaleString();
+		}
+		else if (name == 'filename')
 		{
-			var today = new Date().toLocaleString();
 			var file = ui.getCurrentFile();
-			var filename = (file != null && file.getTitle() != null) ? file.getTitle() : '';
 			
-			result = result.replace('%today%', today).replace('%filename%', filename);
+			return (file != null && file.getTitle() != null) ? file.getTitle() : '';
 		}
 		
-		return result;
+		return graphGetGlobalVariable.apply(this, arguments);
 	};
-    
+	
+	// Adds support for exporting PDF with placeholders
+	var graphGetExportVariables = graph.getExportVariables;
+	
+	Graph.prototype.getExportVariables = function()
+	{
+		var vars = graphGetExportVariables;
+		
+		var file = ui.getCurrentFile();
+		
+		vars['today'] = new Date().toLocaleString();
+		vars['filename'] = (file != null && file.getTitle() != null) ? file.getTitle() : '';
+		
+		return vars;
+	};
+	
 //    // Adds resource for action
 //    mxResources.parse('helloWorldAction=Hello, World!');
 //

File diff suppressed because it is too large
+ 1 - 1
src/main/webapp/shortcuts.svg