Skip to content

Commit fb584b2

Browse files
author
Akos Kitta
committed
ATL-938: Added menu group categories.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
1 parent 26346b4 commit fb584b2

File tree

7 files changed

+189
-66
lines changed

7 files changed

+189
-66
lines changed

arduino-ide-extension/src/browser/contributions/board-selection.ts

+52-29
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { inject, injectable } from 'inversify';
22
import { remote } from 'electron';
33
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
44
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
5+
import { firstToUpperCase } from '../../common/utils';
56
import { BoardsConfig } from '../boards/boards-config';
67
import { MainMenuManager } from '../../common/main-menu-manager';
78
import { BoardsListWidget } from '../boards/boards-list-widget';
89
import { NotificationCenter } from '../notification-center';
910
import { BoardsServiceProvider } from '../boards/boards-service-provider';
10-
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
11+
import { ArduinoMenus, PlaceholderMenuNode, unregisterSubmenu } from '../menu/arduino-menus';
1112
import { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol';
1213
import { SketchContribution, Command, CommandRegistry } from './contribution';
1314

@@ -150,39 +151,61 @@ PID: ${PID}`;
150151
}
151152

152153
// Installed ports
153-
for (const address of Object.keys(availablePorts)) {
154-
if (!!availablePorts[address]) {
155-
const [port, boards] = availablePorts[address];
156-
if (!boards.length) {
157-
boards.push({
158-
name: ''
159-
});
160-
}
161-
for (const { name, fqbn } of boards) {
162-
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
163-
const command = { id };
164-
const handler = {
165-
execute: () => {
166-
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
167-
this.boardsServiceProvider.boardsConfig = {
168-
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
169-
selectedPort: port
154+
const registerPorts = (ports: AvailablePorts) => {
155+
const addresses = Object.keys(ports);
156+
if (!addresses.length) {
157+
return;
158+
}
159+
160+
// Register placeholder for protocol
161+
const [port] = ports[addresses[0]];
162+
const protocol = port.protocol;
163+
const menuPath = [...portsSubmenuPath, protocol];
164+
const placeholder = new PlaceholderMenuNode(menuPath, `${firstToUpperCase(port.protocol)} ports`);
165+
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
166+
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuNode(placeholder.id)));
167+
168+
for (const address of addresses) {
169+
if (!!ports[address]) {
170+
const [port, boards] = ports[address];
171+
if (!boards.length) {
172+
boards.push({
173+
name: ''
174+
});
175+
}
176+
for (const { name, fqbn } of boards) {
177+
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
178+
const command = { id };
179+
const handler = {
180+
execute: () => {
181+
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
182+
this.boardsServiceProvider.boardsConfig = {
183+
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
184+
selectedPort: port
185+
}
170186
}
171-
}
172-
},
173-
isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)
174-
};
175-
const menuAction = {
176-
commandId: id,
177-
label: `${address}${name ? ` (${name})` : ''}`
178-
};
179-
this.commandRegistry.registerCommand(command, handler);
180-
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
181-
this.menuModelRegistry.registerMenuAction(portsSubmenuPath, menuAction);
187+
},
188+
isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)
189+
};
190+
const label = `${address}${name ? ` (${name})` : ''}`;
191+
const menuAction = {
192+
commandId: id,
193+
label,
194+
order: `1${label}` // `1` comes after the placeholder which has order `0`
195+
};
196+
this.commandRegistry.registerCommand(command, handler);
197+
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
198+
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
199+
}
182200
}
183201
}
184202
}
185203

204+
const { serial, network, unknown } = AvailablePorts.groupByProtocol(availablePorts);
205+
registerPorts(serial);
206+
registerPorts(network);
207+
registerPorts(unknown);
208+
186209
this.mainMenuManager.update();
187210
}
188211

arduino-ide-extension/src/browser/contributions/examples.ts

+45-26
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { inject, injectable, postConstruct } from 'inversify';
33
import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu';
44
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
55
import { OpenSketch } from './open-sketch';
6-
import { ArduinoMenus } from '../menu/arduino-menus';
6+
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
77
import { MainMenuManager } from '../../common/main-menu-manager';
88
import { BoardsServiceProvider } from '../boards/boards-service-provider';
99
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
1010
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
1111
import { NotificationCenter } from '../notification-center';
12+
import { Board } from '../../common/protocol';
1213

1314
@injectable()
1415
export abstract class Examples extends SketchContribution {
@@ -32,10 +33,10 @@ export abstract class Examples extends SketchContribution {
3233

3334
@postConstruct()
3435
init(): void {
35-
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard?.fqbn));
36+
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard));
3637
}
3738

38-
protected handleBoardChanged(fqbn: string | undefined): void {
39+
protected handleBoardChanged(board: Board | undefined): void {
3940
// NOOP
4041
}
4142

@@ -58,27 +59,33 @@ export abstract class Examples extends SketchContribution {
5859
}
5960

6061
registerRecursively(
61-
exampleContainer: ExampleContainer,
62+
exampleContainerOrPlaceholder: ExampleContainer | string,
6263
menuPath: MenuPath,
6364
pushToDispose: DisposableCollection = new DisposableCollection()): void {
6465

65-
const { label, sketches, children } = exampleContainer;
66-
const submenuPath = [...menuPath, label];
67-
this.menuRegistry.registerSubmenu(submenuPath, label);
68-
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
69-
for (const sketch of sketches) {
70-
const { uri } = sketch;
71-
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
72-
const command = { id: commandId };
73-
const handler = {
74-
execute: async () => {
75-
const sketch = await this.sketchService.cloneExample(uri);
76-
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
77-
}
78-
};
79-
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
80-
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
81-
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
66+
if (typeof exampleContainerOrPlaceholder === 'string') {
67+
const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder);
68+
this.menuRegistry.registerMenuNode(menuPath, placeholder);
69+
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
70+
} else {
71+
const { label, sketches, children } = exampleContainerOrPlaceholder;
72+
const submenuPath = [...menuPath, label];
73+
this.menuRegistry.registerSubmenu(submenuPath, label);
74+
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
75+
for (const sketch of sketches) {
76+
const { uri } = sketch;
77+
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
78+
const command = { id: commandId };
79+
const handler = {
80+
execute: async () => {
81+
const sketch = await this.sketchService.cloneExample(uri);
82+
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
83+
}
84+
};
85+
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
86+
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
87+
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
88+
}
8289
}
8390
}
8491

@@ -101,10 +108,12 @@ export class BuiltInExamples extends Examples {
101108
return;
102109
}
103110
this.toDispose.dispose();
104-
for (const container of exampleContainers) {
111+
for (const container of ['Built-in examples', ...exampleContainers]) {
105112
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
106113
}
107114
this.menuManager.update();
115+
// TODO: remove
116+
console.log(typeof this.menuRegistry);
108117
}
109118

110119
}
@@ -123,17 +132,27 @@ export class LibraryExamples extends Examples {
123132
this.notificationCenter.onLibraryUninstalled(() => this.register());
124133
}
125134

126-
protected handleBoardChanged(fqbn: string | undefined): void {
127-
this.register(fqbn);
135+
protected handleBoardChanged(board: Board | undefined): void {
136+
this.register(board);
128137
}
129138

130-
protected async register(fqbn: string | undefined = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn) {
139+
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) {
131140
return this.queue.add(async () => {
132141
this.toDispose.dispose();
133-
if (!fqbn) {
142+
if (!board || !board.fqbn) {
134143
return;
135144
}
145+
const { fqbn, name } = board;
136146
const { user, current, any } = await this.examplesService.installed({ fqbn });
147+
if (user.length) {
148+
(user as any).unshift('Examples from Custom Libraries');
149+
}
150+
if (current.length) {
151+
(current as any).unshift(`Examples for ${name}`);
152+
}
153+
if (any.length) {
154+
(any as any).unshift('Examples for any board');
155+
}
137156
for (const container of user) {
138157
this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose);
139158
}

arduino-ide-extension/src/browser/contributions/include-library.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
55
import { EditorManager } from '@theia/editor/lib/browser';
66
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
77
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
8-
import { ArduinoMenus } from '../menu/arduino-menus';
9-
import { LibraryPackage, LibraryLocation, LibraryService } from '../../common/protocol';
8+
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
9+
import { LibraryPackage, LibraryService } from '../../common/protocol';
1010
import { MainMenuManager } from '../../common/main-menu-manager';
1111
import { LibraryListWidget } from '../library/library-list-widget';
1212
import { BoardsServiceProvider } from '../boards/boards-service-provider';
@@ -84,19 +84,35 @@ export class IncludeLibrary extends SketchContribution {
8484
// `Arduino libraries`
8585
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
8686
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
87-
for (const library of libraries) {
88-
this.toDispose.push(this.registerLibrary(library, library.location === LibraryLocation.USER ? userMenuPath : packageMenuPath));
87+
const { user, rest } = LibraryPackage.groupByLocation(libraries);
88+
if (rest.length) {
89+
(rest as any).unshift('Arduino libraries');
90+
}
91+
if (user.length) {
92+
(user as any).unshift('Contributed libraries');
93+
}
94+
95+
for (const library of user) {
96+
this.toDispose.push(this.registerLibrary(library, userMenuPath));
97+
}
98+
for (const library of rest) {
99+
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
89100
}
90101

91102
this.mainMenuManager.update();
92103
});
93104
}
94105

95-
protected registerLibrary(library: LibraryPackage, menuPath: MenuPath): Disposable {
96-
const commandId = `arduino-include-library--${library.name}:${library.author}`;
106+
protected registerLibrary(libraryOrPlaceholder: LibraryPackage | string, menuPath: MenuPath): Disposable {
107+
if (typeof libraryOrPlaceholder === 'string') {
108+
const placeholder = new PlaceholderMenuNode(menuPath, libraryOrPlaceholder);
109+
this.menuRegistry.registerMenuNode(menuPath, placeholder);
110+
return Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id));
111+
}
112+
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
97113
const command = { id: commandId };
98-
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, library) };
99-
const menuAction = { commandId, label: library.name };
114+
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, libraryOrPlaceholder) };
115+
const menuAction = { commandId, label: libraryOrPlaceholder.name };
100116
this.menuRegistry.registerMenuAction(menuPath, menuAction);
101117
return new DisposableCollection(
102118
this.commandRegistry.registerCommand(command, handler),

arduino-ide-extension/src/browser/menu/arduino-menus.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isOSX } from '@theia/core/lib/common/os';
22
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
3-
import { MAIN_MENU_BAR, MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu';
3+
import { MAIN_MENU_BAR, MenuModelRegistry, MenuNode, MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
44

55
export namespace ArduinoMenus {
66

@@ -99,3 +99,24 @@ export function unregisterSubmenu(menuPath: string[], menuRegistry: MenuModelReg
9999
}
100100
(parent.children as Array<MenuNode>).splice(index, 1);
101101
}
102+
103+
/**
104+
* Special menu node that is not backed by any commands and is always disabled.
105+
*/
106+
export class PlaceholderMenuNode implements MenuNode {
107+
108+
constructor(protected readonly menuPath: MenuPath, readonly label: string, protected options: SubMenuOptions = { order: '0' }) { }
109+
110+
get icon(): string | undefined {
111+
return this.options?.iconClass;
112+
}
113+
114+
get sortString(): string {
115+
return this.options?.order || this.label;
116+
}
117+
118+
get id(): string {
119+
return [...this.menuPath, 'placeholder'].join('-');
120+
}
121+
122+
}

arduino-ide-extension/src/common/protocol/boards-service.ts

+19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ import { Installable } from './installable';
55
import { ArduinoComponent } from './arduino-component';
66

77
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
8+
export namespace AvailablePorts {
9+
export function groupByProtocol(availablePorts: AvailablePorts): { serial: AvailablePorts, network: AvailablePorts, unknown: AvailablePorts } {
10+
const serial: AvailablePorts = {};
11+
const network: AvailablePorts = {};
12+
const unknown: AvailablePorts = {};
13+
for (const key of Object.keys(availablePorts)) {
14+
const [port, boards] = availablePorts[key];
15+
const { protocol } = port;
16+
if (protocol === 'serial') {
17+
serial[key] = [port, boards];
18+
} else if (protocol === 'network') {
19+
network[key] = [port, boards];
20+
} else {
21+
unknown[key] = [port, boards];
22+
}
23+
}
24+
return { serial, network, unknown };
25+
}
26+
}
827

928
export interface AttachedBoardsChangeEvent {
1029
readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>;

arduino-ide-extension/src/common/protocol/library-service.ts

+13
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,17 @@ export namespace LibraryPackage {
6969
return left.name === right.name && left.author === right.author;
7070
}
7171

72+
export function groupByLocation(packages: LibraryPackage[]): { user: LibraryPackage[], rest: LibraryPackage[] } {
73+
const user: LibraryPackage[] = [];
74+
const rest: LibraryPackage[] = [];
75+
for (const pkg of packages) {
76+
if (pkg.location === LibraryLocation.USER) {
77+
user.push(pkg);
78+
} else {
79+
rest.push(pkg);
80+
}
81+
}
82+
return { user, rest };
83+
}
84+
7285
}

arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { injectable } from 'inversify'
22
import { remote } from 'electron';
33
import { Keybinding } from '@theia/core/lib/common/keybinding';
4-
import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
5-
import { ArduinoMenus } from '../../../browser/menu/arduino-menus';
4+
import { CompositeMenuNode } from '@theia/core/lib/common/menu';
5+
import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory, ElectronMenuOptions } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
6+
import { ArduinoMenus, PlaceholderMenuNode } from '../../../browser/menu/arduino-menus';
67

78
@injectable()
89
export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
@@ -42,4 +43,15 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
4243
return { label, submenu };
4344
}
4445

46+
protected handleDefault(menuNode: CompositeMenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
47+
if (menuNode instanceof PlaceholderMenuNode) {
48+
return [{
49+
label: menuNode.label,
50+
enabled: false,
51+
visible: true
52+
}];
53+
}
54+
return [];
55+
}
56+
4557
}

0 commit comments

Comments
 (0)