1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Desktop: Wait for note to be saved before closing the app

This commit is contained in:
Laurent Cozic 2020-04-09 18:57:20 +01:00
parent 93dccf62df
commit 75b28c46af
5 changed files with 101 additions and 28 deletions

View File

@ -4,6 +4,7 @@ const url = require('url');
const path = require('path'); const path = require('path');
const { dirname } = require('lib/path-utils'); const { dirname } = require('lib/path-utils');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { ipcMain } = require('electron');
class ElectronAppWrapper { class ElectronAppWrapper {
@ -15,6 +16,7 @@ class ElectronAppWrapper {
this.willQuitApp_ = false; this.willQuitApp_ = false;
this.tray_ = null; this.tray_ = null;
this.buildDir_ = null; this.buildDir_ = null;
this.rendererProcessQuitReply_ = null;
} }
electronApp() { 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 // 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". // 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 (process.platform === 'darwin') {
if (this.willQuitApp_) { if (this.willQuitApp_) {
this.win_ = null; isGoingToExit = true;
} else { } else {
event.preventDefault(); event.preventDefault();
this.hide(); this.hide();
@ -124,9 +128,39 @@ class ElectronAppWrapper {
event.preventDefault(); event.preventDefault();
this.win_.hide(); this.win_.hide();
} else { } 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 // Let us register listeners on the window, so we can update the state

View File

@ -5,6 +5,7 @@ const { SideBar } = require('./SideBar.min.js');
const { NoteList } = require('./NoteList.min.js'); const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js'); const { NoteText } = require('./NoteText.min.js');
const NoteText2 = require('./NoteText2.js').default; const NoteText2 = require('./NoteText2.js').default;
const { stateUtils } = require('lib/reducer.js');
const { PromptDialog } = require('./PromptDialog.min.js'); const { PromptDialog } = require('./PromptDialog.min.js');
const NoteContentPropertiesDialog = require('./NoteContentPropertiesDialog.js').default; const NoteContentPropertiesDialog = require('./NoteContentPropertiesDialog.js').default;
const NotePropertiesDialog = require('./NotePropertiesDialog.min.js'); const NotePropertiesDialog = require('./NotePropertiesDialog.min.js');
@ -23,11 +24,25 @@ const VerticalResizer = require('./VerticalResizer.min');
const PluginManager = require('lib/services/PluginManager'); const PluginManager = require('lib/services/PluginManager');
const TemplateUtils = require('lib/TemplateUtils'); const TemplateUtils = require('lib/TemplateUtils');
const EncryptionService = require('lib/services/EncryptionService'); const EncryptionService = require('lib/services/EncryptionService');
const ipcRenderer = require('electron').ipcRenderer;
class MainScreenComponent extends React.Component { class MainScreenComponent extends React.Component {
constructor() { constructor() {
super(); super();
this.state = {
promptOptions: null,
modalLayer: {
visible: false,
message: '',
},
notePropertiesDialogOptions: {},
noteContentPropertiesDialogOptions: {},
shareNoteDialogOptions: {},
};
this.setupAppCloseHandling();
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this); this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this); this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
this.shareNoteDialog_close = this.shareNoteDialog_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); 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) { sidebar_onDrag(event) {
Setting.setValue('style.sidebar.width', this.props.sidebarWidth + event.deltaX); Setting.setValue('style.sidebar.width', this.props.sidebarWidth + event.deltaX);
} }
@ -55,19 +101,6 @@ class MainScreenComponent extends React.Component {
this.setState({ shareNoteDialogOptions: {} }); this.setState({ shareNoteDialogOptions: {} });
} }
UNSAFE_componentWillMount() {
this.setState({
promptOptions: null,
modalLayer: {
visible: false,
message: '',
},
notePropertiesDialogOptions: {},
noteContentPropertiesDialogOptions: {},
shareNoteDialogOptions: {},
});
}
UNSAFE_componentWillReceiveProps(newProps) { UNSAFE_componentWillReceiveProps(newProps) {
// Execute a command if any, and if we haven't already executed it // Execute a command if any, and if we haven't already executed it
if (newProps.windowCommand && newProps.windowCommand !== this.props.windowCommand) { if (newProps.windowCommand && newProps.windowCommand !== this.props.windowCommand) {
@ -735,6 +768,7 @@ const mapStateToProps = state => {
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null, selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
plugins: state.plugins, plugins: state.plugins,
templates: state.templates, templates: state.templates,
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
}; };
}; };

View File

@ -22,19 +22,17 @@ const { bridge } = require('electron').remote.require('./bridge');
async function initialize() { async function initialize() {
this.wcsTimeoutId_ = null; this.wcsTimeoutId_ = null;
bridge() bridge().window().on('resize', function() {
.window() if (this.wcsTimeoutId_) clearTimeout(this.wcsTimeoutId_);
.on('resize', function() {
if (this.wcsTimeoutId_) clearTimeout(this.wcsTimeoutId_);
this.wcsTimeoutId_ = setTimeout(() => { this.wcsTimeoutId_ = setTimeout(() => {
store.dispatch({ store.dispatch({
type: 'WINDOW_CONTENT_SIZE_SET', type: 'WINDOW_CONTENT_SIZE_SET',
size: bridge().windowContentSize(), size: bridge().windowContentSize(),
}); });
this.wcsTimeoutId_ = null; this.wcsTimeoutId_ = null;
}, 10); }, 10);
}); });
// Need to dispatch this to make sure the components are // Need to dispatch this to make sure the components are
// displayed at the right size. The windowContentSize is // displayed at the right size. The windowContentSize is

View File

@ -627,7 +627,7 @@ class BaseApplication {
initArgs = Object.assign(initArgs, extraFlags); initArgs = Object.assign(initArgs, extraFlags);
this.logger_.addTarget('file', { path: `${profileDir}/log.txt` }); 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); this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_); reg.setLogger(this.logger_);

View File

@ -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) { stateUtils.parentItem = function(state) {
const t = state.notesParentType; const t = state.notesParentType;
let id = null; let id = null;