diff --git a/backend/ipc.js b/backend/ipc.js index 8bace22..12f2127 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,4 +1,6 @@ const fs = require('fs') +const registerMenu = require('./menu.js') + const { openFolderDialog, listFolder, @@ -129,9 +131,18 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { return response != opt.cancelId }) + ipcMain.handle('update-menu-state', (event, state) => { + registerMenu(win, state) + }) + win.on('close', (event) => { console.log('BrowserWindow', 'close') event.preventDefault() win.webContents.send('check-before-close') }) + + // handle disconnection before reload + ipcMain.handle('prepare-reload', async (event) => { + return win.webContents.send('before-reload') + }) } diff --git a/backend/menu.js b/backend/menu.js index 6b62cdf..eb7810e 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,8 +1,10 @@ const { app, Menu } = require('electron') const path = require('path') const openAboutWindow = require('about-window').default +const shortcuts = require('./shortcuts.js') +const { type } = require('os') -module.exports = function registerMenu(win) { +module.exports = function registerMenu(win, state = {}) { const isMac = process.platform === 'darwin' const template = [ ...(isMac ? [{ @@ -10,9 +12,8 @@ module.exports = function registerMenu(win) { submenu: [ { role: 'about'}, { type: 'separator' }, - { role: 'services' }, { type: 'separator' }, - { role: 'hide' }, + { role: 'hide', accelerator: 'CmdOrCtrl+Shift+H' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, @@ -35,7 +36,6 @@ module.exports = function registerMenu(win) { { role: 'copy' }, { role: 'paste' }, ...(isMac ? [ - { role: 'pasteAndMatchStyle' }, { role: 'selectAll' }, { type: 'separator' }, { @@ -51,11 +51,66 @@ module.exports = function registerMenu(win) { ]) ] }, + { + label: 'Board', + submenu: [ + { + label: 'Connect', + accelerator: shortcuts.menu.CONNECT, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CONNECT) + }, + { + label: 'Disconnect', + accelerator: shortcuts.menu.DISCONNECT, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.DISCONNECT) + }, + { type: 'separator' }, + { + label: 'Run', + accelerator: shortcuts.menu.RUN, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RUN) + }, + { + label: 'Run selection', + accelerator: isMac ? shortcuts.menu.RUN_SELECTION : shortcuts.menu.RUN_SELECTION_WL, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', (isMac ? shortcuts.global.RUN_SELECTION : shortcuts.global.RUN_SELECTION_WL)) + }, + { + label: 'Stop', + accelerator: shortcuts.menu.STOP, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.STOP) + }, + { + label: 'Reset', + accelerator: shortcuts.menu.RESET, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RESET) + }, + { type: 'separator' } + ] + }, { label: 'View', submenu: [ - { role: 'reload' }, - { role: 'toggleDevTools' }, + { + label: 'Editor', + accelerator: shortcuts.menu.EDITOR_VIEW, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.EDITOR_VIEW,) + }, + { + label: 'Files', + accelerator: shortcuts.menu.FILES_VIEW, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.FILES_VIEW) + }, + { + label: 'Clear terminal', + accelerator: shortcuts.menu.CLEAR_TERMINAL, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLEAR_TERMINAL) + }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, @@ -67,6 +122,22 @@ module.exports = function registerMenu(win) { { label: 'Window', submenu: [ + { + label: 'Reload', + accelerator: '', + click: async () => { + try { + win.webContents.send('cleanup-before-reload') + setTimeout(() => { + win.reload() + }, 500) + } catch(e) { + console.error('Reload from menu failed:', e) + } + } + }, + { role: 'toggleDevTools'}, + { type: 'separator' }, { role: 'minimize' }, { role: 'zoom' }, ...(isMac ? [ @@ -75,7 +146,7 @@ module.exports = function registerMenu(win) { { type: 'separator' }, { role: 'window' } ] : [ - { role: 'close' } + ]) ] }, @@ -102,7 +173,6 @@ module.exports = function registerMenu(win) { openAboutWindow({ icon_path: path.resolve(__dirname, '../ui/arduino/media/about_image.png'), css_path: path.resolve(__dirname, '../ui/arduino/views/about.css'), - // about_page_dir: path.resolve(__dirname, '../ui/arduino/views/'), copyright: '© Arduino SA 2022', package_json_dir: path.resolve(__dirname, '..'), bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues", diff --git a/backend/shortcuts.js b/backend/shortcuts.js new file mode 100644 index 0000000..e6b7159 --- /dev/null +++ b/backend/shortcuts.js @@ -0,0 +1,29 @@ +module.exports = { + global: { + 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', + CLEAR_TERMINAL: 'CommandOrControl+L', + EDITOR_VIEW: 'CommandOrControl+Alt+1', + FILES_VIEW: 'CommandOrControl+Alt+2', + ESC: 'Escape' + }, + menu: { + 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', + CLEAR_TERMINAL: 'CmdOrCtrl+L', + EDITOR_VIEW: 'CmdOrCtrl+Alt+1', + FILES_VIEW: 'CmdOrCtrl+Alt+2' + } +} diff --git a/index.js b/index.js index 57eba4c..aa5d989 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ -const { app, BrowserWindow, ipcMain, dialog } = require('electron') +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') @@ -49,12 +50,59 @@ function createWindow () { win.show() }) + win.webContents.on('before-reload', async (event) => { + // Prevent the default reload behavior + event.preventDefault() + + try { + // Tell renderer to do cleanup + win.webContents.send('cleanup-before-reload') + + // Wait for cleanup then reload + setTimeout(() => { + // This will trigger a page reload, but won't trigger 'before-reload' again + win.reload() + }, 500) + } catch(e) { + console.error('Reload preparation failed:', e) + } + }) + + const initialMenuState = { + isConnected: false, + view: 'editor' + } + registerIPCHandlers(win, ipcMain, app, dialog) - registerMenu(win) + registerMenu(win, initialMenuState) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) } -app.on('ready', 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/preload.js b/preload.js index ddcb8aa..dd4f28f 100644 --- a/preload.js +++ b/preload.js @@ -1,8 +1,10 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') - +const shortcuts = require('./backend/shortcuts.js').global const MicroPython = require('micropython.js') +const { emit, platform } = require('process') + const board = new MicroPython() board.chunk_size = 192 board.chunk_sleep = 200 @@ -155,12 +157,37 @@ const Window = { setWindowSize: (minWidth, minHeight) => { ipcRenderer.invoke('set-window-size', minWidth, minHeight) }, + onKeyboardShortcut: (callback, key) => { + ipcRenderer.on('shortcut-cmd', (event, k) => { + callback(k); + }) + }, + + onBeforeReload: (callback) => { + ipcRenderer.on('cleanup-before-reload', async () => { + try { + await callback() + } catch(e) { + console.error('Cleanup before reload failed:', e) + } + }) + }, + beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), confirmClose: () => ipcRenderer.invoke('confirm-close'), isPackaged: () => ipcRenderer.invoke('is-packaged'), - openDialog: (opt) => ipcRenderer.invoke('open-dialog', opt) -} + openDialog: (opt) => ipcRenderer.invoke('open-dialog', opt), + + getOS: () => platform, + isWindows: () => platform === 'win32', + isMac: () => platform === 'darwin', + isLinux: () => platform === 'linux', + updateMenuState: (state) => { + return ipcRenderer.invoke('update-menu-state', state) + }, + getShortcuts: () => shortcuts +} contextBridge.exposeInMainWorld('BridgeSerial', Serial) contextBridge.exposeInMainWorld('BridgeDisk', Disk) diff --git a/ui/arduino/main.js b/ui/arduino/main.js index bf693df..ce52be1 100644 --- a/ui/arduino/main.js +++ b/ui/arduino/main.js @@ -46,11 +46,9 @@ window.addEventListener('load', () => { app.use(store); app.route('*', App) app.mount('#app') - app.emitter.on('DOMContentLoaded', () => { if (app.state.diskNavigationRoot) { app.emitter.emit('refresh-files') } }) - }) diff --git a/ui/arduino/media/code.svg b/ui/arduino/media/code.svg new file mode 100644 index 0000000..3b4303f --- /dev/null +++ b/ui/arduino/media/code.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 656b772..f2bd1b0 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -3,6 +3,8 @@ const serial = window.BridgeSerial const disk = window.BridgeDisk const win = window.BridgeWindow +const shortcuts = window.BridgeWindow.getShortcuts() + const newFileContent = `# This program was created in Arduino Lab for MicroPython print('Hello, MicroPython!') @@ -24,6 +26,7 @@ async function confirm(msg, cancelMsg, confirmMsg) { async function store(state, emitter) { win.setWindowSize(720, 640) + state.platform = window.BridgeWindow.getOS() state.view = 'editor' state.diskNavigationPath = '/' state.diskNavigationRoot = getDiskNavigationRootFromStorage() @@ -80,6 +83,14 @@ async function store(state, emitter) { emitter.emit('render') } + // Menu management + const updateMenu = () => { + window.BridgeWindow.updateMenuState({ + isConnected: state.isConnected, + view: state.view + }) + } + // START AND BASIC ROUTING emitter.on('select-disk-navigation-root', async () => { const folder = await selectDiskFolder() @@ -97,6 +108,7 @@ async function store(state, emitter) { emitter.emit('refresh-files') } emitter.emit('render') + updateMenu() }) // CONNECTION DIALOG @@ -142,11 +154,13 @@ async function store(state, emitter) { } // Stop whatever is going on // Recover from getting stuck in raw repl + await serial.getPrompt() clearTimeout(timeout_id) // Connected and ready state.isConnecting = false state.isConnected = true + updateMenu() if (state.view === 'editor' && state.panelHeight <= PANEL_CLOSED) { state.panelHeight = state.savedPanelHeight } @@ -180,6 +194,7 @@ async function store(state, emitter) { state.boardNavigationPath = '/' emitter.emit('refresh-files') emitter.emit('render') + updateMenu() }) emitter.on('connection-timeout', async () => { state.isConnected = false @@ -190,7 +205,7 @@ async function store(state, emitter) { }) // CODE EXECUTION - emitter.on('run', async () => { + emitter.on('run', async (onlySelected = false) => { log('run') const openFile = state.openFiles.find(f => f.id == state.editingFile) let code = openFile.editor.editor.state.doc.toString() @@ -198,7 +213,7 @@ async function store(state, emitter) { // If there is a selection, run only the selected code const startIndex = openFile.editor.editor.state.selection.ranges[0].from const endIndex = openFile.editor.editor.state.selection.ranges[0].to - if (endIndex - startIndex > 0) { + if (endIndex - startIndex > 0 && onlySelected) { selectedCode = openFile.editor.editor.state.doc.toString().substring(startIndex, endIndex) // Checking to see if the user accidentally double-clicked some whitespace // While a random selection would yield an error when executed, @@ -1367,6 +1382,18 @@ async function store(state, emitter) { emitter.emit('render') }) + win.onBeforeReload(async () => { + // Perform any cleanup needed + if (state.isConnected) { + await serial.disconnect() + state.isConnected = false + state.panelHeight = PANEL_CLOSED + state.boardFiles = [] + state.boardNavigationPath = '/' + } + // Any other cleanup needed + }) + win.beforeClose(async () => { const hasChanges = !!state.openFiles.find(f => f.hasChanges) if (hasChanges) { @@ -1376,6 +1403,78 @@ async function store(state, emitter) { await win.confirmClose() }) + // win.shortcutCmdR(() => { + // // Only run if we can execute + + // }) + + win.onKeyboardShortcut((key) => { + if (key === shortcuts.CONNECT) { + emitter.emit('open-connection-dialog') + } + if (key === shortcuts.DISCONNECT) { + emitter.emit('disconnect') + } + if (key === shortcuts.RESET) { + if (state.view != 'editor') return + emitter.emit('reset') + } + if (key === shortcuts.CLEAR_TERMINAL) { + if (state.view != 'editor') return + emitter.emit('clear-terminal') + } + // Future: Toggle REPL panel + // if (key === 'T') { + // if (state.view != 'editor') return + // emitter.emit('clear-terminal') + // } + if (key === shortcuts.RUN) { + if (state.view != 'editor') return + runCode() + } + if (key === shortcuts.RUN_SELECTION || key === shortcuts.RUN_SELECTION_WL) { + if (state.view != 'editor') return + runCodeSelection() + } + if (key === shortcuts.STOP) { + if (state.view != 'editor') return + stopCode() + } + if (key === shortcuts.SAVE) { + if (state.view != 'editor') return + emitter.emit('save') + } + if (key === shortcuts.EDITOR_VIEW) { + if (state.view != 'file-manager') return + emitter.emit('change-view', 'editor') + } + if (key === shortcuts.FILES_VIEW) { + if (state.view != 'editor') return + emitter.emit('change-view', 'file-manager') + } + if (key === shortcuts.ESC) { + if (state.isConnectionDialogOpen) { + emitter.emit('close-connection-dialog') + } + } + + }) + + function runCode() { + if (canExecute({ view: state.view, isConnected: state.isConnected })) { + emitter.emit('run') + } + } + function runCodeSelection() { + if (canExecute({ view: state.view, isConnected: state.isConnected })) { + emitter.emit('run', true) + } + } + function stopCode() { + if (canExecute({ view: state.view, isConnected: state.isConnected })) { + emitter.emit('stop') + } + } function createFile(args) { const { source, @@ -1508,6 +1607,7 @@ function pickRandom(array) { function canSave({ view, isConnected, openFiles, editingFile }) { const isEditor = view === 'editor' const file = openFiles.find(f => f.id === editingFile) + if (!file.hasChanges) return false // Can only save on editor if (!isEditor) return false // Can always save disk files @@ -1620,4 +1720,5 @@ async function getHelperFullPath() { '' ) } + } diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index 7030d49..3d888dd 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -2,7 +2,7 @@ function Button(args) { const { size = '', icon = 'connect.svg', - onClick = () => false, + onClick = (e) => false, disabled = false, active = false, tooltip, diff --git a/ui/arduino/views/components/repl-panel.js b/ui/arduino/views/components/repl-panel.js index ac1760c..3974d50 100644 --- a/ui/arduino/views/components/repl-panel.js +++ b/ui/arduino/views/components/repl-panel.js @@ -50,7 +50,7 @@ function ReplOperations(state, emit) { Button({ icon: 'delete.svg', size: 'small', - tooltip: 'Clean', + tooltip: `Clean (${state.platform === 'darwin' ? 'Cmd' : 'Ctrl'}+L)`, onClick: () => emit('clear-terminal') }) ] diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 4ba1011..0e3d497 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -9,12 +9,13 @@ function Toolbar(state, emit) { view: state.view, isConnected: state.isConnected }) - + const metaKeyString = state.platform === 'darwin' ? 'Cmd' : 'Ctrl' + return html`
${Button({ icon: state.isConnected ? 'connect.svg' : 'disconnect.svg', - tooltip: state.isConnected ? 'Disconnect' : 'Connect', + tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, onClick: () => emit('open-connection-dialog'), active: state.isConnected })} @@ -23,19 +24,25 @@ function Toolbar(state, emit) { ${Button({ icon: 'run.svg', - tooltip: 'Run', + tooltip: `Run (${metaKeyString}+R)`, disabled: !_canExecute, - onClick: () => emit('run') + onClick: (e) => { + if (e.altKey) { + emit('run', true) + }else{ + emit('run') + } + } })} ${Button({ icon: 'stop.svg', - tooltip: 'Stop', + tooltip: `Stop (${metaKeyString}+H)`, disabled: !_canExecute, onClick: () => emit('stop') })} ${Button({ icon: 'reboot.svg', - tooltip: 'Reset', + tooltip: `Reset (${metaKeyString}+Shift+R)`, disabled: !_canExecute, onClick: () => emit('reset') })} @@ -44,7 +51,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'save.svg', - tooltip: 'Save', + tooltip: `Save (${metaKeyString}+S)`, disabled: !_canSave, onClick: () => emit('save') })} @@ -52,14 +59,14 @@ function Toolbar(state, emit) {
${Button({ - icon: 'editor.svg', - tooltip: 'Editor and REPL', + icon: 'code.svg', + tooltip: `Editor (${metaKeyString}+Alt+1)`, active: state.view === 'editor', onClick: () => emit('change-view', 'editor') })} ${Button({ icon: 'files.svg', - tooltip: 'File Manager', + tooltip: `Files (${metaKeyString}+Alt+2)`, active: state.view === 'file-manager', onClick: () => emit('change-view', 'file-manager') })} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy