ElectronApp.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. window.TEMPLATE_PATH = 'templates';
  2. (function()
  3. {
  4. // Overrides default mode
  5. App.mode = App.MODE_DEVICE;
  6. // Redirects printing to iframe to avoid document.write
  7. PrintDialog.showPreview = function(preview, print)
  8. {
  9. var iframe = document.createElement('iframe');
  10. document.body.appendChild(iframe);
  11. // Workaround for lost gradients in print output
  12. var getBaseUrl = mxSvgCanvas2D.prototype.getBaseUrl;
  13. mxSvgCanvas2D.prototype.getBaseUrl = function()
  14. {
  15. return '';
  16. };
  17. // Renders print output into iframe and prints
  18. var result = preview.open(null, iframe.contentWindow);
  19. iframe.contentWindow.print();
  20. iframe.parentNode.removeChild(iframe);
  21. mxSvgCanvas2D.prototype.getBaseUrl = getBaseUrl;
  22. return result;
  23. };
  24. var menusInit = Menus.prototype.init;
  25. Menus.prototype.init = function()
  26. {
  27. menusInit.apply(this, arguments);
  28. var editorUi = this.editorUi;
  29. // Replaces file menu to replace openFrom menu with open and rename downloadAs to export
  30. this.put('file', new Menu(mxUtils.bind(this, function(menu, parent)
  31. {
  32. this.addMenuItems(menu, ['new', 'open', '-', 'save', 'saveAs', '-'], parent);
  33. this.addSubmenu('exportAs', menu, parent);
  34. this.addSubmenu('embed', menu, parent);
  35. this.addMenuItems(menu, ['-', 'newLibrary', 'openLibrary', '-', 'documentProperties', 'print'], parent);
  36. })));
  37. this.put('extras', new Menu(mxUtils.bind(this, function(menu, parent)
  38. {
  39. this.addMenuItems(menu, ['copyConnect', 'collapseExpand', '-', 'mathematicalTypesetting', 'autosave', '-',
  40. 'createShape', 'editDiagram', '-', 'online'], parent);
  41. })));
  42. };
  43. // Initializes the user interface
  44. var editorUiInit = EditorUi.prototype.init;
  45. EditorUi.prototype.init = function()
  46. {
  47. editorUiInit.apply(this, arguments);
  48. var editorUi = this;
  49. var graph = this.editor.graph;
  50. // Adds support for libraries
  51. this.actions.addAction('newLibrary...', mxUtils.bind(this, function()
  52. {
  53. editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE);
  54. }));
  55. this.actions.addAction('openLibrary...', mxUtils.bind(this, function()
  56. {
  57. editorUi.pickLibrary(App.MODE_DEVICE);
  58. }));
  59. // // Replaces import action
  60. // this.actions.addAction('import...', mxUtils.bind(this, function()
  61. // {
  62. // if (this.getCurrentFile() != null)
  63. // {
  64. // chrome.fileSystem.chooseEntry({type: 'openFile', acceptsAllTypes: true}, mxUtils.bind(this, function(fileEntry)
  65. // {
  66. // if (!chrome.runtime.lastError)
  67. // {
  68. // fileEntry.file(mxUtils.bind(this, function(fileObject)
  69. // {
  70. // if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
  71. // {
  72. // var reader = new FileReader();
  73. //
  74. // reader.onload = function(evt)
  75. // {
  76. // editorUi.spinner.stop();
  77. //
  78. // try
  79. // {
  80. // var data = reader.result;
  81. //
  82. // if (fileObject.type.substring(0, 9) == 'image/png')
  83. // {
  84. // data = editorUi.extractGraphModelFromPng(data);
  85. // }
  86. // else if (fileObject.type.substring(0, 6) == 'image/')
  87. // {
  88. // data = null;
  89. // }
  90. //
  91. // if (data != null)
  92. // {
  93. // graph.setSelectionCells(editorUi.importXml(data));
  94. // }
  95. // else
  96. // {
  97. // var img = new Image();
  98. // img.onload = function()
  99. // {
  100. // editorUi.resizeImage(img, reader.result, function(data2, w, h)
  101. // {
  102. // var pt = graph.getInsertPoint();
  103. // graph.setSelectionCell(graph.insertVertex(null, null, '', pt.x, pt.y, w, h,
  104. // 'shape=image;aspect=fixed;image=' + editorUi.convertDataUri(data2) + ';'));
  105. // }, true);
  106. // };
  107. // img.src = reader.result;
  108. // }
  109. // }
  110. // catch(e)
  111. // {
  112. // console.log(e);
  113. // editorUi.handleError(e);
  114. // }
  115. // };
  116. //
  117. // reader.onerror = function(ev)
  118. // {
  119. // editorUi.spinner.stop();
  120. // editorUi.handleError(ev);
  121. // };
  122. //
  123. // if (fileObject.type.substring(0, 6) == 'image/')
  124. // {
  125. // reader.readAsDataURL(fileObject);
  126. // }
  127. // else
  128. // {
  129. // reader.readAsText(fileObject);
  130. // }
  131. // }
  132. // }));
  133. // }
  134. // else if (chrome.runtime.lastError.message != 'User cancelled')
  135. // {
  136. // editorUi.handleError(chrome.runtime.lastError);
  137. // }
  138. // }));
  139. // }
  140. // }));
  141. // // Replaces new action
  142. // this.actions.addAction('new...', mxUtils.bind(this, function()
  143. // {
  144. // if (this.getCurrentFile() == null)
  145. // {
  146. // // LATER: In Chrome OS the extension is not enforced resulting
  147. // // in possible files with no extension. Extensions such as XML,
  148. // // SVG and HTML are and should be allowed. How can we fix this?
  149. // chrome.fileSystem.chooseEntry({type: 'saveFile',
  150. // accepts: [{description: 'Draw.io Diagram (.xml)',
  151. // extensions: ['xml']}]}, mxUtils.bind(this, function(f)
  152. // {
  153. // if (!chrome.runtime.lastError)
  154. // {
  155. // var file = new LocalFile(editorUi, this.emptyDiagramXml, '');
  156. // file.fileObject = f;
  157. //
  158. // editorUi.fileLoaded(file);
  159. // }
  160. // else if (chrome.runtime.lastError.message != 'User cancelled')
  161. // {
  162. // editorUi.handleError(chrome.runtime.lastError);
  163. // }
  164. // }));
  165. // }
  166. // else
  167. // {
  168. // // Could use URL parameter to call new action but conflicts with splash screen
  169. // chrome.app.window.create('index.html',
  170. // {
  171. // bounds :
  172. // {
  173. // width: Math.floor(Math.min(screen.availWidth * 3 / 4, 1024)),
  174. // height: Math.floor(Math.min(screen.availHeight * 3 / 4, 768)),
  175. // left: Math.floor((screen.availWidth - Math.min(screen.availWidth * 3 / 4, 1024)) / 2),
  176. // top: Math.floor((screen.availHeight - Math.min(screen.availHeight * 3 / 4, 768)) / 3)
  177. // }
  178. // });
  179. // }
  180. // }), null, null, 'Ctrl+N');
  181. this.actions.get('open').shortcut = 'Ctrl+O';
  182. // Adds shortcut keys for file operations
  183. editorUi.keyHandler.bindAction(78, true, 'new'); // Ctrl+N
  184. editorUi.keyHandler.bindAction(79, true, 'open'); // Ctrl+O
  185. }
  186. // Uses local picker
  187. App.prototype.pickFile = function()
  188. {
  189. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data)
  190. {
  191. var file = new LocalFile(this, data, '');
  192. file.fileObject = fileEntry;
  193. this.fileLoaded(file);
  194. }));
  195. };
  196. /**
  197. * Selects a library to load from a picker
  198. *
  199. * @param mode the device mode, ignored in this case
  200. */
  201. App.prototype.pickLibrary = function(mode)
  202. {
  203. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data)
  204. {
  205. var library = new LocalLibrary(this, data, fileEntry.name);
  206. library.fileObject = fileEntry;
  207. this.loadLibrary(library);
  208. }));
  209. };
  210. // Uses local picker
  211. App.prototype.chooseFileEntry = function(fn)
  212. {
  213. const electron = require('electron');
  214. var remote = electron.remote;
  215. var dialog = remote.dialog;
  216. var paths = dialog.showOpenDialog({properties: [ 'openFile' ] });
  217. if (paths !== undefined && paths[0] != null)
  218. {
  219. var fs = require('fs');
  220. var path = paths[0];
  221. var index = path.lastIndexOf('.png');
  222. var isPng = index > -1 && index == path.length - 4;
  223. var encoding = isPng ? 'base64' : 'utf-8'
  224. fs.readFile(path, encoding, mxUtils.bind(this, function (e, data)
  225. {
  226. if (e)
  227. {
  228. this.handleError(e);
  229. }
  230. else
  231. {
  232. if (isPng)
  233. {
  234. // Detecting png by extension. Would need https://github.com/mscdex/mmmagic
  235. // to do it by inspection
  236. data = this.extractGraphModelFromPng(data, true);
  237. }
  238. var fileEntry = new Object();
  239. fileEntry.path = path;
  240. fileEntry.name = path.replace(/^.*[\\\/]/, '');
  241. fileEntry.type = encoding;
  242. fn(fileEntry, data);
  243. }
  244. }));
  245. }
  246. };
  247. LocalFile.prototype.isAutosave = function()
  248. {
  249. return this.ui.editor.autosave;
  250. };
  251. LocalFile.prototype.isAutosaveOptional = function()
  252. {
  253. return true;
  254. };
  255. LocalLibrary.prototype.isAutosave = function()
  256. {
  257. return true;
  258. };
  259. LocalFile.prototype.getTitle = function()
  260. {
  261. return (this.fileObject != null) ? this.fileObject.name : null;
  262. };
  263. LocalFile.prototype.isRenamable = function()
  264. {
  265. return false;
  266. };
  267. // Restores default implementation of open with autosave
  268. LocalFile.prototype.open = DrawioFile.prototype.open;
  269. LocalFile.prototype.save = function(revision, success, error)
  270. {
  271. DrawioFile.prototype.save.apply(this, arguments);
  272. this.saveFile(revision, success, error);
  273. };
  274. LocalLibrary.prototype.save = function(revision, success, error)
  275. {
  276. this.saveFile(revision, success, error);
  277. };
  278. LocalFile.prototype.saveFile = function(revision, success, error)
  279. {
  280. var fn = mxUtils.bind(this, function()
  281. {
  282. var doSave = mxUtils.bind(this, function(data)
  283. {
  284. if (!this.savingFile)
  285. {
  286. this.savingFile = true;
  287. // Makes sure no changes get lost while the file is saved
  288. var prevModified = this.isModified;
  289. var modified = this.isModified();
  290. this.setModified(false);
  291. var fs = require('fs');
  292. fs.writeFile(this.fileObject.path, data, this.fileObject.encoding, mxUtils.bind(this, function (e)
  293. {
  294. if (e)
  295. {
  296. this.savingFile = false;
  297. this.isModified = prevModified;
  298. this.setModified(modified || this.isModified());
  299. if (error != null)
  300. {
  301. error();
  302. }
  303. }
  304. else
  305. {
  306. this.savingFile = false;
  307. this.isModified = prevModified;
  308. this.contentChanged();
  309. if (success != null)
  310. {
  311. success();
  312. }
  313. }
  314. }));
  315. }
  316. else
  317. {
  318. // TODO, already saving. Need a better error
  319. error();
  320. }
  321. });
  322. if (!/(\.png)$/i.test(this.fileObject.name))
  323. {
  324. doSave(this.getData());
  325. }
  326. else
  327. {
  328. this.ui.exportToCanvas(mxUtils.bind(this, function(canvas)
  329. {
  330. try
  331. {
  332. var data = canvas.toDataURL('image/png');
  333. data = this.ui.writeGraphModelToPng(data, 'zTXt', 'mxGraphModel',
  334. atob(this.ui.editor.graph.compress(mxUtils.getXml(this.ui.editor.getGraphXml()))));
  335. doSave(data, 'base64');
  336. }
  337. catch (e)
  338. {
  339. if (error != null)
  340. {
  341. error(e);
  342. }
  343. }
  344. }), null, null, null, mxUtils.bind(this, function(e)
  345. {
  346. if (error != null)
  347. {
  348. error(e);
  349. }
  350. }));
  351. }
  352. });
  353. if (this.fileObject == null)
  354. {
  355. const electron = require('electron');
  356. var remote = electron.remote;
  357. var dialog = remote.dialog;
  358. var path = dialog.showSaveDialog();
  359. // chrome.fileSystem.chooseEntry({type: 'saveFile',
  360. // accepts: [(this.constructor == LocalFile) ? {description: 'Draw.io Diagram (.xml)',
  361. // extensions: ['xml']} : {description: 'Draw.io Library (.xml)',
  362. // extensions: ['xml']}]}, mxUtils.bind(this, function(xmlFile)
  363. if (path != null)
  364. {
  365. this.fileObject = new Object();
  366. this.fileObject.path = path;
  367. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  368. this.fileObject.type = 'utf-8';
  369. fn();
  370. }
  371. }
  372. else
  373. {
  374. fn();
  375. }
  376. };
  377. LocalFile.prototype.saveAs = function(title, success, error)
  378. {
  379. const electron = require('electron');
  380. var remote = electron.remote;
  381. var dialog = remote.dialog;
  382. var path = dialog.showSaveDialog();
  383. // chrome.fileSystem.chooseEntry({type: 'saveFile',
  384. // accepts: [(this.constructor == LocalFile) ? {description: 'Draw.io Diagram (.xml)',
  385. // extensions: ['xml']} : {description: 'Draw.io Library (.xml)',
  386. // extensions: ['xml']}]}, mxUtils.bind(this, function(f)
  387. if (path != null)
  388. {
  389. this.fileObject = new Object();
  390. this.fileObject.path = path;
  391. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  392. this.fileObject.type = 'utf-8';
  393. this.save(false, success, error);
  394. }
  395. };
  396. App.prototype.saveFile = function(forceDialog)
  397. {
  398. var file = this.getCurrentFile();
  399. if (file != null)
  400. {
  401. if (!forceDialog && file.getTitle() != null)
  402. {
  403. file.save(true, mxUtils.bind(this, function(resp)
  404. {
  405. this.spinner.stop();
  406. this.editor.setStatus(mxResources.get('allChangesSaved'));
  407. }), mxUtils.bind(this, function(resp)
  408. {
  409. this.editor.setStatus('');
  410. this.handleError(resp, (resp != null) ? mxResources.get('errorSavingFile') : null);
  411. }));
  412. }
  413. else
  414. {
  415. file.saveAs(null, mxUtils.bind(this, function(resp)
  416. {
  417. this.spinner.stop();
  418. this.editor.setStatus(mxResources.get('allChangesSaved'));
  419. }), mxUtils.bind(this, function(resp)
  420. {
  421. this.editor.setStatus('');
  422. this.handleError(resp, (resp != null) ? mxResources.get('errorSavingFile') : null);
  423. }));
  424. }
  425. }
  426. };
  427. /**
  428. * Translates this point by the given vector.
  429. *
  430. * @param {number} dx X-coordinate of the translation.
  431. * @param {number} dy Y-coordinate of the translation.
  432. */
  433. App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn)
  434. {
  435. mode = (mode != null) ? mode : this.mode;
  436. noSpin = (noSpin != null) ? noSpin : false;
  437. noReload = (noReload != null) ? noReload : false;
  438. var xml = this.createLibraryDataFromImages(images);
  439. var error = mxUtils.bind(this, function(resp)
  440. {
  441. this.spinner.stop();
  442. if (fn != null)
  443. {
  444. fn();
  445. }
  446. // Null means cancel by user and is ignored
  447. if (resp != null)
  448. {
  449. this.handleError(resp, mxResources.get('errorSavingFile'));
  450. }
  451. });
  452. // Handles special case for local libraries
  453. if (file == null)
  454. {
  455. file = new LocalLibrary(this, xml, name);
  456. }
  457. if (noSpin || this.spinner.spin(document.body, mxResources.get('saving')))
  458. {
  459. file.setData(xml);
  460. var doSave = mxUtils.bind(this, function()
  461. {
  462. file.save(true, mxUtils.bind(this, function(resp)
  463. {
  464. this.spinner.stop();
  465. this.hideDialog(true);
  466. if (!noReload)
  467. {
  468. this.libraryLoaded(file, images)
  469. }
  470. if (fn != null)
  471. {
  472. fn();
  473. }
  474. }), error);
  475. });
  476. if (name != file.getTitle())
  477. {
  478. var oldHash = file.getHash();
  479. file.rename(name, mxUtils.bind(this, function(resp)
  480. {
  481. // Change hash in stored settings
  482. if (file.constructor != LocalLibrary && oldHash != file.getHash())
  483. {
  484. mxSettings.removeCustomLibrary(oldHash);
  485. mxSettings.addCustomLibrary(file.getHash());
  486. }
  487. // Workaround for library files changing hash so
  488. // the old library cannot be removed from the
  489. // sidebar using the updated file in libraryLoaded
  490. this.removeLibrarySidebar(oldHash);
  491. doSave();
  492. }), error)
  493. }
  494. else
  495. {
  496. doSave();
  497. }
  498. }
  499. };
  500. App.prototype.doSaveLocalFile = function(data, filename, mimeType, base64Encoded)
  501. {
  502. chrome.fileSystem.chooseEntry({type: 'saveFile', suggestedName: filename, acceptsAllTypes: true}, mxUtils.bind(this, function(fileEntry)
  503. {
  504. if (!chrome.runtime.lastError)
  505. {
  506. fileEntry.createWriter(mxUtils.bind(this, function(writer)
  507. {
  508. writer.onwriteend = mxUtils.bind(this, function()
  509. {
  510. writer.onwriteend = null;
  511. writer.write((base64Encoded) ? this.base64ToBlob(data, mimeType) : new Blob([data], {type: mimeType}));
  512. });
  513. writer.onerror = mxUtils.bind(this, function(e)
  514. {
  515. this.handleError(e);
  516. });
  517. writer.truncate(0);
  518. }));
  519. }
  520. else if (chrome.runtime.lastError.message != 'User cancelled')
  521. {
  522. this.handleError(chrome.runtime.lastError);
  523. }
  524. }));
  525. };
  526. })();