Gaudenz Alder 6 rokov pred
rodič
commit
8e787dad54

+ 10 - 0
ChangeLog

@@ -1,3 +1,13 @@
+28-MAY-2019: 10.7.0
+
+- Replaces octet-stream with vnd.jgraph.mxfile in Drive
+- Fixes opaque background for server-side PNG export
+- Fixes line jump rendering with child edge labels
+- Fixes loading of large images via Insert dialog
+- Adds support to show/hide layers via tags
+- Adds edge flow to animation plugin
+- Fixes saving of OneDrive libraries
+
 25-MAY-2019: 10.6.9
 
 - Fixes missing VSDX import in stealth mode

+ 1 - 1
VERSION

@@ -1 +1 @@
-10.6.9
+10.7.0

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

@@ -1,7 +1,7 @@
 CACHE MANIFEST
 
 # THIS FILE WAS GENERATED. DO NOT MODIFY!
-# 05/25/2019 01:24 PM
+# 05/28/2019 12:55 PM
 
 app.html
 index.html?offline=1

+ 177 - 15
src/main/webapp/electron.js

@@ -8,6 +8,8 @@ const dialog = electron.dialog
 const app = electron.app
 const BrowserWindow = electron.BrowserWindow
 const globalShortcut = electron.globalShortcut;
+const crc = require('crc');
+const zlib = require('zlib');
 const log = require('electron-log')
 const program = require('commander')
 const {autoUpdater} = require("electron-updater")
