diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 137a1ec..9f35452 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,8 @@ jobs: config: - os: windows-2019 - os: ubuntu-latest - - os: macos-latest + - os: macos-13 + - os: macos-14 runs-on: ${{ matrix.config.os }} timeout-minutes: 90 @@ -99,6 +100,8 @@ jobs: name: Arduino-Lab-for-MicroPython_Linux_X86-64 - path: "*-mac_x64.zip" name: Arduino-Lab-for-MicroPython_macOS_X86-64 + - path: "*-mac_arm64.zip" + name: Arduino-Lab-for-MicroPython_macOS_arm-64 # - path: "*Windows_64bit.exe" # name: Windows_X86-64_interactive_installer # - path: "*Windows_64bit.msi" diff --git a/backend/helpers.js b/backend/helpers.js index 427d360..0976293 100644 --- a/backend/helpers.js +++ b/backend/helpers.js @@ -4,12 +4,12 @@ const path = require('path') async function openFolderDialog(win) { // https://stackoverflow.com/questions/46027287/electron-open-folder-dialog - let dir = await dialog.showOpenDialog(win, { properties: [ 'openDirectory' ] }) + const dir = await dialog.showOpenDialog(win, { properties: [ 'openDirectory' ] }) return dir.filePaths[0] || null } function listFolder(folder) { - files = fs.readdirSync(path.resolve(folder)) + let files = fs.readdirSync(path.resolve(folder)) // Filter out directories files = files.filter(f => { let filePath = path.resolve(folder, f) @@ -38,7 +38,7 @@ function ilistFolder(folder) { function getAllFiles(dirPath, arrayOfFiles) { // https://coderrocketfuel.com/article/recursively-list-all-the-files-in-a-directory-using-node-js - files = ilistFolder(dirPath) + let files = ilistFolder(dirPath) arrayOfFiles = arrayOfFiles || [] files.forEach(function(file) { const p = path.join(dirPath, file.path) diff --git a/backend/ipc.js b/backend/ipc.js index c97daba..d4ddc74 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -6,7 +6,7 @@ const { getAllFiles } = require('./helpers.js') -module.exports = function registerIPCHandlers(win, ipcMain) { +module.exports = function registerIPCHandlers(win, ipcMain, app) { ipcMain.handle('open-folder', async (event) => { console.log('ipcMain', 'open-folder') const folder = await openFolderDialog(win) @@ -107,4 +107,24 @@ module.exports = function registerIPCHandlers(win, ipcMain) { win.setMinimumSize(minWidth, minHeight) }) + + ipcMain.handle('confirm-close', () => { + console.log('ipcMain', 'confirm-close') + app.exit() + }) + + ipcMain.handle('is-packaged', () => { + return app.isPackaged + }) + + ipcMain.handle('get-app-path', () => { + console.log('ipcMain', 'get-app-path') + return app.getAppPath() + }) + + win.on('close', (event) => { + console.log('BrowserWindow', 'close') + event.preventDefault() + win.webContents.send('check-before-close') + }) } diff --git a/backend/menu.js b/backend/menu.js index 3ee40a6..6b62cdf 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -4,7 +4,6 @@ const openAboutWindow = require('about-window').default module.exports = function registerMenu(win) { const isMac = process.platform === 'darwin' - const isDev = !app.isPackaged const template = [ ...(isMac ? [{ label: app.name, @@ -56,17 +55,13 @@ module.exports = function registerMenu(win) { label: 'View', submenu: [ { role: 'reload' }, + { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, - ...(isDev ? [ - { type: 'separator' }, - { role: 'toggleDevTools' }, - ]:[ - ]) ] }, { diff --git a/index.js b/index.js index 2992967..b38f989 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,8 @@ const registerIPCHandlers = require('./backend/ipc.js') const registerMenu = require('./backend/menu.js') let win = null // main window +let splash = null +let splashTimestamp = null // START APP function createWindow () { @@ -17,17 +19,42 @@ function createWindow () { nodeIntegration: false, webSecurity: true, enableRemoteModule: false, - preload: path.join(__dirname, "preload.js") + preload: path.join(__dirname, "preload.js"), + show: false } }) // and load the index.html of the app. win.loadFile('ui/arduino/index.html') - registerIPCHandlers(win, ipcMain) - registerMenu(win) -} + // If the app takes a while to open, show splash screen + // Create the splash screen + splash = new BrowserWindow({ + width: 450, + height: 140, + transparent: true, + frame: false, + alwaysOnTop: true + }); + splash.loadFile('ui/arduino/splash.html') + splashTimestamp = Date.now() + + win.once('ready-to-show', () => { + if (Date.now()-splashTimestamp > 1000) { + splash.destroy() + } else { + setTimeout(() => { + splash.destroy() + }, 500) + } + win.show() + }) + registerIPCHandlers(win, ipcMain, app) + registerMenu(win) -// TODO: Loading splash screen + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +} -app.whenReady().then(createWindow) +app.on('ready', createWindow) diff --git a/package-lock.json b/package-lock.json index 61b8548..883d73e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "arduino-lab-micropython-ide", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arduino-lab-micropython-ide", - "version": "0.9.0", + "version": "0.10.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ad5c2cd..2d3f773 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "arduino-lab-micropython-ide", "productName": "Arduino Lab for MicroPython", - "version": "0.9.1", + "version": "0.10.0", "description": "Arduino Lab for MicroPython is a project sponsored by Arduino, based on original work by Murilo Polese.\nThis is an experimental pre-release software, please direct any questions exclusively to Github issues.", "main": "index.js", "scripts": { "post-set-shell": "npm config set script-shell bash", "rebuild": "electron-rebuild", "dev": "electron --inspect ./", - "build": "npm run post-set-shell && electron-builder $(if [ $(uname -m) = arm64 ]; then echo --mac --x64; fi)", + "build": "npm run post-set-shell && electron-builder", "postinstall": "npm run post-set-shell && npm run rebuild" }, "devDependencies": { @@ -21,6 +21,7 @@ "build": { "appId": "cc.arduino.micropython-lab", "artifactName": "${productName}-${os}_${arch}.${ext}", + "extraResources": "./ui/arduino/helpers.py", "mac": { "target": "zip", "icon": "build_resources/icon.icns" diff --git a/preload.js b/preload.js index d7b24e8..3388904 100644 --- a/preload.js +++ b/preload.js @@ -13,10 +13,10 @@ const Serial = { return ports.filter(p => p.vendorId && p.productId) }, connect: async (path) => { - return await board.open(path) + return board.open(path) }, disconnect: async () => { - return await board.close() + return board.close() }, run: async (code) => { return board.run(code) @@ -145,15 +145,22 @@ const Disk = { }, fileExists: async (filePath) => { return ipcRenderer.invoke('file-exists', filePath) + }, + getAppPath: () => { + return ipcRenderer.invoke('get-app-path') } } const Window = { setWindowSize: (minWidth, minHeight) => { ipcRenderer.invoke('set-window-size', minWidth, minHeight) - } + }, + beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), + confirmClose: () => ipcRenderer.invoke('confirm-close'), + isPackaged: () => ipcRenderer.invoke('is-packaged') } + contextBridge.exposeInMainWorld('BridgeSerial', Serial) contextBridge.exposeInMainWorld('BridgeDisk', Disk) contextBridge.exposeInMainWorld('BridgeWindow', Window) diff --git a/ui/arduino/index.html b/ui/arduino/index.html index 25f1967..8478cc7 100644 --- a/ui/arduino/index.html +++ b/ui/arduino/index.html @@ -30,6 +30,7 @@ <script type="text/javascript" src="views/components/repl-panel.js" charset="utf-8"></script> <script type="text/javascript" src="views/components/tabs.js" charset="utf-8"></script> <script type="text/javascript" src="views/components/toolbar.js" charset="utf-8"></script> + <script type="text/javascript" src="views/components/overlay.js" charset="utf-8"></script> <!-- Views --> <script type="text/javascript" src="views/editor.js" charset="utf-8"></script> diff --git a/ui/arduino/main.js b/ui/arduino/main.js index f7d1ef7..bf693df 100644 --- a/ui/arduino/main.js +++ b/ui/arduino/main.js @@ -19,27 +19,26 @@ function App(state, emit) { ` } - let overlay = html`<div id="overlay" class="closed"></div>` - - if (state.diskFiles == null) { - emit('load-disk-files') - overlay = html`<div id="overlay" class="open"><p>Loading files...</p></div>` + if (state.view == 'file-manager') { + return html` + <div id="app"> + ${FileManagerView(state, emit)} + ${Overlay(state, emit)} + </div> + ` + } else { + return html` + <div id="app"> + ${EditorView(state, emit)} + ${Overlay(state, emit)} + </div> + ` } - - if (state.isRemoving) overlay = html`<div id="overlay" class="open"><p>Removing...</p></div>` - if (state.isConnecting) overlay = html`<div id="overlay" class="open"><p>Connecting...</p></div>` - if (state.isLoadingFiles) overlay = html`<div id="overlay" class="open"><p>Loading files...</p></div>` - if (state.isSaving) overlay = html`<div id="overlay" class="open"><p>Saving file... ${state.savingProgress}</p></div>` - if (state.isTransferring) overlay = html`<div id="overlay" class="open"><p>Transferring file... ${state.transferringProgress}</p></div>` - - const view = state.view == 'editor' ? EditorView(state, emit) : FileManagerView(state, emit) return html` <div id="app"> - ${view} - ${overlay} + ${Overlay(state, emit)} </div> ` - } window.addEventListener('load', () => { @@ -49,7 +48,9 @@ window.addEventListener('load', () => { app.mount('#app') app.emitter.on('DOMContentLoaded', () => { - app.emitter.emit('refresh-files') + if (app.state.diskNavigationRoot) { + app.emitter.emit('refresh-files') + } }) }) diff --git a/ui/arduino/splash.html b/ui/arduino/splash.html new file mode 100644 index 0000000..15ae0b4 --- /dev/null +++ b/ui/arduino/splash.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> + <head> + <meta charset="utf-8"> + <title>Arduino Lab for MicroPython</title> + <style media="screen"> + body { + margin: 0; + background: white; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + </style> + </head> + <body> + <img width="100%" src="data:image/jpeg;base64,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PNItYbd20mN7ieB1X5YfMQ7ozgYADDI/3sdqA1O9ooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/2f/hDHdodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo3YmJhNjNkYi1iZjBiLTRlNjUtYTVkYy1jOTY2YjRiZDhhM2EiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6YmE3NzI0NWQtZTZlNy00Nzg4LWExNmItMzBiNTRlZTUwMDRiIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NjVkMjkzOGItM2RkNS00YWM5LWE3YjEtMjhlZmQ3YjBmOThkIiBkYzpGb3JtYXQ9ImltYWdlL2pwZWciIEdJTVA6QVBJPSIyLjAiIEdJTVA6UGxhdGZvcm09IkxpbnV4IiBHSU1QOlRpbWVTdGFtcD0iMTcxMzg2NDg3NDcxMzgzMiIgR0lNUDpWZXJzaW9uPSIyLjEwLjMwIiB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDpjaGFuZ2VkPSIvIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmJmNmU4MmMwLWNhY2UtNDExZC05YzZkLTVhMjNkM2NhMWEyYiIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChMaW51eCkiIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMTE6MzQ6MzQrMDI6MDAiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDw/eHBhY2tldCBlbmQ9InciPz7/4gKwSUNDX1BST0ZJTEUAAQEAAAKgbGNtcwQwAABtbnRyUkdCIFhZWiAH6AAEABcACQAgAAFhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1kZXNjAAABIAAAAEBjcHJ0AAABYAAAADZ3dHB0AAABmAAAABRjaGFkAAABrAAAACxyWFlaAAAB2AAAABRiWFlaAAAB7AAAABRnWFlaAAACAAAAABRyVFJDAAACFAAAACBnVFJDAAACFAAAACBiVFJDAAACFAAAACBjaHJtAAACNAAAACRkbW5kAAACWAAAACRkbWRkAAACfAAAACRt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" alt="Arduino Lab For MicroPython Logo"> + </body> +</html> diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 80656a5..f008c4b 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1021,17 +1021,19 @@ async function store(state, emitter) { emitter.on('toggle-file-selection', (file, source, event) => { log('toggle-file-selection', file, source, event) + let parentFolder = source == 'board' ? state.boardNavigationPath : state.diskNavigationPath // Single file selection unless holding keyboard key if (event && !event.ctrlKey && !event.metaKey) { state.selectedFiles = [{ fileName: file.fileName, type: file.type, source: source, - parentFolder: file.parentFolder + parentFolder: parentFolder }] emitter.emit('render') return } + const isSelected = state.selectedFiles.find((f) => { return f.fileName === file.fileName && f.source === source }) @@ -1044,79 +1046,89 @@ async function store(state, emitter) { fileName: file.fileName, type: file.type, source: source, - parentFolder: file.parentFolder + parentFolder: parentFolder }) } emitter.emit('render') }) emitter.on('open-selected-files', async () => { log('open-selected-files') - let files = [] + let filesToOpen = [] + let filesAlreadyOpen = [] for (let i in state.selectedFiles) { let selectedFile = state.selectedFiles[i] - let openFile = null if (selectedFile.type == 'folder') { // Don't open folders continue } - if (selectedFile.source == 'board') { - const fileContent = await serial.loadFile( - serial.getFullPath( - '/', - state.boardNavigationPath, - selectedFile.fileName + // ALl good until here + + const alreadyOpen = state.openFiles.find((f) => { + return f.fileName == selectedFile.fileName + && f.source == selectedFile.source + && f.parentFolder == selectedFile.parentFolder + }) + console.log('already open', alreadyOpen) + + if (!alreadyOpen) { + // This file is not open yet, + // load content and append it to the list of files to open + let file = null + if (selectedFile.source == 'board') { + const fileContent = await serial.loadFile( + serial.getFullPath( + state.boardNavigationRoot, + state.boardNavigationPath, + selectedFile.fileName + ) ) - ) - openFile = createFile({ - parentFolder: state.boardNavigationPath, - fileName: selectedFile.fileName, - source: selectedFile.source, - content: fileContent - }) - openFile.editor.onChange = function() { - openFile.hasChanges = true - emitter.emit('render') - } - } else if (selectedFile.source == 'disk') { - const fileContent = await disk.loadFile( - disk.getFullPath( - state.diskNavigationRoot, - state.diskNavigationPath, - selectedFile.fileName + file = createFile({ + parentFolder: state.boardNavigationPath, + fileName: selectedFile.fileName, + source: selectedFile.source, + content: fileContent + }) + file.editor.onChange = function() { + file.hasChanges = true + emitter.emit('render') + } + } else if (selectedFile.source == 'disk') { + const fileContent = await disk.loadFile( + disk.getFullPath( + state.diskNavigationRoot, + state.diskNavigationPath, + selectedFile.fileName + ) ) - ) - openFile = createFile({ - parentFolder: state.diskNavigationPath, - fileName: selectedFile.fileName, - source: selectedFile.source, - content: fileContent - }) - openFile.editor.onChange = function() { - openFile.hasChanges = true - emitter.emit('render') + file = createFile({ + parentFolder: state.diskNavigationPath, + fileName: selectedFile.fileName, + source: selectedFile.source, + content: fileContent + }) + file.editor.onChange = function() { + file.hasChanges = true + emitter.emit('render') + } } + filesToOpen.push(file) + } else { + // This file is already open, + // append it to the list of files that are already open + filesAlreadyOpen.push(alreadyOpen) } - files.push(openFile) } - files = files.filter((f) => { // find files to open - let isAlready = false - state.openFiles.forEach((g) => { // check if file is already open - if ( - g.fileName == f.fileName - && g.source == f.source - && g.parentFolder == f.parentFolder - ) { - isAlready = true - } - }) - return !isAlready - }) - - if (files.length > 0) { - state.openFiles = state.openFiles.concat(files) - state.editingFile = files[0].id + // If opening an already open file, switch to its tab + if (filesAlreadyOpen.length > 0) { + state.editingFile = filesAlreadyOpen[0].id } + // If there are new files to open, they take priority + if (filesToOpen.length > 0) { + state.editingFile = filesToOpen[0].id + } + + state.openFiles = state.openFiles.concat(filesToOpen) state.view = 'editor' emitter.emit('render') @@ -1303,6 +1315,15 @@ async function store(state, emitter) { emitter.emit('render') }) + win.beforeClose(async () => { + const hasChanges = !!state.openFiles.find(f => f.parentFolder && f.hasChanges) + if (hasChanges) { + const response = await confirm('You may have unsaved changes. Are you sure you want to proceed?', 'Yes', 'Cancel') + if (!response) return false + } + await win.confirmClose() + }) + function createFile(args) { const { source, @@ -1467,26 +1488,9 @@ function canEdit({ selectedFiles }) { return files.length != 0 } -function toggleFileSelection({ fileName, source, selectedFiles }) { - let result = [] - let file = selectedFiles.find((f) => { - return f.fileName === fileName && f.source === source - }) - if (file) { - // filter file out - result = selectedFiles.filter((f) => { - return f.fileName !== fileName && f.source !== source - }) - } else { - // push file - selectedFiles.push({ fileName, source }) - } - return result -} - async function removeBoardFolder(fullPath) { // TODO: Replace with getting the file tree from the board and deleting one by one - let output = await serial.execFile('./ui/arduino/helpers.py') + let output = await serial.execFile(await getHelperFullPath()) await serial.run(`delete_folder('${fullPath}')`) } @@ -1518,7 +1522,7 @@ async function uploadFolder(srcPath, destPath, dataConsumer) { async function downloadFolder(srcPath, destPath, dataConsumer) { dataConsumer = dataConsumer || function() {} await disk.createFolder(destPath) - let output = await serial.execFile('./ui/arduino/helpers.py') + let output = await serial.execFile(await getHelperFullPath()) output = await serial.run(`ilist_all('${srcPath}')`) let files = [] try { @@ -1546,3 +1550,20 @@ async function downloadFolder(srcPath, destPath, dataConsumer) { } } } + +async function getHelperFullPath() { + const appPath = await disk.getAppPath() + if (await win.isPackaged()) { + return disk.getFullPath( + appPath, + '..', + 'ui/arduino/helpers.py' + ) + } else { + return disk.getFullPath( + appPath, + 'ui/arduino/helpers.py', + '' + ) + } +} diff --git a/ui/arduino/views/components/elements/editor.js b/ui/arduino/views/components/elements/editor.js index 81d6fcb..091c6fa 100644 --- a/ui/arduino/views/components/elements/editor.js +++ b/ui/arduino/views/components/elements/editor.js @@ -3,26 +3,43 @@ class CodeMirrorEditor extends Component { super() this.editor = null this.content = '# empty file' + this.scrollTop = 0 } + createElement(content) { + if (content) this.content = content + return html`<div id="code-editor"></div>` + } + + load(el) { const onCodeChange = (update) => { - // console.log('code change', this.content) this.content = update.state.doc.toString() this.onChange() } this.editor = createEditor(this.content, el, onCodeChange) - } - createElement(content) { - if (content) this.content = content - return html`<div id="code-editor"></div>` + setTimeout(() => { + this.editor.scrollDOM.addEventListener('scroll', this.updateScrollPosition.bind(this)) + this.editor.scrollDOM.scrollTo({ + top: this.scrollTop, + left: 0 + }) + }, 10) } update() { return false } + unload() { + this.editor.scrollDOM.removeEventListener('scroll', this.updateScrollPosition) + } + + updateScrollPosition(e) { + this.scrollTop = e.target.scrollTop + } + onChange() { return false } diff --git a/ui/arduino/views/components/elements/tab.js b/ui/arduino/views/components/elements/tab.js index f0070f3..6036d6b 100644 --- a/ui/arduino/views/components/elements/tab.js +++ b/ui/arduino/views/components/elements/tab.js @@ -57,7 +57,7 @@ function Tab(args) { } function selectTab(e) { - if(e.target.tagName === 'BUTTON' || e.target.tagName === 'IMG') return + if(e.target.classList.contains('close-tab')) return onSelectTab(e) } @@ -71,9 +71,9 @@ function Tab(args) { <div class="text"> ${hasChanges ? '*' : ''} ${text} </div> - <div class="options"> - <button onclick=${onCloseTab}> - <img class="icon" src="media/close.svg" /> + <div class="options close-tab"> + <button class="close-tab" onclick=${onCloseTab}> + <img class="close-tab icon" src="media/close.svg" /> </button> </div> </div> diff --git a/ui/arduino/views/components/overlay.js b/ui/arduino/views/components/overlay.js new file mode 100644 index 0000000..1b9389c --- /dev/null +++ b/ui/arduino/views/components/overlay.js @@ -0,0 +1,16 @@ +function Overlay(state, emit) { + let overlay = html`<div id="overlay" class="closed"></div>` + + if (state.diskFiles == null) { + emit('load-disk-files') + overlay = html`<div id="overlay" class="open"><p>Loading files...</p></div>` + } + + if (state.isRemoving) overlay = html`<div id="overlay" class="open"><p>Removing...</p></div>` + if (state.isConnecting) overlay = html`<div id="overlay" class="open"><p>Connecting...</p></div>` + if (state.isLoadingFiles) overlay = html`<div id="overlay" class="open"><p>Loading files...</p></div>` + if (state.isSaving) overlay = html`<div id="overlay" class="open"><p>Saving file... ${state.savingProgress}</p></div>` + if (state.isTransferring) overlay = html`<div id="overlay" class="open"><p>Transferring file... ${state.transferringProgress}</p></div>` + + return overlay +}