diff --git a/ElectronClient/ElectronAppWrapper.js b/ElectronClient/ElectronAppWrapper.js index 9942175e8..b7c5203d3 100644 --- a/ElectronClient/ElectronAppWrapper.js +++ b/ElectronClient/ElectronAppWrapper.js @@ -4,6 +4,7 @@ const url = require('url'); const path = require('path'); const { dirname } = require('lib/path-utils'); const fs = require('fs-extra'); +const { ipcMain } = require('electron'); class ElectronAppWrapper { @@ -15,6 +16,7 @@ class ElectronAppWrapper { this.willQuitApp_ = false; this.tray_ = null; this.buildDir_ = null; + this.rendererProcessQuitReply_ = null; } electronApp() { @@ -112,9 +114,11 @@ class ElectronAppWrapper { // On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which // case the app must be explicitly closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit". + let isGoingToExit = false; + if (process.platform === 'darwin') { if (this.willQuitApp_) { - this.win_ = null; + isGoingToExit = true; } else { event.preventDefault(); this.hide(); @@ -124,9 +128,39 @@ class ElectronAppWrapper { event.preventDefault(); this.win_.hide(); } else { - this.win_ = null; + isGoingToExit = true; } } + + if (isGoingToExit) { + if (!this.rendererProcessQuitReply_) { + // If we haven't notified the renderer process yet, do it now + // so that it can tell us if we can really close the app or not. + // Search for "appClose" event for closing logic on renderer side. + event.preventDefault(); + this.win_.webContents.send('appClose'); + } else { + // If the renderer process has responded, check if we can close or not + if (this.rendererProcessQuitReply_.canClose) { + // Really quit the app + this.rendererProcessQuitReply_ = null; + this.win_ = null; + } else { + // Wait for renderer to finish task + event.preventDefault(); + this.rendererProcessQuitReply_ = null; + } + } + } + }); + + ipcMain.on('asynchronous-message', (event, message, args) => { + if (message === 'appCloseReply') { + // We got the response from the renderer process: + // save the response and try quit again. + this.rendererProcessQuitReply_ = args; + this.electronApp_.quit(); + } }); // Let us register listeners on the window, so we can update the state diff --git a/ElectronClient/gui/MainScreen.jsx b/ElectronClient/gui/MainScreen.jsx index 980214a53..cb555e35c 100644 --- a/ElectronClient/gui/MainScreen.jsx +++ b/ElectronClient/gui/MainScreen.jsx @@ -5,6 +5,7 @@ const { SideBar } = require('./SideBar.min.js'); const { NoteList } = require('./NoteList.min.js'); const { NoteText } = require('./NoteText.min.js'); const NoteText2 = require('./NoteText2.js').default; +const { stateUtils } = require('lib/reducer.js'); const { PromptDialog } = require('./PromptDialog.min.js'); const NoteContentPropertiesDialog = require('./NoteContentPropertiesDialog.js').default; const NotePropertiesDialog = require('./NotePropertiesDialog.min.js'); @@ -23,11 +24,25 @@ const VerticalResizer = require('./VerticalResizer.min'); const PluginManager = require('lib/services/PluginManager'); const TemplateUtils = require('lib/TemplateUtils'); const EncryptionService = require('lib/services/EncryptionService'); +const ipcRenderer = require('electron').ipcRenderer; class MainScreenComponent extends React.Component { constructor() { super(); + this.state = { + promptOptions: null, + modalLayer: { + visible: false, + message: '', + }, + notePropertiesDialogOptions: {}, + noteContentPropertiesDialogOptions: {}, + shareNoteDialogOptions: {}, + }; + + this.setupAppCloseHandling(); + this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this); this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this); this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this); @@ -35,6 +50,37 @@ class MainScreenComponent extends React.Component { this.noteList_onDrag = this.noteList_onDrag.bind(this); } + setupAppCloseHandling() { + this.waitForNotesSavedIID_ = null; + + // This event is dispached from the main process when the app is about + // to close. The renderer process must respond with the "appCloseReply" + // and tell the main process whether the app can really be closed or not. + // For example, it cannot be closed right away if a note is being saved. + // If a note is being saved, we wait till it is saved and then call + // "appCloseReply" again. + ipcRenderer.on('appClose', () => { + if (this.waitForNotesSavedIID_) clearInterval(this.waitForNotesSavedIID_); + this.waitForNotesSavedIID_ = null; + + ipcRenderer.send('asynchronous-message', 'appCloseReply', { + canClose: !this.props.hasNotesBeingSaved, + }); + + if (this.props.hasNotesBeingSaved) { + this.waitForNotesSavedIID_ = setInterval(() => { + if (!this.props.hasNotesBeingSaved) { + clearInterval(this.waitForNotesSavedIID_); + this.waitForNotesSavedIID_ = null; + ipcRenderer.send('asynchronous-message', 'appCloseReply', { + canClose: true, + }); + } + }, 50); + } + }); + } + sidebar_onDrag(event) { Setting.setValue('style.sidebar.width', this.props.sidebarWidth + event.deltaX); } @@ -55,19 +101,6 @@ class MainScreenComponent extends React.Component { this.setState({ shareNoteDialogOptions: {} }); } - UNSAFE_componentWillMount() { - this.setState({ - promptOptions: null, - modalLayer: { - visible: false, - message: '', - }, - notePropertiesDialogOptions: {}, - noteContentPropertiesDialogOptions: {}, - shareNoteDialogOptions: {}, - }); - } - UNSAFE_componentWillReceiveProps(newProps) { // Execute a command if any, and if we haven't already executed it if (newProps.windowCommand && newProps.windowCommand !== this.props.windowCommand) { @@ -735,6 +768,7 @@ const mapStateToProps = state => { selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null, plugins: state.plugins, templates: state.templates, + hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state), }; }; diff --git a/ElectronClient/gui/Root.jsx b/ElectronClient/gui/Root.jsx index 03fe29754..54e8fc36f 100644 --- a/ElectronClient/gui/Root.jsx +++ b/ElectronClient/gui/Root.jsx @@ -22,19 +22,17 @@ const { bridge } = require('electron').remote.require('./bridge'); async function initialize() { this.wcsTimeoutId_ = null; - bridge() - .window() - .on('resize', function() { - if (this.wcsTimeoutId_) clearTimeout(this.wcsTimeoutId_); + bridge().window().on('resize', function() { + if (this.wcsTimeoutId_) clearTimeout(this.wcsTimeoutId_); - this.wcsTimeoutId_ = setTimeout(() => { - store.dispatch({ - type: 'WINDOW_CONTENT_SIZE_SET', - size: bridge().windowContentSize(), - }); - this.wcsTimeoutId_ = null; - }, 10); - }); + this.wcsTimeoutId_ = setTimeout(() => { + store.dispatch({ + type: 'WINDOW_CONTENT_SIZE_SET', + size: bridge().windowContentSize(), + }); + this.wcsTimeoutId_ = null; + }, 10); + }); // Need to dispatch this to make sure the components are // displayed at the right size. The windowContentSize is diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js index bf5b853c5..3df647aba 100644 --- a/ReactNativeClient/lib/BaseApplication.js +++ b/ReactNativeClient/lib/BaseApplication.js @@ -627,7 +627,7 @@ class BaseApplication { initArgs = Object.assign(initArgs, extraFlags); this.logger_.addTarget('file', { path: `${profileDir}/log.txt` }); - // if (Setting.value('env') === 'dev' && Setting.value('appType') === 'desktop') this.logger_.addTarget('console', { level: Logger.LEVEL_DEBUG }); + if (Setting.value('env') === 'dev' && Setting.value('appType') === 'desktop') this.logger_.addTarget('console', { level: Logger.LEVEL_DEBUG }); this.logger_.setLevel(initArgs.logLevel); reg.setLogger(this.logger_); diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js index 9390f4c03..45024fde1 100644 --- a/ReactNativeClient/lib/reducer.js +++ b/ReactNativeClient/lib/reducer.js @@ -89,6 +89,13 @@ stateUtils.foldersOrder = function(stateSettings) { ]); }; +stateUtils.hasNotesBeingSaved = function(state) { + for (const id in state.editorNoteStatuses) { + if (state.editorNoteStatuses[id] === 'saving') return true; + } + return false; +}; + stateUtils.parentItem = function(state) { const t = state.notesParentType; let id = null;