diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 13248f4a0d39349ba8f58b45cbc309246b1c9788..cae9d027e425bcbd6a49af168f50f2d34225eb0a 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -1,8 +1,34 @@ -import { Component, HostBinding, ViewChild, ViewContainerRef, OnDestroy, OnInit, TemplateRef, AfterViewInit, Renderer2 } from "@angular/core"; +import { + Component, + HostBinding, + ViewChild, + OnDestroy, + OnInit, + TemplateRef, + AfterViewInit, + Renderer2, + ElementRef +} from "@angular/core"; import { Store, select, ActionsSubject } from "@ngrx/store"; -import { ViewerStateInterface, isDefined, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA, safeFilter } from "../services/stateStore.service"; -import { Observable, Subscription, combineLatest, interval, merge, of } from "rxjs"; -import { map, filter, distinctUntilChanged, delay, concatMap, withLatestFrom } from "rxjs/operators"; +import { + ViewerStateInterface, + isDefined, + FETCHED_SPATIAL_DATA, + UPDATE_SPATIAL_DATA, + safeFilter, + CHANGE_NAVIGATION, generateLabelIndexId +} from "../services/stateStore.service"; +import {Observable, Subscription, combineLatest, interval, merge, of, Observer} from "rxjs"; +import { + map, + filter, + distinctUntilChanged, + delay, + concatMap, + withLatestFrom, + switchMapTo, + takeUntil, take, tap, mapTo +} from "rxjs/operators"; import { AtlasViewerDataService } from "./atlasViewer.dataService.service"; import { WidgetServices } from "./widgetUnit/widgetService.service"; import { LayoutMainSide } from "../layouts/mainside/mainside.component"; @@ -17,6 +43,9 @@ import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS, SHOW_BOTTOM_SHEET } from "src/ import { TabsetComponent } from "ngx-bootstrap/tabs"; import { LocalFileService } from "src/services/localFile.service"; import { MatDialog, MatDialogRef, MatSnackBar, MatSnackBarRef, MatBottomSheet, MatBottomSheetRef } from "@angular/material"; +import {ADD_TO_REGIONS_SELECTION_WITH_IDS} from "src/services/state/viewerState.store"; +import {VIEWER_STATE_ACTION_TYPES} from "src/services/effect/effect"; +import {RegionMenuComponent} from "src/ui/regionToolsMenu/regionMenu.component"; /** * TODO @@ -71,8 +100,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public dedicatedView$: Observable<string | null> public onhoverSegments$: Observable<string[]> - public onhoverSegmentsForFixed$: Observable<string[]> - + public onhoverLandmark$ : Observable<{landmarkName: string, datasets: any} | null> private subscriptions: Subscription[] = [] @@ -90,7 +118,9 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public sidePanelOpen$: Observable<boolean> - public toggleMessage = this.constantsService.toggleMessage + + onhoverSegmentsForFixed$: Observable<string[]> + regionToolsMenuVisible = false constructor( private store: Store<ViewerStateInterface>, @@ -354,7 +384,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { } ngAfterViewInit() { - /** * preload the main bundle after atlas viewer has been loaded. * This should speed up where user first navigate to the home page, @@ -397,9 +426,26 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) this.onhoverSegmentsForFixed$ = this.rClContextualMenu.onShow.pipe( - withLatestFrom(this.onhoverSegments$), - map(([_flag, onhoverSegments]) => onhoverSegments || []) + withLatestFrom(this.onhoverSegments$), + map(([_flag, onhoverSegments]) => onhoverSegments || []) ) + + } + + mouseDownNehuba(event) { + this.regionToolsMenuVisible = false + this.rClContextualMenu.hide() + } + + mouseUpNehuba(event) { + // if (this.mouseUpLeftPosition === event.pageX && this.mouseUpTopPosition === event.pageY) {} + this.regionToolsMenuVisible = true + if (!this.rClContextualMenu) return + this.rClContextualMenu.mousePos = [ + event.clientX, + event.clientY + ] + this.rClContextualMenu.show() } /** @@ -458,20 +504,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) } - nehubaClickHandler(event:MouseEvent){ - if (!this.rClContextualMenu) return - this.rClContextualMenu.mousePos = [ - event.clientX, - event.clientY - ] - this.rClContextualMenu.show() - } - - openLandmarkUrl(dataset) { - this.rClContextualMenu.hide() - window.open(dataset.externalLink, "_blank") - } - @HostBinding('attr.version') public _version : string = VERSION } diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 20a455ef34dff06334cf514684e342d04a098451..470838c7d62aafb645f27e09668c165065aafd90 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -41,7 +41,9 @@ <ui-nehuba-container iav-mouse-hover #iavMouseHoverEl="iavMouseHover" [currentOnHoverObs$]="iavMouseHoverEl.currentOnHoverObs$" [currentOnHover]="iavMouseHoverEl.currentOnHoverObs$ | async" - (contextmenu)="$event.stopPropagation(); $event.preventDefault();"> + iav-captureClickListenerDirective + (mouseDownEmitter)="mouseDownNehuba($event)" + (mapClicked)="mouseUpNehuba($event)"> </ui-nehuba-container> <div class="z-index-10 position-absolute pe-none w-100 h-100"> @@ -102,53 +104,6 @@ </ng-container> </div> - <!-- TODO document fixedMouseContextualContainerDirective , then deprecate this --> - <!-- TODO move to nehuba overlay container --> - <panel-component class="shadow" fixedMouseContextualContainerDirective #rClContextMenu> - <div heading> - <h5 class="pe-all p-2 m-0"> - What's here? - </h5> - </div> - <div body> - - <div *ngIf="(onhoverSegmentsForFixed$ | async)?.length > 0 || (selectedRegions$ | async)?.length > 0" - class="p-2"> - Search for data relating to: - </div> - - <div *ngFor="let onhoverSegmentFixed of (onhoverSegmentsForFixed$ | async)" - (click)="searchRegion([onhoverSegmentFixed])" - class="ws-no-wrap text-left pe-all btn btn-sm btn-secondary btn-block mt-0" data-toggle="tooltip" - data-placement="top" [title]="onhoverSegmentFixed.name"> - <small class="text-semi-transparent">(hovering)</small> {{ onhoverSegmentFixed.name }} - </div> - - <div *ngIf="(selectedRegions$ | async)?.length > 0 && (selectedRegions$ | async); let selectedRegions" - (click)="searchRegion(selectedRegions)" - class="ws-no-wrap text-left pe-all mt-0 btn btn-sm btn-secondary btn-block"> - <ng-container *ngIf="selectedRegions.length > 1"> - <small class="text-semi-transparent">(selected)</small> {{ selectedRegions.length }} selected regions - </ng-container> - <ng-container *ngIf="selectedRegions.length === 1"> - <small class="text-semi-transparent">(selected)</small> {{ selectedRegions[0].name }} - </ng-container> - </div> - - <div class="p-2 text-muted" - *ngIf="(onhoverSegmentsForFixed$ | async)?.length === 0 && (selectedRegions$ | async)?.length === 0 && (onhoverLandmarksForFixed$ | async)?.length === 0"> - Right click on a parcellation region or select parcellation regions to search KG for associated datasets. - </div> - - <ng-template #noRegionSelected> - <div (click)="searchRegion()" class="ws-no-wrap text-left pe-all mt-0 btn btn-sm btn-secondary btn-block"> - No region selected. Search KG for all datasets in this template space. - </div> - </ng-template> - - </div> - </panel-component> - <div floatingMouseContextualContainerDirective> <div class="d-inline-block" iav-mouse-hover #iavMouseHoverConetxtualBlock="iavMouseHover" contextualBlock> @@ -174,6 +129,24 @@ <!-- TODO Potentially implementing plugin contextual info --> </div> + <panel-component class="shadow p-0 m-0" fixedMouseContextualContainerDirective> + <div body class="pe-all" *ngIf="(onhoverSegmentsForFixed$ | async) as onHoverSegments"> + <mat-card *ngIf="onHoverSegments.length > 0 && regionToolsMenuVisible" class="d-flex flex-column p-0"> + <div *ngFor="let onHoverRegion of onHoverSegments; let first = first" + class="border-dark rounded"> + <mat-divider *ngIf="!first"></mat-divider> + + <region-menu #regionToolsMenu + [selectedRegions$]="selectedRegions$" + [region]="onHoverRegion"> + </region-menu> + + </div> + </mat-card> + </div> + + </panel-component> + </layout-floating-container> <!-- required for manufacturing plugin templates --> @@ -222,4 +195,4 @@ <!-- logo tmpl --> <ng-template #logoTmpl> <logo-container></logo-container> -</ng-template> \ No newline at end of file +</ng-template> diff --git a/src/main.module.ts b/src/main.module.ts index 6dd663fae585edfc1048ea2c5c8d5dc5beece6f9..1db540e32e8630cc0ba0a226ad50a9bac91b52cb 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -30,7 +30,6 @@ import { FloatingContainerDirective } from "./util/directives/floatingContainer. import { PluginFactoryDirective } from "./util/directives/pluginFactory.directive"; import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive"; import { AuthService } from "./services/auth.service"; -import { FixedMouseContextualContainerDirective } from "./util/directives/FixedMouseContextualContainerDirective.directive"; import { DatabrowserService } from "./ui/databrowserModule/databrowser.service"; import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe"; import {HttpClientModule} from "@angular/common/http"; @@ -54,6 +53,7 @@ import 'hammerjs' import 'src/res/css/version.css' import 'src/theme.scss' import 'src/res/css/extra_styles.css' +import {CaptureClickListenerDirective} from "src/util/directives/captureClickListener.directive"; @NgModule({ imports : [ @@ -107,8 +107,8 @@ import 'src/res/css/extra_styles.css' FloatingContainerDirective, PluginFactoryDirective, FloatingMouseContextualContainerDirective, - FixedMouseContextualContainerDirective, DragDropDirective, + CaptureClickListenerDirective, /* pipes */ GetNamesPipe, diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index 2781c6cad4bac7c850d42f3e07cecb7d374d9f66..774f1adebd5fe25aee189b0b48c594e0234ff8af 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -679,4 +679,12 @@ body::after padding: 0!important; overflow: hidden; margin-top: 0.25rem; -} \ No newline at end of file +} + +.linear-gradient-fade:before { + content:''; + width:100%; + height:100%; + position:absolute; + background:linear-gradient(transparent 50px, #424242); +} diff --git a/src/ui/regionToolsMenu/regionMenu.component.ts b/src/ui/regionToolsMenu/regionMenu.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a64bf7d8451e4e3f778855d2e8c00e86f7fe477 --- /dev/null +++ b/src/ui/regionToolsMenu/regionMenu.component.ts @@ -0,0 +1,52 @@ +import {AfterViewInit, Component, Input, ViewChild} from "@angular/core"; +import {Observable} from "rxjs"; +import {map, withLatestFrom} from "rxjs/operators"; +import {FixedMouseContextualContainerDirective} from "src/util/directives/FixedMouseContextualContainerDirective.directive"; +import {VIEWER_STATE_ACTION_TYPES} from "src/services/effect/effect"; +import {CHANGE_NAVIGATION, generateLabelIndexId} from "src/services/stateStore.service"; +import {ADD_TO_REGIONS_SELECTION_WITH_IDS} from "src/services/state/viewerState.store"; +import {Store} from "@ngrx/store"; + +@Component({ + selector: 'region-menu', + templateUrl: './regionMenu.template.html', + styleUrls: ['./regionMenu.style.css'] +}) +export class RegionMenuComponent { + @Input() selectedRegions$: any + @Input() region: any + + regionToolsMenuVisible = false + collapsedRegionDescription = false + + constructor(private store$: Store<any>) {} + + + toggleRegionWithId(ngId, labelIndex, removeFlag: any){ + if (removeFlag) { + this.store$.dispatch({ + type: VIEWER_STATE_ACTION_TYPES.DESELECT_REGIONS_WITH_ID, + deselecRegionIds: [generateLabelIndexId({ ngId, labelIndex })] + }) + } else { + this.store$.dispatch({ + type: ADD_TO_REGIONS_SELECTION_WITH_IDS, + selectRegionIds : [generateLabelIndexId({ ngId, labelIndex })] + }) + } + } + + regionIsSelected(selectedRegions, ngId, labelIndex) { + return selectedRegions.map(sr => generateLabelIndexId({ ngId: sr.ngId, labelIndex: sr.labelIndex })).includes(generateLabelIndexId({ ngId, labelIndex })) + } + + navigateTo(position){ + this.store$.dispatch({ + type: CHANGE_NAVIGATION, + navigation: { + position, + animation: {} + } + }) + } +} \ No newline at end of file diff --git a/src/ui/regionToolsMenu/regionMenu.style.css b/src/ui/regionToolsMenu/regionMenu.style.css new file mode 100644 index 0000000000000000000000000000000000000000..fc202344466f16355a9dd257690a17f7e56c151c --- /dev/null +++ b/src/ui/regionToolsMenu/regionMenu.style.css @@ -0,0 +1,19 @@ +.regionDescriptionTextClass{ + max-height:100px; + transition: max-height 0.15s ease-out; +} +.regionDescriptionTextOpened { + max-height: 310px; + transition: max-height 0.25s ease-in; +} + +[fixedMouseContextualContainerDirective] +{ + width: 15rem; +} + +[fixedMouseContextualContainerDirective] div[body] +{ + overflow: hidden; +} + diff --git a/src/ui/regionToolsMenu/regionMenu.template.html b/src/ui/regionToolsMenu/regionMenu.template.html new file mode 100644 index 0000000000000000000000000000000000000000..eeaed9f0a9dbaba6a91e09f4a4e50937b7638301 --- /dev/null +++ b/src/ui/regionToolsMenu/regionMenu.template.html @@ -0,0 +1,50 @@ +<div class="text-nowrap text-truncate mt-2 ml-2 mr-2 overflow-hidden" [matTooltip]="region.name" + matTooltipShowDelay="1000">{{region.name}}</div> +<!-- ToDo implement it with Descriptions--> +<!-- <div > <!–*ngIf="!region.description && !region.description.length"–>--> +<!-- <div class="row m-2 position-relative overflow-hidden"--> +<!-- [class.regionDescriptionTextOpened] = "collapsedRegionDescription"--> +<!-- [class.overflow-y-auto] = "collapsedRegionDescription"--> +<!-- [class.regionDescriptionTextClass] = "!collapsedRegionDescription"--> +<!-- [class.linear-gradient-fade] = "(+regionDescriptionText.scrollHeight > +regionDescriptionText.clientHeight) && !collapsedRegionDescription"--> +<!-- #regionDescriptionText>--> +<!-- {{region.description}}--> +<!-- </div>--> +<!-- <div (click)="collapsedRegionDescription = true"--> +<!-- *ngIf="+regionDescriptionText.scrollHeight > +regionDescriptionText.clientHeight && !collapsedRegionDescription"--> +<!-- class="w-100 d-flex justify-content-center align-items-center mt-n2 cursorPointer"><i--> +<!-- class="fas fa-angle-down m-1"></i> Read More--> +<!-- </div>--> +<!-- <div (click)="collapsedRegionDescription = false"--> +<!-- *ngIf="collapsedRegionDescription"--> +<!-- class="w-100 d-flex justify-content-center align-items-center mt-n2 cursorPointer"><i--> +<!-- class="fas fa-angle-up m-1"></i> Collapse--> +<!-- </div>--> +<!-- </div>--> +<div class="d-flex align-items-center justify-content-between"> + <button mat-button + style="font-size: 12px" + class="p-0 w-100" + *ngIf="(selectedRegions$ | async) as selectedRegions" + (click)="toggleRegionWithId(region.ngId, region.labelIndex, regionIsSelected(selectedRegions, region.ngId, region.labelIndex)); regionToolsMenuVisible = false; collapsedRegionDescription = false"> + <span class="fa-stack fa-1x"> + <i class="fas fa-hand-pointer fa-stack-1x"></i> + <i class="fas fa-slash fa-stack-1x fa-inverse" + *ngIf="regionIsSelected(selectedRegions, region.ngId, region.labelIndex)"></i> + </span> + <span [innerText]="regionIsSelected(selectedRegions, region.ngId, region.labelIndex)? 'Deselect' : 'Select'"></span> + </button> + <button mat-button + style="font-size: 12px" + class="p-0 w-100" + (click)="navigateTo(region.position); regionToolsMenuVisible = false; collapsedRegionDescription = false"> + <i class="fas fa-crosshairs"></i> Navigate + </button> + + <!-- ToDo Change other buttons font size to 10 if this button is available and remove w-100 classes--> + <!-- <button mat-button--> + <!-- style="font-size: 10px"--> + <!-- class="p-0 mr-1">--> + <!-- <i class="fab fa-connectdevelop"></i> Connectivity--> + <!-- </button>--> +</div> \ No newline at end of file diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 9b8f237729d1bcafe310faab30160358639d16b2..0cbb08e0902e243e0c6ba0b0c5e8c6fab344baca 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -72,6 +72,8 @@ import { CurrentlySelectedRegions } from './viewerStateController/regionsListVie import { RegionTextSearchAutocomplete } from "./viewerStateController/regionSearch/regionSearch.component"; import { RegionsListView } from "./viewerStateController/regionsListView/simpleRegionsListView/regionListView.component"; import {TakeScreenshotComponent} from "src/ui/takeScreenshot/takeScreenshot.component"; +import {RegionMenuComponent} from "src/ui/regionToolsMenu/regionMenu.component"; +import {FixedMouseContextualContainerDirective} from "src/util/directives/FixedMouseContextualContainerDirective.directive"; @NgModule({ imports : [ @@ -119,6 +121,7 @@ import {TakeScreenshotComponent} from "src/ui/takeScreenshot/takeScreenshot.comp RegionTextSearchAutocomplete, RegionsListView, TakeScreenshotComponent, + RegionMenuComponent, /* pipes */ GroupDatasetByRegion, @@ -146,10 +149,12 @@ import {TakeScreenshotComponent} from "src/ui/takeScreenshot/takeScreenshot.comp HumanReadableFileSizePipe, ReorderPanelIndexPipe, + /* directive */ DownloadDirective, TouchSideClass, ElementOutClickDirective, + FixedMouseContextualContainerDirective ], entryComponents : [ @@ -179,6 +184,8 @@ import {TakeScreenshotComponent} from "src/ui/takeScreenshot/takeScreenshot.comp ElementOutClickDirective, SearchSideNav, ViewerStateMini, + RegionMenuComponent, + FixedMouseContextualContainerDirective ] }) diff --git a/src/util/directives/FixedMouseContextualContainerDirective.directive.ts b/src/util/directives/FixedMouseContextualContainerDirective.directive.ts index bf53ed23584df960562ea335dc983a961e102aa0..81736ed6d79d46f9b45971e2f3d1fe13507e1205 100644 --- a/src/util/directives/FixedMouseContextualContainerDirective.directive.ts +++ b/src/util/directives/FixedMouseContextualContainerDirective.directive.ts @@ -25,13 +25,21 @@ export class FixedMouseContextualContainerDirective { } public show(){ - if ((window.innerWidth - this.mousePos[0]) < 220) { - this.mousePos[0] = window.innerWidth-220 - } - this.transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` - this.styleDisplay = 'block' - this.isShown = true - this.onShow.emit() + setTimeout(() => { + if (window.innerHeight - this.mousePos[1] < this.el.nativeElement.clientHeight) { + this.mousePos[1] = window.innerHeight - this.el.nativeElement.clientHeight + } + + if ((window.innerWidth - this.mousePos[0]) < this.el.nativeElement.clientWidth) { + this.mousePos[0] = window.innerWidth-this.el.nativeElement.clientWidth + } + + this.transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` + this.styleDisplay = 'block' + this.isShown = true + this.onShow.emit() + + }) } public hide(){ @@ -47,14 +55,4 @@ export class FixedMouseContextualContainerDirective { @HostBinding('style.transform') public transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` - @HostListener('document:click', ['$event']) - documentClick(event: MouseEvent){ - if (event.button !== 2) { - if (this.styleDisplay === 'none') - return - if (this.el.nativeElement.contains(event.target)) - return - this.hide() - } - } } \ No newline at end of file diff --git a/src/util/directives/captureClickListener.directive.ts b/src/util/directives/captureClickListener.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd92d61fe6cf9944e5142078b964c4e31044cd55 --- /dev/null +++ b/src/util/directives/captureClickListener.directive.ts @@ -0,0 +1,52 @@ +import {Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output} from "@angular/core"; +import {Observable, Observer, Subscription} from "rxjs"; +import {switchMapTo, takeUntil} from "rxjs/operators"; +import {RegionMenuComponent} from "src/ui/regionToolsMenu/regionMenu.component"; + +@Directive({ + selector: '[iav-captureClickListenerDirective]' +}) + +export class CaptureClickListenerDirective implements OnInit, OnDestroy { + + private subscriptions: Subscription[] = [] + @Output() mapClicked: EventEmitter<any> = new EventEmitter() + @Output() mouseDownEmitter: EventEmitter<any> = new EventEmitter() + + + constructor(private el: ElementRef){} + + ngOnInit(): void { + + // Listen click Events + const mouseDownObs$ = new Observable((observer: Observer<any>) => { + this.el.nativeElement.addEventListener('mousedown', event => observer.next({eventName: 'mousedown', event}), true) + }) as Observable<{eventName: string, event: MouseEvent}> + const mouseMoveObs$ = new Observable((observer: Observer<any>) => { + this.el.nativeElement.addEventListener('mousemove', event => observer.next({eventName: 'mousemove', event}), true) + }) as Observable<{eventName: string, event: MouseEvent}> + const mouseUpObs$ = new Observable((observer: Observer<any>) => { + this.el.nativeElement.addEventListener('mouseup', event => observer.next({eventName: 'mouseup', event}), true) + }) as Observable<{eventName: string, event: MouseEvent}> + + this.subscriptions.push( + mouseDownObs$.subscribe(e => { + this.mouseDownEmitter.emit(e.event) + }), + mouseDownObs$.pipe( + switchMapTo( + mouseUpObs$.pipe( + takeUntil(mouseMoveObs$) + ) + ) + ).subscribe(e => { + this.mapClicked.emit(e.event) + }) + ) + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s=> s.unsubscribe()) + } + +} \ No newline at end of file