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 @@
+
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`
`
-
- if (state.diskFiles == null) {
- emit('load-disk-files')
- overlay = html``
+ if (state.view == 'file-manager') {
+ return html`
+
+ ${FileManagerView(state, emit)}
+ ${Overlay(state, emit)}
+
+ `
+ } else {
+ return html`
+
+ ${EditorView(state, emit)}
+ ${Overlay(state, emit)}
+
+ `
}
-
- if (state.isRemoving) overlay = html``
- if (state.isConnecting) overlay = html``
- if (state.isLoadingFiles) overlay = html``
- if (state.isSaving) overlay = html`Saving file... ${state.savingProgress}
`
- if (state.isTransferring) overlay = html`Transferring file... ${state.transferringProgress}
`
-
- const view = state.view == 'editor' ? EditorView(state, emit) : FileManagerView(state, emit)
return html`
- ${view}
- ${overlay}
+ ${Overlay(state, emit)}
`
-
}
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 @@
+
+
+
+
+ Arduino Lab for MicroPython
+
+
+
+
+
+
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``
+ }
+
+
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``
+ 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) {
${hasChanges ? '*' : ''} ${text}
-
-
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``
+
+ if (state.diskFiles == null) {
+ emit('load-disk-files')
+ overlay = html``
+ }
+
+ if (state.isRemoving) overlay = html``
+ if (state.isConnecting) overlay = html``
+ if (state.isLoadingFiles) overlay = html``
+ if (state.isSaving) overlay = html`Saving file... ${state.savingProgress}
`
+ if (state.isTransferring) overlay = html`Transferring file... ${state.transferringProgress}
`
+
+ return overlay
+}