Home Reference Source

cables_dev/cables_electron/src/electron/main.js

  1. import { app, BrowserWindow, dialog, Menu, shell, clipboard, nativeTheme, nativeImage } from "electron";
  2. import path from "path";
  3. import localShortcut from "electron-localshortcut";
  4. import fs from "fs";
  5. import os from "os";
  6. import electronEndpoint from "./electron_endpoint.js";
  7. import electronApi from "./electron_api.js";
  8. import logger from "../utils/logger.js";
  9. import settings from "./electron_settings.js";
  10. import doc from "../utils/doc_util.js";
  11. import projectsUtil from "../utils/projects_util.js";
  12. import filesUtil from "../utils/files_util.js";
  13. import helper from "../utils/helper_util.js";
  14. // this needs to be imported like this to not have to asarUnpack the entire nodejs world - sm,25.07.2024
  15. import Npm from "../../node_modules/npm/lib/npm.js";
  16. import opsUtil from "../utils/ops_util.js";
  17.  
  18. app.commandLine.appendSwitch("disable-http-cache", "true");
  19. app.commandLine.appendSwitch("force_high_performance_gpu", "true");
  20. app.commandLine.appendSwitch("lang", "EN");
  21. app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
  22. app.commandLine.appendSwitch("no-user-gesture-required", "true");
  23. app.commandLine.appendSwitch("disable-hid-blocklist", "true");
  24. app.commandLine.appendSwitch("enable-web-bluetooth");
  25. app.disableDomainBlockingFor3DAPIs();
  26.  
  27. logger.info("--- starting");
  28.  
  29. class ElectronApp
  30. {
  31. constructor()
  32. {
  33. this._log = logger;
  34. this.appName = "name" in app ? app.name : app.getName();
  35. this.appIcon = nativeImage.createFromPath("../../resources/cables.png");
  36.  
  37. this._defaultWindowBounds = { "width": 1920, "height": 1080 };
  38.  
  39. this.editorWindow = null;
  40.  
  41.  
  42. settings.set("uiLoadStart", this._log.loadStart);
  43. this._log.logStartup("started electron");
  44.  
  45. process.on("uncaughtException", (error) =>
  46. {
  47. this._handleError(this.appName + " encountered an error", error);
  48. });
  49.  
  50. process.on("unhandledRejection", (error) =>
  51. {
  52. this._handleError(this.appName + " encountered an error", error);
  53. });
  54.  
  55. app.on("browser-window-created", (event, win) =>
  56. {
  57. if (settings.get(settings.OPEN_DEV_TOOLS_FIELD))
  58. {
  59. win.webContents.once("dom-ready", this._toggleDevTools.bind(this));
  60. }
  61. });
  62.  
  63. nativeTheme.themeSource = "dark";
  64. }
  65.  
  66. init()
  67. {
  68. this._createWindow();
  69. this._createMenu();
  70. this._loadNpm();
  71. }
  72.  
  73. _loadNpm(cb = null)
  74. {
  75. try
  76. {
  77. this._npm = new Npm({
  78. "argv": [
  79. "--no-save",
  80. "--no-package-lock",
  81. "--legacy-peer-deps",
  82. "--no-progress",
  83. "--no-color",
  84. "--yes",
  85. "--no-fund",
  86. "--no-audit"
  87. ],
  88. "excludeNpmCwd": true,
  89. });
  90. this._npm.load().then(() =>
  91. {
  92. this._log.info("loaded npm", this._npm.version);
  93. });
  94. }
  95. catch (e)
  96. {
  97. this._log.error("failed to load npm", e);
  98. }
  99. }
  100.  
  101. async installPackages(targetDir, packageNames, opName = null)
  102. {
  103. if (!targetDir || !packageNames || packageNames.length === 0) return { "stdout": "nothing to install", "packages": [] };
  104.  
  105. const result = await this._installNpmPackages(packageNames, targetDir, opName);
  106. if (opName) result.opName = opName;
  107.  
  108. if (fs.existsSync(path.join(targetDir, "package.json"))) fs.rmSync(path.join(targetDir, "package.json"));
  109. if (fs.existsSync(path.join(targetDir, "package-lock.json"))) fs.rmSync(path.join(targetDir, "package-lock.json"));
  110. return result;
  111. }
  112.  
  113. async addOpPackage(targetDir, opPackageLocation)
  114. {
  115. if (!targetDir || !opPackageLocation) return { "stdout": "nothing to install", "packages": [] };
  116.  
  117. const dirName = path.join(os.tmpdir(), "cables-oppackage-");
  118. const tmpDir = fs.mkdtempSync(dirName);
  119.  
  120. const result = await this._installNpmPackages([opPackageLocation], tmpDir);
  121.  
  122. const nodeModulesDir = path.join(tmpDir, "node_modules");
  123. if (fs.existsSync(nodeModulesDir))
  124. {
  125. const importedDocs = doc.getOpDocsInDir(nodeModulesDir);
  126. Object.keys(importedDocs).forEach((opDocFile) =>
  127. {
  128. const opDoc = importedDocs[opDocFile];
  129. const opName = opDoc.name;
  130. const sourceDir = path.join(nodeModulesDir, path.dirname(opDocFile));
  131. let opTargetDir = path.join(targetDir, opsUtil.getOpTargetDir(opName, true));
  132. fs.cpSync(sourceDir, opTargetDir, { "recursive": true });
  133. result.packages.push(opName);
  134. });
  135. fs.rmSync(tmpDir, { "recursive": true });
  136. }
  137. return result;
  138. }
  139.  
  140. async _installNpmPackages(packageNames, targetDir, opName = null)
  141. {
  142. this._npm.config.localPrefix = targetDir;
  143.  
  144. let result = { "stdout": "", "stderr": "", "packages": packageNames, "targetDir": targetDir };
  145.  
  146. const oldConsole = console.log;
  147. const logToVariable = (level, ...args) =>
  148. {
  149. switch (level)
  150. {
  151. case "standard":
  152. args.forEach((arg) =>
  153. {
  154. result.stdout += arg;
  155. });
  156. break;
  157. case "error":
  158. args.forEach((arg) =>
  159. {
  160. result.error = true;
  161. result.stderr += arg;
  162. });
  163. break;
  164. case "buffer":
  165. case "flush":
  166. default:
  167. }
  168. };
  169. process.on("output", logToVariable);
  170. console.log = (l) => { result.stdout += l; };
  171. this._log.debug("installing", packageNames, "to", targetDir);
  172. try
  173. {
  174. await this._npm.exec("install", packageNames);
  175. }
  176. catch (e)
  177. {
  178. result.exception = String(e);
  179. result.error = true;
  180. result.stderr += e + e.stderr;
  181. if (e.script && e.script.includes("gyp")) result.nativeCompile = true;
  182. }
  183. process.off("output", logToVariable);
  184. console.log = oldConsole;
  185. if (result.exception && result.exception === "Error: command failed")
  186. {
  187. if (result.nativeCompile)
  188. {
  189. if (targetDir.includes(" "))
  190. {
  191. result.stderr = "tried to compile native module <a href=\"https://github.com/nodejs/node-gyp/issues/65\" target=\"_blank\">with a space in the pathname</a>, try moving your op...";
  192. }
  193. else
  194. {
  195. result.stderr = "failed to natively compile using node-gyp";
  196. if (opName)
  197. {
  198. const onClick = "CABLES.CMD.STANDALONE.openOpDir('', '" + opName + "');";
  199. const opDir = opsUtil.getOpSourceDir(opName);
  200. result.stderr += ", try running `npm --prefix ./ install " + packageNames.join(" ") + "` manually <a onclick=\"" + onClick + "\">in the op dir</a>: `" + opDir + "`";
  201. }
  202. }
  203. }
  204. }
  205. return result;
  206. }
  207.  
  208. _createWindow()
  209. {
  210. let patchFile = null;
  211. const openLast = settings.getUserSetting("openlastproject", false) || this._initialPatchFile;
  212. if (openLast)
  213. {
  214. const projectFile = this._initialPatchFile || settings.getCurrentProjectFile();
  215. if (fs.existsSync(projectFile)) patchFile = projectFile;
  216. this._initialPatchFile = null;
  217. }
  218.  
  219. const defaultWindowOptions = {
  220. "width": 1920,
  221. "height": 1080,
  222. "backgroundColor": "#222",
  223. "icon": this.appIcon,
  224. "autoHideMenuBar": true,
  225. "webPreferences": {
  226. "defaultEncoding": "utf-8",
  227. "partition": settings.SESSION_PARTITION,
  228. "nodeIntegration": true,
  229. "nodeIntegrationInWorker": true,
  230. "nodeIntegrationInSubFrames": true,
  231. "contextIsolation": false,
  232. "sandbox": false,
  233. "webSecurity": false,
  234. "allowRunningInsecureContent": true,
  235. "plugins": true,
  236. "experimentalFeatures": true,
  237. "v8CacheOptions": "none",
  238. "backgroundThrottling": false,
  239. "autoplayPolicy": "no-user-gesture-required"
  240. }
  241. };
  242.  
  243. this.editorWindow = new BrowserWindow(defaultWindowOptions);
  244.  
  245. let windowBounds = this._defaultWindowBounds;
  246. if (settings.getUserSetting("storeWindowBounds", true))
  247. {
  248. const userWindowBounds = settings.get(settings.WINDOW_BOUNDS);
  249. if (userWindowBounds) windowBounds = userWindowBounds;
  250. }
  251.  
  252. this.editorWindow.setBounds(windowBounds);
  253.  
  254. this._initCaches(() =>
  255. {
  256. this._registerListeners();
  257. this._registerShortcuts();
  258. this.openPatch(patchFile, false).then(() =>
  259. {
  260. this._log.logStartup("electron loaded");
  261. });
  262. });
  263. }
  264.  
  265. async pickProjectFileDialog()
  266. {
  267. let title = "select patch";
  268. let properties = ["openFile"];
  269. return this._projectFileDialog(title, properties);
  270. }
  271.  
  272. async pickFileDialog(filePath, asUrl = false, filter = [])
  273. {
  274. let title = "select file";
  275. let properties = ["openFile"];
  276. return this._fileDialog(title, filePath, asUrl, filter, properties);
  277. }
  278.  
  279. async saveFileDialog(defaultPath, title = null, properties = [], filters = [])
  280. {
  281. title = title || "select directory";
  282. properties = properties || ["createDirectory"];
  283. return dialog.showSaveDialog(this.editorWindow, {
  284. "title": title,
  285. "defaultPath": defaultPath,
  286. "properties": properties,
  287. "filters": filters
  288. }).then((result) =>
  289. {
  290. if (!result.canceled)
  291. {
  292. return result.filePath;
  293. }
  294. else
  295. {
  296. return null;
  297. }
  298. });
  299. }
  300.  
  301. async pickDirDialog(defaultPath = null)
  302. {
  303. let title = "select file";
  304. let properties = ["openDirectory", "createDirectory"];
  305. return this._dirDialog(title, properties, defaultPath);
  306. }
  307.  
  308. async exportProjectFileDialog(exportName)
  309. {
  310. const extensions = [];
  311. extensions.push("zip");
  312.  
  313. let title = "select directory";
  314. let properties = ["createDirectory"];
  315. return dialog.showSaveDialog(this.editorWindow, {
  316. "title": title,
  317. "defaultPath": exportName,
  318. "properties": properties,
  319. "filters": [{
  320. "name": "cables project",
  321. "extensions": extensions,
  322. }]
  323. }).then((result) =>
  324. {
  325. if (!result.canceled)
  326. {
  327. return result.filePath;
  328. }
  329. else
  330. {
  331. return null;
  332. }
  333. });
  334. }
  335.  
  336. async saveProjectFileDialog(defaultPath)
  337. {
  338. const extensions = [];
  339. extensions.push(projectsUtil.CABLES_PROJECT_FILE_EXTENSION);
  340.  
  341. let title = "select patch";
  342. let properties = ["createDirectory"];
  343. return dialog.showSaveDialog(this.editorWindow, {
  344. "title": title,
  345. "properties": properties,
  346. "defaultPath": defaultPath,
  347. "filters": [{
  348. "name": "cables project",
  349. "extensions": extensions,
  350. }]
  351. }).then((result) =>
  352. {
  353. if (!result.canceled)
  354. {
  355. let patchFile = result.filePath;
  356. if (!patchFile.endsWith(projectsUtil.CABLES_PROJECT_FILE_EXTENSION))
  357. {
  358. patchFile += "." + projectsUtil.CABLES_PROJECT_FILE_EXTENSION;
  359. }
  360. const currentProject = settings.getCurrentProject();
  361. if (currentProject)
  362. {
  363. currentProject.name = path.basename(patchFile);
  364. currentProject.summary = currentProject.summary || {};
  365. currentProject.summary.title = currentProject.name;
  366. projectsUtil.writeProjectToFile(patchFile, currentProject);
  367. }
  368. return patchFile;
  369. }
  370. else
  371. {
  372. return null;
  373. }
  374. });
  375. }
  376.  
  377. async pickOpDirDialog()
  378. {
  379. const title = "select op directory";
  380. const properties = ["openDirectory", "createDirectory"];
  381. return this._dirDialog(title, properties);
  382. }
  383.  
  384. _createMenu()
  385. {
  386. const isOsX = process.platform === "darwin";
  387. let devToolsAcc = "CmdOrCtrl+Shift+I";
  388. let inspectElementAcc = "CmdOrCtrl+Shift+C";
  389. let consoleAcc = "CmdOrCtrl+Shift+J";
  390. if (isOsX)
  391. {
  392. devToolsAcc = "CmdOrCtrl+Option+I";
  393. inspectElementAcc = "CmdOrCtrl+Option+C";
  394. consoleAcc = "CmdOrCtrl+Option+J";
  395. }
  396. const aboutMenu = [];
  397. aboutMenu.push({
  398. "label": "About Cables",
  399. "click": () => { this._showAbout(); }
  400. });
  401. aboutMenu.push({ "type": "separator" });
  402. if (isOsX)
  403. {
  404. aboutMenu.push({ "role": "services" });
  405. aboutMenu.push({ "type": "separator" });
  406. aboutMenu.push({ "role": "hide", "label": "Hide Cables" });
  407. aboutMenu.push({ "role": "hideOthers" });
  408. aboutMenu.push({ "role": "unhide" });
  409. aboutMenu.push({ "type": "separator" });
  410. }
  411.  
  412. aboutMenu.push({
  413. "role": "quit",
  414. "label": "Quit",
  415. "accelerator": "CmdOrCtrl+Q",
  416. "click": () => { app.quit(); }
  417. });
  418.  
  419. const menuTemplate = [
  420. {
  421. "role": "appMenu",
  422. "label": "Cables",
  423. "submenu": aboutMenu
  424. },
  425. {
  426. "label": "File",
  427. "submenu": [
  428. {
  429. "label": "New patch",
  430. "accelerator": "CmdOrCtrl+N",
  431. "click": () =>
  432. {
  433. this.openPatch();
  434. }
  435. },
  436. {
  437. "label": "Open patch",
  438. "accelerator": "CmdOrCtrl+O",
  439. "click": () =>
  440. {
  441. this.pickProjectFileDialog();
  442. }
  443. },
  444. {
  445. "label": "Open Recent",
  446. "role": "recentdocuments",
  447. "submenu": [
  448. {
  449. "label": "Clear Recent",
  450. "role": "clearrecentdocuments"
  451. }
  452. ]
  453. }
  454. ]
  455. },
  456. {
  457. "label": "Edit",
  458. "submenu": [
  459. { "role": "undo" }, { "role": "redo" },
  460. { "type": "separator" },
  461. { "role": "cut" },
  462. { "role": "copy" },
  463. { "role": "paste" },
  464. { "role": "selectAll" },
  465.  
  466. ]
  467. },
  468. {
  469. "label": "Window",
  470. "submenu": [
  471. {
  472. "role": "minimize",
  473. },
  474. {
  475. "role": "zoom",
  476. "visible": isOsX
  477. },
  478. { "role": "togglefullscreen" },
  479. {
  480. "label": "Reset Size and Position",
  481. "click": () =>
  482. {
  483. this._resetSizeAndPostion();
  484. }
  485. },
  486. { "type": "separator" },
  487. {
  488. "label": "Zoom In",
  489. "accelerator": "CmdOrCtrl+Plus",
  490. "click": () =>
  491. {
  492. this._zoomIn();
  493. }
  494. },
  495. {
  496. "label": "Zoom Out",
  497. "accelerator": "CmdOrCtrl+-",
  498. "click": () =>
  499. {
  500. this._zoomOut();
  501. }
  502. },
  503. {
  504. "label": "Reset Zoom",
  505. "click": () =>
  506. {
  507. this._resetZoom();
  508. }
  509. },
  510. { "type": "separator" },
  511. {
  512. "label": "Developer Tools",
  513. "accelerator": devToolsAcc,
  514. "click": () =>
  515. {
  516. this._toggleDevTools();
  517. }
  518. },
  519. {
  520. "label": "Insepect Elements",
  521. "accelerator": inspectElementAcc,
  522. "click": () =>
  523. {
  524. this._inspectElements();
  525. }
  526. },
  527. {
  528. "label": "JavaScript Console",
  529. "accelerator": consoleAcc,
  530. "click": () =>
  531. {
  532. this._toggleDevTools();
  533. }
  534. },
  535. { "role": "close", "visible": false }
  536. ]
  537. }
  538. ];
  539. // prevent osx from showin currently running process as name (e.g. `npm`)
  540. if (process.platform == "darwin") { menuTemplate.unshift({ "label": "" }); }
  541. let menu = Menu.buildFromTemplate(menuTemplate);
  542.  
  543. Menu.setApplicationMenu(menu);
  544. }
  545.  
  546. openFile(patchFile)
  547. {
  548. if (this.editorWindow)
  549. {
  550. this.openPatch(patchFile, true);
  551. }
  552. else
  553. {
  554. // opened by double-clicking and starting the app
  555. this._initialPatchFile = patchFile;
  556. }
  557. }
  558.  
  559. async openPatch(patchFile, rebuildCache = true)
  560. {
  561. this._unsavedContentLeave = false;
  562. const open = async () =>
  563. {
  564. electronApi.loadProject(patchFile, null, rebuildCache);
  565. this.updateTitle();
  566. await this.editorWindow.loadFile("index.html");
  567. const userZoom = settings.get(settings.WINDOW_ZOOM_FACTOR); // maybe set stored zoom later
  568. this._resetZoom();
  569. if (rebuildCache) doc.rebuildOpCaches(() => { this._log.logStartup("rebuild op caches"); }, ["core", "teams", "extensions"], true);
  570. };
  571.  
  572. if (this.isDocumentEdited())
  573. {
  574. const leave = this._unsavedContentDialog();
  575. if (leave)
  576. {
  577. await open();
  578. }
  579. }
  580. else
  581. {
  582. await open();
  583. }
  584. }
  585.  
  586. updateTitle()
  587. {
  588. const buildInfo = settings.getBuildInfo();
  589. let title = "cables";
  590. if (buildInfo && buildInfo.api)
  591. {
  592. if (buildInfo.api.version)
  593. {
  594. title += " - " + buildInfo.api.version;
  595. }
  596. }
  597. const projectFile = settings.getCurrentProjectFile();
  598. if (projectFile)
  599. {
  600. title = title + " - " + projectFile;
  601. }
  602. const project = settings.getCurrentProject();
  603. if (project)
  604. {
  605. this.sendTalkerMessage("updatePatchName", { "name": project.name });
  606. this.sendTalkerMessage("updatePatchSummary", project.summary);
  607. }
  608.  
  609. this.editorWindow.setTitle(title);
  610. }
  611.  
  612. _dirDialog(title, properties, defaultPath = null)
  613. {
  614. const options = {
  615. "title": title,
  616. "properties": properties
  617. };
  618. if (defaultPath) options.defaultPath = defaultPath;
  619. return dialog.showOpenDialog(this.editorWindow, options).then((result) =>
  620. {
  621. if (!result.canceled)
  622. {
  623. return result.filePaths[0];
  624. }
  625. else
  626. {
  627. return null;
  628. }
  629. });
  630. }
  631.  
  632. _fileDialog(title, filePath = null, asUrl = false, extensions = ["*"], properties = null)
  633. {
  634. if (extensions)
  635. {
  636. extensions.forEach((ext, i) =>
  637. {
  638. if (ext.startsWith(".")) extensions[i] = ext.replace(".", "");
  639. });
  640. }
  641. const options = {
  642. "title": title,
  643. "properties": properties,
  644. "filters": [{ "name": "Assets", "extensions": extensions }]
  645. };
  646. if (filePath) options.defaultPath = filePath;
  647. return dialog.showOpenDialog(this.editorWindow, options).then((result) =>
  648. {
  649. if (!result.canceled)
  650. {
  651. if (!asUrl) return result.filePaths[0];
  652. return helper.pathToFileURL(result.filePaths[0]);
  653. }
  654. else
  655. {
  656. return null;
  657. }
  658. });
  659. }
  660.  
  661. _projectFileDialog(title, properties)
  662. {
  663. const extensions = [];
  664. extensions.push(projectsUtil.CABLES_PROJECT_FILE_EXTENSION);
  665.  
  666. return dialog.showOpenDialog(this.editorWindow, {
  667. "title": title,
  668. "properties": properties,
  669. "filters": [{
  670. "name": "cables project",
  671. "extensions": extensions,
  672. }]
  673. }).then((result) =>
  674. {
  675. if (!result.canceled)
  676. {
  677. let projectFile = result.filePaths[0];
  678. this.openPatch(projectFile);
  679. return projectFile;
  680. }
  681. else
  682. {
  683. return null;
  684. }
  685. });
  686. }
  687.  
  688. reload()
  689. {
  690. const projectFile = settings.getCurrentProjectFile();
  691. this.openPatch(projectFile, false).then(() => { this._log.debug("reloaded", projectFile); });
  692. }
  693.  
  694. setDocumentEdited(edited)
  695. {
  696. this._contentChanged = edited;
  697. }
  698.  
  699.  
  700. isDocumentEdited()
  701. {
  702. return this._contentChanged || this.editorWindow.isDocumentEdited();
  703. }
  704.  
  705. cycleFullscreen()
  706. {
  707. if (this.editorWindow.isFullScreen())
  708. {
  709. this.editorWindow.setMenuBarVisibility(true);
  710. this.editorWindow.setFullScreen(false);
  711. }
  712. else
  713. {
  714. this.editorWindow.setMenuBarVisibility(false);
  715. this.editorWindow.setFullScreen(true);
  716. }
  717. }
  718.  
  719. sendTalkerMessage(cmd, data)
  720. {
  721. this.editorWindow.webContents.send("talkerMessage", { "cmd": cmd, "data": data });
  722. }
  723.  
  724. _registerShortcuts()
  725. {
  726. let devToolsAcc = "CmdOrCtrl+Shift+I";
  727. let inspectElementAcc = "CmdOrCtrl+Shift+C";
  728. if (process.platform === "darwin") devToolsAcc = "CmdOrCtrl+Option+I";
  729.  
  730. // https://github.com/sindresorhus/electron-debug/blob/main/index.js
  731. localShortcut.register(this.editorWindow, inspectElementAcc, this._inspectElements.bind(this));
  732. localShortcut.register(this.editorWindow, devToolsAcc, this._toggleDevTools.bind(this));
  733. localShortcut.register(this.editorWindow, "F12", this._toggleDevTools.bind(this));
  734. localShortcut.register(this.editorWindow, "CommandOrControl+R", this._reloadWindow.bind(this));
  735. localShortcut.register(this.editorWindow, "F5", this._reloadWindow.bind(this));
  736. localShortcut.register(this.editorWindow, "CmdOrCtrl+O", this.pickProjectFileDialog.bind(this));
  737. localShortcut.register(this.editorWindow, "CmdOrCtrl+=", this._zoomIn.bind(this));
  738. localShortcut.register(this.editorWindow, "CmdOrCtrl+Plus", this._zoomIn.bind(this));
  739. localShortcut.register(this.editorWindow, "CmdOrCtrl+-", this._zoomOut.bind(this));
  740. }
  741.  
  742. _toggleDevTools()
  743. {
  744. if (this.editorWindow.webContents.isDevToolsOpened())
  745. {
  746. this.editorWindow.webContents.closeDevTools();
  747. }
  748. else
  749. {
  750. this.editorWindow.webContents.openDevTools({ "mode": "previous" });
  751. }
  752. }
  753.  
  754. _inspectElements()
  755. {
  756. const inspect = () =>
  757. {
  758. this.editorWindow.devToolsWebContents.executeJavaScript("DevToolsAPI.enterInspectElementMode()");
  759. };
  760.  
  761. if (this.editorWindow.webContents.isDevToolsOpened())
  762. {
  763. inspect();
  764. }
  765. else
  766. {
  767. this.editorWindow.webContents.once("devtools-opened", inspect);
  768. this.editorWindow.openDevTools();
  769. }
  770. }
  771.  
  772. _reloadWindow()
  773. {
  774. this.editorWindow.webContents.reloadIgnoringCache();
  775. }
  776.  
  777. _registerListeners()
  778. {
  779. app.on("browser-window-created", (e, win) =>
  780. {
  781. win.setMenuBarVisibility(false);
  782. });
  783.  
  784. this.editorWindow.on("close", () =>
  785. {
  786. if (settings.getUserSetting("storeWindowBounds", true)) settings.set(settings.WINDOW_BOUNDS, this.editorWindow.getBounds());
  787. });
  788.  
  789. this.editorWindow.webContents.on("will-prevent-unload", (event) =>
  790. {
  791. if (!this._unsavedContentLeave && this.isDocumentEdited())
  792. {
  793. const leave = this._unsavedContentDialog();
  794. if (leave) event.preventDefault();
  795. }
  796. else
  797. {
  798. event.preventDefault();
  799. }
  800. });
  801. this.editorWindow.webContents.setWindowOpenHandler(({ url }) =>
  802. {
  803. if (url && url.startsWith("http"))
  804. {
  805. shell.openExternal(url);
  806. return { "action": "deny" };
  807. }
  808. return { "action": "allow" };
  809. });
  810.  
  811. this.editorWindow.webContents.on("devtools-opened", (event, win) =>
  812. {
  813. settings.set(settings.OPEN_DEV_TOOLS_FIELD, true);
  814. });
  815.  
  816. this.editorWindow.webContents.on("devtools-closed", (event, win) =>
  817. {
  818. settings.set(settings.OPEN_DEV_TOOLS_FIELD, false);
  819. });
  820.  
  821. this.editorWindow.webContents.session.on("will-download", (event, item, webContents) =>
  822. {
  823. if (item)
  824. {
  825. const filename = item.getFilename();
  826. const savePath = path.join(settings.getDownloadPath(), filename);
  827. // Set the save path, making Electron not to prompt a save dialog.
  828. item.setSavePath(savePath);
  829. const fileUrl = helper.pathToFileURL(savePath);
  830. const cablesUrl = fileUrl.replace("file:", "cables:///openDir/");
  831. const link = "<a href=\"" + cablesUrl + "\" download>" + savePath + "</a>";
  832. this.sendTalkerMessage("notify", { "msg": "File saved to " + link });
  833. }
  834. });
  835. }
  836.  
  837. _zoomIn()
  838. {
  839. let newZoom = this.editorWindow.webContents.getZoomFactor() + 0.2;
  840. this.editorWindow.webContents.setZoomFactor(newZoom);
  841. settings.set(settings.WINDOW_ZOOM_FACTOR, newZoom);
  842. }
  843.  
  844. _zoomOut()
  845. {
  846. let newZoom = this.editorWindow.webContents.getZoomFactor() - 0.2;
  847. newZoom = Math.round(newZoom * 100) / 100;
  848. if (newZoom > 0)
  849. {
  850. this.editorWindow.webContents.setZoomFactor(newZoom);
  851. settings.set(settings.WINDOW_ZOOM_FACTOR, newZoom);
  852. }
  853. }
  854.  
  855. _resetZoom()
  856. {
  857. this.editorWindow.webContents.setZoomFactor(1.0);
  858. }
  859.  
  860. _resetSizeAndPostion()
  861. {
  862. if (this.editorWindow)
  863. {
  864. this.editorWindow.setBounds(this._defaultWindowBounds);
  865. this.editorWindow.center();
  866. }
  867. }
  868.  
  869. _initCaches(cb)
  870. {
  871. doc.addOpsToLookup([], true);
  872. cb();
  873. }
  874.  
  875. _handleError(title, error)
  876. {
  877. this._log.error(title, error);
  878. if (app.isReady())
  879. {
  880. const buttons = [
  881. "&Reload",
  882. "&New Patch",
  883. "&Quit",
  884. process.platform === "darwin" ? "Copy Error" : "Copy error",
  885. ];
  886. const buttonIndex = dialog.showMessageBoxSync({
  887. "type": "error",
  888. buttons,
  889. "defaultId": 0,
  890. "noLink": true,
  891. "message": title,
  892. "detail": error.stack,
  893. "normalizeAccessKeys": true
  894. });
  895. if (buttonIndex === 0)
  896. {
  897. this.reload();
  898. }
  899. if (buttonIndex === 1)
  900. {
  901. this.openPatch(null);
  902. }
  903. if (buttonIndex === 2)
  904. {
  905. app.quit();
  906. }
  907. if (buttonIndex === 3)
  908. {
  909. clipboard.writeText(title + "\n" + error.stack);
  910. }
  911. }
  912. else
  913. {
  914. dialog.showErrorBox(title, (error.stack));
  915. }
  916. }
  917.  
  918. _unsavedContentDialog()
  919. {
  920. if (this._unsavedContentLeave) return true;
  921. const choice = dialog.showMessageBoxSync(this.editorWindow, {
  922. "type": "question",
  923. "buttons": ["Leave", "Stay"],
  924. "title": "unsaved content!",
  925. "message": "unsaved content!",
  926. "defaultId": 0,
  927. "cancelId": 1
  928. });
  929. this._unsavedContentLeave = (choice === 0);
  930. return this._unsavedContentLeave;
  931. }
  932.  
  933. _showAbout()
  934. {
  935. const options = {
  936. "icon": this.appIcon,
  937. "type": "info",
  938. "buttons": [],
  939. "message": "cables standalone",
  940. };
  941.  
  942. const buildInfo = settings.getBuildInfo();
  943. if (buildInfo)
  944. {
  945. let versionText = "";
  946. if (buildInfo.api.git)
  947. {
  948. if (buildInfo.api.version)
  949. {
  950. versionText += "version: " + buildInfo.api.version + "\n";
  951. }
  952. else
  953. {
  954. versionText += "local build" + "\n\n";
  955. if (buildInfo.api.git)
  956. {
  957. versionText += "branch: " + buildInfo.api.git.branch + "\n";
  958. versionText += "message: " + buildInfo.api.git.message + "\n";
  959. }
  960. }
  961. if (buildInfo.api.git.tag) versionText += "tag: " + buildInfo.api.git.tag + "\n";
  962. }
  963. if (buildInfo.api.platform)
  964. {
  965. versionText += "\nbuilt with:\n";
  966. if (buildInfo.api.platform.node) versionText += "node: " + buildInfo.api.platform.node + "\n";
  967. if (buildInfo.api.platform.npm) versionText += "npm: " + buildInfo.api.platform.npm;
  968. }
  969. if (process.versions)
  970. {
  971. versionText += "\n\nrunning in:\n";
  972. if (process.versions.electron) versionText += "electron: " + process.versions.electron + "\n";
  973. if (process.versions.chrome) versionText += "chrome: " + process.versions.chrome + "\n";
  974. if (process.versions.v8) versionText += "v8: " + process.versions.v8 + "\n";
  975.  
  976. if (process.versions.node) versionText += "node: " + process.versions.node + "\n";
  977. if (buildInfo.api.platform.npm) versionText += "npm: " + buildInfo.api.platform.npm;
  978. }
  979.  
  980. options.detail = versionText;
  981. }
  982. dialog.showMessageBox(options);
  983. }
  984. }
  985. Menu.setApplicationMenu(null);
  986.  
  987. const electronApp = new ElectronApp();
  988.  
  989. app.on("open-file", (e, p) =>
  990. {
  991. if (p.endsWith("." + projectsUtil.CABLES_PROJECT_FILE_EXTENSION) && fs.existsSync(p))
  992. {
  993. electronApp.openFile(p);
  994. }
  995. });
  996.  
  997. app.on("window-all-closed", () =>
  998. {
  999. app.quit();
  1000. });
  1001. app.on("will-quit", (event) =>
  1002. {
  1003. event.preventDefault();
  1004. filesUtil.unregisterChangeListeners().then(() =>
  1005. {
  1006. process.exit(0);
  1007. }).catch((e) =>
  1008. {
  1009. console.error("error during shutdown", e);
  1010. process.exit(1);
  1011. });
  1012. });
  1013.  
  1014. Menu.setApplicationMenu(null);
  1015. app.whenReady().then(() =>
  1016. {
  1017. electronApp.init();
  1018. electronApi.init();
  1019. electronEndpoint.init();
  1020. app.on("activate", () =>
  1021. {
  1022. if (BrowserWindow.getAllWindows().length === 0) electronApp.init();
  1023. });
  1024. });
  1025.  
  1026. export default electronApp;
  1027.  
  1028.