Can check if the current window is the first one.

Closes #1070

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-08-03 16:17:37 +02:00 committed by Akos Kitta
parent bf193b1cac
commit 36ac47b975
13 changed files with 193 additions and 71 deletions

View File

@ -150,6 +150,10 @@
"frontend": "lib/browser/theia/core/browser-menu-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
},
{
"frontend": "lib/browser/theia/core/browser-window-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
},
{
"electronMain": "lib/electron-main/arduino-electron-main-module"
}

View File

@ -43,7 +43,7 @@ export class FirstStartupInstaller extends Contribution {
// If arduino:avr installation fails because it's already installed we don't want to retry on next start-up
console.error(e);
} else {
// But if there is any other error (e.g.: no interntet cconnection), we want to retry next time
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
avrPackageError = e;
}
}
@ -64,7 +64,7 @@ export class FirstStartupInstaller extends Contribution {
// If Arduino_BuiltIn installation fails because it's already installed we don't want to retry on next start-up
console.log('error installing core', e);
} else {
// But if there is any other error (e.g.: no interntet cconnection), we want to retry next time
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
builtInLibraryError = e;
}
}

View File

@ -0,0 +1,10 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { ContainerModule } from '@theia/core/shared/inversify';
import { DefaultWindowService } from './default-window-service';
import { WindowServiceExt } from './window-service-ext';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DefaultWindowService).toSelf().inSingletonScope();
rebind(TheiaDefaultWindowService).toService(DefaultWindowService);
bind(WindowServiceExt).toService(DefaultWindowService);
});

View File

@ -0,0 +1,17 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { injectable } from '@theia/core/shared/inversify';
import { WindowServiceExt } from './window-service-ext';
@injectable()
export class DefaultWindowService
extends TheiaDefaultWindowService
implements WindowServiceExt
{
/**
* The default implementation always resolves to `true`.
* IDE2 does not use it. It's currently an electron-only app.
*/
async isFirstWindow(): Promise<boolean> {
return true;
}
}

View File

@ -0,0 +1,7 @@
export const WindowServiceExt = Symbol('WindowServiceExt');
export interface WindowServiceExt {
/**
* Returns with a promise that resolves to `true` if the current window is the first window.
*/
isFirstWindow(): Promise<boolean>;
}

View File

@ -1,14 +1,7 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import { ElectronMenuContribution as TheiaElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import {
SplashService,
splashServicePath,
} from '../../../electron-common/splash-service';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { ElectronWindowService } from '../../electron-window-service';
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { ElectronMenuContribution } from './electron-menu-contribution';
import { nls } from '@theia/core/lib/common/nls';
@ -16,23 +9,25 @@ import { nls } from '@theia/core/lib/common/nls';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dialogs from '@theia/core/lib/browser/dialogs';
Object.assign(dialogs, {
confirmExit: async () => {
const messageBoxResult = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: nls.localize('theia/core/quitMessage', 'Any unsaved changes will not be saved.'),
title: nls.localize('theia/core/quitTitle', 'Are you sure you want to quit?'),
message: nls.localize(
'theia/core/quitMessage',
'Any unsaved changes will not be saved.'
),
title: nls.localize(
'theia/core/quitTitle',
'Are you sure you want to quit?'
),
type: 'question',
buttons: [
dialogs.Dialog.CANCEL,
dialogs.Dialog.YES,
],
buttons: [dialogs.Dialog.CANCEL, dialogs.Dialog.YES],
}
)
);
return messageBoxResult.response === 1;
}
},
});
export default new ContainerModule((bind, unbind, isBound, rebind) => {
@ -41,14 +36,4 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaElectronMenuContribution).toService(ElectronMenuContribution);
bind(ElectronMainMenuFactory).toSelf().inSingletonScope();
rebind(TheiaElectronMainMenuFactory).toService(ElectronMainMenuFactory);
bind(ElectronWindowService).toSelf().inSingletonScope();
rebind(WindowService).toService(ElectronWindowService);
bind(SplashService)
.toDynamicValue((context) =>
ElectronIpcConnectionProvider.createProxy(
context.container,
splashServicePath
)
)
.inSingletonScope();
});

View File

