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/menu.js b/backend/menu.js index bdd3452..fe543a2 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,10 +1,43 @@ 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 = [ @@ -22,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' } ] }, { @@ -166,23 +214,7 @@ module.exports = function registerMenu(win, state = {}) { }, { label:'About Arduino Lab for MicroPython', - 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', - }) - } + click: () => { openAppInfo(win) } }, ] } @@ -190,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/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..dda3fa4 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') @@ -63,28 +61,15 @@ 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() + console.log("win focus") }) + win.on('blur', () => { - globalShortcut.unregisterAll() + console.log("win blur") }) -}) \ No newline at end of file +}) diff --git a/package.json b/package.json index b3f5abb..751e1db 100644 --- a/package.json +++ b/package.json @@ -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..fbc1579 100644 --- a/preload.js +++ b/preload.js @@ -1,7 +1,7 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') -const shortcuts = require('./backend/shortcuts.js').global +const shortcuts = require('./backend/shortcuts.js').shortcuts.global const { emit, platform } = require('process') const SerialBridge = require('./backend/serial/serial-bridge.js') @@ -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'), 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..84d1095 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; @@ -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/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..b40a1c8 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -10,9 +10,12 @@ const newFileContent = `# This program was created in Arduino Lab for MicroPytho print('Hello, MicroPython!') ` -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 (cancelMsg) buttons.push(cancelMsg) + if (confirmMsg) buttons.push(confirmMsg) let response = await win.openDialog({ type: 'question', buttons: [cancelMsg, confirmMsg], @@ -36,6 +39,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 +54,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 +67,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) { @@ -103,10 +102,14 @@ async function store(state, emitter) { emitter.emit('render') }) emitter.on('change-view', (view) => { - state.view = view + if (state.view === 'file-manager') { + if (view != state.view) { + state.selectedFiles = [] + } emitter.emit('refresh-files') } + state.view = view emitter.emit('render') updateMenu() }) @@ -115,15 +118,19 @@ async function store(state, emitter) { 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') @@ -210,7 +217,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) @@ -295,7 +325,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 +420,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 +477,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 +486,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 +547,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 +582,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') @@ -550,7 +597,7 @@ async function store(state, emitter) { serialBridge.getFullPath( '/', state.boardNavigationPath, - value + fileNameParameter ), newFileContent ) @@ -558,10 +605,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 +620,7 @@ async function store(state, emitter) { disk.getFullPath( state.diskNavigationRoot, state.diskNavigationPath, - value + fileNameParameter ), newFileContent ) @@ -581,6 +628,7 @@ async function store(state, emitter) { setTimeout(() => { state.creatingFile = null + dismissOpenDialogs() emitter.emit('refresh-files') emitter.emit('render') }, 200) @@ -609,7 +657,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 +686,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 +743,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') @@ -783,7 +831,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 +870,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 +927,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 +961,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 +1020,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 +1032,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 { @@ -1190,6 +1235,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,7 +1248,7 @@ async function store(state, emitter) { } state.openFiles = state.openFiles.concat(filesToOpen) - + state.selectedFiles = [] state.view = 'editor' updateMenu() emitter.emit('render') @@ -1240,7 +1286,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 +1313,9 @@ async function store(state, emitter) { state.transferringProgress = `${fileName}: ${progress}` emitter.emit('render') } + ) + state.transferringProgress = '' } else { await serialBridge.uploadFile( srcPath, destPath, @@ -1276,6 +1324,7 @@ async function store(state, emitter) { emitter.emit('render') } ) + state.transferringProgress = '' } } @@ -1305,7 +1354,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 +1441,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 +1488,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 +1504,51 @@ 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 + console.log('>>> RUN CODE ACTUAL <<<') + emitter.emit('run', onlySelected) + timedReset() + } + function runCode() { + console.log('>>> RUN CODE REQUEST <<<') if (canExecute({ view: state.view, isConnected: state.isConnected })) { - emitter.emit('run') + filterDoubleRun() } } function runCodeSelection() { + console.log('>>> RUN CODE REQUEST <<<') if (canExecute({ view: state.view, isConnected: state.isConnected })) { - emitter.emit('run', true) + filterDoubleRun(true) } } function stopCode() { @@ -1491,14 +1577,63 @@ 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 = 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() { + console.log('editor has changes') + newFile.hasChanges = true + emitter.emit('render') + } + state.openFiles.push(newFile) + state.editingFile = newFile.id + return true } + } 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` -