ElectronApp.js 55 KB

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