From 669dd6d8ab3ee443af5470fb081f8bb8a3e74ca3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Correnson?= <loic.correnson@cea.fr>
Date: Wed, 13 Mar 2024 18:58:33 +0100
Subject: [PATCH] [ivette/lab] tab keys

---
 ivette/src/renderer/Laboratory.tsx | 243 ++++++++++++++---------------
 1 file changed, 118 insertions(+), 125 deletions(-)

diff --git a/ivette/src/renderer/Laboratory.tsx b/ivette/src/renderer/Laboratory.tsx
index 6d1d191bee7..2a2cab5e855 100644
--- a/ivette/src/renderer/Laboratory.tsx
+++ b/ivette/src/renderer/Laboratory.tsx
@@ -43,13 +43,14 @@ import * as Ext from './Extensions';
 /* --- LabView State                                                      --- */
 /* -------------------------------------------------------------------------- */
 
+type tabKey = string;
 type viewId = string;
 
 interface Split { H: number, V: number }
 interface Layout { A: compId, B: compId, C: compId, D: compId }
 
 interface TabViewState {
-  key: string,
+  key: tabKey, /* viewId@custom for custom, or viewId */
   viewId: viewId,
   custom: number, /* -1: transient, 0: favorite, n: custom */
   split: Split,
@@ -61,8 +62,8 @@ interface LabViewState {
   stack: Layout[];
   panels: Set<compId>;
   docked: Map<compId, LayoutPosition>;
-  tabs: TabViewState[];
-  tabIndex: number;
+  tabs: Map<tabKey, TabViewState>;
+  tabKey: tabKey;
   sideView: viewId; // from Sidebar or TAB selection
   sideComp: compId; // from Sidebar selection
 }
@@ -75,8 +76,8 @@ const LAB = new States.GlobalState<LabViewState>({
   stack: [defaultLayout],
   panels: new Set(),
   docked: new Map(),
-  tabs: [],
-  tabIndex: -1,
+  tabs: new Map(),
+  tabKey: '',
   sideView: '',
   sideComp: '',
 });
@@ -142,11 +143,11 @@ const eLabSettings: Json.Encoder<LabSettings> =
   (s: LabSettings): Json.json => ((s as object) as Json.json);
 
 function labSettings(state: LabViewState): LabSettings {
-  let tabIndex = 0;
   const tabs: TabSettings[] = [];
-  state.tabs.forEach((tab, index) => {
+  let tabIndex = -1;
+  state.tabs.forEach((tab: TabViewState) => {
     if (tab.custom === 0) {
-      if (index === state.tabIndex)
+      if (tab.key === state.tabKey)
         tabIndex = tabs.length;
       tabs.push({
         view: tab.viewId,
@@ -323,41 +324,54 @@ function getLayoutPosition(
 /* --- Tabs Utilities                                                     --- */
 /* -------------------------------------------------------------------------- */
 
-function findTab(tabs: TabViewState[], viewId: viewId) : number
+function previousTab(tabs: Map<tabKey, TabViewState>, key: tabKey):
+  TabViewState | undefined
 {
-  return tabs.findIndex(tab => tab.viewId === viewId && tab.custom <= 0);
+  let prev: tabKey | undefined = undefined;
+  let last: tabKey | undefined = undefined;
+  tabs.forEach(t => {
+    if (t.key === key) prev = last; else last = t.key;
+  });
+  const next = prev || last;
+  return next && tabs.get(next);
 }
 
-function newCustom(tabs: TabViewState[], viewId: viewId): number
+function newCustom(tabs: Map<tabKey, TabViewState>, viewId: viewId): number
 {
-  return 1 + tabs.reduce((n, tab) => (
-    tab.viewId === viewId ? Math.max(n, tab.custom) : n
-  ), 0);
+  let custom = 0;
+  tabs.forEach(tab => {
+    if (tab.viewId === viewId)
+      custom = Math.max(custom, tab.custom);
+  });
+  return custom+1;
 }
 
 function newTab(
-  tabs: TabViewState[],
+  tabs: Map<tabKey, TabViewState>,
   view: Ivette.ViewLayoutProps,
   custom: number,
-): TabViewState[]
+): TabViewState
 {
-  return tabs.concat({
-    key: `${view.id}@${custom < 0 ? 0 : custom}`,
-    viewId: view.id, custom,
+  const { id: viewId } = view;
+  const key = custom > 0 ? `${viewId}@${custom}` : viewId;
+  const tab = {
+    key, viewId, custom,
     split: defaultSplit,
     stack: [makeViewLayout(view.layout)],
-  });
+  };
+  tabs.set(key, tab);
+  return tab;
 }
 
 function saveTab(
-  newTabs: TabViewState[],
+  tabs: Map<tabKey, TabViewState>,
   oldState: LabViewState,
 ): void {
-  const oldIndex = oldState.tabIndex;
-  const toSave = newTabs[oldIndex];
+  const oldKey = oldState.tabKey;
+  const toSave = tabs.get(oldKey);
   if (toSave !== undefined) {
     const { stack, split } = oldState;
-    newTabs[oldIndex] = { ...toSave, stack, split };
+    tabs.set(oldKey, { ...toSave, stack, split });
   }
 }
 
@@ -368,7 +382,6 @@ function addPanels(panels: Set<compId>, layout: Layout): Set<compId>
     return panels;
   else
     return copySet(panels).add(A).add(B).add(C).add(D);
-
 }
 
 /* -------------------------------------------------------------------------- */
@@ -403,14 +416,14 @@ function setCurrentNone(): void {
   LAB.setValue({ ...state, sideComp: '', sideView: '' });
 }
 
-function applyTab(index = -1): void {
+function applyTab(key: tabKey): void {
   const state = LAB.getValue();
-  const old = state.tabIndex;
-  const tab = state.tabs[index];
-  if (tab === undefined) return;
+  const old = state.tabKey;
+  if (old === key) return;
+  const tab = state.tabs.get(key);
+  if (!tab) return;
   const { stack, split } = tab;
-  if (old === index) return;
-  const tabs = [...state.tabs];
+  const tabs = copyMap(state.tabs);
   const layout = stack[0] ?? defaultLayout;
   saveTab(tabs, state);
   const panels = addPanels(state.panels, layout);
@@ -420,103 +433,90 @@ function applyTab(index = -1): void {
     stack,
     split,
     tabs,
-    tabIndex: index,
-  });
-}
-
-function duplicateView(view: Ivette.ViewLayoutProps): void {
-  const state = LAB.getValue();
-  const custom = newCustom(state.tabs, view.id);
-  const tabs = newTab(state.tabs, view, custom);
-  LAB.setValue({
-    ...state,
-    tabs,
+    tabKey: key,
   });
 }
 
-function closeTab(index: number): void {
+function closeTab(key: tabKey): void {
   const state = LAB.getValue();
-  const old = state.tabIndex;
-  const preTabs = state.tabs.slice(0, index);
-  const postTabs = state.tabs.slice(index+1);
-  const newTabs = [ ...preTabs, ...postTabs ];
-  const tabIndex = old > 0 ? old - 1 : 0;
-  const tab = newTabs[tabIndex];
+  const tab = previousTab(state.tabs, key);
+  const tabs = copyMap(state.tabs);
+  tabs.delete(key);
   if (tab === undefined) {
     LAB.setValue({
       ...state,
-      stack: [defaultLayout],
+      stack: [],
       split: defaultSplit,
-      tabs: newTabs,
-      tabIndex: -1,
+      tabs, tabKey: ''
     });
   } else {
-    const { stack, split } = tab;
+    const { key, stack, split } = tab;
     const layout = stack[0] ?? defaultLayout;
     const panels = addPanels(state.panels, layout);
     LAB.setValue({
       ...state,
-      panels,
-      stack,
-      split,
-      tabs: newTabs,
-      tabIndex,
+      panels, stack, split, tabs, tabKey: key
     });
   }
 }
 
+function restoreDefault(key: tabKey): void {
+  const state = LAB.getValue();
+  const tab = state.tabs.get(key);
+  if (!tab) return;
+  const view = VIEW.getElement(tab.viewId);
+  if (!view) return;
+  const layout = makeViewLayout(view.layout);
+  const tabs = copyMap(state.tabs).set(key, { ...tab, stack: [layout] });
+  if (key === state.tabKey) {
+    LAB.setValue({ ...state, tabs, stack: [layout] });
+  } else {
+    LAB.setValue({ ...state, tabs });
+  }
+}
+
 function applyView(view: Ivette.ViewLayoutProps): void {
   const state = LAB.getValue();
   const viewId = view.id;
-  const index = findTab(state.tabs, viewId);
-  if (0 <= index) {
-    applyTab(index);
-  } else {
+  if (state.tabs.has(viewId))
+    applyTab(viewId);
+  else {
     const layout = makeViewLayout(view.layout);
     const panels = addPanels(state.panels, layout);
-    const tabs = newTab(state.tabs, view, -1);
-    const tabIndex = tabs.length - 1;
+    const tabs = copyMap(state.tabs);
+    const tab = newTab(tabs, view, -1);
     saveTab(tabs, state);
     LAB.setValue({
       ...state,
       panels,
       split: defaultSplit,
       stack: [layout],
-      docked: state.docked,
-      tabs,
-      tabIndex,
+      tabs, tabKey: tab.key
     });
   }
 }
 
-function restoreDefault(view: Ivette.ViewLayoutProps): void {
+function duplicateView(view: Ivette.ViewLayoutProps): void {
   const state = LAB.getValue();
-  const viewId = view.id;
-  const index = findTab(state.tabs, viewId);
-  const layout = makeViewLayout(view.layout);
-  const tab = state.tabs[index];
-  const newTabs = [...state.tabs];
-  newTabs[index] = { ...tab, stack: [layout] };
-  if (index === state.tabIndex) {
-    LAB.setValue({ ...state, tabs: newTabs, stack: [layout] });
-  } else {
-    LAB.setValue({ ...state, tabs: newTabs });
-  }
+  const custom = newCustom(state.tabs, view.id);
+  const tabs = copyMap(state.tabs);
+  newTab(tabs, view, custom);
+  LAB.setValue({ ...state, tabs });
 }
 
-function applyFavorite(view: Ivette.ViewLayoutProps, favorite: boolean): void {
+function applyFavorite(
+  view: Ivette.ViewLayoutProps,
+  favorite: boolean
+): void {
   const state = LAB.getValue();
-  const index = findTab(state.tabs, view.id);
-  if (0 <= index) {
-    const tabs = [...state.tabs];
-    const tab = tabs[index];
+  const tab = state.tabs.get(view.id);
+  if (tab) {
     const custom = favorite ? 0 : -1;
-    if (tab.custom !== custom) {
-      tabs[index] = { ...tab, custom };
-      LAB.setValue({ ...state, tabs });
-    }
+    const tabs = copyMap(state.tabs).set(tab.key, { ...tab, custom });
+    LAB.setValue({ ...state, tabs });
   } else if (favorite) {
-    const tabs = newTab(state.tabs, view, 0);
+    const tabs = copyMap(state.tabs);
+    newTab(tabs, view, 0);
     LAB.setValue({ ...state, tabs });
   }
 }
@@ -609,12 +609,9 @@ Settings.onWindowSettings(() => {
       });
       if (gotoView !== undefined) {
         const state = LAB.getValue();
-        if (state.tabIndex < 0) {
-          const tabIndex = findTab(state.tabs, gotoView);
-          if (0 <= tabIndex) {
-            applyTab(tabIndex);
-            setCurrentNone();
-          }
+        if (!state.tabKey) {
+          applyTab(gotoView);
+          setCurrentNone();
         }
       }
     } finally {
@@ -898,7 +895,7 @@ function ViewItem(props: ViewItemProps): JSX.Element {
     setCurrentView(id);
     const onDisplay = (): void => applyView(view);
     const onFavorite = (): void => applyFavorite(view, !favorite);
-    const onRestore = (): void => restoreDefault(view);
+    const onRestore = (): void => restoreDefault(view.id);
     const onDuplicate = (): void => duplicateView(view);
     const favAction = !favorite ? 'Add to Favorite' : 'Remove from Favorite';
     Dome.popupMenu([
@@ -924,16 +921,13 @@ function ViewItem(props: ViewItemProps): JSX.Element {
 
 function ViewSection(): JSX.Element {
   const views = Ext.useElements(VIEW);
-  const [state] = States.useGlobalState(LAB);
-  const { tabs, tabIndex } = state;
-
+  const [{ tabs, tabKey, sideView, stack }] = States.useGlobalState(LAB);
   const items = views.map((view) => {
     const { id } = view;
-    const index = findTab(state.tabs, id);
-    const favorite = 0 <= index && tabs[index].custom === 0;
-    const displayed = 0 <= index && index === tabIndex;
-    const tab = 0 <= index ? tabs[index] : undefined;
-    const layout = displayed ? state.stack[0] : tab?.stack[0];
+    const tab = tabs.get(id);
+    const favorite = tab ? tab.custom === 0 : false;
+    const displayed = tab ? tab.key === tabKey : false;
+    const layout = displayed ? stack[0] : tab?.stack[0];
     return (
       <ViewItem
         key={id}
@@ -941,7 +935,7 @@ function ViewSection(): JSX.Element {
         favorite={favorite}
         layout={layout}
         displayed={displayed}
-        selected={id === state.sideView} />
+        selected={id === sideView} />
     );
   });
 
@@ -1166,17 +1160,16 @@ export function Dock(): JSX.Element {
 
 interface TabViewProps {
   tab: TabViewState;
-  index: number;
-  tabIndex: number;
+  tabKey: tabKey;
   layout: Layout;
 }
 
 function TabView(props: TabViewProps): JSX.Element | null {
-  const { tab, index, tabIndex } = props;
-  const { viewId, custom } = tab;
+  const { tab, tabKey } = props;
+  const { viewId, custom, key } = tab;
   const view = Ext.useElement(VIEW, viewId);
   if (!view) return null;
-  const selected = index === tabIndex;
+  const selected = key === tabKey;
   const top = tab.stack[0] ?? defaultLayout;
   const layout = selected ? props.layout : top;
   const modified = !compareLayout(layout, makeViewLayout(view.layout));
@@ -1188,12 +1181,12 @@ function TabView(props: TabViewProps): JSX.Element | null {
   const tmod = modified ? ' (modified)': '';
   const title = tdup + vname + tmod;
 
-  const onClick = (): void => { applyTab(index); setCurrentNone(); };
-  const onClose = (): void => closeTab(index);
+  const onClick = (): void => { applyTab(key); setCurrentNone(); };
+  const onClose = (): void => closeTab(key);
   const onContextMenu = (): void => {
-    const onDisplay = (): void => applyTab(index);
+    const onDisplay = (): void => applyTab(key);
     const onFavorite = (): void => applyFavorite(view, !favorite);
-    const onRestore = (): void => restoreDefault(view);
+    const onRestore = (): void => restoreDefault(key);
     const favAction = !favorite ? 'Add to Favorite' : 'Remove from Favorite';
     Dome.popupMenu([
       { label: 'Display View', enabled: !selected, onClick: onDisplay },
@@ -1209,8 +1202,7 @@ function TabView(props: TabViewProps): JSX.Element | null {
       icon={selected ? 'DISPLAY' : undefined}
       label={label}
       title={title}
-      value={index}
-      selection={tabIndex}
+      selected={selected}
       onClick={onClick}
       onContextMenu={onContextMenu}
     >
@@ -1225,16 +1217,17 @@ function TabView(props: TabViewProps): JSX.Element | null {
 }
 
 export function Tabs(): JSX.Element {
-  const [{ tabIndex, stack, tabs }] = States.useGlobalState(LAB);
+  const [{ tabKey, stack, tabs }] = States.useGlobalState(LAB);
   const layout = stack[0] ?? defaultLayout;
-  const items = tabs.map((tab: TabViewState, k: number) => (
-    <TabView
-      key={tab.key}
-      tab={tab}
-      index={k}
-      tabIndex={tabIndex}
-      layout={layout}
-    />
+  const items: JSX.Element[] = [];
+  tabs.forEach((tab: TabViewState) =>
+    items.push(
+      <TabView
+        key={tab.key}
+        tab={tab}
+        tabKey={tabKey}
+        layout={layout}
+      />
   ));
   return <>{items}</>;
 }
-- 
GitLab