diff --git a/src/services/effect/effect.ts b/src/services/effect/effect.ts index 054568c678a82eec150dde0260e95a53d086c183..0784fdb11edad7e9687d06448fc0e04ff0e75de7 100644 --- a/src/services/effect/effect.ts +++ b/src/services/effect/effect.ts @@ -46,6 +46,22 @@ export class UseEffects implements OnDestroy{ }) ) + this.onDeselectRegionsWithId$ = this.actions$.pipe( + ofType(ACTION_TYPES.DESELECT_REGIONS_WITH_ID), + map(action => { + const { deselecRegionIds } = action as any + return deselecRegionIds + }), + withLatestFrom(this.regionsSelected$), + map(([ deselecRegionIds, alreadySelectedRegions ]) => { + const deselectSet = new Set(deselecRegionIds) + return { + type: SELECT_REGIONS, + selectRegions: alreadySelectedRegions.filter(({ ngId, labelIndex }) => !deselectSet.has(generateLabelIndexId({ ngId, labelIndex }))) + } + }) + ) + this.addToSelectedRegions$ = this.actions$.pipe( ofType(ADD_TO_REGIONS_SELECTION_WITH_IDS), map(action => { @@ -104,6 +120,9 @@ export class UseEffects implements OnDestroy{ @Effect() onDeselectRegions: Observable<any> + @Effect() + onDeselectRegionsWithId$: Observable<any> + private convertRegionIdsToRegion = ([selectRegionIds, parcellation]) => { const { ngId: defaultNgId } = parcellation return (<any[]>selectRegionIds) @@ -215,4 +234,10 @@ export const compareRegions: (r1: any,r2: any) => boolean = (r1, r2) => { return r1.ngId === r2.ngId && r1.labelIndex === r2.labelIndex && r1.name === r2.name -} \ No newline at end of file +} + +const ACTION_TYPES = { + DESELECT_REGIONS_WITH_ID: 'DESELECT_REGIONS_WITH_ID' +} + +export const VIEWER_STATE_ACTION_TYPES = ACTION_TYPES \ No newline at end of file diff --git a/src/ui/searchSideNav/searchSideNav.template.html b/src/ui/searchSideNav/searchSideNav.template.html index a531e4cc1650db113d0c544676b20d26971efd1b..93fb21bb0aa479d2f1999cd70302d09553414c1d 100644 --- a/src/ui/searchSideNav/searchSideNav.template.html +++ b/src/ui/searchSideNav/searchSideNav.template.html @@ -26,15 +26,20 @@ <ng-container *ngIf="showDataset"> <!-- selected regions container --> - <mat-card *ngIf="viewerStateController.regionsSelected$ | async as regionsSelected" + <mat-card *ngIf="false && viewerStateController.regionsSelected$ | async as regionsSelected" [ngClass]="{'h-117px flex-grow-1': regionsSelected.length > 1, 'flex-grow-0': regionsSelected.length < 2}" class="flex-shrink-0 mb-1 pe-all"> <!-- show when no region is selected --> - <mat-card-subtitle *ngIf="regionsSelected.length === 0"> - In the current view - </mat-card-subtitle> - + <mat-card-content *ngIf="regionsSelected.length === 0"> + <div class="pt-2 pb-2 d-flex flex-row align-items-center flex-nowrap"> + <i *ngIf="false" class="fas fa-brain font-2x mr-2"></i> + + <span class="font-weight-bold"> + In this parcellation atlas + </span> + </div> + </mat-card-content> <!-- show when regions are selected --> <mat-card-content *ngIf="regionsSelected.length > 0" class="h-100"> @@ -91,10 +96,19 @@ [parcellation]="viewerStateController.parcellationSelected$ | async" [regions]="viewerStateController.regionsSelected$ | async" (dataentriesUpdated)="availableDatasets = $event.length"> - - <mat-card-subtitle card-header> - Related datasets - </mat-card-subtitle> + + <ng-container card-content='prepend'> + <ng-container *ngTemplateOutlet="selectedRegionsTmpl"> + + </ng-container> + + <mat-divider class="mt-2 mb-4 position-relative"></mat-divider> + + <mat-card-subtitle card-header> + Related datasets + </mat-card-subtitle> + + </ng-container> <!-- footer content --> <div class="d-flex flex-row justify-content-center" card-footer> @@ -129,4 +143,72 @@ Hide </button> </mat-dialog-actions> +</ng-template> + +<!-- selected regions container --> +<ng-template #selectedRegionsTmpl> + <ng-container *ngIf="viewerStateController.regionsSelected$ | async as regionsSelected"> + <div [ngClass]="{'h-117px flex-grow-1': regionsSelected.length > 1, 'flex-grow-0': regionsSelected.length < 2}" + class="flex-shrink-0 mb-1 pe-all d-flex flex-column"> + + <!-- show when no region is selected --> + <div *ngIf="regionsSelected.length === 0" + class="pt-2 pb-2 d-flex flex-row align-items-center flex-nowrap"> + <i *ngIf="false" class="fas fa-brain font-2x mr-2"></i> + + <span class="font-weight-bold"> + In this parcellation atlas + </span> + </div> + + + <!-- show when regions are selected --> + <div *ngIf="regionsSelected.length > 0" class="h-100"> + + <!-- single region --> + <ng-template [ngIf]="regionsSelected.length === 1" [ngIfElse]="multiRegionTemplate"> + + <!-- selected brain region --> + <div class="pt-2 pb-2 d-flex flex-row align-items-center flex-nowrap"> + <i class="fas fa-brain font-2x mr-2"></i> + + <span class="font-weight-bold"> + {{ regionsSelected[0].name }} + </span> + + <button (click)="removeRegion(regionsSelected[0])" mat-icon-button> + <i class="fas fa-trash"></i> + </button> + </div> + </ng-template> + + <!-- multi region --> + <ng-template #multiRegionTemplate> + <cdk-virtual-scroll-viewport class="h-100" itemSize="78"> + <div *cdkVirtualFor="let region of regionsSelected; trackBy: trackByFn ; let index = index" + class="region-wrapper d-flex flex-column" > + <!-- divider if index !== 0 --> + <mat-divider class="flex-grow-0 flex-shrink-0" *ngIf="index !== 0"></mat-divider> + + <!-- selected brain region --> + <div class="flex-grow-1 flex-shrink-1 pt-2 pb-2 d-flex flex-row align-items-center flex-nowrap"> + <i class="flex-grow-0 flex-shrink-0 fas fa-brain font-2x mr-2"></i> + + <span class="flex-grow-1 flex-shrink-1 font-weight-bold"> + {{ region.name }} + </span> + + <button mat-icon-button + class="flex-grow-0 flex-shrink-0" + (click)="removeRegion(region)" > + <i class="fas fa-trash"></i> + </button> + </div> + </div> + </cdk-virtual-scroll-viewport> + </ng-template> + + </div> + </div> + </ng-container> </ng-template> \ No newline at end of file diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index e34a1fd34a17713358efeff465b7ea1028bf47a0..d68c6e5d63b10a3cdfd3c267fad3ccadf57be0c5 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -1,13 +1,14 @@ import { Component, EventEmitter, Output, ViewChild, ElementRef, TemplateRef, Input } from "@angular/core"; import { Store, select } from "@ngrx/store"; -import { Observable } from "rxjs"; -import { map, distinctUntilChanged, startWith, withLatestFrom, debounceTime, shareReplay, take, filter } from "rxjs/operators"; +import { Observable, BehaviorSubject } from "rxjs"; +import { map, distinctUntilChanged, startWith, withLatestFrom, debounceTime, shareReplay, take, filter, tap } from "rxjs/operators"; import { getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId } from "src/services/stateStore.service"; import { FormControl } from "@angular/forms"; import { MatAutocompleteSelectedEvent, MatDialog } from "@angular/material"; -import { ADD_TO_REGIONS_SELECTION_WITH_IDS, SELECT_REGIONS } from "src/services/state/viewerState.store"; +import { ADD_TO_REGIONS_SELECTION_WITH_IDS, SELECT_REGIONS, CHANGE_NAVIGATION } from "src/services/state/viewerState.store"; import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "../viewerState.base"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; +import { VIEWER_STATE_ACTION_TYPES } from "src/services/effect/effect"; const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase().includes(searchTerm.toLowerCase()) @@ -29,6 +30,11 @@ export class RegionTextSearchAutocomplete{ public useMobileUI$: Observable<boolean> + private focusedRegionId$: BehaviorSubject<string> = new BehaviorSubject(null) + public focusedRegion$: Observable<any> + + public selectedRegionLabelIndexSet: Set<string> = new Set() + constructor( private store$: Store<any>, private dialog: MatDialog, @@ -59,13 +65,21 @@ export class RegionTextSearchAutocomplete{ } } return returnArray + }), + shareReplay(1) + ) + + this.focusedRegion$ = this.focusedRegionId$.pipe( + withLatestFrom(this.regionsWithLabelIndex$), + map(([ id, regions ]) => { + if (!id) return null + return regions.find(({ labelIndexId }) => labelIndexId === id) }) - ) + ) this.autocompleteList$ = this.formControl.valueChanges.pipe( startWith(''), debounceTime(200), - filter(string => string.length > 0), withLatestFrom(this.regionsWithLabelIndex$.pipe( startWith([]) )), @@ -76,6 +90,10 @@ export class RegionTextSearchAutocomplete{ this.regionsSelected$ = viewerState$.pipe( select('regionsSelected'), distinctUntilChanged(), + tap(regions => { + const arrLabelIndexId = regions.map(({ ngId, labelIndex }) => generateLabelIndexId({ ngId, labelIndex })) + this.selectedRegionLabelIndexSet = new Set(arrLabelIndexId) + }), shareReplay(1) ) @@ -86,15 +104,40 @@ export class RegionTextSearchAutocomplete{ ) } - public optionSelected(ev: MatAutocompleteSelectedEvent){ - const id = ev.option.value + public toggleRegionWithId(id: string, removeFlag=false){ + if (removeFlag) { + this.store$.dispatch({ + type: VIEWER_STATE_ACTION_TYPES.DESELECT_REGIONS_WITH_ID, + deselecRegionIds: [id] + }) + } else { + this.store$.dispatch({ + type: ADD_TO_REGIONS_SELECTION_WITH_IDS, + selectRegionIds : [id] + }) + } + } + + public navigateTo(position){ this.store$.dispatch({ - type: ADD_TO_REGIONS_SELECTION_WITH_IDS, - selectRegionIds : [id] + type: CHANGE_NAVIGATION, + navigation: { + position, + animation: {} + } }) + } + public optionSelected(ev: MatAutocompleteSelectedEvent){ + const id = ev.option.value this.autoTrigger.nativeElement.value = '' - this.autoTrigger.nativeElement.focus() + this.focusedRegionId$.next(id) + } + + public openRegionFocusDialog(tmpl:TemplateRef<any>){ + this.dialog.open(tmpl).afterClosed().subscribe(() => { + this.autoTrigger.nativeElement.focus() + }) } private regionsWithLabelIndex$: Observable<any[]> diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.template.html b/src/ui/viewerStateController/regionSearch/regionSearch.template.html index 02128727b8151b99c22c977b09d54ed2e5b00b2e..b8bacd2acc6ebdde5811c577d86466a8786cfaac 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.template.html +++ b/src/ui/viewerStateController/regionSearch/regionSearch.template.html @@ -14,7 +14,7 @@ <mat-autocomplete (opened)="focused = true" (closed)="focused = false" - (optionSelected)="optionSelected($event)" + (optionSelected)="optionSelected($event); openRegionFocusDialog(regionFocusDialog)" autoActiveFirstOption #auto="matAutocomplete"> <mat-option @@ -36,6 +36,41 @@ </button> </div> +<ng-template #regionFocusDialog> + <ng-container *ngIf="focusedRegion$ | async as focusedRegion; else noRegionSelected"> + <div mat-dialog-title> + {{ focusedRegion.name }} + </div> + <div class="justify-content-end" mat-dialog-actions> + <button mat-flat-button + *ngIf="focusedRegion.position" + (click)="navigateTo(focusedRegion.position)" + mat-dialog-close + class="ml-1" + color="primary"> + Navigate + </button> + <button mat-flat-button + *ngIf="focusedRegion.labelIndexId" + mat-dialog-close + (click)="toggleRegionWithId(focusedRegion.labelIndexId, selectedRegionLabelIndexSet.has(focusedRegion.labelIndexId))" + class="ml-1" + [color]="selectedRegionLabelIndexSet.has(focusedRegion.labelIndexId) ? 'warn' : 'primary'"> + {{ selectedRegionLabelIndexSet.has(focusedRegion.labelIndexId) ? 'Remove from selection' : 'Add to selection' }} + </button> + <button mat-button + [mat-dialog-close]="null" + class="ml-1"> + Dismiss + </button> + </div> + </ng-container> + + <ng-template #noRegionSelected> + No region selected. + </ng-template> +</ng-template> + <ng-template #regionHierarchyDialog> <div class="h-100 d-flex flex-column"> <mat-dialog-content class="flex-grow-1 flex-shrink-1">