From f4e6682988c02c9dc16adacc71fee3416cfdf5f4 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Fri, 12 Apr 2019 14:21:13 +0200
Subject: [PATCH] feat: enabling disposable widget feat: enable window
 minimisation

---
 .../atlasViewer.apiService.service.ts         | 18 +++++++-
 src/atlasViewer/atlasViewer.component.ts      |  5 +--
 .../atlasViewer.pluginService.service.ts      | 36 +++++++++++++--
 .../widgetUnit/widgetService.service.ts       |  7 +++
 .../widgetUnit/widgetUnit.component.ts        | 15 ++++++-
 .../widgetUnit/widgetUnit.template.html       |  6 +++
 src/plugin_examples/plugin_api.md             |  2 +
 .../databrowserModule/databrowser.service.ts  | 13 +++++-
 src/ui/menuicons/menuicons.component.ts       | 18 ++++++--
 src/ui/menuicons/menuicons.template.html      | 45 +++++++++++++++++++
 10 files changed, 150 insertions(+), 15 deletions(-)

diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts
index 69912b85b..bdaf63c10 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 10ec8c368..0c2c74981 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 6a3d590f6..0ad03718b 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 0e490d289..521d69ac8 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 ef4beb9bd..84b1655d6 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 7fc94cf63..2b32cf50d 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 913e24aef..f71f417d2 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 b700feb5e..35adfb7c6 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 12537ec16..e5da46c7f 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 7b1fe8bc9..8897cbb6b 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
-- 
GitLab