aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Amine <amine@mouafik.fr>2020-03-02 13:57:49 +0100
committerLibravatar GitHub <noreply@github.com>2020-03-02 13:57:49 +0100
commit7cc26c9b84191e9ecff0866a7b5d5248bb4207b1 (patch)
treed18858559d8aad6c881c65e7b309d19686cba2db
parentNew Crowdin translations (#420) (diff)
parentRun linter (diff)
downloadferdium-app-7cc26c9b84191e9ecff0866a7b5d5248bb4207b1.tar.gz
ferdium-app-7cc26c9b84191e9ecff0866a7b5d5248bb4207b1.tar.zst
ferdium-app-7cc26c9b84191e9ecff0866a7b5d5248bb4207b1.zip
Merge pull request #419 from getferdi/fix/context-menu
Fix and enhance context menu
-rw-r--r--src/lib/Menu.js2
-rw-r--r--src/stores/SettingsStore.js7
-rw-r--r--src/webview/contextMenu.js355
-rw-r--r--src/webview/recipe.js11
-rw-r--r--src/webview/spellchecker.js12
5 files changed, 51 insertions, 336 deletions
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 1c4cc6ab5..6d5eb0095 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -819,7 +819,7 @@ export default class FranzMenu {
819 locked: true, 819 locked: true,
820 }, 820 },
821 }); 821 });
822 } 822 },
823 }); 823 });
824 824
825 if (serviceTpl.length > 0) { 825 if (serviceTpl.length > 0) {
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index 26e83b725..71d4e1702 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -1,5 +1,7 @@
1import { ipcRenderer, remote } from 'electron'; 1import { ipcRenderer, remote } from 'electron';
2import { action, computed, observable, reaction } from 'mobx'; 2import {
3 action, computed, observable, reaction,
4} from 'mobx';
3import localStorage from 'mobx-localstorage'; 5import localStorage from 'mobx-localstorage';
4import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES, LOCAL_SERVER } from '../config'; 6import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES, LOCAL_SERVER } from '../config';
5import { API } from '../environment'; 7import { API } from '../environment';
@@ -12,6 +14,7 @@ const debug = require('debug')('Ferdi:SettingsStore');
12 14
13export default class SettingsStore extends Store { 15export default class SettingsStore extends Store {
14 @observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings'); 16 @observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings');
17
15 startup = true; 18 startup = true;
16 19
17 fileSystemSettingsTypes = FILE_SYSTEM_SETTINGS_TYPES; 20 fileSystemSettingsTypes = FILE_SYSTEM_SETTINGS_TYPES;
@@ -103,7 +106,7 @@ export default class SettingsStore extends Store {
103 // So we lock manually 106 // So we lock manually
104 window.ferdi.stores.router.push('/auth/locked'); 107 window.ferdi.stores.router.push('/auth/locked');
105 } 108 }
106 }) 109 });
107 } 110 }
108 debug('Get appSettings resolves', resp.type, resp.data); 111 debug('Get appSettings resolves', resp.type, resp.data);
109 Object.assign(this._fileSystemSettingsCache[resp.type], resp.data); 112 Object.assign(this._fileSystemSettingsCache[resp.type], resp.data);
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js
index acd62d675..eeb825ece 100644
--- a/src/webview/contextMenu.js
+++ b/src/webview/contextMenu.js
@@ -1,323 +1,50 @@
1// This is heavily based on https://github.com/sindresorhus/electron-context-menu 1import { remote } from 'electron';
2// @sindresorhus 2import { ContextMenuBuilder, ContextMenuListener } from 'electron-spellchecker';
3 3
4import {
5 clipboard, remote, ipcRenderer, shell,
6} from 'electron';
7
8import { isDevMode, isMac } from '../environment';
9import { SPELLCHECKER_LOCALES } from '../i18n/languages';
10
11const debug = require('debug')('Ferdi:contextMenu');
12
13const { Menu } = remote;
14
15// const win = remote.getCurrentWindow();
16const webContents = remote.getCurrentWebContents(); 4const webContents = remote.getCurrentWebContents();
17 5
18function delUnusedElements(menuTpl) { 6export default async function setupContextMenu(handler) {
19 let notDeletedPrevEl; 7 const addCustomMenuItems = (menu, menuInfo) => {
20 return menuTpl.filter(el => el.visible !== false).filter((el, i, array) => { 8 // Add "Paste as plain text" item when right-clicking editable content
21 const toDelete = el.type === 'separator' && (!notDeletedPrevEl || i === array.length - 1 || array[i + 1].type === 'separator'); 9 if (
22 notDeletedPrevEl = toDelete ? notDeletedPrevEl : el; 10 menuInfo.editFlags.canPaste
23 return !toDelete; 11 && !menuInfo.linkText
24 }); 12 && !menuInfo.hasImageContents
25} 13 ) {
26 14 menu.insert(
27const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheckerLanguage, spellcheckerLanguage) => { 15 3,
28 const { editFlags } = props; 16 new remote.MenuItem({
29 const textSelection = props.selectionText.trim(); 17 label: 'Paste as plain text',
30 const hasText = textSelection.length > 0; 18 accelerator: 'CommandOrControl+Shift+V',
31 const can = type => editFlags[`can${type}`] && hasText; 19 click: () => webContents.pasteAndMatchStyle(),
32 20 }),
33 const canGoBack = webContents.canGoBack(); 21 );
34 const canGoForward = webContents.canGoForward(); 22 }
35 23
36 // @adlk: we can't use roles here due to a bug with electron where electron.remote.webContents.getFocusedWebContents() returns the first webview in DOM instead of the focused one 24 // Add "Open Link in Ferdi" item for links
37 // Github issue creation is pending 25 if (menuInfo.linkURL) {
38 let menuTpl = [ 26 menu.insert(
39 { 27 2,
40 type: 'separator', 28 new remote.MenuItem({
41 }, { 29 label: 'Open Link in Ferdi',
42 id: 'createTodo', 30 click: () => {
43 label: `Create todo: "${textSelection.length > 15 ? `${textSelection.slice(0, 15)}...` : textSelection}"`, 31 window.location.href = menuInfo.linkURL;
44 visible: hasText,
45 click() {
46 debug('Create todo from selected text', textSelection);
47 ipcRenderer.sendToHost('feature:todos', {
48 action: 'todos:create',
49 data: {
50 title: textSelection,
51 url: window.location.href,
52 }, 32 },
53 }); 33 }),
54 }, 34 );
55 },
56 {
57 type: 'separator',
58 }, {
59 id: 'lookup',
60 label: `Look Up "${textSelection.length > 15 ? `${textSelection.slice(0, 15)}...` : textSelection}"`,
61 visible: isMac && props.mediaType === 'none' && hasText,
62 click() {
63 debug('Show definition for selection', textSelection);
64 webContents.showDefinitionForSelection();
65 },
66 }, {
67 type: 'separator',
68 }, {
69 id: 'cut',
70 label: 'Cut',
71 click() {
72 if (can('Cut')) {
73 webContents.cut();
74 }
75 },
76 enabled: can('Cut'),
77 visible: hasText && props.isEditable,
78 }, {
79 id: 'copy',
80 label: 'Copy',
81 click() {
82 if (can('Copy')) {
83 webContents.copy();
84 }
85 },
86 enabled: can('Copy'),
87 visible: props.isEditable || hasText,
88 }, {
89 id: 'paste',
90 label: 'Paste',
91 click() {
92 if (editFlags.canPaste) {
93 webContents.paste();
94 }
95 },
96 enabled: editFlags.canPaste,
97 visible: props.isEditable,
98 }, {
99 type: 'separator',
100 visible: props.isEditable && hasText,
101 }, {
102 id: 'searchTextSelection',
103 label: `Search Google for "${textSelection.length > 15 ? `${textSelection.slice(0, 15)}...` : textSelection}"`,
104 visible: hasText,
105 click() {
106 const url = `https://www.google.com/search?q=${textSelection}`;
107 debug('Search on Google', url);
108 shell.openExternal(url);
109 },
110 }, {
111 type: 'separator',
112 },
113 ];
114
115 if (props.linkURL && props.mediaType === 'none') {
116 menuTpl = [{
117 type: 'separator',
118 }, {
119 id: 'openLink',
120 label: 'Open Link in Browser',
121 click() {
122 debug('Open link in Browser', props.linkURL);
123 shell.openExternal(props.linkURL);
124 },
125 }, {
126 id: 'copyLink',
127 label: 'Copy Link',
128 click() {
129 clipboard.write({
130 bookmark: props.linkText,
131 text: props.linkURL,
132 });
133 },
134 }, {
135 type: 'separator',
136 }];
137 }
138
139 if (props.mediaType === 'image') {
140 menuTpl.push({
141 type: 'separator',
142 }, {
143 id: 'openImage',
144 label: 'Open Image in Browser',
145 click() {
146 debug('Open image in Browser', props.srcURL);
147 shell.openExternal(props.srcURL);
148 },
149 }, {
150 id: 'copyImageAddress',
151 label: 'Copy Image Address',
152 click() {
153 clipboard.write({
154 bookmark: props.srcURL,
155 text: props.srcURL,
156 });
157 },
158 }, {
159 type: 'separator',
160 });
161 }
162
163 if (props.mediaType === 'image') {
164 menuTpl.push({
165 id: 'saveImageAs',
166 label: 'Save Image As…',
167 async click() {
168 if (props.srcURL.startsWith('blob:')) {
169 const url = new window.URL(props.srcURL.substr(5));
170 const fileName = url.pathname.substr(1);
171 const resp = await window.fetch(props.srcURL);
172 const blob = await resp.blob();
173 const reader = new window.FileReader();
174 reader.readAsDataURL(blob);
175 reader.onloadend = () => {
176 const base64data = reader.result;
177
178 ipcRenderer.send('download-file', {
179 content: base64data,
180 fileOptions: {
181 name: fileName,
182 mime: blob.type,
183 },
184 });
185 };
186 debug('binary string', blob);
187 } else {
188 ipcRenderer.send('download-file', { url: props.srcURL });
189 }
190 },
191 }, {
192 type: 'separator',
193 });
194 }
195
196 if (suggestions.length > 0) {
197 suggestions.reverse().map(suggestion => menuTpl.unshift({
198 id: `suggestion-${suggestion}`,
199 label: suggestion,
200 click() {
201 webContents.replaceMisspelling(suggestion);
202 },
203 }));
204 }
205
206 if (canGoBack || canGoForward) {
207 menuTpl.push({
208 type: 'separator',
209 }, {
210 id: 'goBack',
211 label: 'Go Back',
212 enabled: canGoBack,
213 click() {
214 webContents.goBack();
215 },
216 }, {
217 id: 'goForward',
218 label: 'Go Forward',
219 enabled: canGoForward,
220 click() {
221 webContents.goForward();
222 },
223 }, {
224 type: 'separator',
225 });
226 }
227
228 const spellcheckingLanguages = [];
229 Object.keys(SPELLCHECKER_LOCALES).sort(Intl.Collator().compare).forEach((key) => {
230 spellcheckingLanguages.push({
231 id: `lang-${key}`,
232 label: SPELLCHECKER_LOCALES[key],
233 type: 'radio',
234 checked: spellcheckerLanguage === key,
235 click() {
236 debug('Setting service spellchecker to', key);
237 ipcRenderer.sendToHost('set-service-spellchecker-language', key);
238 },
239 });
240 });
241
242 menuTpl.push({
243 type: 'separator',
244 }, {
245 id: 'spellchecker',
246 label: 'Spell Checking',
247 visible: isSpellcheckEnabled,
248 submenu: [
249 {
250 id: 'spellchecker',
251 label: 'Available Languages',
252 enabled: false,
253 }, {
254 type: 'separator',
255 },
256 {
257 id: 'resetToDefault',
258 label: `Reset to system default (${defaultSpellcheckerLanguage === 'automatic' ? 'Automatic' : SPELLCHECKER_LOCALES[defaultSpellcheckerLanguage]})`,
259 type: 'radio',
260 visible: defaultSpellcheckerLanguage !== spellcheckerLanguage || (defaultSpellcheckerLanguage !== 'automatic' && spellcheckerLanguage === 'automatic'),
261 click() {
262 debug('Resetting service spellchecker to system default');
263 ipcRenderer.sendToHost('set-service-spellchecker-language', 'reset');
264 },
265 },
266 {
267 id: 'automaticDetection',
268 label: 'Automatic language detection',
269 type: 'radio',
270 checked: spellcheckerLanguage === 'automatic',
271 click() {
272 debug('Detect language automatically');
273 ipcRenderer.sendToHost('set-service-spellchecker-language', 'automatic');
274 },
275 },
276 {
277 type: 'separator',
278 visible: defaultSpellcheckerLanguage !== spellcheckerLanguage,
279 },
280 ...spellcheckingLanguages],
281 });
282
283
284 if (isDevMode) {
285 menuTpl.push({
286 type: 'separator',
287 }, {
288 id: 'inspect',
289 label: 'Inspect Element',
290 click() {
291 webContents.inspectElement(props.x, props.y);
292 },
293 });
294 }
295
296 return delUnusedElements(menuTpl);
297};
298
299export default function contextMenu(spellcheckProvider, isSpellcheckEnabled, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage) {
300 webContents.on('context-menu', async (e, props) => {
301 e.preventDefault();
302
303 let suggestions = [];
304 if (spellcheckProvider && props.misspelledWord) {
305 debug('Mispelled word', props.misspelledWord);
306 suggestions = await spellcheckProvider.getSuggestion(props.misspelledWord);
307
308 debug('Suggestions', suggestions);
309 } 35 }
310 36
311 const menu = Menu.buildFromTemplate( 37 return menu;
312 buildMenuTpl( 38 };
313 props, 39
314 suggestions.slice(0, 5), 40 const contextMenuBuilder = new ContextMenuBuilder(
315 isSpellcheckEnabled(), 41 handler,
316 getDefaultSpellcheckerLanguage(), 42 null,
317 getSpellcheckerLanguage(), 43 true,
318 ), 44 addCustomMenuItems,
319 ); 45 );
320 46 // eslint-disable-next-line no-new
321 menu.popup(); 47 new ContextMenuListener((info) => {
48 contextMenuBuilder.showPopupMenu(info);
322 }); 49 });
323} 50}
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index 1a22542d8..07d29f477 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -23,7 +23,6 @@ import RecipeWebview from './lib/RecipeWebview';
23 23
24import spellchecker, { switchDict, disable as disableSpellchecker, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; 24import spellchecker, { switchDict, disable as disableSpellchecker, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker';
25import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; 25import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
26import contextMenu from './contextMenu';
27import './notifications'; 26import './notifications';
28 27
29import { DEFAULT_APP_SETTINGS } from '../config'; 28import { DEFAULT_APP_SETTINGS } from '../config';
@@ -72,15 +71,7 @@ class RecipeController {
72 71
73 debug('Send "hello" to host'); 72 debug('Send "hello" to host');
74 setTimeout(() => ipcRenderer.sendToHost('hello'), 100); 73 setTimeout(() => ipcRenderer.sendToHost('hello'), 100);
75 74 await spellchecker();
76 this.spellcheckingProvider = await spellchecker();
77 contextMenu(
78 this.spellcheckingProvider,
79 () => this.settings.app.enableSpellchecking,
80 () => this.settings.app.spellcheckerLanguage,
81 () => this.spellcheckerLanguage,
82 );
83
84 autorun(() => this.update()); 75 autorun(() => this.update());
85 } 76 }
86 77
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js
index 8a1c8782b..a33a506b2 100644
--- a/src/webview/spellchecker.js
+++ b/src/webview/spellchecker.js
@@ -1,13 +1,12 @@
1import { webFrame } from 'electron'; 1import { webFrame } from 'electron';
2import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; 2import { SpellCheckHandler } from 'electron-spellchecker';
3
4import { SPELLCHECKER_LOCALES } from '../i18n/languages'; 3import { SPELLCHECKER_LOCALES } from '../i18n/languages';
4import setupContextMenu from './contextMenu';
5 5
6const debug = require('debug')('Franz:spellchecker'); 6const debug = require('debug')('Franz:spellchecker');
7 7
8let handler; 8let handler;
9let currentDict; 9let currentDict;
10let contextMenuBuilder;
11let _isEnabled = false; 10let _isEnabled = false;
12 11
13export async function switchDict(locale) { 12export async function switchDict(locale) {
@@ -46,12 +45,7 @@ export default async function initialize(languageCode = 'en-us') {
46 debug('Init spellchecker'); 45 debug('Init spellchecker');
47 46
48 switchDict(locale); 47 switchDict(locale);
49 48 setupContextMenu(handler);
50 contextMenuBuilder = new ContextMenuBuilder(handler);
51 // eslint-disable-next-line no-new
52 new ContextMenuListener((info) => {
53 contextMenuBuilder.showPopupMenu(info);
54 });
55 49
56 return handler; 50 return handler;
57 } catch (err) { 51 } catch (err) {