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