import { dialog, getCurrentWindow } from "@electron/remote"; import React from "react"; import { observable, computed, action, toJS, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { readCsvFile, writeCsvFile, getValidFileNameFromFileName } from "eez-studio-shared/util-electron"; import { stringCompare } from "eez-studio-shared/string"; import { beginTransaction, commitTransaction } from "eez-studio-shared/store"; import { formatDateTimeLong } from "eez-studio-shared/util"; import { validators } from "eez-studio-shared/validation"; import { Icon } from "eez-studio-ui/icon"; import { Splitter } from "eez-studio-ui/splitter"; import { VerticalHeaderWithBody, ToolbarHeader, Body } from "eez-studio-ui/header-with-body"; import { IconAction, ButtonAction } from "eez-studio-ui/action"; import { List as ListComponent } from "eez-studio-ui/list"; import { Dialog, showDialog } from "eez-studio-ui/dialog"; import { error, confirm } from "eez-studio-ui/dialog-electron"; import { showGenericDialog } from "eez-studio-ui/generic-dialog"; import * as notification from "eez-studio-ui/notification"; import { PropertyList, NumberInputProperty } from "eez-studio-ui/properties"; import { Header } from "eez-studio-ui/header-with-body"; import { logGet, logUpdate } from "instrument/window/history/activity-log"; import { DEFAULT_INSTRUMENT_PROPERTIES } from "instrument/DEFAULT_INSTRUMENT_PROPERTIES"; import { getList, sendList } from "instrument/connection/list-operations-renderer"; import type { InstrumentAppStore } from "instrument/window/app-store"; import type { IAppStore, IInstrumentObject } from "instrument/window/history/history"; import type { BaseList, ITableListData } from "instrument/window/lists/store-renderer"; import { createEmptyListData, createTableListFromData } from "instrument/window/lists/factory"; import { getTableListData } from "instrument/window/lists/table-data"; //////////////////////////////////////////////////////////////////////////////// const CONF_DEFAULT_ENVELOPE_LIST_DURATION = 1; // 1 second //////////////////////////////////////////////////////////////////////////////// const MasterView = observer( class MasterView extends React.Component< { appStore: InstrumentAppStore; selectedList: BaseList | undefined; selectList: (list: BaseList) => void; }, {} > { constructor(props: { appStore: InstrumentAppStore; selectedList: BaseList | undefined; selectList: (list: BaseList) => void; }) { super(props); makeObservable(this, { sortedLists: computed }); } get sortedLists() { return this.props.appStore.instrumentLists .slice() .sort((a, b) => stringCompare(a.name, b.name)) .map(list => ({ id: list.id, data: list, selected: this.props.selectedList !== undefined && list.id === this.props.selectedList.id })); } addList = () => { showGenericDialog({ dialogDefinition: { fields: [ { name: "type", type: "enum", enumItems: ["table", "envelope"] }, { name: "name", type: "string", validators: [ validators.required, validators.unique( {}, this.props.appStore.instrumentLists ) ] }, { name: "description", type: "string" }, { name: "duration", unit: "time", validators: [validators.rangeExclusive(0)], visible: (values: any) => values.type === "envelope" }, { name: "numSamples", displayName: "No. of samples", type: "integer", validators: [ validators.rangeInclusive( 1, this.props.appStore.instrument .listsMaxPointsProperty ) ], visible: (values: any) => values.type === "envelope" } ] }, values: { type: "table", name: "", description: "", duration: CONF_DEFAULT_ENVELOPE_LIST_DURATION, numSamples: this.props.appStore.instrument.listsMaxPointsProperty } }) .then(async result => { beginTransaction("Add instrument list"); let listId = this.props.appStore.instrumentListStore.createObject({ type: result.values.type, name: result.values.name, description: result.values.description, data: createEmptyListData( result.values.type, { duration: result.values.duration, numSamples: result.values.numSamples }, this.props.appStore.instrument ) }); commitTransaction(); await this.props.appStore.navigationStore.changeSelectedListId( listId ); setTimeout(() => { let element = document.querySelector( `.EezStudio_InstrumentList_${listId}` ); if (element) { element.scrollIntoView(); } }, 10); }) .catch(() => {}); }; removeList = () => { confirm("Are you sure?", undefined, () => { beginTransaction("Remove instrument list"); this.props.appStore.instrumentListStore.deleteObject( this.props.selectedList!.toJS() ); commitTransaction(); }); }; render() { return ( (
{node.data.name}
)} selectNode={node => this.props.selectList(node.data) } />
); } } ); export const DetailsView = observer( class DetailsView extends React.Component<{ appStore: IAppStore; list: BaseList | undefined; }> { render() { const { list } = this.props; const description = list && list.description; const modifiedAtStr = list && list.modifiedAt ? formatDateTimeLong(list.modifiedAt) : undefined; return ( {(description || modifiedAtStr) && (
{" "} {description}{" "} {modifiedAtStr && ( {description ? " - " : ""}Modified at{" "} {modifiedAtStr} )}
)} {list && list.renderDetailsView(this.props.appStore)}
); } } ); export const ListsEditor = observer( class ListsEditor extends React.Component< { appStore: InstrumentAppStore }, {} > { constructor(props: { appStore: InstrumentAppStore }) { super(props); makeObservable(this, { selectedList: computed }); } get selectedList() { return this.props.appStore.instrumentLists.find( list => list.id == this.props.appStore.navigationStore.selectedListId ); } render() { return ( this.props.appStore.navigationStore.changeSelectedListId( list.id ) )} /> ); } } ); //////////////////////////////////////////////////////////////////////////////// export const SelectChannelDialog = observer( class SelectChannelDialog extends React.Component<{ label: string; numChannels: number; callback: (channelIndex: number) => void; }> { static lastChannelIndex = 1; channelIndex: number = SelectChannelDialog.lastChannelIndex; inputErrors: string[] = []; constructor(props: any) { super(props); makeObservable(this, { channelIndex: observable, inputErrors: observable, handleSubmit: action.bound }); } handleSubmit() { if (this.channelIndex <= 0) { this.inputErrors = ["Invalid value"]; return false; } SelectChannelDialog.lastChannelIndex = this.channelIndex; this.props.callback(this.channelIndex - 1); return true; } render() { const { label } = this.props; // const {numChannels} = this.props; // const property = ( // (this.channelIndex = parseInt(value)))} // > // {_range(numChannels).map(channelIndex => ( // // ))} // // ); const property = ( (this.channelIndex = value))} min={1} errors={this.inputErrors} > ); return ( {property} ); } } ); async function selectChannel(label: string, numChannels: number) { return new Promise(resolve => { showDialog( ); }); } //////////////////////////////////////////////////////////////////////////////// export function getCsvDataColumnDefinitions(instrument: IInstrumentObject) { return [ { id: "dwell", digits: instrument.listsDwellDigitsProperty }, { id: "voltage", digits: instrument.listsVoltageDigitsProperty }, { id: "current", digits: instrument.listsCurrentDigitsProperty } ]; } export async function saveTableListData( instrument: IInstrumentObject, listName: string, tableListData: ITableListData ) { const result = await dialog.showSaveDialog(getCurrentWindow(), { defaultPath: listName ? getValidFileNameFromFileName(listName) + ".list" : undefined, filters: [{ name: "EEZ List Files", extensions: ["list"] }] }); let filePath = result.filePath; if (filePath) { if (!filePath.toLowerCase().endsWith(".list")) { filePath += ".list"; } try { await writeCsvFile( filePath, tableListData, getCsvDataColumnDefinitions(instrument) ); notification.success(`List exported to "${filePath}".`); } catch (err) { error("Failed to write list file.", err.toString()); } } } //////////////////////////////////////////////////////////////////////////////// export const ListsButtons = observer( class ListsButtons extends React.Component< { appStore: InstrumentAppStore }, {} > { constructor(props: { appStore: InstrumentAppStore }) { super(props); makeObservable(this, { selectedList: computed }); } get selectedList() { return this.props.appStore.instrumentLists.find( list => list.id == this.props.appStore.navigationStore.selectedListId ); } import = async () => { const result = await dialog.showOpenDialog(getCurrentWindow(), { properties: ["openFile"], filters: [ { name: "CSV Files", extensions: ["csv"] }, { name: "All Files", extensions: ["*"] } ] }); const filePaths = result.filePaths; if (filePaths && filePaths[0]) { let data = await readCsvFile( filePaths[0], getCsvDataColumnDefinitions(this.props.appStore.instrument) ); if (!data) { error("Failed to load CSV file.", undefined); return; } showGenericDialog({ dialogDefinition: { fields: [ { name: "name", type: "string", validators: [ validators.required, validators.unique( {}, this.props.appStore.instrumentLists ) ] }, { name: "description", type: "string" } ] }, values: { name: "", description: "" } }) .then(async result => { let list = createTableListFromData(data); list.name = result.values.name; list.description = result.values.description; beginTransaction("Import instrument list"); let listId = this.props.appStore.instrumentListStore.createObject( list.toJS() ); commitTransaction(); await this.props.appStore.navigationStore.changeSelectedListId( listId ); setTimeout(() => { let element = document.querySelector( `.EezStudio_InstrumentList_${listId}` ); if (element) { element.scrollIntoView(); } }, 10); }) .catch(() => {}); } }; export = () => { if (this.selectedList) { saveTableListData( this.props.appStore.instrument, this.selectedList.name, getTableListData( this.selectedList, this.props.appStore.instrument ) ); } }; get numChannels(): number { const channels = this.props.appStore.instrument.channelsProperty; if (channels) { return channels.length; } return DEFAULT_INSTRUMENT_PROPERTIES.properties.channels!.length; } getList = async () => { let channelIndex = await selectChannel( "Get list from channel:", this.numChannels ); const connection = this.props.appStore.instrument.connection; try { await connection.acquire(false); } catch (err) { notification.error(`Failed to get list: ${err.toString()}`); return; } let listData, logId: string; try { ({ listData, logId } = await getList( this.props.appStore.history.oid, channelIndex )); } catch (err) { notification.error(`Failed to get list: ${err.toString()}`); return; } finally { connection.release(); } const tableListData = Object.assign({}, listData[0]); const tableList = createTableListFromData(tableListData); showGenericDialog({ dialogDefinition: { fields: [ { name: "name", type: "string", validators: [ validators.required, validators.unique( {}, this.props.appStore.instrumentLists ) ] }, { name: "description", type: "string" } ] }, values: { name: "", description: "" } }) .then(async result => { tableList.name = result.values.name; tableList.description = result.values.description; beginTransaction("Get instrument list"); let listId = this.props.appStore.instrumentListStore.createObject( tableList.toJS() ); commitTransaction(); await this.props.appStore.navigationStore.changeSelectedListId( listId ); setTimeout(() => { let element = document.querySelector( `.EezStudio_InstrumentList_${listId}` ); if (element) { element.scrollIntoView(); } }, 10); // set list name in activity log let activityLog = logGet( this.props.appStore.history.options.store, logId ); let message = JSON.parse(activityLog.message); message.listName = tableList.name; activityLog.message = JSON.stringify(message); logUpdate( this.props.appStore.history.options.store, activityLog, { undoable: false } ); }) .catch(() => {}); }; sendList = async () => { if (this.selectedList) { let channelIndex = await selectChannel( "Send list to channel:", this.numChannels ); const channel = getTableListData( this.selectedList, this.props.appStore.instrument ); const connection = this.props.appStore.instrument.connection; try { await connection.acquire(false); } catch (err) { notification.error( `Failed to send list: ${err.toString()}` ); return; } try { await sendList( this.props.appStore.history.oid, channelIndex, this.selectedList.name, { dwell: toJS(channel.dwell), current: toJS(channel.current), voltage: toJS(channel.voltage) } ); notification.success(`List sent.`); } catch (err) { notification.error( `Failed to send list: ${err.toString()}` ); } finally { connection.release(); } } }; updateModifedAt() { const list = this.selectedList; if (list) { let oldModifiedAt = list.modifiedAt; let newModifedAt = new Date(); this.props.appStore.undoManager.addCommand( "Set list modifiedAt", this.props.appStore.instrumentListStore, list, { execute: action(() => { list.modifiedAt = newModifedAt; }), undo: action(() => { list.modifiedAt = oldModifiedAt; }) } ); } } render() { return ( {this.props.appStore.undoManager.modified && ( { this.updateModifedAt(); this.props.appStore.undoManager.commit(); }} /> )} ); } } ); //////////////////////////////////////////////////////////////////////////////// export function render(appStore: InstrumentAppStore) { return ; } export function toolbarButtonsRender(appStore: InstrumentAppStore) { return appStore.instrument ? :
; }