From c0e42d9d95a5be4f7843e4aa9f6a7fc0a191ce09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Correnson?= <loic.correnson@cea.fr>
Date: Mon, 26 Feb 2024 18:25:42 +0100
Subject: [PATCH] [ivette/lab] docked components

---
 ivette/src/renderer/Application.tsx |   1 -
 ivette/src/renderer/Laboratory.tsx  | 143 +++++++++++++++++++---------
 ivette/src/renderer/style.css       |  24 +++++
 3 files changed, 122 insertions(+), 46 deletions(-)

diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx
index ffe62cbb4d5..8e55109e8a5 100644
--- a/ivette/src/renderer/Application.tsx
+++ b/ivette/src/renderer/Application.tsx
@@ -88,7 +88,6 @@ export default function Application(): JSX.Element {
         <>{StatusBar}</>
         <Toolbar.Filler />
         <Lab.Dock />
-        <Controller.Stats />
       </Toolbar.ToolBar>
     </Vfill>
   );
diff --git a/ivette/src/renderer/Laboratory.tsx b/ivette/src/renderer/Laboratory.tsx
index 5205bf69e00..f93019bef89 100644
--- a/ivette/src/renderer/Laboratory.tsx
+++ b/ivette/src/renderer/Laboratory.tsx
@@ -82,7 +82,7 @@ const LAB = new States.GlobalState<LabViewState>({
 /* --- Layout Utilities                                                   --- */
 /* -------------------------------------------------------------------------- */
 
-function removeComponent(layout: Layout, compId: compId): Layout
+function removeLayoutComponent(layout: Layout, compId: compId): Layout
 {
   const { A, B, C, D } = layout;
   return {
@@ -93,11 +93,11 @@ function removeComponent(layout: Layout, compId: compId): Layout
   };
 }
 
-function addComponent(
+function addLayoutComponent(
   layout: Layout, compId: compId, p: LayoutPosition
 ): Layout
 {
-  layout = removeComponent(layout, compId);
+  layout = removeLayoutComponent(layout, compId);
   switch(p) {
     case 'A': return { ...layout, A: compId };
     case 'B': return { ...layout, B: compId };
@@ -112,7 +112,7 @@ function addComponent(
   }
 }
 
-function addLayout(m: Layout, view: Ivette.Layout): Layout
+function addLayoutView(m: Layout, view: Ivette.Layout): Layout
 {
   type Unstructured = {
     A ?: compId, B ?: compId, C ?: compId, D ?: compId,
@@ -127,7 +127,7 @@ function addLayout(m: Layout, view: Ivette.Layout): Layout
   return { A, B, C, D };
 }
 
-function fillLayout(m: Layout): Layout
+function completeLayout(m: Layout): Layout
 {
   const { A, B, C, D } = m;
   if (A && B && C && D) return m;
@@ -142,7 +142,7 @@ function fillLayout(m: Layout): Layout
   };
 }
 
-function getPosition(
+function getLayoutPosition(
   layout: Layout, compId: compId
 ): LayoutPosition | undefined
 {
@@ -167,6 +167,19 @@ function getPosition(
 /* --- LabView Actions                                                    --- */
 /* -------------------------------------------------------------------------- */
 
+function copySet<A>(s: Set<A>): Set<A>
+{
+  const r = new Set<A>();
+  s.forEach((a) => r.add(a));
+  return r;
+}
+
+function copyMap<A, B>(m: Map<A, B>): Map<A, B> {
+  const u = new Map<A, B>();
+  m.forEach((v, k) => u.set(k, v));
+  return u;
+}
+
 function setCurrentView(viewId: viewId = ''):void {
   const state = LAB.getValue();
   LAB.setValue({ ...state, sideView: viewId, sideComp: '' });
@@ -179,13 +192,14 @@ function setCurrentComp(compId: compId = ''):void {
 
 function applyView(view: Ivette.ViewLayoutProps): void {
   const state = LAB.getValue();
-  const layout = addLayout(state.layout, view.layout);
-  const panels = state.panels;
-  panels.add(layout.A);
-  panels.add(layout.B);
-  panels.add(layout.C);
-  panels.add(layout.D);
-  LAB.setValue({ ...state, layout, sideView: view.id, sideComp: '' });
+  const layout = addLayoutView(state.layout, view.layout);
+  const panels =
+    copySet(state.panels)
+      .add(layout.A)
+      .add(layout.B)
+      .add(layout.C)
+      .add(layout.D);
+  LAB.setValue({ ...state, panels, layout, sideView: view.id, sideComp: '' });
 }
 
 function applyComponent(
@@ -195,18 +209,32 @@ function applyComponent(
   const state = LAB.getValue();
   const { id, preferredPosition } = comp;
   const pos = at ?? preferredPosition ?? 'D';
-  const layout = addComponent(state.layout, id, pos);
-  state.panels.add(id);
-  LAB.setValue({ ...state, layout, sideView: '', sideComp: id });
+  const layout = addLayoutComponent(state.layout, id, pos);
+  const panels = copySet(state.panels).add(id);
+  LAB.setValue({ ...state, panels, layout, sideView: '', sideComp: id });
+}
+
+function dockComponent(comp: Ivette.ComponentProps): void
+{
+  const { id, preferredPosition } = comp;
+  const state = LAB.getValue();
+  const pos = getLayoutPosition(state.layout, id) ?? preferredPosition ?? 'D';
+  const layout = removeLayoutComponent(state.layout, id);
+  const docked = copyMap(state.docked).set(id, pos);
+  LAB.setValue({ ...state, docked, layout, sideView: '', sideComp: id });
 }
 
 function closeComponent(compId: compId): void
 {
   const state = LAB.getValue();
-  const layout = removeComponent(state.layout, compId);
-  state.panels.delete(compId);
-  state.docked.delete(compId);
-  LAB.setValue({ ...state, layout, sideView: '', sideComp: compId });
+  const layout = removeLayoutComponent(state.layout, compId);
+  const panels = copySet(state.panels);
+  const docked = copyMap(state.docked);
+  panels.delete(compId);
+  docked.delete(compId);
+  LAB.setValue({
+    ...state, panels, docked, layout, sideView: '', sideComp: compId
+  });
 }
 
 /* -------------------------------------------------------------------------- */
@@ -264,7 +292,7 @@ function Quarter(props: QuarterProps): JSX.Element {
     const comp = COMPONENT.getElement(compId);
     if (comp) applyComponent(comp, pos);
   };
-  const curp = getPosition(layout, compId);
+  const curp = getLayoutPosition(layout, compId);
   return (
     <IconButton
       className='labview-layout-quarter'
@@ -300,6 +328,8 @@ function LayoutMenu(): JSX.Element | null {
   const divElt = href.current;
   const [menu] = States.useGlobalState(MENU);
   const [state] = States.useGlobalState(LAB);
+  const [panelWidth, setWidth] = React.useState(80);
+  const [panelHeight, setHeight] = React.useState(80);
   const layout = state.layout;
   const { compId, dock, close } = menu;
   const display = compId !== '';
@@ -310,6 +340,11 @@ function LayoutMenu(): JSX.Element | null {
     }
   }, [display, divElt]);
 
+  const width = Math.max(divElt?.offsetWidth ?? 0, panelWidth);
+  const height = Math.max(divElt?.offsetHeight ?? 0, panelHeight);
+  React.useEffect(() => setWidth(width), [width]);
+  React.useEffect(() => setHeight(height), [height]);
+
   const className = classes(
     'dome-color-frame',
     'labview-layout-menu',
@@ -318,17 +353,16 @@ function LayoutMenu(): JSX.Element | null {
 
   const maxWidth = window.innerWidth;
   const maxHeight = window.innerHeight;
-  const panelWidth = divElt?.offsetWidth ?? 0;
-  const panelHeight = divElt?.offsetHeight ?? 0;
 
-  const left = Math.max(0, Math.min(menu.x, maxWidth - panelWidth));
-  const top = Math.max(0, Math.min(menu.y, maxHeight - panelHeight));
+  const left = Math.max(0, Math.min(menu.x, maxWidth - width));
+  const top = Math.max(0, Math.min(menu.y, maxHeight - height));
 
   const onDock = (): void => {
     closeMenu();
+    const comp = COMPONENT.getElement(compId);
+    if (comp) dockComponent(comp);
   };
 
-
   const onClose = (): void => {
     closeMenu();
     closeComponent(compId);
@@ -354,10 +388,10 @@ function LayoutMenu(): JSX.Element | null {
         <Quarter compId={compId} layout={layout} pos='CD'   />
         <Quarter compId={compId} layout={layout} pos='D'    />
       </Grid>
-      <Action display={dock}
-              label='Dock' icon='QSPLIT.DOCK' onClick={onDock} />
-      <Action display={close}
-              label='Close' icon='TRASH' onClick={onClose} />
+      <Action
+        display={dock} label='Dock' icon='QSPLIT.DOCK' onClick={onDock} />
+      <Action
+        display={close} label='Close' icon='TRASH' onClick={onClose} />
     </div>
   );
 }
@@ -413,7 +447,7 @@ export function LabView(): JSX.Element {
     (H, V) => LAB.setValue({ ...state, scroll: { H, V } }),
     [state]
   );
-  const { A, B, C, D } = fillLayout(state.layout);
+  const { A, B, C, D } = completeLayout(state.layout);
   const { H, V } = state.scroll;
   const panels : JSX.Element[] = [];
   state.panels.forEach((id) => panels.push(<Pane key={id} compId={id}/>));
@@ -565,7 +599,7 @@ function GroupSection(props: GroupSectionProps): JSX.Element | null {
       <ComponentItem
         key={id}
         comp={comp}
-        position={getPosition(layout, id)}
+        position={getLayoutPosition(layout, id)}
         selected={id === sideComp}
         active={panels.has(id)}
         docked={docked.has(id)}
@@ -627,37 +661,56 @@ Ivette.registerSidebar({
 // --- Docked Components
 // --------------------------------------------------------------------------
 
-const dockActions: Actions = { dock: false, close: true };
-
 interface DockItemProps {
   compId: compId;
-  pos: LayoutPosition;
+  enabled: boolean;
+  position: LayoutPosition;
 }
 
-function DockItem(props: DockItemProps): JSX.Element {
-  const { compId: id, pos } = props;
-  const comp = Ext.useElement(COMPONENT, id);
-  const label = comp?.label ?? id;
-  const icon = 'QSPLIT.' + pos;
+function DockItem(props: DockItemProps): JSX.Element | null {
+  const { compId, enabled, position } = props;
+  const comp = Ext.useElement(COMPONENT, compId);
+  if (comp === undefined) return null;
+  const label = comp.label ?? compId;
+  const icon = 'QSPLIT.' + position;
   const title = `Display ${label} (right-click for more actions)`;
-  const onContextMenu = (_: void, evt: React.MouseEvent): void => {
-    openLayoutMenu(id, dockActions, evt);
+
+  const className = classes(
+    'labview-docked', !enabled && 'disabled',
+  );
+
+  const onClick = (): void => {
+    if (enabled) applyComponent(comp, position);
+  };
+
+  const onContextMenu = (evt: React.MouseEvent): void => {
+    openLayoutMenu(compId, { dock: !enabled, close: true }, evt);
   };
+
   return (
-    <Toolbar.Button
+    <Label
+      className={className}
       icon={icon}
       label={label}
       title={title}
+      onClick={onClick}
       onContextMenu={onContextMenu}
     />
   );
 }
 
 export function Dock(): JSX.Element {
-  const [{ docked }] = States.useGlobalState(LAB);
+  const [{ docked, layout }] = States.useGlobalState(LAB);
   const items: JSX.Element[] = [];
   docked.forEach((pos, compId) => {
-    items.push(<DockItem key={compId} compId={compId} pos={pos} />);
+    const curr = getLayoutPosition(layout, compId);
+    items.push(
+      <DockItem
+        key={compId}
+        compId={compId}
+        enabled={curr === undefined}
+        position={curr ?? pos}
+      />);
   });
   return <>{items}</>;
 }
diff --git a/ivette/src/renderer/style.css b/ivette/src/renderer/style.css
index dcb64171d95..0d01daa9249 100644
--- a/ivette/src/renderer/style.css
+++ b/ivette/src/renderer/style.css
@@ -162,4 +162,28 @@
     fill: var(--text);
 }
 
+.labview-docked {
+    font-size: smaller;
+    background: var(--background-softer);
+    padding: 2px 5px 2px 2px;
+    border-radius: 5px;
+}
+
+.labview-docked.disabled {
+    color: var(--disabled-text);
+}
+
+.labview-docked.disabled svg {
+    fill: var(--disabled-text);
+}
+
+.labview-docked:hover {
+    background: var(--background-profound-hover);
+}
+
+.labview-docked svg {
+    bottom: 0px !important;
+    height: 8px;
+}
+
 /* -------------------------------------------------------------------------- */
-- 
GitLab