diff --git a/src/mainview.tsx b/src/mainview.tsx index a5b1a055..25b34a5a 100644 --- a/src/mainview.tsx +++ b/src/mainview.tsx @@ -11,10 +11,10 @@ import { JupyterCadModel } from './model'; import { IDict, IDisplayShape, + IJupyterCadClientState, IMainMessage, IWorkerMessage, MainAction, - Position, WorkerAction } from './types'; @@ -32,7 +32,8 @@ interface IProps { } interface IStates { - id: string; + id: string; // ID of the component, it is used to identify which component + //is the source of awareness updates. loading: boolean; lightTheme: boolean; } @@ -45,10 +46,7 @@ export class MainView extends React.Component { this._geometry.setDrawRange(0, 3 * 10000); this._refLength = 0; this._sceneAxe = []; - // this.shapeGroup = new THREE.Group(); - // this.sceneScaled = false; - // this.computedScene = {}; - // this.progressData = { time_step: -1, data: {} }; + this._resizeTimeout = null; const lightTheme = @@ -76,15 +74,13 @@ export class MainView extends React.Component { { action: WorkerAction.REGISTER, payload: { id: this.state.id } }, this._messageChannel.port2 ); - this._model.themeChanged.connect((_, arg) => { - this.handleThemeChange(); - }); - this._model.cameraChanged.connect(this._onCameraChanged); + this._model.themeChanged.connect(this._handleThemeChange); + this._model.clientStateChanged.connect(this._onClientSharedStateChanged); }); } componentDidMount(): void { - window.addEventListener('resize', this.handleWindowResize); + window.addEventListener('resize', this._handleWindowResize); this.generateScene(); } @@ -94,7 +90,7 @@ export class MainView extends React.Component { componentWillUnmount(): void { window.cancelAnimationFrame(this._requestID); - window.removeEventListener('resize', this.handleWindowResize); + window.removeEventListener('resize', this._handleWindowResize); this._controls.dispose(); this.postMessage({ action: WorkerAction.CLOSE_FILE, @@ -102,21 +98,10 @@ export class MainView extends React.Component { fileName: this._context.path } }); + this._model.themeChanged.disconnect(this._handleThemeChange); + this._model.clientStateChanged.disconnect(this._onClientSharedStateChanged); } - handleThemeChange = (): void => { - const lightTheme = - document.body.getAttribute('data-jp-theme-light') === 'true'; - this.setState(old => ({ ...old, lightTheme })); - }; - - handleWindowResize = () => { - clearTimeout(this._resizeTimeout); - this._resizeTimeout = setTimeout(() => { - this.forceUpdate(); - }, 500); - }; - addSceneAxe = (dir: THREE.Vector3, color: number): void => { const origin = new THREE.Vector3(0, 0, 0); const length = 20; @@ -255,13 +240,16 @@ export class MainView extends React.Component { canvas.addEventListener( evtName as any, (event: MouseEvent | WheelEvent) => { - this._model.syncCamera({ - offsetX: event.offsetX, - offsetY: event.offsetY, - x: this._camera.position.x, - y: this._camera.position.y, - z: this._camera.position.z - }); + this._model.syncCamera( + { + offsetX: event.offsetX, + offsetY: event.offsetY, + x: this._camera.position.x, + y: this._camera.position.y, + z: this._camera.position.z + }, + this.state.id + ); } ); }); @@ -395,7 +383,17 @@ export class MainView extends React.Component { } private _onClick(e: MouseEvent) { - this._selectedMesh = this._pick(); + const selectedMesh = this._pick(); + if (selectedMesh) { + if (selectedMesh === this._selectedMesh) { + this._selectedMesh = null; + } else { + this._selectedMesh = selectedMesh; + } + if (this._selectedMesh) { + this._model.syncSelectedObject(this._selectedMesh.name, this.state.id); + } + } } private shapeToMesh = (payload: IDisplayShape['payload']) => { @@ -531,61 +529,76 @@ export class MainView extends React.Component { } }; - private _onCameraChanged = ( + private _onClientSharedStateChanged = ( sender: JupyterCadModel, - clients: Map + clients: Map ): void => { - clients.forEach((client, key) => { - if (this._context.model.getClientId() !== key) { - const id = key.toString(); - const mouse = client.mouse as Position; - if (mouse && this._cameraClients[id]) { - if (mouse.offsetX > 0) { - this._cameraClients[id]!.style.left = mouse.offsetX + 'px'; - } - if (mouse.offsetY > 0) { - this._cameraClients[id]!.style.top = mouse.offsetY + 'px'; - } - if (!this._mouseDown) { - this._camera.position.set(mouse.x, mouse.y, mouse.z); + const clientId = this._context.model.getClientId(); + // TODO Handle state changes from another user in follow mode. + const targetId: number | null = null; + if (targetId) { + const remoteState = clients.get(targetId)!; + const mouse = remoteState?.mouse.value; + if (mouse && this._cameraClients[targetId]) { + if (mouse.offsetX > 0) { + this._cameraClients[targetId]!.style.left = mouse.offsetX + 'px'; + } + if (mouse.offsetY > 0) { + this._cameraClients[targetId]!.style.top = mouse.offsetY + 'px'; + } + if (!this._mouseDown) { + this._camera.position.set(mouse.x, mouse.y, mouse.z); + } + } else if (mouse && !this._cameraClients[targetId]) { + const el = document.createElement('div'); + el.className = 'jpcad-camera-client'; + el.style.left = mouse.offsetX + 'px'; + el.style.top = mouse.offsetY + 'px'; + el.style.backgroundColor = remoteState.user.color; + el.innerText = remoteState.user.name; + this._cameraClients[targetId] = el; + this._cameraRef.current?.appendChild(el); + } else if (!mouse && this._cameraClients[targetId]) { + this._cameraRef.current?.removeChild(this._cameraClients[targetId]!); + this._cameraClients[targetId] = undefined; + } + } else { + // Sync local state updated by other components + + const localState = clients.get(clientId); + if (localState) { + if ( + localState.selected?.emitter && + localState.selected?.emitter !== this.state.id + ) { + if (this._selectedMesh?.name !== localState.selected.value) { + this._meshGroup?.children.forEach(obj => { + if (obj.name === localState.selected.value) { + this._selectedMesh = obj as THREE.Mesh< + THREE.BufferGeometry, + THREE.MeshBasicMaterial + >; + } + }); } - } else if (mouse && !this._cameraClients[id]) { - const el = document.createElement('div'); - el.className = 'jpcad-camera-client'; - el.style.left = mouse.offsetX + 'px'; - el.style.top = mouse.offsetY + 'px'; - el.style.backgroundColor = client.user.color; - el.innerText = client.user.name; - this._cameraClients[id] = el; - this._cameraRef.current?.appendChild(el); - } else if (!mouse && this._cameraClients[id]) { - this._cameraRef.current?.removeChild(this._cameraClients[id]!); - this._cameraClients[id] = undefined; + } else { + this._selectedMesh = null; } - - // if (client.mouse) { - // const cameraPos = client.mouse as Position; - // if (el) { - // el.style.left = cameraPos.offsetX + 'px'; - // el.style.top = cameraPos.offsetY + 'px'; - // } else { - // const newEl = document.createElement('div'); - // newEl.className = 'jpcad-camera-client'; - // newEl.style.left = cameraPos.offsetX + 'px'; - // newEl.style.top = cameraPos.offsetY + 'px'; - // newEl.style.backgroundColor = client.user.color; - // newEl.innerText = client.user.name; - // this._cameraClients[id] = el; - // this._cameraRef.current?.appendChild(newEl); - // } - // } else { - // if (el) { - // this._cameraRef.current?.removeChild(el); - // this._cameraClients[id] = undefined; - // } - // } } - }); + } + }; + + private _handleThemeChange = (): void => { + const lightTheme = + document.body.getAttribute('data-jp-theme-light') === 'true'; + this.setState(old => ({ ...old, lightTheme })); + }; + + private _handleWindowResize = (): void => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => { + this.forceUpdate(); + }, 500); }; render(): JSX.Element { diff --git a/src/model.ts b/src/model.ts index 3d6d6abb..267375bf 100644 --- a/src/model.ts +++ b/src/model.ts @@ -10,6 +10,7 @@ import { IJCadContent, IJCadModel } from './_interface/jcad'; import jcadSchema from './schema/jcad.json'; import { IJCadObjectDoc, + IJupyterCadClientState, IJupyterCadDoc, IJupyterCadDocChange, IJupyterCadModel, @@ -21,7 +22,7 @@ export class JupyterCadModel implements IJupyterCadModel { constructor(languagePreference?: string, modelDB?: IModelDB) { this.modelDB = modelDB || new ModelDB(); this.sharedModel.changed.connect(this._onSharedModelChanged); - this.sharedModel.awareness.on('change', this._onCameraChanged); + this.sharedModel.awareness.on('change', this._onClientStateChanged); } get isDisposed(): boolean { @@ -141,21 +142,35 @@ export class JupyterCadModel implements IJupyterCadModel { return all; } - syncCamera(pos: Position | undefined): void { - this.sharedModel.awareness.setLocalStateField('mouse', pos); + syncCamera(pos: Position | undefined, emitter?: string): void { + this.sharedModel.awareness.setLocalStateField('mouse', { + value: pos, + emitter: emitter + }); + } + + syncSelectedObject(name: string | null, emitter?: string): void { + this.sharedModel.awareness.setLocalStateField('selected', { + value: name, + emitter: emitter + }); } getClientId(): number { return this.sharedModel.awareness.clientID; } - get cameraChanged(): ISignal> { - return this._cameraChanged; + get clientStateChanged(): ISignal> { + return this._clientStateChanged; } - private _onCameraChanged = () => { - const clients = this.sharedModel.awareness.getStates(); - this._cameraChanged.emit(clients); + private _onClientStateChanged = changed => { + const clients = this.sharedModel.awareness.getStates() as Map< + number, + IJupyterCadClientState + >; + + this._clientStateChanged.emit(clients); }; private _onSharedModelChanged = ( @@ -176,7 +191,10 @@ export class JupyterCadModel implements IJupyterCadModel { private _contentChanged = new Signal(this); private _stateChanged = new Signal>(this); private _themeChanged = new Signal>(this); - private _cameraChanged = new Signal>(this); + private _clientStateChanged = new Signal< + this, + Map + >(this); private _sharedModelChanged = new Signal(this); static worker: Worker; } diff --git a/src/panelview/model.ts b/src/panelview/model.ts index 9a6e616b..97cbb98f 100644 --- a/src/panelview/model.ts +++ b/src/panelview/model.ts @@ -1,60 +1,45 @@ -import { ObservableMap } from '@jupyterlab/observables'; import { ISignal } from '@lumino/signaling'; import { IJupyterCadTracker } from '../token'; import { IControlPanelModel, - IControlPanelState, IJupyterCadDoc, IJupyterCadModel, - IJupyterCadWidget, - ISateChangedSignal, - IStateValue + IJupyterCadWidget } from '../types'; export class ControlPanelModel implements IControlPanelModel { constructor(options: ControlPanelModel.IOptions) { - const state = { activatedObject: '', visibleObjects: [] }; - this._state = new ObservableMap({ values: state }); - this._stateChanged = this._state.changed; this._tracker = options.tracker; this._documentChanged = this._tracker.currentChanged; } - get state(): ObservableMap { - return this._state; - } - get stateChanged(): ISateChangedSignal { - return this._stateChanged; - } + get documentChanged(): ISignal { return this._documentChanged; } + get filePath(): string | undefined { return this._tracker.currentWidget?.context.localPath; } + get jcadModel(): IJupyterCadModel | undefined { return this._tracker.currentWidget?.context.model; } + get sharedModel(): IJupyterCadDoc | undefined { return this._tracker.currentWidget?.context.model.sharedModel; } - set(key: keyof IControlPanelState, value: IStateValue): void { - this._state.set(key, value); - } - get(key: keyof IControlPanelState): IStateValue | undefined { - return this._state.get(key); - } - has(key: keyof IControlPanelState): boolean { - return this._state.has(key); - } + disconnect(f: any): void { this._tracker.forEach(w => w.context.model.sharedModelChanged.disconnect(f) ); + this._tracker.forEach(w => w.context.model.themeChanged.disconnect(f)); + this._tracker.forEach(w => + w.context.model.clientStateChanged.disconnect(f) + ); } - private readonly _stateChanged: ISateChangedSignal; - private readonly _state: ObservableMap; private readonly _tracker: IJupyterCadTracker; private _documentChanged: ISignal< IJupyterCadTracker, diff --git a/src/panelview/objectproperties.tsx b/src/panelview/objectproperties.tsx index 8d4f595c..46c06ab1 100644 --- a/src/panelview/objectproperties.tsx +++ b/src/panelview/objectproperties.tsx @@ -4,10 +4,18 @@ import { Panel } from '@lumino/widgets'; import * as React from 'react'; import { itemFromName } from '../tools'; -import { IControlPanelModel, IDict, IJupyterCadDocChange } from '../types'; +import { + IControlPanelModel, + IDict, + IJupyterCadClientState, + IJupyterCadDocChange, + IJupyterCadModel +} from '../types'; import { IJCadModel } from '../_interface/jcad'; import { ObjectPropertiesForm } from './formbuilder'; import formSchema from '../_interface/forms.json'; +import { v4 as uuid } from 'uuid'; + export class ObjectProperties extends PanelWithToolbar { constructor(params: ObjectProperties.IOptions) { super(params); @@ -27,6 +35,9 @@ interface IStates { selectedObjectData?: IDict; selectedObject?: string; schema?: IDict; + clientId: number | null; // ID of the yjs client + id: string; // ID of the component, it is used to identify which component + //is the source of awareness updates. } interface IProps { @@ -38,20 +49,29 @@ class ObjectPropertiesReact extends React.Component { super(props); this.state = { filePath: this.props.cpModel.filePath, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() + jcadObject: this.props.cpModel.jcadModel?.getAllObject(), + clientId: null, + id: uuid() }; this.props.cpModel.jcadModel?.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged ); this.props.cpModel.documentChanged.connect((_, changed) => { if (changed) { + this.props.cpModel.disconnect(this._sharedJcadModelChanged); + this.props.cpModel.disconnect(this._onClientSharedStateChanged); + changed.context.model.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged + ); + changed.context.model.clientStateChanged.connect( + this._onClientSharedStateChanged ); this.setState(old => ({ ...old, filePath: changed.context.localPath, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() + jcadObject: this.props.cpModel.jcadModel?.getAllObject(), + clientId: changed.context.model.getClientId() })); } else { this.setState({ @@ -64,42 +84,9 @@ class ObjectPropertiesReact extends React.Component { }); } }); - this.props.cpModel.stateChanged.connect((changed, value) => { - const selected = '' + value.newValue; - if (selected.length === 0) { - this.setState(old => ({ - ...old, - schema: undefined, - selectedObjectData: undefined - })); - return; - } - if (selected.includes('#')) { - const name = selected.split('#')[0]; - const objectData = this.props.cpModel.jcadModel?.getAllObject(); - if (objectData) { - let schema; - const selectedObj = itemFromName(name, objectData); - if (!selectedObj) { - return; - } - - if (selectedObj.shape) { - schema = formSchema[selectedObj.shape]; - } - const selectedObjectData = selectedObj['parameters']; - this.setState(old => ({ - ...old, - selectedObjectData, - selectedObject: name, - schema - })); - } - } - }); } - sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { + _sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { this.setState(old => { if (old.selectedObject) { const jcadObject = this.props.cpModel.jcadModel?.getAllObject(); @@ -145,6 +132,60 @@ class ObjectPropertiesReact extends React.Component { } } + private _onClientSharedStateChanged = ( + sender: IJupyterCadModel, + clients: Map + ): void => { + const targetId: number | null = null; + const clientId = this.state.clientId; + if (targetId) { + //TODO Sync with remote user in the follow-mode + } else { + // Update from other components of current client + const localState = clientId ? clients.get(clientId) : null; + + if (localState) { + if ( + localState.selected?.emitter && + localState.selected.emitter !== this.state.id && + localState.selected?.value + ) { + const selected = '' + localState.selected.value; + if (selected !== this.state.selectedObject) { + if (selected.length === 0) { + this.setState(old => ({ + ...old, + schema: undefined, + selectedObjectData: undefined + })); + return; + } + + const objectData = this.props.cpModel.jcadModel?.getAllObject(); + if (objectData) { + let schema; + const selectedObj = itemFromName(selected, objectData); + if (!selectedObj) { + return; + } + + if (selectedObj.shape) { + schema = formSchema[selectedObj.shape]; + } + const selectedObjectData = selectedObj['parameters']; + this.setState(old => ({ + ...old, + selectedObjectData, + selectedObject: selected, + schema + })); + } + } + } + } + } + }; + render(): React.ReactNode { return this.state.schema && this.state.selectedObjectData ? ( { this.state = { filePath: this.props.cpModel.filePath, jcadObject: this.props.cpModel.jcadModel?.getAllObject(), - lightTheme + lightTheme, + selectedNode: null, + clientId: null, + id: uuid(), + openNodes: [] }; this.props.cpModel.jcadModel?.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged ); this.props.cpModel.documentChanged.connect((_, changed) => { if (changed) { - this.props.cpModel.disconnect(this.sharedJcadModelChanged); + this.props.cpModel.disconnect(this._sharedJcadModelChanged); + this.props.cpModel.disconnect(this._handleThemeChange); + this.props.cpModel.disconnect(this._onClientSharedStateChanged); + changed.context.model.sharedModelChanged.connect( - this.sharedJcadModelChanged + this._sharedJcadModelChanged + ); + changed.context.model.themeChanged.connect(this._handleThemeChange); + changed.context.model.clientStateChanged.connect( + this._onClientSharedStateChanged ); - changed.context.model.themeChanged.connect((_, arg) => { - this.handleThemeChange(); - }); this.setState(old => ({ ...old, filePath: changed.context.localPath, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() + jcadObject: this.props.cpModel.jcadModel?.getAllObject(), + clientId: changed.context.model.getClientId() })); } else { this.setState({ @@ -115,19 +136,6 @@ class ObjectTreeReact extends React.Component { }); } - handleThemeChange = (): void => { - const lightTheme = - document.body.getAttribute('data-jp-theme-light') === 'true'; - this.setState(old => ({ ...old, lightTheme })); - }; - - sharedJcadModelChanged = (_, changed: IJupyterCadDocChange): void => { - this.setState(old => ({ - ...old, - jcadObject: this.props.cpModel.jcadModel?.getAllObject() - })); - }; - stateToTree = () => { if (this.state.jcadObject) { return this.state.jcadObject.map(obj => { @@ -168,21 +176,87 @@ class ObjectTreeReact extends React.Component { } } + private _handleThemeChange = (): void => { + const lightTheme = + document.body.getAttribute('data-jp-theme-light') === 'true'; + this.setState(old => ({ ...old, lightTheme })); + }; + + private _sharedJcadModelChanged = ( + _, + changed: IJupyterCadDocChange + ): void => { + this.setState(old => ({ + ...old, + jcadObject: this.props.cpModel.jcadModel?.getAllObject() + })); + }; + + private _onClientSharedStateChanged = ( + sender: IJupyterCadModel, + clients: Map + ): void => { + const targetId: number | null = null; + const clientId = this.state.clientId; + if (targetId) { + //TODO Sync with remote user in the follow-mode + } else { + // Update from other components of current client + const localState = clientId ? clients.get(clientId) : null; + if (localState) { + if ( + localState.selected?.emitter && + localState.selected.emitter !== this.state.id + ) { + const selectedNode = localState.selected.value!; + this.setState(old => ({ + ...old, + selectedNode, + openNodes: [...old.openNodes, selectedNode] + })); + } + } + } + }; + render(): React.ReactNode { const data = this.stateToTree(); - + let selectedNode: (number | string)[] = []; + if (this.state.selectedNode) { + const parentNode = data.filter( + node => node.id === this.state.selectedNode + ); + if (parentNode.length > 0 && parentNode[0].items.length > 0) { + selectedNode = [parentNode[0].items[0].id]; + } + } return (
{ if (id && id.length > 0) { - this.props.cpModel.set('activatedObject', id[0]); + let name = id[0] as string; + if (name.includes('#')) { + name = name.split('#')[0]; + + this.props.cpModel.jcadModel?.syncSelectedObject( + name, + this.state.id + ); + } + } else { + this.props.cpModel.jcadModel?.syncSelectedObject(null); } }} + onToggleOpenNodes={nodes => + this.setState(old => ({ ...old, openNodes: nodes })) + } RenderNode={options => { // const paddingLeft = 25 * (options.level + 1); const jcadObj = this.getObjectFromName( @@ -233,10 +307,6 @@ class ObjectTreeReact extends React.Component { }} icon={visible ? visibilityIcon : visibilityOffIcon} /> - {/* - {visible ? 'Hide' : 'Show'} - */} - {/* */} { @@ -244,7 +314,9 @@ class ObjectTreeReact extends React.Component { this.props.cpModel.jcadModel?.sharedModel.removeObjectByName( objectId ); - this.props.cpModel.set('activatedObject', ''); + this.props.cpModel.jcadModel?.syncSelectedObject( + null + ); }} icon={closeIcon} /> diff --git a/src/toolbar/operatortoolbar.tsx b/src/toolbar/operatortoolbar.tsx index 9a3ebae3..830baaa5 100644 --- a/src/toolbar/operatortoolbar.tsx +++ b/src/toolbar/operatortoolbar.tsx @@ -53,7 +53,6 @@ export class OperatorToolbarReact extends React.Component { }; const model = this.props.toolbarModel.sharedModel; if (model) { - console.log(parameters); const base = model.getObjectByName(parameters['Base']); const tool = model.getObjectByName(parameters['Tool']); const object = new Y.Map(Object.entries(objectModel)); diff --git a/src/types.ts b/src/types.ts index 5d3d1a32..a2f4cd87 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,6 @@ import { IJCadObject } from './_interface/jcad.d'; import { IChangedArgs } from '@jupyterlab/coreutils'; import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry'; -import { IObservableMap, ObservableMap } from '@jupyterlab/observables'; import { MapChange, YDocument } from '@jupyterlab/shared-models'; import { ReactWidget } from '@jupyterlab/ui-components'; import { ISignal, Signal } from '@lumino/signaling'; @@ -103,13 +102,6 @@ export interface IJcadObjectDocChange { objectChange?: MapChange; } -// export interface IJcadObjectDoc extends Y.Map { -// getObject(): IJcadObject; -// getProperty(key: keyof IJcadObject): ValueOf | undefined; -// setProperty(key: keyof IJcadObject, value: ValueOf): void; -// changed: ISignal; -// } - export interface IJupyterCadDocChange { contextChange?: MapChange; contentChange?: MapChange; @@ -131,6 +123,11 @@ export interface IJupyterCadDoc extends YDocument { setOption(key: string, value: any): void; } +export interface IJupyterCadClientState { + mouse: { value?: Position | null; emitter?: string | null }; + selected: { value?: string | null; emitter?: string | null }; + user: any; +} export interface IJupyterCadModel extends DocumentRegistry.IModel { isDisposed: boolean; sharedModelChanged: ISignal; @@ -138,31 +135,21 @@ export interface IJupyterCadModel extends DocumentRegistry.IModel { IJupyterCadModel, IChangedArgs >; - cameraChanged: ISignal>; + clientStateChanged: ISignal< + IJupyterCadModel, + Map + >; sharedModel: IJupyterCadDoc; getWorker(): Worker; getContent(): IJCadContent; getAllObject(): IJCadModel; - syncCamera(pos: Position | undefined): void; + syncCamera(pos: Position | undefined, emitter?: string): void; + syncSelectedObject(name: string | null, emitter?: string): void; getClientId(): number; } -export interface IControlPanelState { - activatedObject: string; -} - -export type IStateValue = string | number | any[]; -export type ISateChangedSignal = ISignal< - ObservableMap, - IObservableMap.IChangedArgs ->; export type IJupyterCadWidget = IDocumentWidget; export interface IControlPanelModel { - state: ObservableMap; - stateChanged: ISateChangedSignal; - set(key: keyof IControlPanelState, value: IStateValue): void; - get(key: keyof IControlPanelState): IStateValue | undefined; - has(key: keyof IControlPanelState): boolean; disconnect(f: any): void; documentChanged: ISignal; filePath: string | undefined;