Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 42f6237

Browse files
author
Akos Kitta
committed
feat: icon for cloud sketch in File > Open Recent
Ref: #1826 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 9687fc6 commit 42f6237

File tree

7 files changed

+238
-38
lines changed

7 files changed

+238
-38
lines changed

‎arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ import { ConfigServiceClient } from './config/config-service-client';
347347
import { ValidateSketch } from './contributions/validate-sketch';
348348
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
349349
import { CreateFeatures } from './create/create-features';
350+
import { NativeImageCache } from './native-image-cache';
350351

351352
export default new ContainerModule((bind, unbind, isBound, rebind) => {
352353
// Commands and toolbar items
@@ -1014,4 +1015,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
10141015
},
10151016
}))
10161017
.inSingletonScope();
1018+
1019+
// manages native images for the electron menu icons
1020+
bind(NativeImageCache).toSelf().inSingletonScope();
1021+
bind(FrontendApplicationContribution).toService(NativeImageCache);
10171022
});
Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,49 @@
1-
import { inject, injectable } from '@theia/core/shared/inversify';
2-
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
1+
import { NativeImage } from '@theia/core/electron-shared/electron';
32
import {
43
Disposable,
54
DisposableCollection,
65
} from '@theia/core/lib/common/disposable';
7-
import {
8-
SketchContribution,
9-
CommandRegistry,
10-
MenuModelRegistry,
11-
Sketch,
12-
} from './contribution';
6+
import { MenuAction } from '@theia/core/lib/common/menu';
7+
import { nls } from '@theia/core/lib/common/nls';
8+
import { inject, injectable } from '@theia/core/shared/inversify';
9+
import { SketchesError } from '../../common/protocol';
10+
import { ConfigServiceClient } from '../config/config-service-client';
1311
import { ArduinoMenus } from '../menu/arduino-menus';
14-
import { MainMenuManager } from '../../common/main-menu-manager';
15-
import { OpenSketch } from './open-sketch';
12+
import { NativeImageCache } from '../native-image-cache';
1613
import { NotificationCenter } from '../notification-center';
17-
import { nls } from '@theia/core/lib/common';
18-
import { SketchesError } from '../../common/protocol';
14+
import { CloudSketchContribution } from './cloud-contribution';
15+
import { CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
16+
import { OpenSketch } from './open-sketch';
1917

2018
@injectable()
21-
export class OpenRecentSketch extends SketchContribution {
19+
export class OpenRecentSketch extends CloudSketchContribution {
2220
@inject(CommandRegistry)
23-
protected readonly commandRegistry: CommandRegistry;
24-
21+
private readonly commandRegistry: CommandRegistry;
2522
@inject(MenuModelRegistry)
26-
protected readonly menuRegistry: MenuModelRegistry;
27-
28-
@inject(MainMenuManager)
29-
protected readonly mainMenuManager: MainMenuManager;
30-
31-
@inject(WorkspaceServer)
32-
protected readonly workspaceServer: WorkspaceServer;
33-
23+
private readonly menuRegistry: MenuModelRegistry;
3424
@inject(NotificationCenter)
35-
protected readonly notificationCenter: NotificationCenter;
25+
private readonly notificationCenter: NotificationCenter;
26+
@inject(NativeImageCache)
27+
private readonly imageCache: NativeImageCache;
28+
@inject(ConfigServiceClient)
29+
private readonly configServiceClient: ConfigServiceClient;
3630

37-
protected toDispose = new DisposableCollection();
31+
private readonly toDispose = new DisposableCollection();
32+
private cloudImage: NativeImage | undefined;
3833

3934
override onStart(): void {
4035
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
4136
this.refreshMenu(sketches)
4237
);
38+
this.imageCache
39+
.getImage('cloud')
40+
.then((image) => (this.cloudImage = image));
4341
}
4442

4543
override async onReady(): Promise<void> {
4644
this.update();
4745
}
4846

49-
private update(forceUpdate?: boolean): void {
50-
this.sketchesService
51-
.recentlyOpenedSketches(forceUpdate)
52-
.then((sketches) => this.refreshMenu(sketches));
53-
}
54-
5547
override registerMenus(registry: MenuModelRegistry): void {
5648
registry.registerSubmenu(
5749
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
@@ -60,12 +52,18 @@ export class OpenRecentSketch extends SketchContribution {
6052
);
6153
}
6254

55+
private update(forceUpdate?: boolean): void {
56+
this.sketchesService
57+
.recentlyOpenedSketches(forceUpdate)
58+
.then((sketches) => this.refreshMenu(sketches));
59+
}
60+
6361
private refreshMenu(sketches: Sketch[]): void {
6462
this.register(sketches);
65-
this.mainMenuManager.update();
63+
this.menuManager.update();
6664
}
6765

68-
protected register(sketches: Sketch[]): void {
66+
private register(sketches: Sketch[]): void {
6967
const order = 0;
7068
this.toDispose.dispose();
7169
for (const sketch of sketches) {
@@ -88,13 +86,14 @@ export class OpenRecentSketch extends SketchContribution {
8886
},
8987
};
9088
this.commandRegistry.registerCommand(command, handler);
89+
const menuAction = this.assignImage(sketch, {
90+
commandId: command.id,
91+
label: sketch.name,
92+
order: String(order),
93+
});
9194
this.menuRegistry.registerMenuAction(
9295
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
93-
{
94-
commandId: command.id,
95-
label: sketch.name,
96-
order: String(order),
97-
}
96+
menuAction
9897
);
9998
this.toDispose.pushAll([
10099
new DisposableCollection(
@@ -108,4 +107,15 @@ export class OpenRecentSketch extends SketchContribution {
108107
]);
109108
}
110109
}
110+
111+
private assignImage(sketch: Sketch, menuAction: MenuAction): MenuAction {
112+
if (this.cloudImage) {
113+
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
114+
const isCloud = this.createFeatures.isCloud(sketch, dataDirUri);
115+
if (isCloud) {
116+
Object.assign(menuAction, { nativeImage: this.cloudImage });
117+
}
118+
}
119+
return menuAction;
120+
}
111121
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
NativeImage,
3+
nativeImage,
4+
Size,
5+
} from '@theia/core/electron-shared/electron';
6+
import { Endpoint } from '@theia/core/lib/browser/endpoint';
7+
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
8+
import { Deferred } from '@theia/core/lib/common/promise-util';
9+
import { injectable } from '@theia/core/shared/inversify';
10+
import fetch from 'cross-fetch';
11+
12+
const nativeImageIdentifierLiterals = ['cloud'] as const;
13+
export type NativeImageIdentifier =
14+
typeof nativeImageIdentifierLiterals[number];
15+
export const nativeImages: Record<NativeImageIdentifier, string> = {
16+
cloud: 'cloud.png',
17+
};
18+
19+
@injectable()
20+
export class NativeImageCache implements FrontendApplicationContribution {
21+
private readonly cache = new Map<NativeImageIdentifier, NativeImage>();
22+
private readonly loading = new Map<
23+
NativeImageIdentifier,
24+
Promise<NativeImage>
25+
>();
26+
27+
onStart(): void {
28+
Object.keys(nativeImages).forEach((identifier: NativeImageIdentifier) =>
29+
this.getImage(identifier)
30+
);
31+
}
32+
33+
tryGetImage(identifier: NativeImageIdentifier): NativeImage | undefined {
34+
return this.cache.get(identifier);
35+
}
36+
37+
async getImage(identifier: NativeImageIdentifier): Promise<NativeImage> {
38+
const image = this.cache.get(identifier);
39+
if (image) {
40+
return image;
41+
}
42+
let loading = this.loading.get(identifier);
43+
if (!loading) {
44+
const deferred = new Deferred<NativeImage>();
45+
loading = deferred.promise;
46+
this.loading.set(identifier, loading);
47+
this.fetchIconData(identifier).then(
48+
(image) => {
49+
if (!this.cache.has(identifier)) {
50+
this.cache.set(identifier, image);
51+
}
52+
this.loading.delete(identifier);
53+
deferred.resolve(image);
54+
},
55+
(err) => {
56+
this.loading.delete(identifier);
57+
deferred.reject(err);
58+
}
59+
);
60+
}
61+
return loading;
62+
}
63+
64+
private async fetchIconData(
65+
identifier: NativeImageIdentifier
66+
): Promise<NativeImage> {
67+
const path = `nativeImage/${nativeImages[identifier]}`;
68+
const endpoint = new Endpoint({ path }).getRestUrl().toString();
69+
const response = await fetch(endpoint);
70+
const arrayBuffer = await response.arrayBuffer();
71+
const view = new Uint8Array(arrayBuffer);
72+
const buffer = Buffer.alloc(arrayBuffer.byteLength);
73+
buffer.forEach((_, index) => (buffer[index] = view[index]));
74+
const image = nativeImage.createFromBuffer(buffer);
75+
return this.maybeResize(image);
76+
}
77+
78+
private maybeResize(image: NativeImage): NativeImage {
79+
const currentSize = image.getSize();
80+
if (sizeEquals(currentSize, preferredSize)) {
81+
return image;
82+
}
83+
return image.resize(preferredSize);
84+
}
85+
}
86+
87+
const pixel = 16;
88+
const preferredSize: Size = { height: pixel, width: pixel };
89+
function sizeEquals(left: Size, right: Size): boolean {
90+
return left.height === right.height && left.width === right.width;
91+
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as remote from '@theia/core/electron-shared/@electron/remote';
2+
import { NativeImage } from '@theia/core/electron-shared/electron';
23
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
34
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
45
import {
6+
ActionMenuNode,
57
CommandMenuNode,
68
CompoundMenuNode,
79
CompoundMenuNodeRole,
@@ -278,6 +280,12 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
278280
delete menuItem.click;
279281
}
280282
}
283+
284+
// Native image customization for IDE2
285+
if (isMenuNodeWithNativeImage(node)) {
286+
menuItem.icon = node.action.nativeImage;
287+
}
288+
281289
parentItems.push(menuItem);
282290

283291
if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
@@ -314,3 +322,23 @@ const AlwaysVisibleSubmenus: MenuPath[] = [
314322
ArduinoMenus.TOOLS__PORTS_SUBMENU, // #655
315323
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, // #569
316324
];
325+
326+
// Theia does not support icons for electron menu items.
327+
// This is a hack to show a cloud icon as a native image for the cloud sketches in `File` > `Open Recent` menu.
328+
type MenuNodeWithNativeImage = MenuNode & {
329+
action: ActionMenuNode & { nativeImage: NativeImage };
330+
};
331+
type ActionMenuNodeWithNativeImage = ActionMenuNode & {
332+
nativeImage: NativeImage;
333+
};
334+
function isMenuNodeWithNativeImage(
335+
node: MenuNode
336+
): node is MenuNodeWithNativeImage {
337+
if (node instanceof ActionMenuNode) {
338+
const action: unknown = node['action'];
339+
if ((<ActionMenuNodeWithNativeImage>action).nativeImage !== undefined) {
340+
return true;
341+
}
342+
}
343+
return false;
344+
}

‎arduino-ide-extension/src/node/arduino-ide-backend-module.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ import {
118118
LocalDirectoryPluginDeployerResolverWithFallback,
119119
PluginDeployer_GH_12064,
120120
} from './theia/plugin-ext/plugin-deployer';
121+
import { NativeImageDataProvider } from './native-image-data-provider';
121122

122123
export default new ContainerModule((bind, unbind, isBound, rebind) => {
123124
bind(BackendApplication).toSelf().inSingletonScope();
@@ -403,6 +404,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
403404
.toSelf()
404405
.inSingletonScope();
405406
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
407+
408+
// to serve native images for the electron menus
409+
bind(NativeImageDataProvider).toSelf().inSingletonScope();
410+
bind(BackendApplicationContribution).toService(NativeImageDataProvider);
406411
});
407412

408413
function bindChildLogger(bind: interfaces.Bind, name: string): void {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Deferred } from '@theia/core/lib/common/promise-util';
2+
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
3+
import { Application } from '@theia/core/shared/express';
4+
import { injectable } from '@theia/core/shared/inversify';
5+
import { promises as fs } from 'fs';
6+
import { join } from 'path';
7+
import { ErrnoException } from './utils/errors';
8+
9+
@injectable()
10+
export class NativeImageDataProvider implements BackendApplicationContribution {
11+
private readonly rootPath = join(__dirname, '../../src/node/static/icons');
12+
private readonly dataCache = new Map<string, Promise<Buffer | undefined>>();
13+
14+
onStart(): void {
15+
console.log(`Serving native images from ${this.rootPath}`);
16+
}
17+
18+
configure(app: Application): void {
19+
app.get('/nativeImage/:filename', async (req, resp) => {
20+
const filename = req.params.filename;
21+
if (!filename) {
22+
resp.status(400).send('Bad Request');
23+
return;
24+
}
25+
try {
26+
const data = await this.getOrCreateData(filename);
27+
if (!data) {
28+
resp.status(404).send('Not found');
29+
return;
30+
}
31+
resp.send(data);
32+
} catch (err) {
33+
resp.status(500).send(err instanceof Error ? err.message : String(err));
34+
}
35+
});
36+
}
37+
38+
private async getOrCreateData(filename: string): Promise<Buffer | undefined> {
39+
let data = this.dataCache.get(filename);
40+
if (!data) {
41+
const deferred = new Deferred<Buffer | undefined>();
42+
data = deferred.promise;
43+
this.dataCache.set(filename, data);
44+
const path = join(this.rootPath, filename);
45+
fs.readFile(path).then(
46+
(buffer) => deferred.resolve(buffer),
47+
(err) => {
48+
if (ErrnoException.isENOENT(err)) {
49+
console.error(`File not found: ${path}`);
50+
deferred.resolve(undefined);
51+
} else {
52+
console.error(`Failed to load file: ${path}`, err);
53+
this.dataCache.delete(filename);
54+
deferred.reject(err);
55+
}
56+
}
57+
);
58+
}
59+
return data;
60+
}
61+
}
9.24 KB
Loading[フレーム]

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /