Skip to content

Commit 754eb84

Browse files
authored
Implement stubbed window#registerURIhandler (#13306)
fixes #13169 contributed on behalf of STMicroelectronics Signed-off-by: Remi Schnekenburger <[email protected]>
1 parent 2fe7d5d commit 754eb84

21 files changed

+343
-39
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)
66

7-
87
## Unreleased
8+
- [plugin] implement stubbed API window registerUriHandler() [#13306](https://github.com/eclipse-theia/theia/pull/13306) - contributed on behalf of STMicroelectronics
99
- [core] Download json schema catalog at build-time - [#14065](https://github.com/eclipse-theia/theia/pull/14065/) - Contributed on behalf of STMicroelectronics
1010

1111
<a name="breaking_changes_1.53.0">[Breaking Changes:](#breaking_changes_1.53.0)</a>
1212
- [dependencies] Updated electron to version 30.1.2 - [#14041](https://github.com/eclipse-theia/theia/pull/14041) - Contributed on behalf of STMicroelectronics
1313
- [dependencies] increased minimum node version to 18. [#14027](https://github.com/eclipse-theia/theia/pull/14027) - Contributed on behalf of STMicroelectronics
1414

15+
1516
## 1.52.0 - 07/25/2024
1617

1718
- [application-package] bumped the default supported API from `1.90.2` to `1.91.1` [#13955](https://github.com/eclipse-theia/theia/pull/13955) - Contributed on behalf of STMicroelectronics

dev-packages/application-package/src/application-props.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export namespace ElectronFrontendApplicationConfig {
3434
export const DEFAULT: ElectronFrontendApplicationConfig = {
3535
windowOptions: {},
3636
showWindowEarly: true,
37-
splashScreenOptions: {}
37+
splashScreenOptions: {},
38+
uriScheme: 'theia'
3839
};
3940
export interface SplashScreenOptions {
4041
/**
@@ -85,6 +86,11 @@ export namespace ElectronFrontendApplicationConfig {
8586
* Defaults to `{}` which results in no splash screen being displayed.
8687
*/
8788
readonly splashScreenOptions?: SplashScreenOptions;
89+
90+
/**
91+
* The custom uri scheme the application registers to in the operating system.
92+
*/
93+
readonly uriScheme: string;
8894
}
8995
}
9096

@@ -122,7 +128,8 @@ export namespace FrontendApplicationConfig {
122128
electron: ElectronFrontendApplicationConfig.DEFAULT,
123129
defaultLocale: '',
124130
validatePreferencesSchema: true,
125-
reloadOnReconnect: false
131+
reloadOnReconnect: false,
132+
uriScheme: 'theia'
126133
};
127134
export interface Partial extends ApplicationConfig {
128135

packages/core/src/browser/frontend-application-module.ts

-1
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,6 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is
465465
bind(FrontendApplicationContribution).toService(StylingService);
466466

467467
bind(SecondaryWindowHandler).toSelf().inSingletonScope();
468-
469468
bind(ViewColumnService).toSelf().inSingletonScope();
470469

471470
bind(UndoRedoHandlerService).toSelf().inSingletonScope();

packages/core/src/browser/opener-service.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ export interface OpenerService {
7979
* Add open handler i.e. for custom editors
8080
*/
8181
addHandler?(openHandler: OpenHandler): Disposable;
82+
83+
/**
84+
* Remove open handler
85+
*/
86+
removeHandler?(openHandler: OpenHandler): void;
87+
8288
/**
8389
* Event that fires when a new opener is added or removed.
8490
*/
@@ -108,11 +114,15 @@ export class DefaultOpenerService implements OpenerService {
108114
this.onDidChangeOpenersEmitter.fire();
109115

110116
return Disposable.create(() => {
111-
this.customEditorOpenHandlers.splice(this.customEditorOpenHandlers.indexOf(openHandler), 1);
112-
this.onDidChangeOpenersEmitter.fire();
117+
this.removeHandler(openHandler);
113118
});
114119
}
115120

121+
removeHandler(openHandler: OpenHandler): void {
122+
this.customEditorOpenHandlers.splice(this.customEditorOpenHandlers.indexOf(openHandler), 1);
123+
this.onDidChangeOpenersEmitter.fire();
124+
}
125+
116126
async getOpener(uri: URI, options?: OpenerOptions): Promise<OpenHandler> {
117127
const handlers = await this.prioritize(uri, options);
118128
if (handlers.length >= 1) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 STMicroelectronics and others.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { FrontendApplicationContribution, OpenerService } from '../browser';
18+
19+
import { injectable, inject } from 'inversify';
20+
import { URI } from '../common';
21+
22+
@injectable()
23+
export class ElectronUriHandlerContribution implements FrontendApplicationContribution {
24+
@inject(OpenerService)
25+
protected readonly openenerService: OpenerService;
26+
27+
initialize(): void {
28+
window.electronTheiaCore.setOpenUrlHandler(async url => {
29+
const uri = new URI(url);
30+
try {
31+
const handler = await this.openenerService.getOpener(uri);
32+
if (handler) {
33+
await handler.open(uri);
34+
return true;
35+
}
36+
} catch (e) {
37+
// no handler
38+
}
39+
return false;
40+
});
41+
}
42+
}

packages/core/src/electron-browser/preload.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import {
2626
CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART,
2727
CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD,
2828
CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE, CHANNEL_SET_BACKGROUND_COLOR,
29-
CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP
29+
CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP,
30+
CHANNEL_OPEN_URL
3031
} from '../electron-common/electron-api';
3132

3233
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -38,6 +39,16 @@ let nextHandlerId = 0;
3839
const mainMenuId = 0;
3940
let nextMenuId = mainMenuId + 1;
4041

42+
let openUrlHandler: ((url: string) => Promise<boolean>) | undefined;
43+
44+
ipcRenderer.on(CHANNEL_OPEN_URL, async (event: Electron.IpcRendererEvent, url: string, replyChannel: string) => {
45+
if (openUrlHandler) {
46+
event.sender.send(replyChannel, await openUrlHandler(url));
47+
} else {
48+
event.sender.send(replyChannel, false);
49+
}
50+
});
51+
4152
function convertMenu(menu: MenuDto[] | undefined, handlerMap: Map<number, () => void>): InternalMenuDto[] | undefined {
4253
if (!menu) {
4354
return undefined;
@@ -135,6 +146,10 @@ const api: TheiaCoreAPI = {
135146
return Disposable.create(() => ipcRenderer.off(CHANNEL_ABOUT_TO_CLOSE, h));
136147
},
137148

149+
setOpenUrlHandler(handler: (url: string) => Promise<boolean>): void {
150+
openUrlHandler = handler;
151+
},
152+
138153
onWindowEvent: function (event: WindowEvent, handler: () => void): Disposable {
139154
const h = (_event: unknown, evt: WindowEvent) => {
140155
if (event === evt) {

packages/core/src/electron-browser/window/electron-window-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ElectronSecondaryWindowService } from './electron-secondary-window-serv
2929
import { bindWindowPreferences } from './electron-window-preferences';
3030
import { ElectronWindowService } from './electron-window-service';
3131
import { ExternalAppOpenHandler } from './external-app-open-handler';
32+
import { ElectronUriHandlerContribution } from '../electron-uri-handler';
3233

3334
export default new ContainerModule((bind, unbind, isBound, rebind) => {
3435
bind(ElectronMainWindowService).toDynamicValue(context =>
@@ -37,6 +38,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
3738
bindWindowPreferences(bind);
3839
bind(WindowService).to(ElectronWindowService).inSingletonScope();
3940
bind(FrontendApplicationContribution).toService(WindowService);
41+
bind(ElectronUriHandlerContribution).toSelf().inSingletonScope();
42+
bind(FrontendApplicationContribution).toService(ElectronUriHandlerContribution);
4043
bind(ClipboardService).to(ElectronClipboardService).inSingletonScope();
4144
rebind(FrontendApplicationStateService).to(ElectronFrontendApplicationStateService).inSingletonScope();
4245
bind(SecondaryWindowService).to(ElectronSecondaryWindowService).inSingletonScope();

packages/core/src/electron-browser/window/external-app-open-handler.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ExternalAppOpenHandler implements OpenHandler {
3636
async open(uri: URI): Promise<undefined> {
3737
// For files 'file:' scheme, system accepts only the path.
3838
// For other protocols e.g. 'vscode:' we use the full URI to propagate target app information.
39-
window.electronTheiaCore.openWithSystemApp(uri.scheme === 'file' ? uri.path.fsPath() : uri.toString(true));
39+
window.electronTheiaCore.openWithSystemApp(uri.toString(true));
4040
return undefined;
4141
}
4242
}

packages/core/src/electron-common/electron-api.ts

+3
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export interface TheiaCoreAPI {
7474
onAboutToClose(handler: () => void): Disposable;
7575
setCloseRequestHandler(handler: (reason: StopReason) => Promise<boolean>): void;
7676

77+
setOpenUrlHandler(handler: (url: string) => Promise<boolean>): void;
78+
7779
setSecondaryWindowCloseRequestHandler(windowName: string, handler: () => Promise<boolean>): void;
7880

7981
toggleDevTools(): void;
@@ -129,6 +131,7 @@ export const CHANNEL_MAXIMIZE = 'Maximize';
129131
export const CHANNEL_IS_MAXIMIZED = 'IsMaximized';
130132

131133
export const CHANNEL_ABOUT_TO_CLOSE = 'AboutToClose';
134+
export const CHANNEL_OPEN_URL = 'OpenUrl';
132135

133136
export const CHANNEL_UNMAXIMIZE = 'UnMaximize';
134137
export const CHANNEL_ON_WINDOW_EVENT = 'OnWindowEvent';

packages/core/src/electron-main/electron-api-main.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ import {
5454
CHANNEL_SET_BACKGROUND_COLOR,
5555
CHANNEL_WC_METADATA,
5656
CHANNEL_ABOUT_TO_CLOSE,
57-
CHANNEL_OPEN_WITH_SYSTEM_APP
57+
CHANNEL_OPEN_WITH_SYSTEM_APP,
58+
CHANNEL_OPEN_URL
5859
} from '../electron-common/electron-api';
5960
import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application';
6061
import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common';
@@ -165,8 +166,8 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {
165166
shell.showItemInFolder(fsPath);
166167
});
167168

168-
ipcMain.on(CHANNEL_OPEN_WITH_SYSTEM_APP, (event, fsPath) => {
169-
shell.openPath(fsPath);
169+
ipcMain.on(CHANNEL_OPEN_WITH_SYSTEM_APP, (event, uri) => {
170+
shell.openExternal(uri);
170171
});
171172

172173
ipcMain.handle(CHANNEL_GET_TITLE_STYLE_AT_STARTUP, event => application.getTitleBarStyleAtStartup(event.sender));
@@ -285,6 +286,20 @@ export namespace TheiaRendererAPI {
285286
wc.send(CHANNEL_ON_WINDOW_EVENT, event);
286287
}
287288

289+
export function openUrl(wc: WebContents, url: string): Promise<boolean> {
290+
return new Promise<boolean>(resolve => {
291+
const channelNr = nextReplyChannel++;
292+
const replyChannel = `openUrl${channelNr}`;
293+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
294+
const l = createDisposableListener(ipcMain, replyChannel, (e, args: any[]) => {
295+
l.dispose();
296+
resolve(args[0]);
297+
});
298+
299+
wc.send(CHANNEL_OPEN_URL, url, replyChannel);
300+
});
301+
}
302+
288303
export function sendAboutToClose(wc: WebContents): Promise<void> {
289304
return new Promise<void>(resolve => {
290305
const channelNr = nextReplyChannel++;

0 commit comments

Comments
 (0)