Skip to content
Snippets Groups Projects
Commit ab8c6b5f authored by David Sehnal's avatar David Sehnal
Browse files

State now always queues update, plugin commands are always immedaite

parent 868f3d43
No related branches found
No related tags found
No related merge requests found
......@@ -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}`);
}
......
......@@ -76,7 +76,7 @@ export function RemoveObject(ctx: PluginContext) {
curr = parent;
}
} else {
remove(state, ref);
return remove(state, ref);
}
});
}
......
......@@ -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
......@@ -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();
}
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
......@@ -76,9 +76,7 @@ export class PluginContext {
},
labels: {
highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] })
},
command: this.commands.behaviour
}
};
readonly canvas3d: Canvas3D;
......
......@@ -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'>
......
/**
* 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,14 +132,32 @@ 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 => {
const ok = await this.updateQueue.enqueue(params);
if (!ok) return;
try {
const ret = await this._updateTree(taskCtx, params);
return ret;
} finally {
this.updateQueue.handled(params);
}
}, () => {
this.updateQueue.remove(params);
});
}
private updateQueue = new AsyncQueue<UpdateParams>();
private async _updateTree(taskCtx: RuntimeContext, params: UpdateParams) {
this.events.isUpdating.next(true);
let updated = false;
const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, options);
const ctx = this.updateTreeAndCreateCtx(params.tree, taskCtx, params.options);
try {
updated = await update(ctx);
if (StateBuilder.isTo(tree)) {
const cell = this.select(tree.ref)[0];
if (StateBuilder.isTo(params.tree)) {
const cell = this.select(params.tree.ref)[0];
return cell && cell.obj;
}
} finally {
......@@ -151,7 +170,6 @@ class State {
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
......
......@@ -57,3 +57,21 @@ 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
}
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
/**
* 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment