diff --git a/docs/releases/v2.11.0.md b/docs/releases/v2.11.0.md index ba3c4a7e8c159b4602581b5bcfdaae78443e31c2..a205f109cb7f3a78543c7bc908a30dc70294fd58 100644 --- a/docs/releases/v2.11.0.md +++ b/docs/releases/v2.11.0.md @@ -3,3 +3,4 @@ ## Feature - Automatically selects human multilevel atlas on startup, if no atlases are selected +- Allow multiple region selection for visualization purposes (hold ctrl & select region) diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts index 07e87e2182654ee0a76d6561f02faabdb97c87be..ac272bc82f976687831e70f358104f302bb52c8a 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts @@ -58,7 +58,10 @@ export class SapiViewsCoreRichRegionsHierarchy { } @Output('sxplr-sapiviews-core-rich-regionshierarchy-region-select') - nodeClicked = new EventEmitter<SxplrRegion>() + selectRegion = new EventEmitter<SxplrRegion>() + + @Output('sxplr-sapiviews-core-rich-regionshierarchy-region-toggle') + toggleRegion = new EventEmitter<SxplrRegion>() @ViewChild(SxplrFlatHierarchyTreeView) treeView: SxplrFlatHierarchyTreeView<SxplrRegion> @@ -95,7 +98,7 @@ export class SapiViewsCoreRichRegionsHierarchy { private subs: Subscription[] = [] - onNodeClick(roi: SxplrRegion){ + onNodeClick({node: roi, event }: {node: SxplrRegion, event: MouseEvent}){ /** * only allow leave nodes to be selectable for now */ @@ -103,6 +106,10 @@ export class SapiViewsCoreRichRegionsHierarchy { if (children.length > 0) { return } - this.nodeClicked.emit(roi) + if (event.ctrlKey) { + this.toggleRegion.emit(roi) + } else { + this.selectRegion.emit(roi) + } } } diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts index 15de8e31fe6a57d4545f2ab50f8fbcbef914376b..e55fb7e12f574713a7a9c8aab1de3c6cf62e7367 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from "@angular/core"; +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, HostListener, Input, Output, TemplateRef } from "@angular/core"; import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"; import { ARIA_LABELS } from "common/constants" import { UntypedFormControl } from "@angular/forms"; @@ -45,6 +45,9 @@ export class SapiViewsCoreRichRegionListSearch { @Output('sxplr-sapiviews-core-rich-regionlistsearch-region-select') onOptionSelected = new EventEmitter<SxplrRegion>() + + @Output('sxplr-sapiviews-core-rich-regionlistsearch-region-toggle') + onRegionToggle = new EventEmitter<SxplrRegion>() public searchFormControl = new UntypedFormControl() @@ -70,6 +73,22 @@ export class SapiViewsCoreRichRegionListSearch { optionSelected(opt: MatAutocompleteSelectedEvent) { const selectedRegion = opt.option.value as SxplrRegion - this.onOptionSelected.emit(selectedRegion) + if (this.ctrlFlag) { + this.onRegionToggle.emit(selectedRegion) + } else { + this.onOptionSelected.emit(selectedRegion) + } + } + + ctrlFlag = false + + @HostListener('document:keydown', ['$event']) + keydown(event: KeyboardEvent) { + this.ctrlFlag = event.ctrlKey + } + + @HostListener('document:keyup', ['$event']) + keyup(event: KeyboardEvent) { + this.ctrlFlag = event.ctrlKey } } diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html index 71158649f12a4f782a0f149da06ce81b3bd7a25c..fa966e0aae72178d1cf123cceee3ee6a8f1a1cd1 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html @@ -4,7 +4,7 @@ Vertices </span> -<mat-chip-list> +<mat-chip-list class="wrapped-chips"> <mat-chip *ngFor="let point of (updateAnnotation?.points || []); let i = index" (click)="gotoRoi(point)" [matTooltip]="point.toString()"> diff --git a/src/components/flatHierarchy/treeView/treeView.component.ts b/src/components/flatHierarchy/treeView/treeView.component.ts index f67f7cdc8a157e42c763d63021cf052f594a3c1e..211d8f6298bb526b6042d64c48ae0db4726d47e3 100644 --- a/src/components/flatHierarchy/treeView/treeView.component.ts +++ b/src/components/flatHierarchy/treeView/treeView.component.ts @@ -40,7 +40,7 @@ export class SxplrFlatHierarchyTreeView<T extends Record<string, unknown>> exten expandOnInit: boolean = true @Output('sxplr-flat-hierarchy-tree-view-node-clicked') - nodeClicked = new EventEmitter<T>() + nodeClicked = new EventEmitter<{ node: T, event: MouseEvent}>() ngOnChanges(changes: SimpleChanges): void { if (changes.sxplrNodes || changes.sxplrIsParent) { @@ -110,11 +110,11 @@ export class SxplrFlatHierarchyTreeView<T extends Record<string, unknown>> exten return `` } - handleClickNode(node: TreeNode<T>){ + handleClickNode(node: TreeNode<T>, event: MouseEvent){ if (this.nodeLabelToggles) { this.treeControl.toggle(node) } - this.nodeClicked.emit(node.node) + this.nodeClicked.emit({ node: node.node, event }) } expandAll(){ diff --git a/src/components/flatHierarchy/treeView/treeView.template.html b/src/components/flatHierarchy/treeView/treeView.template.html index c35e36cf8cd85e639d99b8ca6bbfd8dd5d466b65..730cd04b0122a538195b971e9f8f90f39eed7e46 100644 --- a/src/components/flatHierarchy/treeView/treeView.template.html +++ b/src/components/flatHierarchy/treeView/treeView.template.html @@ -47,7 +47,7 @@ <!-- template to render the node --> <div class="node-render-tmpl" [ngClass]="nodeLabelToggles ? 'label-toggles' : ''" - (click)="handleClickNode(node)"> + (click)="handleClickNode(node, $event)"> <ng-template [ngTemplateOutlet]="renderNodeTmplRef" [ngTemplateOutletContext]="{ diff --git a/src/extra_styles.css b/src/extra_styles.css index eda87b2bf8ac0ab8b8b2ab18a73a1383d070223c..741c49c6f681474c3abe5e514be06dcd32320068 100644 --- a/src/extra_styles.css +++ b/src/extra_styles.css @@ -849,13 +849,7 @@ iav-cmp-viewer-container .mat-chip-list-wrapper flex-wrap: nowrap; } -sxplr-sapiviews-features-ieeg-ieegdataset .mat-chip-list-wrapper -{ - overflow-y: hidden; - overflow-x: auto; -} - -iav-cmp-viewer-container poly-update-cmp .mat-chip-list-wrapper +.wrapped-chips .mat-chip-list-wrapper { flex-wrap: wrap; } diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts index a175464262a0b08da1039bac1d9a46e188194c30..0b9f5d3c980f18f6440f7e50ff6364e6b710b596 100644 --- a/src/state/atlasSelection/actions.ts +++ b/src/state/atlasSelection/actions.ts @@ -67,6 +67,13 @@ export const selectRegion = createAction( }>() ) +export const toggleRegion = createAction( + `${nameSpace} toggleRegion`, + props<{ + region: SxplrRegion + }>() +) + export const setSelectedRegions = createAction( `${nameSpace} setSelectedRegions`, props<{ diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts index 1138c493706646809b09753841a10ec9c6a9095b..5fcf2e1820a936bc605d18fac8717b6d347629b1 100644 --- a/src/state/atlasSelection/store.ts +++ b/src/state/atlasSelection/store.ts @@ -37,13 +37,7 @@ const reducer = createReducer( on( actions.selectRegion, (state, { region }) => { - /** - * if roi does not have visualizedIn defined - * or internal identifier - * - * ignore - */ - const selected = state.selectedRegions.includes(region) + const selected = state.selectedRegions.length === 1 && state.selectedRegions.find(r => r.name === region.name) return { ...state, selectedRegions: selected @@ -52,6 +46,18 @@ const reducer = createReducer( } } ), + on( + actions.toggleRegion, + (state, { region }) => { + const selected = state.selectedRegions.find(r => r.name === region.name) + return { + ...state, + selectedRegions: selected + ? state.selectedRegions.filter(r => r.name !== region.name) + : [...state.selectedRegions, region] + } + } + ), on( actions.setSelectedRegions, (state, { regions }) => { diff --git a/src/util/side-panel/side-panel.component.html b/src/util/side-panel/side-panel.component.html new file mode 100644 index 0000000000000000000000000000000000000000..667876ee42702f5714ec80f84e3bb2e9dfd20a12 --- /dev/null +++ b/src/util/side-panel/side-panel.component.html @@ -0,0 +1,26 @@ +<ng-template #headerTmpl> + <ng-content select="[header]"></ng-content> +</ng-template> + +<mat-card class="mat-elevation-z4"> + <div + [style.backgroundColor]="cardColor" + class="vanishing-border" + [ngClass]="{ + 'darktheme': darktheme === true, + 'lighttheme': darktheme === false + }"> + + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> + + <mat-card-title class="sxplr-custom-cmp text"> + <ng-content select="[title]"></ng-content> + </mat-card-title> + + <mat-card-subtitle> + <ng-content select="[subtitle]"></ng-content> + </mat-card-subtitle> + </div> +</mat-card> + +<ng-content></ng-content> diff --git a/src/util/side-panel/side-panel.component.scss b/src/util/side-panel/side-panel.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..44bed767cbc98d3cf2ecdc694422842c758e9177 --- /dev/null +++ b/src/util/side-panel/side-panel.component.scss @@ -0,0 +1,5 @@ +.vanishing-border +{ + padding: 16px; + margin: -16px!important; +} diff --git a/src/util/side-panel/side-panel.component.spec.ts b/src/util/side-panel/side-panel.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..73a50a69a04470d96248180aafab1d09ced161fd --- /dev/null +++ b/src/util/side-panel/side-panel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SidePanelComponent } from './side-panel.component'; + +describe('SidePanelComponent', () => { + let component: SidePanelComponent; + let fixture: ComponentFixture<SidePanelComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SidePanelComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SidePanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/util/side-panel/side-panel.component.ts b/src/util/side-panel/side-panel.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6108664f0e51eaedb4a61d8e302de7f5c0a36354 --- /dev/null +++ b/src/util/side-panel/side-panel.component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'sxplr-side-panel', + templateUrl: './side-panel.component.html', + styleUrls: ['./side-panel.component.scss'] +}) +export class SidePanelComponent { + @Input('sxplr-side-panel-card-color') + cardColor: string = `rgb(200, 200, 200)` + + @Input('sxplr-side-panel-card-color') + darktheme: boolean = false +} diff --git a/src/util/util.module.ts b/src/util/util.module.ts index de0d23cb14d309d82c2126ebe8b4f2b0421ea9d1..4e483fe9b6621b48cbdda0180b7f7060a1b155ad 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -19,10 +19,15 @@ import { GetFilenamePipe } from "./pipes/getFilename.pipe"; import { CombineFnPipe } from "./pipes/combineFn.pipe"; import { MergeObjPipe } from "./mergeObj.pipe"; import { IncludesPipe } from "./includes.pipe"; +import { SidePanelComponent } from './side-panel/side-panel.component'; +import { MatCardModule } from "@angular/material/card"; +import { CommonModule } from "@angular/common"; @NgModule({ imports:[ - LayoutModule + LayoutModule, + MatCardModule, + CommonModule, ], declarations: [ StopPropagationDirective, @@ -44,6 +49,7 @@ import { IncludesPipe } from "./includes.pipe"; CombineFnPipe, MergeObjPipe, IncludesPipe, + SidePanelComponent, ], exports: [ StopPropagationDirective, @@ -65,6 +71,7 @@ import { IncludesPipe } from "./includes.pipe"; CombineFnPipe, MergeObjPipe, IncludesPipe, + SidePanelComponent ] }) diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 8bcb8567114a8989b98fca2355c0a33864f5a2e7..1ff596295312d22d8d87e775c3c12821f0f1cbcb 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -93,17 +93,26 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy { this.onDestroyCb.push(() => onhovSegSub.unsubscribe()) } - private selectHoveredRegion(_ev: any): boolean{ + private selectHoveredRegion(ev: PointerEvent): boolean{ /** * If label indicies are not defined by the ontology, it will be a string in the format of `{ngId}#{labelIndex}` */ const trueOnhoverSegments = this.onhoverSegments && this.onhoverSegments.filter(v => typeof v === 'object') if (!trueOnhoverSegments || (trueOnhoverSegments.length === 0)) return true - this.store$.dispatch( - atlasSelection.actions.selectRegion({ - region: trueOnhoverSegments[0] - }) - ) + + if (ev.ctrlKey) { + this.store$.dispatch( + atlasSelection.actions.toggleRegion({ + region: trueOnhoverSegments[0] + }) + ) + } else { + this.store$.dispatch( + atlasSelection.actions.selectRegion({ + region: trueOnhoverSegments[0] + }) + ) + } return true } } diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index b19b8224e9b8ea1117baacc101101e233564b184..2c3def567e5eab3af34a144c6a072353e83872e1 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -271,6 +271,14 @@ export class ViewerCmp implements OnDestroy { ) } + public toggleRoi(roi: SxplrRegion) { + this.store$.dispatch( + atlasSelection.actions.toggleRegion({ + region: roi + }) + ) + } + public exitSpecialViewMode(): void{ this.store$.dispatch( atlasSelection.actions.clearViewerMode() diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 19914ede12cbe2b99d2be1d8b227000aefca2dd7..6d99be5dc05c4313212a99e0962aef2db6fcc79f 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -386,29 +386,62 @@ </sxplr-wrapper-atp-selector> <!-- selected region chip --> + <ng-template [ngIf]="selectedRegions$ | async" let-regions> + + <!-- single region --> + <ng-template [ngIf]="regions.length === 1"> + <sxplr-smart-chip + *ngFor="let region of regions" + [noMenu]="true" + [color]="sapiViewsCoreRegion.regionRgbString" + (click)="showFullSideNav()" + sxplr-sapiviews-core-region + [sxplr-sapiviews-core-region-atlas]="selectedAtlas$ | async" + [sxplr-sapiviews-core-region-template]="templateSelected$ | async" + [sxplr-sapiviews-core-region-parcellation]="parcellationSelected$ | async" + [sxplr-sapiviews-core-region-region]="region" + [sxplr-sapiviews-core-region-detail-flag]="true" + #sapiViewsCoreRegion="sapiViewsCoreRegion"> + <ng-template sxplrSmartChipContent> + <span class="regionname"> + {{ region.name }} + </span> + <button class="sxplr-mr-n3" + mat-icon-button + (click)="clearRoi()"> + <i class="fas fa-times"></i> + </button> + </ng-template> + </sxplr-smart-chip> + </ng-template> + + + <!-- multiple-regions --> + <ng-template [ngIf]="regions.length > 1"> + <sxplr-smart-chip + [noMenu]="true" + + (click)="showFullSideNav()" + sxplr-sapiviews-core-region + [sxplr-sapiviews-core-region-atlas]="selectedAtlas$ | async" + [sxplr-sapiviews-core-region-template]="templateSelected$ | async" + [sxplr-sapiviews-core-region-parcellation]="parcellationSelected$ | async"> + <ng-template sxplrSmartChipContent> + <span class="regionname"> + {{ regions.length }} regions selected + </span> + <button class="sxplr-mr-n3" + mat-icon-button + (click)="clearRoi()"> + <i class="fas fa-times"></i> + </button> + </ng-template> + </sxplr-smart-chip> + + </ng-template> + + </ng-template> <ng-template ngFor [ngForOf]="selectedRegions$ | async" let-region> - <sxplr-smart-chip - [noMenu]="true" - [color]="sapiViewsCoreRegion.regionRgbString" - (click)="showFullSideNav()" - sxplr-sapiviews-core-region - [sxplr-sapiviews-core-region-atlas]="selectedAtlas$ | async" - [sxplr-sapiviews-core-region-template]="templateSelected$ | async" - [sxplr-sapiviews-core-region-parcellation]="parcellationSelected$ | async" - [sxplr-sapiviews-core-region-region]="region" - [sxplr-sapiviews-core-region-detail-flag]="true" - #sapiViewsCoreRegion="sapiViewsCoreRegion"> - <ng-template sxplrSmartChipContent> - <span class="regionname"> - {{ region.name }} - </span> - <button class="sxplr-mr-n3" - mat-icon-button - (click)="clearRoi()"> - <i class="fas fa-times"></i> - </button> - </ng-template> - </sxplr-smart-chip> </ng-template> </div> @@ -474,6 +507,7 @@ [sxplr-sapiviews-core-rich-regionshierarchy-regions]="allAvailableRegions$ | async" [sxplr-sapiviews-core-rich-regionshierarchy-accent-regions]="selectedRegions$ | async" (sxplr-sapiviews-core-rich-regionshierarchy-region-select)="selectRoi($event)" + (sxplr-sapiviews-core-rich-regionshierarchy-region-toggle)="toggleRoi($event)" > </sxplr-sapiviews-core-rich-regionshierarchy> @@ -490,7 +524,8 @@ <sxplr-sapiviews-core-rich-regionlistsearch [sxplr-sapiviews-core-rich-regionlistsearch-regions]="allAvailableRegions$ | async" [sxplr-sapiviews-core-rich-regionlistsearch-current-search]="selectedRegions$ | async | getProperty : 0 | getProperty : 'name'" - (sxplr-sapiviews-core-rich-regionlistsearch-region-select)="selectRoi($event)"> + (sxplr-sapiviews-core-rich-regionlistsearch-region-select)="selectRoi($event)" + (sxplr-sapiviews-core-rich-regionlistsearch-region-toggle)="toggleRoi($event)"> <ng-template regionTemplate let-region> <div class="sxplr-d-flex"> <button @@ -723,43 +758,59 @@ <ng-template #multiRegionTmpl let-regions="regions"> <ng-template [ngIf]="regions.length > 0" [ngIfElse]="regionPlaceholderTmpl"> - <!-- other regions detail accordion --> - <mat-accordion class="bs-border-box ml-15px-n mr-15px-n mt-2"> - <!-- Multi regions include --> - - <mat-expansion-panel - [attr.data-opened]="expansionPanel.expanded" - [attr.data-mat-expansion-title]="'Brain regions'" - hideToggle - #expansionPanel="matExpansionPanel"> - - <mat-expansion-panel-header> - - <!-- title --> - <mat-panel-title> - Brain regions - </mat-panel-title> - - <!-- desc + icon --> - <mat-panel-description class="d-flex align-items-center justify-content-end"> - <span class="mr-3">{{ regions.length }}</span> - <span class="accordion-icon d-inline-flex justify-content-center"> - <i class="fas fa-brain"></i> - </span> - </mat-panel-description> - - </mat-expansion-panel-header> - - <!-- content --> - <ng-template matExpansionPanelContent> - - <!-- TODO use actual region chip in sapiViews/core/region/chip --> - SOMETHING - </ng-template> - </mat-expansion-panel> - - </mat-accordion> + <sxplr-side-panel> + <div class="sapi-container" header></div> + <div title> + Multiple regions selected + </div> + <!-- other regions detail accordion --> + <mat-accordion class="bs-border-box ml-15px-n mr-15px-n mt-2"> + + <!-- Multi regions include --> + + <mat-expansion-panel + [attr.data-opened]="expansionPanel.expanded" + [attr.data-mat-expansion-title]="'Brain regions'" + hideToggle + #expansionPanel="matExpansionPanel"> + + <mat-expansion-panel-header> + + <!-- title --> + <mat-panel-title> + Selected regions + </mat-panel-title> + + <!-- desc + icon --> + <mat-panel-description class="d-flex align-items-center justify-content-end"> + <span class="mr-3">{{ regions.length }}</span> + <span class="accordion-icon d-inline-flex justify-content-center"> + <i class="fas fa-brain"></i> + </span> + </mat-panel-description> + + </mat-expansion-panel-header> + + <!-- content --> + <ng-template matExpansionPanelContent> + <mat-chip-list class="wrapped-chips"> + <mat-chip *ngFor="let region of regions"> + <span> + {{ region.name }} + </span> + <button mat-icon-button + (click)="toggleRoi(region)" + iav-stop="mousedown click"> + <i class="fas fa-times"></i> + </button> + </mat-chip> + </mat-chip-list> + </ng-template> + </mat-expansion-panel> + + </mat-accordion> + </sxplr-side-panel> </ng-template> </ng-template>