Home Reference Source

cables_dev/cables_electron/src/electron/main.js

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