Obsidian UI 技术原理
4 min read

Obsidian UI 技术原理

Obsidian 有比较复杂的 UI 设计,为了能够更好的进行个性化定制和插件开发,UI 元素相关的技术细节值得深入研究,最后附上基于 UI 操作的 local graph 快捷工具的演示
Obsidian UI 技术原理
Photo by Kelly Sikkema on Unsplash

在插件开发的过程中发现 Obsidian 有比较复杂的 UI 设计,不同的功能被归属到不同的类中,同时为了方便管理还引入了 workspace、leaf 等概念。为了能够更加高效的进行 Obsidian 插件开发和个性化定制,有必要对这一技术细节进行梳理总结。

UI 概览

Obsidian UI 中主要分为:

  1. Ribbon,上面主要是一些功能/插件的按钮
  2. Right Sidebar/Left Sidebar,左右侧边栏
  3. Tab view,主要的显示区域
  4. Editor,是 view 的成员,用来与 CodeMirror 交互操作 markdown 文本
  5. Status bar,是右下角的状态栏
overview of Obsidian UI

workspace

与 VS Code 类似,Obsidian 支持在任何时候配置哪些内容是否可见,例如在不需要时可以配置隐藏文件资源管理器,例如可以并排显示多个文档。整个 Obsidian 应用程序窗口中可见内容的配置称为工作区(workspace)。这就意味着,workspace 中会包含上文提到的所有元素,如 view、editor、sidebar、statusbar 等。

workspace 数据结构

workspace 的底层实现是一个树形结构,树的节点是 WorkspaceItem,它包括两种类型:WorkspaceParentWorkspaceLeaf

WorkspaceParent 与 WorkspaceLeaf 区别

  • WorkspaceParent 可以包括子节点(包括 WorkspaceParent 类型),有两种类型: WorkspaceSplitsWorkspaceTabs
  • WorkspaceLeaf 不包括任何子节点

WorkspaceSplit 与 WorkspaceTabs 区别

  • WorkspaceSplits 沿着垂直或水平方向一个接一个地排列它的子项
  • WorkspaceTabs 一次只显示一个子项,并隐藏其他的子项

workspace 示意图

workspace tree struct of Obsidian

Class 定义

上面提到的 workspace 相关的类型在 Obsidian API 中都有详细的定义,可以参考:obsidian-api/obsidian.d.ts

/**
 * @public
 */
export class WorkspaceLeaf extends WorkspaceItem {
    /**
     * @public
     */
    view: View;
    /**
     * By default, `openFile` will also make the leaf active.
     * Pass in `{ active: false }` to override.
     *
     * @public
     */
    openFile(file: TFile, openState?: OpenViewState): Promise<void>;
    /**
     * @public
     */
    open(view: View): Promise<View>;
    /**
     * @public
     */
    getViewState(): ViewState;
    /**
     * @public
     */
    setViewState(viewState: ViewState, eState?: any): Promise<void>;
    /**
     * @public
     */
    getEphemeralState(): any;
    /**
     * @public
     */
    setEphemeralState(state: any): void;
    /**
     * @public
     */
    togglePinned(): void;
    /**
     * @public
     */
    setPinned(pinned: boolean): void;
    /**
     * @public
     */
    setGroupMember(other: WorkspaceLeaf): void;
    /**
     * @public
     */
    setGroup(group: string): void;
    /**
     * @public
     */
    detach(): void;
    /**
     * @public
     */
    getIcon(): IconName;
    /**
     * @public
     */
    getDisplayText(): string;
    /**
     * @public
     */
    onResize(): void;
    /**
     * @public
     */
    on(name: 'pinned-change', callback: (pinned: boolean) => any, ctx?: any): EventRef;
    /**
     * @public
     */
    on(name: 'group-change', callback: (group: string) => any, ctx?: any): EventRef;
}

view

view 决定了 Obisidian 如何显示内容,文件资源管理器,Graph 视图和 Markdown 视图都是一种视图,在插件开发的过程中如果需要自定义显示的内容还可以创建自己的 ItemViewItemView 继承自 View,关于 View class 的具体定义可以参考:obsidian-api View

自定义 View

如下是一个扩展示例:

import { ItemView, WorkspaceLeaf } from "obsidian";
export const VIEW_TYPE_EXAMPLE = "example-view";

export class ExampleView extends ItemView {
  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }
  getViewType() {
    return VIEW_TYPE_EXAMPLE;
  }
  getDisplayText() {
    return "Example view";
  }
  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    container.createEl("h4", { text: "Example view" });
  }
  async onClose() {
    // Nothing to clean up.
  }
}

view 和 workspace 的关系

根据 WorkspaceLeaf class 定义可以确认 view 是 WorkspaceLeaf 的成员,同样的 View 也引用了对应的 WorkspaceLeaf 成员。

实用 Obsidian UI 编程

1. 当前 workspace 检查

this.app.workspace.iterateAllLeaves((leaf) => {
	// 遍历 workspace 树形结构,可以获取没有节点对应的 view
	// view 可以对显示内容进行控制和管理
    console.log(leaf.getViewState().type);
});

2. view 生命周期

import { Plugin } from "obsidian";
import { ExampleView, VIEW_TYPE_EXAMPLE } from "./view";

export default class ExamplePlugin extends Plugin {
  async onload() {
    this.registerView(
      VIEW_TYPE_EXAMPLE,
      (leaf) => new ExampleView(leaf)
    );

    this.addRibbonIcon("dice", "Activate view", () => {
      this.activateView();
    });
  }
  async onunload() {
    this.app.workspace.detachLeavesOfType(VIEW_TYPE_EXAMPLE);
  }
  async activateView() {
    this.app.workspace.detachLeavesOfType(VIEW_TYPE_EXAMPLE);

    await this.app.workspace.getRightLeaf(false).setViewState({
      type: VIEW_TYPE_EXAMPLE,
      active: true,
    });
    this.app.workspace.revealLeaf(
      this.app.workspace.getLeavesOfType(VIEW_TYPE_EXAMPLE)[0]
    );
  }
}

开发实践

利用 QuickAdd 提供的执行 js 脚本的能力实现操作 UI 显示当前笔记对应的 graph 视图,效果如下:

0:00
/
local graph with Obsidian UI equipment

References

  1. User interface | Obsidian Plugin Developer Docs
  2. Workspace | Obsidian Plugin Developer Docs

Public discussion