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", "frontend": "lib/browser/theia/core/browser-menu-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-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" "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 // If arduino:avr installation fails because it's already installed we don't want to retry on next start-up
console.error(e); console.error(e);
} else { } 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; 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 // 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); console.log('error installing core', e);
} else { } 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; 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 { 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 { 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 { 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 { MainMenuManager } from '../../../common/main-menu-manager';
import { ElectronWindowService } from '../../electron-window-service';
import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { ElectronMenuContribution } from './electron-menu-contribution'; import { ElectronMenuContribution } from './electron-menu-contribution';
import { nls } from '@theia/core/lib/common/nls'; 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 remote from '@theia/core/electron-shared/@electron/remote';
import * as dialogs from '@theia/core/lib/browser/dialogs'; import * as dialogs from '@theia/core/lib/browser/dialogs';
Object.assign(dialogs, { Object.assign(dialogs, {
confirmExit: async () => { confirmExit: async () => {
const messageBoxResult = await remote.dialog.showMessageBox( const messageBoxResult = await remote.dialog.showMessageBox(
remote.getCurrentWindow(), remote.getCurrentWindow(),
{ {
message: nls.localize('theia/core/quitMessage', 'Any unsaved changes will not be saved.'), message: nls.localize(
title: nls.localize('theia/core/quitTitle', 'Are you sure you want to quit?'), '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', type: 'question',
buttons: [ buttons: [dialogs.Dialog.CANCEL, dialogs.Dialog.YES],
dialogs.Dialog.CANCEL,
dialogs.Dialog.YES,
],
} }
) );
return messageBoxResult.response === 1; return messageBoxResult.response === 1;
} },
}); });
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
@ -41,14 +36,4 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaElectronMenuContribution).toService(ElectronMenuContribution); rebind(TheiaElectronMenuContribution).toService(ElectronMenuContribution);
bind(ElectronMainMenuFactory).toSelf().inSingletonScope(); bind(ElectronMainMenuFactory).toSelf().inSingletonScope();
rebind(TheiaElectronMainMenuFactory).toService(ElectronMainMenuFactory); 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 * as remote from '@theia/core/electron-shared/@electron/remote';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { import {
@ -6,19 +10,27 @@ import {
ConnectionStatusService, ConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service'; } from '@theia/core/lib/browser/connection-status-service';
import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-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 { 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() @injectable()
export class ElectronWindowService extends TheiaElectronWindowService { export class ElectronWindowService
extends TheiaElectronWindowService
implements WindowServiceExt
{
@inject(ConnectionStatusService) @inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService; private readonly connectionStatusService: ConnectionStatusService;
@inject(SplashService) @inject(SplashService)
protected readonly splashService: SplashService; private readonly splashService: SplashService;
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService; private readonly appStateService: FrontendApplicationStateService;
@inject(ElectronMainWindowServiceExt)
private readonly mainWindowServiceExt: ElectronMainWindowServiceExt;
@postConstruct() @postConstruct()
protected override init(): void { protected override init(): void {
@ -55,4 +67,15 @@ export class ElectronWindowService extends TheiaElectronWindowService {
}); });
return response === 0; // 'Yes', close the window. 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 { 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 { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import { import {
ElectronMainApplication as TheiaElectronMainApplication, ElectronMainApplication as TheiaElectronMainApplication,
ElectronMainApplicationContribution, ElectronMainApplicationContribution,
} from '@theia/core/lib/electron-main/electron-main-application'; } from '@theia/core/lib/electron-main/electron-main-application';
import { import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
SplashService, import { ContainerModule } from '@theia/core/shared/inversify';
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 { import {
IDEUpdater, IDEUpdater,
IDEUpdaterClient, IDEUpdaterClient,
IDEUpdaterPath, IDEUpdaterPath,
} from '../common/protocol/ide-updater'; } 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 { import {
SurveyNotificationService, ElectronMainWindowServiceExt,
SurveyNotificationServicePath, electronMainWindowServiceExtPath,
} from '../common/protocol/survey-service'; } 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) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMainApplication).toSelf().inSingletonScope(); bind(ElectronMainApplication).toSelf().inSingletonScope();
@ -67,19 +67,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TheiaElectronWindow).toSelf(); bind(TheiaElectronWindow).toSelf();
rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow); rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow);
// Survey notification bindings bind(ElectronMainWindowServiceExt)
bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope(); .to(ElectronMainWindowServiceExtImpl)
bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl); .inSingletonScope();
bind(ElectronMainApplicationContribution).toService(
SurveyNotificationService
);
bind(ElectronConnectionHandler) bind(ElectronConnectionHandler)
.toDynamicValue( .toDynamicValue(
(context) => (context) =>
new JsonRpcConnectionHandler(SurveyNotificationServicePath, () => new JsonRpcConnectionHandler(electronMainWindowServiceExtPath, () =>
context.container.get<SurveyNotificationService>( context.container.get(ElectronMainWindowServiceExt)
SurveyNotificationService
)
) )
) )
.inSingletonScope(); .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() @injectable()
export class ElectronMainApplication extends TheiaElectronMainApplication { export class ElectronMainApplication extends TheiaElectronMainApplication {
protected startup = false; private startup = false;
protected openFilePromise = new Deferred(); private _firstWindowId: number | undefined;
private openFilePromise = new Deferred();
@inject(SplashServiceImpl) @inject(SplashServiceImpl)
protected readonly splashService: SplashServiceImpl; private readonly splashService: SplashServiceImpl;
override async start(config: FrontendApplicationConfig): Promise<void> { override async start(config: FrontendApplicationConfig): Promise<void> {
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit") // 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 // OSX: register open-file event
if (os.isOSX) { if (os.isOSX) {
app.on('open-file', async (event, uri) => { 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)); 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 params: ElectronMainExecutionParams
): Promise<boolean> { ): Promise<boolean> {
// Copy to prevent manipulation of original array // Copy to prevent manipulation of original array
@ -223,7 +224,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
return false; return false;
} }
protected async openSketch( private async openSketch(
workspace: WorkspaceOptions | string workspace: WorkspaceOptions | string
): Promise<BrowserWindow> { ): Promise<BrowserWindow> {
const options = await this.getLastWindowOptions(); const options = await this.getLastWindowOptions();
@ -257,7 +258,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
} }
protected override getTitleBarStyle( protected override getTitleBarStyle(
config: FrontendApplicationConfig // eslint-disable-next-line @typescript-eslint/no-unused-vars
_config: FrontendApplicationConfig
): 'native' | 'custom' { ): 'native' | 'custom' {
return 'native'; return 'native';
} }
@ -354,6 +356,9 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
electronWindow.webContents.openDevTools(); electronWindow.webContents.openDevTools();
} }
this.attachListenersToWindow(electronWindow); this.attachListenersToWindow(electronWindow);
if (this._firstWindowId === undefined) {
this._firstWindowId = electronWindow.id;
}
return electronWindow; return electronWindow;
} }
@ -389,7 +394,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}); });
event.newGuest = new BrowserWindow(options); event.newGuest = new BrowserWindow(options);
event.newGuest.setMenu(null); event.newGuest.setMenu(null);
event.newGuest?.on('closed', (e: any) => { event.newGuest?.on('closed', () => {
electronWindow?.webContents.send('CLOSE_CHILD_WINDOW'); electronWindow?.webContents.send('CLOSE_CHILD_WINDOW');
}); });
event.newGuest?.loadURL(url); 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 // Since the `before-quit` event is only fired when closing the *last* window
// We need to keep track of recently closed windows/workspaces manually // We need to keep track of recently closed windows/workspaces manually
window.on('close', () => { window.on('close', () => {
@ -522,4 +527,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
get browserWindows(): BrowserWindow[] { get browserWindows(): BrowserWindow[] {
return Array.from(this.windows.values()).map(({ window }) => window); 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 { FormatterPath } from '../common/protocol/formatter';
import { LocalizationBackendContribution } from './i18n/localization-backend-contribution'; import { LocalizationBackendContribution } from './i18n/localization-backend-contribution';
import { LocalizationBackendContribution as TheiaLocalizationBackendContribution } from '@theia/core/lib/node/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) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope(); bind(BackendApplication).toSelf().inSingletonScope();
@ -401,4 +406,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaLocalizationBackendContribution).toService( rebind(TheiaLocalizationBackendContribution).toService(
LocalizationBackendContribution 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();
}); });