diff --git a/src/apps/viewer/extensions/jolecule.ts b/src/apps/viewer/extensions/jolecule.ts index f6c1800a2d5be043a764a01a532955eac7cef2a9..824376263ab6be9e89d271a4927c650021e8cae8 100644 --- a/src/apps/viewer/extensions/jolecule.ts +++ b/src/apps/viewer/extensions/jolecule.ts @@ -31,7 +31,7 @@ export const CreateJoleculeState = StateAction.build({ data.sort((a, b) => a.order - b.order); - await PluginCommands.State.RemoveObject.dispatch(plugin, { state, ref }, true); + await PluginCommands.State.RemoveObject.dispatch(plugin, { state, ref }); plugin.state.snapshots.clear(); const template = createTemplate(plugin, state.tree, id); @@ -40,7 +40,7 @@ export const CreateJoleculeState = StateAction.build({ plugin.state.snapshots.add(s); } - PluginCommands.State.Snapshots.Apply.dispatch(plugin, { id: snapshots[0].snapshot.id }, true); + PluginCommands.State.Snapshots.Apply.dispatch(plugin, { id: snapshots[0].snapshot.id }); } catch (e) { plugin.log.error(`Jolecule Failed: ${e}`); } diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index dd444dd5fe757e88a4ad7db9a7a24bc22cafd407..566d4410ef53caca3b6907f65382cba1334cf48c 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -76,7 +76,7 @@ export function RemoveObject(ctx: PluginContext) { curr = parent; } } else { - remove(state, ref); + return remove(state, ref); } }); } diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index 73accd0fc786cd73132a44538dadf92e8ac93b1f..5c28e8e992d919b6476e520b542b38ff3f230836 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -22,46 +22,46 @@ export const PluginCommands = { RemoveObject: PluginCommand<{ state: State, ref: StateTransform.Ref, removeParentGhosts?: boolean }>(), - ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), - ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), - Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), - ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), + ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), + ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), + Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), + ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), Snapshots: { - Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }), - Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }), - Move: PluginCommand<{ id: string, dir: -1 | 1 }>({ isImmediate: true }), - Remove: PluginCommand<{ id: string }>({ isImmediate: true }), - Apply: PluginCommand<{ id: string }>({ isImmediate: true }), - Clear: PluginCommand<{}>({ isImmediate: true }), + Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>(), + Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>(), + Move: PluginCommand<{ id: string, dir: -1 | 1 }>(), + Remove: PluginCommand<{ id: string }>(), + Apply: PluginCommand<{ id: string }>(), + Clear: PluginCommand<{}>(), - Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>({ isImmediate: true }), + Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>(), Fetch: PluginCommand<{ url: string }>(), - DownloadToFile: PluginCommand<{ name?: string }>({ isImmediate: true }), - OpenFile: PluginCommand<{ file: File }>({ isImmediate: true }), + DownloadToFile: PluginCommand<{ name?: string }>(), + OpenFile: PluginCommand<{ file: File }>(), } }, Interactivity: { Structure: { - Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true }), - Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true }) + Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>(), + Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>() } }, Layout: { - Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>({ isImmediate: true }) + Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>() }, Camera: { - Reset: PluginCommand<{}>({ isImmediate: true }), - SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>({ isImmediate: true }), + Reset: PluginCommand<{}>(), + SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>(), Snapshots: { - Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }), - Remove: PluginCommand<{ id: string }>({ isImmediate: true }), - Apply: PluginCommand<{ id: string }>({ isImmediate: true }), - Clear: PluginCommand<{}>({ isImmediate: true }), + Add: PluginCommand<{ name?: string, description?: string }>(), + Remove: PluginCommand<{ id: string }>(), + Apply: PluginCommand<{ id: string }>(), + Clear: PluginCommand<{}>(), } }, Canvas3D: { - SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>({ isImmediate: true }) + SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>() } } \ No newline at end of file diff --git a/src/mol-plugin/command/base.ts b/src/mol-plugin/command/base.ts index b2ea69de7667ceb2ee3b7fc5a8e6c4560f856f27..70aca58427031bc88899b0e454839e4be830f30b 100644 --- a/src/mol-plugin/command/base.ts +++ b/src/mol-plugin/command/base.ts @@ -5,33 +5,30 @@ */ import { PluginContext } from '../context'; -import { LinkedList } from 'mol-data/generic'; -import { RxEventHelper } from 'mol-util/rx-event-helper'; import { UUID } from 'mol-util'; export { PluginCommand } interface PluginCommand<T = unknown> { readonly id: UUID, - dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void>, - subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription, - params: { isImmediate: boolean } + dispatch(ctx: PluginContext, params: T): Promise<void>, + subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription } /** namespace.id must a globally unique identifier */ -function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginCommand<T> { - return new Impl({ isImmediate: false, ...params }); +function PluginCommand<T>(): PluginCommand<T> { + return new Impl(); } class Impl<T> implements PluginCommand<T> { - dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void> { - return ctx.commands.dispatch(this, params, isChild); + dispatch(ctx: PluginContext, params: T): Promise<void> { + return ctx.commands.dispatch(this, params); } subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription { return ctx.commands.subscribe(this, action); } id = UUID.create22(); - constructor(public params: PluginCommand<T>['params']) { + constructor() { } } @@ -43,23 +40,12 @@ namespace PluginCommand { } export type Action<T> = (params: T) => unknown | Promise<unknown> - type Instance = { cmd: PluginCommand<any>, params: any, isChild: boolean, resolve: () => void, reject: (e: any) => void } + type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void } export class Manager { private subs = new Map<string, Action<any>[]>(); - private queue = LinkedList<Instance>(); private disposing = false; - private ev = RxEventHelper.create(); - - readonly behaviour = { - locked: this.ev.behavior<boolean>(false) - }; - - lock(locked: boolean = true) { - this.behaviour.locked.next(locked); - } - subscribe<T>(cmd: PluginCommand<T>, action: Action<T>): Subscription { let actions = this.subs.get(cmd.id); if (!actions) { @@ -84,7 +70,7 @@ namespace PluginCommand { /** Resolves after all actions have completed */ - dispatch<T>(cmd: PluginCommand<T>, params: T, isChild = false) { + dispatch<T>(cmd: PluginCommand<T>, params: T) { return new Promise<void>((resolve, reject) => { if (this.disposing) { reject('disposed'); @@ -97,37 +83,22 @@ namespace PluginCommand { return; } - const instance: Instance = { cmd, params, resolve, reject, isChild }; - - if (cmd.params.isImmediate || isChild) { - this.resolve(instance); - } else { - this.queue.addLast(instance); - this.next(); - } + this.resolve({ cmd, params, resolve, reject }); }); } dispose() { this.subs.clear(); - while (this.queue.count > 0) { - this.queue.removeFirst(); - } } private async resolve(instance: Instance) { const actions = this.subs.get(instance.cmd.id); if (!actions) { - try { - instance.resolve(); - } finally { - if (!instance.cmd.params.isImmediate && !this.disposing) this.next(); - } + instance.resolve(); return; } try { - if (!instance.cmd.params.isImmediate && !instance.isChild) this.executing = true; // TODO: should actions be called "asynchronously" ("setImmediate") instead? for (const a of actions) { await a(instance.params); @@ -135,19 +106,7 @@ namespace PluginCommand { instance.resolve(); } catch (e) { instance.reject(e); - } finally { - if (!instance.cmd.params.isImmediate && !instance.isChild) { - this.executing = false; - if (!this.disposing) this.next(); - } } } - - private executing = false; - private async next() { - if (this.queue.count === 0 || this.executing) return; - const instance = this.queue.removeFirst()!; - this.resolve(instance); - } } } \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 58a870c7682f37ee7f610b123dfdd1d146364785..6b2e8e6e44db466294b3d80b08593d3ff86472b9 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -76,9 +76,7 @@ export class PluginContext { }, labels: { highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] }) - }, - - command: this.commands.behaviour + } }; readonly canvas3d: Canvas3D; diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 2e3336bd65cf3131392f3e4ed8a4b69ff7f562df..6cc2eea162c2ab17fa51773c63738892fa6318a3 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -12,7 +12,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParameterControls } from './controls/parameters'; import { Canvas3DParams } from 'mol-canvas3d/canvas3d'; import { PluginLayoutStateParams } from 'mol-plugin/layout'; -import { ControlGroup } from './controls/common'; +import { ControlGroup, IconButton } from './controls/common'; interface ViewportState { noWebGl: boolean @@ -59,22 +59,16 @@ export class ViewportControls extends PluginUIComponent { } icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) { - return <button - className={`msp-btn msp-btn-link msp-btn-link-toggle-${isOn ? 'on' : 'off'}`} - onClick={onClick} - title={title}> - <span className={`msp-icon msp-icon-${name}`}/> - </button> + return <IconButton icon={name} toggleState={isOn} onClick={onClick} title={title} />; } render() { - // TODO: show some icons dimmed etc.. return <div className={'msp-viewport-controls'}> <div className='msp-viewport-controls-buttons'> {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}<br/> {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}<br/> - {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)} - {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}<br/> + {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}<br /> + {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)} </div> {this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'> diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 4d2febfa85c2a2e1442173bab3b4eada0e1781c9..8e336eaed0c41cd12caadf774c725afe88323004 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ @@ -19,6 +19,7 @@ import { LogEntry } from 'mol-util/log-entry'; import { now, formatTimespan } from 'mol-util/now'; import { ParamDefinition } from 'mol-util/param-definition'; import { StateTreeSpine } from './tree/spine'; +import { AsyncQueue } from 'mol-util/async-queue'; export { State } @@ -122,7 +123,7 @@ class State { } /** - * Reconcialites the existing state tree with the new version. + * Queues up a reconciliation of the existing state tree. * * If the tree is StateBuilder.To<T>, the corresponding StateObject is returned by the task. * @param tree Tree instance or a tree builder instance @@ -131,27 +132,44 @@ class State { updateTree<T extends StateObject>(tree: StateBuilder.To<T>, options?: Partial<State.UpdateOptions>): Task<T> updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<void> updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<any> { + const params: UpdateParams = { tree, options }; return Task.create('Update Tree', async taskCtx => { - this.events.isUpdating.next(true); - let updated = false; - const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, options); + const ok = await this.updateQueue.enqueue(params); + if (!ok) return; + try { - updated = await update(ctx); - if (StateBuilder.isTo(tree)) { - const cell = this.select(tree.ref)[0]; - return cell && cell.obj; - } + const ret = await this._updateTree(taskCtx, params); + return ret; } finally { - this.spine.setSurrent(); + this.updateQueue.handled(params); + } + }, () => { + this.updateQueue.remove(params); + }); + } - if (updated) this.events.changed.next(); - this.events.isUpdating.next(false); + private updateQueue = new AsyncQueue<UpdateParams>(); - for (const ref of ctx.stateChanges) { - this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) }); - } + private async _updateTree(taskCtx: RuntimeContext, params: UpdateParams) { + this.events.isUpdating.next(true); + let updated = false; + const ctx = this.updateTreeAndCreateCtx(params.tree, taskCtx, params.options); + try { + updated = await update(ctx); + if (StateBuilder.isTo(params.tree)) { + const cell = this.select(params.tree.ref)[0]; + return cell && cell.obj; } - }); + } finally { + this.spine.setSurrent(); + + if (updated) this.events.changed.next(); + this.events.isUpdating.next(false); + + for (const ref of ctx.stateChanges) { + this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) }); + } + } } private updateTreeAndCreateCtx(tree: StateTree | StateBuilder, taskCtx: RuntimeContext, options: Partial<State.UpdateOptions> | undefined) { @@ -239,6 +257,8 @@ const StateUpdateDefaultOptions: State.UpdateOptions = { type Ref = StateTransform.Ref +type UpdateParams = { tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions> } + interface UpdateContext { parent: State, editInfo: StateBuilder.EditInfo | undefined diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts index 95b5ef5be19ea24383680e0e6ca84054cf6c70d7..ac3b10a8132ef7d885a482606b2488fb169e46c1 100644 --- a/src/mol-util/array.ts +++ b/src/mol-util/array.ts @@ -56,4 +56,22 @@ export function arrayRms(array: NumberArray) { export function fillSerial<T extends NumberArray> (array: T, n?: number) { for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i return array -} \ No newline at end of file +} + +export function arrayRemoveInPlace<T>(xs: T[], x: T) { + let i = 0, l = xs.length, found = false; + for (; i < l; i++) { + if (xs[i] === x) { + found = true; + break; + } + } + if (!found) return false; + i++; + for (; i < l; i++) { + xs[i] = xs[i - 1]; + } + xs.pop(); + return true; +} +(window as any).arrayRem = arrayRemoveInPlace \ No newline at end of file diff --git a/src/mol-util/async-queue.ts b/src/mol-util/async-queue.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2ef601c3f6ab3cd54d436a1e446b2373626538a --- /dev/null +++ b/src/mol-util/async-queue.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { arrayRemoveInPlace } from './array'; +import { Subject } from 'rxjs'; + +export class AsyncQueue<T> { + private queue: T[] = []; + private signal = new Subject<{ v: T, removed: boolean }>(); + + enqueue(v: T) { + this.queue.push(v); + if (this.queue.length === 1) return true; + return this.waitFor(v); + } + + handled(v: T) { + arrayRemoveInPlace(this.queue, v); + if (this.queue.length > 0) this.signal.next({ v: this.queue[0], removed: false }); + } + + remove(v: T) { + const rem = arrayRemoveInPlace(this.queue, v); + if (rem) + this.signal.next({ v, removed: true }) + return rem; + } + + private waitFor(t: T): Promise<boolean> { + return new Promise(res => { + const sub = this.signal.subscribe(({ v, removed }) => { + if (v === t) { + sub.unsubscribe(); + res(removed); + } + }); + }) + } +} \ No newline at end of file