diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index 69912b85bd23c5061dbca866882aba3d2c616b24..bdaf63c10763e0cf58075995cb2f570262a40552 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -7,6 +7,7 @@ import { BsModalService } from "ngx-bootstrap/modal"; import { ModalUnit } from "./modalUnit/modalUnit.component"; import { ModalHandler } from "../util/pluginHandlerClasses/modalHandler"; import { ToastHandler } from "../util/pluginHandlerClasses/toastHandler"; +import { PluginManifest } from "./atlasViewer.pluginService.service"; declare var window @@ -107,6 +108,13 @@ export class AtlasViewerAPIServices{ /* to be overwritten by atlasViewer.component.ts */ getToastHandler : () => { throw new Error('getToast Handler not overwritten by atlasViewer.component.ts') + }, + + /** + * to be overwritten by atlas + */ + launchNewWidget: (manifest) => { + return Promise.reject('Needs to be overwritted') } }, pluginControl : { @@ -119,6 +127,11 @@ export class AtlasViewerAPIServices{ } window['interactiveViewer'] = this.interactiveViewer this.init() + + /** + * TODO debugger debug + */ + window.uiHandle = this.interactiveViewer.uiHandle } private init(){ @@ -164,8 +177,9 @@ export interface InteractiveViewerInterface{ } uiHandle : { - getModalHandler : () => ModalHandler - getToastHandler : () => ToastHandler + getModalHandler: () => ModalHandler + getToastHandler: () => ToastHandler + launchNewWidget: (manifest:PluginManifest) => Promise<any> } pluginControl : { diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 10ec8c368936e00df59fd3756bca317f4482e589..0c2c749811b5318b3f4e2ab6eac2cf1a80c39f05 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -82,8 +82,7 @@ export class AtlasViewer implements OnDestroy, OnInit { public urlService: AtlasViewerURLService, public apiService: AtlasViewerAPIServices, private modalService: BsModalService, - private databrowserService: DatabrowserService, - private injector: Injector + private databrowserService: DatabrowserService ) { this.ngLayerNames$ = this.store.pipe( select('viewerState'), @@ -352,7 +351,7 @@ export class AtlasViewer implements OnDestroy, OnInit { private selectedTemplate: any searchRegion(regions:any[]){ this.rClContextualMenu.hide() - this.databrowserService.createDatabrowser({ regions, parcellation: this.selectedParcellation, template: this.selectedTemplate }) + this.databrowserService.queryData({ regions, parcellation: this.selectedParcellation, template: this.selectedTemplate }) } @HostBinding('attr.version') diff --git a/src/atlasViewer/atlasViewer.pluginService.service.ts b/src/atlasViewer/atlasViewer.pluginService.service.ts index 6a3d590f639535b9f3fb80e1298ccc599307ac54..0ad03718bd285ce3d53cb48cb99ce06707269619 100644 --- a/src/atlasViewer/atlasViewer.pluginService.service.ts +++ b/src/atlasViewer/atlasViewer.pluginService.service.ts @@ -10,6 +10,7 @@ import '../res/css/plugin_styles.css' import { interval } from "rxjs"; import { take, takeUntil } from "rxjs/operators"; import { Store } from "@ngrx/store"; +import { WidgetUnit } from "./widgetUnit/widgetUnit.component"; @Injectable({ providedIn : 'root' @@ -31,6 +32,14 @@ export class PluginServices{ ){ this.pluginUnitFactory = this.cfr.resolveComponentFactory( PluginUnit ) + this.apiService.interactiveViewer.uiHandle.launchNewWidget = (manifest) => this.launchPlugin(manifest) + .then(handler => { + this.orphanPlugins.add(manifest) + handler.onShutdown(() => { + this.orphanPlugins.delete(manifest) + }) + }) + this.atlasDataService.promiseFetchedPluginManifests .then(arr=> @@ -57,14 +66,24 @@ export class PluginServices{ ]) } + public launchedPlugins: Set<string> = new Set() + private mapPluginNameToWidgetUnit: Map<string, WidgetUnit> = new Map() + + pluginMinimised(pluginManifest:PluginManifest){ + return this.widgetService.minimisedWindow.has( this.mapPluginNameToWidgetUnit.get(pluginManifest.name) ) + } + + public orphanPlugins: Set<PluginManifest> = new Set() launchPlugin(plugin:PluginManifest){ if(this.apiService.interactiveViewer.pluginControl[plugin.name]) { console.warn('plugin already launched. blinking for 10s.') this.apiService.interactiveViewer.pluginControl[plugin.name].blink(10) - return + const wu = this.mapPluginNameToWidgetUnit.get(plugin.name) + this.widgetService.minimisedWindow.delete(wu) + return Promise.reject('plugin already launched') } - this.readyPlugin(plugin) + return this.readyPlugin(plugin) .then(()=>{ const pluginUnit = this.pluginViewContainerRef.createComponent( this.pluginUnitFactory ) /* TODO in v0.2, I used: @@ -112,8 +131,15 @@ export class PluginServices{ title : plugin.displayName || plugin.name }) + this.launchedPlugins.add(plugin.name) + this.mapPluginNameToWidgetUnit.set(plugin.name, widgetCompRef.instance) + const unsubscribeOnPluginDestroy = [] - const shutdownCB = [] + const shutdownCB = [ + () => { + this.launchedPlugins.delete(plugin.name) + } + ] handler.onShutdown = (cb)=>{ if(typeof cb !== 'function'){ @@ -148,6 +174,7 @@ export class PluginServices{ handler.onShutdown(()=>{ unsubscribeOnPluginDestroy.forEach(s=>s.unsubscribe()) delete this.apiService.interactiveViewer.pluginControl[plugin.name] + this.mapPluginNameToWidgetUnit.delete(plugin.name) }) pluginUnit.onDestroy(()=>{ @@ -155,8 +182,9 @@ export class PluginServices{ shutdownCB.pop()() } }) + + return handler }) - .catch(console.error) } } diff --git a/src/atlasViewer/widgetUnit/widgetService.service.ts b/src/atlasViewer/widgetUnit/widgetService.service.ts index 0e490d2890c3de7a6b0e0df47619e29fe2f1690f..521d69ac8356087702807ec34ebeec44204f7888 100644 --- a/src/atlasViewer/widgetUnit/widgetService.service.ts +++ b/src/atlasViewer/widgetUnit/widgetService.service.ts @@ -20,6 +20,8 @@ export class WidgetServices{ private clickedListener : Subscription[] = [] + public minimisedWindow: Set<WidgetUnit> = new Set() + constructor( private cfr:ComponentFactoryResolver, private constantServce:AtlasViewerConstantsServices, @@ -36,6 +38,10 @@ export class WidgetServices{ this.clickedListener.forEach(s=>s.unsubscribe()) } + minimise(wu:WidgetUnit){ + this.minimisedWindow.add(wu) + } + addNewWidget(guestComponentRef:ComponentRef<any>,options?:Partial<WidgetOptionsInterface>):ComponentRef<WidgetUnit>{ const component = this.widgetUnitFactory.create(this.injector) const _option = getOption(options) @@ -83,6 +89,7 @@ export class WidgetServices{ _component.instance.setWidthHeight() this.widgetComponentRefs.add( _component ) + _component.onDestroy(() => this.minimisedWindow.delete(_component.instance)) this.clickedListener.push( _component.instance.clickedEmitter.subscribe((widgetUnit:WidgetUnit)=>{ diff --git a/src/atlasViewer/widgetUnit/widgetUnit.component.ts b/src/atlasViewer/widgetUnit/widgetUnit.component.ts index ef4beb9bdfca4e6bc9f8fa6bfa290c8cb921adce..84b1655d64036495646ffde9ba4fe4238e9d7b2a 100644 --- a/src/atlasViewer/widgetUnit/widgetUnit.component.ts +++ b/src/atlasViewer/widgetUnit/widgetUnit.component.ts @@ -22,6 +22,14 @@ export class WidgetUnit implements OnInit{ @HostBinding('style.height') height : string = this.state === 'docked' ? null : '0px' + @HostBinding('style.display') + get isMinimised(){ + return this.widgetServices.minimisedWindow.has(this) ? 'none' : null + } + /** + * TODO + * upgrade to angular>=7, and use cdk to handle draggable components + */ get transform(){ return this.state === 'floating' ? `translate(${this.position[0]}px, ${this.position[1]}px)` : @@ -48,8 +56,13 @@ export class WidgetUnit implements OnInit{ public titleHTML : string = null public guestComponentRef : ComponentRef<any> - public cf : ComponentRef<WidgetUnit> public widgetServices:WidgetServices + public cf : ComponentRef<WidgetUnit> + + public id: string + constructor(){ + this.id = Date.now().toString() + } ngOnInit(){ this.canBeDocked = typeof this.widgetServices.dockedContainer !== 'undefined' diff --git a/src/atlasViewer/widgetUnit/widgetUnit.template.html b/src/atlasViewer/widgetUnit/widgetUnit.template.html index 7fc94cf63bc6102cda81c4076906d983fa40c869..2b32cf50dba13f635d9bd4bb33b57d95b4af23be 100644 --- a/src/atlasViewer/widgetUnit/widgetUnit.template.html +++ b/src/atlasViewer/widgetUnit/widgetUnit.template.html @@ -19,6 +19,12 @@ </div> </div> <div icons> + <i + (click)="widgetServices.minimise(this)" + class="fas fa-window-minimize" + hoverable> + + </i> <i *ngIf = "canBeDocked && state === 'floating'" (click) = "dock($event)" class = "fas fa-window-minimize" diff --git a/src/plugin_examples/plugin_api.md b/src/plugin_examples/plugin_api.md index 913e24aef20c8c936f3d51cce87d9370d02dadd9..f71f417d2e4bf27fc38a1eb997a93a9f1f112352 100644 --- a/src/plugin_examples/plugin_api.md +++ b/src/plugin_examples/plugin_api.md @@ -140,6 +140,8 @@ window.interactiveViewer - dismissable : allow user dismiss the toast via x - timeout : auto hide (in ms). set to 0 for not auto hide. + - *launchNewWidget(manifest)* returns a Promise. expects a JSON object, with the same key value as a plugin manifest. the *name* key must be unique, or the promise will be rejected. + - pluginControl - *loadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])* Function that loads external libraries. Pass the name of the libraries as an Array of string, and returns a Promise. When promise resolves, the libraries are loaded. **n.b.** while unlikely, there is a possibility that multiple requests to load external libraries in quick succession can cause the promise to resolve before the library is actually loaded. diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index b700feb5e2c1585e9a619f12eefd9815e99fb424..35adfb7c65f1e81697a49001fef129ec0e371118 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -8,6 +8,9 @@ import { map, distinctUntilChanged, debounceTime, filter, tap } from "rxjs/opera import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { FilterDataEntriesByRegion } from "./util/filterDataEntriesByRegion.pipe"; import { NO_METHODS } from "./util/filterDataEntriesByMethods.pipe"; +import { ComponentRef } from "@angular/core/src/render3"; +import { DataBrowser } from "./databrowser/databrowser.component"; +import { WidgetUnit } from "src/atlasViewer/widgetUnit/widgetUnit.component"; const noMethodDisplayName = 'No methods described' @@ -30,7 +33,15 @@ export class DatabrowserService implements OnDestroy{ public darktheme: boolean = false - public createDatabrowser: (arg:{regions:any[], template:any, parcellation:any}) => void + public instantiatedWidgetUnits: WidgetUnit[] = [] + public queryData: (arg:{regions: any[], template:any, parcellation: any}) => void = (arg) => { + const { dataBrowser, widgetUnit } = this.createDatabrowser(arg) + this.instantiatedWidgetUnits.push(widgetUnit.instance) + widgetUnit.onDestroy(() => { + this.instantiatedWidgetUnits = this.instantiatedWidgetUnits.filter(db => db !== widgetUnit.instance) + }) + } + public createDatabrowser: (arg:{regions:any[], template:any, parcellation:any}) => {dataBrowser: ComponentRef<DataBrowser>, widgetUnit:ComponentRef<WidgetUnit>} public getDataByRegion: ({regions, parcellation, template}:{regions:any[], parcellation:any, template: any}) => Promise<DataEntry[]> = ({regions, parcellation, template}) => new Promise((resolve, reject) => { this.lowLevelQuery(template.name, parcellation.name) .then(de => this.filterDEByRegion.transform(de, regions)) diff --git a/src/ui/menuicons/menuicons.component.ts b/src/ui/menuicons/menuicons.component.ts index 12537ec1699a45300e08146fae1d858a93395f79..e5da46c7fccefc6c77fca5afb29f99f69787d278 100644 --- a/src/ui/menuicons/menuicons.component.ts +++ b/src/ui/menuicons/menuicons.component.ts @@ -7,6 +7,7 @@ import { DataBrowser } from "src/ui/databrowserModule/databrowser/databrowser.co import { PluginBannerUI } from "../pluginBanner/pluginBanner.component"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; import { DatabrowserService } from "../databrowserModule/databrowser.service"; +import { PluginServices } from "src/atlasViewer/atlasViewer.pluginService.service"; @Component({ selector: 'menu-icons', @@ -48,11 +49,12 @@ export class MenuIconsBar{ private widgetServices:WidgetServices, private injector:Injector, private constantService:AtlasViewerConstantsServices, - dbService: DatabrowserService, - cfr: ComponentFactoryResolver + public dbService: DatabrowserService, + cfr: ComponentFactoryResolver, + public pluginServices:PluginServices ){ - dbService.createDatabrowser = this.clickSearch.bind(this) + this.dbService.createDatabrowser = this.clickSearch.bind(this) this.dbcf = cfr.resolveComponentFactory(DataBrowser) this.lbcf = cfr.resolveComponentFactory(LayerBrowser) @@ -72,13 +74,21 @@ export class MenuIconsBar{ const title = regions.length > 1 ? `Data associated with ${regions.length} regions` : `Data associated with ${regions[0].name}` - this.widgetServices.addNewWidget(dataBrowser, { + const widgetUnit = this.widgetServices.addNewWidget(dataBrowser, { exitable: true, persistency: true, state: 'floating', title, titleHTML: `<i class="fas fa-search"></i> ${title}` }) + return { + dataBrowser, + widgetUnit + } + } + + public catchError(e) { + } public clickLayer(event: MouseEvent){ diff --git a/src/ui/menuicons/menuicons.template.html b/src/ui/menuicons/menuicons.template.html index 7b1fe8bc9b0f5191a9be3912e419454c5465733e..8897cbb6b517be6e2e1e28c1fc50e1dad82c1454 100644 --- a/src/ui/menuicons/menuicons.template.html +++ b/src/ui/menuicons/menuicons.template.html @@ -45,6 +45,7 @@ </div> <div + *ngIf="false" [ngClass]="isMobile ? 'btnWrapper-lg' : ''" class="btnWrapper"> <div @@ -57,4 +58,48 @@ </i> </div> +</div> + +<div + *ngFor="let manifest of pluginServices.fetchedPluginManifests" + [tooltip]="manifest.displayName || manifest.name" + placement="right" + [ngClass]="isMobile ? 'btnWrapper-lg' : ''" + class="btnWrapper"> + + <div + (click)="pluginServices.launchPlugin(manifest).catch(catchError)" + [ngClass]="!pluginServices.launchedPlugins.has(manifest.name) ? 'btn-outline-secondary' : pluginServices.pluginMinimised(manifest) ? 'btn-outline-info' : 'btn-info'" + class="shadow btn btn-sm rounded-circle"> + {{ (manifest.displayName || manifest.name).slice(0, 1) }} + </div> +</div> + +<div + *ngFor="let manifest of pluginServices.orphanPlugins" + [tooltip]="manifest.displayName || manifest.name" + placement="right" + [ngClass]="isMobile ? 'btnWrapper-lg' : ''" + class="btnWrapper"> + + <div + (click)="pluginServices.launchPlugin(manifest).catch(catchError)" + [ngClass]="pluginServices.pluginMinimised(manifest) ? 'btn-outline-info' : 'btn-info'" + class="shadow btn btn-sm rounded-circle"> + {{ (manifest.displayName || manifest.name).slice(0, 1) }} + </div> +</div> + +<div + *ngFor="let wu of dbService.instantiatedWidgetUnits" + [ngClass]="isMobile ? 'btnWrapper-lg' : ''" + placement="right" + [tooltip]="wu.title" + class="btnWrapper"> + <div + (click)="widgetServices.minimisedWindow.delete(wu)" + [ngClass]="widgetServices.minimisedWindow.has(wu) ? 'btn-outline-info' : 'btn-info'" + class="shadow btn btn-sm rounded-circle"> + <i class="fas fa-search"></i> + </div> </div> \ No newline at end of file