From c7ebdb10d1a6efab31bf992a87f5edaeebe4ab5b Mon Sep 17 00:00:00 2001 From: Adam Midlik Date: Tue, 9 Jul 2024 17:24:23 +0100 Subject: [PATCH] GenericLeftPanelControls: bind to leftPanelTabName and regionState --- src/app/ui/pdbe-left-panel.tsx | 52 +++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/src/app/ui/pdbe-left-panel.tsx b/src/app/ui/pdbe-left-panel.tsx index bbaf925..719bada 100644 --- a/src/app/ui/pdbe-left-panel.tsx +++ b/src/app/ui/pdbe-left-panel.tsx @@ -11,8 +11,9 @@ import { PluginCommands } from 'Molstar/mol-plugin/commands'; import { PluginContext } from 'Molstar/mol-plugin/context'; import { StateTransform } from 'Molstar/mol-state'; import { ParamDefinition as PD } from 'Molstar/mol-util/param-definition'; +import { sleep } from 'Molstar/mol-util/sleep'; import * as React from 'react'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { PluginCustomState } from '../plugin-custom-state'; import { SegmentTree } from './segment-tree'; @@ -45,7 +46,7 @@ export class PDBeLeftPanelControls extends PluginUIComponent<{}, { tab: PDBeLeft } set = (tab: PDBeLeftPanelTabName) => { - if (this.state.tab === tab) { + if (tab === this.state.tab) { tab = 'none'; // clicking on active tab should collapse panel } this.plugin.behaviors.layout.leftPanelTabName.next(tab as any); // will update state via subscription @@ -242,21 +243,30 @@ function resolveIcon(Icon: React.JSXElementConstructor<{}> | string): React.FC { const NO_TAB = 'none'; -function GenericLeftPanelControls(tabs: TabSpec[], defaultTab: string = NO_TAB): React.ComponentClass<{}> { +export function GenericLeftPanelControls(tabs: TabSpec[], options?: { defaultTab?: string, boundBehavior?: (plugin: PluginContext) => BehaviorSubject, onTabChange?: (plugin: PluginContext, tab: string) => any }): React.ComponentClass<{}> { if (tabs.some(tab => tab.id === NO_TAB)) throw new Error(`Cannot use '${NO_TAB}' as tab id because it is reserved.`); return class _GenericLeftPanelControls extends PluginUIComponent<{}, { tab: string, dirtyTabs: string[] }> { + readonly boundBehavior = options?.boundBehavior?.(this.plugin); readonly state = { - tab: defaultTab, + tab: options?.defaultTab ?? this.boundBehavior?.value ?? NO_TAB, dirtyTabs: [] as string[], }; + setTab = async (tab: string) => { + if (tab === this.state.tab) return; // important to avoid infinite loop when state is bound to `boundBehavior` + this.setDirtyTab(tab, false); + this.setState({ tab }, () => { + this.boundBehavior?.next(tab); + options?.onTabChange?.(this.plugin, tab); + }); + }; toggleTab = (tab: string) => { - if (this.state.tab === tab) { - tab = NO_TAB; // clicking on active tab should collapse panel + if (tab === this.state.tab) { + this.setTab(NO_TAB); + } else { + this.setTab(tab); } - this.setState({ tab }); - // this.plugin.behaviors.layout.leftPanelTabName.next(tab as any); // will update state via subscription }; setDirtyTab = (tab: string, dirty: boolean) => { if (dirty && !this.state.dirtyTabs.includes(tab)) { @@ -265,8 +275,13 @@ function GenericLeftPanelControls(tabs: TabSpec[], defaultTab: string = NO_TAB): this.setState({ dirtyTabs: this.state.dirtyTabs.filter(t => t !== tab) }); // Remove from dirty tabs } }; - componentDidMount(): void { + if (this.boundBehavior) { + if (this.boundBehavior.value !== this.state.tab) { + this.boundBehavior.next(this.state.tab); + } + this.subscribe(this.boundBehavior, tab => this.setTab(tab)); + } for (const tab of tabs) { const dirtySubject = tab.dirtyOn?.(this.plugin); if (dirtySubject) { @@ -275,18 +290,18 @@ function GenericLeftPanelControls(tabs: TabSpec[], defaultTab: string = NO_TAB): }); } } + options?.onTabChange?.(this.plugin, this.state.tab); } render() { const currentTabId = this.state.tab; - this.setDirtyTab(currentTabId as string, false); const currentTab = currentTabId ? tabs.find(t => t.id === currentTabId) : undefined; const CurrentTabHeader = currentTab?.header; const CurrentTabBody = currentTab?.body; const iconForTab = (tab: TabSpec) => { if (tab.showWhen && !tab.showWhen(this.plugin)) return null; - return this.toggleTab(tab.id)} transparent style={{ position: 'relative' }} extraContent={this.state.dirtyTabs.includes(tab.id) ?
: undefined} />; }; @@ -311,6 +326,16 @@ function GenericLeftPanelControls(tabs: TabSpec[], defaultTab: string = NO_TAB): }; } +async function adjustLeftPanelState(plugin: PluginContext, expanded: boolean) { + await sleep(0); // this ensures PluginCommands.Layout.Update runs after componentDidMount, without this the panel will not collapse when defaultTab is none (not sure why) + if (expanded && plugin.layout.state.regionState.left === 'collapsed') { + await PluginCommands.Layout.Update(plugin, { state: { regionState: { ...plugin.layout.state.regionState, left: 'full' } } }); + } + if (!expanded && plugin.layout.state.regionState.left === 'full') { + await PluginCommands.Layout.Update(plugin, { state: { regionState: { ...plugin.layout.state.regionState, left: 'collapsed' } } }); + } +} + export const PDBeLeftPanelControls2 = GenericLeftPanelControls([ { id: 'root', @@ -354,5 +379,6 @@ export const PDBeLeftPanelControls2 = GenericLeftPanelControls([ body: FullSettings, position: 'bottom', }, -]); -// TODO: ensure binding with this.plugin.behaviors.layout.leftPanelTabName, binding with collapsed state, create tab registry and allow only using tab names +], { defaultTab: 'none', boundBehavior: plugin => plugin.behaviors.layout.leftPanelTabName as BehaviorSubject, onTabChange: (plugin, tab) => adjustLeftPanelState(plugin, tab !== NO_TAB) }); + +// TODO: binding with collapsed state - collapsed when tab not found, create tab registry and allow only using tab names