1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/packages/app-desktop/services/electron-context-menu.js

269 lines
8.4 KiB
JavaScript

'use strict';
// This is a fork of electron-context-menu@0.15.0. We need to fork it because
// the latest version only runs from the main process and we need it in the
// renderer process. It also has a dependency to electron-is-dev which also only
// runs in the main process.
//
// In fact we almost don't use any features of electron-context-menu, which is
// just a wrapper over Electron's own native context menu but with more bugs, so
// we should get rid of it, but for now this is good enough as a quick fix.
const electron = require('electron');
const electronRemote = require('@electron/remote');
const webContents = win => win.webContents || (win.getWebContents && win.getWebContents());
const decorateMenuItem = menuItem => {
return (options = {}) => {
if (options.transform && !options.click) {
menuItem.transform = options.transform;
}
return menuItem;
};
};
const removeUnusedMenuItems = menuTemplate => {
let notDeletedPreviousElement;
return menuTemplate
.filter(menuItem => menuItem !== undefined && menuItem !== false && menuItem.visible !== false)
.filter((menuItem, index, array) => {
const toDelete = menuItem.type === 'separator' && (!notDeletedPreviousElement || index === array.length - 1 || array[index + 1].type === 'separator');
notDeletedPreviousElement = toDelete ? notDeletedPreviousElement : menuItem;
return !toDelete;
});
};
const create = (win, options) => {
webContents(win).on('context-menu', (event, props) => {
if (typeof options.shouldShowMenu === 'function' && options.shouldShowMenu(event, props) === false) {
return;
}
// If another listener has called .preventDefault, don't show the default context menu.
if (event.defaultPrevented) {
return;
}
const { editFlags } = props;
const hasText = props.selectionText.trim().length > 0;
const isLink = Boolean(props.linkURL);
const can = type => editFlags[`can${type}`] && hasText;
const defaultActions = {
separator: () => ({ type: 'separator' }),
lookUpSelection: decorateMenuItem({
id: 'lookUpSelection',
label: 'Look Up “{selection}”',
visible: process.platform === 'darwin' && hasText && !isLink,
click() {
if (process.platform === 'darwin') {
webContents(win).showDefinitionForSelection();
}
},
}),
cut: decorateMenuItem({
id: 'cut',
label: 'Cut',
enabled: can('Cut'),
visible: props.isEditable,
click(menuItem) {
props.selectionText = menuItem.transform ? menuItem.transform(props.selectionText) : props.selectionText;
electron.clipboard.writeText(props.selectionText);
webContents(win).delete();
},
}),
copy: decorateMenuItem({
id: 'copy',
label: 'Copy',
enabled: can('Copy'),
visible: props.isEditable || hasText,
click(menuItem) {
props.selectionText = menuItem.transform ? menuItem.transform(props.selectionText) : props.selectionText;
electron.clipboard.writeText(props.selectionText);
},
}),
paste: decorateMenuItem({
id: 'paste',
label: 'Paste',
enabled: editFlags.canPaste,
visible: props.isEditable,
click(menuItem) {
let clipboardContent = electron.clipboard.readText(props.selectionText);
clipboardContent = menuItem.transform ? menuItem.transform(clipboardContent) : clipboardContent;
webContents(win).insertText(clipboardContent);
},
}),
saveImage: decorateMenuItem({
id: 'saveImage',
label: 'Save Image',
visible: props.mediaType === 'image',
click(menuItem) {
props.srcURL = menuItem.transform ? menuItem.transform(props.srcURL) : props.srcURL;
// download(win, props.srcURL);
},
}),
saveImageAs: decorateMenuItem({
id: 'saveImageAs',
label: 'Save Image As…',
visible: props.mediaType === 'image',
click(menuItem) {
props.srcURL = menuItem.transform ? menuItem.transform(props.srcURL) : props.srcURL;
// download(win, props.srcURL, {saveAs: true});
},
}),
copyLink: decorateMenuItem({
id: 'copyLink',
label: 'Copy Link',
visible: props.linkURL.length !== 0 && props.mediaType === 'none',
click(menuItem) {
props.linkURL = menuItem.transform ? menuItem.transform(props.linkURL) : props.linkURL;
electron.clipboard.write({
bookmark: props.linkText,
text: props.linkURL,
});
},
}),
copyImage: decorateMenuItem({
id: 'copyImage',
label: 'Copy Image',
visible: props.mediaType === 'image',
click() {
webContents(win).copyImageAt(props.x, props.y);
},
}),
copyImageAddress: decorateMenuItem({
id: 'copyImageAddress',
label: 'Copy Image Address',
visible: props.mediaType === 'image',
click(menuItem) {
props.srcURL = menuItem.transform ? menuItem.transform(props.srcURL) : props.srcURL;
electron.clipboard.write({
bookmark: props.srcURL,
text: props.srcURL,
});
},
}),
inspect: () => ({
id: 'inspect',
label: 'Inspect Element',
click() {
win.inspectElement(props.x, props.y);
if (webContents(win).isDevToolsOpened()) {
webContents(win).devToolsWebContents.focus();
}
},
}),
services: () => ({
id: 'services',
label: 'Services',
role: 'services',
visible: process.platform === 'darwin' && (props.isEditable || hasText),
}),
};
const shouldShowInspectElement = typeof options.showInspectElement === 'boolean' ? options.showInspectElement : false;
let menuTemplate = [
defaultActions.separator(),
options.showLookUpSelection !== false && defaultActions.lookUpSelection(),
defaultActions.separator(),
defaultActions.cut(),
defaultActions.copy(),
defaultActions.paste(),
defaultActions.separator(),
defaultActions.saveImage(),
options.showSaveImageAs && defaultActions.saveImageAs(),
options.showCopyImage !== false && defaultActions.copyImage(),
options.showCopyImageAddress && defaultActions.copyImageAddress(),
defaultActions.separator(),
defaultActions.copyLink(),
defaultActions.separator(),
shouldShowInspectElement && defaultActions.inspect(),
options.showServices && defaultActions.services(),
defaultActions.separator(),
];
if (options.menu) {
menuTemplate = options.menu(defaultActions, props, win);
}
if (options.prepend) {
const result = options.prepend(defaultActions, props, win);
if (Array.isArray(result)) {
menuTemplate.unshift(...result);
}
}
if (options.append) {
const result = options.append(defaultActions, props, win);
if (Array.isArray(result)) {
menuTemplate.push(...result);
}
}
// Filter out leading/trailing separators
// TODO: https://github.com/electron/electron/issues/5869
menuTemplate = removeUnusedMenuItems(menuTemplate);
for (const menuItem of menuTemplate) {
// Apply custom labels for default menu items
if (options.labels && options.labels[menuItem.id]) {
menuItem.label = options.labels[menuItem.id];
}
// Replace placeholders in menu item labels
// if (typeof menuItem.label === 'string' && menuItem.label.includes('{selection}')) {
// const selectionString = typeof props.selectionText === 'string' ? props.selectionText.trim() : '';
// menuItem.label = menuItem.label.replace('{selection}', cliTruncate(selectionString, 25));
// }
}
if (menuTemplate.length > 0) {
const menu = (electronRemote ? electronRemote.Menu : electron.Menu).buildFromTemplate(menuTemplate);
//
// When `electronRemote` is not available, this runs in the browser process.
//
// We can safely use `win` in this case as it refers to the window the
// context-menu should open in.
//
// When this is being called from a web view, we can't use `win` as this
// would refer to the web view which is not allowed to render a popup menu.
//
menu.popup({ window: electronRemote ? electronRemote.getCurrentWindow() : win });
}
});
};
module.exports = (options = {}) => {
if (options.window) {
const win = options.window;
// When window is a webview that has not yet finished loading webContents is not available
if (webContents(win) === undefined) {
win.addEventListener('dom-ready', () => {
create(win, options);
}, { once: true });
return;
}
return create(win, options);
}
for (const win of (electron.BrowserWindow || electronRemote.BrowserWindow).getAllWindows()) {
create(win, options);
}
(electron.app || electronRemote.app).on('browser-window-created', (event, win) => {
create(win, options);
});
};