diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7c6d53..318b18a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,9 +24,11 @@ jobs: matrix: config: - os: [self-hosted, windows-sign-pc] + id: windows - os: ubuntu-latest - - os: macos-13 - - os: macos-14 + id: linux + - os: macos-latest + id: macos-universal runs-on: ${{ matrix.config.os }} timeout-minutes: 90 @@ -92,9 +94,9 @@ jobs: npm run build - name: Upload [GitHub Actions] - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: ${{ env.JOB_TRANSFER_ARTIFACT }} + name: ${{ env.JOB_TRANSFER_ARTIFACT }}-${{ matrix.config.id }} path: dist artifacts: @@ -108,26 +110,29 @@ jobs: artifact: - path: "*-linux_x64.zip" name: Arduino-Lab-for-MicroPython_Linux_X86-64 - - path: "*-mac_x64.zip" - name: Arduino-Lab-for-MicroPython_macOS_X86-64 - - path: "*-mac_arm64.zip" - name: Arduino-Lab-for-MicroPython_macOS_arm-64 + id: linux + - path: "*-mac_universal.zip" + name: Arduino-Lab-for-MicroPython_macOS_Universal + id: macos-universal # - path: "*Windows_64bit.exe" # name: Windows_X86-64_interactive_installer + # id: windows # - path: "*Windows_64bit.msi" # name: Windows_X86-64_MSI + # id: windows - path: "*-win_x64.zip" name: Arduino-Lab-for-MicroPython_Windows_X86-64 + id: windows steps: - name: Download job transfer artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: ${{ env.JOB_TRANSFER_ARTIFACT }} + name: ${{ env.JOB_TRANSFER_ARTIFACT }}-${{ matrix.artifact.id }} path: ${{ env.JOB_TRANSFER_ARTIFACT }} - name: Upload tester build artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.artifact.name }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }} @@ -137,23 +142,25 @@ jobs: if: github.repository == 'arduino/lab-micropython-editor' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - name: Download [GitHub Actions] - uses: actions/download-artifact@v3 + - name: Download all artifacts + uses: actions/download-artifact@v4 with: - name: ${{ env.JOB_TRANSFER_ARTIFACT }} - path: ${{ env.JOB_TRANSFER_ARTIFACT }} + path: artifacts + + - name: List artifacts + run: ls -R artifacts - name: Get Tag id: tag_name run: | - echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/} + echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Publish Release [GitHub] uses: svenstaro/upload-release-action@2.2.0 with: repo_token: ${{ secrets.GITHUB_TOKEN }} release_name: ${{ steps.tag_name.outputs.TAG_NAME }} - file: ${{ env.JOB_TRANSFER_ARTIFACT }}/* + file: artifacts/**/* tag: ${{ github.ref }} file_glob: true @@ -167,7 +174,11 @@ jobs: runs-on: ubuntu-latest steps: - - name: Remove unneeded job transfer artifact + - name: Remove unneeded job transfer artifacts uses: geekyeggo/delete-artifact@v2 with: - name: ${{ env.JOB_TRANSFER_ARTIFACT }} + name: | + ${{ env.JOB_TRANSFER_ARTIFACT }}-windows + ${{ env.JOB_TRANSFER_ARTIFACT }}-linux + ${{ env.JOB_TRANSFER_ARTIFACT }}-macos-x64 + ${{ env.JOB_TRANSFER_ARTIFACT }}-macos-arm64 \ No newline at end of file diff --git a/backend/ipc.js b/backend/ipc.js index 62a404f..0716d17 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,6 +1,7 @@ const fs = require('fs') const registerMenu = require('./menu.js') const serial = require('./serial/serial.js').sharedInstance +const { shell } = require('electron'); const { openFolderDialog, @@ -138,6 +139,30 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { registerMenu(win, state) }) + ipcMain.handle('launch-app', async (event, urlScheme) => { + // Launch an external app with a custom protocol + return new Promise((resolve, reject) => { + if(app.getApplicationNameForProtocol(urlScheme) === '') { + resolve(false); // App not installed + return; + } + + try { + shell.openExternal(urlScheme).then(() => { + resolve(true); // App opened successfully + }).catch(() => { + resolve(false); // App not installed + }); + } catch (err) { + reject(err); + } + }); + }); + + ipcMain.handle('open-url', async (event, url) => { + shell.openExternal(url); + }); + win.on('close', (event) => { console.log('BrowserWindow', 'close') event.preventDefault() diff --git a/backend/menu.js b/backend/menu.js index 6db8023..fe543a2 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,18 +1,49 @@ const { app, Menu } = require('electron') +const { shortcuts, disableShortcuts } = require('./shortcuts.js') const path = require('path') const serial = require('./serial/serial.js').sharedInstance const openAboutWindow = require('about-window').default -const shortcuts = require('./shortcuts.js') + const { type } = require('os') +let appInfoWindow = null + +function closeAppInfo(win) { + disableShortcuts(win, false) + appInfoWindow.off('close', () => closeAppInfo(win)) + appInfoWindow = null + +} +function openAppInfo(win) { + if (appInfoWindow != null) { + appInfoWindow.show() + } else { + appInfoWindow = openAboutWindow({ + icon_path: path.resolve(__dirname, '../ui/arduino/media/about_image.png'), + css_path: path.resolve(__dirname, '../ui/arduino/views/about.css'), + copyright: '© Arduino SA 2022', + package_json_dir: path.resolve(__dirname, '..'), + bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues", + bug_link_text: "report an issue", + homepage: "https://labs.arduino.cc", + use_version_info: false, + win_options: { + parent: win, + modal: true, + }, + show_close_button: 'Close', + }) + appInfoWindow.on('close', () => closeAppInfo(win)); + disableShortcuts(win, true) + } +} + module.exports = function registerMenu(win, state = {}) { const isMac = process.platform === 'darwin' const template = [ ...(isMac ? [{ label: app.name, submenu: [ - { role: 'about'}, - { type: 'separator' }, { type: 'separator' }, { role: 'hide', accelerator: 'CmdOrCtrl+Shift+H' }, { role: 'hideOthers' }, @@ -24,7 +55,22 @@ module.exports = function registerMenu(win, state = {}) { { label: 'File', submenu: [ - isMac ? { role: 'close' } : { role: 'quit' } + { label: 'New', + accelerator: shortcuts.menu.NEW, + enabled: state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.NEW) + }, + { label: 'Save', + accelerator: shortcuts.menu.SAVE, + enabled: state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.SAVE) + }, + { label: 'Close tab', + accelerator: 'CmdOrCtrl+W', + enabled: state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLOSE) + }, + { role: 'quit' } ] }, { @@ -167,24 +213,8 @@ module.exports = function registerMenu(win, state = {}) { } }, { - label:'Info about this app', - click: () => { - openAboutWindow({ - icon_path: path.resolve(__dirname, '../ui/arduino/media/about_image.png'), - css_path: path.resolve(__dirname, '../ui/arduino/views/about.css'), - copyright: '© Arduino SA 2022', - package_json_dir: path.resolve(__dirname, '..'), - bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues", - bug_link_text: "report an issue", - homepage: "https://labs.arduino.cc", - use_version_info: false, - win_options: { - parent: win, - modal: true, - }, - show_close_button: 'Close', - }) - } + label:'About Arduino Lab for MicroPython', + click: () => { openAppInfo(win) } }, ] } @@ -192,16 +222,6 @@ module.exports = function registerMenu(win, state = {}) { const menu = Menu.buildFromTemplate(template) - app.setAboutPanelOptions({ - applicationName: app.name, - applicationVersion: app.getVersion(), - copyright: app.copyright, - credits: '(See "Info about this app" in the Help menu)', - authors: ['Arduino'], - website: 'https://arduino.cc', - iconPath: path.join(__dirname, '../assets/image.png'), - }) - Menu.setApplicationMenu(menu) } diff --git a/backend/serial/serial-bridge.js b/backend/serial/serial-bridge.js index 90d4d2e..a7f5f35 100644 --- a/backend/serial/serial-bridge.js +++ b/backend/serial/serial-bridge.js @@ -97,7 +97,7 @@ const SerialBridge = { return path.posix.join(navigation, target) }, getFullPath: (root, navigation, file) => { - return path.posix.join(root, navigation, file) + return path.posix.join(root, navigation.replaceAll(path.win32.sep, path.posix.sep), file.replaceAll(path.win32.sep, path.posix.sep)) }, getParentPath: (navigation) => { return path.posix.dirname(navigation) diff --git a/backend/shortcuts.js b/backend/shortcuts.js index e6b7159..925468e 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -1,29 +1,46 @@ -module.exports = { +const { globalShortcut } = require('electron') +let shortcutsActive = false +const shortcuts = { global: { + CLOSE: 'CommandOrControl+W', CONNECT: 'CommandOrControl+Shift+C', DISCONNECT: 'CommandOrControl+Shift+D', - SAVE: 'CommandOrControl+S', RUN: 'CommandOrControl+R', RUN_SELECTION: 'CommandOrControl+Alt+R', RUN_SELECTION_WL: 'CommandOrControl+Alt+S', STOP: 'CommandOrControl+H', RESET: 'CommandOrControl+Shift+R', + NEW: 'CommandOrControl+N', + SAVE: 'CommandOrControl+S', CLEAR_TERMINAL: 'CommandOrControl+L', EDITOR_VIEW: 'CommandOrControl+Alt+1', FILES_VIEW: 'CommandOrControl+Alt+2', - ESC: 'Escape' }, menu: { + CLOSE: 'CmdOrCtrl+W', CONNECT: 'CmdOrCtrl+Shift+C', DISCONNECT: 'CmdOrCtrl+Shift+D', - SAVE: 'CmdOrCtrl+S', RUN: 'CmdOrCtrl+R', RUN_SELECTION: 'CmdOrCtrl+Alt+R', RUN_SELECTION_WL: 'CmdOrCtrl+Alt+S', STOP: 'CmdOrCtrl+H', RESET: 'CmdOrCtrl+Shift+R', + NEW: 'CmdOrCtrl+N', + SAVE: 'CmdOrCtrl+S', CLEAR_TERMINAL: 'CmdOrCtrl+L', EDITOR_VIEW: 'CmdOrCtrl+Alt+1', FILES_VIEW: 'CmdOrCtrl+Alt+2' - } + }, + // Shortcuts +} + +function disableShortcuts (win, value) { + console.log(value ? 'disabling' : 'enabling', 'shortcuts') + win.send('ignore-shortcuts', value) +} + +module.exports = { + shortcuts, + disableShortcuts } + diff --git a/index.js b/index.js index a6fcc04..3b3a0fd 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,6 @@ const { app, BrowserWindow, ipcMain, dialog, globalShortcut } = require('electron') const path = require('path') const fs = require('fs') -const shortcuts = require('./backend/shortcuts.js').global - const registerIPCHandlers = require('./backend/ipc.js') const registerMenu = require('./backend/menu.js') @@ -14,8 +12,8 @@ let splashTimestamp = null function createWindow () { // Create the browser window. win = new BrowserWindow({ - width: 720, - height: 640, + width: 820, + height: 700, webPreferences: { nodeIntegration: false, webSecurity: true, @@ -63,28 +61,13 @@ function createWindow () { }) } -function shortcutAction(key) { - win.webContents.send('shortcut-cmd', key); -} - -// Shortcuts -function registerShortcuts() { - Object.entries(shortcuts).forEach(([command, shortcut]) => { - globalShortcut.register(shortcut, () => { - shortcutAction(shortcut) - }); - }) -} - app.on('ready', () => { createWindow() - registerShortcuts() win.on('focus', () => { - registerShortcuts() }) + win.on('blur', () => { - globalShortcut.unregisterAll() }) -}) \ No newline at end of file +}) diff --git a/package-lock.json b/package-lock.json index c40dfba..0ad9236 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "arduino-lab-micropython-ide", - "version": "0.11.1", + "version": "0.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arduino-lab-micropython-ide", - "version": "0.11.1", + "version": "0.20.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -210,9 +210,9 @@ } }, "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -510,15 +510,15 @@ "optional": true }, "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true }, "node_modules/@types/node": { - "version": "16.18.119", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.119.tgz", - "integrity": "sha512-ia7V9a2FnhUFfetng4/sRPBMTwHZUkPFY736rb1cg9AgG7MZdR97q7/nLR9om+sq5f1la9C857E0l/nrI0RiFQ==", + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", "dev": true }, "node_modules/@types/plist": { @@ -542,9 +542,9 @@ } }, "node_modules/@types/verror": { - "version": "1.10.10", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", - "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", "dev": true, "optional": true }, @@ -602,9 +602,9 @@ } }, "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "dev": true, "dependencies": { "humanize-ms": "^1.2.1" @@ -745,9 +745,9 @@ } }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -1246,6 +1246,19 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1498,9 +1511,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -1512,9 +1525,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { "ms": "^2.1.3" }, @@ -1609,9 +1622,9 @@ "dev": true }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "dev": true, "engines": { "node": ">=8" @@ -1756,6 +1769,20 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -2259,9 +2286,9 @@ } }, "node_modules/electron-rebuild/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2330,14 +2357,10 @@ "dev": true }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "optional": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -2347,7 +2370,33 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { "node": ">= 0.4" } @@ -2382,9 +2431,9 @@ } }, "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", "dev": true }, "node_modules/extract-zip": { @@ -2479,13 +2528,14 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -2529,7 +2579,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2564,17 +2613,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "optional": true, "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2583,6 +2636,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -2635,9 +2701,9 @@ } }, "node_modules/global-agent/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "optional": true, "bin": { @@ -2681,13 +2747,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "optional": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2749,12 +2814,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, - "optional": true, "engines": { "node": ">= 0.4" }, @@ -2762,12 +2826,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -2786,7 +2852,6 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, - "optional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -3264,9 +3329,18 @@ "node": ">=10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/micropython.js": { - "version": "1.5.0", - "resolved": "git+ssh://git@github.com/arduino/micropython.js.git#7657256c7f7c37d9f3b3112deb98d64665c68c65", + "version": "1.5.1", + "resolved": "git+ssh://git@github.com/arduino/micropython.js.git#62696afbf4c3eb2d520eebbcb676cf7b88c1d1d6", "dependencies": { "serialport": "^10.4.0" }, @@ -3463,9 +3537,9 @@ } }, "node_modules/node-abi": { - "version": "3.71.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", - "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -3475,9 +3549,9 @@ } }, "node_modules/node-abi/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3503,9 +3577,9 @@ } }, "node_modules/node-api-version/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3540,9 +3614,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", - "integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -3550,9 +3624,9 @@ } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4102,9 +4176,9 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "dev": true, "dependencies": { "ip-address": "^9.0.5", diff --git a/package.json b/package.json index b3f5abb..2074bb4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arduino-lab-micropython-ide", "productName": "Arduino Lab for MicroPython", - "version": "0.11.1", + "version": "0.20.0", "description": "Arduino Lab for MicroPython is a project sponsored by Arduino, based on original work by Murilo Polese.\nThis is an experimental pre-release software, please direct any questions exclusively to Github issues.", "main": "index.js", "scripts": { @@ -23,7 +23,10 @@ "artifactName": "${productName}-${os}_${arch}.${ext}", "extraResources": "./ui/arduino/helpers.py", "mac": { - "target": "zip", + "target": [{ + "target": "zip", + "arch": ["universal"] + }], "icon": "build_resources/icon.icns" }, "win": { diff --git a/preload.js b/preload.js index f67d43c..daef453 100644 --- a/preload.js +++ b/preload.js @@ -1,8 +1,8 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') -const shortcuts = require('./backend/shortcuts.js').global -const { emit, platform } = require('process') +const shortcuts = require('./backend/shortcuts.js').shortcuts.global +const { platform } = require('process') const SerialBridge = require('./backend/serial/serial-bridge.js') const Disk = { @@ -63,6 +63,11 @@ const Window = { callback(k); }) }, + onDisableShortcuts: (callback, value) => { + ipcRenderer.on('ignore-shortcuts', (e, value) => { + callback(value); + }) + }, beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), confirmClose: () => ipcRenderer.invoke('confirm-close'), @@ -80,6 +85,21 @@ const Window = { getShortcuts: () => shortcuts } +/** + * Launches an app using the provided URL scheme (e.g. myapp://). If the app is not installed, it will + * fallback to open the provided fallback URL. + * @param {string} url The URL scheme to use to launch the app + * @param {string} fallbackUrl The URL to open if the app is not installed + */ +async function launchApp(url, fallbackUrl) { + const success = await ipcRenderer.invoke('launch-app', url); + + if (!success) { + await ipcRenderer.invoke('open-url', fallbackUrl); // Fallback to open a URL in the default browser + } +} + +contextBridge.exposeInMainWorld('launchApp', launchApp) contextBridge.exposeInMainWorld('BridgeSerial', SerialBridge) contextBridge.exposeInMainWorld('BridgeDisk', Disk) contextBridge.exposeInMainWorld('BridgeWindow', Window) \ No newline at end of file diff --git a/ui/arduino/helpers.py b/ui/arduino/helpers.py index cb7b757..15ab644 100644 --- a/ui/arduino/helpers.py +++ b/ui/arduino/helpers.py @@ -1,6 +1,13 @@ import os import json -os.chdir('/') +import sys + +def get_root(): + if '/flash' in sys.path: + return '/flash' + else: + return '/' + def is_directory(path): return True if os.stat(path)[0] == 0x4000 else False @@ -18,6 +25,9 @@ def get_all_files(path, array_of_files = []): return array_of_files +def iget_root(): + print(get_root(), end='') + def ilist_all(path): print(json.dumps(get_all_files(path))) @@ -30,3 +40,5 @@ def delete_folder(path): if file['type'] == 'folder': os.rmdir(file['path']) os.rmdir(path) + +os.chdir(get_root()) \ No newline at end of file diff --git a/ui/arduino/index.html b/ui/arduino/index.html index 8478cc7..332dfc3 100644 --- a/ui/arduino/index.html +++ b/ui/arduino/index.html @@ -25,6 +25,7 @@ + diff --git a/ui/arduino/main.css b/ui/arduino/main.css index cc0e95c..ad0b22b 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -1,17 +1,43 @@ +/* + On 20250303, due to font files inconsistencies, we sourced the updated fonts from here: + https://github.com/alsacreations/webfonts + +*/ @font-face { - font-family: "RobotoMono", monospace; + font-family: "CodeFont"; src: - url("media/roboto-mono-latin-ext-400-normal.woff"), - url("media/roboto-mono-latin-ext-400-normal.woff2"); + url("media/Roboto-Mono-Regular-webfont.woff") format("woff"); font-weight: normal; font-style: normal; } +@font-face { + font-family: "CodeFont"; + src: + url("media/Roboto-Mono-Bold-webfont.woff") format("woff"); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: "OpenSans"; + src: url("media/opensans-regular.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "OpenSans"; + src: url("media/opensans-bold.woff2") format("woff2"); + font-weight: bold; + font-style: normal; +} + * { -moz-user-select: none; -webkit-user-select: none; user-select: none; - font-family: "RobotoMono", monospace; + font-family: "OpenSans", sans-serif; } body, html { @@ -36,7 +62,7 @@ button { align-items: center; border: none; border-radius: 45px; - background: rgba(255, 255, 255, 0.8); + background: rgba(255, 255, 255, 0.6); cursor: pointer; transition: all 0.1s; } @@ -45,22 +71,30 @@ button.small { height: 28px; border-radius: 28px; } +button.square { + border-radius: 8px; +} button.inverted:hover, button.inverted.active { - background: rgba(0, 129, 132, 0.8); + background: rgba(0, 129, 132, 0.8) !important; } button.inverted { - background: rgba(0, 129, 132, 1); + background: rgba(0, 129, 132, 1) !important; } -button[disabled] { +button[disabled], button[disabled]:hover{ + cursor: default; opacity: 0.5; - cursor: not-allowed; } -button:hover, button.active { + +button:not([disabled]):hover { background: rgba(255, 255, 255, 1); } +button.active { + background: rgba(255, 255, 255); +} + button .icon { width: 63%; height: 63%; @@ -73,6 +107,23 @@ button.small .icon { .button { position: relative; + display: flex; + flex-direction: column; + align-content: space-between; + align-items: center; + gap: .5em; + width: auto +} +.button.first{ + width:80px; +} +.button .label { + text-align: center; + color: rgba(255, 255, 255, 0.2); + font-family: "OpenSans", sans-serif; +} +.button .label.active { + color: rgba(255, 255, 255, .9); } .button .tooltip { opacity: 0; @@ -107,7 +158,7 @@ button.small .icon { height: 100%; justify-content: center; align-items: center; - font-family: "RobotoMono", monospace; + font-family: "CodeFont", monospace; overflow: hidden; } @@ -120,15 +171,66 @@ button.small .icon { flex-shrink: 0; } +#navigation-bar { + display: flex; + width: 100%; + background: #008184; + justify-content: space-between; +} + #toolbar { display: flex; - padding: 20px; + padding: 16px 10px 10px 10px; align-items: center; - gap: 20px; + gap: 16px; align-self: stretch; background: #008184; } +#app-views { + display: flex; + padding: 16px 10px 10px 10px; + width: 120px; + /* gap: 16px; */ +} + +#app-views .button{ + flex-grow: 1; + width: 100%; +} + +#app-views .button button{ + width: 100% +} + +#app-views .button .label{ + +} +#app-views .button .label.selected{ + font-weight: bold; +} + +#app-views div:first-child button{ + border-radius: 8px 0px 0px 8px; + +} +#app-views div:last-child button{ + border-radius: 0px 8px 8px 0px; + +} + +.separator { + height: 100%; + min-width: 1px; + flex-basis: fit-content; + background: #fff; + opacity: 0.7; + position: relative; + margin-left: 0.5em; + margin-right: 0.5em; + height: 65%; +} + #tabs { display: flex; padding: 10px 10px 0px 60px; @@ -167,7 +269,7 @@ button.small .icon { color: #000; font-style: normal; font-weight: 400; - line-height: 1.1em; + line-height: 1.3em; flex: 1 0 0; max-width: calc(100% - 46px); overflow: hidden; @@ -213,8 +315,12 @@ button.small .icon { font-size: 16px; height: 100%; overflow: hidden; + } +#code-editor * { + font-family: "CodeFont", monospace; +} #code-editor .cm-editor { width: 100%; height: 100%; @@ -272,10 +378,16 @@ button.small .icon { min-height: 45px; } +#panel.dialog-open { + pointer-events: none; +} + #panel #drag-handle { - width: 100%; + flex-grow: 2; height: 100%; cursor: grab; + position: absolute; + width: 100%; } #panel #drag-handle:active { @@ -291,8 +403,25 @@ button.small .icon { gap: 10px; align-self: stretch; background: #008184; + position: relative; } +.panel-bar #connection-status { + display: flex; + align-items: center; + gap: 10px; + color: white; +} + +.panel-bar #connection-status img { + width: 1.25em; + height: 1.25em; + filter: invert(1); +} + +.panel-bar .spacer { + flex-grow: 1; +} .panel-bar .term-operations { transition: opacity 0.15s; display: flex; @@ -330,7 +459,7 @@ button.small .icon { opacity: 0.5; } -#dialog { +.dialog { display: flex; flex-direction: column; justify-content: center; @@ -350,13 +479,16 @@ button.small .icon { line-height: normal; background: rgba(236, 241, 241, 0.50); } -#dialog.open { + +.dialog.open { opacity: 1; pointer-events: inherit; transition: opacity 0.15s; } -#dialog .dialog-content { + + +.dialog .dialog-content { display: flex; width: 576px; padding: 36px; @@ -372,16 +504,22 @@ button.small .icon { transition: transform 0.15s; } -#dialog.open .dialog-content { +.dialog.open .dialog-content { transform: translateY(0px); transition: transform 0.15s; } -#dialog .dialog-content > * { - width: 100%; + +.dialog .dialog-content #file-name { + font-size: 1.3em; + width:100%; + font-family: "CodeFont", monospace; } -#dialog .dialog-content .item { +.dialog .dialog-content input:focus { + outline-color: #008184; +} +.dialog .dialog-content .item { border-radius: 4.5px; display: flex; padding: 10px; @@ -391,11 +529,38 @@ button.small .icon { cursor: pointer; } -#dialog .dialog-content .item:hover { +.dialog .dialog-content .item:hover { background: #008184; color: #ffffff; } +.dialog .buttons-horizontal { + display: flex; + flex-direction: row; + justify-content: center; + width: 100%; + gap: 12px; +} +.dialog .buttons-horizontal .item { + flex-basis: 50%; + align-items: center; + background-color: #eee;; +} + +.dialog-title{ + width: 100%; + font-size: 0.8em; + padding: 0; + margin: 0; + flex-basis: max-content; +} +.dialog-feedback { + font-size: 0.6em; + align-self: stretch; + padding: 0.5em; + background: #eee; +} + #file-manager { display: flex; padding: 12px 32px 24px 32px; @@ -427,13 +592,17 @@ button.small .icon { align-self: stretch; } +#file-actions button[disabled], #file-actions button[disabled]:hover { + opacity: 0.4; +} + #file-actions button .icon { width: 100%; height: 100%; } #file-actions button:hover { - opacity: 0.2; + opacity: 0.5; } .device-header { @@ -461,7 +630,7 @@ button.small .icon { position: relative; cursor: pointer; color: #000; - font-family: "RobotoMono", monospace; + font-family: "CodeFont", monospace; font-size: 14px; font-style: normal; font-weight: 400; @@ -503,7 +672,7 @@ button.small .icon { .file-list .list { display: flex; - padding: 10px 20px; + padding: 6px 8px; flex-direction: column; align-items: flex-start; flex: 1 0 0; @@ -547,7 +716,7 @@ button.small .icon { } .file-list .item .text { color: #000; - font-family: "RobotoMono", monospace; + font-family: "CodeFont", monospace; font-size: 14px; font-style: normal; font-weight: 400; @@ -556,7 +725,7 @@ button.small .icon { width: 100%; overflow: hidden; text-overflow: ellipsis; - line-height: 1.1em; + line-height: 1.3em; } .file-list .item .checkbox .icon.off, diff --git a/ui/arduino/media/Roboto-Mono-Bold-webfont.woff b/ui/arduino/media/Roboto-Mono-Bold-webfont.woff new file mode 100644 index 0000000..f0ca065 Binary files /dev/null and b/ui/arduino/media/Roboto-Mono-Bold-webfont.woff differ diff --git a/ui/arduino/media/Roboto-Mono-Regular-webfont.woff b/ui/arduino/media/Roboto-Mono-Regular-webfont.woff new file mode 100644 index 0000000..f6a50fc Binary files /dev/null and b/ui/arduino/media/Roboto-Mono-Regular-webfont.woff differ diff --git a/ui/arduino/media/files.svg b/ui/arduino/media/files.svg deleted file mode 100644 index 59ffe3f..0000000 --- a/ui/arduino/media/files.svg +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/ui/arduino/media/install-package.svg b/ui/arduino/media/install-package.svg new file mode 100644 index 0000000..f26feff --- /dev/null +++ b/ui/arduino/media/install-package.svg @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/ui/arduino/media/opensans-bold.woff2 b/ui/arduino/media/opensans-bold.woff2 new file mode 100644 index 0000000..04b3556 Binary files /dev/null and b/ui/arduino/media/opensans-bold.woff2 differ diff --git a/ui/arduino/media/opensans-regular.woff2 b/ui/arduino/media/opensans-regular.woff2 new file mode 100644 index 0000000..8ceeab5 Binary files /dev/null and b/ui/arduino/media/opensans-regular.woff2 differ diff --git a/ui/arduino/media/roboto-mono-latin-ext-400-normal.woff b/ui/arduino/media/roboto-mono-latin-ext-400-normal.woff deleted file mode 100644 index 50943d5..0000000 Binary files a/ui/arduino/media/roboto-mono-latin-ext-400-normal.woff and /dev/null differ diff --git a/ui/arduino/media/roboto-mono-latin-ext-400-normal.woff2 b/ui/arduino/media/roboto-mono-latin-ext-400-normal.woff2 deleted file mode 100644 index cb00b8b..0000000 Binary files a/ui/arduino/media/roboto-mono-latin-ext-400-normal.woff2 and /dev/null differ diff --git a/ui/arduino/media/roboto-regular.woff2 b/ui/arduino/media/roboto-regular.woff2 new file mode 100644 index 0000000..c0b2dd6 Binary files /dev/null and b/ui/arduino/media/roboto-regular.woff2 differ diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 09a373e..966e906 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -9,17 +9,24 @@ const newFileContent = `# This program was created in Arduino Lab for MicroPytho print('Hello, MicroPython!') ` +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} -async function confirm(msg, cancelMsg, confirmMsg) { - cancelMsg = cancelMsg || 'Cancel' - confirmMsg = confirmMsg || 'Yes' +async function confirmDialog(msg, cancelMsg, confirmMsg) { + // cancelMsg = cancelMsg || 'Cancel' + // confirmMsg = confirmMsg || 'Yes' + let buttons = [] + if (confirmMsg) buttons.push(confirmMsg) + if (cancelMsg) buttons.push(cancelMsg) + let response = await win.openDialog({ type: 'question', - buttons: [cancelMsg, confirmMsg], - cancelId: 0, + buttons: buttons, + defaultId: 0, + cancelId: 1, message: msg }) - console.log('confirm', response) return Promise.resolve(response) } @@ -36,6 +43,8 @@ async function store(state, emitter) { state.boardFiles = [] state.openFiles = [] state.selectedFiles = [] + + state.newTabFileName = null state.editingFile = null state.creatingFile = null state.renamingFile = null @@ -49,10 +58,12 @@ async function store(state, emitter) { state.isConnected = false state.connectedPort = null + state.isNewFileDialogOpen = false + state.isSaving = false state.savingProgress = 0 state.isTransferring = false - state.transferringProgress = 0 + state.transferringProgress = '' state.isRemoving = false state.isLoadingFiles = false @@ -60,17 +71,9 @@ async function store(state, emitter) { state.isTerminalBound = false - const newFile = createEmptyFile({ - parentFolder: null, // Null parent folder means not saved? - source: 'disk' - }) - newFile.editor.onChange = function() { - newFile.hasChanges = true - emitter.emit('render') - } - state.openFiles.push(newFile) - state.editingFile = newFile.id + state.shortcutsDisabled = false + await createNewTab('disk') state.savedPanelHeight = PANEL_DEFAULT state.panelHeight = PANEL_CLOSED state.resizePanel = function(e) { @@ -102,28 +105,44 @@ async function store(state, emitter) { } emitter.emit('render') }) - emitter.on('change-view', (view) => { - state.view = view - if (state.view === 'file-manager') { + + emitter.on('change-view', async (view) => { + if (state.view === view) { + return + } else { + state.selectedFiles = [] + } + if(view === 'file-manager') { + emitter.emit('stop') + await sleep(250) // Give the board time to stop the program and return to the prompt emitter.emit('refresh-files') } + state.view = view emitter.emit('render') updateMenu() }) + emitter.on('launch-app', async (url, fallbackUrl) => { + window.launchApp(url, fallbackUrl) + }) + // CONNECTION DIALOG emitter.on('open-connection-dialog', async () => { log('open-connection-dialog') // UI should be in disconnected state, no need to update + dismissOpenDialogs() await serialBridge.disconnect() state.availablePorts = await getAvailablePorts() state.isConnectionDialogOpen = true emitter.emit('render') + document.addEventListener('keydown', dismissOpenDialogs) }) emitter.on('close-connection-dialog', () => { state.isConnectionDialogOpen = false + dismissOpenDialogs() emitter.emit('render') }) + emitter.on('update-ports', async () => { state.availablePorts = await getAvailablePorts() emitter.emit('render') @@ -145,7 +164,6 @@ async function store(state, emitter) { cancelId: 0, message: "Could not connect to the board. Reset it and try again." }) - console.log('Reset request acknowledged', response) emitter.emit('connection-timeout') }, 3500) try { @@ -161,6 +179,7 @@ async function store(state, emitter) { // Connected and ready state.isConnecting = false state.isConnected = true + state.boardNavigationPath = await getBoardNavigationPath() updateMenu() if (state.view === 'editor' && state.panelHeight <= PANEL_CLOSED) { state.panelHeight = state.savedPanelHeight @@ -210,7 +229,30 @@ async function store(state, emitter) { emitter.emit('render') }) + emitter.on('connect', async () => { + try { + state.availablePorts = await getAvailablePorts() + } catch(e) { + console.error('Could not get available ports. ', e) + } + + if(state.availablePorts.length == 1) { + emitter.emit('select-port', state.availablePorts[0]) + } else { + emitter.emit('open-connection-dialog') + } + }) + // CODE EXECUTION + emitter.on('run-from-button', (onlySelected = false) => { + if (onlySelected) { + runCodeSelection() + } else { + runCode() + } + }) + + emitter.on('run', async (onlySelected = false) => { log('run') const openFile = state.openFiles.find(f => f.id == state.editingFile) @@ -247,7 +289,10 @@ async function store(state, emitter) { } emitter.emit('open-panel') emitter.emit('render') - await serialBridge.getPrompt() + if (state.isConnected) { + await serialBridge.getPrompt() + } + }) emitter.on('reset', async () => { log('reset') @@ -295,7 +340,20 @@ async function store(state, emitter) { window.removeEventListener('mousemove', state.resizePanel) }) - // SAVING + // NEW FILE AND SAVING + emitter.on('create-new-file', () => { + log('create-new-file') + dismissOpenDialogs() + state.isNewFileDialogOpen = true + emitter.emit('render') + document.addEventListener('keydown', dismissOpenDialogs) + }) + emitter.on('close-new-file-dialog', () => { + state.isNewFileDialogOpen = false + + dismissOpenDialogs() + emitter.emit('render') + }) emitter.on('save', async () => { log('save') let response = canSave({ @@ -377,7 +435,7 @@ async function store(state, emitter) { } if (willOverwrite) { - const confirmation = await confirm(`You are about to overwrite the file ${openFile.fileName} on your ${openFile.source}.\n\n Are you sure you want to proceed?`, 'Cancel', 'Yes') + const confirmation = await confirmDialog(`You are about to overwrite the file ${openFile.fileName} on your ${openFile.source}.\n\n Are you sure you want to proceed?`, 'Cancel', 'Yes') if (!confirmation) { state.isSaving = false openFile.parentFolder = oldParentFolder @@ -434,7 +492,7 @@ async function store(state, emitter) { log('close-tab', id) const currentTab = state.openFiles.find(f => f.id === id) if (currentTab.hasChanges) { - let response = await confirm("Your file has unsaved changes. Are you sure you want to proceed?") + let response = await confirmDialog("Your file has unsaved changes. Are you sure you want to proceed?", "Cancel", "Yes") if (!response) return false } state.openFiles = state.openFiles.filter(f => f.id !== id) @@ -443,16 +501,7 @@ async function store(state, emitter) { if(state.openFiles.length > 0) { state.editingFile = state.openFiles[0].id } else { - const newFile = createEmptyFile({ - source: 'disk', - parentFolder: null - }) - newFile.editor.onChange = function() { - newFile.hasChanges = true - emitter.emit('render') - } - state.openFiles.push(newFile) - state.editingFile = newFile.id + await createNewTab('disk') } emitter.emit('render') @@ -513,19 +562,32 @@ async function store(state, emitter) { }) emitter.emit('render') }) - - emitter.on('create-file', (device) => { + emitter.on('create-new-tab', async (device, fileName = null) => { + const parentFolder = device == 'board' ? state.boardNavigationPath : state.diskNavigationPath + log('create-new-tab', device, fileName, parentFolder) + const success = await createNewTab(device, fileName, parentFolder) + if (success) { + emitter.emit('close-new-file-dialog') + emitter.emit('render') + } + }) + emitter.on('create-file', (device, fileName = null) => { log('create-file', device) if (state.creatingFile !== null) return + state.creatingFile = device state.creatingFolder = null + if (fileName != null) { + emitter.emit('finish-creating-file', fileName) + } emitter.emit('render') }) - emitter.on('finish-creating-file', async (value) => { - log('finish-creating', value) + + emitter.on('finish-creating-file', async (fileNameParameter) => { + log('finish-creating', fileNameParameter) if (!state.creatingFile) return - if (!value) { + if (!fileNameParameter) { state.creatingFile = null emitter.emit('render') return @@ -535,10 +597,10 @@ async function store(state, emitter) { let willOverwrite = await checkBoardFile({ root: state.boardNavigationRoot, parentFolder: state.boardNavigationPath, - fileName: value + fileName: fileNameParameter }) if (willOverwrite) { - const confirmAction = await confirm(`You are about to overwrite the file ${value} on your board.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(`You are about to overwrite the file ${fileNameParameter} on your board.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') if (!confirmAction) { state.creatingFile = null emitter.emit('render') @@ -548,9 +610,9 @@ async function store(state, emitter) { } await serialBridge.saveFileContent( serialBridge.getFullPath( - '/', + state.boardNavigationRoot, state.boardNavigationPath, - value + fileNameParameter ), newFileContent ) @@ -558,10 +620,10 @@ async function store(state, emitter) { let willOverwrite = await checkDiskFile({ root: state.diskNavigationRoot, parentFolder: state.diskNavigationPath, - fileName: value + fileName: fileNameParameter }) if (willOverwrite) { - const confirmAction = await confirm(`You are about to overwrite the file ${value} on your disk.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(`You are about to overwrite the file ${fileNameParameter} on your disk.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') if (!confirmAction) { state.creatingFile = null emitter.emit('render') @@ -573,7 +635,7 @@ async function store(state, emitter) { disk.getFullPath( state.diskNavigationRoot, state.diskNavigationPath, - value + fileNameParameter ), newFileContent ) @@ -581,6 +643,7 @@ async function store(state, emitter) { setTimeout(() => { state.creatingFile = null + dismissOpenDialogs() emitter.emit('refresh-files') emitter.emit('render') }, 200) @@ -609,7 +672,7 @@ async function store(state, emitter) { fileName: value }) if (willOverwrite) { - const confirmAction = await confirm(`You are about to overwrite ${value} on your board.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(`You are about to overwrite ${value} on your board.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') if (!confirmAction) { state.creatingFolder = null emitter.emit('render') @@ -638,7 +701,7 @@ async function store(state, emitter) { fileName: value }) if (willOverwrite) { - const confirmAction = await confirm(`You are about to overwrite ${value} on your disk.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(`You are about to overwrite ${value} on your disk.\n\nAre you sure you want to proceed?`, 'Cancel', 'Yes') if (!confirmAction) { state.creatingFolder = null emitter.emit('render') @@ -695,7 +758,7 @@ async function store(state, emitter) { } message += `Are you sure you want to proceed?` - const confirmAction = await confirm(message, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(message, 'Cancel', 'Yes') if (!confirmAction) { state.isRemoving = false emitter.emit('render') @@ -726,7 +789,7 @@ async function store(state, emitter) { if (file.source === 'board') { await serialBridge.removeFile( serialBridge.getFullPath( - '/', + state.boardNavigationRoot, state.boardNavigationPath, file.fileName ) @@ -783,7 +846,7 @@ async function store(state, emitter) { let message = `You are about to overwrite the following file/folder on your board:\n\n` message += `${value}\n\n` message += `Are you sure you want to proceed?` - const confirmAction = await confirm(message, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(message, 'Cancel', 'Yes') if (!confirmAction) { state.isSaving = false state.renamingFile = null @@ -822,7 +885,7 @@ async function store(state, emitter) { let message = `You are about to overwrite the following file/folder on your disk:\n\n` message += `${value}\n\n` message += `Are you sure you want to proceed?` - const confirmAction = await confirm(message, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(message, 'Cancel', 'Yes') if (!confirmAction) { state.isSaving = false state.renamingFile = null @@ -879,6 +942,12 @@ async function store(state, emitter) { ) ) } + // Update tab is renaming successful + const tabToRenameIndex = state.openFiles.findIndex(f => f.fileName === file.fileName && f.source === file.source && f.parentFolder === file.parentFolder) + if (tabToRenameIndex > -1) { + state.openFiles[tabToRenameIndex].fileName = value + emitter.emit('render') + } } catch (e) { alert(`The file ${file.fileName} could not be renamed to ${value}`) } @@ -907,17 +976,6 @@ async function store(state, emitter) { return } - let response = canSave({ - view: state.view, - isConnected: state.isConnected, - openFiles: state.openFiles, - editingFile: state.editingFile - }) - if (response == false) { - log("can't save") - return - } - state.isSaving = true emitter.emit('render') @@ -977,7 +1035,7 @@ async function store(state, emitter) { } if (willOverwrite) { - const confirmation = await confirm(`You are about to overwrite the file ${openFile.fileName} on your ${openFile.source}.\n\n Are you sure you want to proceed?`, 'Cancel', 'Yes') + const confirmation = await confirmDialog(`You are about to overwrite the file ${openFile.fileName} on your ${openFile.source}.\n\n Are you sure you want to proceed?`, 'Cancel', 'Yes') if (!confirmation) { state.renamingTab = null state.isSaving = false @@ -989,34 +1047,36 @@ async function store(state, emitter) { if (fullPathExists) { // SAVE FILE CONTENTS - const contents = openFile.editor.editor.state.doc.toString() - try { - if (openFile.source == 'board') { - await serialBridge.getPrompt() - await serialBridge.saveFileContent( - serialBridge.getFullPath( - state.boardNavigationRoot, - openFile.parentFolder, - oldName - ), - contents, - (e) => { - state.savingProgress = e - emitter.emit('render') - } - ) - } else if (openFile.source == 'disk') { - await disk.saveFileContent( - disk.getFullPath( - state.diskNavigationRoot, - openFile.parentFolder, - oldName - ), - contents - ) + if (openFile.hasChanges) { + const contents = openFile.editor.editor.state.doc.toString() + try { + if (openFile.source == 'board') { + await serialBridge.getPrompt() + await serialBridge.saveFileContent( + serialBridge.getFullPath( + state.boardNavigationRoot, + openFile.parentFolder, + oldName + ), + contents, + (e) => { + state.savingProgress = e + emitter.emit('render') + } + ) + } else if (openFile.source == 'disk') { + await disk.saveFileContent( + disk.getFullPath( + state.diskNavigationRoot, + openFile.parentFolder, + oldName + ), + contents + ) + } + } catch (e) { + log('error', e) } - } catch (e) { - log('error', e) } // RENAME FILE try { @@ -1127,6 +1187,9 @@ async function store(state, emitter) { log('open-selected-files') let filesToOpen = [] let filesAlreadyOpen = [] + if (state.isLoadingFiles) return + state.isLoadingFiles = true + emitter.emit('render') for (let i in state.selectedFiles) { let selectedFile = state.selectedFiles[i] if (selectedFile.type == 'folder') { @@ -1140,7 +1203,6 @@ async function store(state, emitter) { && f.source == selectedFile.source && f.parentFolder == selectedFile.parentFolder }) - console.log('already open', alreadyOpen) if (!alreadyOpen) { // This file is not open yet, @@ -1190,6 +1252,7 @@ async function store(state, emitter) { // append it to the list of files that are already open filesAlreadyOpen.push(alreadyOpen) } + } // If opening an already open file, switch to its tab @@ -1202,9 +1265,10 @@ async function store(state, emitter) { } state.openFiles = state.openFiles.concat(filesToOpen) - + state.selectedFiles = [] state.view = 'editor' updateMenu() + state.isLoadingFiles = false emitter.emit('render') }) emitter.on('open-file', (source, file) => { @@ -1240,7 +1304,7 @@ async function store(state, emitter) { willOverwrite.forEach(f => message += `${f.fileName}\n`) message += `\n` message += `Are you sure you want to proceed?` - const confirmAction = await confirm(message, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(message, 'Cancel', 'Yes') if (!confirmAction) { state.isTransferring = false emitter.emit('render') @@ -1267,7 +1331,9 @@ async function store(state, emitter) { state.transferringProgress = `${fileName}: ${progress}` emitter.emit('render') } + ) + state.transferringProgress = '' } else { await serialBridge.uploadFile( srcPath, destPath, @@ -1276,6 +1342,7 @@ async function store(state, emitter) { emitter.emit('render') } ) + state.transferringProgress = '' } } @@ -1305,7 +1372,7 @@ async function store(state, emitter) { willOverwrite.forEach(f => message += `${f.fileName}\n`) message += `\n` message += `Are you sure you want to proceed?` - const confirmAction = await confirm(message, 'Cancel', 'Yes') + const confirmAction = await confirmDialog(message, 'Cancel', 'Yes') if (!confirmAction) { state.isTransferring = false emitter.emit('render') @@ -1392,20 +1459,24 @@ async function store(state, emitter) { win.beforeClose(async () => { const hasChanges = !!state.openFiles.find(f => f.hasChanges) if (hasChanges) { - const response = await confirm('You may have unsaved changes. Are you sure you want to proceed?', 'Cancel', 'Yes') + const response = await confirmDialog('You may have unsaved changes. Are you sure you want to proceed?', 'Cancel', 'Yes') if (!response) return false } await win.confirmClose() }) - // win.shortcutCmdR(() => { - // // Only run if we can execute - - // }) - + win.onDisableShortcuts((disable) => { + state.shortcutsDisabled = disable + }), + win.onKeyboardShortcut((key) => { + if (state.isTransferring || state.isRemoving || state.isSaving || state.isConnectionDialogOpen || state.isNewFileDialogOpen) return + if (state.shortcutsDisabled) return + if (key === shortcuts.CLOSE) { + emitter.emit('close-tab', state.editingFile) + } if (key === shortcuts.CONNECT) { - emitter.emit('open-connection-dialog') + emitter.emit('connect') } if (key === shortcuts.DISCONNECT) { emitter.emit('disconnect') @@ -1435,6 +1506,10 @@ async function store(state, emitter) { if (state.view != 'editor') return stopCode() } + if (key === shortcuts.NEW) { + if (state.view != 'editor') return + emitter.emit('create-new-file') + } if (key === shortcuts.SAVE) { if (state.view != 'editor') return emitter.emit('save') @@ -1447,22 +1522,48 @@ async function store(state, emitter) { if (state.view != 'editor') return emitter.emit('change-view', 'file-manager') } - if (key === shortcuts.ESC) { - if (state.isConnectionDialogOpen) { - emitter.emit('close-connection-dialog') - } - } + // if (key === shortcuts.ESC) { + // if (state.isConnectionDialogOpen) { + // emitter.emit('close-connection-dialog') + // } + // } }) + function dismissOpenDialogs(keyEvent = null) { + if (keyEvent && keyEvent.key != 'Escape') return + document.removeEventListener('keydown', dismissOpenDialogs) + state.isConnectionDialogOpen = false + state.isNewFileDialogOpen = false + emitter.emit('render') + } + + // Ensures that even if the RUN button is clicked multiple times + // there's a 100ms delay between each execution to prevent double runs + // and entering an unstable state because of getPrompt() calls + let preventDoubleRun = false + function timedReset() { + preventDoubleRun = true + setTimeout(() => { + preventDoubleRun = false + }, 500); + + } + + function filterDoubleRun(onlySelected = false) { + if (preventDoubleRun) return + emitter.emit('run', onlySelected) + timedReset() + } + function runCode() { if (canExecute({ view: state.view, isConnected: state.isConnected })) { - emitter.emit('run') + filterDoubleRun() } } function runCodeSelection() { if (canExecute({ view: state.view, isConnected: state.isConnected })) { - emitter.emit('run', true) + filterDoubleRun(true) } } function stopCode() { @@ -1491,14 +1592,62 @@ async function store(state, emitter) { } } - function createEmptyFile({ source, parentFolder }) { - return createFile({ - fileName: generateFileName(), - parentFolder, - source, + // function createEmptyFile({ source, parentFolder }) { + // return createFile({ + // fileName: generateFileName(), + // parentFolder, + // source, + // hasChanges: true + // }) + // } + + async function createNewTab(source, fileName = null, parentFolder = null) { + const navigationPath = source == 'board' ? state.boardNavigationPath : state.diskNavigationPath + const newFile = createFile({ + fileName: fileName === null ? generateFileName() : fileName, + parentFolder: parentFolder, + source: source, hasChanges: true }) + + let fullPathExists = false + + if (parentFolder != null) { + if (source == 'board') { + await serialBridge.getPrompt() + fullPathExists = await serialBridge.fileExists( + serialBridge.getFullPath( + state.boardNavigationRoot, + newFile.parentFolder, + newFile.fileName + ) + ) + } else if (source == 'disk') { + fullPathExists = await disk.fileExists( + disk.getFullPath( + state.diskNavigationRoot, + newFile.parentFolder, + newFile.fileName + ) + ) + } + } + const tabExists = state.openFiles.find(f => f.parentFolder === newFile.parentFolder && f.fileName === newFile.fileName && f.source === newFile.source) + if (tabExists || fullPathExists) { + const confirmation = await confirmDialog(`File ${newFile.fileName} already exists on ${source}. Please choose another name.`, 'OK') + return false + } + // LEAK > listeners keep getting added and not removed when tabs are closed + // additionally I found that closing a tab has actually added an extra listener + newFile.editor.onChange = function() { + newFile.hasChanges = true + emitter.emit('render') + } + state.openFiles.push(newFile) + state.editingFile = newFile.id + return true } + } @@ -1550,6 +1699,23 @@ async function getAvailablePorts() { return await serialBridge.loadPorts() } +async function getBoardNavigationPath() { + let output = await serialBridge.execFile(await getHelperFullPath()) + output = await serialBridge.run(`iget_root()`) + let boardRoot = '' + try { + // Extracting the json output from serial response + output = output.substring( + output.indexOf('OK')+2, + output.indexOf('\x04') + ) + boardRoot = output + } catch (e) { + log('error', output) + } + return boardRoot +} + async function getBoardFiles(path) { await serialBridge.getPrompt() let files = await serialBridge.ilistFiles(path) diff --git a/ui/arduino/views/components/connection-dialog.js b/ui/arduino/views/components/connection-dialog.js index 2d99a47..8723464 100644 --- a/ui/arduino/views/components/connection-dialog.js +++ b/ui/arduino/views/components/connection-dialog.js @@ -1,23 +1,31 @@ function ConnectionDialog(state, emit) { const stateClass = state.isConnectionDialogOpen ? 'open' : 'closed' - function onClick(e) { - if (e.target.id == 'dialog') { + function clickDismiss(e) { + if (e.target.id == 'dialog-connection') { emit('close-connection-dialog') } } - return html` -