@ -0,0 +1,32 @@
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { ContainerModule } from '@theia/core/shared/inversify';
import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext';
import {
ElectronMainWindowServiceExt,
electronMainWindowServiceExtPath,
} from '../../../electron-common/electron-main-window-service-ext';
import {
SplashService,
splashServicePath,
} from '../../../electron-common/splash-service';
import { ElectronWindowService } from './electron-window-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronWindowService).toSelf().inSingletonScope();
rebind(WindowService).toService(ElectronWindowService);
bind(WindowServiceExt).toService(ElectronWindowService);
bind(ElectronMainWindowServiceExt)
.toDynamicValue(({ container }) =>
ElectronIpcConnectionProvider.createProxy(
container,
electronMainWindowServiceExtPath
)
)
.inSingletonScope();
bind(SplashService)
.toDynamicValue(({ container }) =>
ElectronIpcConnectionProvider.createProxy(container, splashServicePath)
)
.inSingletonScope();
});

View File

@ -1,4 +1,8 @@
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
@ -6,19 +10,27 @@ import {
ConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service';
import { SplashService } from '../electron-common/splash-service';
import { SplashService } from '../../../electron-common/splash-service';
import { nls } from '@theia/core/lib/common';
import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext';
import { ElectronMainWindowServiceExt } from '../../../electron-common/electron-main-window-service-ext';
@injectable()
export class ElectronWindowService extends TheiaElectronWindowService {
export class ElectronWindowService
extends TheiaElectronWindowService
implements WindowServiceExt
{
@inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService;
private readonly connectionStatusService: ConnectionStatusService;
@inject(SplashService)
protected readonly splashService: SplashService;
private readonly splashService: SplashService;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
private readonly appStateService: FrontendApplicationStateService;
@inject(ElectronMainWindowServiceExt)
private readonly mainWindowServiceExt: ElectronMainWindowServiceExt;
@postConstruct()
protected override init(): void {
@ -55,4 +67,15 @@ export class ElectronWindowService extends TheiaElectronWindowService {
});
return response === 0; // 'Yes', close the window.
}
private _firstWindow: boolean | undefined;
async isFirstWindow(): Promise<boolean> {
if (this._firstWindow === undefined) {
const windowId = remote.getCurrentWindow().id; // This is expensive and synchronous so we check it once per FE.
this._firstWindow = await this.mainWindowServiceExt.isFirstWindow(
windowId
);
}
return this._firstWindow;
}
}

View File

@ -0,0 +1,7 @@
export const electronMainWindowServiceExtPath = '/services/electron-window-ext';
export const ElectronMainWindowServiceExt = Symbol(
'ElectronMainWindowServiceExt'
);
export interface ElectronMainWindowServiceExt {
isFirstWindow(windowId: number): Promise<boolean>;
}

View File

@ -1,31 +1,31 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import {
ElectronMainApplication as TheiaElectronMainApplication,
ElectronMainApplicationContribution,
} from '@theia/core/lib/electron-main/electron-main-application';
import {
SplashService,
splashServicePath,
} from '../electron-common/splash-service';
import { SplashServiceImpl } from './splash/splash-service-impl';
import { ElectronMainApplication } from './theia/electron-main-application';
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { ContainerModule } from '@theia/core/shared/inversify';
import {
IDEUpdater,
IDEUpdaterClient,
IDEUpdaterPath,
} from '../common/protocol/ide-updater';
import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
import { TheiaElectronWindow } from './theia/theia-electron-window';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { SurveyNotificationServiceImpl } from '../node/survey-service-impl';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
ElectronMainWindowServiceExt,
electronMainWindowServiceExtPath,
} from '../electron-common/electron-main-window-service-ext';
import {
SplashService,
splashServicePath,
} from '../electron-common/splash-service';
import { ElectronMainWindowServiceExtImpl } from './electron-main-window-service-ext-impl';
import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
import { SplashServiceImpl } from './splash/splash-service-impl';
import { ElectronMainApplication } from './theia/electron-main-application';
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
import { TheiaElectronWindow } from './theia/theia-electron-window';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMainApplication).toSelf().inSingletonScope();
@ -67,19 +67,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TheiaElectronWindow).toSelf();
rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow);
// Survey notification bindings
bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope();
bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl);
bind(ElectronMainApplicationContribution).toService(
SurveyNotificationService
);
bind(ElectronMainWindowServiceExt)
.to(ElectronMainWindowServiceExtImpl)
.inSingletonScope();
bind(ElectronConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(SurveyNotificationServicePath, () =>
context.container.get<SurveyNotificationService>(
SurveyNotificationService
)
new JsonRpcConnectionHandler(electronMainWindowServiceExtPath, () =>
context.container.get(ElectronMainWindowServiceExt)
)
)
.inSingletonScope();

View File

@ -0,0 +1,15 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { ElectronMainWindowServiceExt } from '../electron-common/electron-main-window-service-ext';
import { ElectronMainApplication } from './theia/electron-main-application';
@injectable()
export class ElectronMainWindowServiceExtImpl
implements ElectronMainWindowServiceExt
{
@inject(ElectronMainApplication)
private readonly app: ElectronMainApplication;
async isFirstWindow(windowId: number): Promise<boolean> {
return this.app.firstWindowId === windowId;
}
}

View File

@ -66,11 +66,12 @@ const APP_STARTED_WITH_CONTENT_TRACE =
@injectable()
export class ElectronMainApplication extends TheiaElectronMainApplication {
protected startup = false;
protected openFilePromise = new Deferred();
private startup = false;
private _firstWindowId: number | undefined;
private openFilePromise = new Deferred();
@inject(SplashServiceImpl)
protected readonly splashService: SplashServiceImpl;
private readonly splashService: SplashServiceImpl;
override async start(config: FrontendApplicationConfig): Promise<void> {
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
@ -142,7 +143,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
})();
}
attachFileAssociations() {
private attachFileAssociations(): void {
// OSX: register open-file event
if (os.isOSX) {
app.on('open-file', async (event, uri) => {
@ -158,7 +159,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
}
protected async isValidSketchPath(uri: string): Promise<boolean | undefined> {
private async isValidSketchPath(uri: string): Promise<boolean | undefined> {
return typeof uri === 'string' && (await fs.pathExists(uri));
}
@ -201,7 +202,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
}
protected async launchFromArgs(
private async launchFromArgs(
params: ElectronMainExecutionParams
): Promise<boolean> {
// Copy to prevent manipulation of original array
@ -223,7 +224,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
return false;
}
protected async openSketch(
private async openSketch(
workspace: WorkspaceOptions | string
): Promise<BrowserWindow> {
const options = await this.getLastWindowOptions();
@ -257,7 +258,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
protected override getTitleBarStyle(
config: FrontendApplicationConfig
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config: FrontendApplicationConfig
): 'native' | 'custom' {
return 'native';
}
@ -354,6 +356,9 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
electronWindow.webContents.openDevTools();
}
this.attachListenersToWindow(electronWindow);
if (this._firstWindowId === undefined) {
this._firstWindowId = electronWindow.id;
}
return electronWindow;
}
@ -389,7 +394,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
});
event.newGuest = new BrowserWindow(options);
event.newGuest.setMenu(null);
event.newGuest?.on('closed', (e: any) => {
event.newGuest?.on('closed', () => {
electronWindow?.webContents.send('CLOSE_CHILD_WINDOW');
});
event.newGuest?.loadURL(url);
@ -462,9 +467,9 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
}
protected closedWorkspaces: WorkspaceOptions[] = [];
private closedWorkspaces: WorkspaceOptions[] = [];
protected attachClosedWorkspace(window: BrowserWindow): void {
private attachClosedWorkspace(window: BrowserWindow): void {
// Since the `before-quit` event is only fired when closing the *last* window
// We need to keep track of recently closed windows/workspaces manually
window.on('close', () => {
@ -522,4 +527,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
get browserWindows(): BrowserWindow[] {
return Array.from(this.windows.values()).map(({ window }) => window);
}
get firstWindowId(): number | undefined {
return this._firstWindowId;
}
}

View File

@ -104,6 +104,11 @@ import { ClangFormatter } from './clang-formatter';
import { FormatterPath } from '../common/protocol/formatter';
import { LocalizationBackendContribution } from './i18n/localization-backend-contribution';
import { LocalizationBackendContribution as TheiaLocalizationBackendContribution } from '@theia/core/lib/node/i18n/localization-backend-contribution';
import { SurveyNotificationServiceImpl } from './survey-service-impl';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@ -401,4 +406,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaLocalizationBackendContribution).toService(
LocalizationBackendContribution
);
// Survey notification bindings
// It's currently unused. https://github.com/arduino/arduino-ide/pull/1150
bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope();
bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
({ container }) =>
new JsonRpcConnectionHandler(SurveyNotificationServicePath, () =>
container.get<SurveyNotificationService>(SurveyNotificationService)
)
)
.inSingletonScope();
});