From bc4f166a9f852f529781d72ce2925afd2da6a7dd Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Thu, 22 Feb 2024 12:24:31 -0500 Subject: [PATCH] feat(tools): custom keybinds and layer independent tool keybinds --- src/ui/default_viewer_setup.ts | 104 ++++++++++++++++++++++++++++++++- src/ui/tool.ts | 6 +- src/viewer.ts | 5 +- 3 files changed, 107 insertions(+), 8 deletions(-) diff --git a/src/ui/default_viewer_setup.ts b/src/ui/default_viewer_setup.ts index fc5d5aeae..e4a1c9983 100644 --- a/src/ui/default_viewer_setup.ts +++ b/src/ui/default_viewer_setup.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import type { UserLayer, UserLayerConstructor } from "#src/layer/index.js"; +import { layerTypes } from "#src/layer/index.js"; import { StatusMessage } from "#src/status.js"; import { bindDefaultCopyHandler, @@ -21,19 +23,115 @@ import { } from "#src/ui/default_clipboard_handling.js"; import { setDefaultInputEventBindings } from "#src/ui/default_input_event_bindings.js"; import { makeDefaultViewer } from "#src/ui/default_viewer.js"; -import type { MinimalViewerOptions } from "#src/ui/minimal_viewer.js"; import { bindTitle } from "#src/ui/title.js"; +import type { Tool } from "#src/ui/tool.js"; +import { restoreTool } from "#src/ui/tool.js"; import { UrlHashBinding } from "#src/ui/url_hash_binding.js"; +import { + verifyObject, + verifyObjectProperty, + verifyString, +} from "#src/util/json.js"; declare let NEUROGLANCER_DEFAULT_STATE_FRAGMENT: string | undefined; +type CustomToolBinding = { + layer: string; + tool: unknown; + provider?: string; +}; + +type CustomBindings = { + [key: string]: CustomToolBinding | string | boolean; +}; + +declare const CUSTOM_BINDINGS: CustomBindings | undefined; +export const hasCustomBindings = + typeof CUSTOM_BINDINGS !== "undefined" && + Object.keys(CUSTOM_BINDINGS).length > 0; + /** * Sets up the default neuroglancer viewer. */ -export function setupDefaultViewer(options?: Partial) { - const viewer = ((window).viewer = makeDefaultViewer(options)); +export function setupDefaultViewer() { + const viewer = ((window).viewer = makeDefaultViewer()); setDefaultInputEventBindings(viewer.inputEventBindings); + const bindNonLayerSpecificTool = ( + obj: unknown, + toolKey: string, + desiredLayerType: UserLayerConstructor, + desiredProvider?: string, + ) => { + let previousTool: Tool | undefined; + let previousLayer: UserLayer | undefined; + if (typeof obj === "string") { + obj = { type: obj }; + } + verifyObject(obj); + const type = verifyObjectProperty(obj, "type", verifyString); + viewer.bindAction(`tool-${type}`, () => { + const acceptableLayers = viewer.layerManager.managedLayers.filter( + (managedLayer) => { + const correctLayerType = + managedLayer.layer instanceof desiredLayerType; + if (desiredProvider && correctLayerType) { + for (const dataSource of managedLayer.layer?.dataSources || []) { + const protocol = viewer.dataSourceProvider.getProvider( + dataSource.spec.url, + )[2]; + if (protocol === desiredProvider) { + return true; + } + } + return false; + } else { + return correctLayerType; + } + }, + ); + if (acceptableLayers.length > 0) { + const firstLayer = acceptableLayers[0].layer; + if (firstLayer) { + if (firstLayer !== previousLayer) { + previousTool = restoreTool(firstLayer, obj); + previousLayer = firstLayer; + } + if (previousTool) { + viewer.activateTool(toolKey, previousTool); + } + } + } + }); + }; + + if (hasCustomBindings) { + for (const [key, val] of Object.entries(CUSTOM_BINDINGS!)) { + if (typeof val === "string") { + viewer.inputEventBindings.global.set(key, val); + } else if (typeof val === "boolean") { + if (!val) { + viewer.inputEventBindings.global.delete(key); + viewer.inputEventBindings.global.parents.map((parent) => + parent.delete(key), + ); + } + } else { + viewer.inputEventBindings.global.set(key, `tool-${val.tool}`); + const layerConstructor = layerTypes.get(val.layer); + if (layerConstructor) { + const toolKey = key.charAt(key.length - 1).toUpperCase(); + bindNonLayerSpecificTool( + val.tool, + toolKey, + layerConstructor, + val.provider, + ); + } + } + } + } + const hashBinding = viewer.registerDisposer( new UrlHashBinding( viewer.state, diff --git a/src/ui/tool.ts b/src/ui/tool.ts index c422e430f..d49e00025 100644 --- a/src/ui/tool.ts +++ b/src/ui/tool.ts @@ -318,15 +318,15 @@ export class GlobalToolBinder extends RefCounted { this.changed.dispatch(); } - activate(key: string): Borrowed | undefined { - const tool = this.get(key); + activate(key: string, tool?: Tool): Borrowed | undefined { + tool = tool || this.get(key); if (tool === undefined) { this.deactivate_(); return; } this.debounceDeactivate.cancel(); const activeTool = this.activeTool_; - if (tool === activeTool?.tool) { + if (tool.toJSON() === activeTool?.tool.toJSON()) { if (tool.toggle) { this.deactivate_(); } diff --git a/src/viewer.ts b/src/viewer.ts index e1e81de29..76043d77f 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -93,6 +93,7 @@ import { SelectionDetailsPanel } from "#src/ui/selection_details.js"; import { SidePanelManager } from "#src/ui/side_panel.js"; import { StateEditorDialog } from "#src/ui/state_editor.js"; import { StatisticsDisplayState, StatisticsPanel } from "#src/ui/statistics.js"; +import type { Tool } from "#src/ui/tool.js"; import { GlobalToolBinder, LocalToolBinder } from "#src/ui/tool.js"; import { ViewerSettingsPanel, @@ -1124,8 +1125,8 @@ export class Viewer extends RefCounted implements ViewerState { new LocalToolBinder(this, this.globalToolBinder), ); - activateTool(uppercase: string) { - this.globalToolBinder.activate(uppercase); + activateTool(key: string, tool?: Tool) { + this.globalToolBinder.activate(key, tool); } editJsonState() {