Skip to content

Commit 8b74a5c

Browse files
committed
feat: add accordions to tabs to sort controls
1 parent ce31ec9 commit 8b74a5c

File tree

15 files changed

+686
-45
lines changed

15 files changed

+686
-45
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import argparse
2+
3+
import neuroglancer
4+
import neuroglancer.cli
5+
import numpy as np
6+
7+
8+
def add_example_layers(state):
9+
state.dimensions = neuroglancer.CoordinateSpace(
10+
names=["x", "y", "z"], units="nm", scales=[10, 10, 10]
11+
)
12+
state.layers.append(
13+
name="example_layer",
14+
layer=neuroglancer.LocalVolume(
15+
data=np.random.rand(10, 10, 10).astype(np.float32),
16+
dimensions=state.dimensions,
17+
),
18+
)
19+
return state.layers[0]
20+
21+
22+
if __name__ == "__main__":
23+
ap = argparse.ArgumentParser()
24+
neuroglancer.cli.add_server_arguments(ap)
25+
args = ap.parse_args()
26+
neuroglancer.cli.handle_server_arguments(args)
27+
viewer = neuroglancer.Viewer()
28+
with viewer.txn() as s:
29+
add_example_layers(s)
30+
s.layers[0].annotations_accordion.annotations_expanded = False
31+
s.layers[0].annotations_accordion.related_segments_expanded = True
32+
s.layers[0].rendering_accordion.slice_expanded = True
33+
s.layers[0].rendering_accordion.shader_expanded = False
34+
s.layers[0].source_accordion.source_expanded = False
35+
36+
print(viewer)

python/neuroglancer/viewer_state.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,75 @@ class DimensionPlaybackVelocity(JsonObjectWrapper):
406406
paused = wrapped_property("paused", optional(bool, True))
407407

408408

409+
@export
410+
class SourceAccordion(JsonObjectWrapper):
411+
"""Accordion state for layer data source controls."""
412+
413+
__slots__ = ()
414+
415+
source_expanded = sourceExpanded = wrapped_property(
416+
"sourceExpanded", optional(bool)
417+
)
418+
create_expanded = createExpanded = wrapped_property(
419+
"createExpanded", optional(bool)
420+
)
421+
422+
423+
@export
424+
class AnnotationsAccordion(JsonObjectWrapper):
425+
"""Accordion state for layer annotation controls."""
426+
427+
__slots__ = ()
428+
429+
spacing_expanded = spacingExpanded = wrapped_property(
430+
"spacingExpanded", optional(bool)
431+
)
432+
related_segments_expanded = relatedSegmentsExpanded = wrapped_property(
433+
"relatedSegmentsExpanded", optional(bool)
434+
)
435+
annotations_expanded = annotationsExpanded = wrapped_property(
436+
"annotationsExpanded", optional(bool)
437+
)
438+
439+
440+
@export
441+
class ImageRenderingAccordion(JsonObjectWrapper):
442+
"""Accordion state for image layer rendering controls."""
443+
444+
__slots__ = ()
445+
446+
slice_expanded = sliceExpanded = wrapped_property("sliceExpanded", optional(bool))
447+
volume_rendering_expanded = volumeRenderingExpanded = wrapped_property(
448+
"volumeRenderingExpanded", optional(bool)
449+
)
450+
shader_expanded = shaderExpanded = wrapped_property(
451+
"shaderExpanded", optional(bool)
452+
)
453+
454+
455+
@export
456+
class SegmentationRenderingAccordion(JsonObjectWrapper):
457+
"""Accordion state for segmentation layer rendering controls."""
458+
459+
__slots__ = ()
460+
461+
visibility_expanded = visibilityExpanded = wrapped_property(
462+
"visibilityExpanded", optional(bool)
463+
)
464+
appearance_expanded = appearanceExpanded = wrapped_property(
465+
"appearanceExpanded", optional(bool)
466+
)
467+
slice_rendering_expanded = sliceRenderingExpanded = wrapped_property(
468+
"sliceRenderingExpanded", optional(bool)
469+
)
470+
mesh_rendering_expanded = meshRenderingExpanded = wrapped_property(
471+
"meshRenderingExpanded", optional(bool)
472+
)
473+
skeletons_expanded = skeletonsExpanded = wrapped_property(
474+
"skeletonsExpanded", optional(bool)
475+
)
476+
477+
409478
@export
410479
class Layer(JsonObjectWrapper):
411480
__slots__ = ()
@@ -428,6 +497,13 @@ class Layer(JsonObjectWrapper):
428497
)
429498
tool = wrapped_property("tool", optional(Tool))
430499