@@ -440,8 +442,124 @@ autoUpdater.on('update-available', (a, b) =>
 
 //Pdf export
 const MICRON_TO_PIXEL = 264.58 		//264.58 micron = 1 pixel
+const PNG_CHUNK_IDAT = 1229209940;
+const LARGE_IMAGE_AREA = 30000000;
 
-ipcMain.on('pdf-export', (event, args) =>
+//NOTE: Key length must not be longer than 79 bytes (not checked)
+function writePngWithText(origBuff, key, text, compressed, base64encoded)
+{
+	var inOffset = 0;
+	var outOffset = 0;
+	var data = text;
+	var dataLen = key.length + data.length + 1; //we add 1 zeros with non-compressed data
+	
+	//prepare compressed data to get its size
+	if (compressed)
+	{
+		data = zlib.deflateRawSync(encodeURIComponent(text));
+		dataLen = key.length + data.length + 2; //we add 2 zeros with compressed data
+	}
+	
+	var outBuff = Buffer.allocUnsafe(origBuff.length + dataLen + 4); //4 is the header size "zTXt" or "tEXt"
+	
+	try
+	{
+		var magic1 = origBuff.readUInt32BE(inOffset);
+		inOffset += 4;
+		var magic2 = origBuff.readUInt32BE(inOffset);
+		inOffset += 4;
+		
+		if (magic1 != 0x89504e47 && magic2 != 0x0d0a1a0a)
+		{
+			throw new Error("PNGImageDecoder0");
+		}
+		
+		outBuff.writeUInt32BE(magic1, outOffset);
+		outOffset += 4;
+		outBuff.writeUInt32BE(magic2, outOffset);
+		outOffset += 4;
+	}
+	catch (e)
+	{
+		log.error(e.message, {stack: e.stack});
+		throw new Error("PNGImageDecoder1");
+	}
+
+	try
+	{
+		while (inOffset < origBuff.length)
+		{
+			var length = origBuff.readInt32BE(inOffset);
+			inOffset += 4;
+			var type = origBuff.readInt32BE(inOffset)
+			inOffset += 4;
+
+			if (type == PNG_CHUNK_IDAT)
+			{
+				// Insert zTXt chunk before IDAT chunk
+				outBuff.writeInt32BE(dataLen, outOffset);
+				outOffset += 4;
+				
+				var typeSignature = (compressed) ? "zTXt" : "tEXt";
+				outBuff.write(typeSignature, outOffset);
+				
+				outOffset += 4;
+				outBuff.write(key, outOffset);
+				outOffset += key.length;
+				outBuff.writeInt8(0, outOffset);
+				outOffset ++;
+
+				if (compressed)
+				{
+					outBuff.writeInt8(0, outOffset);
+					outOffset ++;
+					data.copy(outBuff, outOffset);
+				}
+				else
+				{
+					outBuff.write(data, outOffset);	
+				}
+				
+				outOffset += data.length;				
+
+				var crcVal = crc.crc32(typeSignature);
+				crc.crc32(data, crcVal);
+
+				// CRC
+				outBuff.writeInt32BE(crcVal ^ 0xffffffff, outOffset);
+				outOffset += 4;
+
+				// Writes the IDAT chunk after the zTXt
+				outBuff.writeInt32BE(length, outOffset);
+				outOffset += 4;
+				outBuff.writeInt32BE(type, outOffset);
+				outOffset += 4;
+
+				origBuff.copy(outBuff, outOffset, inOffset);
+
+				// Encodes the buffer using base64 if requested
+				return base64encoded? outBuff.toString('base64') : outBuff;
+			}
+
+			outBuff.writeInt32BE(length, outOffset);
+			outOffset += 4;
+			outBuff.writeInt32BE(type, outOffset);
+			outOffset += 4;
+
+			origBuff.copy(outBuff, outOffset, inOffset, inOffset + length + 4);// +4 to move past the crc
+			
+			inOffset += length + 4;
+			outOffset += length + 4;
+		}
+	}
+	catch (e)
+	{
+		log.error(e.message, {stack: e.stack});
+		throw e;
+	}
+}
+
+ipcMain.on('export', (event, args) =>
 {
 	var browser = null;
 	
@@ -449,9 +567,13 @@ ipcMain.on('pdf-export', (event, args) =>
 	{
 		browser = new BrowserWindow({
 			webPreferences: {
+				backgroundThrottling: false,
 				nodeIntegration: true
 			},
 			show : false,
+			frame: false,
+			enableLargerThanScreen: true,
+			transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'),
 			parent: windowsRegistry[0] //set parent to first opened window. Not very accurate, but useful when all visible windows are closed
 		});
 
@@ -461,9 +583,9 @@ ipcMain.on('pdf-export', (event, args) =>
 		
 		contents.on('did-finish-load', function()
 	    {
-			browser.webContents.send('render', {
+			contents.send('render', {
 				xml: args.xml,
-				format: 'pdf',
+				format: args.format,
 				w: args.w,
 				h: args.h,
 				border: args.border || 0,
@@ -491,8 +613,6 @@ ipcMain.on('pdf-export', (event, args) =>
 					// Increase this if more cropped PDFs have extra empty pages
 					var h = Math.ceil(bounds.height * fixingScale + 0.1);
 	
-					//page.setViewport({width: w, height: h});
-					
 					pdfOptions = {
 						printBackground: true,
 						pageSize : {
@@ -503,17 +623,59 @@ ipcMain.on('pdf-export', (event, args) =>
 					}
 				}
 				
-				contents.printToPDF(pdfOptions, (error, data) => 
+				var base64encoded = args.base64 == '1';
+				
+				if (args.format == 'png' || args.format == 'jpg' || args.format == 'jpeg')
 				{
-					if (error)
+					browser.setBounds({width: Math.ceil(bounds.width + bounds.x), height: Math.ceil(bounds.height + bounds.y)});
+					
+					//TODO The browser takes sometime to show the graph (also after resize it takes some time to render)
+					//	 	1 sec is most probably enough (for small images, 5 for large ones) BUT not a stable solution
+					setTimeout(function()
 					{
-						event.reply('pdf-export-error', error);
-					}
-					else
+						browser.capturePage().then(function(img)
+						{
+							//Image is double the given bounds, so resize is needed!
+							img = img.resize({width: args.w || Math.ceil(bounds.width + bounds.x), height: args.h || Math.ceil(bounds.height + bounds.y)});
+
+							var data = args.format == 'png'? img.toPNG() : img.toJPEG(90);
+							
+							if (args.embedXml == "1" && args.format == 'png')
+							{
+								data = writePngWithText(data, "mxGraphModel", args.xml, true,
+										base64encoded);
+							}
+							else
+							{
+								if (base64encoded)
+								{
+									data = data.toString('base64');
+								}
+
+								if (data.length == 0)
+								{
+									throw new Error("Invalid image");
+								}
+							}
+							
+							event.reply('export-success', data);
+						});
+					}, bounds.width * bounds.height < LARGE_IMAGE_AREA? 1000 : 5000);
+				}
+				else if (args.format == 'pdf')
+				{
+					contents.printToPDF(pdfOptions, (error, data) => 
 					{
-						event.reply('pdf-export-success', data);
-					}
-				})
+						if (error)
+						{
+							event.reply('export-error', error);
+						}
+						else
+						{
+							event.reply('export-success', data);
+						}
+					})
+				}
 				
 				//Destroy the window after 30 sec which is more than enough (test with 1 sec works)
 				setTimeout(function()
@@ -530,7 +692,7 @@ ipcMain.on('pdf-export', (event, args) =>
 			browser.destroy();
 		}
 
-		event.reply('pdf-export-error', e);
-		console.log('pdf-export-error', e);
+		event.reply('export-error', e);
+		console.log('export-error', e);
 	}
 })

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 268 - 267
src/main/webapp/js/app.min.js


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

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

+ 13 - 2
src/main/webapp/js/diagramly/Dialogs.js

@@ -6389,7 +6389,7 @@ var TagsWindow = function(editorUi, x, y, w, h)
 
 	function searchCells(cells)
 	{
-		return graph.getCellsForTags(searchInput.value.split(' '), cells, propertyName);
+		return graph.getCellsForTags(searchInput.value.split(' '), cells, propertyName, true);
 	};
 
 	function setCellsVisible(cells, visible)
@@ -6418,7 +6418,18 @@ var TagsWindow = function(editorUi, x, y, w, h)
 		
 		if (graph.isEnabled())
 		{
-			graph.setSelectionCells(cells);
+			// Ignores layers for selection
+			var temp = [];
+			
+			for (var i = 0; i < cells.length; i++)
+			{
+				if (graph.model.isVertex(cells[i]) || graph.model.isEdge(cells[i]))
+				{
+					temp.push(cells[i]);
+				}
+			}
+			
+			graph.setSelectionCells(temp);
 		}
 		else
 		{

+ 7 - 2
src/main/webapp/js/diagramly/DriveClient.js

@@ -1183,6 +1183,11 @@ DriveClient.prototype.saveFile = function(file, revision, success, errFn, noChec
 						revision = true;
 						pinned = true;
 					}
+					// Overrides mime type for unknown file type uploads
+					else if (meta.mimeType == 'application/octet-stream')
+					{
+						meta.mimeType = this.xmlMimeType;
+					}
 					
 					if (file.constructor == DriveFile)
 					{
@@ -1261,8 +1266,8 @@ DriveClient.prototype.saveFile = function(file, revision, success, errFn, noChec
 									reasons.push('stale revision');
 								}
 								
-								var temp = ': ' + reasons.join(', ');
-								error({message: mxResources.get('errorSavingFile') + temp}, resp);
+								var temp = reasons.join(', ');
+								error({message: mxResources.get('errorSavingFile') + ': ' + temp}, resp);
 								
 								// Logs failed save
 								try

+ 8 - 7
src/main/webapp/js/diagramly/Editor.js

@@ -3754,17 +3754,17 @@
 
 		if (action.toggle != null)
 		{
-			this.toggleCells(this.getCellsForAction(action.toggle));
+			this.toggleCells(this.getCellsForAction(action.toggle, true));
 		}
 		
 		if (action.show != null)
 		{
-			this.setCellsVisible(this.getCellsForAction(action.show), true);
+			this.setCellsVisible(this.getCellsForAction(action.show, true), true);
 		}
 		
 		if (action.hide != null)
 		{
-			this.setCellsVisible(this.getCellsForAction(action.hide), false);
+			this.setCellsVisible(this.getCellsForAction(action.hide, true), false);
 		}
 
 		if (action.scroll != null)
@@ -3782,10 +3782,10 @@
 	 * Handles each action in the action array of a custom link. This code
 	 * handles toggle actions for cell IDs.
 	 */
-	Graph.prototype.getCellsForAction = function(action)
+	Graph.prototype.getCellsForAction = function(action, includeLayers)
 	{
 		return this.getCellsById(action.cells).concat(
-			this.getCellsForTags(action.tags));
+			this.getCellsForTags(action.tags, null, null, includeLayers));
 	};
 	
 	/**
@@ -3828,7 +3828,7 @@
 	 * Returns the cells in the model (or given array) that have all of the
 	 * given tags in their tags property.
 	 */
-	Graph.prototype.getCellsForTags = function(tagList, cells, propertyName)
+	Graph.prototype.getCellsForTags = function(tagList, cells, propertyName, includeLayers)
 	{
 		var result = [];
 		
@@ -3839,7 +3839,8 @@
 			
 			for (var i = 0; i < cells.length; i++)
 			{
-				if (this.model.isVertex(cells[i]) || this.model.isEdge(cells[i]))
+				if ((includeLayers && this.model.getParent(cells[i]) == this.model.root) ||
+					this.model.isVertex(cells[i]) || this.model.isEdge(cells[i]))
 				{
 					var tags = (cells[i].value != null && typeof(cells[i].value) == 'object') ?
 						mxUtils.trim(cells[i].value.getAttribute(propertyName) || '') : '';

+ 246 - 215
src/main/webapp/js/diagramly/EditorUi.js

@@ -1729,7 +1729,11 @@
 		{
 			bg = mxConstants.NONE;
 		}
-       	
+		else if (!transparent && (bg == null || bg == mxConstants.NONE))
+		{
+			bg = '#ffffff';
+		}
+		
 		return new mxXmlRequest(EXPORT_URL, 'format=' + format + range + allPages +
 			'&bg=' + ((bg != null) ? bg : mxConstants.NONE) +
 			'&base64=' + base64 + '&embedXml=' + embed + '&xml=' +
@@ -7427,219 +7431,219 @@
 							{
 								if (filterFn == null || filterFn(file))
 								{
-							    		if (file.type.substring(0, 6) == 'image/')
-							    		{
-							    			if (file.type.substring(0, 9) == 'image/svg')
-							    			{
-							    				// Checks if SVG contains content attribute
-						    					var data = e.target.result;
-						    					var comma = data.indexOf(',');
-						    					var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1))));
-						    					var root = mxUtils.parseXml(svgText);
-					    						var svgs = root.getElementsByTagName('svg');
-					    						
-					    						if (svgs.length > 0)
+						    		if (file.type.substring(0, 6) == 'image/')
+						    		{
+						    			if (file.type.substring(0, 9) == 'image/svg')
+						    			{
+						    				// Checks if SVG contains content attribute
+					    					var data = e.target.result;
+					    					var comma = data.indexOf(',');
+					    					var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1))));
+					    					var root = mxUtils.parseXml(svgText);
+				    						var svgs = root.getElementsByTagName('svg');
+				    						
+				    						if (svgs.length > 0)
+					    					{
+				    							var svgRoot = svgs[0];
+						    					var cont = (ignoreEmbeddedXml) ? null : svgRoot.getAttribute('content');
+		
+						    					if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%')
 						    					{
-					    							var svgRoot = svgs[0];
-							    					var cont = (ignoreEmbeddedXml) ? null : svgRoot.getAttribute('content');
-			
-							    					if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%')
-							    					{
-							    						cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true));
-							    					}
-							    					
-							    					if (cont != null && cont.charAt(0) == '%')
-							    					{
-							    						cont = decodeURIComponent(cont);
-							    					}
-			
-							    					if (cont != null && (cont.substring(0, 8) === '<mxfile ' ||
-							    						cont.substring(0, 14) === '<mxGraphModel '))
-							    					{
-							    						barrier(index, mxUtils.bind(this, function()
-									    				{
-									    					return fn(cont, 'text/xml', x + index * gs, y + index * gs, 0, 0, file.name);	
-									    				}));
-							    					}
-							    					else
-							    					{
-									    				// SVG needs special handling to add viewbox if missing and
-									    				// find initial size from SVG attributes (only for IE11)
-									    				barrier(index, mxUtils.bind(this, function()
-									    				{
-								    						try
-								    						{
-										    					var prefix = data.substring(0, comma + 1);
-										    					
-										    					// Parses SVG and find width and height
-										    					if (root != null)
-										    					{
-										    						var svgs = root.getElementsByTagName('svg');
-										    						
-										    						if (svgs.length > 0)
-											    					{
-										    							var svgRoot = svgs[0];
-											    						var w = parseFloat(svgRoot.getAttribute('width'));
-											    						var h = parseFloat(svgRoot.getAttribute('height'));
-											    						
-											    						// Check if viewBox attribute already exists
-											    						var vb = svgRoot.getAttribute('viewBox');
-											    						
-											    						if (vb == null || vb.length == 0)
-											    						{
-											    							svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
-											    						}
-											    						// Uses width and height from viewbox for
-											    						// missing width and height attributes
-											    						else if (isNaN(w) || isNaN(h))
-											    						{
-											    							var tokens = vb.split(' ');
-											    							
-											    							if (tokens.length > 3)
-											    							{
-											    								w = parseFloat(tokens[2]);
-											    								h = parseFloat(tokens[3]);
-											    							}
-											    						}
-		
-											    						data = this.createSvgDataUri(mxUtils.getXml(svgRoot));
-											    						var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h));
-											    						var cells = fn(data, file.type, x + index * gs, y + index * gs, Math.max(
-											    							1, Math.round(w * s)), Math.max(1, Math.round(h * s)), file.name);
-											    						
-											    						// Hack to fix width and height asynchronously
-											    						if (isNaN(w) || isNaN(h))
-											    						{
-											    							var img = new Image();
-											    							
-											    							img.onload = mxUtils.bind(this, function()
-											    							{
-											    								w = Math.max(1, img.width);
-											    								h = Math.max(1, img.height);
-											    								
-											    								cells[0].geometry.width = w;
-											    								cells[0].geometry.height = h;
-											    								
-											    								svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
-											    								data = this.createSvgDataUri(mxUtils.getXml(svgRoot));
-											    								
-											    								var semi = data.indexOf(';');
-											    								
-											    								if (semi > 0)
-											    								{
-											    									data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1));
-											    								}
-											    								
-											    								graph.setCellStyles('image', data, [cells[0]]);
-											    							});
-											    							
-											    							img.src = this.createSvgDataUri(mxUtils.getXml(svgRoot));
-											    						}
-											    						
-											    						return cells;
-											    					}
-										    					}
-								    						}
-								    						catch (e)
-								    						{
-								    							// ignores any SVG parsing errors
-								    						}
-									    					
-									    					return null;
-									    				}));
-							    					}
+						    						cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true));
 						    					}
-					    						else
-					    						{
+						    					
+						    					if (cont != null && cont.charAt(0) == '%')
+						    					{
+						    						cont = decodeURIComponent(cont);
+						    					}
+		
+						    					if (cont != null && (cont.substring(0, 8) === '<mxfile ' ||
+						    						cont.substring(0, 14) === '<mxGraphModel '))
+						    					{
 						    						barrier(index, mxUtils.bind(this, function()
 								    				{
+								    					return fn(cont, 'text/xml', x + index * gs, y + index * gs, 0, 0, file.name);	
+								    				}));
+						    					}
+						    					else
+						    					{
+								    				// SVG needs special handling to add viewbox if missing and
+								    				// find initial size from SVG attributes (only for IE11)
+								    				barrier(index, mxUtils.bind(this, function()
+								    				{
+							    						try
+							    						{
+									    					var prefix = data.substring(0, comma + 1);
+									    					
+									    					// Parses SVG and find width and height
+									    					if (root != null)
+									    					{
+									    						var svgs = root.getElementsByTagName('svg');
+									    						
+									    						if (svgs.length > 0)
+										    					{
+									    							var svgRoot = svgs[0];
+										    						var w = parseFloat(svgRoot.getAttribute('width'));
+										    						var h = parseFloat(svgRoot.getAttribute('height'));
+										    						
+										    						// Check if viewBox attribute already exists
+										    						var vb = svgRoot.getAttribute('viewBox');
+										    						
+										    						if (vb == null || vb.length == 0)
+										    						{
+										    							svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
+										    						}
+										    						// Uses width and height from viewbox for
+										    						// missing width and height attributes
+										    						else if (isNaN(w) || isNaN(h))
+										    						{
+										    							var tokens = vb.split(' ');
+										    							
+										    							if (tokens.length > 3)
+										    							{
+										    								w = parseFloat(tokens[2]);
+										    								h = parseFloat(tokens[3]);
+										    							}
+										    						}
+	
+										    						data = this.createSvgDataUri(mxUtils.getXml(svgRoot));
+										    						var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h));
+										    						var cells = fn(data, file.type, x + index * gs, y + index * gs, Math.max(
+										    							1, Math.round(w * s)), Math.max(1, Math.round(h * s)), file.name);
+										    						
+										    						// Hack to fix width and height asynchronously
+										    						if (isNaN(w) || isNaN(h))
+										    						{
+										    							var img = new Image();
+										    							
+										    							img.onload = mxUtils.bind(this, function()
+										    							{
+										    								w = Math.max(1, img.width);
+										    								h = Math.max(1, img.height);
+										    								
+										    								cells[0].geometry.width = w;
+										    								cells[0].geometry.height = h;
+										    								
+										    								svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
+										    								data = this.createSvgDataUri(mxUtils.getXml(svgRoot));
+										    								
+										    								var semi = data.indexOf(';');
+										    								
+										    								if (semi > 0)
+										    								{
+										    									data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1));
+										    								}
+										    								
+										    								graph.setCellStyles('image', data, [cells[0]]);
+										    							});
+										    							
+										    							img.src = this.createSvgDataUri(mxUtils.getXml(svgRoot));
+										    						}
+										    						
+										    						return cells;
+										    					}
+									    					}
+							    						}
+							    						catch (e)
+							    						{
+							    							// ignores any SVG parsing errors
+							    						}
+								    					
 								    					return null;
 								    				}));
-					    						}
-							    			}
-							    			else
-							    			{
-							    				// Checks if PNG+XML is available to bypass code below
-							    				var containsModel = false;
-							    				
-							    				if (file.type == 'image/png')
+						    					}
+					    					}
+				    						else
+				    						{
+					    						barrier(index, mxUtils.bind(this, function()
 							    				{
-							    					var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(e.target.result);
-							    					
-							    					if (xml != null && xml.length > 0)
-							    					{
-							    						var img = new Image();
-							    						img.src = e.target.result;
-							    						
-									    				barrier(index, mxUtils.bind(this, function()
+							    					return null;
+							    				}));
+				    						}
+						    			}
+						    			else
+						    			{
+						    				// Checks if PNG+XML is available to bypass code below
+						    				var containsModel = false;
+						    				
+						    				if (file.type == 'image/png')
+						    				{
+						    					var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(e.target.result);
+						    					
+						    					if (xml != null && xml.length > 0)
+						    					{
+						    						var img = new Image();
+						    						img.src = e.target.result;
+						    						
+								    				barrier(index, mxUtils.bind(this, function()
+								    				{
+								    					return fn(xml, 'text/xml', x + index * gs, y + index * gs,
+								    						img.width, img.height, file.name);	
+								    				}));
+						    						
+						    						containsModel = true;
+						    					}
+						    				}
+						    				
+							    			// Additional asynchronous step for finding image size
+						    				if (!containsModel)
+						    				{
+						    					// Cannot load local files in Chrome App
+						    					if (mxClient.IS_CHROMEAPP)
+						    					{
+						    						this.spinner.stop();
+						    						this.showError(mxResources.get('error'), mxResources.get('dragAndDropNotSupported'),
+						    							mxResources.get('cancel'), mxUtils.bind(this, function()
+					    								{
+					    									// Hides the dialog
+					    								}), null, mxResources.get('ok'), mxUtils.bind(this, function()
+					    								{
+						    								// Redirects to import function
+					    									this.actions.get('import').funct();
+					    								})
+					    							);
+						    					}
+						    					else
+						    					{
+									    			this.loadImage(e.target.result, mxUtils.bind(this, function(img)
+									    			{
+									    				this.resizeImage(img, e.target.result, mxUtils.bind(this, function(data2, w2, h2)
 									    				{
-									    					return fn(xml, 'text/xml', x + index * gs, y + index * gs,
-									    						img.width, img.height, file.name);	
-									    				}));
-							    						
-							    						containsModel = true;
-							    					}
-							    				}
-							    				
-								    			// Additional asynchronous step for finding image size
-							    				if (!containsModel)
-							    				{
-							    					// Cannot load local files in Chrome App
-							    					if (mxClient.IS_CHROMEAPP)
-							    					{
-							    						this.spinner.stop();
-							    						this.showError(mxResources.get('error'), mxResources.get('dragAndDropNotSupported'),
-							    							mxResources.get('cancel'), mxUtils.bind(this, function()
-						    								{
-						    									// Hides the dialog
-						    								}), null, mxResources.get('ok'), mxUtils.bind(this, function()
-						    								{
-							    								// Redirects to import function
-						    									this.actions.get('import').funct();
-						    								})
-						    							);
-							    					}
-							    					else
-							    					{
-										    			this.loadImage(e.target.result, mxUtils.bind(this, function(img)
-										    			{
-										    				this.resizeImage(img, e.target.result, mxUtils.bind(this, function(data2, w2, h2)
-										    				{
-											    				barrier(index, mxUtils.bind(this, function()
-													    		{
-											    					// Refuses to insert images above a certain size as they kill the app
-											    					if (data2 != null && data2.length < maxBytes)
-											    					{
-												    					var s = (!resizeImages || !this.isResampleImage(e.target.result, resampleThreshold)) ? 1 : Math.min(1, Math.min(maxSize / w2, maxSize / h2));
-													    				
-												    					return fn(data2, file.type, x + index * gs, y + index * gs, Math.round(w2 * s), Math.round(h2 * s), file.name);
-											    					}
-											    					else
-											    					{
-											    						this.handleError({message: mxResources.get('imageTooBig')});
-											    						
-											    						return null;
-											    					}
-													    		}));
-										    				}), resizeImages, maxSize, resampleThreshold);
-										    			}), mxUtils.bind(this, function()
-										    			{
-										    				this.handleError({message: mxResources.get('invalidOrMissingFile')});
-										    			}));
-							    					}
-							    				}
-							    			}
-							    		}
-							    		else
-							    		{
-											fn(e.target.result, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells)
-											{
-												barrier(index, function()
-					    	    				{
-					    		    				return cells;
-					    	    				});
-											});
-							    		}
+										    				barrier(index, mxUtils.bind(this, function()
+												    		{
+										    					// Refuses to insert images above a certain size as they kill the app
+										    					if (data2 != null && data2.length < maxBytes)
+										    					{
+											    					var s = (!resizeImages || !this.isResampleImage(e.target.result, resampleThreshold)) ? 1 : Math.min(1, Math.min(maxSize / w2, maxSize / h2));
+												    				
+											    					return fn(data2, file.type, x + index * gs, y + index * gs, Math.round(w2 * s), Math.round(h2 * s), file.name);
+										    					}
+										    					else
+										    					{
+										    						this.handleError({message: mxResources.get('imageTooBig')});
+										    						
+										    						return null;
+										    					}
+												    		}));
+									    				}), resizeImages, maxSize, resampleThreshold);
+									    			}), mxUtils.bind(this, function()
+									    			{
+									    				this.handleError({message: mxResources.get('invalidOrMissingFile')});
+									    			}));
+						    					}
+						    				}
+						    			}
+						    		}
+						    		else
+						    		{
+										fn(e.target.result, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells)
+										{
+											barrier(index, function()
+				    	    				{
+				    		    				return cells;
+				    	    				});
+										});
+						    		}
 								}
 							});
 							
