import path from "path"; import React from "react"; import { observable, computed, values, action, runInAction, toJS, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { stringCompare } from "eez-studio-shared/string"; import { validators } from "eez-studio-shared/validation"; import { readTextFile } from "eez-studio-shared/util-electron"; import { AlertDanger } from "eez-studio-ui/alert"; import { Splitter } from "eez-studio-ui/splitter"; import { List } from "eez-studio-ui/list"; import { IconAction, ButtonAction } from "eez-studio-ui/action"; import { CodeEditor } from "eez-studio-ui/code-editor"; import { VerticalHeaderWithBody, ToolbarHeader, Header, Body } from "eez-studio-ui/header-with-body"; import { showGenericDialog } from "eez-studio-ui/generic-dialog"; import { confirm } from "eez-studio-ui/dialog-electron"; import { Icon } from "eez-studio-ui/icon"; import * as notification from "eez-studio-ui/notification"; import { Toolbar } from "eez-studio-ui/toolbar"; import type { IShortcut } from "shortcuts/interfaces"; import { DEFAULT_TOOLBAR_BUTTON_COLOR } from "shortcuts/toolbar-button-colors"; import { showShortcutDialog } from "shortcuts/shortcut-dialog"; import type { InstrumentAppStore } from "instrument/window/app-store"; import { executeShortcut, isShorcutRunning, stopActiveShortcut } from "instrument/window/script"; import type { IModel } from "instrument/window/undo"; import { Terminal } from "instrument/window/terminal/terminal"; export class ScriptsModel implements IModel { constructor(private appStore: InstrumentAppStore) { makeObservable(this, { _newActionCode: observable, errorMessage: observable, errorLineNumber: observable, errorColumnNumber: observable, terminalVisible: observable, selectedScript: computed, modified: computed, commit: action.bound, rollback: action.bound, dismissError: action.bound, toggleTerminal: action.bound, canUpload: computed }); } _newActionCode: string | undefined; get newActionCode() { return this._newActionCode; } set newActionCode(value: string | undefined) { runInAction(() => { this._newActionCode = value; }); if (this.modified) { this.appStore.undoManager.model = this; } else { this.appStore.undoManager.model = undefined; } } errorMessage: string | undefined; errorLineNumber: number | undefined; errorColumnNumber: number | undefined; terminalVisible: boolean; get selectedScript() { return values(this.appStore.shortcutsStore.shortcuts).find( script => script.id === this.appStore.navigationStore.selectedScriptId ); } get modified() { return !!( this.selectedScript && this.newActionCode && this.selectedScript.action.data !== this.newActionCode ); } commit() { if (this.selectedScript) { this.appStore.shortcutsStore.updateShortcut({ id: this.selectedScript.id, action: Object.assign({}, toJS(this.selectedScript.action), { data: this.newActionCode }) }); } this.newActionCode = undefined; } rollback() { if (this.selectedScript) { this._newActionCode = undefined; } } dismissError() { this.errorMessage = undefined; } toggleTerminal() { this.terminalVisible = !this.terminalVisible; } get codeEditor() { return this.appStore.scriptView && this.appStore.scriptView.codeEditor; } get canUndo() { return this.codeEditor ? this.codeEditor.canUndo : false; } undo() { if (this.codeEditor) { this.codeEditor.undo(); } } get canRedo() { return this.codeEditor ? this.codeEditor.canRedo : false; } redo() { if (this.codeEditor) { this.codeEditor.redo(); } } addScript = () => { showGenericDialog({ dialogDefinition: { fields: [ { name: "name", type: "string", validators: [ validators.required, validators.unique( {}, values(this.appStore.shortcutsStore.shortcuts) ) ] }, { name: "type", type: "enum", enumItems: this.appStore.instrument.commandsProtocol == "SCPI" ? [ { id: "scpi-commands", label: "SCPI" }, { id: "javascript", label: "JavaScript" }, { id: "micropython", label: "MicroPython" } ] : [ { id: "commands", label: "Commands" }, { id: "javascript", label: "JavaScript" } ], validators: [validators.required] } ] }, values: { name: "", type: this.appStore.instrument.commandsProtocol == "SCPI" ? "scpi-commands" : "commands" } }) .then(result => { const scriptId = this.appStore.shortcutsStore.addShortcut({ name: result.values.name, action: { type: result.values.type, data: "" }, keybinding: "", groupName: "", showInToolbar: false, toolbarButtonPosition: 0, toolbarButtonColor: DEFAULT_TOOLBAR_BUTTON_COLOR, requiresConfirmation: false, selected: false }); if (scriptId) { this.appStore.navigationStore.changeSelectedScriptId( scriptId ); } }) .catch(() => {}); }; deleteScript = () => { const selectedScript = this.selectedScript; if (selectedScript) { confirm("Are you sure?", undefined, () => { this.appStore.shortcutsStore.deleteShortcut(selectedScript); }); } }; run = () => { if (this.selectedScript) { executeShortcut(this.appStore, this.selectedScript); } }; get canUpload() { const instrument = this.appStore.instrument; const connection = instrument.connection; return ( this.selectedScript && this.selectedScript.action.type === "micropython" && connection.isConnected && instrument.getFileDownloadProperty() ); } upload = () => { if (this.canUpload) { const instrument = this.appStore.instrument; const connection = instrument.connection; connection.doUpload( Object.assign({}, instrument.defaultFileUploadInstructions, { sourceData: this.newActionCode || this.selectedScript!.action.data, sourceFileType: "text/x-python", destinationFileName: this.selectedScript!.name + ".py", destinationFolderPath: "/Scripts" }), () => notification.success(`Script uploaded.`), err => notification.error( `Failed to upload script: ${err.toString()}` ) ); } }; } export class ScriptViewComponent extends React.Component< { appStore: InstrumentAppStore }, {} > { codeEditor: CodeEditor | null; componentDidMount() { this.props.appStore.scriptView = this; } componentWillUnmount() { this.props.appStore.scriptView = null; } render() { const scriptsModel = this.props.appStore.scriptsModel; let codeEditor; if (scriptsModel.selectedScript) { codeEditor = ( (this.codeEditor = ref)} value={scriptsModel.selectedScript.action.data} onChange={(value: string) => { scriptsModel.newActionCode = value; }} mode={ scriptsModel.selectedScript.action.type === "scpi-commands" || scriptsModel.selectedScript.action.type === "commands" ? "scpi" : scriptsModel.selectedScript.action.type === "javascript" ? "javascript" : "python" } lineNumber={scriptsModel.errorLineNumber} columnNumber={scriptsModel.errorColumnNumber} /> ); } return (
{scriptsModel.errorMessage && ( {scriptsModel.errorMessage} )} {codeEditor}
); } } export const ScriptView = observer(ScriptViewComponent); function getScriptIcon(script: IShortcut) { if (script.action.type === "scpi-commands") { return ; } else if (script.action.type === "commands") { return ; } else if (script.action.type === "javascript") { return ; } else { return ; } } const MasterView = observer( class MasterView extends React.Component<{ appStore: InstrumentAppStore; selectedScript: IShortcut | undefined; selectScript: (script: IShortcut) => void; }> { constructor(props: { appStore: InstrumentAppStore; selectedScript: IShortcut | undefined; selectScript: (script: IShortcut) => void; }) { super(props); makeObservable(this, { sortedLists: computed }); } get sortedLists() { return Array.from( this.props.appStore.shortcutsStore.shortcuts.values() ) .sort((a, b) => stringCompare(a.name, b.name)) .map(script => ({ id: script.id, data: script, selected: this.props.appStore.scriptsModel.selectedScript === script })); } render() { const scriptsModel = this.props.appStore.scriptsModel; return ( (
{getScriptIcon(node.data)} {node.data.name}
)} selectNode={node => this.props.selectScript(node.data) } />
); } } ); export const ScriptHeader = observer( class ScriptHeader extends React.Component<{ appStore: InstrumentAppStore; }> { editShortcut = () => { const selectedScript = this.props.appStore.scriptsModel.selectedScript; if (!selectedScript) { return; } showShortcutDialog( this.props.appStore.shortcutsStore, this.props.appStore.groupsStore, selectedScript, shortcut => { this.props.appStore.shortcutsStore.updateShortcut!( shortcut ); }, undefined, undefined, undefined, true ); }; searchAndReplace = action(() => { if (this.props.appStore.scriptsModel.codeEditor) { this.props.appStore.scriptsModel.codeEditor.openSearchbox(); } }); render() { return (
); } } ); export const DetailsView = observer( class DetailsView extends React.Component<{ appStore: InstrumentAppStore; }> { render() { const { appStore } = this.props; return this.props.appStore.scriptsModel.selectedScript ? ( ) : null; } } ); export const ScriptsEditor = observer( class ScriptsEditor extends React.Component<{ appStore: InstrumentAppStore; }> { render() { const appStore = this.props.appStore; const scriptsModel = this.props.appStore.scriptsModel; const navigationStore = this.props.appStore.navigationStore; return ( navigationStore.changeSelectedScriptId( script.id ) } /> {scriptsModel.terminalVisible && appStore.instrument && ( )} ); } } ); export function render(appStore: InstrumentAppStore) { return ; } export function toolbarButtonsRender(appStore: InstrumentAppStore) { const scriptsModel = appStore.scriptsModel; return ( {scriptsModel.modified && ( )} {scriptsModel.canUpload && ( )} {!isShorcutRunning() && scriptsModel.selectedScript && appStore.instrument.connection.isConnected && (scriptsModel.selectedScript.action.type === "scpi-commands" || scriptsModel.selectedScript.action.type === "commands" || scriptsModel.selectedScript.action.type === "javascript") && ( )} {isShorcutRunning() && ( )} ); } export async function importScript( appStore: InstrumentAppStore, filePath: string ) { const isMicroPython = filePath.toLowerCase().endsWith(".py"); const isJavaScript = filePath.toLowerCase().endsWith(".js"); if (!isMicroPython && !isJavaScript) { return false; } const scriptSourceText = await readTextFile(filePath); const name = path.basename(filePath, filePath.slice(-3)); const script = values(appStore.shortcutsStore.shortcuts).find( script => script.name === name ); let scriptId: string | undefined; if (script) { scriptId = script.id; if (scriptId == appStore.navigationStore.selectedScriptId) { appStore.scriptsModel.newActionCode = scriptSourceText; return; } appStore.shortcutsStore.updateShortcut({ id: scriptId, action: Object.assign({}, toJS(script.action), { data: scriptSourceText }) }); } else { scriptId = appStore.shortcutsStore.addShortcut({ name, action: { type: isMicroPython ? "micropython" : "javascript", data: scriptSourceText }, keybinding: "", groupName: "", showInToolbar: false, toolbarButtonPosition: 0, toolbarButtonColor: DEFAULT_TOOLBAR_BUTTON_COLOR, requiresConfirmation: false, selected: false }); } if (scriptId) { appStore.navigationStore.navigateToScripts(); appStore.navigationStore.changeSelectedScriptId(scriptId); } return true; }