diff --git a/arduino-ide-extension/src/browser/contributions/close.ts b/arduino-ide-extension/src/browser/contributions/close.ts index 92026a38..47278fb9 100644 --- a/arduino-ide-extension/src/browser/contributions/close.ts +++ b/arduino-ide-extension/src/browser/contributions/close.ts @@ -1,6 +1,13 @@ import { injectable } from '@theia/core/shared/inversify'; import * as remote from '@theia/core/electron-shared/@electron/remote'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import type { MaybePromise } from '@theia/core/lib/common/types'; +import type { + FrontendApplication, + OnWillStopAction, +} from '@theia/core/lib/browser/frontend-application'; +import { nls } from '@theia/core/lib/common/nls'; +import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { ArduinoMenus } from '../menu/arduino-menus'; import { SketchContribution, @@ -11,17 +18,21 @@ import { Sketch, URI, } from './contribution'; -import { nls } from '@theia/core/lib/common'; import { Dialog } from '@theia/core/lib/browser/dialogs'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; import { SaveAsSketch } from './save-as-sketch'; -import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application'; /** * Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window. */ @injectable() export class Close extends SketchContribution { + private shell: ApplicationShell | undefined; + + override onStart(app: FrontendApplication): MaybePromise { + this.shell = app.shell; + } + override registerCommands(registry: CommandRegistry): void { registry.registerCommand(Close.Commands.CLOSE, { execute: () => remote.getCurrentWindow().close(), @@ -46,20 +57,41 @@ export class Close extends SketchContribution { // `FrontendApplicationContribution#onWillStop` onWillStop(): OnWillStopAction { return { - reason: 'temp-sketch', + reason: 'save-sketch', action: () => { - return this.showSaveTempSketchDialog(); + return this.showSaveSketchDialog(); }, }; } - private async showSaveTempSketchDialog(): Promise { - const sketch = await this.sketchServiceClient.currentSketch(); - if (!CurrentSketch.isValid(sketch)) { - return true; - } - const isTemp = await this.sketchService.isTemp(sketch); - if (!isTemp) { + /** + * If returns with `true`, IDE2 will close. Otherwise, it won't. + */ + private async showSaveSketchDialog(): Promise { + const sketch = await this.isCurrentSketchTemp(); + if (!sketch) { + // Normal close workflow: if there are dirty editors prompt the user. + if (!this.shell) { + console.error( + `Could not get the application shell. Something went wrong.` + ); + return true; + } + if (this.shell.canSaveAll()) { + const prompt = await this.prompt(false); + switch (prompt) { + case Prompt.DoNotSave: + return true; + case Prompt.Cancel: + return false; + case Prompt.Save: { + await this.shell.saveAll(); + return true; + } + default: + throw new Error(`Unexpected prompt: ${prompt}`); + } + } return true; } @@ -71,11 +103,36 @@ export class Close extends SketchContribution { return true; } - const messageBoxResult = await remote.dialog.showMessageBox( + const prompt = await this.prompt(true); + switch (prompt) { + case Prompt.DoNotSave: + return true; + case Prompt.Cancel: + return false; + case Prompt.Save: { + // If `save as` was canceled by user, the result will be `undefined`, otherwise the new URI. + const result = await this.commandService.executeCommand( + SaveAsSketch.Commands.SAVE_AS_SKETCH.id, + { + execOnlyIfTemp: false, + openAfterMove: false, + wipeOriginal: true, + markAsRecentlyOpened: true, + } + ); + return !!result; + } + default: + throw new Error(`Unexpected prompt: ${prompt}`); + } + } + + private async prompt(isTemp: boolean): Promise { + const { response } = await remote.dialog.showMessageBox( remote.getCurrentWindow(), { message: nls.localize( - 'arduino/sketch/saveTempSketch', + 'arduino/sketch/saveSketch', 'Save your sketch to open it again later.' ), title: nls.localize( @@ -84,24 +141,32 @@ export class Close extends SketchContribution { ), type: 'question', buttons: [ - Dialog.CANCEL, - nls.localizeByDefault('Save As...'), nls.localizeByDefault("Don't Save"), + Dialog.CANCEL, + nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'), ], + defaultId: 2, // `Save`/`Save As...` button index is the default. } ); - const result = messageBoxResult.response; - if (result === 2) { - return true; - } else if (result === 1) { - return !!(await this.commandService.executeCommand( - SaveAsSketch.Commands.SAVE_AS_SKETCH.id, - { - execOnlyIfTemp: false, - openAfterMove: false, - wipeOriginal: true, - } - )); + switch (response) { + case 0: + return Prompt.DoNotSave; + case 1: + return Prompt.Cancel; + case 2: + return Prompt.Save; + default: + throw new Error(`Unexpected response: ${response}`); + } + } + + private async isCurrentSketchTemp(): Promise { + const currentSketch = await this.sketchServiceClient.currentSketch(); + if (CurrentSketch.isValid(currentSketch)) { + const isTemp = await this.sketchService.isTemp(currentSketch); + if (isTemp) { + return currentSketch; + } } return false; } @@ -128,6 +193,12 @@ export class Close extends SketchContribution { } } +enum Prompt { + Save, + DoNotSave, + Cancel, +} + export namespace Close { export namespace Commands { export const CLOSE: Command = { diff --git a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts index e31b8135..2954a603 100644 --- a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts @@ -57,6 +57,7 @@ export class SaveAsSketch extends SketchContribution { execOnlyIfTemp, openAfterMove, wipeOriginal, + markAsRecentlyOpened, }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT ): Promise { const sketch = await this.sketchServiceClient.currentSketch(); @@ -102,6 +103,9 @@ export class SaveAsSketch extends SketchContribution { }); if (workspaceUri) { await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri); + if (markAsRecentlyOpened) { + this.sketchService.markAsRecentlyOpened(workspaceUri); + } } if (workspaceUri && openAfterMove) { this.windowService.setSafeToShutDown(); @@ -171,12 +175,14 @@ export namespace SaveAsSketch { * Ignored if `openAfterMove` is `false`. */ readonly wipeOriginal?: boolean; + readonly markAsRecentlyOpened?: boolean; } export namespace Options { export const DEFAULT: Options = { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false, + markAsRecentlyOpened: false, }; } } diff --git a/arduino-ide-extension/src/browser/theia/core/application-shell.ts b/arduino-ide-extension/src/browser/theia/core/application-shell.ts index f0610a56..d3d7cc2f 100644 --- a/arduino-ide-extension/src/browser/theia/core/application-shell.ts +++ b/arduino-ide-extension/src/browser/theia/core/application-shell.ts @@ -1,6 +1,5 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import { EditorWidget } from '@theia/editor/lib/browser'; -import { CommandService } from '@theia/core/lib/common/command'; import { MessageService } from '@theia/core/lib/common/message-service'; import { OutputWidget } from '@theia/output/lib/browser/output-widget'; import { @@ -15,9 +14,9 @@ import { TabBar, Widget, SHELL_TABBAR_CONTEXT_MENU, + SaveOptions, } from '@theia/core/lib/browser'; import { Sketch } from '../../../common/protocol'; -import { SaveAsSketch } from '../../contributions/save-as-sketch'; import { CurrentSketch, SketchesServiceClientImpl, @@ -28,9 +27,6 @@ import { ToolbarAwareTabBar } from './tab-bars'; @injectable() export class ApplicationShell extends TheiaApplicationShell { - @inject(CommandService) - private readonly commandService: CommandService; - @inject(MessageService) private readonly messageService: MessageService; @@ -106,7 +102,7 @@ export class ApplicationShell extends TheiaApplicationShell { return topPanel; } - override async saveAll(): Promise { + override async saveAll(options?: SaveOptions): Promise { if ( this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE ) { @@ -118,12 +114,7 @@ export class ApplicationShell extends TheiaApplicationShell { ); return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803 } - await super.saveAll(); - const options = { execOnlyIfTemp: true, openAfterMove: true }; - await this.commandService.executeCommand( - SaveAsSketch.Commands.SAVE_AS_SKETCH.id, - options - ); + return super.saveAll(options); } } diff --git a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts index c92d4972..0785cad0 100644 --- a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts @@ -5,6 +5,7 @@ import { CommonCommands, } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommandRegistry } from '@theia/core/lib/common/command'; +import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application'; @injectable() export class CommonFrontendContribution extends TheiaCommonFrontendContribution { @@ -48,4 +49,9 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution registry.unregisterMenuAction(command); } } + + override onWillStop(): OnWillStopAction | undefined { + // This is NOOP here. All window close and app quit requests are handled in the `Close` contribution. + return undefined; + } } diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts index 101e44b5..7e827ff4 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts @@ -4,31 +4,6 @@ import { ElectronMenuContribution as TheiaElectronMenuContribution } from '@thei import { MainMenuManager } from '../../../common/main-menu-manager'; import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { ElectronMenuContribution } from './electron-menu-contribution'; -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?' - ), - type: 'question', - buttons: [dialogs.Dialog.CANCEL, dialogs.Dialog.YES], - } - ); - return messageBoxResult.response === 1; - }, -}); export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMenuContribution).toSelf().inSingletonScope();