diff --git a/src/layer/index.ts b/src/layer/index.ts index ff3f7d8d2..cbf2b493e 100644 --- a/src/layer/index.ts +++ b/src/layer/index.ts @@ -99,7 +99,7 @@ import { } from "#src/util/json.js"; import { MessageList } from "#src/util/message_list.js"; import type { AnyConstructor } from "#src/util/mixin.js"; -import { NullarySignal } from "#src/util/signal.js"; +import { NullarySignal, Signal } from "#src/util/signal.js"; import type { SignalBindingUpdater } from "#src/util/signal_binding_updater.js"; import { addSignalBinding, @@ -192,6 +192,27 @@ export class UserLayer extends RefCounted { messages = new MessageList(); + layerEventListeners = new Map(); + + dispatchLayerEvent(type: string) { + this.layerEventListeners.get(type)?.dispatch(); + } + + registerLayerEvent(type: string, handler: () => void) { + const { layerEventListeners } = this; + let existingSignal = layerEventListeners.get(type); + if (!existingSignal) { + existingSignal = new Signal(); + layerEventListeners.set(type, existingSignal); + } + const unregister = existingSignal.add(handler); + return () => { + const res = unregister(); + // TODO delete from map? currently handlers is private + return res; + }; + } + initializeSelectionState(state: this["selectionState"]) { state.generation = -1; state.localPositionValid = false; diff --git a/src/ui/annotations.ts b/src/ui/annotations.ts index 4a8e7b21b..783382000 100644 --- a/src/ui/annotations.ts +++ b/src/ui/annotations.ts @@ -231,6 +231,26 @@ interface AnnotationLayerViewAttachedState { listOffset: number; } +const moveToAnnotation = ( + layer: UserLayer, + annotation: Annotation, + state: AnnotationLayerState, +) => { + const chunkTransform = state.chunkTransform.value as ChunkTransformParameters; + const { layerRank } = chunkTransform; + const chunkPosition = new Float32Array(layerRank); + const layerPosition = new Float32Array(layerRank); + getCenterPosition(chunkPosition, annotation); + matrix.transformPoint( + layerPosition, + chunkTransform.chunkToLayerTransform, + layerRank + 1, + chunkPosition, + layerRank, + ); + setLayerPosition(layer, chunkTransform, layerPosition); +}; + export class AnnotationLayerView extends Tab { private previousSelectedState: | { @@ -456,7 +476,31 @@ export class AnnotationLayerView extends Tab { this.virtualList.element.addEventListener("mouseleave", () => { this.displayState.hoverState.value = undefined; }); - + const changeSelectedIndex = (offset: number) => { + const selectedIndex = this.getSelectedAnnotationIndex(); + if (selectedIndex === undefined) return; + const nextAnnotation = this.listElements[selectedIndex + offset]; + if (nextAnnotation) { + const { state, annotation } = nextAnnotation; + this.layer.selectAnnotation(state, annotation.id, true); + moveToAnnotation(this.layer, annotation, state); + } + }; + this.registerDisposer( + this.layer.registerLayerEvent("select-previous", () => { + // if (this.layer.panels.panels[0].selectedTab.value === "annotations") { + if (this.element.checkVisibility()) { + changeSelectedIndex(-1); + } + }), + ); + this.registerDisposer( + this.layer.registerLayerEvent("select-next", () => { + if (this.element.checkVisibility()) { + changeSelectedIndex(1); + } + }), + ); const bindings = getDefaultAnnotationListBindings(); this.registerDisposer( new MouseEventBinder(this.virtualList.element, bindings), @@ -487,6 +531,17 @@ export class AnnotationLayerView extends Tab { this.updateSelectionView(); } + private getSelectedAnnotationIndex() { + const { previousSelectedState: state } = this; + if (state === undefined) return; + const { annotationLayerState, annotationId } = state; + const attached = this.attachedAnnotationStates.get(annotationLayerState); + if (attached === undefined) return; + const index = attached.idToIndex.get(annotationId); + if (index === undefined) return; + return attached.listOffset + index; + } + private getRenderedAnnotationListElement( state: AnnotationLayerState, id: AnnotationId, @@ -2196,18 +2251,7 @@ export function makeAnnotationListElement( element.addEventListener("action:move-to-annotation", (event) => { event.stopPropagation(); event.preventDefault(); - const { layerRank } = chunkTransform; - const chunkPosition = new Float32Array(layerRank); - const layerPosition = new Float32Array(layerRank); - getCenterPosition(chunkPosition, annotation); - matrix.transformPoint( - layerPosition, - chunkTransform.chunkToLayerTransform, - layerRank + 1, - chunkPosition, - layerRank, - ); - setLayerPosition(layer, chunkTransform, layerPosition); + moveToAnnotation(layer, annotation, state); }); return [element, columnWidths]; } diff --git a/src/ui/default_input_event_bindings.ts b/src/ui/default_input_event_bindings.ts index 3e1816582..d7912f5bc 100644 --- a/src/ui/default_input_event_bindings.ts +++ b/src/ui/default_input_event_bindings.ts @@ -48,6 +48,9 @@ export function getDefaultGlobalBindings() { map.set("space", "toggle-layout"); map.set("shift+space", "toggle-layout-alternative"); map.set("backslash", "toggle-show-statistics"); + + map.set("alt+arrowup", "select-previous"); + map.set("alt+arrowdown", "select-next"); defaultGlobalBindings = map; } return defaultGlobalBindings; diff --git a/src/viewer.ts b/src/viewer.ts index 8e4fcd86a..5d288d655 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -1028,6 +1028,30 @@ export class Viewer extends RefCounted implements ViewerState { }); } + const sendEventToSelectedLayerTab = (type: string) => { + const elements = document.querySelectorAll( + '[data-neuroglancer-layer-panel-pinned="false"] .neuroglancer-stack-view > .neuroglancer-tab-content:not([style*="display: none"]):not([style*="display: none"]) > *', + ); + for (const element of elements) { + const event = new Event(type); + console.log("sending", type, "to", element); + element.dispatchEvent(event); + } + + const selectedLayer = this.selectedLayer.layer?.layer; + if (selectedLayer) { + selectedLayer.dispatchLayerEvent(type); + } + }; + + this.bindAction("select-previous", () => { + sendEventToSelectedLayerTab("select-previous"); + }); + + this.bindAction("select-next", () => { + sendEventToSelectedLayerTab("select-next"); + }); + for (const action of ["select", "star"]) { this.bindAction(action, () => { this.mouseState.updateUnconditionally();