Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for icon contribution by extensions #2984

Merged
merged 1 commit into from Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/main/src/plugin/api/font-info.ts
@@ -0,0 +1,32 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

export interface FontSource {
readonly location: string;
readonly format: string;
}

export interface FontDefinition {
readonly fontId: string;
readonly src: FontSource[];
}

export interface FontInfo {
id: string;
definition: FontDefinition;
}
30 changes: 30 additions & 0 deletions packages/main/src/plugin/api/icon-info.ts
@@ -0,0 +1,30 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { FontDefinition } from './font-info.js';

export interface IconDefinition {
description?: string;
font?: FontDefinition;
fontCharacter: string;
}

export interface IconInfo {
id: string;
definition: IconDefinition;
}
2 changes: 2 additions & 0 deletions packages/main/src/plugin/authentication.spec.ts
Expand Up @@ -44,6 +44,7 @@ import type { StatusBarRegistry } from './statusbar/statusbar-registry.js';
import type { Telemetry } from './telemetry/telemetry.js';
import type { TrayMenuRegistry } from './tray-menu-registry.js';
import type { Proxy } from './proxy.js';
import type { IconRegistry } from './icon-registry.js';

function randomNumber(n = 5) {
return Math.round(Math.random() * 10 * n);
Expand Down Expand Up @@ -243,6 +244,7 @@ suite('Authentication', () => {
vi.fn() as unknown as ContainerProviderRegistry,
vi.fn() as unknown as InputQuickPickRegistry,
authentication,
vi.fn() as unknown as IconRegistry,
vi.fn() as unknown as Telemetry,
);
providerMock = {
Expand Down
4 changes: 4 additions & 0 deletions packages/main/src/plugin/extension-loader.spec.ts
Expand Up @@ -42,6 +42,7 @@ import type { AuthenticationImpl } from './authentication.js';
import type { MessageBox } from './message-box.js';
import type { Telemetry } from './telemetry/telemetry.js';
import type * as containerDesktopAPI from '@podman-desktop/api';
import type { IconRegistry } from './icon-registry.js';

class TestExtensionLoader extends ExtensionLoader {
public async setupScanningDirectory(): Promise<void> {
Expand Down Expand Up @@ -97,6 +98,8 @@ const inputQuickPickRegistry: InputQuickPickRegistry = {} as unknown as InputQui

const authenticationProviderRegistry: AuthenticationImpl = {} as unknown as AuthenticationImpl;

const iconRegistry: IconRegistry = {} as unknown as IconRegistry;

const telemetryTrackMock = vi.fn();
const telemetry: Telemetry = { track: telemetryTrackMock } as unknown as Telemetry;

Expand All @@ -120,6 +123,7 @@ beforeAll(() => {
containerProviderRegistry,
inputQuickPickRegistry,
authenticationProviderRegistry,
iconRegistry,
telemetry,
);
});
Expand Down
7 changes: 7 additions & 0 deletions packages/main/src/plugin/extension-loader.ts
Expand Up @@ -56,6 +56,7 @@ import type { Telemetry } from './telemetry/telemetry.js';
import { TelemetryTrustedValue } from './types/telemetry.js';
import { clipboard as electronClipboard } from 'electron';
import { securityRestrictionCurrentHandler } from '../security-restrictions-handler.js';
import type { IconRegistry } from './icon-registry.js';

/**
* Handle the loading of an extension
Expand Down Expand Up @@ -128,6 +129,7 @@ export class ExtensionLoader {
private containerProviderRegistry: ContainerProviderRegistry,
private inputQuickPickRegistry: InputQuickPickRegistry,
private authenticationProviderRegistry: AuthenticationImpl,
private iconRegistry: IconRegistry,
private telemetry: Telemetry,
) {}

Expand Down Expand Up @@ -516,6 +518,11 @@ export class ExtensionLoader {
this.menuRegistry.registerMenus(menus);
}

const icons = extension.manifest?.contributes?.icons;
if (icons) {
this.iconRegistry.registerIconContribution(extension, icons);
}

this.analyzedExtensions.set(extension.id, extension);
this.extensionState.delete(extension.id);
this.extensionStateErrors.delete(extension.id);
Expand Down
81 changes: 81 additions & 0 deletions packages/main/src/plugin/icon-registry.spec.ts
@@ -0,0 +1,81 @@
/**********************************************************************
* Copyright (C) 2022 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import { beforeAll, beforeEach, expect, test, vi } from 'vitest';
import type { ApiSenderType } from './api.js';

import { IconRegistry } from './icon-registry.js';
import type { AnalyzedExtension } from './extension-loader.js';

let iconRegistry: IconRegistry;
const apiSenderSendMock = vi.fn();

beforeAll(async () => {
iconRegistry = new IconRegistry({
send: apiSenderSendMock,
} as unknown as ApiSenderType);
});

beforeEach(() => {
vi.clearAllMocks();
});

test('should register icon contribution', async () => {
const fontPath = 'bootstrap-icons.woff2';
const fontCharacter = '\\F844';
const iconDescription = 'This is my icon';
const icons = {
'my-icon-id': {
description: iconDescription,
default: {
fontPath,
fontCharacter,
},
},
};

const extensionPath = '/root/path';
const extensionId = 'myextension.id';
const extension = {
path: extensionPath,
id: extensionId,
} as AnalyzedExtension;

// register icons
iconRegistry.registerIconContribution(extension, icons);

// expect to have registered the icon
expect(apiSenderSendMock).toHaveBeenCalledWith('icon-update');
expect(apiSenderSendMock).toHaveBeenCalledWith('font-update');

// grab the icons
const allIcons = iconRegistry.listIcons();
expect(allIcons).toHaveLength(1);
const icon = allIcons[0];
expect(icon.id).toBe('my-icon-id');
expect(icon.definition.fontCharacter).toBe(fontCharacter);
expect(icon.definition.description).toBe(iconDescription);
expect(icon.definition.font).toBeDefined();
expect(icon.definition.font?.src).toStrictEqual([
{
format: 'woff2',
location: `${extensionPath}/${fontPath}`,
},
]);
expect(icon.definition.font?.fontId).toBe(`${extensionId}-${fontPath}`);
});
122 changes: 122 additions & 0 deletions packages/main/src/plugin/icon-registry.ts
@@ -0,0 +1,122 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { ApiSenderType } from './api.js';
import type { FontDefinition } from './api/font-info.js';
import type { IconDefinition, IconInfo } from './api/icon-info.js';
import type { AnalyzedExtension } from './extension-loader.js';
import { join } from 'node:path';

export class IconRegistry {
private icons: Map<string, IconDefinition>;
private fonts: Map<string, FontDefinition>;

constructor(private apiSender: ApiSenderType) {
this.icons = new Map();
this.fonts = new Map();
}

protected registerIcon(iconId: string, definition: IconDefinition): void {
if (this.icons.has(iconId)) {
console.warn(`Icon ${iconId} already registered.`);
return;
}
this.icons.set(iconId, definition);
this.apiSender.send('icon-update');
}

protected registerFont(fontId: string, definition: FontDefinition): void {
if (this.fonts.has(fontId)) {
console.warn(`Font ${fontId} already registered.`);
return;
}
this.fonts.set(fontId, definition);
this.apiSender.send('font-update');
}

public registerIconContribution(
extension: AnalyzedExtension,
icons: { [key: string]: { description?: string; default?: { fontPath?: string; fontCharacter: string } } },
): void {
// register each font and icon
Object.entries(icons).forEach(([iconId, iconContribution]) => {
// is there any default icon?
const defaultAttributes = iconContribution.default;
if (!defaultAttributes) {
console.warn(`Expected contributes.icons.default for icon id ${iconId} to be defined.`);
return;
}
// check we have a font character
if (!defaultAttributes.fontCharacter) {
console.warn(`Expected contributes.icons.default.fontCharacter for icon id ${iconId} to be defined.`);
return;
}

// font Path ?
if (!defaultAttributes.fontPath) {
console.warn(`Expected contributes.icons.default.fontPath for icon id ${iconId} to be defined.`);
return;
}

// get file extension of the font path
const format = defaultAttributes.fontPath.split('.').pop() || '';
if (format !== 'woff2') {
console.warn(
`Expected contributes.icons.default.fontPath to have file extension 'woff2' but found '${format}'."`,
);
return;
}

const iconFontLocation = join(extension.path, defaultAttributes.fontPath);

// check that this location is inside the extension folder
if (!iconFontLocation.startsWith(extension.path)) {
console.warn(
`Expected contributes.icons.default.fontPath for icon id ${iconId} to be included inside extension's folder (${extension.path}).`,
);
return;
}

// fontId is based on the extension id and the font path
const fontId = `${extension.id}-${defaultAttributes.fontPath}`;

// font definition
const fontDefinition: FontDefinition = {
fontId,
src: [{ location: iconFontLocation, format }],
};

// register font
this.registerFont(fontId, fontDefinition);

// icon definition
const iconDefinition: IconDefinition = {
description: iconContribution.description,
fontCharacter: defaultAttributes.fontCharacter,
font: fontDefinition,
};

// and now register the icon
this.registerIcon(iconId, iconDefinition);
});
}

listIcons(): IconInfo[] {
return Array.from(this.icons.entries()).map(([id, definition]) => ({ id, definition }));
}
}
9 changes: 9 additions & 0 deletions packages/main/src/plugin/index.ts
Expand Up @@ -106,6 +106,8 @@ import { ExtensionsCatalog } from './extensions-catalog/extensions-catalog.js';
import { securityRestrictionCurrentHandler } from '../security-restrictions-handler.js';
import { ExtensionsUpdater } from './extensions-updater/extensions-updater.js';
import type { CatalogExtension } from './extensions-catalog/extensions-catalog-api.js';
import { IconRegistry } from './icon-registry.js';
import type { IconInfo } from './api/icon-info.js';

type LogType = 'log' | 'warn' | 'trace' | 'debug' | 'error';

Expand Down Expand Up @@ -342,6 +344,8 @@ export class PluginSystem {
// init api sender
const apiSender = this.getApiSender(this.getWebContentsSender());

const iconRegistry = new IconRegistry(apiSender);

const configurationRegistry = new ConfigurationRegistry();
configurationRegistry.init();

Expand Down Expand Up @@ -652,6 +656,7 @@ export class PluginSystem {
containerProviderRegistry,
inputQuickPickRegistry,
authentication,
iconRegistry,
telemetry,
);
await this.extensionLoader.init();
Expand Down Expand Up @@ -1587,6 +1592,10 @@ export class PluginSystem {
return app.getVersion();
});

this.ipcHandle('iconRegistry:listIcons', async (): Promise<IconInfo[]> => {
return iconRegistry.listIcons();
});

this.ipcHandle('window:minimize', async (): Promise<void> => {
const window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed());
if (!window) {
Expand Down
5 changes: 5 additions & 0 deletions packages/preload/src/index.ts
Expand Up @@ -33,6 +33,7 @@ import type { ImageInspectInfo } from '../../main/src/plugin/api/image-inspect-i
import type { HistoryInfo } from '../../main/src/plugin/api/history-info';
import type { ContainerInspectInfo } from '../../main/src/plugin/api/container-inspect-info';
import type { ContainerStatsInfo } from '../../main/src/plugin/api/container-stats-info';
import type { IconInfo } from '../../main/src/plugin/api/icon-info';
import type { ExtensionInfo } from '../../main/src/plugin/api/extension-info';
import type { FeaturedExtension } from '../../main/src/plugin/featured/featured-api';
import type { CatalogExtension } from '../../main/src/plugin/extensions-catalog/extensions-catalog-api';
Expand Down Expand Up @@ -912,6 +913,10 @@ function initExposure(): void {
return ipcInvoke('contributions:listContributions');
});

contextBridge.exposeInMainWorld('listIcons', async (): Promise<IconInfo[]> => {
return ipcInvoke('iconRegistry:listIcons');
});

// Handle callback to open devtools for extensions
// by delegating to the renderer process
ipcRenderer.on('dev-tools:open-extension', (_, extensionId: string) => {
Expand Down