UNPKG

5.33 kBPlain TextView Raw
1/*
2 * Copyright (c) Jupyter Development Team.
3 * Distributed under the terms of the Modified BSD License.
4 */
5
6import { Text } from '@jupyterlab/coreutils';
7import { ISettingRegistry } from '@jupyterlab/settingregistry';
8import { LabIcon } from '@jupyterlab/ui-components';
9import { JSONExt } from '@lumino/coreutils';
10import { ContextMenu, Menu } from '@lumino/widgets';
11
12/**
13 * Helper functions to build a menu from the settings
14 */
15export namespace MenuFactory {
16 /**
17 * Menu constructor options
18 */
19 export interface IMenuOptions {
20 /**
21 * The unique menu identifier.
22 */
23 id: string;
24
25 /**
26 * The menu label.
27 */
28 label?: string;
29
30 /**
31 * The menu rank.
32 */
33 rank?: number;
34 }
35
36 /**
37 * Create menus from their description
38 *
39 * @param data Menubar description
40 * @param menuFactory Factory for empty menu
41 */
42 export function createMenus(
43 data: ISettingRegistry.IMenu[],
44 menuFactory: (options: IMenuOptions) => Menu
45 ): Menu[] {
46 return data
47 .filter(item => !item.disabled)
48 .sort((a, b) => (a.rank ?? Infinity) - (b.rank ?? Infinity))
49 .map(menuItem => {
50 return dataToMenu(menuItem, menuFactory);
51 });
52 }
53
54 /**
55 * Convert a menu description in a JupyterLabMenu object
56 *
57 * @param item Menu description
58 * @param menuFactory Empty menu factory
59 * @returns The menu widget
60 */
61 function dataToMenu(
62 item: ISettingRegistry.IMenu,
63 menuFactory: (options: IMenuOptions) => Menu
64 ): Menu {
65 const menu = menuFactory(item);
66 menu.id = item.id;
67
68 // Set the label in case the menu factory did not.
69 if (!menu.title.label) {
70 menu.title.label = item.label ?? Text.titleCase(menu.id.trim());
71 }
72
73 if (item.icon) {
74 menu.title.icon = LabIcon.resolve({ icon: item.icon });
75 }
76 if (item.mnemonic !== undefined) {
77 menu.title.mnemonic = item.mnemonic;
78 }
79
80 item.items
81 ?.filter(item => !item.disabled)
82 .sort((a, b) => (a.rank ?? Infinity) - (b.rank ?? Infinity))
83 .map(item => {
84 addItem(item, menu, menuFactory);
85 });
86 return menu;
87 }
88
89 /**
90 * Convert an item description in a context menu item object
91 *
92 * @param item Context menu item
93 * @param menu Context menu to populate
94 * @param menuFactory Empty menu factory
95 */
96 export function addContextItem(
97 item: ISettingRegistry.IContextMenuItem,
98 menu: ContextMenu,
99 menuFactory: (options: IMenuOptions) => Menu
100 ): void {
101 const { submenu, ...newItem } = item;
102 // Commands may not have been registered yet; so we don't force it to exist
103 menu.addItem({
104 ...newItem,
105 submenu: submenu ? dataToMenu(submenu, menuFactory) : null
106 } as any);
107 }
108
109 /**
110 * Convert an item description in a menu item object
111 *
112 * @param item Menu item
113 * @param menu Menu to populate
114 * @param menuFactory Empty menu factory
115 */
116 function addItem(
117 item: ISettingRegistry.IMenuItem,
118 menu: Menu,
119 menuFactory: (options: IMenuOptions) => Menu
120 ): void {
121 const { submenu, ...newItem } = item;
122 // Commands may not have been registered yet; so we don't force it to exist
123 menu.addItem({
124 ...newItem,
125 submenu: submenu ? dataToMenu(submenu, menuFactory) : null
126 } as any);
127 }
128
129 /**
130 * Update an existing list of menu and returns
131 * the new elements.
132 *
133 * #### Note
134 * New elements are added to the current menu list.
135 *
136 * @param menus Current menus
137 * @param data New description to take into account
138 * @param menuFactory Empty menu factory
139 * @returns Newly created menus
140 */
141 export function updateMenus(
142 menus: Menu[],
143 data: ISettingRegistry.IMenu[],
144 menuFactory: (options: IMenuOptions) => Menu
145 ): Menu[] {
146 const newMenus: Menu[] = [];
147 data.forEach(item => {
148 const menu = menus.find(menu => menu.id === item.id);
149 if (menu) {
150 mergeMenus(item, menu, menuFactory);
151 } else {
152 if (!item.disabled) {
153 newMenus.push(dataToMenu(item, menuFactory));
154 }
155 }
156 });
157 menus.push(...newMenus);
158 return newMenus;
159 }
160
161 function mergeMenus(
162 item: ISettingRegistry.IMenu,
163 menu: Menu,
164 menuFactory: (options: IMenuOptions) => Menu
165 ) {
166 if (item.disabled) {
167 menu.dispose();
168 } else {
169 item.items?.forEach(entry => {
170 const existingItem = menu?.items.find(
171 (i, idx) =>
172 i.type === entry.type &&
173 i.command === (entry.command ?? '') &&
174 i.submenu?.id === entry.submenu?.id
175 );
176
177 if (existingItem && entry.type !== 'separator') {
178 if (entry.disabled) {
179 menu.removeItem(existingItem);
180 } else {
181 switch (entry.type ?? 'command') {
182 case 'command':
183 if (entry.command) {
184 if (!JSONExt.deepEqual(existingItem.args, entry.args ?? {})) {
185 addItem(entry, menu, menuFactory);
186 }
187 }
188 break;
189 case 'submenu':
190 if (entry.submenu) {
191 mergeMenus(entry.submenu, existingItem.submenu!, menuFactory);
192 }
193 }
194 }
195 } else {
196 addItem(entry, menu, menuFactory);
197 }
198 });
199 }
200 }
201}