@@ -7670,6 +7674,16 @@
 		
 		if (largeImages)
 		{
+			// Workaround for lost files array in async code
+			var tmp = [];
+			
+			for (var i = 0; i < files.length; i++)
+			{
+				tmp.push(files[i]);
+			}
+			
+			files = tmp;
+			
 			this.confirmImageResize(function(doResize)
 			{
 				resizeImages = doResize;
@@ -11792,22 +11806,39 @@
 			}
 		    else
 		    {
-		    		var data = editorUi.getFileData(true, null, null, null, null, true);
-		    		var bounds = graph.getGraphBounds();
+		    	var data = editorUi.getFileData(true, null, null, null, null, true);
+		    	var bounds = graph.getGraphBounds();
 				var w = Math.floor(bounds.width * s / graph.view.scale);
 				var h = Math.floor(bounds.height * s / graph.view.scale);
 				
 				if (data.length <= MAX_REQUEST_SIZE && w * h < MAX_AREA)
 				{
 					editorUi.hideDialog();
-					editorUi.saveRequest(name, format,
-						function(newTitle, base64)
+					
+					if ((format == 'png' || format == 'jpg' || format == 'jpeg') && editorUi.isExportToCanvas())
+					{
+						if (format == 'png')
 						{
-							return new mxXmlRequest(EXPORT_URL, 'format=' + format + '&base64=' + (base64 || '0') +
-								((newTitle != null) ? '&filename=' + encodeURIComponent(newTitle) : '') +
-								'&bg=' + ((bg != null) ? bg : 'none') + '&w=' + w + '&h=' + h +
-								'&border=' + b + '&xml=' + encodeURIComponent(data));
-						});
+							editorUi.exportImage(s, bg == null || bg == 'none', true,
+						   		false, false, b, true, false);
+						}
+						else 
+						{
+							editorUi.exportImage(s, false, true,
+								false, false, b, true, false, 'jpeg');
+						}
+					}
+					else 
+					{
+						editorUi.saveRequest(name, format,
+							function(newTitle, base64)
+							{
+								return new mxXmlRequest(EXPORT_URL, 'format=' + format + '&base64=' + (base64 || '0') +
+									((newTitle != null) ? '&filename=' + encodeURIComponent(newTitle) : '') +
+									'&bg=' + ((bg != null) ? bg : 'none') + '&w=' + w + '&h=' + h +
+									'&border=' + b + '&xml=' + encodeURIComponent(data));
+							});
+					}
 				}
 				else
 				{

+ 62 - 38
src/main/webapp/js/diagramly/ElectronApp.js

@@ -1055,43 +1055,65 @@ FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
 	if (mxIsElectron5)
 	{
 		//Direct export to pdf
-		var origCreateDownloadRequest = EditorUi.prototype.createDownloadRequest;
-		
 		EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64, transparent, currentPage)
 		{
-			if (format == 'pdf')
+			var bounds = this.editor.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
+			// the from/to URL parameters to specify the page to be exported.
+			var data = this.getFileData(true, null, null, null, ignoreSelection, currentPage == false? false : format != 'xmlpng');
+			var range = null;
+			var allPages = null;
+			
+			if (bounds.width * bounds.height > MAX_AREA || data.length > MAX_REQUEST_SIZE)
 			{
-				var bounds = this.editor.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
-				// the from/to URL parameters to specify the page to be exported.
-				var data = this.getFileData(true, null, null, null, ignoreSelection, currentPage == false? false : format != 'xmlpng');
-				var allPages = null;
-				
-				if (bounds.width * bounds.height > MAX_AREA || data.length > MAX_REQUEST_SIZE)
-				{
-					throw {message: mxResources.get('drawingTooLarge')};
-				}
-				
-				if (currentPage == false)
-				{
-					allPages = '1';
-				}
-				
-				var bg = this.editor.graph.background;
-				
-				return new mxElectronRequest('pdf-export', {
-					xml: data,
-					bg: (bg != null) ? bg : mxConstants.NONE,
-					filename: (filename != null) ? filename : null,
-					allPages: allPages
-				});
+				throw {message: mxResources.get('drawingTooLarge')};
 			}
-			else
+			
+			var embed = '0';
+			
+			if (format == 'pdf' && currentPage == false)
 			{
-				return origCreateDownloadRequest.apply(this, arguments);
+				allPages = '1';
 			}
+			
+			if (format == 'xmlpng')
+	       	{
+	       		embed = '1';
+	       		format = 'png';
+	       		
+	       		// Finds the current page number
+	       		if (this.pages != null && this.currentPage != null)
+	       		{
+	       			for (var i = 0; i < this.pages.length; i++)
+	       			{
+	       				if (this.pages[i] == this.currentPage)
+	       				{
+	       					range = i;
+	       					break;
+	       				}
+	       			}
+	       		}
+	       	}
+			
+			var bg = this.editor.graph.background;
+			
+			if (format == 'png' && transparent)
+			{
+				bg = mxConstants.NONE;
+			}
+			
+			return new mxElectronRequest('export', {
+				format: format,
+				xml: data,
+				from: range,
+				bg: (bg != null) ? bg : mxConstants.NONE,
+				filename: (filename != null) ? filename : null,
+				allPages: allPages,
+				base64: base64,
+				embedXml: embed
+			});
 		};
 		
 		//Export Dialog Pdf case
@@ -1101,7 +1123,11 @@ FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
 		{
 			var graph = editorUi.editor.graph;
 			
-			if (format == 'pdf')
+			if (format == 'xml' || format == 'svg')
+			{
+				return origExportFile.apply(this, arguments);
+			}
+			else
 			{
 				var data = editorUi.getFileData(true, null, null, null, null, true);
 	    		var bounds = graph.getGraphBounds();
@@ -1114,13 +1140,15 @@ FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
 					editorUi.saveRequest(name, format,
 						function(newTitle, base64)
 						{
-							return new mxElectronRequest('pdf-export', {
+							return new mxElectronRequest('export', {
+								format: format,
 								xml: data,
 								bg: (bg != null) ? bg : mxConstants.NONE,
 								filename: (newTitle != null) ? newTitle : null,
 								w: w,
 								h: h,
-								border: b
+								border: b,
+								base64: (base64 || '0')
 							}); 
 						});
 				}
@@ -1129,10 +1157,6 @@ FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
 					mxUtils.alert(mxResources.get('drawingTooLarge'));
 				}
 			}
-			else
-			{
-				return origExportFile.apply(this, arguments);
-			}
 		};
 	}
 	

+ 17 - 18
src/main/webapp/js/diagramly/Menus.js

@@ -130,7 +130,7 @@
 				false, mxResources.get('insert'));
 
 			editorUi.showDialog(dlg.container, 620, 440, true, true);
-		}));
+		})).isEnabled = isGraphEnabled;
 		
 		editorUi.actions.put('exportXml', new Action(mxResources.get('formatXml') + '...', function()
 		{
@@ -2321,7 +2321,7 @@
 						// Executed after dialog is added to dom
 						dlg.init();
 					}
-				}), parent);
+				}), parent, null, isGraphEnabled());
 			}
 		};
 		
