diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 25e5d220a..e0a95828c 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -17,9 +17,11 @@ import { DisposableCollection, } from '@theia/core'; import { + Dialog, FrontendApplication, FrontendApplicationContribution, LocalStorageService, + OnWillStopAction, SaveableWidget, StatusBar, StatusBarAlignment, @@ -659,4 +661,51 @@ export class ArduinoFrontendContribution } ); } + + onWillStop(): OnWillStopAction { + return { + reason: 'temp-sketch', + action: () => { + return this.showTempSketchDialog(); + } + } + } + + private async showTempSketchDialog(): Promise { + const sketch = await this.sketchServiceClient.currentSketch(); + if (!sketch) { + return true; + } + const isTemp = await this.sketchService.isTemp(sketch); + if (!isTemp) { + return true; + } + const messageBoxResult = await remote.dialog.showMessageBox( + remote.getCurrentWindow(), + { + message: nls.localize('arduino/sketch/saveTempSketch', 'Save your sketch to open it again later.'), + title: nls.localize('theia/core/quitTitle', 'Are you sure you want to quit?'), + type: 'question', + buttons: [ + Dialog.CANCEL, + nls.localizeByDefault('Save As...'), + nls.localizeByDefault("Don't Save"), + ], + } + ) + const result = messageBoxResult.response; + if (result === 2) { + return true; + } else if (result === 1) { + return !!(await this.commandRegistry.executeCommand( + SaveAsSketch.Commands.SAVE_AS_SKETCH.id, + { + execOnlyIfTemp: false, + openAfterMove: false, + wipeOriginal: true + } + )); + } + return false + } } diff --git a/arduino-ide-extension/src/browser/contributions/close.ts b/arduino-ide-extension/src/browser/contributions/close.ts index f27b832cf..f801b5b47 100644 --- a/arduino-ide-extension/src/browser/contributions/close.ts +++ b/arduino-ide-extension/src/browser/contributions/close.ts @@ -1,12 +1,10 @@ import { inject, injectable } from '@theia/core/shared/inversify'; -import { toArray } from '@theia/core/shared/@phosphor/algorithm'; import * as remote from '@theia/core/electron-shared/@electron/remote'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { SaveAsSketch } from './save-as-sketch'; import { SketchContribution, Command, @@ -33,76 +31,7 @@ export class Close extends SketchContribution { registerCommands(registry: CommandRegistry): void { registry.registerCommand(Close.Commands.CLOSE, { - execute: async () => { - // Close current editor if closeable. - const { currentEditor } = this.editorManager; - if (currentEditor && currentEditor.title.closable) { - currentEditor.close(); - return; - } - - // Close current widget from the main area if possible. - const { currentWidget } = this.shell; - if (currentWidget) { - const currentWidgetInMain = toArray( - this.shell.mainPanel.widgets() - ).find((widget) => widget === currentWidget); - if (currentWidgetInMain && currentWidgetInMain.title.closable) { - return currentWidgetInMain.close(); - } - } - - // Close the sketch (window). - const sketch = await this.sketchServiceClient.currentSketch(); - if (!sketch) { - return; - } - const isTemp = await this.sketchService.isTemp(sketch); - const uri = await this.sketchServiceClient.currentSketchFile(); - if (!uri) { - return; - } - if (isTemp && (await this.wasTouched(uri))) { - const { response } = await remote.dialog.showMessageBox({ - type: 'question', - buttons: [ - nls.localize( - 'vscode/abstractTaskService/saveBeforeRun.dontSave', - "Don't Save" - ), - nls.localize('vscode/issueMainService/cancel', 'Cancel'), - nls.localize( - 'vscode/abstractTaskService/saveBeforeRun.save', - 'Save' - ), - ], - message: nls.localize( - 'arduino/common/saveChangesToSketch', - 'Do you want to save changes to this sketch before closing?' - ), - detail: nls.localize( - 'arduino/common/loseChanges', - "If you don't save, your changes will be lost." - ), - }); - if (response === 1) { - // Cancel - return; - } - if (response === 2) { - // Save - const saved = await this.commandService.executeCommand( - SaveAsSketch.Commands.SAVE_AS_SKETCH.id, - { openAfterMove: false, execOnlyIfTemp: true } - ); - if (!saved) { - // If it was not saved, do bail the close. - return; - } - } - } - window.close(); - }, + execute: () => remote.getCurrentWindow().close() }); } 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 1e3b86e60..fd6a27aa5 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 @@ -11,6 +11,29 @@ 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'; + +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(); diff --git a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts index 6ba05e283..7df38d649 100644 --- a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts +++ b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts @@ -19,6 +19,8 @@ import { 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'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMainApplication).toSelf().inSingletonScope(); @@ -56,4 +58,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { ) ) .inSingletonScope(); + + bind(TheiaElectronWindow).toSelf(); + rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow); }); diff --git a/arduino-ide-extension/src/electron-main/theia/theia-electron-window.ts b/arduino-ide-extension/src/electron-main/theia/theia-electron-window.ts new file mode 100644 index 000000000..073ba5f48 --- /dev/null +++ b/arduino-ide-extension/src/electron-main/theia/theia-electron-window.ts @@ -0,0 +1,36 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { StopReason } from '@theia/core/lib/electron-common/messaging/electron-messages'; +import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window'; +import { FileUri } from '@theia/core/lib/node'; +import URI from '@theia/core/lib/common/uri'; + +@injectable() +export class TheiaElectronWindow extends DefaultTheiaElectronWindow { + protected async handleStopRequest( + onSafeCallback: () => unknown, + reason: StopReason + ): Promise { + // Only confirm close to windows that have loaded our frontend. + // Both the windows's URL and the FS path of the `index.html` should be converted to the "same" format to be able to compare them. (#11226) + // Notes: + // - Windows: file:///C:/path/to/somewhere vs file:///c%3A/path/to/somewhere + // - macOS: file:///Applications/App%20Name.app/Contents vs /Applications/App Name.app/Contents + // This URL string comes from electron, we can expect that this is properly encoded URL. For example, a space is `%20` + const currentUrl = new URI(this.window.webContents.getURL()).toString(); + // THEIA_FRONTEND_HTML_PATH is an FS path, we have to covert to an encoded URI string. + const frontendUri = FileUri.create( + this.globals.THEIA_FRONTEND_HTML_PATH + ).toString(); + const safeToClose = + !currentUrl.includes(frontendUri) || (await this.checkSafeToStop(reason)); + if (safeToClose) { + try { + await onSafeCallback(); + return true; + } catch (e) { + console.warn(`Request ${StopReason[reason]} failed.`, e); + } + } + return false; + } +} diff --git a/i18n/en.json b/i18n/en.json index ccda42fcf..3bd7251da 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -93,13 +93,11 @@ }, "common": { "later": "Later", - "loseChanges": "If you don't save, your changes will be lost.", "noBoardSelected": "No board selected", "notConnected": "[not connected]", "offlineIndicator": "You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.", "oldFormat": "The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?", "processing": "Processing", - "saveChangesToSketch": "Do you want to save changes to this sketch before closing?", "selectBoard": "Select Board", "selectedOn": "on {0}", "serialMonitor": "Serial Monitor", @@ -297,6 +295,7 @@ "openSketchInNewWindow": "Open Sketch in New Window", "saveFolderAs": "Save sketch folder as...", "saveSketchAs": "Save sketch folder as...", + "saveTempSketch": "Save your sketch to open it again later.", "showFolder": "Show Sketch Folder", "sketch": "Sketch", "sketchbook": "Sketchbook", @@ -325,7 +324,9 @@ "cannotConnectDaemon": "Cannot connect to the CLI daemon.", "couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.", "daemonOffline": "CLI Daemon Offline", - "offline": "Offline" + "offline": "Offline", + "quitMessage": "Any unsaved changes will not be saved.", + "quitTitle": "Are you sure you want to quit?" }, "debug": { "start": "Start...", diff --git a/package.json b/package.json index 5ad471310..71534a9c9 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "test": "lerna run test", "download:plugins": "theia download:plugins", "update:version": "node ./scripts/update-version.js", - "i18n:generate": "theia nls-extract -e vscode -f \"+(arduino-ide-extension|browser-app|electron|electron-app|plugins)/**/*.ts?(x)\" -o ./i18n/en.json", + "i18n:generate": "theia nls-extract -e vscode -f \"+(arduino-ide-extension|browser-app|electron-app|plugins)/**/*.ts?(x)\" -o ./i18n/en.json", "i18n:check": "yarn i18n:generate && git add -N ./i18n && git diff --exit-code ./i18n", "i18n:push": "node ./scripts/i18n/transifex-push.js ./i18n/en.json", "i18n:pull": "node ./scripts/i18n/transifex-pull.js ./i18n/",