add: persist ui layout, pinned pane, and outline mode

resolve: #949
This commit is contained in:
windingwind 2024-06-27 12:24:28 +08:00
parent 744e2df5a0
commit e93e78f158
10 changed files with 224 additions and 11 deletions

View File

@ -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,

View File

@ -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");
}

View File

@ -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(`
<linkset>
<html:link
@ -25,13 +47,54 @@ export class DetailsPane extends ItemDetails {
init() {
MozXULElement.insertFTLIfNeeded(`${config.addonRef}-notePreview.ftl`);
MozXULElement.insertFTLIfNeeded(`${config.addonRef}-noteRelation.ftl`);
this._prefObserverID = registerPrefObserver(
persistKey,
this._restoreState.bind(this),
);
super.init();
}
destroy() {
unregisterPrefObserver(this._prefObserverID);
super.destroy();
}
render() {
super.render();
this._restoreState();
}
forceUpdateSideNav() {
this._sidenav
.querySelectorAll("toolbarbutton")
.forEach((elem: HTMLElement) => (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);
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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";

View File

@ -28,7 +28,7 @@ export async function openWorkspaceTab(
onClose: () => {},
});
const workspace = await initWorkspace(container, item);
workspace.scrollEditorTo({
workspace?.scrollEditorTo({
lineIndex,
sectionName,
});

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}