@@ -2354,23 +2354,22 @@
 	    	return cell;
 		};
 		
-		
 		editorUi.actions.put('exportSvg', new Action(mxResources.get('formatSvg') + '...', function()
+		{
+			editorUi.showExportDialog(mxResources.get('formatSvg'), true, mxResources.get('export'),
+				'https://support.draw.io/display/DO/Exporting+Files',
+				mxUtils.bind(this, function(scale, transparentBackground, ignoreSelection, addShadow,
+					editable, embedImages, border, cropImage, currentPage, linkTarget)
 				{
-					editorUi.showExportDialog(mxResources.get('formatSvg'), true, mxResources.get('export'),
-						'https://support.draw.io/display/DO/Exporting+Files',
-						mxUtils.bind(this, function(scale, transparentBackground, ignoreSelection, addShadow,
-							editable, embedImages, border, cropImage, currentPage, linkTarget)
-						{
-							var val = parseInt(scale);
-							
-							if (!isNaN(val) && val > 0)
-							{
-							   	editorUi.exportSvg(val / 100, transparentBackground, ignoreSelection, addShadow,
-							   		editable, embedImages, border, !cropImage, currentPage, linkTarget);
-							}
-						}), true, null, 'svg');
-				}));
+					var val = parseInt(scale);
+					
+					if (!isNaN(val) && val > 0)
+					{
+					   	editorUi.exportSvg(val / 100, transparentBackground, ignoreSelection, addShadow,
+					   		editable, embedImages, border, !cropImage, currentPage, linkTarget);
+					}
+				}), true, null, 'svg');
+		}));
 		
 		editorUi.actions.put('insertText', new Action(mxResources.get('text'), function()
 		{
@@ -2436,7 +2435,7 @@
 			menu.addItem(mxResources.get('csv') + '...', null, function()
 			{
 				editorUi.showImportCsvDialog();
-			}, parent);
+			}, parent, null, isGraphEnabled());
 		})));
 
 		this.put('insertLayout', new Menu(mxUtils.bind(this, function(menu, parent)

+ 14 - 3
src/main/webapp/js/diagramly/OneDriveLibrary.js

@@ -22,11 +22,22 @@ OneDriveLibrary.prototype.isAutosave = function()
 };
 
 /**
- * Overridden to avoid updating data with current file.
+ * Translates this point by the given vector.
+ * 
+ * @param {number} dx X-coordinate of the translation.
+ * @param {number} dy Y-coordinate of the translation.
  */
-OneDriveLibrary.prototype.doSave = function(title, success, error)
+OneDriveLibrary.prototype.save = function(revision, success, error)
 {
-	this.saveFile(title, false, success, error);
+	this.ui.oneDrive.saveFile(this, mxUtils.bind(this, function(resp)
+	{
+		this.desc = resp;
+		
+		if (success != null)
+		{
+			success(resp);
+		}
+	}), error);
 };
 
 /**

+ 2 - 2
src/main/webapp/js/diagramly/sidebar/Sidebar-VVD.js

@@ -8,7 +8,7 @@
 		// Space savers
 		var sb = this;
 		var gn = 'mxgraph.vvd';
-		var dt = 'vmware validated diagram';
+		var dt = 'vmware validated design';
 		
 		var w = 50;
 		var h = 50;
@@ -211,7 +211,7 @@
 					w, h, '', 'Wi-Fi', null, null, this.getTagsForStencil(gn, 'wi fi wifi', dt).join(' '))
 		];
 			
-		this.addPalette('vvd', 'VMware Validated Diagram', false, mxUtils.bind(this, function(content)
+		this.addPalette('vvd', 'VMware Validated Design', false, mxUtils.bind(this, function(content)
 				{
 					for (var i = 0; i < fns.length; i++)
 					{

+ 3 - 2
src/main/webapp/js/mxgraph/Graph.js

@@ -4030,10 +4030,11 @@ HoverIcons.prototype.setCurrentState = function(state)
 	
 	mxGraphView.prototype.validateCellState = function(cell, recurse)
 	{
+		recurse = (recurse != null) ? recurse : true;
 		var state = this.getState(cell);
 		
 		// Forces repaint if jumps change on a valid edge
-		if (state != null && this.graph.model.isEdge(state.cell) &&
+		if (state != null && recurse && this.graph.model.isEdge(state.cell) &&
 			state.style != null && state.style[mxConstants.STYLE_CURVED] != 1 &&
 			!state.invalid && this.updateLineJumps(state))
 		{
@@ -4043,7 +4044,7 @@ HoverIcons.prototype.setCurrentState = function(state)
 		state = mxGraphViewValidateCellState.apply(this, arguments);
 		
 		// Adds to the list of edges that may intersect with later edges
-		if (state != null && this.graph.model.isEdge(state.cell) &&
+		if (state != null && recurse && this.graph.model.isEdge(state.cell) &&
 			state.style != null && state.style[mxConstants.STYLE_CURVED] != 1)
 		{
 			// LATER: Reuse jumps for valid edges

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 572 - 571
src/main/webapp/js/viewer.min.js


+ 4 - 2
src/main/webapp/package.json

@@ -27,9 +27,11 @@
     "electron-log": "^2.2.14",
     "electron-updater": "^4.0.6",
     "electron-progressbar": "^1.2.0",
-    "electron-store": "^3.2.0"
+    "electron-store": "^3.2.0",
+    "compression": "^1.7.4",
+    "crc": "^3.8.0"
   },
   "devDependencies": {
     "electron": "^2.0.2"
   }
-}
+}

+ 138 - 53
src/main/webapp/plugins/animation.js

@@ -336,6 +336,13 @@ Draw.loadPlugin(function(editorUi)
 											animateCells(graph, [cell]);
 										}
 									}
+				                    else if (tokens[0] == 'flow')
+									{
+					                    if (graph.model.isEdge(cell))
+					                    {
+					                      toggleFlowAnim(graph, [cell], tokens[2]);
+					                    }
+									}
 									else if (tokens[0] == 'hide')
 									{
 										fadeOut(getNodesForCells(graph, [cell]));
@@ -420,60 +427,59 @@ Draw.loadPlugin(function(editorUi)
 		graph.maxFitScale = null;
 		graph.centerZoom = true;
 
-		var fadeInBtn = mxUtils.button('Fade In', function()
-		{
-			var cells = editorUi.editor.graph.getSelectionCells();
-			
-			if (cells.length > 0)
-			{
-				for (var i = 0; i < cells.length; i++)
-				{
-					list.value = list.value + 'show ' + cells[i].id + ' fade\n';
-				}
-				
-				list.value = list.value + 'wait 1000\n';
-			}
-		});
-		td21.appendChild(fadeInBtn);
+	    var buttons = {
+	      'Fade In': 'show CELL fade',
+	      'Wipe In': 'show CELL',
+	      'Fade Out': 'hide CELL',
+	      'Flow On': 'flow CELL start',
+	      'Flow Off': 'flow CELL stop',
+	      'Flow Toggle': 'flow CELL',
+	      'Wait': '', // added by default
+	    }
+	    
+	    var bkeys = Object.keys(buttons);
+	    
+	    for (var i = 0; i < bkeys.length; i++)
+	    {
+	      var wait = 'wait 1000\n';
+	    	  
+	      (function(key)
+	      {
+		      var btn = mxUtils.button(key, function()
+		      {
+		        // we have a cell object
+		        var val = buttons[key]
+		        
+		        if (val.indexOf('CELL') > -1)
+		        {
+		          var cells = editorUi.editor.graph.getSelectionCells();
+		          
+		          if (cells.length > 0)
+		          {
+		            for (var i = 0; i < cells.length; i++)
+		            {
+		              var tmp = val.replace('CELL', cells[i].id)
+		              list.value += tmp + '\n'
+		            }
+		            
+		            list.value += wait
+		          }
+		        }
+		        else
+		        {
+		          if (val)
+		          {
+		            list.value += val + '\n'
+		          }
+		          
+		          list.value += wait
+		        }
 		
-		var animateBtn = mxUtils.button('Wipe In', function()
-		{
-			var cells = editorUi.editor.graph.getSelectionCells();
-			
-			if (cells.length > 0)
-			{
-				for (var i = 0; i < cells.length; i++)
-				{
-					list.value = list.value + 'show ' + cells[i].id + '\n';
-				}
-				
-				list.value = list.value + 'wait 1000\n';
-			}
-		});
-		td21.appendChild(animateBtn);
-		
-		var addBtn = mxUtils.button('Fade Out', function()
-		{
-			var cells = editorUi.editor.graph.getSelectionCells();
-			
-			if (cells.length > 0)
-			{
-				for (var i = 0; i < cells.length; i++)
-				{
-					list.value = list.value + 'hide ' + cells[i].id + '\n';
-				}
+		      });
+		      td21.appendChild(btn);
+	      })(bkeys[i]);
+	    }
 
-				list.value = list.value + 'wait 1000\n';
-			}
-		});
-		td21.appendChild(addBtn);
-		
-		var waitBtn = mxUtils.button('Wait', function()
-		{
-			list.value = list.value + 'wait 1000\n';
-		});
-		td21.appendChild(waitBtn);
-		
 		var runBtn = mxUtils.button('Preview', function()
 		{
 			graph.getModel().clear();
@@ -542,4 +548,83 @@ Draw.loadPlugin(function(editorUi)
 			editorUi.editor.addListener('fileLoaded', startAnimation);
 		}
 	}
-});
+
+	// Add flow capability
+	function toggleFlowAnim(graph, cells, status)
+	{
+	    if (!status)
+	    {
+	      status = 'toggle'
+	    }
+	    
+		for (var i = 0; i < cells.length; i++)
+		{
+			if (editorUi.editor.graph.model.isEdge(cells[i]))
+			{
+				var state = graph.view.getState(cells[i]);
+				
+				if (state && state.shape != null)
+				{
+					var paths = state.shape.node.getElementsByTagName('path');
+					
+					if (paths.length > 1)
+					{
+						if ((status == 'toggle' && paths[1].getAttribute('class') == 'mxEdgeFlow') || status == 'stop')
+						{
+							paths[1].removeAttribute('class');
+
+							if (mxUtils.getValue(state.style, mxConstants.STYLE_DASHED, '0') != '1')
+							{
+								paths[1].removeAttribute('stroke-dasharray');
+							}
+						}
+						else if ((status == 'toggle' && paths[1].getAttribute('class') != 'mxEdgeFlow') || status == 'start')
+						{
+							paths[1].setAttribute('class', 'mxEdgeFlow');
+			
+							if (mxUtils.getValue(state.style, mxConstants.STYLE_DASHED, '0') != '1')
+							{
+								paths[1].setAttribute('stroke-dasharray', '8');
+							}
+						}
+					}
+				}
+			}
+		}
+	};
+
+  function showCell(graph, cell)
+  {
+    graph.setCellStyles('opacity', '100', cell);
+    graph.setCellStyles('noLabel', null, [cell]);
+		nodes = getNodesForCells(graph, [cell]);
+		if (nodes != null)
+    {
+			for (var i = 0; i < nodes.length; i++)
+      {
+        mxUtils.setPrefixedStyle(nodes[i].style, 'transition', null);
+        nodes[i].style.opacity = '0';
+      }
+    }
+  }
+
+	try
+	{
+		var style = document.createElement('style')
+		style.type = 'text/css';
+		style.innerHTML = ['.mxEdgeFlow {',
+			  'animation: mxEdgeFlow 0.5s linear;',
+			  'animation-iteration-count: infinite;',
+			'}',
+			'@keyframes mxEdgeFlow {',
+			  'to {',
+			    'stroke-dashoffset: -16;',
+			  '}',
+			'}'].join('\n');
+		document.getElementsByTagName('head')[0].appendChild(style);
+	}
+	catch (e)
+	{
+		// ignore
+	}
+});

+ 5 - 1
src/main/webapp/plugins/replay.js

@@ -130,7 +130,7 @@ Draw.loadPlugin(function(ui) {
 		
 		mxResources.parse('record=Record');
 		mxResources.parse('replay=Replay');
-	
+		
 	    // Adds actions
 	    var action = ui.actions.addAction('record...', function()
 	    {
@@ -140,9 +140,11 @@ Draw.loadPlugin(function(ui) {
 		    	var state = codec.document.createElement('state');
 		    	state.appendChild(node);
 		    	tape =[mxUtils.getXml(state)];
+		    	ui.editor.setStatus('Recording started');
 	    	}
 	    	else if (tape != null)
 	    	{
+	    		ui.editor.setStatus('Recording stopped');
 	    		var tmp = tape;
 	    		tape = null;
 
@@ -160,6 +162,8 @@ Draw.loadPlugin(function(ui) {
 				ui.showDialog(dlg.container, 300, 80, true, true);
 				dlg.init();
 	    	}
+	    	
+	    	action.label = (tape != null) ? 'Stop recording' : mxResources.get('record') + '...';
 	    });
 		
 	    ui.actions.addAction('replay...', function()