1window.PLUGINS_BASE_PATH = '.';
2window.TEMPLATE_PATH = 'templates';
3window.DRAW_MATH_URL = 'math';
4window.DRAWIO_BASE_URL = '.'; //Prevent access to online website since it is not allowed
5FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
6
7//Disables eval for JS (uses shapes-14-6-5.min.js)
8mxStencilRegistry.allowEval = false;
9
10(function()
11{
12	// Overrides default mode
13	App.mode = App.MODE_DEVICE;
14
15	// Disables preview option in embed dialog
16	EmbedDialog.showPreviewOption = false;
17
18	// Disables new window option in edit diagram dialog
19	EditDiagramDialog.showNewWindowOption = false;
20
21	PrintDialog.previewEnabled = false;
22
23	PrintDialog.electronPrint = function(editorUi, allPages, pagesFrom, pagesTo,
24			fit, sheetsAcross, sheetsDown, zoom, pageScale, pageFormat)
25	{
26		var xml = '', title = '';
27		var file = editorUi.getCurrentFile();
28
29		if (file)
30		{
31			file.updateFileData();
32			xml = file.getData();
33			title = file.title;
34		}
35
36		new mxElectronRequest('export', {
37			print: true,
38			format: 'pdf',
39			xml: xml,
40			from: pagesFrom - 1,
41			to: pagesTo - 1,
42			allPages: allPages,
43			pageWidth: pageFormat.width,
44			pageHeight: pageFormat.height,
45			pageScale: pageScale,
46			fit: fit,
47			sheetsAcross: sheetsAcross,
48			sheetsDown: sheetsDown,
49			scale: zoom,
50			fileTitle: title
51		}).send(function(){}, function(){});
52	};
53
54	var oldWindowOpen = window.open;
55	window.open = function(url)
56	{
57		if (url != null && url.startsWith('http'))
58		{
59			const {shell} = require('electron');
60			shell.openExternal(url);
61		}
62		else
63		{
64			return oldWindowOpen(url);
65		}
66	}
67
68	var origAppMain = App.main;
69
70	App.main = function()
71	{
72		//TODO Move all file system operations to this worker to offload the renderer thread
73		//TODO Use async version of any sync function used here especially if it is in critical path. For example, open dialog sync block the UI until dialog is shown
74		App.filesWorker = new Worker('electronFilesWorker.js');
75
76		App.filesWorkerReqId = 1;
77		App.filesWorkerReqInfo = {};
78
79		App.filesWorkerReq = function(msg, callback, error)
80		{
81			msg.reqId = App.filesWorkerReqId++;
82			App.filesWorkerReqInfo[msg.reqId] = {callback: callback, error: error};
83			App.filesWorker.postMessage(msg);
84		};
85
86		App.filesWorker.onmessage = function(e)
87		{
88			var resp = e.data;
89			var callbacks = App.filesWorkerReqInfo[resp.reqId];
90
91			if (resp.error)
92			{
93				callbacks.error(resp.msg, resp.e);
94			}
95			else
96			{
97				callbacks.callback(resp.data);
98			}
99
100			delete App.filesWorkerReqInfo[resp.reqId];
101		};
102
103		//Load desktop plugins
104		var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null;
105		App.initPluginCallback();
106
107		if (plugins != null && plugins.length > 0)
108		{
109			for (var i = 0; i < plugins.length; i++)
110			{
111				try
112				{
113					if (plugins[i].startsWith('/plugins/'))
114					{
115						plugins[i] = '.' + plugins[i];
116					}
117					else if (plugins[i].startsWith('plugins/'))
118					{
119						plugins[i] = './' + plugins[i];
120					}
121					//Support old plugins added using file:// workaround
122					else if (!plugins[i].startsWith('file://'))
123					{
124						var fs = require('fs');
125						var sysPath = require('path');
126			        	var pluginsFile = sysPath.join(getAppDataFolder(), '/plugins', plugins[i]);
127
128			        	if (fs.existsSync(pluginsFile))
129			        	{
130			        		plugins[i] = 'file://' + pluginsFile;
131			        	}
132			        	else
133		        		{
134			        		continue; //skip not found files
135		        		}
136					}
137
138					mxscript(plugins[i]);
139				}
140				catch (e)
141				{
142					// ignore
143				}
144			}
145		}
146
147		//Disable web plugins loading
148		urlParams['plugins'] = '0';
149		origAppMain.apply(this, arguments);
150	};
151
152	var menusInit = Menus.prototype.init;
153	Menus.prototype.init = function()
154	{
155		menusInit.apply(this, arguments);
156
157		var editorUi = this.editorUi;
158
159		editorUi.actions.put('useOffline', new Action(mxResources.get('useOffline') + '...', function()
160		{
161			editorUi.openLink('https://www.draw.io/')
162		}));
163
164		this.put('openRecent', new Menu(function(menu, parent)
165		{
166			var recent = editorUi.getRecent();
167
168			if (recent != null)
169			{
170				for (var i = 0; i < recent.length; i++)
171				{
172					(function(entry)
173					{
174						menu.addItem(entry.title, null, function()
175						{
176							function doOpenRecent()
177							{
178								//Simulate opening a file via args
179								editorUi.loadArgs({args: [entry.id]});
180							};
181
182							var file = editorUi.getCurrentFile();
183
184							if (file != null && file.isModified())
185							{
186								editorUi.confirm(mxResources.get('allChangesLost'), null, doOpenRecent,
187									mxResources.get('cancel'), mxResources.get('discardChanges'));
188							}
189							else
190							{
191								doOpenRecent();
192							}
193						}, parent);
194					})(recent[i]);
195				}
196
197				menu.addSeparator(parent);
198			}
199
200			menu.addItem(mxResources.get('reset'), null, function()
201			{
202				editorUi.resetRecent();
203			}, parent);
204		}));
205
206		// Replaces file menu to replace openFrom menu with open and rename downloadAs to export
207		this.put('file', new Menu(mxUtils.bind(this, function(menu, parent)
208		{
209			this.addMenuItems(menu, ['new', 'open'], parent);
210			this.addSubmenu('openRecent', menu, parent);
211			this.addMenuItems(menu, ['-', 'synchronize', '-', 'save', 'saveAs', '-', 'import'], parent);
212			this.addSubmenu('exportAs', menu, parent);
213			menu.addSeparator(parent);
214			this.addSubmenu('embed', menu, parent);
215			menu.addSeparator(parent);
216			this.addMenuItems(menu, ['newLibrary', 'openLibrary'], parent);
217
218			var file = editorUi.getCurrentFile();
219
220			if (file != null && editorUi.fileNode != null)
221			{
222				var filename = (file.getTitle() != null) ?
223					file.getTitle() : editorUi.defaultFilename;
224
225				if (!/(\.html)$/i.test(filename) &&
226					!/(\.svg)$/i.test(filename))
227				{
228					this.addMenuItems(menu, ['-', 'properties']);
229				}
230			}
231
232			this.addMenuItems(menu, ['-', 'pageSetup', 'print', '-', 'close'], parent);
233			// LATER: Find API for application.quit
234		})));
235	};
236
237	function getDocumentsFolder()
238	{
239		//On windows, misconfigured Documents folder cause an exception
240		try
241		{
242			return require('@electron/remote').app.getPath('documents');
243		}
244		catch(e) {}
245
246		return '.';
247	};
248
249	function getAppDataFolder()
250	{
251		try
252		{
253			var fs = require('fs');
254			var appDataDir = require('@electron/remote').app.getPath('appData');
255        	var drawioDir = appDataDir + '/draw.io';
256
257        	if (!fs.existsSync(drawioDir)) //Usually this dir already exists
258        	{
259        		fs.mkdirSync(drawioDir);
260        	}
261
262			return drawioDir;
263		}
264		catch(e) {}
265
266		return '.';
267	};
268
269	var graphCreateLinkForHint = Graph.prototype.createLinkForHint;
270
271	Graph.prototype.createLinkForHint = function(href, label)
272	{
273		var a = graphCreateLinkForHint.call(this, href, label);
274
275		if (href != null && !this.isCustomLink(href))
276		{
277			// KNOWN: Event with gesture handler mouseUp the middle click opens a framed window
278			mxEvent.addListener(a, 'click', mxUtils.bind(this, function(evt)
279			{
280				this.openLink(a.getAttribute('href'), a.getAttribute('target'));
281				mxEvent.consume(evt);
282			}));
283		}
284
285		return a;
286	};
287
288	Graph.prototype.openLink = function(url, target)
289	{
290		require('electron').shell.openExternal(url);
291	};
292
293	// Initializes the user interface
294	var editorUiInit = EditorUi.prototype.init;
295	EditorUi.prototype.init = function()
296	{
297		editorUiInit.apply(this, arguments);
298
299		var editorUi = this;
300		var graph = this.editor.graph;
301
302		global.__emt_isModified =
303		e => {
304			if (editorUi.getCurrentFile())
305			{
306				return editorUi.getCurrentFile().isModified()
307			}
308
309			return false
310		}
311
312		// global.__emt_getCurrentFile = e => {
313		// 	return this.getCurrentFile()
314		// }
315
316		// Adds support for libraries
317		this.actions.addAction('newLibrary...', mxUtils.bind(this, function()
318		{
319			editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE);
320		}));
321
322		this.actions.addAction('openLibrary...', mxUtils.bind(this, function()
323		{
324			editorUi.pickLibrary(App.MODE_DEVICE);
325		}));
326
327		// Replaces import action
328		this.actions.addAction('import...', mxUtils.bind(this, function()
329		{
330			if (editorUi.getCurrentFile() != null)
331			{
332				var remote = require('@electron/remote');
333				var dialog = remote.dialog;
334				const sysPath = require('path')
335				var lastDir = localStorage.getItem('.lastImpDir');
336
337		        var paths = dialog.showOpenDialogSync({
338		        	defaultPath: lastDir || getDocumentsFolder(),
339		        	properties: ['openFile']
340		        });
341
342		        if (paths !== undefined && paths[0] != null)
343		        {
344		        	var path = paths[0];
345		        	localStorage.setItem('.lastImpDir', sysPath.dirname(path));
346		        	var asImage = /\.png$/i.test(path) || /\.gif$/i.test(path) || /\.jpe?g$/i.test(path);
347		        	var encoding = (asImage || /\.pdf$/i.test(path) || /\.vsdx$/i.test(path) || /\.vssx$/i.test(path)) ?
348		        		'base64' : 'utf-8';
349
350					if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
351					{
352						var fs = require('fs');
353
354						fs.readFile(path, encoding, mxUtils.bind(this, function (e, data)
355				        {
356			        		if (e)
357			        		{
358			        			editorUi.spinner.stop();
359			        			editorUi.handleError(e);
360			        		}
361			        		else
362				        	{
363								try
364								{
365									if (editorUi.isLucidChartData(data))
366									{
367										editorUi.convertLucidChart(data, function(xml)
368										{
369											editorUi.spinner.stop();
370											graph.setSelectionCells(editorUi.importXml(xml));
371										}, function(e)
372										{
373											editorUi.spinner.stop();
374											editorUi.handleError(e);
375										});
376									}
377									else if  (/(\.vsdx)($|\?)/i.test(path))
378									{
379										editorUi.importVisio(editorUi.base64ToBlob(data, 'application/octet-stream'), function(xml)
380										{
381											editorUi.spinner.stop();
382											graph.setSelectionCells(editorUi.importXml(xml));
383										});
384									}
385									else if (!editorUi.isOffline() && new XMLHttpRequest().upload && editorUi.isRemoteFileFormat(data, path))
386									{
387										// Asynchronous parsing via server
388										editorUi.parseFile(new Blob([data], {type : 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
389										{
390											if (xhr.readyState == 4)
391											{
392												editorUi.spinner.stop();
393
394												if (xhr.status >= 200 && xhr.status <= 299)
395												{
396													graph.setSelectionCells(editorUi.importXml(xhr.responseText));
397												}
398											}
399										}), path);
400									}
401									else
402									{
403										if (/\.pdf$/i.test(path))
404									    {
405											var tmp = Editor.extractGraphModelFromPdf(data);
406
407											if (tmp != null)
408											{
409												data = tmp;
410											}
411							    		}
412										else if (/\.png$/i.test(path))
413										{
414											var tmp = editorUi.extractGraphModelFromPng(data);
415
416											if (tmp != null)
417											{
418												asImage = false;
419												data = tmp;
420											}
421										}
422										else if (/\.svg$/i.test(path))
423						    			{
424											// LATER: Use importXml without throwing exception if no data
425						    				// Checks if SVG contains content attribute
426					    					var root = mxUtils.parseXml(data);
427				    						var svgs = root.getElementsByTagName('svg');
428
429				    						if (svgs.length > 0)
430					    					{
431				    							var svgRoot = svgs[0];
432						    					var cont = svgRoot.getAttribute('content');
433
434						    					if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%')
435						    					{
436						    						cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true));
437						    					}
438
439						    					if (cont != null && cont.charAt(0) == '%')
440						    					{
441						    						cont = decodeURIComponent(cont);
442						    					}
443
444						    					if (cont != null && (cont.substring(0, 8) === '<mxfile ' ||
445						    						cont.substring(0, 14) === '<mxGraphModel '))
446						    					{
447						    						asImage = false;
448						    						data = cont;
449						    					}
450						    					else
451						    					{
452						    						asImage = true;
453						    					}
454					    					}
455						    			}
456
457										if (asImage)
458										{
459											var img = new Image();
460											img.onload = function()
461											{
462												editorUi.resizeImage(img, img.src, function(data2, w, h)
463												{
464													editorUi.spinner.stop();
465													var pt = graph.getInsertPoint();
466													graph.setSelectionCell(graph.insertVertex(null, null, '', pt.x, pt.y, w, h,
467														'shape=image;aspect=fixed;image=' + editorUi.convertDataUri(data2) + ';'));
468												}, true);
469											};
470
471											img.onerror = function(e)
472											{
473												editorUi.spinner.stop();
474												editorUi.handleError();
475											};
476
477											var format = path.substring(path.lastIndexOf('.') + 1);
478											img.src = (format == 'svg') ? Editor.createSvgDataUri(data) :
479												'data:image/' + format + ';base64,' + data;
480										}
481										else
482										{
483											editorUi.spinner.stop();
484
485											if (data != null)
486											{
487												graph.setSelectionCells(editorUi.importXml(data));
488											}
489										}
490									}
491								}
492								catch(e)
493								{
494									editorUi.spinner.stop();
495									editorUi.handleError(e);
496								}
497			        		}
498			        	}));
499					}
500		        }
501			}
502		}));
503
504		// Replaces new action
505		var oldNew = this.actions.get('new').funct;
506
507		this.actions.addAction('new...', mxUtils.bind(this, function()
508		{
509			if (this.getCurrentFile() == null)
510			{
511				oldNew();
512			}
513			else
514			{
515				const ipc = require('electron').ipcRenderer
516				ipc.sendSync('winman', {action: 'newfile', opt: {width: 1600}})
517			}
518		}), null, null, Editor.ctrlKey + '+N');
519
520		this.actions.get('open').shortcut = Editor.ctrlKey + '+O';
521
522		// Adds shortcut keys for file operations
523		editorUi.keyHandler.bindAction(78, true, 'new'); // Ctrl+N
524		editorUi.keyHandler.bindAction(79, true, 'open'); // Ctrl+O
525
526		function createGraph()
527		{
528			var graph = new Graph();
529	        graph.setExtendParents(false);
530	        graph.setExtendParentsOnAdd(false);
531	        graph.setConstrainChildren(false);
532	        graph.setHtmlLabels(true);
533	        graph.getModel().maintainEdgeParent = false;
534	        return graph;
535		};
536
537		function cloneMxCLipboardToSys()
538		{
539			var cells = mxClipboard.getCells();
540
541			if (cells && cells.length > 0)
542			{
543				try
544				{
545					var tmpGraph = createGraph();
546					tmpGraph.importCells(cells, 0, 0, tmpGraph.getDefaultParent());
547					var remote = require('@electron/remote');
548					var clipboard = remote.clipboard;
549					var codec = new mxCodec();
550		            var node = codec.encode(tmpGraph.getModel());
551		            var modelString = mxUtils.getXml(node);
552					clipboard.writeText(encodeURIComponent(modelString));
553				}
554				catch(e)
555				{
556					//Ignore
557				}
558			}
559		};
560
561		function cloneSysCLipboardToMx()
562		{
563			try
564			{
565				var remote = require('@electron/remote');
566				var clipboard = remote.clipboard;
567				var modelString = clipboard.readText();
568
569				if (modelString)
570				{
571					modelString = decodeURIComponent(modelString);
572					var xmlDoc = mxUtils.parseXml(modelString);
573					var tmpGraph = createGraph();
574					var codec = new mxCodec(xmlDoc);
575					var model = tmpGraph.getModel();
576					codec.decode(xmlDoc.documentElement, model);
577					mxClipboard.setCells(model.root.children[0].children);
578				}
579			}
580			catch(e)
581			{
582				//Ignore, the contents of mxClipboard will be used
583			}
584		};
585
586		//Set system clipboard on menu copy/cut
587		var origCut = this.actions.get('cut').funct;
588
589		editorUi.actions.addAction('cut', function()
590		{
591			origCut();
592			cloneMxCLipboardToSys();
593		}, null, 'sprite-cut', Editor.ctrlKey + '+X');
594
595		var origCopy = this.actions.get('copy').funct;
596
597		editorUi.actions.addAction('copy', function()
598		{
599			origCopy();
600			cloneMxCLipboardToSys();
601		}, null, 'sprite-copy', Editor.ctrlKey + '+C');
602
603		//Get data from system clipboard for pase/pasteHere
604		var origPaste = this.actions.get('paste').funct;
605
606		editorUi.actions.addAction('paste', function()
607		{
608			cloneSysCLipboardToMx();
609			origPaste();
610		}, false, 'sprite-paste', Editor.ctrlKey + '+V');
611
612		var origPasteHere = this.actions.get('pasteHere').funct;
613
614		editorUi.actions.addAction('pasteHere', function()
615		{
616			cloneSysCLipboardToMx();
617			origPasteHere();
618		});
619
620		//Enable paste action even if mxClipboard is empty! TODO Is this OK?
621		editorUi.updatePasteActionStates = function()
622		{
623			var graph = this.editor.graph;
624			var paste = this.actions.get('paste');
625			var pasteHere = this.actions.get('pasteHere');
626
627			paste.setEnabled(this.editor.graph.cellEditor.isContentEditing() ||
628					(graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())));
629			pasteHere.setEnabled(paste.isEnabled());
630		};
631
632		editorUi.actions.addAction('plugins...', function()
633		{
634			editorUi.showDialog(new PluginsDialog(editorUi, function(callback)
635			{
636				var div = document.createElement('div');
637
638				var title = document.createElement('span');
639				title.style.marginTop = '6px';
640				mxUtils.write(title, mxResources.get('builtinPlugins') + ': ');
641				div.appendChild(title);
642
643				var pluginsSelect = document.createElement('select');
644				pluginsSelect.style.width = '150px';
645
646				for (var i = 0; i < App.publicPlugin.length; i++)
647				{
648					var option = document.createElement('option');
649					mxUtils.write(option, App.publicPlugin[i]);
650					option.value = App.publicPlugin[i];
651					pluginsSelect.appendChild(option);
652				}
653
654				div.appendChild(pluginsSelect);
655				mxUtils.br(div);
656				mxUtils.br(div);
657
658				title = document.createElement('span');
659				mxUtils.write(title, mxResources.get('extPlugins') + ': ');
660				div.appendChild(title);
661
662				var extPluginsBtn = mxUtils.button(mxResources.get('selectFile') + '...', function()
663				{
664					var remote = require('@electron/remote');
665					var dialog = remote.dialog;
666					const sysPath = require('path');
667					var lastDir = localStorage.getItem('.lastPluginDir');
668
669			        var paths = dialog.showOpenDialogSync({
670			        	defaultPath: lastDir || getDocumentsFolder(),
671			        	filters: [
672			        	    { name: 'draw.io Plugins', extensions: ['js'] },
673			        	    { name: 'All Files', extensions: ['*'] }
674			    	    ],
675			        	properties: ['openFile']
676			        });
677
678			        if (paths !== undefined && paths[0] != null)
679			        {
680			        	localStorage.setItem('.lastPluginDir', sysPath.dirname(paths[0]));
681			        	var fs = require('fs');
682			        	var pluginsDir = sysPath.join(getAppDataFolder(), '/plugins');
683
684			        	if (!fs.existsSync(pluginsDir))
685			        	{
686			        		fs.mkdirSync(pluginsDir);
687			        	}
688
689			        	var pluginName = sysPath.basename(paths[0]);
690			        	var dstFile = sysPath.join(pluginsDir, pluginName);
691
692			        	if (fs.existsSync(dstFile))
693		        		{
694			        		alert(mxResources.get('fileExists'));
695		        		}
696			        	else
697			        	{
698				        	fs.copyFile(paths[0], dstFile, (err) =>
699				        	{
700				        		if (err)
701				        		{
702				        			alert('Adding plugin failed.');
703				        		}
704				        		else
705				        		{
706					        		callback(pluginName);
707					        		editorUi.hideDialog();
708				        		}
709			        		});
710			        	}
711			        }
712				});
713
714				extPluginsBtn.className = 'geBtn';
715				div.appendChild(extPluginsBtn);
716
717				var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function()
718				{
719	        		callback(App.pluginRegistry[pluginsSelect.value]);
720				}));
721				editorUi.showDialog(dlg.container, 300, 120, true, true);
722			},
723			function(plugin)
724			{
725				var fs = require('fs');
726				const sysPath = require('path')
727				var pluginsFile = sysPath.join(getAppDataFolder(), '/plugins', plugin);
728
729	        	if (fs.existsSync(pluginsFile))
730	        	{
731	        		fs.unlinkSync(pluginsFile);
732	        	}
733			}).container, 360, 170, true, false);
734		});
735	}
736
737	var appLoad = App.prototype.load;
738
739	App.prototype.load = function()
740	{
741		appLoad.apply(this, arguments);
742		const {ipcRenderer} = require('electron');
743
744		ipcRenderer.on('args-obj', (event, argsObj) =>
745		{
746			this.loadArgs(argsObj)
747		})
748
749		var editorUi = this;
750
751		ipcRenderer.on('export-vsdx', (event, argsObj) =>
752		{
753			var file = new LocalFile(editorUi, argsObj.xml, '');
754
755			editorUi.fileLoaded(file);
756
757			try
758			{
759				editorUi.saveData = function(filename, format, data, mimeType, base64Encoded)
760				{
761					ipcRenderer.send('export-vsdx-finished', data);
762				};
763
764				var expSuccess = new VsdxExport(editorUi).exportCurrentDiagrams();
765
766				if (!expSuccess)
767				{
768					ipcRenderer.send('export-vsdx-finished', null);
769				}
770			}
771			catch (e)
772			{
773				ipcRenderer.send('export-vsdx-finished', null);
774			}
775		})
776
777		//We do some async stuff during app loading so we need to know exactly when loading is finished (it is not when onload is finished)
778		ipcRenderer.send('app-load-finished', null);
779	}
780
781	App.prototype.loadArgs = function(argsObj)
782	{
783		var paths = argsObj.args;
784
785		// If a file is passed, and it is not an argument (has a leading -)
786		if (paths !== undefined && paths[0] != null && paths[0].indexOf('-') != 0 && this.spinner.spin(document.body, mxResources.get('loading')))
787		{
788			var path = paths[0];
789			this.hideDialog();
790
791			var success = mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
792			{
793				this.spinner.stop();
794
795				if (data != null)
796				{
797					var file = new LocalFile(this, data, name || '');
798					file.fileObject = fileEntry;
799					file.stat = stat;
800					file.setModified(isModified? true : false);
801					this.fileLoaded(file);
802				}
803			});
804
805			var error = mxUtils.bind(this, function(e)
806			{
807				this.spinner.stop();
808
809				if (e.code === 'ENOENT')
810				{
811					var title = path.replace(/^.*[\\\/]/, '');
812					var data = this.emptyDiagramXml;
813					var file = new LocalFile(this, data, title, null);
814
815					file.fileObject = new Object();
816					file.fileObject.path = path;
817					file.fileObject.name = title;
818					file.fileObject.type = 'utf-8';
819					this.fileCreated(file, null, null, null);
820					this.saveFile();
821				}
822				else
823				{
824					this.handleError(e);
825				}
826
827			});
828
829			// Tries to open the file
830			this.readGraphFile(success, error, path);
831		}
832		// If no file is passed, but there is the "create-if-not-exists" flag
833		else if (argsObj.create != null)
834		{
835			var title = 'Untitled document';
836			var data = this.emptyDiagramXml;
837			var file = new LocalFile(this, data, title, null);
838			this.fileCreated(file, null, null, null);
839		}
840	}
841
842	var origFileLoaded = EditorUi.prototype.fileLoaded;
843
844	EditorUi.prototype.fileLoaded = function(file)
845	{
846		var fs = require('fs');
847		var oldFile = this.getCurrentFile();
848
849		if (oldFile != null && oldFile.fileObject != null)
850		{
851			fs.unwatchFile(oldFile.fileObject.path);
852		}
853
854		if (file != null)
855		{
856			if (file.fileObject == null)
857			{
858				var fname = file.getTitle();
859
860				var fileInfo = openFilesMap[fname];
861
862				if (fileInfo != null)
863				{
864					file.fileObject = {
865						name: fileInfo.name,
866						path: fileInfo.path,
867						type: fileInfo.type || 'utf-8'
868					};
869					//delete it such that it is not used again incorrectly
870					delete openFilesMap[fname];
871				}
872			}
873
874			if (file.fileObject != null)
875			{
876				var title = file.fileObject.path;
877
878				if (title.length > 100)
879				{
880					title = '...' + title.substr(title.length - 97);
881				}
882
883				this.addRecent({id: file.fileObject.path, title: title});
884
885				fs.watchFile(file.fileObject.path, mxUtils.bind(this, function(curr, prev)
886				{
887					//File is changed (not just accessed)
888					if (curr.mtimeMs != prev.mtimeMs)
889					{
890						//Ignore our own changes
891						if (file.unwatchedSaves || (file.state != null && file.stat.mtimeMs == curr.mtimeMs))
892						{
893							file.unwatchedSaves = false;
894							return;
895						}
896
897						file.inConflictState = true;
898
899						this.showError(mxResources.get('externalChanges'),
900							mxResources.get('fileChangedSyncDialog'),
901							mxResources.get('synchronize'), mxUtils.bind(this, function()
902							{
903								if (this.spinner.spin(document.body, mxResources.get('updatingDocument')))
904								{
905									file.synchronizeFile(mxUtils.bind(this, function()
906									{
907										this.spinner.stop();
908									}), mxUtils.bind(this, function(err)
909									{
910										file.handleFileError(err, true);
911									}));
912								}
913							}), null, null, null,
914							mxResources.get('cancel'), mxUtils.bind(this, function()
915							{
916								this.hideDialog();
917								file.handleFileError(null, false);
918							}), 340, 150);
919					}
920				}));
921			}
922		}
923
924		origFileLoaded.apply(this, arguments);
925	};
926
927	// Uses local picker
928	App.prototype.pickFile = function()
929	{
930		var doPickFile = mxUtils.bind(this, function()
931		{
932			this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
933			{
934				var file = new LocalFile(this, data, '');
935				file.fileObject = fileEntry;
936				file.stat = stat;
937				file.setModified(isModified? true : false);
938				this.fileLoaded(file);
939			}));
940		});
941
942		var file = this.getCurrentFile();
943
944		if (file != null && file.isModified())
945		{
946			this.confirm(mxResources.get('allChangesLost'), null, doPickFile,
947				mxResources.get('cancel'), mxResources.get('discardChanges'));
948		}
949		else
950		{
951			doPickFile();
952		}
953	};
954
955	/**
956	 * Selects a library to load from a picker
957	 *
958	 * @param mode the device mode, ignored in this case
959	 */
960	App.prototype.pickLibrary = function(mode)
961	{
962		this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat)
963		{
964			try
965			{
966				var library = new DesktopLibrary(this, data, fileEntry);
967				this.loadLibrary(library);
968			}
969			catch (e)
970			{
971				this.handleError(e, mxResources.get('errorLoadingFile'));
972			}
973		}));
974	};
975
976	// Uses local picker
977	App.prototype.chooseFileEntry = function(fn)
978	{
979		var remote = require('@electron/remote');
980		var dialog = remote.dialog;
981		const sysPath = require('path')
982		var lastDir = localStorage.getItem('.lastOpenDir');
983
984        var paths = dialog.showOpenDialogSync({
985        	defaultPath: lastDir || getDocumentsFolder(),
986        	filters: [
987        	    { name: 'draw.io Diagrams', extensions: ['drawio', 'xml', 'png', 'svg', 'html'] },
988        	    { name: 'VSDX Documents', extensions: ['vsdx'] },
989        	    { name: 'All Files', extensions: ['*'] }
990    	    ],
991        	properties: ['openFile']
992        });
993
994        if (paths !== undefined && paths[0] != null)
995        {
996        	localStorage.setItem('.lastOpenDir', sysPath.dirname(paths[0]));
997
998			this.readGraphFile(fn, mxUtils.bind(this, function(err)
999			{
1000				this.handleError(err);
1001			}), paths[0]);
1002        }
1003        else
1004        {
1005        	this.spinner.stop();
1006        }
1007	};
1008
1009	//In order not to repeat the logic for opening a file, we collect files information here and use them in openLocalFile
1010	var origOpenFiles = EditorUi.prototype.openFiles;
1011	var openFilesMap = {};
1012
1013	EditorUi.prototype.openFiles = function(files, temp)
1014	{
1015		openFilesMap = {};
1016
1017		for (var i = 0; i < files.length; i++)
1018		{
1019			openFilesMap[files[i].name] = files[i];
1020		}
1021
1022		origOpenFiles.apply(this, arguments);
1023	};
1024
1025	App.prototype.readGraphFile = function(fn, fnErr, path)
1026	{
1027		var fs = require('fs');
1028		var index = path.lastIndexOf('.png');
1029		var isPng = index > -1 && index == path.length - 4;
1030		var isVsdx = /\.vsdx$/i.test(path) || /\.vssx$/i.test(path);
1031		var encoding = isVsdx? null : ((isPng || /\.pdf$/i.test(path)) ? 'base64' : 'utf-8');
1032		var isModified = false, fileLoaded = false;
1033
1034		var readData = mxUtils.bind(this, function (e, data)
1035		{
1036			if (e)
1037			{
1038				fnErr(e);
1039				fileLoaded = true;
1040			}
1041			else
1042			{
1043				var fileEntry = new Object();
1044				fileEntry.path = path;
1045				fileEntry.name = path.replace(/^.*[\\\/]/, '');
1046				fileEntry.type = encoding;
1047
1048				// VSDX and PDF files are imported instead of being opened
1049				if (isVsdx)
1050				{
1051					var name = fileEntry.name;
1052
1053					this.importVisio(data, mxUtils.bind(this, function(xml)
1054					{
1055						var dot = name.lastIndexOf('.');
1056
1057						if (dot >= 0)
1058						{
1059							name = name.substring(0, name.lastIndexOf('.')) + '.drawio';
1060						}
1061						else
1062						{
1063							name = name + '.drawio';
1064						}
1065
1066						if (xml.substring(0, 10) == '<mxlibrary')
1067						{
1068							// Creates new temporary file if library is dropped in splash screen
1069							if (this.getCurrentFile() == null && urlParams['embed'] != '1')
1070							{
1071								this.openLocalFile(this.emptyDiagramXml, this.defaultFilename);
1072							}
1073
1074							try
1075			    			{
1076								this.loadLibrary(new LocalLibrary(this, xml, name));
1077			    			}
1078							catch (e)
1079			    			{
1080			    				this.handleError(e, mxResources.get('errorLoadingFile'));
1081			    			}
1082
1083							fn();
1084						}
1085						else
1086						{
1087							fn(null, xml, null, name, isModified);
1088						}
1089
1090						fileLoaded = true;
1091					}), null, name);
1092
1093					return;
1094				}
1095				else if (/\.pdf$/i.test(path))
1096			    {
1097					var tmp = Editor.extractGraphModelFromPdf('data:application/pdf;base64,' + data);
1098
1099					if (tmp != null)
1100					{
1101						var name = fileEntry.name;
1102						fn(null, tmp, null, name.substring(0, name.lastIndexOf('.')) + '.drawio', isModified);
1103						fileLoaded = true;
1104
1105						return;
1106					}
1107	    		}
1108				else if (isPng)
1109				{
1110					// Detecting png by extension. Would need https://github.com/mscdex/mmmagic
1111					// to do it by inspection
1112					data = this.extractGraphModelFromPng('data:image/png;base64,' + data);
1113				}
1114
1115				fs.stat(path, function(err, stat)
1116				{
1117					if (err)
1118					{
1119						fnErr(err);
1120					}
1121					else
1122					{
1123						fn(fileEntry, data, stat, null, isModified);
1124					}
1125
1126					fileLoaded = true;
1127				});
1128			}
1129		});
1130
1131		fs.readFile(path, encoding, readData);
1132
1133    	//Check if a bkp file exists, if one exists, ask user to restore/ignore
1134		var checkBkpFile = mxUtils.bind(this, function (e, data)
1135		{
1136			//Backup file must be loaded after actual file
1137			if (!fileLoaded)
1138			{
1139				setTimeout(function()
1140				{
1141					checkBkpFile(e, data);
1142				}, 10);
1143				return;
1144			}
1145
1146			if (!e)
1147			{
1148				var dlg = new DraftDialog(this, mxResources.get('backupFound'),
1149						data, mxUtils.bind(this, function()
1150				{
1151					this.hideDialog();
1152					isModified = true;
1153					readData(null, data);
1154					fs.unlink(bkpFile, (err) => {}); //Ignore errors!
1155				}), mxUtils.bind(this, function()
1156				{
1157					this.hideDialog();
1158					fs.unlink(bkpFile, (err) => {}); //Ignore errors!
1159				}));
1160
1161				this.showDialog(dlg.container, 640, 480, true, false, mxUtils.bind(this, function(cancel)
1162				{
1163					if (cancel)
1164					{
1165						//TODO Rename backup file?
1166					}
1167				}));
1168
1169				dlg.init();
1170			}
1171		});
1172
1173		var bkpFile = getBkpFilePath(path);
1174		fs.readFile(bkpFile, encoding, checkBkpFile);
1175	};
1176
1177	// Disables temp files in Electron
1178	var LocalFileCtor = LocalFile;
1179
1180	LocalFile = function(ui, data, title, temp)
1181	{
1182		LocalFileCtor.call(this, ui, data, title, false);
1183	};
1184
1185	mxUtils.extend(LocalFile, LocalFileCtor);
1186
1187	LocalFile.prototype.getLatestVersion = function(success, error)
1188	{
1189		if (this.fileObject == null)
1190		{
1191			if (error != null)
1192			{
1193				error({message: mxResources.get('fileNotFound')});
1194			}
1195		}
1196		else
1197		{
1198			this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
1199			{
1200				var file = new LocalFile(this, data, '');
1201				file.stat = stat;
1202				file.setModified(isModified? true : false);
1203				success(file);
1204			}), error, this.fileObject.path);
1205		}
1206	};
1207
1208	// Call save as for copy
1209	LocalFile.prototype.copyFile = function(success, error)
1210	{
1211		this.saveAs(this.ui.getCopyFilename(this), success, error);
1212	};
1213
1214	/**
1215	 * Adds all listeners.
1216	 */
1217	LocalFile.prototype.getDescriptor = function()
1218	{
1219		return this.stat;
1220	};
1221
1222	/**
1223	* Updates the descriptor of this file with the one from the given file.
1224	*/
1225	LocalFile.prototype.setDescriptor = function(stat)
1226	{
1227		this.stat = stat;
1228	};
1229
1230	LocalFile.prototype.reloadFile = function(success)
1231	{
1232		if (this.fileObject == null)
1233		{
1234			this.ui.handleError({message: mxResources.get('fileNotFound')});
1235		}
1236		else
1237		{
1238			this.ui.spinner.stop();
1239
1240			var fn = mxUtils.bind(this, function()
1241			{
1242				this.setModified(false);
1243				var page = this.ui.currentPage;
1244				var viewState = this.ui.editor.graph.getViewState();
1245				var selection = this.ui.editor.graph.getSelectionCells();
1246
1247				if (this.ui.spinner.spin(document.body, mxResources.get('loading')))
1248				{
1249					this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
1250					{
1251						this.ui.spinner.stop();
1252
1253						var file = new LocalFile(this.ui, data, '');
1254						file.fileObject = fileEntry;
1255						file.stat = stat;
1256						file.setModified(isModified? true : false);
1257						this.ui.fileLoaded(file);
1258						this.ui.restoreViewState(page, viewState, selection);
1259
1260						if (this.backupPatch != null)
1261						{
1262							this.patch([this.backupPatch]);
1263						}
1264
1265						if (success != null)
1266						{
1267							success();
1268						}
1269					}), mxUtils.bind(this, function(err)
1270					{
1271						this.handleFileError(err);
1272					}), this.fileObject.path);
1273				}
1274			});
1275
1276			if (this.isModified() && this.backupPatch == null)
1277			{
1278				this.ui.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function()
1279				{
1280					this.handleFileSuccess(DrawioFile.SYNC == 'manual');
1281				}), fn, mxResources.get('cancel'), mxResources.get('discardChanges'));
1282			}
1283			else
1284			{
1285				fn();
1286			}
1287		}
1288	};
1289
1290	LocalFile.prototype.isAutosave = function()
1291	{
1292		return this.fileObject != null && DrawioFile.prototype.isAutosave.apply(this, arguments);
1293	};
1294
1295	LocalFile.prototype.isAutosaveOptional = function()
1296	{
1297		return this.fileObject != null;
1298	};
1299
1300	LocalFile.prototype.getTitle = function()
1301	{
1302		return (this.fileObject != null) ? this.fileObject.name : this.title;
1303	};
1304
1305	LocalFile.prototype.isRenamable = function()
1306	{
1307		return false;
1308	};
1309
1310	// Restores default implementation of open with autosave
1311	LocalFile.prototype.open = DrawioFile.prototype.open;
1312
1313	LocalFile.prototype.save = function(revision, success, error, unloading, overwrite)
1314	{
1315		DrawioFile.prototype.save.apply(this, [revision, mxUtils.bind(this, function()
1316		{
1317			this.saveFile(revision, success, error, unloading, overwrite);
1318		}), error, unloading, overwrite]);
1319	};
1320
1321	LocalFile.prototype.isConflict = function(stat)
1322	{
1323		return stat != null && this.stat != null && stat.mtimeMs != this.stat.mtimeMs;
1324	};
1325
1326	LocalFile.prototype.getFilename = function()
1327	{
1328		var filename = this.title;
1329
1330		// Adds default extension
1331		if (filename.length > 0 && (!/(\.xml)$/i.test(filename) && !/(\.html)$/i.test(filename) &&
1332			!/(\.svg)$/i.test(filename) && !/(\.png)$/i.test(filename) && !/(\.drawio)$/i.test(filename)))
1333		{
1334			filename += '.drawio';
1335		}
1336
1337		return filename;
1338	};
1339
1340	function getBkpFilePath(filePath)
1341	{
1342		const path = require('path');
1343		return path.join(path.dirname(filePath), '~$' + path.basename(filePath) + '.bkp');
1344	};
1345
1346	// Prototype inheritance needs new functions to be added to subclasses
1347	LocalLibrary.prototype.getFilename = LocalFile.prototype.getFilename;
1348
1349	LocalFile.prototype.saveFile = function(revision, success, error, unloading, overwrite)
1350	{
1351		//Safeguard in case saveFile is called from online code in the future
1352		if (typeof success !== 'function')
1353		{
1354			if (typeof unloading === 'function')
1355			{
1356				//Call error
1357				unloading({message: 'This is a bug, please report!'}); //Original draw.io function parameters are (title, revision, success, error, useCurrentData)
1358			}
1359			return;
1360		}
1361
1362		if (!this.savingFile)
1363		{
1364			var fn = mxUtils.bind(this, function()
1365			{
1366				var doSave = mxUtils.bind(this, function(data, enc)
1367				{
1368					var savedData = this.data;
1369
1370					// Makes sure no changes get lost while the file is saved
1371					this.setShadowModified(false);
1372					this.savingFile = true;
1373
1374					var errorWrapper = mxUtils.bind(this, function(e)
1375					{
1376						this.savingFile = false;
1377
1378						if (error != null)
1379						{
1380	        				error(e);
1381						}
1382					});
1383
1384					if (this.fileObject.bkpPath == null)
1385					{
1386						this.fileObject.bkpPath = getBkpFilePath(this.fileObject.path);
1387					}
1388
1389					this.unwatchedSaves = true; //Multiple saves doesn't call watch the same number, so use a boolean and check for changes
1390
1391					App.filesWorkerReq({
1392						action: 'saveFile',
1393						fileObject: this.fileObject,
1394						defEnc: enc,
1395						data: data,
1396						origStat: this.stat,
1397						overwrite: overwrite
1398					}, mxUtils.bind(this, function(resp)
1399					{
1400						//No changes during the saving process?
1401						this.setModified(this.getShadowModified());
1402						this.savingFile = false;
1403						var lastDesc = this.stat;
1404						this.stat = resp.stat;
1405
1406						this.fileSaved(savedData, lastDesc, mxUtils.bind(this, function()
1407						{
1408							this.contentChanged();
1409
1410							if (success != null)
1411							{
1412								success();
1413							}
1414						}), error);
1415					}),
1416					mxUtils.bind(this, function(errMsg, err)
1417					{
1418						if (errMsg == 'empty data')
1419						{
1420							this.ui.handleError({message: mxResources.get('errorSavingFile')});
1421						}
1422						else if (errMsg == 'conflict')
1423						{
1424							this.inConflictState = true;
1425						}
1426
1427						errorWrapper();
1428					}));
1429				});
1430
1431				if (!/(\.png)$/i.test(this.fileObject.name))
1432				{
1433					doSave(this.getData());
1434				}
1435				else
1436				{
1437					var p = this.ui.getPngFileProperties(this.ui.fileNode);
1438
1439					this.ui.getEmbeddedPng(function(data)
1440					{
1441						doSave(atob(data), 'binary');
1442					}, error, null, p.scale, p.border);
1443				}
1444			});
1445
1446			if (this.fileObject == null)
1447			{
1448				var remote = require('@electron/remote');
1449				var dialog = remote.dialog;
1450				const sysPath = require('path')
1451				var lastDir = localStorage.getItem('.lastSaveDir');
1452				var name = this.getFilename();
1453				var ext = null;
1454
1455				if (name != null)
1456				{
1457					var idx = name.lastIndexOf('.');
1458
1459					if (idx > 0)
1460					{
1461						ext = name.substring(idx + 1);
1462						name = name.substring(0, idx);
1463					}
1464				}
1465
1466				var path = dialog.showSaveDialogSync({
1467					defaultPath: (lastDir || getDocumentsFolder()) + '/' + name,
1468					filters: this.ui.createFileSystemFilters(ext)
1469				});
1470
1471		        if (path != null)
1472		        {
1473		        	localStorage.setItem('.lastSaveDir', sysPath.dirname(path));
1474					this.fileObject = new Object();
1475					this.fileObject.path = path;
1476					this.fileObject.name = path.replace(/^.*[\\\/]/, '');
1477					this.fileObject.type = 'utf-8';
1478					fn();
1479				}
1480		        else
1481		        {
1482	            	this.ui.spinner.stop();
1483		        }
1484			}
1485			else
1486			{
1487				fn();
1488			}
1489		}
1490	};
1491
1492	LocalFile.prototype.saveAs = function(title, success, error)
1493	{
1494		var remote = require('@electron/remote');
1495		var dialog = remote.dialog;
1496		const sysPath = require('path')
1497		var lastDir = localStorage.getItem('.lastSaveDir');
1498		var name = this.getFilename();
1499		var ext = null;
1500
1501		if (name == '' && this.fileObject != null && this.fileObject.name != null)
1502		{
1503			name = this.fileObject.name;
1504			var idx = name.lastIndexOf('.');
1505
1506			if (idx > 0)
1507			{
1508				ext = name.substring(idx + 1);
1509				name = name.substring(0, idx);
1510			}
1511		}
1512
1513		var path = dialog.showSaveDialogSync({
1514			defaultPath: (lastDir || getDocumentsFolder()) + '/' + name,
1515			filters: this.ui.createFileSystemFilters(ext)
1516		});
1517
1518        if (path != null)
1519        {
1520        	localStorage.setItem('.lastSaveDir', sysPath.dirname(path));
1521			this.fileObject = new Object();
1522			this.fileObject.path = path;
1523			this.fileObject.name = path.replace(/^.*[\\\/]/, '');
1524			this.fileObject.type = 'utf-8';
1525
1526			this.save(false, success, error, null, true);
1527		}
1528	};
1529
1530	/**
1531	 * Loads the given file handle as a local file.
1532	 */
1533	App.prototype.createFileSystemFilters = function(defaultExt)
1534	{
1535		var ext = [];
1536
1537		for (var i = 0; i < this.editor.diagramFileTypes.length; i++)
1538		{
1539			var obj = {name: mxResources.get(this.editor.diagramFileTypes[i].description) +
1540				' (.' + this.editor.diagramFileTypes[i].extension + ')',
1541				extensions: [this.editor.diagramFileTypes[i].extension]};
1542
1543			if (this.editor.diagramFileTypes[i].extension == defaultExt)
1544			{
1545				ext.splice(0, 0, obj);
1546			}
1547			else
1548			{
1549				ext.push(obj);
1550			}
1551		}
1552
1553		return ext;
1554	};
1555
1556	/**
1557	 * Loads the given file handle as a local file.
1558	 */
1559	App.prototype.saveFile = function(forceDialog)
1560	{
1561		var file = this.getCurrentFile();
1562
1563		if (file != null)
1564		{
1565			if (!forceDialog && file.getTitle() != null)
1566			{
1567				file.save(true, mxUtils.bind(this, function()
1568				{
1569					if (EditorUi.enableDrafts)
1570					{
1571						file.removeDraft();
1572					}
1573
1574					file.handleFileSuccess(true);
1575				}), mxUtils.bind(this, function(err)
1576				{
1577					file.handleFileError(err, true);
1578				}));
1579			}
1580			else
1581			{
1582				file.saveAs(null, mxUtils.bind(this, function()
1583				{
1584					if (EditorUi.enableDrafts)
1585					{
1586						file.removeDraft();
1587					}
1588
1589					file.handleFileSuccess(true);
1590				}), mxUtils.bind(this, function(err)
1591				{
1592					file.handleFileError(err, true);
1593				}));
1594			}
1595		}
1596	};
1597
1598	/**
1599	 * Translates this point by the given vector.
1600	 */
1601	App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn)
1602	{
1603		mode = (mode != null) ? mode : this.mode;
1604		noSpin = (noSpin != null) ? noSpin : false;
1605		noReload = (noReload != null) ? noReload : false;
1606		var xml = this.createLibraryDataFromImages(images);
1607
1608		var error = mxUtils.bind(this, function(resp)
1609		{
1610			this.spinner.stop();
1611
1612			if (fn != null)
1613			{
1614				fn();
1615			}
1616
1617			// Null means cancel by user and is ignored
1618			if (resp != null)
1619			{
1620				this.handleError(resp, mxResources.get('errorSavingFile'));
1621			}
1622		});
1623
1624		// Handles special case for local libraries
1625		if (file == null)
1626		{
1627			file = new LocalLibrary(this, xml, name);
1628		}
1629
1630		if (noSpin || this.spinner.spin(document.body, mxResources.get('saving')))
1631		{
1632			file.setData(xml);
1633
1634			var doSave = mxUtils.bind(this, function()
1635			{
1636				file.save(true, mxUtils.bind(this, function(resp)
1637				{
1638					this.spinner.stop();
1639					this.hideDialog(true);
1640
1641					if (!noReload)
1642					{
1643						this.libraryLoaded(file, images)
1644					}
1645
1646					if (fn != null)
1647					{
1648						fn();
1649					}
1650				}), error);
1651			});
1652
1653			if (name != file.getTitle())
1654			{
1655				var oldHash = file.getHash();
1656
1657				file.rename(name, mxUtils.bind(this, function(resp)
1658				{
1659					// Change hash in stored settings
1660					if (file.constructor != LocalLibrary && oldHash != file.getHash())
1661					{
1662						mxSettings.removeCustomLibrary(oldHash);
1663						mxSettings.addCustomLibrary(file.getHash());
1664					}
1665
1666					// Workaround for library files changing hash so
1667					// the old library cannot be removed from the
1668					// sidebar using the updated file in libraryLoaded
1669					this.removeLibrarySidebar(oldHash);
1670
1671					doSave();
1672				}), error)
1673			}
1674			else
1675			{
1676				doSave();
1677			}
1678		}
1679	};
1680
1681	App.prototype.checkForUpdates = function()
1682	{
1683		const ipcRenderer = require('electron').ipcRenderer;
1684		ipcRenderer.send('checkForUpdates');
1685	}
1686
1687	var origUpdateHeader = App.prototype.updateHeader;
1688
1689	App.prototype.updateHeader = function()
1690	{
1691		origUpdateHeader.apply(this, arguments);
1692
1693		document.querySelectorAll('.geMenuItem').forEach(i => i.style.webkitAppRegion = 'no-drag');
1694		var menubarContainer = document.querySelector('.geMenubarContainer');
1695
1696		if (urlParams['sketch'] == '1')
1697		{
1698			menubarContainer = this.menubarContainer;
1699			//TODO find a better place for dragging the window
1700			menubarContainer.parentNode.style.webkitAppRegion = 'drag';
1701		}
1702
1703		menubarContainer.style.webkitAppRegion = 'drag';
1704
1705		//Add window control buttons
1706		this.windowControls = document.createElement('div');
1707		this.windowControls.id = 'geWindow-controls';
1708		this.windowControls.innerHTML =
1709			'<div class="button" id="min-button">' +
1710			'    <svg width="10" height="1" viewBox="0 0 11 1">' +
1711			'        <path d="m11 0v1h-11v-1z" stroke-width=".26208"/>' +
1712			'    </svg>' +
1713			'</div>' +
1714			'<div class="button" id="max-button">' +
1715			'    <svg width="10" height="10" viewBox="0 0 10 10">' +
1716			'        <path d="m10-1.6667e-6v10h-10v-10zm-1.001 1.001h-7.998v7.998h7.998z" stroke-width=".25" />' +
1717			'    </svg>' +
1718			'</div>' +
1719			'<div class="button" id="restore-button">' +
1720			'    <svg width="10" height="10" viewBox="0 0 11 11">' +
1721			'        <path' +
1722			'        d="m11 8.7978h-2.2021v2.2022h-8.7979v-8.7978h2.2021v-2.2022h8.7979zm-3.2979-5.5h-6.6012v6.6011h6.6012zm2.1968-2.1968h-6.6012v1.1011h5.5v5.5h1.1011z"' +
1723			'        stroke-width=".275" />' +
1724			'    </svg>' +
1725			'</div>' +
1726			'<div class="button" id="close-button">' +
1727			'    <svg width="10" height="10" viewBox="0 0 12 12">' +
1728			'        <path' +
1729			'        d="m6.8496 6 5.1504 5.1504-0.84961 0.84961-5.1504-5.1504-5.1504 5.1504-0.84961-0.84961 5.1504-5.1504-5.1504-5.1504 0.84961-0.84961 5.1504 5.1504 5.1504-5.1504 0.84961 0.84961z"' +
1730			'        stroke-width=".3" />' +
1731			'    </svg>' +
1732			'</div>';
1733
1734		if (uiTheme == 'atlas')
1735		{
1736			this.windowControls.style.top = '9px';
1737		}
1738
1739		menubarContainer.appendChild(this.windowControls);
1740
1741		var handleDarkModeChange = mxUtils.bind(this, function ()
1742		{
1743			if (uiTheme == 'atlas' || Editor.isDarkMode())
1744			{
1745				this.windowControls.style.fill = 'white';
1746				document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button dark');
1747			}
1748			else
1749			{
1750				this.windowControls.style.fill = '#999';
1751				document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button white');
1752			}
1753		});
1754
1755		handleDarkModeChange();
1756		this.addListener('darkModeChanged', handleDarkModeChange);
1757
1758		if (urlParams['sketch'] == '1')
1759		{
1760			this.windowControls.style.position = 'inherit';
1761		}
1762
1763		if (this.appIcon != null)
1764		{
1765			this.appIcon.style.webkitAppRegion = 'no-drag';
1766		}
1767
1768		if (this.menubar != null)
1769		{
1770			this.menubar.container.style.webkitAppRegion = 'no-drag';
1771		}
1772
1773		const remote = require('@electron/remote');
1774		const win = remote.getCurrentWindow();
1775
1776		window.onbeforeunload = (event) => {
1777		    /* If window is reloaded, remove win event listeners
1778		    (DOM element listeners get auto garbage collected but not
1779		    Electron win listeners as the win is not dereferenced unless closed) */
1780		    win.removeAllListeners();
1781		}
1782
1783	    // Make minimise/maximise/restore/close buttons work when they are clicked
1784	    document.getElementById('min-button').addEventListener("click", event => {
1785	        win.minimize();
1786	    });
1787
1788	    document.getElementById('max-button').addEventListener("click", event => {
1789	        win.maximize();
1790	    });
1791
1792	    document.getElementById('restore-button').addEventListener("click", event => {
1793	        win.unmaximize();
1794	    });
1795
1796	    document.getElementById('close-button').addEventListener("click", event => {
1797	        win.close();
1798	    });
1799
1800	    // Toggle maximise/restore buttons when maximisation/unmaximisation occurs
1801	    toggleMaxRestoreButtons();
1802	    win.on('maximize', toggleMaxRestoreButtons);
1803	    win.on('unmaximize', toggleMaxRestoreButtons);
1804		win.on('resize', toggleMaxRestoreButtons);
1805
1806	    function toggleMaxRestoreButtons() {
1807	        if (win.isMaximized()) {
1808	            document.body.classList.add('geMaximized');
1809	        } else {
1810	            document.body.classList.remove('geMaximized');
1811	        }
1812	    }
1813	}
1814
1815	/**
1816	 * Copies the given cells and XML to the clipboard as an embedded image.
1817	 */
1818	EditorUi.prototype.writeImageToClipboard = function(dataUrl, w, h, error)
1819	{
1820		try
1821		{
1822			const remote = require('@electron/remote');
1823
1824			remote.clipboard.write({image: remote.
1825				nativeImage.createFromDataURL(dataUrl), html: '<img src="' +
1826				dataUrl + '" width="' + w + '" height="' + h + '">'});
1827		}
1828		catch (e)
1829		{
1830			error(e);
1831		}
1832	};
1833
1834	/**
1835	 * Updates action states depending on the selection.
1836	 */
1837	var editorUiUpdateActionStates = EditorUi.prototype.updateActionStates;
1838	EditorUi.prototype.updateActionStates = function()
1839	{
1840		editorUiUpdateActionStates.apply(this, arguments);
1841
1842		var file = this.getCurrentFile();
1843		var syncEnabled = file != null && file.fileObject != null;
1844		this.actions.get('synchronize').setEnabled(syncEnabled);
1845	};
1846
1847	EditorUi.prototype.saveLocalFile = function(data, filename, mimeType, base64Encoded, format, allowBrowser)
1848	{
1849		this.saveData(filename, format, data, mimeType, base64Encoded);
1850	};
1851
1852	EditorUi.prototype.saveRequest = function(filename, format, fn, data, base64Encoded, mimeType)
1853	{
1854		var xhr = fn(null, '1');
1855
1856		if (xhr != null && this.spinner.spin(document.body, mxResources.get('saving')))
1857		{
1858			xhr.send(mxUtils.bind(this, function()
1859			{
1860				this.spinner.stop();
1861
1862				if (xhr.getStatus() >= 200 && xhr.getStatus() <= 299)
1863				{
1864					this.saveData(filename, format, xhr.getText(), mimeType, true);
1865				}
1866				else
1867				{
1868					this.handleError({message: mxResources.get('errorSavingFile')});
1869				}
1870			}), mxUtils.bind(this, function(resp)
1871			{
1872				this.spinner.stop();
1873				this.handleError(resp);
1874			}));
1875		}
1876	};
1877
1878	function mxElectronRequest(reqType, reqObj)
1879	{
1880		this.reqType = reqType;
1881		this.reqObj = reqObj;
1882	};
1883
1884	//Extends mxXmlRequest
1885	mxUtils.extend(mxElectronRequest, mxXmlRequest);
1886
1887	mxElectronRequest.prototype.send = function(callback, error)
1888	{
1889		const ipcRenderer = require('electron').ipcRenderer;
1890		ipcRenderer.send(this.reqType, this.reqObj);
1891
1892		ipcRenderer.once(this.reqType + '-success', (event, data) =>
1893		{
1894			this.response = data;
1895			callback();
1896			ipcRenderer.send(this.reqType + '-finalize');
1897		})
1898
1899		ipcRenderer.once(this.reqType + '-error', (event, err) =>
1900		{
1901			this.hasError = true;
1902			error(err);
1903			ipcRenderer.send(this.reqType + '-finalize');
1904		})
1905	};
1906
1907	mxElectronRequest.prototype.getStatus = function()
1908	{
1909		return this.hasError? 500 : 200;
1910	}
1911
1912	mxElectronRequest.prototype.getText = function()
1913	{
1914		return this.response;
1915	}
1916
1917		//Direct export to pdf
1918		EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64, transparent,
1919			currentPage, scale, border, grid, includeXml)
1920		{
1921			var graph = this.editor.graph;
1922			var bounds = graph.getGraphBounds();
1923
1924			// Exports only current page for images that does not contain file data, but for
1925			// the other formats with XML included or pdf with all pages, we need to send the complete data and use
1926			// the from/to URL parameters to specify the page to be exported.
1927			var data = this.getFileData(true, null, null, null, ignoreSelection, currentPage == false? false : format != 'xmlpng');
1928			var range = null;
1929			var allPages = null;
1930
1931			var embed = (includeXml) ? '1' : '0';
1932
1933			if (format == 'pdf' && currentPage == false)
1934			{
1935				allPages = '1';
1936			}
1937
1938			if (format == 'xmlpng')
1939	       	{
1940	       		embed = '1';
1941	       		format = 'png';
1942
1943	       		// Finds the current page number
1944	       		if (this.pages != null && this.currentPage != null)
1945	       		{
1946	       			for (var i = 0; i < this.pages.length; i++)
1947	       			{
1948	       				if (this.pages[i] == this.currentPage)
1949	       				{
1950	       					range = i;
1951	       					break;
1952	       				}
1953	       			}
1954	       		}
1955	       	}
1956
1957			var bg = graph.background;
1958
1959			if (format == 'png' && transparent)
1960			{
1961				bg = mxConstants.NONE;
1962			}
1963			else if (!transparent && (bg == null || bg == mxConstants.NONE))
1964			{
1965				bg = '#ffffff';
1966			}
1967
1968			var extras = {globalVars: graph.getExportVariables()};
1969
1970			if (grid)
1971			{
1972				extras.grid = {
1973					size: graph.gridSize,
1974					steps: graph.view.gridSteps,
1975					color: graph.view.gridColor
1976				};
1977			}
1978
1979			return new mxElectronRequest('export', {
1980				format: format,
1981				xml: data,
1982				from: range,
1983				bg: (bg != null) ? bg : mxConstants.NONE,
1984				filename: (filename != null) ? filename : null,
1985				allPages: allPages,
1986				base64: base64,
1987				embedXml: embed,
1988				extras: encodeURIComponent(JSON.stringify(extras)),
1989				scale: scale,
1990				border: border
1991			});
1992		};
1993
1994	//Export Dialog Pdf case
1995	var origExportFile = ExportDialog.exportFile;
1996
1997	ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi)
1998	{
1999		var graph = editorUi.editor.graph;
2000
2001		if (format == 'xml' || format == 'svg')
2002		{
2003			return origExportFile.apply(this, arguments);
2004		}
2005		else
2006		{
2007			var data = editorUi.getFileData(true, null, null, null, null, true);
2008    		var bounds = graph.getGraphBounds();
2009			var w = Math.floor(bounds.width * s / graph.view.scale);
2010			var h = Math.floor(bounds.height * s / graph.view.scale);
2011
2012			editorUi.hideDialog();
2013
2014			if ((format == 'png' || format == 'jpg' || format == 'jpeg') && editorUi.isExportToCanvas())
2015			{
2016				if (format == 'png')
2017				{
2018					editorUi.exportImage(s, bg == null || bg == 'none', true,
2019				   		false, false, b, true, false, null, null, dpi);
2020				}
2021				else
2022				{
2023					editorUi.exportImage(s, false, true,
2024						false, false, b, true, false, 'jpeg');
2025				}
2026			}
2027			else
2028			{
2029				var extras = {globalVars: graph.getExportVariables()};
2030
2031				editorUi.saveRequest(name, format,
2032					function(newTitle, base64)
2033					{
2034						return new mxElectronRequest('export', {
2035							format: format,
2036							xml: data,
2037							bg: (bg != null) ? bg : mxConstants.NONE,
2038							filename: (newTitle != null) ? newTitle : null,
2039							w: w,
2040							h: h,
2041							border: b,
2042							base64: (base64 || '0'),
2043							extras: JSON.stringify(extras),
2044							dpi: dpi > 0? dpi : null
2045						});
2046					});
2047			}
2048		}
2049	};
2050
2051	EditorUi.prototype.saveData = function(filename, format, data, mimeType, base64Encoded)
2052	{
2053		var remote = require('@electron/remote');
2054		var dialog = remote.dialog;
2055		var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {};
2056		const sysPath = require('path')
2057		var lastDir = localStorage.getItem('.lastExpDir');
2058
2059		// Spinner.stop is asynchronous so we must invoke save dialog asynchronously
2060		// to give the spinner some time to stop spinning
2061		window.setTimeout(mxUtils.bind(this, function()
2062		{
2063			var dlgConfig = {defaultPath: (lastDir || getDocumentsFolder()) + '/' + filename};
2064			var filters = null;
2065
2066			switch (format)
2067			{
2068				case 'xmlpng':
2069				case 'png':
2070					filters = [
2071				          { name: 'PNG Images', extensions: ['png'] }
2072				       ];
2073				break;
2074				case 'jpg':
2075				case 'jpeg':
2076					filters = [
2077				          { name: 'JPEG Images', extensions: ['jpg', 'jpeg'] }
2078				       ];
2079				break;
2080				case 'svg':
2081					filters = [
2082				          { name: 'SVG Images', extensions: ['svg'] }
2083				       ];
2084				break;
2085				case 'pdf':
2086					filters = [
2087				          { name: 'PDF Documents', extensions: ['pdf'] }
2088				       ];
2089				break;
2090				case 'vsdx':
2091					filters = [
2092				          { name: 'VSDX Documents', extensions: ['vsdx'] }
2093				       ];
2094				break;
2095				case 'html':
2096					filters = [
2097				          { name: 'HTML Documents', extensions: ['html'] }
2098				       ];
2099				break;
2100				case 'xml':
2101					filters = [
2102				          { name: 'XML Documents', extensions: ['xml'] }
2103				       ];
2104				break;
2105			};
2106
2107			dlgConfig['filters'] = filters;
2108			var path = dialog.showSaveDialogSync(dlgConfig);
2109
2110	        if (path != null)
2111	        {
2112	        	localStorage.setItem('.lastExpDir', sysPath.dirname(path));
2113
2114	        	if (data == null || data.length == 0)
2115				{
2116					this.handleError({message: mxResources.get('errorSavingFile')});
2117				}
2118				else
2119				{
2120					var fs = require('fs');
2121					resume();
2122
2123					var fileObject = new Object();
2124					fileObject.path = path;
2125					fileObject.name = path.replace(/^.*[\\\/]/, '');
2126					fileObject.type = (base64Encoded) ? 'base64' : 'utf-8';
2127
2128					fs.writeFile(fileObject.path, data, fileObject.type, mxUtils.bind(this, function (e)
2129				    {
2130						this.spinner.stop();
2131
2132						if (e)
2133						{
2134							this.handleError({message: mxResources.get('errorSavingFile')});
2135						}
2136		        	}));
2137				}
2138			}
2139		}), 50);
2140	};
2141
2142	EditorUi.prototype.addBeforeUnloadListener = function() {};
2143
2144	EditorUi.prototype.loadDesktopLib = function(libPath, success, error)
2145	{
2146		this.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat)
2147		{
2148			var library = new DesktopLibrary(this, data, fileEntry);
2149			this.loadLibrary(library);
2150			success(library);
2151		}), error, libPath);
2152	};
2153})();
2154