From 65e53cc7bdd84a6dadabdd089f42186faa4b1f4e Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 17 Dec 2024 16:55:12 +0100 Subject: [PATCH 01/39] Auto connect if only one port available --- ui/arduino/store.js | 14 ++++++++++++++ ui/arduino/views/components/toolbar.js | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 09a373e..116c0dc 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -210,6 +210,20 @@ 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', async (onlySelected = false) => { log('run') diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 70982b0..eff0731 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -16,7 +16,7 @@ function Toolbar(state, emit) { ${Button({ icon: state.isConnected ? 'connect.svg' : 'disconnect.svg', tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, - onClick: () => state.isConnected ? emit('disconnect') : emit('open-connection-dialog'), + onClick: () => state.isConnected ? emit('disconnect') : emit('connect'), active: state.isConnected })} From 07fa1cea111127303b839bbc31c2da1c62d670c2 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 17 Dec 2024 16:55:55 +0100 Subject: [PATCH 02/39] Display connection status --- ui/arduino/main.css | 15 ++++++++++++++- ui/arduino/views/components/repl-panel.js | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index cc0e95c..37fa561 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -273,7 +273,7 @@ button.small .icon { } #panel #drag-handle { - width: 100%; + flex-grow: 2; height: 100%; cursor: grab; } @@ -293,6 +293,19 @@ button.small .icon { background: #008184; } +.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 .term-operations { transition: opacity 0.15s; display: flex; diff --git a/ui/arduino/views/components/repl-panel.js b/ui/arduino/views/components/repl-panel.js index 3974d50..7a89fbd 100644 --- a/ui/arduino/views/components/repl-panel.js +++ b/ui/arduino/views/components/repl-panel.js @@ -13,6 +13,10 @@ function ReplPanel(state, emit) { return html`
+
+ +
${state.isConnected ? 'Connected to ' + state.connectedPort : ''}
+
emit('start-resizing-panel')} onmouseup=${() => emit('stop-resizing-panel')} From e2784f4d043c8f2e66f3eb5a336a88bff06fe9ed Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Wed, 18 Dec 2024 18:24:57 +0100 Subject: [PATCH 03/39] WIP: new file from editor. Signed-off-by: ubi de feo --- ui/arduino/index.html | 1 + ui/arduino/main.css | 17 +++++---- ui/arduino/store.js | 38 +++++++++++++------ .../views/components/connection-dialog.js | 4 +- .../components/new-file-destination-dialog.js | 22 +++++++++++ ui/arduino/views/components/toolbar.js | 7 ++++ ui/arduino/views/editor.js | 1 + ui/arduino/views/file-manager.js | 1 + 8 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 ui/arduino/views/components/new-file-destination-dialog.js diff --git a/ui/arduino/index.html b/ui/arduino/index.html index 8478cc7..63a5981 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..f25c61e 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -330,7 +330,7 @@ button.small .icon { opacity: 0.5; } -#dialog { +.dialog { display: flex; flex-direction: column; justify-content: center; @@ -350,13 +350,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 +375,16 @@ 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 > * { +.dialog .dialog-content > * { width: 100%; } -#dialog .dialog-content .item { +.dialog .dialog-content .item { border-radius: 4.5px; display: flex; padding: 10px; @@ -391,7 +394,7 @@ button.small .icon { cursor: pointer; } -#dialog .dialog-content .item:hover { +.dialog .dialog-content .item:hover { background: #008184; color: #ffffff; } diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 09a373e..67b2b70 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -49,6 +49,8 @@ async function store(state, emitter) { state.isConnected = false state.connectedPort = null + state.isNewFileDialogOpen = false + state.isSaving = false state.savingProgress = 0 state.isTransferring = false @@ -295,7 +297,17 @@ async function store(state, emitter) { window.removeEventListener('mousemove', state.resizePanel) }) - // SAVING + // NEW FILE AND SAVING + emitter.on('create-new-file', () => { + log('create-new-file') + state.isNewFileDialogOpen = true + emitter.emit('render') + }) + emitter.on('close-new-file-dialog', () => { + state.isNewFileDialogOpen = false + emitter.emit('render') + }) + emitter.on('save', async () => { log('save') let response = canSave({ @@ -514,18 +526,22 @@ async function store(state, emitter) { emitter.emit('render') }) - emitter.on('create-file', (device) => { + 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 +551,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 confirm(`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 +566,7 @@ async function store(state, emitter) { serialBridge.getFullPath( '/', state.boardNavigationPath, - value + fileNameParameter ), newFileContent ) @@ -558,10 +574,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 confirm(`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 +589,7 @@ async function store(state, emitter) { disk.getFullPath( state.diskNavigationRoot, state.diskNavigationPath, - value + fileNameParameter ), newFileContent ) diff --git a/ui/arduino/views/components/connection-dialog.js b/ui/arduino/views/components/connection-dialog.js index 2d99a47..56b1424 100644 --- a/ui/arduino/views/components/connection-dialog.js +++ b/ui/arduino/views/components/connection-dialog.js @@ -1,13 +1,13 @@ function ConnectionDialog(state, emit) { const stateClass = state.isConnectionDialogOpen ? 'open' : 'closed' function onClick(e) { - if (e.target.id == 'dialog') { + if (e.target.id == 'dialog-connection') { emit('close-connection-dialog') } } return html` -
+
${state.availablePorts.map( (port) => html` diff --git a/ui/arduino/views/components/new-file-destination-dialog.js b/ui/arduino/views/components/new-file-destination-dialog.js new file mode 100644 index 0000000..9979443 --- /dev/null +++ b/ui/arduino/views/components/new-file-destination-dialog.js @@ -0,0 +1,22 @@ +function NewFileDestinationDialog(state, emit) { + const stateClass = state.isNewFileDialogOpen ? 'open' : 'closed' + function onClick(e) { + if (e.target.id == 'dialog-new-file') { + emit('close-new-file-dialog') + } + } + let boardOption = '' + if (state.isConnected) { + boardOption = html` +
{emit('create-file','board', 'board_capocchia.py')}}>Board
+ ` + } + return html` +
+
+ ${boardOption} +
{emit('create-file', 'disk', 'disk_capocchia.py')}}>Computer
+
+
+ ` +} diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 70982b0..2aaa447 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -49,6 +49,13 @@ function Toolbar(state, emit) {
+ ${Button({ + icon: 'new-file.svg', + tooltip: `New (${metaKeyString}+N)`, + disabled: state.view != 'editor', + onClick: () => emit('create-new-file') + })} + ${Button({ icon: 'save.svg', tooltip: `Save (${metaKeyString}+S)`, diff --git a/ui/arduino/views/editor.js b/ui/arduino/views/editor.js index fd93b08..796bdea 100644 --- a/ui/arduino/views/editor.js +++ b/ui/arduino/views/editor.js @@ -7,5 +7,6 @@ function EditorView(state, emit) { ${ReplPanel(state, emit)}
${ConnectionDialog(state, emit)} + ${NewFileDestinationDialog(state, emit)} ` } diff --git a/ui/arduino/views/file-manager.js b/ui/arduino/views/file-manager.js index eafdf65..fd8a3c8 100644 --- a/ui/arduino/views/file-manager.js +++ b/ui/arduino/views/file-manager.js @@ -44,5 +44,6 @@ function FileManagerView(state, emit) {
${ConnectionDialog(state, emit)} + ${NewFileDestinationDialog(state, emit)} ` } From 6f025a78c165578801ab655adc8117bd8f816088 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Wed, 18 Dec 2024 19:36:14 +0100 Subject: [PATCH 04/39] WIP: dialog style. Signed-off-by: ubi de feo --- ui/arduino/main.css | 2 +- ui/arduino/views/components/new-file-destination-dialog.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index f25c61e..36e9468 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -381,7 +381,7 @@ button.small .icon { } .dialog .dialog-content > * { - width: 100%; + /* width: 100%; */ } .dialog .dialog-content .item { diff --git a/ui/arduino/views/components/new-file-destination-dialog.js b/ui/arduino/views/components/new-file-destination-dialog.js index 9979443..cf6b620 100644 --- a/ui/arduino/views/components/new-file-destination-dialog.js +++ b/ui/arduino/views/components/new-file-destination-dialog.js @@ -14,6 +14,7 @@ function NewFileDestinationDialog(state, emit) { return html`
+ ${boardOption}
{emit('create-file', 'disk', 'disk_capocchia.py')}}>Computer
From 5a7bd0f6f77f50dd314b77d9e1d2c098f7ae8e96 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Thu, 19 Dec 2024 07:59:22 +0100 Subject: [PATCH 05/39] WIP: new file dialog box. Signed-off-by: ubi de feo --- ui/arduino/main.css | 20 +++++++++ ui/arduino/store.js | 3 ++ .../components/new-file-destination-dialog.js | 42 +++++++++++++++++-- ui/arduino/views/components/repl-panel.js | 7 +++- 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index 36e9468..e141767 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -272,6 +272,10 @@ button.small .icon { min-height: 45px; } +#panel.dialog-open { + pointer-events: none; +} + #panel #drag-handle { width: 100%; height: 100%; @@ -384,6 +388,10 @@ button.small .icon { /* width: 100%; */ } +.dialog .dialog-content input { + font-size: 1.4em; + width:100%; +} .dialog .dialog-content .item { border-radius: 4.5px; display: flex; @@ -399,6 +407,18 @@ button.small .icon { color: #ffffff; } +.dialog .buttons-horizontal { + display: flex; + flex-direction: row; + justify-content: center; + width: 100%; + gap: 20px; +} +.dialog .buttons-horizontal .item { + flex-basis: 40%; + align-items: center; + background-color: #eee;; +} #file-manager { display: flex; padding: 12px 32px 24px 32px; diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 67b2b70..51202d5 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1467,6 +1467,9 @@ async function store(state, emitter) { if (state.isConnectionDialogOpen) { emitter.emit('close-connection-dialog') } + if (state.isNewFileDialogOpen) { + emitter.emit('close-new-file-dialog') + } } }) diff --git a/ui/arduino/views/components/new-file-destination-dialog.js b/ui/arduino/views/components/new-file-destination-dialog.js index cf6b620..c9944e9 100644 --- a/ui/arduino/views/components/new-file-destination-dialog.js +++ b/ui/arduino/views/components/new-file-destination-dialog.js @@ -11,13 +11,49 @@ function NewFileDestinationDialog(state, emit) {
{emit('create-file','board', 'board_capocchia.py')}}>Board
` } + const now = new Date(); + const dateStr = String(now.getMonth() + 1).padStart(2, '0') + + String(now.getDate()).padStart(2, '0') + + String(now.getHours()).padStart(2, '0') + + String(now.getMinutes()).padStart(2, '0') + + const fileName = `script_${dateStr}.py`; + + const newFileDialog = html` +
+
+ +
+ ${boardOption} +
{emit('create-file', 'disk', 'disk_capocchia.py')}}>Computer
+
+
+
+` + + + const observer = new MutationObserver((mutations, obs) => { + const el = newFileDialog.querySelector('input') + if (el) { + el.focus() + obs.disconnect() + } + }) + observer.observe(newFileDialog, { childList: true, subtree:true, attributes: true }) + + return newFileDialog + return html`
- - ${boardOption} -
{emit('create-file', 'disk', 'disk_capocchia.py')}}>Computer
+ +
+ ${boardOption} +
{emit('create-file', 'disk', 'disk_capocchia.py')}}>Computer
+
` + + } diff --git a/ui/arduino/views/components/repl-panel.js b/ui/arduino/views/components/repl-panel.js index 3974d50..63a608e 100644 --- a/ui/arduino/views/components/repl-panel.js +++ b/ui/arduino/views/components/repl-panel.js @@ -7,8 +7,13 @@ function ReplPanel(state, emit) { } } const panelOpenClass = state.isPanelOpen ? 'open' : 'closed' + // const pointerEventsClass = state.isNewFileDialogOpen || state.isDialogOpen ? 'open' : 'closed' const termOperationsVisibility = state.panelHeight > PANEL_TOO_SMALL ? 'visible' : 'hidden' - const terminalDisabledClass = state.isConnected ? 'terminal-enabled' : 'terminal-disabled' + let terminalDisabledClass = 'terminal-enabled' + if (!state.isConnected || state.isNewFileDialogOpen) { + terminalDisabledClass = 'terminal-disabled' + } + // const terminalDisabledClass = state.isConnected ? 'terminal-enabled' : 'terminal-disabled' return html`
From cc935436ef75cc80920c1a59c4e1017abc418fd5 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Thu, 19 Dec 2024 08:07:00 +0100 Subject: [PATCH 06/39] Removed ESC from global shortcuts to locally handle it in dialog. Signed-off-by: ubi de feo --- backend/shortcuts.js | 1 - ui/arduino/store.js | 10 +++++----- ui/arduino/views/components/connection-dialog.js | 13 +++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/backend/shortcuts.js b/backend/shortcuts.js index e6b7159..49311a4 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -11,7 +11,6 @@ module.exports = { CLEAR_TERMINAL: 'CommandOrControl+L', EDITOR_VIEW: 'CommandOrControl+Alt+1', FILES_VIEW: 'CommandOrControl+Alt+2', - ESC: 'Escape' }, menu: { CONNECT: 'CmdOrCtrl+Shift+C', diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 09a373e..28e3a0a 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1447,11 +1447,11 @@ 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') + // } + // } }) diff --git a/ui/arduino/views/components/connection-dialog.js b/ui/arduino/views/components/connection-dialog.js index 2d99a47..6b0e3ca 100644 --- a/ui/arduino/views/components/connection-dialog.js +++ b/ui/arduino/views/components/connection-dialog.js @@ -6,6 +6,19 @@ function ConnectionDialog(state, emit) { } } + function onKeyDown(e) { + if (e.key.toLowerCase() === 'escape') { + emit('close-connection-dialog') + } + } + + // Add/remove event listener based on dialog state + if (state.isConnectionDialogOpen) { + document.addEventListener('keydown', onKeyDown) + } else { + document.removeEventListener('keydown', onKeyDown) + } + return html`
From 7fbc40caf8941d4cb30e829b8121a47ea494f9a6 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 20 Dec 2024 14:58:37 +0100 Subject: [PATCH 07/39] Refactor of Tab creation and new file action/button. Signed-off-by: ubi de feo --- backend/shortcuts.js | 6 +- ui/arduino/index.html | 2 +- ui/arduino/main.css | 19 +++ ui/arduino/store.js | 149 ++++++++++++------ .../views/components/connection-dialog.js | 47 +++--- .../components/new-file-destination-dialog.js | 59 ------- .../views/components/new-file-dialog.js | 76 +++++++++ ui/arduino/views/editor.js | 2 +- ui/arduino/views/file-manager.js | 2 +- 9 files changed, 228 insertions(+), 134 deletions(-) delete mode 100644 ui/arduino/views/components/new-file-destination-dialog.js create mode 100644 ui/arduino/views/components/new-file-dialog.js diff --git a/backend/shortcuts.js b/backend/shortcuts.js index 49311a4..7d970fa 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -2,12 +2,13 @@ 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', + NEW: 'CommandOrControl+N', + SAVE: 'CommandOrControl+S', CLEAR_TERMINAL: 'CommandOrControl+L', EDITOR_VIEW: 'CommandOrControl+Alt+1', FILES_VIEW: 'CommandOrControl+Alt+2', @@ -15,12 +16,13 @@ module.exports = { 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', + NEW: 'CmdOrCtrl+N', + SAVE: 'CmdOrCtrl+S', CLEAR_TERMINAL: 'CmdOrCtrl+L', EDITOR_VIEW: 'CmdOrCtrl+Alt+1', FILES_VIEW: 'CmdOrCtrl+Alt+2' diff --git a/ui/arduino/index.html b/ui/arduino/index.html index 63a5981..332dfc3 100644 --- a/ui/arduino/index.html +++ b/ui/arduino/index.html @@ -25,7 +25,7 @@ - + diff --git a/ui/arduino/main.css b/ui/arduino/main.css index e141767..68af001 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -392,6 +392,10 @@ button.small .icon { font-size: 1.4em; width:100%; } + +.dialog .dialog-content input:focus { + outline-color: #008184; +} .dialog .dialog-content .item { border-radius: 4.5px; display: flex; @@ -419,6 +423,21 @@ button.small .icon { align-items: center; background-color: #eee;; } + +.dialog-title{ + width: 100%; + font-size: 0.8em; + padding: 0.7em; + 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; diff --git a/ui/arduino/store.js b/ui/arduino/store.js index c25fdc1..948e7f7 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 @@ -62,17 +67,7 @@ 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 - + await createNewTab('disk') state.savedPanelHeight = PANEL_DEFAULT state.panelHeight = PANEL_CLOSED state.resizePanel = function(e) { @@ -117,15 +112,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') @@ -300,14 +299,17 @@ async function store(state, emitter) { // 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({ @@ -389,7 +391,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 @@ -446,7 +448,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) @@ -455,16 +457,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') @@ -525,10 +518,19 @@ async function store(state, emitter) { }) emitter.emit('render') }) - + 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) { @@ -554,7 +556,7 @@ async function store(state, emitter) { fileName: fileNameParameter }) if (willOverwrite) { - const confirmAction = await confirm(`You are about to overwrite the file ${fileNameParameter} 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') @@ -577,7 +579,7 @@ async function store(state, emitter) { fileName: fileNameParameter }) if (willOverwrite) { - const confirmAction = await confirm(`You are about to overwrite the file ${fileNameParameter} 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') @@ -597,6 +599,7 @@ async function store(state, emitter) { setTimeout(() => { state.creatingFile = null + dismissOpenDialogs() emitter.emit('refresh-files') emitter.emit('render') }, 200) @@ -625,7 +628,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') @@ -654,7 +657,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') @@ -711,7 +714,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') @@ -799,7 +802,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 @@ -838,7 +841,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 @@ -993,7 +996,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 @@ -1256,7 +1259,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') @@ -1321,7 +1324,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') @@ -1408,7 +1411,7 @@ 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() @@ -1451,6 +1454,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') @@ -1471,6 +1478,14 @@ async function store(state, emitter) { }) + function dismissOpenDialogs(keyEvent = null) { + if (keyEvent && keyEvent.key != 'Escape') return + document.removeEventListener('keydown', dismissOpenDialogs) + state.isConnectionDialogOpen = false + state.isNewFileDialogOpen = false + emitter.emit('render') + } + function runCode() { if (canExecute({ view: state.view, isConnected: state.isConnected })) { emitter.emit('run') @@ -1507,14 +1522,60 @@ 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 + } + newFile.editor.onChange = function() { + 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 c75b21c..8723464 100644 --- a/ui/arduino/views/components/connection-dialog.js +++ b/ui/arduino/views/components/connection-dialog.js @@ -1,36 +1,31 @@ function ConnectionDialog(state, emit) { const stateClass = state.isConnectionDialogOpen ? 'open' : 'closed' - function onClick(e) { + function clickDismiss(e) { if (e.target.id == 'dialog-connection') { emit('close-connection-dialog') } } - function onKeyDown(e) { - if (e.key.toLowerCase() === 'escape') { - emit('close-connection-dialog') - } - } - - // Add/remove event listener based on dialog state - if (state.isConnectionDialogOpen) { - document.addEventListener('keydown', onKeyDown) - } else { - document.removeEventListener('keydown', onKeyDown) - } - - return html` -
-
- ${state.availablePorts.map( - (port) => html` -
emit('select-port', port)}> - ${port.path} -
- ` - )} -
emit('update-ports')}>Refresh
-
+ const connectionDialog = html` +
+ +
+
Connect to...
+ ${state.availablePorts.map( + (port) => html` +
emit('select-port', port)}> + ${port.path} +
+ ` + )} +
emit('update-ports')}>Refresh
+
+ +
` + if (state.isConnectionDialogOpen) { + return connectionDialog + } + } diff --git a/ui/arduino/views/components/new-file-destination-dialog.js b/ui/arduino/views/components/new-file-destination-dialog.js deleted file mode 100644 index c9944e9..0000000 --- a/ui/arduino/views/components/new-file-destination-dialog.js +++ /dev/null @@ -1,59 +0,0 @@ -function NewFileDestinationDialog(state, emit) { - const stateClass = state.isNewFileDialogOpen ? 'open' : 'closed' - function onClick(e) { - if (e.target.id == 'dialog-new-file') { - emit('close-new-file-dialog') - } - } - let boardOption = '' - if (state.isConnected) { - boardOption = html` -
{emit('create-file','board', 'board_capocchia.py')}}>Board
- ` - } - const now = new Date(); - const dateStr = String(now.getMonth() + 1).padStart(2, '0') + - String(now.getDate()).padStart(2, '0') + - String(now.getHours()).padStart(2, '0') + - String(now.getMinutes()).padStart(2, '0') - - const fileName = `script_${dateStr}.py`; - - const newFileDialog = html` -
-
- -
- ${boardOption} -
{emit('create-file', 'disk', 'disk_capocchia.py')}}>Computer
-
-
-
-` - - - const observer = new MutationObserver((mutations, obs) => { - const el = newFileDialog.querySelector('input') - if (el) { - el.focus() - obs.disconnect() - } - }) - observer.observe(newFileDialog, { childList: true, subtree:true, attributes: true }) - - return newFileDialog - - return html` -
-
- -
- ${boardOption} -
{emit('create-file', 'disk', 'disk_capocchia.py')}}>Computer
-
-
-
- ` - - -} diff --git a/ui/arduino/views/components/new-file-dialog.js b/ui/arduino/views/components/new-file-dialog.js new file mode 100644 index 0000000..83ace3f --- /dev/null +++ b/ui/arduino/views/components/new-file-dialog.js @@ -0,0 +1,76 @@ +function NewFileDialog(state, emit) { + const stateClass = state.isNewFileDialogOpen ? 'open' : 'closed' + function clickDismiss(e) { + if (e.target.id == 'dialog-new-file') { + emit('close-new-file-dialog') + } + } + + function triggerTabCreation(device) { + return () => { + const input = document.querySelector('#file-name') + const fileName = input.value.trim() || input.placeholder + emit('create-new-tab', device, fileName) + } + } + + let boardOption = '' + let inputFocus = '' + if (state.isConnected) { + boardOption = html` +
Board
+ ` + } + + const newFileDialogObserver = new MutationObserver((mutations, obs) => { + const input = document.querySelector('#dialog-new-file input') + if (input) { + input.focus() + obs.disconnect() + } + }) + + newFileDialogObserver.observe(document.body, { + childList: true, + subtree: true + }) + + + + let inputFieldValue = `` + let inputFieldPlaceholder = `` + + inputFieldPlaceholder = generateFileName() + + const inputAttrs = { + type: 'text', + id: 'file-name', + value: inputFieldValue, + placeholder: inputFieldPlaceholder + } + + const randomFileName = generateFileName() + const placeholderAttr = state.newFileName === null ? `placeholder="${randomFileName}"` : '' + const newFileDialog = html` +
+
+
Create new file
+ +
+ ${boardOption} +
Computer
+
+
+
+` + + if (state.isNewFileDialogOpen) { + const el = newFileDialog.querySelector('#dialog-new-file .dialog-contents > input') + if (el) { + el.focus() + } + return newFileDialog + } + + +} diff --git a/ui/arduino/views/editor.js b/ui/arduino/views/editor.js index 796bdea..c6267f0 100644 --- a/ui/arduino/views/editor.js +++ b/ui/arduino/views/editor.js @@ -7,6 +7,6 @@ function EditorView(state, emit) { ${ReplPanel(state, emit)}
${ConnectionDialog(state, emit)} - ${NewFileDestinationDialog(state, emit)} + ${NewFileDialog(state, emit)} ` } diff --git a/ui/arduino/views/file-manager.js b/ui/arduino/views/file-manager.js index fd8a3c8..89f7b89 100644 --- a/ui/arduino/views/file-manager.js +++ b/ui/arduino/views/file-manager.js @@ -44,6 +44,6 @@ function FileManagerView(state, emit) {
${ConnectionDialog(state, emit)} - ${NewFileDestinationDialog(state, emit)} + ${NewFileDialog(state, emit)} ` } From ea5b62063dc02257b95e1eae64415a6288f1c4a4 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 20 Dec 2024 18:22:57 +0100 Subject: [PATCH 08/39] Rework toolbar and CSS. Signed-off-by: ubi de feo --- ui/arduino/main.css | 31 ++++++++++++++++--- .../views/components/elements/button.js | 3 ++ ui/arduino/views/components/toolbar.js | 8 +++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index 68af001..f137269 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -1,8 +1,8 @@ @font-face { - font-family: "RobotoMono", monospace; + font-family: "RobotoMono"; src: - url("media/roboto-mono-latin-ext-400-normal.woff"), - url("media/roboto-mono-latin-ext-400-normal.woff2"); + url("media/roboto-mono-latin-ext-400-normal.woff2") format("woff2"), + url("media/roboto-mono-latin-ext-400-normal.woff") format("woff"); font-weight: normal; font-style: normal; } @@ -73,6 +73,19 @@ button.small .icon { .button { position: relative; + display: flex; + flex-direction: column; + align-content: space-between; + align-items: center; + gap: 10px; +} +.button .label { + text-align: center; + color: #eee; + opacity: 50%; +} +.button .label.active { + opacity: 100%; } .button .tooltip { opacity: 0; @@ -124,11 +137,21 @@ button.small .icon { display: flex; padding: 20px; align-items: center; - gap: 20px; + gap: 16px; align-self: stretch; background: #008184; } +.separator { + height: 100%; + min-width: 1px; + flex-basis: fit-content; + background: #fff; + position: relative; + margin-left: 0.5em; + margin-right: 0.5em; +} + #tabs { display: flex; padding: 10px 10px 0px 60px; diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index 3d888dd..56cd77c 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -5,6 +5,7 @@ function Button(args) { onClick = (e) => false, disabled = false, active = false, + label, tooltip, background } = args @@ -14,11 +15,13 @@ function Button(args) { } let activeClass = active ? 'active' : '' let backgroundClass = background ? 'inverted' : '' + let labelActiveClass = disabled ? '' : 'active' return html`
+
${label}
${tooltipEl}
` diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 2aaa447..4f8f484 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -15,6 +15,7 @@ function Toolbar(state, emit) {
${Button({ icon: state.isConnected ? 'connect.svg' : 'disconnect.svg', + label: state.isConnected ? 'Disconnect' : 'Connect', tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, onClick: () => state.isConnected ? emit('disconnect') : emit('open-connection-dialog'), active: state.isConnected @@ -24,6 +25,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'run.svg', + label: 'Run', tooltip: `Run (${metaKeyString}+R)`, disabled: !_canExecute, onClick: (e) => { @@ -36,12 +38,14 @@ function Toolbar(state, emit) { })} ${Button({ icon: 'stop.svg', + label: 'Stop', tooltip: `Stop (${metaKeyString}+H)`, disabled: !_canExecute, onClick: () => emit('stop') })} ${Button({ icon: 'reboot.svg', + label: 'Reset', tooltip: `Reset (${metaKeyString}+Shift+R)`, disabled: !_canExecute, onClick: () => emit('reset') @@ -51,6 +55,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'new-file.svg', + label: 'New', tooltip: `New (${metaKeyString}+N)`, disabled: state.view != 'editor', onClick: () => emit('create-new-file') @@ -58,6 +63,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'save.svg', + label: 'Save', tooltip: `Save (${metaKeyString}+S)`, disabled: !_canSave, onClick: () => emit('save') @@ -67,12 +73,14 @@ function Toolbar(state, emit) { ${Button({ icon: 'code.svg', + label: 'Editor', tooltip: `Editor (${metaKeyString}+Alt+1)`, active: state.view === 'editor', onClick: () => emit('change-view', 'editor') })} ${Button({ icon: 'files.svg', + label: 'Files', tooltip: `Files (${metaKeyString}+Alt+2)`, active: state.view === 'file-manager', onClick: () => emit('change-view', 'file-manager') From 49d3ec82c4d1f1831c0e3e13424a4e93bbb63a9b Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 20 Dec 2024 19:28:11 +0100 Subject: [PATCH 09/39] UI font update, CSS update, labels. Signed-off-by: ubi de feo --- ui/arduino/main.css | 34 +++++++++++++----- .../open-sans_5.0.29_latin-wght-normal.woff2 | Bin 0 -> 48236 bytes ui/arduino/store.js | 3 ++ .../views/components/elements/button.js | 6 ++-- ui/arduino/views/components/toolbar.js | 3 +- 5 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 ui/arduino/media/open-sans_5.0.29_latin-wght-normal.woff2 diff --git a/ui/arduino/main.css b/ui/arduino/main.css index f137269..eff09bc 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -7,11 +7,18 @@ font-style: normal; } +@font-face { + font-family: "OpenSans"; + src: url("media/open-sans_5.0.29_latin-wght-normal.woff2") format("woff2"); + font-weight: normal; + 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 { @@ -78,14 +85,19 @@ button.small .icon { align-content: space-between; align-items: center; gap: 10px; + width: 50px +} +.button.first{ + width:80px; } .button .label { text-align: center; color: #eee; - opacity: 50%; + opacity: 0.5; + font-family: "OpenSans", sans-serif; } .button .label.active { - opacity: 100%; + opacity: 1; } .button .tooltip { opacity: 0; @@ -147,6 +159,7 @@ button.small .icon { min-width: 1px; flex-basis: fit-content; background: #fff; + opacity: 0.7; position: relative; margin-left: 0.5em; margin-right: 0.5em; @@ -236,8 +249,12 @@ button.small .icon { font-size: 16px; height: 100%; overflow: hidden; + } +#code-editor * { + font-family: "RobotoMono", monospace; +} #code-editor .cm-editor { width: 100%; height: 100%; @@ -411,9 +428,10 @@ button.small .icon { /* width: 100%; */ } -.dialog .dialog-content input { - font-size: 1.4em; +.dialog .dialog-content #file-name { + font-size: 1.1em; width:100%; + font-family: "RobotoMono", monospace; } .dialog .dialog-content input:focus { @@ -439,10 +457,10 @@ button.small .icon { flex-direction: row; justify-content: center; width: 100%; - gap: 20px; + gap: 12px; } .dialog .buttons-horizontal .item { - flex-basis: 40%; + flex-basis: 50%; align-items: center; background-color: #eee;; } @@ -450,7 +468,7 @@ button.small .icon { .dialog-title{ width: 100%; font-size: 0.8em; - padding: 0.7em; + padding: 0; margin: 0; flex-basis: max-content; } diff --git a/ui/arduino/media/open-sans_5.0.29_latin-wght-normal.woff2 b/ui/arduino/media/open-sans_5.0.29_latin-wght-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0beab54695b31975886051af14f1ab9763d29f0c GIT binary patch literal 48236 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 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') } diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index 56cd77c..b25a02e 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -7,7 +7,8 @@ function Button(args) { active = false, label, tooltip, - background + background, + first } = args let tooltipEl = html`` if (tooltip) { @@ -16,8 +17,9 @@ function Button(args) { let activeClass = active ? 'active' : '' let backgroundClass = background ? 'inverted' : '' let labelActiveClass = disabled ? '' : 'active' + let buttonFirstClass = first ? 'first' : '' return html` -

+
diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 4f8f484..db6fecb 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -18,7 +18,8 @@ function Toolbar(state, emit) { label: state.isConnected ? 'Disconnect' : 'Connect', tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, onClick: () => state.isConnected ? emit('disconnect') : emit('open-connection-dialog'), - active: state.isConnected + active: state.isConnected, + first: true })}
From 5bf544d898ba7e012e948b6496173748b7e5d168 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 20 Dec 2024 23:55:18 +0100 Subject: [PATCH 10/39] Adjusted toolbar colours (hover) and layout. Signed-off-by: ubi de feo --- ui/arduino/main.css | 23 +++++++++++++------ .../views/components/elements/button.js | 13 +++++++---- ui/arduino/views/components/toolbar.js | 17 +++++++------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index eff09bc..bf077cc 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -43,7 +43,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; } @@ -52,6 +52,9 @@ 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); @@ -61,12 +64,18 @@ button.inverted { } button[disabled] { - opacity: 0.5; + background: rgba(255, 255, 255, 0.2); cursor: not-allowed; } +button[disabled]:hover { + background: rgba(255, 255, 255, 0.2); +} button:hover, button.active { background: rgba(255, 255, 255, 1); } +/* button.inactive:hover { + background: rgba(255, 255, 255, 0.2); +} */ button .icon { width: 63%; @@ -85,19 +94,19 @@ button.small .icon { align-content: space-between; align-items: center; gap: 10px; - width: 50px + width: auto } .button.first{ width:80px; } .button .label { text-align: center; - color: #eee; - opacity: 0.5; + /* color: #eee; */ + color: rgba(255, 255, 255, 0.2); font-family: "OpenSans", sans-serif; } .button .label.active { - opacity: 1; + color: rgba(255, 255, 255, 1); } .button .tooltip { opacity: 0; @@ -147,7 +156,7 @@ button.small .icon { #toolbar { display: flex; - padding: 20px; + padding: 16px 10px 10px 10px; align-items: center; gap: 16px; align-self: stretch; diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index b25a02e..db2c576 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -8,22 +8,25 @@ function Button(args) { label, tooltip, background, - first + first, + square } = args let tooltipEl = html`` if (tooltip) { tooltipEl = html`
${tooltip}
` } - let activeClass = active ? 'active' : '' + let activeClass = active ? 'active' : 'inactive' let backgroundClass = background ? 'inverted' : '' - let labelActiveClass = disabled ? '' : 'active' + let labelActiveClass = disabled ? 'inactive' : 'active' let buttonFirstClass = first ? 'first' : '' + let squareClass = square ? 'square' : '' + let labelItem = size === 'small' ? '' : html`
${label}
` return html`
- -
${label}
+ ${labelItem} ${tooltipEl}
` diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index db6fecb..4bc90b4 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -21,7 +21,13 @@ function Toolbar(state, emit) { active: state.isConnected, first: true })} - + ${Button({ + icon: 'reboot.svg', + label: 'Reset', + tooltip: `Reset (${metaKeyString}+Shift+R)`, + disabled: !_canExecute, + onClick: () => emit('reset') + })}
${Button({ @@ -44,13 +50,6 @@ function Toolbar(state, emit) { disabled: !_canExecute, onClick: () => emit('stop') })} - ${Button({ - icon: 'reboot.svg', - label: 'Reset', - tooltip: `Reset (${metaKeyString}+Shift+R)`, - disabled: !_canExecute, - onClick: () => emit('reset') - })}
@@ -77,6 +76,7 @@ function Toolbar(state, emit) { label: 'Editor', tooltip: `Editor (${metaKeyString}+Alt+1)`, active: state.view === 'editor', + square: true, onClick: () => emit('change-view', 'editor') })} ${Button({ @@ -84,6 +84,7 @@ function Toolbar(state, emit) { label: 'Files', tooltip: `Files (${metaKeyString}+Alt+2)`, active: state.view === 'file-manager', + square: true, onClick: () => emit('change-view', 'file-manager') })}
From 07c7bc29d03b2d3104596cfcb844a206f87da81c Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 01:13:30 +0100 Subject: [PATCH 11/39] Split navigation bar actions|views. Signed-off-by: ubi de feo --- ui/arduino/main.css | 12 +++ ui/arduino/views/components/toolbar.js | 143 +++++++++++++------------ 2 files changed, 85 insertions(+), 70 deletions(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index bf077cc..e7ec23a 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -154,6 +154,13 @@ button.small .icon { flex-shrink: 0; } +#navigation-bar { + display: flex; + width: 100%; + background: #008184; + justify-content: space-between; +} + #toolbar { display: flex; padding: 16px 10px 10px 10px; @@ -163,6 +170,11 @@ button.small .icon { background: #008184; } +#app-views { + display: flex; + padding: 16px 10px 10px 10px; + gap: 16px; +} .separator { height: 100%; min-width: 1px; diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 4bc90b4..138d891 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -12,81 +12,84 @@ function Toolbar(state, emit) { const metaKeyString = state.platform === 'darwin' ? 'Cmd' : 'Ctrl' return html` -
- ${Button({ - icon: state.isConnected ? 'connect.svg' : 'disconnect.svg', - label: state.isConnected ? 'Disconnect' : 'Connect', - tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, - onClick: () => state.isConnected ? emit('disconnect') : emit('open-connection-dialog'), - active: state.isConnected, - first: true - })} - ${Button({ - icon: 'reboot.svg', - label: 'Reset', - tooltip: `Reset (${metaKeyString}+Shift+R)`, - disabled: !_canExecute, - onClick: () => emit('reset') - })} -
+ ` } From 689863a6578603e500a1dc27502d98513beb6196 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 09:53:56 +0100 Subject: [PATCH 12/39] Fixed file actions buttons states and CSS. Signed-off-by: ubi de feo --- ui/arduino/main.css | 12 ++++--- .../views/components/elements/button.js | 31 ++++++++++--------- ui/arduino/views/components/file-actions.js | 2 ++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index e7ec23a..50be8db 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -57,15 +57,15 @@ button.square { } 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] { background: rgba(255, 255, 255, 0.2); - cursor: not-allowed; + cursor: default; } button[disabled]:hover { background: rgba(255, 255, 255, 0.2); @@ -531,13 +531,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 { diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index db2c576..2a956c6 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -1,33 +1,34 @@ function Button(args) { const { + first = false, size = '', + square = false, icon = 'connect.svg', - onClick = (e) => false, + onClick = (e) => {console.log(e); false}, disabled = false, active = false, - label, tooltip, - background, - first, - square + label, + background } = args let tooltipEl = html`` if (tooltip) { tooltipEl = html`
${tooltip}
` } - let activeClass = active ? 'active' : 'inactive' + let activeClass = active ? 'active' : '' let backgroundClass = background ? 'inverted' : '' - let labelActiveClass = disabled ? 'inactive' : 'active' let buttonFirstClass = first ? 'first' : '' let squareClass = square ? 'square' : '' + let labelActiveClass = disabled ? 'inactive' : 'active' let labelItem = size === 'small' ? '' : html`
${label}
` + return html` -
- - ${labelItem} - ${tooltipEl} -
- ` +
+ + ${labelItem} + ${tooltipEl} +
+ ` } diff --git a/ui/arduino/views/components/file-actions.js b/ui/arduino/views/components/file-actions.js index f48e0ad..75ffd54 100644 --- a/ui/arduino/views/components/file-actions.js +++ b/ui/arduino/views/components/file-actions.js @@ -15,6 +15,7 @@ function FileActions(state, emit) { icon: 'arrow-left-white.svg', size: 'small', background: 'inverted', + active: true, disabled: !canUpload({ isConnected, selectedFiles }), onClick: () => emit('upload-files') })} @@ -22,6 +23,7 @@ function FileActions(state, emit) { icon: 'arrow-right-white.svg', size: 'small', background: 'inverted', + active: true, disabled: !canDownload({ isConnected, selectedFiles }), onClick: () => emit('download-files') })} From 3a67bbbe4a223dbe2c945dc8cb77fa53b17ee11b Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 10:02:17 +0100 Subject: [PATCH 13/39] Fixed clear state.selectedFiles. Signed-off-by: ubi de feo --- ui/arduino/store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 0d3d9c6..0ebb572 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1209,6 +1209,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 @@ -1221,7 +1222,7 @@ async function store(state, emitter) { } state.openFiles = state.openFiles.concat(filesToOpen) - + state.selectedFiles = [] state.view = 'editor' updateMenu() emitter.emit('render') From aede0a2668944288f0c9e0c4ad846ede166d535b Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 10:50:08 +0100 Subject: [PATCH 14/39] Separator height adjustments. Signed-off-by: ubi de feo --- ui/arduino/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index 50be8db..493e0a8 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -184,6 +184,7 @@ button.small .icon { position: relative; margin-left: 0.5em; margin-right: 0.5em; + height: 65%; } #tabs { From c996824fe8ffa9da846bcd622cbd6f5f03ca986e Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 11:47:33 +0100 Subject: [PATCH 15/39] Toolbar: Reduced space between buttons and label. Signed-off-by: ubi de feo --- ui/arduino/main.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index 493e0a8..e2219ef 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -93,7 +93,7 @@ button.small .icon { flex-direction: column; align-content: space-between; align-items: center; - gap: 10px; + gap: .5em; width: auto } .button.first{ @@ -106,7 +106,7 @@ button.small .icon { font-family: "OpenSans", sans-serif; } .button .label.active { - color: rgba(255, 255, 255, 1); + color: rgba(255, 255, 255, .9); } .button .tooltip { opacity: 0; From fa82c79e7545630b91366a7a2b82bb76ea330ff3 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 17:48:08 +0100 Subject: [PATCH 16/39] Refactor menu, shortcuts, about window logic. Signed-off-by: ubi de feo --- backend/menu.js | 67 ++++++++++++++++++++++++++------------------ backend/shortcuts.js | 33 ++++++++++++++++++++-- index.js | 23 +++------------ preload.js | 8 +++++- ui/arduino/store.js | 14 +++++---- 5 files changed, 90 insertions(+), 55 deletions(-) diff --git a/backend/menu.js b/backend/menu.js index bdd3452..11e41bc 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,6 +55,10 @@ module.exports = function registerMenu(win, state = {}) { { label: 'File', submenu: [ + { label: 'Save', + accelerator: shortcuts.menu.SAVE, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.SAVE) + }, isMac ? { role: 'close' } : { role: 'quit' } ] }, @@ -166,23 +203,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 +211,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..1f6a2a3 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -1,4 +1,6 @@ -module.exports = { +const { globalShortcut } = require('electron') +let shortcutsActive = false +const shortcuts = { global: { CONNECT: 'CommandOrControl+Shift+C', DISCONNECT: 'CommandOrControl+Shift+D', @@ -25,5 +27,32 @@ module.exports = { CLEAR_TERMINAL: 'CmdOrCtrl+L', EDITOR_VIEW: 'CmdOrCtrl+Alt+1', FILES_VIEW: 'CmdOrCtrl+Alt+2' - } + }, + // Shortcuts +} + +function shortcutAction(key, win) { + console.log("key:", key) + win.send('shortcut-cmd', key); +} + +function registerShortcuts (win) { + console.log("registering shortcuts") + win.send('ignore-shortcuts', false) +} +function unregisterShortcuts(win) { + console.log("unregistering shortcuts") + // globalShortcut.unregisterAll() + win.send('ignore-shortcuts', true) +} + +function disableShortcuts (win, value) { + console.log("registering 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/preload.js b/preload.js index f67d43c..66b9ab0 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,12 @@ const Window = { callback(k); }) }, + onDisableShortcuts: (callback, value) => { + ipcRenderer.on('ignore-shortcuts', (e, value) => { + console.log("ipcRenderer ignore-shortcuts", value) + callback(value); + }) + }, beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), confirmClose: () => ipcRenderer.invoke('confirm-close'), diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 09a373e..9844a88 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -60,6 +60,8 @@ async function store(state, emitter) { state.isTerminalBound = false + state.shortcutsDisabled = false + const newFile = createEmptyFile({ parentFolder: null, // Null parent folder means not saved? source: 'disk' @@ -1398,12 +1400,14 @@ async function store(state, emitter) { await win.confirmClose() }) - // win.shortcutCmdR(() => { - // // Only run if we can execute - - // }) - + win.onDisableShortcuts((disable) => { + console.log('state.shortcutsDisabled', disable) + state.shortcutsDisabled = disable + }), + win.onKeyboardShortcut((key) => { + if (state.shortcutsDisabled) return + if (key === shortcuts.CONNECT) { emitter.emit('open-connection-dialog') } From ff5ca09a38d23cc70df74cb0ce7e648b74733755 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 18:11:31 +0100 Subject: [PATCH 17/39] Cleaned CSS typo. Signed-off-by: ubi de feo --- ui/arduino/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index e2219ef..926108a 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -542,7 +542,7 @@ button.small .icon { } #file-actions button:hover { - opacity: 0.5; */ + opacity: 0.5; } .device-header { From 2791e006942bb401b7c5a4c8f484d8c8a0dcce4b Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 18:45:30 +0100 Subject: [PATCH 18/39] Added New/Save shortcuts to menu. Signed-off-by: ubi de feo --- backend/menu.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/menu.js b/backend/menu.js index 11e41bc..033420f 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -55,8 +55,14 @@ module.exports = function registerMenu(win, state = {}) { { label: 'File', submenu: [ + { 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) }, isMac ? { role: 'close' } : { role: 'quit' } From 838aa6f8102d928dbc1fa01e23b25eefe5eb64b6 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 20:19:05 +0100 Subject: [PATCH 19/39] Updated Connect shortcut. Signed-off-by: ubi de feo --- ui/arduino/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index b558e4d..66b7ccf 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1443,7 +1443,7 @@ async function store(state, emitter) { if (state.shortcutsDisabled) return if (key === shortcuts.CONNECT) { - emitter.emit('open-connection-dialog') + emitter.emit('connect') } if (key === shortcuts.DISCONNECT) { emitter.emit('disconnect') From 9a05dd90174d26e244e8396c2c855a487eccdfc6 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 20:48:38 +0100 Subject: [PATCH 20/39] Cleanup unused code. Signed-off-by: ubi de feo --- backend/shortcuts.js | 17 +---------------- preload.js | 1 - ui/arduino/store.js | 1 - ui/arduino/views/components/elements/button.js | 2 +- 4 files changed, 2 insertions(+), 19 deletions(-) diff --git a/backend/shortcuts.js b/backend/shortcuts.js index 3f8bde0..2be714c 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -32,23 +32,8 @@ const shortcuts = { // Shortcuts } -function shortcutAction(key, win) { - console.log("key:", key) - win.send('shortcut-cmd', key); -} - -function registerShortcuts (win) { - console.log("registering shortcuts") - win.send('ignore-shortcuts', false) -} -function unregisterShortcuts(win) { - console.log("unregistering shortcuts") - // globalShortcut.unregisterAll() - win.send('ignore-shortcuts', true) -} - function disableShortcuts (win, value) { - console.log("registering shortcuts") + console.log(value ? 'disabling' : 'enabling', 'shortcuts') win.send('ignore-shortcuts', value) } diff --git a/preload.js b/preload.js index 66b9ab0..fbc1579 100644 --- a/preload.js +++ b/preload.js @@ -65,7 +65,6 @@ const Window = { }, onDisableShortcuts: (callback, value) => { ipcRenderer.on('ignore-shortcuts', (e, value) => { - console.log("ipcRenderer ignore-shortcuts", value) callback(value); }) }, diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 66b7ccf..fca99fe 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1435,7 +1435,6 @@ async function store(state, emitter) { }) win.onDisableShortcuts((disable) => { - console.log('state.shortcutsDisabled', disable) state.shortcutsDisabled = disable }), diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index 2a956c6..2e69931 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -4,7 +4,7 @@ function Button(args) { size = '', square = false, icon = 'connect.svg', - onClick = (e) => {console.log(e); false}, + onClick = (e) => {}, disabled = false, active = false, tooltip, From b5c766ee98cac5de4a7db75d33cf95f30b13f97a Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 20:52:43 +0100 Subject: [PATCH 21/39] Concealed tooltips. Signed-off-by: ubi de feo --- ui/arduino/views/components/elements/button.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index 2e69931..1dd69ec 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -15,6 +15,7 @@ function Button(args) { if (tooltip) { tooltipEl = html`
${tooltip}
` } + tooltipEl = html`` let activeClass = active ? 'active' : '' let backgroundClass = background ? 'inverted' : '' let buttonFirstClass = first ? 'first' : '' From be5f149ca2ed2879d6a8fa289a00d587294d126a Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 21:34:26 +0100 Subject: [PATCH 22/39] CSS fix line-height to show underscores in come cases. Signed-off-by: ubi de feo --- ui/arduino/main.css | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index 957c777..10b9cef 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -225,7 +225,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; @@ -459,12 +459,9 @@ button.small .icon { transition: transform 0.15s; } -.dialog .dialog-content > * { - /* width: 100%; */ -} .dialog .dialog-content #file-name { - font-size: 1.1em; + font-size: 1.3em; width:100%; font-family: "RobotoMono", monospace; } @@ -678,7 +675,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, From 5b81693178944771d0bbfc7192d8b1d6411b4d4d Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 21 Dec 2024 23:17:15 +0100 Subject: [PATCH 23/39] Added META+W (close tab) and META+Q (quit). Signed-off-by: ubi de feo --- backend/menu.js | 7 ++++++- backend/shortcuts.js | 2 ++ ui/arduino/store.js | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/menu.js b/backend/menu.js index 033420f..fe543a2 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -65,7 +65,12 @@ module.exports = function registerMenu(win, state = {}) { enabled: state.view === 'editor', click: () => win.webContents.send('shortcut-cmd', shortcuts.global.SAVE) }, - isMac ? { role: 'close' } : { role: 'quit' } + { label: 'Close tab', + accelerator: 'CmdOrCtrl+W', + enabled: state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLOSE) + }, + { role: 'quit' } ] }, { diff --git a/backend/shortcuts.js b/backend/shortcuts.js index 2be714c..925468e 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -2,6 +2,7 @@ const { globalShortcut } = require('electron') let shortcutsActive = false const shortcuts = { global: { + CLOSE: 'CommandOrControl+W', CONNECT: 'CommandOrControl+Shift+C', DISCONNECT: 'CommandOrControl+Shift+D', RUN: 'CommandOrControl+R', @@ -16,6 +17,7 @@ const shortcuts = { FILES_VIEW: 'CommandOrControl+Alt+2', }, menu: { + CLOSE: 'CmdOrCtrl+W', CONNECT: 'CmdOrCtrl+Shift+C', DISCONNECT: 'CmdOrCtrl+Shift+D', RUN: 'CmdOrCtrl+R', diff --git a/ui/arduino/store.js b/ui/arduino/store.js index fca99fe..bc41090 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1440,7 +1440,9 @@ async function store(state, emitter) { win.onKeyboardShortcut((key) => { if (state.shortcutsDisabled) return - + if (key === shortcuts.CLOSE) { + emitter.emit('close-tab', state.editingFile) + } if (key === shortcuts.CONNECT) { emitter.emit('connect') } From 3c1fc6c9744b3b271f55316cff58e8bab48b579c Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 22 Dec 2024 00:03:55 +0100 Subject: [PATCH 24/39] Added multiple run safeguard to prevent too many Promises from crashing the app. Signed-off-by: ubi de feo --- ui/arduino/store.js | 34 +++++++++++++++++-- .../views/components/elements/button.js | 2 ++ ui/arduino/views/components/toolbar.js | 4 +-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index bc41090..04afdb3 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -228,6 +228,15 @@ async function store(state, emitter) { }) // 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) @@ -1506,14 +1515,35 @@ async function store(state, emitter) { 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() { diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index 1dd69ec..b1d1f55 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -11,6 +11,8 @@ function Button(args) { label, background } = args + + let tooltipEl = html`` if (tooltip) { tooltipEl = html`
${tooltip}
` diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 5acc40d..cbdb82a 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -38,9 +38,9 @@ function Toolbar(state, emit) { disabled: !_canExecute, onClick: (e) => { if (e.altKey) { - emit('run', true) + emit('run-from-button', true) }else{ - emit('run') + emit('run-from-button') } } })} From f4426285017576b250aa84f914f6227765886332 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 22 Dec 2024 00:27:12 +0100 Subject: [PATCH 25/39] Transfer operations lock shortcuts. Progress is reset. Signed-off-by: ubi de feo --- ui/arduino/store.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 04afdb3..8ca4062 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -59,7 +59,7 @@ async function store(state, emitter) { state.isSaving = false state.savingProgress = 0 state.isTransferring = false - state.transferringProgress = 0 + state.transferringProgress = '' state.isRemoving = false state.isLoadingFiles = false @@ -1312,7 +1312,9 @@ async function store(state, emitter) { state.transferringProgress = `${fileName}: ${progress}` emitter.emit('render') } + ) + state.transferringProgress = '' } else { await serialBridge.uploadFile( srcPath, destPath, @@ -1321,6 +1323,7 @@ async function store(state, emitter) { emitter.emit('render') } ) + state.transferringProgress = '' } } @@ -1448,6 +1451,7 @@ async function store(state, emitter) { }), 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) From 9f2adfdcfa536a5e9081c6dfe6edf551d09756b8 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 22 Dec 2024 00:46:49 +0100 Subject: [PATCH 26/39] Reworked panel bar to allow full area drag-to-resize. Signed-off-by: ubi de feo --- ui/arduino/main.css | 6 ++++++ ui/arduino/views/components/repl-panel.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/ui/arduino/main.css b/ui/arduino/main.css index 10b9cef..d9bce9a 100644 --- a/ui/arduino/main.css +++ b/ui/arduino/main.css @@ -342,6 +342,8 @@ button.small .icon { flex-grow: 2; height: 100%; cursor: grab; + position: absolute; + width: 100%; } #panel #drag-handle:active { @@ -357,6 +359,7 @@ button.small .icon { gap: 10px; align-self: stretch; background: #008184; + position: relative; } .panel-bar #connection-status { @@ -372,6 +375,9 @@ button.small .icon { filter: invert(1); } +.panel-bar .spacer { + flex-grow: 1; +} .panel-bar .term-operations { transition: opacity 0.15s; display: flex; diff --git a/ui/arduino/views/components/repl-panel.js b/ui/arduino/views/components/repl-panel.js index 4c85fcc..eca67d9 100644 --- a/ui/arduino/views/components/repl-panel.js +++ b/ui/arduino/views/components/repl-panel.js @@ -22,6 +22,7 @@ function ReplPanel(state, emit) {
${state.isConnected ? 'Connected to ' + state.connectedPort : ''}
+
emit('start-resizing-panel')} onmouseup=${() => emit('stop-resizing-panel')} @@ -34,6 +35,7 @@ function ReplPanel(state, emit) { size: 'small', onClick: onToggle })} +
${state.cache(XTerm, 'terminal').render()} From aa91886f79a634538dc88af13f3e7dfeb0a12bda Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 22 Dec 2024 08:07:50 +0100 Subject: [PATCH 27/39] Clearing state.selectedFiles on open/view-switch. Signed-off-by: ubi de feo --- ui/arduino/store.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 8ca4062..0039775 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -102,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() }) From 828f21b8573f2f7fa005ce945b2e4afca582628a Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 22 Dec 2024 09:34:25 +0100 Subject: [PATCH 28/39] Renaming tabs/files will now reflect on the other view. Signed-off-by: ubi de feo --- ui/arduino/store.js | 73 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 0039775..b40a1c8 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -927,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}`) } @@ -955,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') @@ -1037,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 { From 954010b75f654d0838180671768704f143bc0d47 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 3 Mar 2025 11:15:47 +0100 Subject: [PATCH 29/39] Fixed auto-connect for FM view and amended text to connect. Signed-off-by: ubi de feo --- ui/arduino/views/file-manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/arduino/views/file-manager.js b/ui/arduino/views/file-manager.js index 89f7b89..fa43eff 100644 --- a/ui/arduino/views/file-manager.js +++ b/ui/arduino/views/file-manager.js @@ -1,5 +1,5 @@ function FileManagerView(state, emit) { - let boardFullPath = 'Select a board...' + let boardFullPath = 'Connect to board' let diskFullPath = `${state.diskNavigationRoot}${state.diskNavigationPath}` if (state.isConnected) { @@ -13,7 +13,7 @@ function FileManagerView(state, emit) {
-
emit('open-connection-dialog')} class="text"> +
emit('connect')} class="text"> ${boardFullPath}