From e93e78f15800b63fc2e7dbdbb3e532c10cd38843 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:24:28 +0800 Subject: [PATCH] add: persist ui layout, pinned pane, and outline mode resolve: #949 --- src/api.ts | 6 ++- src/elements/workspace/contextPane.ts | 5 ++- src/elements/workspace/detailsPane.ts | 63 +++++++++++++++++++++++++++ src/elements/workspace/outlinePane.ts | 46 ++++++++++++++++++- src/elements/workspace/workspace.ts | 59 +++++++++++++++++++++++++ src/modules/workspace/content.ts | 3 +- src/modules/workspace/tab.ts | 2 +- src/modules/workspace/window.ts | 3 +- src/utils/prefs.ts | 24 ++++++++++ src/utils/workspace.ts | 24 ++++++++-- 10 files changed, 224 insertions(+), 11 deletions(-) diff --git a/src/api.ts b/src/api.ts index 64b381f..09ecf5f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -79,8 +79,12 @@ import { linkAnnotationToTarget, updateNoteLinkRelation, } from "./utils/relation"; +import { getWorkspaceByTabID, getWorkspaceByUID } from "./utils/workspace"; -const workspace = {}; +const workspace = { + getWorkspaceByTabID, + getWorkspaceByUID, +}; const sync = { isSyncNote, diff --git a/src/elements/workspace/contextPane.ts b/src/elements/workspace/contextPane.ts index 60cb271..2fe6e93 100644 --- a/src/elements/workspace/contextPane.ts +++ b/src/elements/workspace/contextPane.ts @@ -1,10 +1,11 @@ import { config } from "../../../package.json"; import { PluginCEBase } from "../base"; +import { DetailsPane } from "./detailsPane"; export class ContextPane extends PluginCEBase { _item?: Zotero.Item; - _details!: any; + _details!: DetailsPane; _sidenav: any; get item() { @@ -31,7 +32,7 @@ export class ContextPane extends PluginCEBase { } init(): void { - this._details = this._queryID("container"); + this._details = this._queryID("container") as unknown as DetailsPane; this._sidenav = this._queryID("sidenav"); } diff --git a/src/elements/workspace/detailsPane.ts b/src/elements/workspace/detailsPane.ts index 6692ccb..e578241 100644 --- a/src/elements/workspace/detailsPane.ts +++ b/src/elements/workspace/detailsPane.ts @@ -1,8 +1,30 @@ import { config } from "../../../package.json"; +import { + getPrefJSON, + registerPrefObserver, + setPref, + unregisterPrefObserver, +} from "../../utils/prefs"; + const ItemDetails = document.createXULElement("item-details") .constructor! as any; +const persistKey = "persist.workspaceContext"; + export class DetailsPane extends ItemDetails { + _prefObserverID!: symbol; + + get pinnedPane() { + // @ts-ignore super + return super.pinnedPane; + } + + set pinnedPane(val) { + // @ts-ignore super + super.pinnedPane = val; + this._persistState(); + } + content = MozXULElement.parseXULToFragment(` (elem.parentElement!.hidden = true)); super.forceUpdateSideNav(); } + + _restorePinnedPane() {} + + _persistState() { + let state = getPrefJSON(persistKey); + + if (state?.pinnedPane === this.pinnedPane) { + return; + } + + state = { + ...state, + pinnedPane: this.pinnedPane, + }; + + setPref(persistKey, JSON.stringify(state)); + } + + _restoreState() { + const state = getPrefJSON(persistKey); + + console.trace("Restore State", state); + + this.pinnedPane = state?.pinnedPane; + this.scrollToPane(this.pinnedPane); + } } diff --git a/src/elements/workspace/outlinePane.ts b/src/elements/workspace/outlinePane.ts index 683cc09..648d029 100644 --- a/src/elements/workspace/outlinePane.ts +++ b/src/elements/workspace/outlinePane.ts @@ -4,9 +4,17 @@ import { formatPath } from "../../utils/str"; import { waitUtilAsync } from "../../utils/wait"; import { OutlineType } from "../../utils/workspace"; import { PluginCEBase } from "../base"; -import { getPref } from "../../utils/prefs"; +import { + getPref, + getPrefJSON, + registerPrefObserver, + setPref, + unregisterPrefObserver, +} from "../../utils/prefs"; import { showHintWithLink } from "../../utils/hint"; +const persistKey = "persist.workspaceOutline"; + export class OutlinePane extends PluginCEBase { _outlineType: OutlineType = OutlineType.empty; _item?: Zotero.Item; @@ -15,6 +23,8 @@ export class OutlinePane extends PluginCEBase { _outlineContainer!: HTMLIFrameElement; _notifierID!: string; + _prefObserverID!: symbol; + static outlineSources = [ "", `chrome://${config.addonRef}/content/treeView.html`, @@ -108,6 +118,7 @@ export class OutlinePane extends PluginCEBase { } this._outlineType = newType; + this._persistState(); } get item() { @@ -139,9 +150,15 @@ export class OutlinePane extends PluginCEBase { ["item"], "attachmentsBox", ); + + this._prefObserverID = registerPrefObserver( + persistKey, + this._restoreState.bind(this), + ); } destroy(): void { + unregisterPrefObserver(this._prefObserverID); Zotero.Notifier.unregisterObserver(this._notifierID); this._outlineContainer.contentWindow?.removeEventListener( "message", @@ -165,6 +182,7 @@ export class OutlinePane extends PluginCEBase { } async render() { + this._restoreState(); if (this.outlineType === OutlineType.empty) { this.outlineType = OutlineType.treeView; } @@ -380,4 +398,30 @@ export class OutlinePane extends PluginCEBase { return; } }; + + _persistState() { + let state = getPrefJSON(persistKey); + + if (state?.outlineType === this.outlineType) { + return; + } + + state = { + ...state, + outlineType: this.outlineType, + }; + + setPref(persistKey, JSON.stringify(state)); + } + + _restoreState() { + const state = getPrefJSON(persistKey); + if ( + typeof state.outlineType === "number" && + state.outlineType !== this.outlineType + ) { + this.outlineType = state.outlineType; + this.updateOutline(); + } + } } diff --git a/src/elements/workspace/workspace.ts b/src/elements/workspace/workspace.ts index 4fc6046..17025d4 100644 --- a/src/elements/workspace/workspace.ts +++ b/src/elements/workspace/workspace.ts @@ -1,17 +1,26 @@ import { config } from "../../../package.json"; +import { getPrefJSON, registerPrefObserver, setPref, unregisterPrefObserver } from "../../utils/prefs"; import { waitUtilAsync } from "../../utils/wait"; import { PluginCEBase } from "../base"; import { ContextPane } from "./contextPane"; import { OutlinePane } from "./outlinePane"; +const persistKey = "persist.workspace"; + export class Workspace extends PluginCEBase { uid: string = Zotero.Utilities.randomString(8); _item?: Zotero.Item; + _prefObserverID!: symbol; + _editorElement!: EditorElement; _outline!: OutlinePane; + _editorContainer!: XUL.Box; _context!: ContextPane; + _leftSplitter!: XUL.Splitter; + _rightSplitter!: XUL.Splitter; + resizeOb!: ResizeObserver; get content() { @@ -77,11 +86,23 @@ export class Workspace extends PluginCEBase { this._addon.data.workspace.instances[this.uid] = new WeakRef(this); this._outline = this._queryID("left-container") as unknown as OutlinePane; + + this._editorContainer = this._queryID("center-container") as XUL.Box; this._editorElement = this._queryID("editor-main") as EditorElement; this._outline._editorElement = this._editorElement; this._context = this._queryID("right-container") as unknown as ContextPane; + this._leftSplitter = this._queryID("left-splitter") as XUL.Splitter; + this._rightSplitter = this._queryID("right-splitter") as XUL.Splitter; + + this._leftSplitter.addEventListener("mouseup", () => { + this._persistState(); + }); + this._rightSplitter.addEventListener("mouseup", () => { + this._persistState(); + }); + this.resizeOb = new ResizeObserver(() => { if (!this.editor) return; this._addon.api.editor.scroll( @@ -90,9 +111,15 @@ export class Workspace extends PluginCEBase { ); }); this.resizeOb.observe(this._editorElement); + + this._prefObserverID = registerPrefObserver( + persistKey, + this._restoreState.bind(this), + ); } destroy(): void { + unregisterPrefObserver(this._prefObserverID); this.resizeOb.disconnect(); delete this._addon.data.workspace.instances[this.uid]; } @@ -101,6 +128,8 @@ export class Workspace extends PluginCEBase { await this._outline.render(); await this.updateEditor(); await this._context.render(); + + this._restoreState(); } async updateEditor() { @@ -125,4 +154,34 @@ export class Workspace extends PluginCEBase { this._addon.api.editor.scrollToSection(this.editor, options.sectionName); } } + + _persistState() { + const state = { + leftState: this._leftSplitter.getAttribute("state"), + rightState: this._rightSplitter.getAttribute("state"), + leftWidth: window.getComputedStyle(this._outline)?.width, + centerWidth: window.getComputedStyle(this._editorContainer)?.width, + rightWidth: window.getComputedStyle(this._context)?.width, + }; + setPref(persistKey, JSON.stringify(state)); + } + + _restoreState() { + const state = getPrefJSON(persistKey); + if (typeof state.leftState === "string") { + this._leftSplitter.setAttribute("state", state.leftState); + } + if (typeof state.rightState === "string") { + this._rightSplitter.setAttribute("state", state.rightState); + } + if (state.leftWidth) { + this._outline.style.width = state.leftWidth; + } + if (state.centerWidth) { + this._editorContainer.style.width = state.centerWidth; + } + if (state.rightWidth) { + this._context.style.width = state.rightWidth; + } + } } diff --git a/src/modules/workspace/content.ts b/src/modules/workspace/content.ts index 52636f2..99b2c09 100644 --- a/src/modules/workspace/content.ts +++ b/src/modules/workspace/content.ts @@ -1,3 +1,4 @@ +import { Workspace } from "../../elements/workspace/workspace"; import { waitUtilAsync } from "../../utils/wait"; export async function initWorkspace(container: XUL.Box, item: Zotero.Item) { @@ -14,7 +15,7 @@ export async function initWorkspace(container: XUL.Box, item: Zotero.Item) { await waitUtilAsync(() => !!customElements.get("bn-workspace")); - const workspace = new (customElements.get("bn-workspace")!)() as any; + const workspace = new (customElements.get("bn-workspace")!)() as Workspace; container.append(workspace); workspace.item = item; workspace.containerType = "tab"; diff --git a/src/modules/workspace/tab.ts b/src/modules/workspace/tab.ts index ae7863a..5c9d450 100644 --- a/src/modules/workspace/tab.ts +++ b/src/modules/workspace/tab.ts @@ -28,7 +28,7 @@ export async function openWorkspaceTab( onClose: () => {}, }); const workspace = await initWorkspace(container, item); - workspace.scrollEditorTo({ + workspace?.scrollEditorTo({ lineIndex, sectionName, }); diff --git a/src/modules/workspace/window.ts b/src/modules/workspace/window.ts index f90f75c..b284384 100644 --- a/src/modules/workspace/window.ts +++ b/src/modules/workspace/window.ts @@ -19,10 +19,9 @@ export async function openWorkspaceWindow( "#workspace-container", ) as XUL.Box; const workspace = await addon.hooks.onInitWorkspace(container, item); - workspace.scrollEditorTo(options); + workspace?.scrollEditorTo(options); win.focus(); - // @ts-ignore win.updateTitle(); return win; } diff --git a/src/utils/prefs.ts b/src/utils/prefs.ts index 807277e..dc99072 100644 --- a/src/utils/prefs.ts +++ b/src/utils/prefs.ts @@ -11,3 +11,27 @@ export function setPref(key: string, value: string | number | boolean) { export function clearPref(key: string) { return Zotero.Prefs.clear(`${config.prefsPrefix}.${key}`, true); } + +export function getPrefJSON(key: string) { + try { + return JSON.parse(String(getPref(key) || "{}")); + } catch (e) { + setPref(key, "{}"); + } + return {}; +} + +export function registerPrefObserver( + key: string, + callback: (value: any) => void, +) { + return Zotero.Prefs.registerObserver( + `${config.prefsPrefix}.${key}`, + callback, + true, + ); +} + +export function unregisterPrefObserver(observerID: symbol) { + return Zotero.Prefs.unregisterObserver(observerID); +} diff --git a/src/utils/workspace.ts b/src/utils/workspace.ts index 3499089..1eb32c5 100644 --- a/src/utils/workspace.ts +++ b/src/utils/workspace.ts @@ -1,15 +1,33 @@ -export enum OutlineType { +import { Workspace } from "../elements/workspace/workspace"; + +export { getWorkspaceByTabID, getWorkspaceByUID, OutlineType }; + +enum OutlineType { empty = 0, treeView, mindMap, bubbleMap, } -export function getWorkspaceByUID(uid: string): HTMLElement | undefined { +function getWorkspaceByUID(uid: string): Workspace | undefined { const workspace = addon.data.workspace.instances[uid]?.deref(); if (!workspace?.ownerDocument) { delete addon.data.workspace.instances[uid]; return undefined; } - return workspace; + return workspace as Workspace; +} + +function getWorkspaceByTabID(tabID?: string): Workspace | undefined { + const win = Zotero.getMainWindow(); + if (!tabID) { + const _Zotero_Tabs = win.Zotero_Tabs as typeof Zotero_Tabs; + if (_Zotero_Tabs.selectedType !== "note") return; + tabID = Zotero_Tabs.selectedID; + } + const workspace = Zotero.getMainWindow().document.querySelector( + `#${tabID} > bn-workspace`, + ); + if (!workspace) return; + return workspace as Workspace; }