500+
annotations_accordion = annotationsAccordion = wrapped_property(
501+
"annotationsAccordion", AnnotationsAccordion
502+
)
503+
source_accordion = sourceAccordion = wrapped_property(
504+
"sourceAccordion", SourceAccordion
505+
)
506+
431507
@staticmethod
432508
def interpolate(a, b, t):
433509
c = copy.deepcopy(a)
@@ -609,6 +685,9 @@ def __init__(self, *args, **kwargs):
609685
cross_section_render_scale = crossSectionRenderScale = wrapped_property(
610686
"crossSectionRenderScale", optional(float, 1)
611687
)
688+
rendering_accordion = renderingAccordion = wrapped_property(
689+
"renderingAccordion", ImageRenderingAccordion
690+
)
612691

613692
@staticmethod
614693
def interpolate(a, b, t):
@@ -946,6 +1025,9 @@ def visible_segments(self, segments):
9461025
skeleton_rendering = skeletonRendering = wrapped_property(
9471026
"skeletonRendering", SkeletonRenderingOptions
9481027
)
1028+
rendering_accordion = renderingAccordion = wrapped_property(
1029+
"renderingAccordion", SegmentationRenderingAccordion
1030+
)
9491031

9501032
@property
9511033
def skeleton_shader(self):

src/datasource/graphene/frontend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1954,7 +1954,7 @@ class MulticutAnnotationLayerView extends AnnotationLayerView {
19541954
public layer: SegmentationUserLayer,
19551955
public displayState: AnnotationDisplayState,
19561956
) {
1957-
super(layer, displayState);
1957+
super(layer, displayState, layer.annotationAccordionState);
19581958
const {
19591959
graphConnection: { value: graphConnection },
19601960
} = layer;

src/layer/annotation/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ import type {
5656
AnnotationLayerView,
5757
MergedAnnotationStates,
5858
} from "#src/ui/annotations.js";
59-
import { UserLayerWithAnnotationsMixin } from "#src/ui/annotations.js";
59+
import {
60+
RELATED_SEGMENT_SECTION_JSON_KEY,
61+
SPACING_SECTION_JSON_KEY,
62+
UserLayerWithAnnotationsMixin,
63+
} from "#src/ui/annotations.js";
6064
import { animationFrameDebounce } from "#src/util/animation_frame_debounce.js";
6165
import type { Borrowed, Owned } from "#src/util/disposable.js";
6266
import { RefCounted } from "#src/util/disposable.js";
@@ -675,12 +679,14 @@ export class AnnotationUserLayer extends Base {
675679
renderScaleWidget.label.textContent = "Spacing (projection)";
676680
parent.appendChild(renderScaleWidget.element);
677681
}
682+
tab.showSection(SPACING_SECTION_JSON_KEY);
678683
},
679684
),
680685
);
681-
tab.element.insertBefore(
686+
tab.appendChild(
682687
renderScaleControls.element,
683-
tab.element.firstChild,
688+
SPACING_SECTION_JSON_KEY,
689+
true /* hidden */,
684690
);
685691
{
686692
const checkbox = tab.registerDisposer(
@@ -695,12 +701,13 @@ export class AnnotationUserLayer extends Base {
695701
label.title =
696702
"Display all annotations if filtering by related segments is enabled but no segments are selected";
697703
label.appendChild(checkbox.element);
698-
tab.element.appendChild(label);
704+
tab.appendChild(label, RELATED_SEGMENT_SECTION_JSON_KEY);
699705
}
700-
tab.element.appendChild(
706+
tab.appendChild(
701707
tab.registerDisposer(
702708
new LinkedSegmentationLayersWidget(this.linkedSegmentationLayers),
703709
).element,
710+
RELATED_SEGMENT_SECTION_JSON_KEY,
704711
);
705712
}
706713

src/layer/image/index.ts

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
setControlsInShader,
8080
ShaderControlState,
8181
} from "#src/webgl/shader_ui_controls.js";
82+
import { AccordionState, AccordionTab } from "#src/widget/accordion.js";
8283
import { ChannelDimensionsWidget } from "#src/widget/channel_dimensions_widget.js";
8384
import { makeCopyButton } from "#src/widget/copy_button.js";
8485
import type { DependentViewContext } from "#src/widget/dependent_view_widget.js";
@@ -102,7 +103,6 @@ import {
102103
registerLayerShaderControlsTool,
103104
ShaderControls,
104105
} from "#src/widget/shader_controls.js";
105-
import { Tab } from "#src/widget/tab_view.js";
106106

107107
const OPACITY_JSON_KEY = "opacity";
108108
const BLEND_JSON_KEY = "blend";
@@ -114,6 +114,10 @@ const CHANNEL_DIMENSIONS_JSON_KEY = "channelDimensions";
114114
const VOLUME_RENDERING_JSON_KEY = "volumeRendering";
115115
const VOLUME_RENDERING_GAIN_JSON_KEY = "volumeRenderingGain";
116116
const VOLUME_RENDERING_DEPTH_SAMPLES_JSON_KEY = "volumeRenderingDepthSamples";
117+
const RENDERING_ACCORDION_JSON_KEY = "renderingAccordion";
118+
const SLICE_SECTION_JSON_KEY = "sliceExpanded";
119+
const VOLUME_RENDERING_SECTION_JSON_KEY = "volumeRenderingExpanded";
120+
const SHADER_SECTION_JSON_KEY = "shaderExpanded";
117121

118122
export interface ImageLayerSelectionState extends UserLayerSelectionState {
119123
value: any;
@@ -157,6 +161,28 @@ export class ImageUserLayer extends Base {
157161
);
158162
volumeRenderingMode = trackableShaderModeValue();
159163

164+
renderingAccordionState = this.registerDisposer(
165+
new AccordionState({
166+
accordionJsonKey: RENDERING_ACCORDION_JSON_KEY,
167+
sections: [
168+
{
169+
jsonKey: SLICE_SECTION_JSON_KEY,
170+
displayName: "Slice 2D",
171+
},
172+
{
173+
jsonKey: VOLUME_RENDERING_SECTION_JSON_KEY,
174+
displayName: "Volume rendering",
175+
},
176+
{
177+
jsonKey: SHADER_SECTION_JSON_KEY,
178+
displayName: "Shader controls",
179+
defaultExpanded: true,
180+
isDefaultKey: true,
181+
},
182+
],
183+
}),
184+
);
185+
160186
shaderControlState = this.registerDisposer(
161187
new ShaderControlState(
162188
this.fragmentMain,
@@ -219,10 +245,13 @@ export class ImageUserLayer extends Base {
219245
this.volumeRenderingDepthSamplesTarget.changed.add(
220246
this.specificationChanged.dispatch,
221247
);
248+
this.renderingAccordionState.specificationChanged.add(
249+
this.specificationChanged.dispatch,
250+
);
222251
this.tabs.add("rendering", {
223252
label: "Rendering",
224253
order: -100,
225-
getter: () => new RenderingOptionsTab(this),
254+
getter: () => new RenderingOptionsTab(this, this.renderingAccordionState),
226255
});
227256
this.tabs.default = "rendering";
228257
}
@@ -339,6 +368,13 @@ export class ImageUserLayer extends Base {
339368
volumeRenderingDepthSamplesTarget,
340369
),
341370
);
371+
verifyOptionalObjectProperty(
372+
specification,
373+
RENDERING_ACCORDION_JSON_KEY,
374+
(accordionState) => {
375+
this.renderingAccordionState.restoreState(accordionState);
376+
},
377+
);
342378
}
343379
toJSON() {
344380
const x = super.toJSON();
@@ -354,6 +390,7 @@ export class ImageUserLayer extends Base {
354390
x[VOLUME_RENDERING_GAIN_JSON_KEY] = this.volumeRenderingGain.toJSON();
355391
x[VOLUME_RENDERING_DEPTH_SAMPLES_JSON_KEY] =
356392
this.volumeRenderingDepthSamplesTarget.toJSON();
393+
x[RENDERING_ACCORDION_JSON_KEY] = this.renderingAccordionState.toJSON();
357394
return x;
358395
}
359396

@@ -470,6 +507,7 @@ const LAYER_CONTROLS: LayerControlDefinition<ImageUserLayer>[] = [
470507
{
471508
label: "Resolution (slice)",
472509
toolJson: CROSS_SECTION_RENDER_SCALE_JSON_KEY,
510+
sectionKey: SLICE_SECTION_JSON_KEY,
473511
...renderScaleLayerControl((layer) => ({
474512
histogram: layer.sliceViewRenderScaleHistogram,
475513
target: layer.sliceViewRenderScaleTarget,
@@ -478,21 +516,25 @@ const LAYER_CONTROLS: LayerControlDefinition<ImageUserLayer>[] = [
478516
{
479517
label: "Blending (slice)",
480518
toolJson: BLEND_JSON_KEY,
519+
sectionKey: SLICE_SECTION_JSON_KEY,
481520
...enumLayerControl((layer) => layer.blendMode),
482521
},
483522
{
484523
label: "Opacity (slice)",
485524
toolJson: OPACITY_JSON_KEY,
525+
sectionKey: SLICE_SECTION_JSON_KEY,
486526
...rangeLayerControl((layer) => ({ value: layer.opacity })),
487527
},
488528
{
489529
label: "Volume rendering (experimental)",
490530
toolJson: VOLUME_RENDERING_JSON_KEY,
531+
sectionKey: VOLUME_RENDERING_SECTION_JSON_KEY,
491532
...enumLayerControl((layer) => layer.volumeRenderingMode),
492533
},
493534
{
494535
label: "Gain (3D)",
495536
toolJson: VOLUME_RENDERING_GAIN_JSON_KEY,
537+
sectionKey: VOLUME_RENDERING_SECTION_JSON_KEY,
496538
isValid: (layer) =>
497539
makeCachedDerivedWatchableValue(
498540
(volumeRenderingMode) =>
@@ -507,6 +549,7 @@ const LAYER_CONTROLS: LayerControlDefinition<ImageUserLayer>[] = [
507549
{
508550
label: "Resolution (3D)",
509551
toolJson: VOLUME_RENDERING_DEPTH_SAMPLES_JSON_KEY,
552+
sectionKey: VOLUME_RENDERING_SECTION_JSON_KEY,
510553
isValid: (layer) =>
511554
makeCachedDerivedWatchableValue(
512555
(volumeRenderingMode) =>
@@ -527,21 +570,25 @@ for (const control of LAYER_CONTROLS) {
527570
registerLayerControl(ImageUserLayer, control);
528571
}
529572

530-
class RenderingOptionsTab extends Tab {
573+
class RenderingOptionsTab extends AccordionTab {
531574
codeWidget: ShaderCodeWidget;
532-
constructor(public layer: ImageUserLayer) {
533-
super();
575+
constructor(
576+
public layer: ImageUserLayer,
577+
protected accordionState: AccordionState,
578+
) {
579+
super(accordionState);
534580
const { element } = this;
535581
this.codeWidget = this.registerDisposer(makeShaderCodeWidget(this.layer));
536582
element.classList.add("neuroglancer-image-dropdown");
537583

538584
for (const control of LAYER_CONTROLS) {
539-
element.appendChild(
585+
this.appendChild(
540586
addLayerControlToOptionsTab(this, layer, this.visibility, control),
587+
control.sectionKey,
541588
);
542589
}
543590

544-
element.appendChild(
591+
this.appendChild(
545592
makeShaderCodeWidgetTopRow(
546593
this.layer,
547594
this.codeWidget,
@@ -553,14 +600,14 @@ class RenderingOptionsTab extends Tab {
553600
"neuroglancer-image-dropdown-top-row",
554601
),
555602
);
556-
element.appendChild(
603+
this.appendChild(
557604
this.registerDisposer(
558605
new ChannelDimensionsWidget(layer.channelCoordinateSpaceCombiner),
559606
).element,
560607
);
561608

562-
element.appendChild(this.codeWidget.element);
563-
element.appendChild(
609+
this.appendChild(this.codeWidget.element);
610+
this.appendChild(
564611
this.registerDisposer(
565612
new ShaderControls(
566613
layer.shaderControlState,

0 commit comments

Comments
 (0)