From b386630d3c330282fe3ce3a0ee48d448ce82b3d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4usler?= Date: Fri, 27 Sep 2019 19:25:42 +0200 Subject: [PATCH] Add per-tab notifications (#1377) * Add per-tab notifications * fix: file watcher depth on macOS * Free array reference --- src/main/app/index.js | 7 +- src/main/filesystem/watcher.js | 5 +- .../components/editorWithTabs/index.vue | 5 +- .../editorWithTabs/notifications.vue | 123 +++++++++++++ src/renderer/store/editor.js | 163 +++++++++++++----- src/renderer/store/help.js | 4 +- 6 files changed, 253 insertions(+), 54 deletions(-) create mode 100644 src/renderer/components/editorWithTabs/notifications.vue diff --git a/src/main/app/index.js b/src/main/app/index.js index 26230baf..c3056be4 100644 --- a/src/main/app/index.js +++ b/src/main/app/index.js @@ -100,16 +100,13 @@ class App { // Prevent to load webview and opening links or new windows via HTML/JS. app.on('web-contents-created', (event, contents) => { - contents.on('will-attach-webview', (event, webPreferences, params) => { - console.warn('Prevented webview creation.') + contents.on('will-attach-webview', event => { event.preventDefault() }) contents.on('will-navigate', event => { - console.warn('Prevented opening a link.') event.preventDefault() }) - contents.on('new-window', (event, url) => { - console.warn('Prevented opening a new window.') + contents.on('new-window', event => { event.preventDefault() }) }) diff --git a/src/main/filesystem/watcher.js b/src/main/filesystem/watcher.js index 437ba28e..d1e2af25 100644 --- a/src/main/filesystem/watcher.js +++ b/src/main/filesystem/watcher.js @@ -6,7 +6,7 @@ import { exists } from 'common/filesystem' import { hasMarkdownExtension } from 'common/filesystem/paths' import { getUniqueId } from '../utils' import { loadMarkdownFile } from '../filesystem/markdown' -import { isLinux } from '../config' +import { isLinux, isOsx } from '../config' // TODO(refactor): Please see GH#1035. @@ -159,7 +159,8 @@ class Watcher { ignorePermissionErrors: true, // Just to be sure when a file is replaced with a directory don't watch recursively. - depth: type === 'file' ? 0 : undefined, + depth: type === 'file' + ? (isOsx ? 1 : 0) : undefined, // Please see GH#1043 awaitWriteFinish: { diff --git a/src/renderer/components/editorWithTabs/index.vue b/src/renderer/components/editorWithTabs/index.vue index a12e332c..427e1735 100644 --- a/src/renderer/components/editorWithTabs/index.vue +++ b/src/renderer/components/editorWithTabs/index.vue @@ -18,6 +18,7 @@ :text-direction="textDirection" > + @@ -25,6 +26,7 @@ import Tabs from './tabs.vue' import Editor from './editor.vue' import SourceCode from './sourceCode.vue' +import TabNotifications from './notifications.vue' export default { props: { @@ -61,7 +63,8 @@ export default { components: { Tabs, Editor, - SourceCode + SourceCode, + TabNotifications } } diff --git a/src/renderer/components/editorWithTabs/notifications.vue b/src/renderer/components/editorWithTabs/notifications.vue new file mode 100644 index 00000000..c337929c --- /dev/null +++ b/src/renderer/components/editorWithTabs/notifications.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index b4e19501..e2c83708 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -96,29 +96,59 @@ const mutations = { const { data, pathname } = change const { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, encoding, markdown, filename } = data const options = { encoding, lineEnding, adjustLineEndingOnSave } + + // Create a new document and update few entires later. const newFileState = getSingleFileState({ markdown, filename, pathname, options }) - if (isMixedLineEndings) { - notice.notify({ - title: 'Line Ending', - message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`, - type: 'primary', - time: 20000, - showConfirm: false - }) - } const tab = tabs.find(t => isSamePathSync(t.pathname, pathname)) if (!tab) { // The tab may be closed in the meanwhile. console.error('LOAD_CHANGE: Cannot find tab in tab list.') + notice.notify({ + title: 'Error loading tab', + message: 'There was an error while loading the file change because the tab cannot be found.', + type: 'error', + time: 20000, + showConfirm: false + }) return } - // Upate file content but not tab id. + // Backup few entries that we need to restore later. const oldId = tab.id + const oldNotifications = tab.notifications + let oldHistory = null + if (tab.history.index >= 0 && tab.history.stack.length >= 1) { + // Allow to restore the old document. + oldHistory = { + stack: [tab.history.stack[tab.history.index]], + index: 0 + } + + // Free reference from array + tab.history.index-- + tab.history.stack.pop() + } + + // Update file content and restore some entries. Object.assign(tab, newFileState) tab.id = oldId + tab.notifications = oldNotifications + if (oldHistory) { + tab.history = oldHistory + } + if (isMixedLineEndings) { + tab.notifications.push({ + msg: `"${filename}" has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`, + showConfirm: false, + style: 'info', + exclusiveType: '', + action: () => {} + }) + } + + // Reload the editor if the tab is currently opened. if (pathname === currentFile.pathname) { state.currentFile = tab const { id, cursor, history } = tab @@ -236,6 +266,44 @@ const mutations = { // TODO: Remove "SET_GLOBAL_LINE_ENDING" because nowhere used. SET_GLOBAL_LINE_ENDING (state, ending) { state.lineEnding = ending + }, + + // Push a tab specific notification on stack that never disappears. + PUSH_TAB_NOTIFICATION (state, data) { + const defaultAction = () => {} + const { tabId, msg } = data + const action = data.action || defaultAction + const showConfirm = data.showConfirm || false + const style = data.style || 'info' + // Whether only one notification should exist. + const exclusiveType = data.exclusiveType || '' + + const { tabs } = state + const tab = tabs.find(t => t.id === tabId) + if (!tab) { + console.error('PUSH_TAB_NOTIFICATION: Cannot find tab in tab list.') + return + } + + const { notifications } = tab + + // Remove the old notification if only one should exist. + if (exclusiveType) { + const index = notifications.findIndex(n => n.exclusiveType === exclusiveType) + if (index >= 0) { + // Reorder current notification + notifications.splice(index, 1) + } + } + + // Push new notification on stack. + notifications.push({ + msg, + showConfirm, + style, + exclusiveType, + action: action + }) } } @@ -388,12 +456,24 @@ const actions = { }) ipcRenderer.on('mt::tab-save-failure', (e, tabId, msg) => { - notice.notify({ - title: 'Save failure', - message: msg, - type: 'error', - time: 20000, - showConfirm: false + const { tabs } = state + const tab = tabs.find(t => t.id === tabId) + if (!tab) { + notice.notify({ + title: 'Save failure', + message: msg, + type: 'error', + time: 20000, + showConfirm: false + }) + return + } + + commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false }) + commit('PUSH_TAB_NOTIFICATION', { + tabId, + msg: `There was an error while saving: ${msg}`, + style: 'crit' }) }) }, @@ -703,14 +783,10 @@ const actions = { } if (isMixedLineEndings) { - // TODO(watcher): Show (this) notification(s) per tab. const { filename, lineEnding } = markdownDocument - notice.notify({ - title: 'Line Ending', - message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`, - type: 'primary', - time: 20000, - showConfirm: false + commit('PUSH_TAB_NOTIFICATION', { + tabId: id, + msg: `${filename}" has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.` }) } }, @@ -858,8 +934,8 @@ const actions = { LINTEN_FOR_EXPORT_SUCCESS ({ commit }) { ipcRenderer.on('AGANI::export-success', (e, { type, filePath }) => { notice.notify({ - title: 'Export', - message: `Export ${path.basename(filePath)} successfully`, + title: 'Exported successfully', + message: `Exported "${path.basename(filePath)}" successfully!`, showConfirm: true }) .then(() => { @@ -893,31 +969,28 @@ const actions = { LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) { ipcRenderer.on('AGANI::update-file', (e, { type, change }) => { - // TODO: A new "changed" notification from different files overwrite the old notification - // and the old notification disappears. I think we should bind the notification to the tab. + // TODO: We should only load the changed content if the user want to reload the document. const { tabs } = state const { pathname } = change const tab = tabs.find(t => isSamePathSync(t.pathname, pathname)) if (tab) { - const { id, isSaved } = tab - + const { id, isSaved, filename } = tab switch (type) { case 'unlink': { commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false }) - notice.notify({ - title: 'File Removed on Disk', - message: `${pathname} has been removed or moved.`, - type: 'warning', - time: 0, - showConfirm: false + commit('PUSH_TAB_NOTIFICATION', { + tabId: id, + msg: `"${filename}" has been removed on disk.`, + style: 'warn', + showConfirm: false, + exclusiveType: 'file_changed' }) break } case 'add': case 'change': { const { autoSave } = rootState.preferences - const { filename } = change.data if (autoSave) { if (autoSaveTimers.has(id)) { const timer = autoSaveTimers.get(id) @@ -933,17 +1006,17 @@ const actions = { } commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false }) - - notice.clear() - notice.notify({ - title: 'File Changed on Disk', - message: `${filename} has been changed on disk, do you want to reload it?`, + commit('PUSH_TAB_NOTIFICATION', { + tabId: id, + msg: `"${filename}" has been changed on disk. Do you want to reload it?`, showConfirm: true, - time: 0 + exclusiveType: 'file_changed', + action: status => { + if (status) { + commit('LOAD_CHANGE', change) + } + } }) - .then(() => { - commit('LOAD_CHANGE', change) - }) break } default: diff --git a/src/renderer/store/help.js b/src/renderer/store/help.js index 7146399a..87cfea96 100644 --- a/src/renderer/store/help.js +++ b/src/renderer/store/help.js @@ -30,7 +30,9 @@ export const defaultFileState = { index: -1, matches: [], value: '' - } + }, + // Per tab notifications + notifications: [] } export const getOptionsFromState = file => {