Menu Bar and Ribbon Extension
Menu Bar and Ribbon Extension
This chapter describes in detail how to extend the WebCAD menu bar and Ribbon toolbar through plugins.
Menu Bar Extension
Default Menu IDs
WebCAD provides the following default menu items:
| ID | Name | Description |
|---|---|---|
file | File | New, open, save, export, etc. |
create | Create | Drawing commands (line, circle, polyline, etc.) |
edit | Edit | Editing commands (move, copy, rotate, etc.) |
view | View | View control, draw order, layer management |
insert | Insert | Block and image insertion and management |
dim | Dimension | Dimensioning |
property | Property | Entity properties |
tool | Tools | Tools, measurement, statistics |
appearance | Appearance | UI appearance settings |
help | Help | Help and About |
Add Menu Items to Existing Menus
import type { Plugin, PluginContext } from 'vjcad';
const plugin: Plugin = {
manifest: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
async onActivate(context: PluginContext): Promise<void> {
// Register commands
context.registerCommand('MYCMD', 'My Command', MyCommand);
context.registerIcon('MYCMD', '<svg>...</svg>');
// Add to Tools menu
context.addMenuItem('tool', {
command: 'MYCMD',
shortcut: 'Ctrl+M' // Optional shortcut hint
});
// Add to Edit menu at a specific position
context.addMenuItem('edit', {
command: 'MYCMD2',
after: 'COPY' // Insert after COPY command
});
}
};Create a New Top-Level Menu
When you need to add a group of related commands, you can create a new top-level menu:
async onActivate(context: PluginContext): Promise<void> {
// Register commands and icons
context.registerCommand('TOOL1', 'Tool 1', Tool1Command);
context.registerCommand('TOOL2', 'Tool 2', Tool2Command);
context.registerIcon('TOOL1', '<svg>...</svg>');
context.registerIcon('TOOL2', '<svg>...</svg>');
// Create a new top-level menu (if it does not already exist)
// Multiple plugins can safely call this; only the first will create it
context.addMenu({
id: 'my-tools', // Unique ID
label: 'My Tools', // Display name
after: 'tool' // After Tools menu (optional)
});
// Add menu items
context.addMenuItem('my-tools', { command: 'TOOL1' });
context.addMenuItem('my-tools', { command: 'TOOL2' });
}
async onDeactivate(context: PluginContext): Promise<void> {
// Remove menu items
context.removeMenuItem('my-tools', 'TOOL1');
context.removeMenuItem('my-tools', 'TOOL2');
// Remove top-level menu (if created by this plugin)
context.removeMenu('my-tools');
}Menu Item Configuration
interface MenuItemConfig {
/** Command name (required) */
command: string;
/** Shortcut hint (optional) */
shortcut?: string;
/** Insert before specified command (optional) */
before?: string;
/** Insert after specified command (optional) */
after?: string;
}
interface MenuConfig2 {
/** Unique menu ID (required) */
id: string;
/** Display name (required) */
label: string;
/** Insert after specified menu (optional) */
after?: string;
}Menu Icons
Menu items automatically use the icon with the same name as the command. Make sure to register the icon before adding the menu item:
// Icon name must match command name
context.registerIcon('MYCMD', '<svg viewBox="0 0 24 24">...</svg>');
context.registerCommand('MYCMD', 'My Command', MyCommand);
context.addMenuItem('tool', { command: 'MYCMD' }); // Automatically uses MYCMD iconRibbon Toolbar Extension
Default Ribbon Tabs
| ID | Name | Description |
|---|---|---|
default | Default | Common drawing, modify, editing, and related tools |
tools | Tools | Grouping, cleanup, properties, images, measurement, etc. |
plugins | Plugins | Plugin management |
recent | Recent Commands | Dynamically shows recently executed commands |
Default Ribbon Groups (by Tab)
default tab:
| Group ID | Name | Main Commands |
|---|---|---|
draw | Draw | LINE, PLINE, ARC, CIRCLE, RECTANG, ELLIPSE, HATCH, SPLINE... |
modify | Modify | MOVE, ROTATE, TRIM, COPY, MIRROR, FILLET, STRETCH, EXTEND, OFFSET... |
edit | Edit | UNDO, REDO, COPYCLIP, CUTCLIP, PASTECLIP... |
annotation | Annotation | TEXT, MTEXT, DIMLINEAR... |
layer | Layer Properties | LAYER, MAKELAYER, LAYALLON... |
navigation | Navigation | ZOOM, ZOOMEXTENTS, REGENALL... |
block | Block | INSERT, BLOCK, QBLOCK... |
tools tab:
| Group ID | Name | Main Commands |
|---|---|---|
group | Group | GROUP, UNGROUP |
purge | Purge | PURGE, PURGEBLOCK, PURGEIMAGE... |
properties | Properties | PROPERTIES, MATCHPROP... |
image | Image | IMAGE, IMAGEADJUST, IMAGECLIP... |
measure | Measure | DIST, MEASUREANGLE, ID |
stats | Statistics | ENTITYSTATS, COUNTBLOCK... |
script | Script | EXECSTR, EXECJS |
help | Help | ABOUT, GRAPHICSINFO |
plugins tab:
| Group ID | Name | Main Commands |
|---|---|---|
plugin-manager | Plugin Manager | PLUGINS |
Add Button to Existing Group
import type { RibbonButtonConfig } from 'vjcad';
async onActivate(context: PluginContext): Promise<void> {
context.registerCommand('MYTOOL', 'My Tool', MyToolCommand);
context.registerIcon('mytool', '<svg>...</svg>');
// Add button to the draw group on the default tab
const button: RibbonButtonConfig = {
icon: 'mytool', // Icon ID
cmd: 'MYTOOL', // Command name
prompt: 'My drawing tool',// Tooltip
label: 'Tool', // Text shown in compact mode (optional)
type: 'small' // Button type (optional)
};
context.addRibbonButton('default', 'draw', button, 'primary');
// Or add to the more-buttons area
// context.addRibbonButton('default', 'draw', button, 'more');
}
async onDeactivate(context: PluginContext): Promise<void> {
context.removeRibbonButton('default', 'draw', 'MYTOOL');
}Add Group to Existing Tab
import type { RibbonGroupConfig } from 'vjcad';
async onActivate(context: PluginContext): Promise<void> {
// Register commands and icons
context.registerCommand('CMD1', 'Command 1', Cmd1);
context.registerCommand('CMD2', 'Command 2', Cmd2);
context.registerIcon('cmd1', '<svg>...</svg>');
context.registerIcon('cmd2', '<svg>...</svg>');
// Add group to tools tab
const group: RibbonGroupConfig = {
id: 'my-group',
label: 'My Tools',
pinnable: true, // Whether pinning is supported
displayMode: 'large', // Display mode
primaryButtons: [
{ icon: 'cmd1', cmd: 'CMD1', prompt: 'Command 1', type: 'large' },
{ icon: 'cmd2', cmd: 'CMD2', prompt: 'Command 2', type: 'large' }
],
moreButtons: [
{ icon: 'cmd3', cmd: 'CMD3', prompt: 'Command 3' }
]
};
context.addRibbonGroup('tools', group);
}
async onDeactivate(context: PluginContext): Promise<void> {
context.removeRibbonGroup('tools', 'my-group');
}Create New Ribbon Tab
import type { RibbonTabConfig } from 'vjcad';
async onActivate(context: PluginContext): Promise<void> {
// Register commands and icons...
// Create new tab (if not already present)
// Multiple plugins can safely call with the same ID; only the first creates it
context.addRibbonTab({
id: 'my-plugin-tab',
label: 'My Plugin',
groups: [] // Initially empty; groups added later
}, 'tools'); // Insert after tools tab (optional)
// Add group to the new tab
context.addRibbonGroup('my-plugin-tab', {
id: 'main-group',
label: 'Main Features',
pinnable: true,
primaryButtons: [
{ icon: 'cmd1', cmd: 'CMD1', prompt: 'Command 1', type: 'large' }
]
});
}
async onDeactivate(context: PluginContext): Promise<void> {
context.removeRibbonGroup('my-plugin-tab', 'main-group');
context.removeRibbonTab('my-plugin-tab');
}Ribbon Configuration Interfaces
/** Button type */
type RibbonButtonType = 'large' | 'small' | 'icon-only' | 'list';
/** Group display mode */
type RibbonGroupDisplayMode =
| 'large' // Large icon mode (icon + text vertically arranged)
| 'compact' // Compact mode (small icon + text)
| 'small-icons' // Small icons only (no text, two-line layout)
| 'layer' // Layer mode (special layer list)
| 'recent'; // Recent command mode
/** Button config */
interface RibbonButtonConfig {
icon: string; // Icon ID
cmd: string; // Command name
prompt?: string; // Tooltip
label?: string; // Text shown in compact mode
type?: RibbonButtonType; // Button type
items?: RibbonButtonConfig[]; // Dropdown items (when type = 'list')
}
/** Group config */
interface RibbonGroupConfig {
id: string; // Unique group ID
label: string; // Group name
pinnable?: boolean; // Whether pinning is supported
displayMode?: RibbonGroupDisplayMode; // Display mode
primaryButtons: RibbonButtonConfig[]; // Primary buttons
moreButtons?: RibbonButtonConfig[]; // More buttons
}
/** Tab config */
interface RibbonTabConfig {
id: string; // Unique tab ID
label: string; // Tab name
groups: RibbonGroupConfig[]; // Group list
}Menu Bar and Ribbon Customization (MainView Constructor)
In addition to extending menus and Ribbon item-by-item through the plugin PluginContext API, you can customize the menu bar and Ribbon toolbar in one go by passing configuration to the MainView constructor. Two modes are supported: full replacement and incremental modification.
Online Demo{target="_blank"}
Menu Bar Customization (MenuBarCustomConfig)
Pass via MainViewConfig.menuBar:
interface MenuBarCustomConfig {
/** Full replacement: provide a complete menu definition list, ignoring all defaults */
menus?: MenuDefinition[];
/** Incremental: list of top-level menu IDs to remove */
removeMenus?: string[];
/** Incremental: new top-level menus to add */
addMenus?: AddMenuDefinition[];
/** Incremental: modifications to existing menus (key is menu ID) */
modifyMenus?: Record<string, MenuModification>;
}When menus is present, it is treated as full replacement mode and incremental fields are ignored.
Menu Item Definition
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[] };Incremental Modification Interfaces
interface MenuModification {
removeItems?: string[];
addItems?: Array<MenuItemDef & { before?: string; after?: string }>;
}
interface AddMenuDefinition {
id: string;
label: string;
items: MenuItemDef[];
before?: string;
after?: string;
}Incremental Mode Example
const cadView = new MainView({
// ...
menuBar: {
removeMenus: ['help', 'appearance'],
addMenus: [{
id: 'custom-menu',
label: 'Custom',
after: 'insert',
items: [
{ type: 'command', command: 'DRAW_STAR' },
{ type: 'command', command: 'DRAW_GRID' },
{ type: 'separator' },
{ type: 'command', command: 'REGEN' },
]
}],
modifyMenus: {
file: {
removeItems: ['SWITCHWORKSPACE'],
addItems: [
{ type: 'command', command: 'DRAW_STAR', after: 'EXPORTPNG' }
]
}
}
}
});Full Replacement Example
const cadView = new MainView({
// ...
menuBar: {
menus: [
{ id: 'my-file', label: 'File', items: [
{ type: 'command', command: 'NEW' },
{ type: 'command', command: 'OPEN' },
{ type: 'separator' },
{ type: 'command', command: 'SAVELOCAL', shortcut: 'Ctrl+S' },
]},
{ id: 'my-draw', label: 'Draw', items: [
{ type: 'command', command: 'LINE' },
{ type: 'command', command: 'CIRCLE' },
]},
]
}
});Ribbon Customization (RibbonCustomConfig)
Pass via MainViewConfig.ribbon:
interface RibbonCustomConfig {
/** Full replacement: complete Ribbon configuration */
config?: RibbonConfig;
/** Incremental: list of tab IDs to remove */
removeTabs?: string[];
/** Incremental: new tabs to add */
addTabs?: AddRibbonTabDefinition[];
/** Incremental: modifications to existing tabs (key is tab ID) */
modifyTabs?: Record<string, RibbonTabModification>;
}When config is present, it is treated as full replacement mode and incremental fields are ignored.
Incremental Modification Interfaces
interface RibbonTabModification {
removeGroups?: string[];
addGroups?: Array<RibbonGroupConfig & { before?: string; after?: string }>;
modifyGroups?: Record<string, RibbonGroupModification>;
}
interface RibbonGroupModification {
removePrimaryButtons?: string[];
removeMoreButtons?: string[];
addPrimaryButtons?: Array<RibbonButtonConfig & { before?: string; after?: string }>;
addMoreButtons?: Array<RibbonButtonConfig & { before?: string; after?: string }>;
}
interface AddRibbonTabDefinition {
tab: RibbonTabConfig;
before?: string;
after?: string;
}Incremental Mode Example
const cadView = new MainView({
// ...
ribbon: {
removeTabs: ['recent'],
addTabs: [{
tab: {
id: 'custom-tab',
label: 'Custom',
groups: [{
id: 'custom-draw',
label: 'Custom Draw',
primaryButtons: [
{ icon: 'line', cmd: 'DRAW_STAR', prompt: 'Draw star', label: 'Star' },
{ icon: 'rectang', cmd: 'DRAW_GRID', prompt: 'Draw grid', label: 'Grid' },
]
}]
},
after: 'default'
}],
modifyTabs: {
'default': {
modifyGroups: {
draw: {
addPrimaryButtons: [
{ icon: 'circle', cmd: 'DRAW_STAR', prompt: 'Star',
label: '★', after: 'CIRCLE' }
]
}
}
}
}
}
});Plugin API vs MainView Customization
| Plugin API (PluginContext) | MainView Constructor | |
|---|---|---|
| Timing | Dynamic add/remove at runtime | One-time configuration at initialization |
| Granularity | Per-item operations (addMenuItem, etc.) | Batch configuration (full or incremental) |
| Cleanup | Must manually clean up in onDeactivate | Managed by MainView lifecycle |
| Use case | Plugin development | Application-level customization |
Complete Example: Quick Select Plugin
The following is a complete plugin example showing how to extend both the menu bar and Ribbon:
import type { Plugin, PluginManifest, PluginContext } from 'vjcad';
// Command class
class QuickSelectCommand {
async main(): Promise<void> {
// Open quick selection panel...
}
}
// Icon SVG
const ICON_QSELECT = `<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>`;
// Plugin manifest
const manifest: PluginManifest = {
id: 'quick-select',
name: 'Quick Select',
version: '1.0.0',
author: 'WebCAD Team',
description: 'Quickly select and filter entities'
};
// Plugin definition
const plugin: Plugin = {
manifest,
async onActivate(context: PluginContext): Promise<void> {
// 1. Register command
context.registerCommand('QSELECT', 'Quick Select', QuickSelectCommand);
// 2. Register icons (same-name icon for menu)
context.registerIcon('QSELECT', ICON_QSELECT);
// Lowercase version for Ribbon
context.registerIcon('qselect', ICON_QSELECT);
// 3. Add to Plugins top-level menu (create if missing)
context.addMenu({
id: 'plugins',
label: 'Plugins',
after: 'tool'
});
context.addMenuItem('plugins', { command: 'QSELECT' });
// 4. Add Plugins Ribbon tab (create if missing)
context.addRibbonTab({
id: 'plugins',
label: 'Plugins',
groups: []
});
// 5. Add Ribbon group
context.addRibbonGroup('plugins', {
id: 'quick-select',
label: 'Quick Select',
pinnable: true,
primaryButtons: [
{
icon: 'qselect',
cmd: 'QSELECT',
prompt: 'Quickly select entities',
type: 'large'
}
]
});
console.log('[Quick Select] Plugin activated');
},
async onDeactivate(context: PluginContext): Promise<void> {
// Clean up resources in reverse order
context.removeRibbonGroup('plugins', 'quick-select');
context.removeMenuItem('plugins', 'QSELECT');
context.unregisterCommand('QSELECT');
console.log('[Quick Select] Plugin deactivated');
}
};
export default plugin;API Reference
Menu Methods
| Method | Description |
|---|---|
addMenu(config) | Add top-level menu (skip if already exists) |
removeMenu(menuId) | Remove top-level menu |
addMenuItem(menuId, config) | Add menu item |
removeMenuItem(menuId, command) | Remove menu item |
Ribbon Methods
| Method | Description |
|---|---|
addRibbonTab(tab, afterTabId?) | Add tab (skip if already exists) |
removeRibbonTab(tabId) | Remove tab |
addRibbonGroup(tabId, group) | Add group to tab |
removeRibbonGroup(tabId, groupId) | Remove group |
addRibbonButton(tabId, groupId, btn, type) | Add button to group |
removeRibbonButton(tabId, groupId, cmd) | Remove button |
Icon Methods
| Method | Description |
|---|---|
registerIcon(name, svg) | Register SVG icon |
Best Practices
Naming conventions
- Use uppercase for command names, such as
MYCMD - Use lowercase for icon names, such as
mycmd - Use kebab-case for menu / group / tab IDs, such as
my-plugin
- Use uppercase for command names, such as
Resource cleanup
- Clean up all registered resources in
onDeactivate - Clean up in reverse order of creation
- Clean up all registered resources in
Shared resources
- Use consistent IDs such as
pluginsso multiple plugins can share the same menu or tab addMenuandaddRibbonTabautomatically check whether the target already exists
- Use consistent IDs such as
Icon consistency
- Menu items automatically use the icon with the same name as the command
- Ribbon buttons must explicitly specify an icon ID
Next Steps
- Plugin Basics - plugin lifecycle
- Plugin Context -
PluginContextAPI - Plugin Examples - more complete examples