# 菜单与 Ribbon 扩展

> 文档：`vjcad/doc/plugin/plugin-menu-ribbon.md`
> 示例：`vjcad/example/plugin/04pluginmanager.js`、`vjcad/example/ui/16menucustom.js`

## 1. 默认菜单 ID

| ID | 名称 | 说明 |
|---|---|---|
| `file` | 文件 | 新建、打开、保存、导出 |
| `create` | 创建 | 直线、圆、多段线等绘图命令 |
| `edit` | 编辑 | 移动、复制、旋转等 |
| `view` | 显示 | 视图控制、显示顺序、图层 |
| `insert` | 插入 | 块、图像 |
| `dim` | 标注 | 尺寸标注 |
| `property` | 属性 | 实体属性 |
| `tool` | 工具 | 测量、统计、脚本 |
| `appearance` | 外观 | 界面外观 |
| `help` | 帮助 | 帮助、关于 |

## 2. 默认 Ribbon 标签 / 组

| Tab ID | Group ID | 名称 |
|---|---|---|
| `default`（默认） | `draw` / `modify` / `edit` / `annotation` / `layer` / `navigation` | 绘图、修改、编辑、注释、图层特性、导航 |
| `tools`（工具） | `quick-tools` / `group` / `script` / `clip-region` / `purge` / `properties` / `measure` / `stats` / `help` | 快捷工具、组、脚本、裁剪、清理、特性、测量、统计、帮助 |
| `insert`（插入） | `block` / `image` / `symbol` / `template` / `import` | 块、图像、符号、模板、导入 |
| `plugins`（插件） | `plugin-manager` | 插件管理（组标题为空） |
| `recent`（最近命令） | `recent-commands` | 最近命令（动态填充，组标题为空） |

> 以上取自 `defaultRibbonConfig`（vjcad@1.0.16）；运行时以 `RibbonConfigManager.getConfig().tabs` 为准。

## 3. 三套写法

### 3.1 插件运行时（`PluginContext`）

```js
context.addMenu({ id: 'my-tools', label: '我的工具', after: 'tool' });
context.addMenuItem('my-tools', { command: 'TOOL1' });
context.addRibbonTab({ id: 'my', label: '我的', groups: [] }, 'tools');
context.addRibbonGroup('my', {
    id: 'main', label: '主要',
    primaryButtons: [{ icon: 'tool1', cmd: 'TOOL1', prompt: '工具 1', type: 'large' }]
});
```

### 3.2 MainView 一次性配置（增量）

```js
new MainView({
    // ...其他 MainViewConfig 字段
    menuBar: {
        removeMenus: ['help', 'appearance'],
        addMenus: [{
            id: 'custom-menu', label: '自定义菜单', after: 'insert',
            items: [
                { type: 'command', command: 'DRAW_STAR' },
                { type: 'separator' },
                { type: 'command', command: 'REGEN' }
            ]
        }],
        modifyMenus: {
            file: {
                removeItems: ['SWITCHWORKSPACE'],
                addItems: [{ type: 'command', command: 'DRAW_STAR', after: 'EXPORTPNG' }]
            }
        }
    },
    ribbon: {
        removeTabs: ['recent'],
        addTabs: [{
            tab: {
                id: 'custom-tab', label: '自定义',
                groups: [{
                    id: 'custom-draw', label: '自定义绘图',
                    primaryButtons: [
                        { icon: 'line', cmd: 'DRAW_STAR', prompt: '五角星', label: '五角星' }
                    ]
                }]
            },
            after: 'default'
        }],
        modifyTabs: {
            default: {
                modifyGroups: {
                    draw: {
                        addPrimaryButtons: [
                            { icon: 'circle', cmd: 'DRAW_STAR', prompt: '五角星',
                              label: '★', after: 'CIRCLE' }
                        ]
                    }
                }
            }
        }
    }
});
```

> 完整代码：`vjcad/example/ui/16menucustom.js`

### 3.3 MainView 全量替换

```js
new MainView({
    menuBar: {
        menus: [
            { id: 'my-file', label: '文件', items: [
                { type: 'command', command: 'NEW' },
                { type: 'command', command: 'OPEN' },
                { type: 'separator' },
                { type: 'command', command: 'SAVELOCAL', shortcut: 'Ctrl+S' }
            ]},
            { id: 'my-draw', label: '绘图', items: [
                { type: 'command', command: 'LINE' },
                { type: 'command', command: 'CIRCLE' }
            ]}
        ]
    },
    ribbon: {
        config: { tabs: [ /* RibbonConfig 完整定义 */ ] }
    }
});
```

`menus` 与 `config` 二选一；提供它们就 **忽略所有默认菜单 / Ribbon**。

## 4. 数据结构

### 菜单项

```ts
type MenuItemDef =
    | { type: 'command';  command: string; label?: string; icon?: string; shortcut?: string; column?: number }
    | { type: 'separator'; column?: number }
    | { type: 'submenu';  label: string; icon?: string; column?: number; children: MenuItemDef[] };
```

### Ribbon 按钮

```ts
type RibbonButtonType = 'large' | 'small' | 'icon-only' | 'list';

interface RibbonButtonConfig {
    icon: string;        // 图标 ID（registerIcon 名）
    cmd:  string;        // 命令名
    prompt?: string;     // 工具提示
    label?:  string;     // 紧凑模式显示文本
    type?:   RibbonButtonType;
    items?:  RibbonButtonConfig[];   // type='list' 时下拉子项
}
```

### Ribbon 组

```ts
type RibbonGroupDisplayMode = 'large' | 'compact' | 'small-icons' | 'layer' | 'recent';

interface RibbonGroupConfig {
    id: string;
    label: string;
    pinnable?: boolean;
    displayMode?: RibbonGroupDisplayMode;
    primaryButtons: RibbonButtonConfig[];
    moreButtons?: RibbonButtonConfig[];
}
```

### Ribbon 标签

```ts
interface RibbonTabConfig {
    id: string;
    label: string;
    groups: RibbonGroupConfig[];
}
```

## 5. 实战：完整插件示例（MYQSELECT）

```js
const ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
    <rect x="3" y="3" width="18" height="18" rx="2" stroke-width="2"/>
    <path d="M9 12l2 2 4-4" stroke-width="2"/>
</svg>`;

// 源码写法：按 URL 加载需打包成 IIFE（见 overview §3）；直接用这段字符串则走 pm.loadFromContent
export default {
    manifest: { id: 'my-quick-select', name: '我的快速选择', version: '1.0.0' },

    async onActivate(context) {
        context.registerCommand('MYQSELECT', '我的快速选择', MyQuickSelectCommand);

        context.registerIcon('MYQSELECT', ICON);   // 菜单与 Ribbon 共用（键名大小写不敏感）

        context.addMenu({ id: 'plugins', label: '插件', after: 'tool' });
        context.addMenuItem('plugins', { command: 'MYQSELECT' });

        context.addRibbonTab({ id: 'plugins', label: '插件', groups: [] });
        context.addRibbonGroup('plugins', {
            id: 'my-quick-select', label: '我的快速选择', pinnable: true,
            primaryButtons: [{ icon: 'myqselect', cmd: 'MYQSELECT', prompt: '快速选择图元', type: 'large' }]
        });
    },

    async onDeactivate(context) {
        context.removeRibbonGroup('plugins', 'my-quick-select');
        context.removeMenuItem('plugins', 'MYQSELECT');
        context.unregisterCommand('MYQSELECT');
    }
};
```

## 6. 命名规范

| 元素 | 规则 | 例 |
|---|---|---|
| 命令名 | 全大写 | `DRAWCROSS` |
| 图标名 | 与命令同名；注册表统一转小写，大小写不敏感，注册一次菜单与 Ribbon 共用 | `DRAWCROSS` ≡ `drawcross` |
| 主菜单 / Ribbon ID | kebab-case | `my-plugin` |

## 7. 三种写法对比

| 维度 | PluginContext | MainView 增量 | MainView 全量 |
|---|---|---|---|
| 时机 | 运行时（激活/停用） | 启动一次 | 启动一次 |
| 粒度 | 逐项 | 批量增量 | 整体替换 |
| 自动清理 | 是（onDeactivate） | 否（生命周期跟随 MainView） | 否 |
| 适合 | 第三方插件 | 应用级定制（删/改默认） | 完全自定义 |

## 红旗

- 默认主菜单 / Ribbon 的 ID 写错（如 `tools` ↔ `tool`）会让命令藏起来；记牢上面默认表
- 增量与全量同时写时，**全量赢**；调试时不要混用
- `addMenu` / `addRibbonTab` 重名是 no-op，不能用于"重置"；要替换请先 remove 再 add
- 图标键名注册与查找都转小写：菜单按命令名找图标、Ribbon 按钮按 `icon` 找图标，大小写都无所谓，**不必**为大小写各注册一份
- 移除按钮时 `removeRibbonButton(tab, group, cmd)` 用的是 **cmd 名**，不是 button 的 id
- 命令名不要与内置命令重名（`QSELECT`、`FINDREPLACE` 等已是内置命令）：`registerCommand` 是直接 push、查找按首个匹配，重名会被内置命令遮蔽，且 `unregisterCommand` 会误删内置项

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/plugin/plugin-menu-ribbon.md`
- 示例：`vjcad/example/plugin/04pluginmanager.js`、`vjcad/example/ui/16menucustom.js`
