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 as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
|
||||
import { CreateCloudCopy } from './contributions/create-cloud-copy';
|
||||
import { NativeImageCache } from './native-image-cache';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Commands and toolbar items
|
||||
@ -1034,4 +1035,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(DaemonPort);
|
||||
bind(IsOnline).toSelf().inSingletonScope();
|
||||
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 { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
||||
import { NativeImage } from '@theia/core/electron-shared/electron';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
SketchContribution,
|
||||
CommandRegistry,
|
||||
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 { MenuAction } from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
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()
|
||||
export class OpenRecentSketch extends SketchContribution {
|
||||
export class OpenRecentSketch extends CloudSketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(WorkspaceServer)
|
||||
protected readonly workspaceServer: WorkspaceServer;
|
||||
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@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 {
|
||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||
this.refreshMenu(sketches)
|
||||
);
|
||||
this.imageCache
|
||||
.getImage('cloud')
|
||||
.then((image) => (this.cloudImage = image));
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchesService
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||
@ -60,12 +52,18 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
);
|
||||
}
|
||||
|
||||
private refreshMenu(sketches: Sketch[]): void {
|
||||
this.register(sketches);
|
||||
this.mainMenuManager.update();
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchesService
|
||||
.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;
|
||||
this.toDispose.dispose();
|
||||
for (const sketch of sketches) {
|
||||
@ -88,13 +86,14 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
},
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
const menuAction = this.assignImage(sketch, {
|
||||
commandId: command.id,
|
||||
label: sketch.name,
|
||||
order: String(order),
|
||||
});
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||
{
|
||||
commandId: command.id,
|
||||
label: sketch.name,
|
||||
order: String(order),
|
||||
}
|
||||
menuAction
|
||||
);
|
||||
this.toDispose.pushAll([
|
||||
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 { NativeImage } from '@theia/core/electron-shared/electron';
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import {
|
||||
ActionMenuNode,
|
||||
CommandMenuNode,
|
||||
CompoundMenuNode,
|
||||
CompoundMenuNodeRole,
|
||||
@ -278,6 +280,12 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
||||
delete menuItem.click;
|
||||
}
|
||||
}
|
||||
|
||||
// Native image customization for IDE2
|
||||
if (isMenuNodeWithNativeImage(node)) {
|
||||
menuItem.icon = node.action.nativeImage;
|
||||
}
|
||||
|
||||
parentItems.push(menuItem);
|
||||
|
||||
if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
|
||||
@ -314,3 +322,23 @@ const AlwaysVisibleSubmenus: MenuPath[] = [
|
||||
ArduinoMenus.TOOLS__PORTS_SUBMENU, // #655
|
||||
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,
|
||||
} from './theia/plugin-ext/plugin-deployer';
|
||||
import { SettingsReader } from './settings-reader';
|
||||
import { NativeImageDataProvider } from './native-image-data-provider';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BackendApplication).toSelf().inSingletonScope();
|
||||
@ -406,6 +407,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
rebind(PluginDeployer).to(PluginDeployer_GH_12064).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 {
|
||||
|
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