mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-17 17:46:33 +00:00
feat: icon for cloud sketch in File > Open Recent
Ref: #1826 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
9b49712669
commit
2e7f2d94bd
@ -356,6 +356,7 @@ import { Account } from './contributions/account';
|
|||||||
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
|
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
|
||||||
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
|
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
|
||||||
import { CreateCloudCopy } from './contributions/create-cloud-copy';
|
import { CreateCloudCopy } from './contributions/create-cloud-copy';
|
||||||
|
import { NativeImageCache } from './native-image-cache';
|
||||||
|
|
||||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
// Commands and toolbar items
|
// Commands and toolbar items
|
||||||
@ -1034,4 +1035,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
bind(FrontendApplicationContribution).toService(DaemonPort);
|
bind(FrontendApplicationContribution).toService(DaemonPort);
|
||||||
bind(IsOnline).toSelf().inSingletonScope();
|
bind(IsOnline).toSelf().inSingletonScope();
|
||||||
bind(FrontendApplicationContribution).toService(IsOnline);
|
bind(FrontendApplicationContribution).toService(IsOnline);
|
||||||
|
// manages native images for the electron menu icons
|
||||||
|
bind(NativeImageCache).toSelf().inSingletonScope();
|
||||||
|
bind(FrontendApplicationContribution).toService(NativeImageCache);
|
||||||
});
|
});
|
||||||
|
@ -1,57 +1,49 @@
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { NativeImage } from '@theia/core/electron-shared/electron';
|
||||||
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
|
||||||
import {
|
import {
|
||||||
Disposable,
|
Disposable,
|
||||||
DisposableCollection,
|
DisposableCollection,
|
||||||
} from '@theia/core/lib/common/disposable';
|
} from '@theia/core/lib/common/disposable';
|
||||||
import {
|
import { MenuAction } from '@theia/core/lib/common/menu';
|
||||||
SketchContribution,
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
CommandRegistry,
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
MenuModelRegistry,
|
|
||||||
Sketch,
|
|
||||||
} from './contribution';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
|
||||||
import { OpenSketch } from './open-sketch';
|
|
||||||
import { NotificationCenter } from '../notification-center';
|
|
||||||
import { nls } from '@theia/core/lib/common';
|
|
||||||
import { SketchesError } from '../../common/protocol';
|
import { SketchesError } from '../../common/protocol';
|
||||||
|
import { ConfigServiceClient } from '../config/config-service-client';
|
||||||
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { NativeImageCache } from '../native-image-cache';
|
||||||
|
import { NotificationCenter } from '../notification-center';
|
||||||
|
import { CloudSketchContribution } from './cloud-contribution';
|
||||||
|
import { CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
|
||||||
|
import { OpenSketch } from './open-sketch';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenRecentSketch extends SketchContribution {
|
export class OpenRecentSketch extends CloudSketchContribution {
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
private readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
private readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@inject(MainMenuManager)
|
|
||||||
protected readonly mainMenuManager: MainMenuManager;
|
|
||||||
|
|
||||||
@inject(WorkspaceServer)
|
|
||||||
protected readonly workspaceServer: WorkspaceServer;
|
|
||||||
|
|
||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
private readonly notificationCenter: NotificationCenter;
|
||||||
|
@inject(NativeImageCache)
|
||||||
|
private readonly imageCache: NativeImageCache;
|
||||||
|
@inject(ConfigServiceClient)
|
||||||
|
private readonly configServiceClient: ConfigServiceClient;
|
||||||
|
|
||||||
protected toDispose = new DisposableCollection();
|
private readonly toDispose = new DisposableCollection();
|
||||||
|
private cloudImage: NativeImage | undefined;
|
||||||
|
|
||||||
override onStart(): void {
|
override onStart(): void {
|
||||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||||
this.refreshMenu(sketches)
|
this.refreshMenu(sketches)
|
||||||
);
|
);
|
||||||
|
this.imageCache
|
||||||
|
.getImage('cloud')
|
||||||
|
.then((image) => (this.cloudImage = image));
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onReady(): Promise<void> {
|
override async onReady(): Promise<void> {
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
private update(forceUpdate?: boolean): void {
|
|
||||||
this.sketchesService
|
|
||||||
.recentlyOpenedSketches(forceUpdate)
|
|
||||||
.then((sketches) => this.refreshMenu(sketches));
|
|
||||||
}
|
|
||||||
|
|
||||||
override registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerSubmenu(
|
registry.registerSubmenu(
|
||||||
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||||
@ -60,12 +52,18 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshMenu(sketches: Sketch[]): void {
|
private update(forceUpdate?: boolean): void {
|
||||||
this.register(sketches);
|
this.sketchesService
|
||||||
this.mainMenuManager.update();
|
.recentlyOpenedSketches(forceUpdate)
|
||||||
|
.then((sketches) => this.refreshMenu(sketches));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected register(sketches: Sketch[]): void {
|
private refreshMenu(sketches: Sketch[]): void {
|
||||||
|
this.register(sketches);
|
||||||
|
this.menuManager.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private register(sketches: Sketch[]): void {
|
||||||
const order = 0;
|
const order = 0;
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
for (const sketch of sketches) {
|
for (const sketch of sketches) {
|
||||||
@ -88,13 +86,14 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.commandRegistry.registerCommand(command, handler);
|
||||||
|
const menuAction = this.assignImage(sketch, {
|
||||||
|
commandId: command.id,
|
||||||
|
label: sketch.name,
|
||||||
|
order: String(order),
|
||||||
|
});
|
||||||
this.menuRegistry.registerMenuAction(
|
this.menuRegistry.registerMenuAction(
|
||||||
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||||
{
|
menuAction
|
||||||
commandId: command.id,
|
|
||||||
label: sketch.name,
|
|
||||||
order: String(order),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
this.toDispose.pushAll([
|
this.toDispose.pushAll([
|
||||||
new DisposableCollection(
|
new DisposableCollection(
|
||||||
@ -108,4 +107,15 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private assignImage(sketch: Sketch, menuAction: MenuAction): MenuAction {
|
||||||
|
if (this.cloudImage) {
|
||||||
|
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
|
||||||
|
const isCloud = this.createFeatures.isCloud(sketch, dataDirUri);
|
||||||
|
if (isCloud) {
|
||||||
|
Object.assign(menuAction, { nativeImage: this.cloudImage });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return menuAction;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
91
arduino-ide-extension/src/browser/native-image-cache.ts
Normal file
91
arduino-ide-extension/src/browser/native-image-cache.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
NativeImage,
|
||||||
|
nativeImage,
|
||||||
|
Size,
|
||||||
|
} from '@theia/core/electron-shared/electron';
|
||||||
|
import { Endpoint } from '@theia/core/lib/browser/endpoint';
|
||||||
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||||
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import fetch from 'cross-fetch';
|
||||||
|
|
||||||
|
const nativeImageIdentifierLiterals = ['cloud'] as const;
|
||||||
|
export type NativeImageIdentifier =
|
||||||
|
typeof nativeImageIdentifierLiterals[number];
|
||||||
|
export const nativeImages: Record<NativeImageIdentifier, string> = {
|
||||||
|
cloud: 'cloud.png',
|
||||||
|
};
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class NativeImageCache implements FrontendApplicationContribution {
|
||||||
|
private readonly cache = new Map<NativeImageIdentifier, NativeImage>();
|
||||||
|
private readonly loading = new Map<
|
||||||
|
NativeImageIdentifier,
|
||||||
|
Promise<NativeImage>
|
||||||
|
>();
|
||||||
|
|
||||||
|
onStart(): void {
|
||||||
|
Object.keys(nativeImages).forEach((identifier: NativeImageIdentifier) =>
|
||||||
|
this.getImage(identifier)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tryGetImage(identifier: NativeImageIdentifier): NativeImage | undefined {
|
||||||
|
return this.cache.get(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImage(identifier: NativeImageIdentifier): Promise<NativeImage> {
|
||||||
|
const image = this.cache.get(identifier);
|
||||||
|
if (image) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
let loading = this.loading.get(identifier);
|
||||||
|
if (!loading) {
|
||||||
|
const deferred = new Deferred<NativeImage>();
|
||||||
|
loading = deferred.promise;
|
||||||
|
this.loading.set(identifier, loading);
|
||||||
|
this.fetchIconData(identifier).then(
|
||||||
|
(image) => {
|
||||||
|
if (!this.cache.has(identifier)) {
|
||||||
|
this.cache.set(identifier, image);
|
||||||
|
}
|
||||||
|
this.loading.delete(identifier);
|
||||||
|
deferred.resolve(image);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.loading.delete(identifier);
|
||||||
|
deferred.reject(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchIconData(
|
||||||
|
identifier: NativeImageIdentifier
|
||||||
|
): Promise<NativeImage> {
|
||||||
|
const path = `nativeImage/${nativeImages[identifier]}`;
|
||||||
|
const endpoint = new Endpoint({ path }).getRestUrl().toString();
|
||||||
|
const response = await fetch(endpoint);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const view = new Uint8Array(arrayBuffer);
|
||||||
|
const buffer = Buffer.alloc(arrayBuffer.byteLength);
|
||||||
|
buffer.forEach((_, index) => (buffer[index] = view[index]));
|
||||||
|
const image = nativeImage.createFromBuffer(buffer);
|
||||||
|
return this.maybeResize(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
private maybeResize(image: NativeImage): NativeImage {
|
||||||
|
const currentSize = image.getSize();
|
||||||
|
if (sizeEquals(currentSize, preferredSize)) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
return image.resize(preferredSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixel = 16;
|
||||||
|
const preferredSize: Size = { height: pixel, width: pixel };
|
||||||
|
function sizeEquals(left: Size, right: Size): boolean {
|
||||||
|
return left.height === right.height && left.width === right.width;
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
|
import { NativeImage } from '@theia/core/electron-shared/electron';
|
||||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
import {
|
import {
|
||||||
|
ActionMenuNode,
|
||||||
CommandMenuNode,
|
CommandMenuNode,
|
||||||
CompoundMenuNode,
|
CompoundMenuNode,
|
||||||
CompoundMenuNodeRole,
|
CompoundMenuNodeRole,
|
||||||
@ -278,6 +280,12 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
|||||||
delete menuItem.click;
|
delete menuItem.click;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Native image customization for IDE2
|
||||||
|
if (isMenuNodeWithNativeImage(node)) {
|
||||||
|
menuItem.icon = node.action.nativeImage;
|
||||||
|
}
|
||||||
|
|
||||||
parentItems.push(menuItem);
|
parentItems.push(menuItem);
|
||||||
|
|
||||||
if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
|
if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
|
||||||
@ -314,3 +322,23 @@ const AlwaysVisibleSubmenus: MenuPath[] = [
|
|||||||
ArduinoMenus.TOOLS__PORTS_SUBMENU, // #655
|
ArduinoMenus.TOOLS__PORTS_SUBMENU, // #655
|
||||||
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, // #569
|
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, // #569
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Theia does not support icons for electron menu items.
|
||||||
|
// This is a hack to show a cloud icon as a native image for the cloud sketches in `File` > `Open Recent` menu.
|
||||||
|
type MenuNodeWithNativeImage = MenuNode & {
|
||||||
|
action: ActionMenuNode & { nativeImage: NativeImage };
|
||||||
|
};
|
||||||
|
type ActionMenuNodeWithNativeImage = ActionMenuNode & {
|
||||||
|
nativeImage: NativeImage;
|
||||||
|
};
|
||||||
|
function isMenuNodeWithNativeImage(
|
||||||
|
node: MenuNode
|
||||||
|
): node is MenuNodeWithNativeImage {
|
||||||
|
if (node instanceof ActionMenuNode) {
|
||||||
|
const action: unknown = node['action'];
|
||||||
|
if ((<ActionMenuNodeWithNativeImage>action).nativeImage !== undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@ -119,6 +119,7 @@ import {
|
|||||||
PluginDeployer_GH_12064,
|
PluginDeployer_GH_12064,
|
||||||
} from './theia/plugin-ext/plugin-deployer';
|
} from './theia/plugin-ext/plugin-deployer';
|
||||||
import { SettingsReader } from './settings-reader';
|
import { SettingsReader } from './settings-reader';
|
||||||
|
import { NativeImageDataProvider } from './native-image-data-provider';
|
||||||
|
|
||||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
bind(BackendApplication).toSelf().inSingletonScope();
|
bind(BackendApplication).toSelf().inSingletonScope();
|
||||||
@ -406,6 +407,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
|
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
|
||||||
|
|
||||||
bind(SettingsReader).toSelf().inSingletonScope();
|
bind(SettingsReader).toSelf().inSingletonScope();
|
||||||
|
// to serve native images for the electron menus
|
||||||
|
bind(NativeImageDataProvider).toSelf().inSingletonScope();
|
||||||
|
bind(BackendApplicationContribution).toService(NativeImageDataProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
function bindChildLogger(bind: interfaces.Bind, name: string): void {
|
function bindChildLogger(bind: interfaces.Bind, name: string): void {
|
||||||
|
61
arduino-ide-extension/src/node/native-image-data-provider.ts
Normal file
61
arduino-ide-extension/src/node/native-image-data-provider.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
|
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||||
|
import { Application } from '@theia/core/shared/express';
|
||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { ErrnoException } from './utils/errors';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class NativeImageDataProvider implements BackendApplicationContribution {
|
||||||
|
private readonly rootPath = join(__dirname, '../../src/node/static/icons');
|
||||||
|
private readonly dataCache = new Map<string, Promise<Buffer | undefined>>();
|
||||||
|
|
||||||
|
onStart(): void {
|
||||||
|
console.log(`Serving native images from ${this.rootPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(app: Application): void {
|
||||||
|
app.get('/nativeImage/:filename', async (req, resp) => {
|
||||||
|
const filename = req.params.filename;
|
||||||
|
if (!filename) {
|
||||||
|
resp.status(400).send('Bad Request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await this.getOrCreateData(filename);
|
||||||
|
if (!data) {
|
||||||
|
resp.status(404).send('Not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.send(data);
|
||||||
|
} catch (err) {
|
||||||
|
resp.status(500).send(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateData(filename: string): Promise<Buffer | undefined> {
|
||||||
|
let data = this.dataCache.get(filename);
|
||||||
|
if (!data) {
|
||||||
|
const deferred = new Deferred<Buffer | undefined>();
|
||||||
|
data = deferred.promise;
|
||||||
|
this.dataCache.set(filename, data);
|
||||||
|
const path = join(this.rootPath, filename);
|
||||||
|
fs.readFile(path).then(
|
||||||
|
(buffer) => deferred.resolve(buffer),
|
||||||
|
(err) => {
|
||||||
|
if (ErrnoException.isENOENT(err)) {
|
||||||
|
console.error(`File not found: ${path}`);
|
||||||
|
deferred.resolve(undefined);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to load file: ${path}`, err);
|
||||||
|
this.dataCache.delete(filename);
|
||||||
|
deferred.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
BIN
arduino-ide-extension/src/node/static/icons/cloud.png
Normal file
BIN
arduino-ide-extension/src/node/static/icons/cloud.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
Loading…
x
Reference in New Issue
Block a user