331 lines
8.9 KiB
TypeScript
331 lines
8.9 KiB
TypeScript
import { Menu, ipcMain, shell, app } from 'electron';
|
|
import { ensureDir } from 'fs-extra';
|
|
|
|
import { ensureItemIds } from './ensure-item-ids';
|
|
import { MenuEvent } from '../../commons/type/menu-event';
|
|
import { openDirectorySafe } from '../shell';
|
|
|
|
|
|
export function buildDefaultMenu(
|
|
): Electron.Menu {
|
|
const template = new Array<Electron.MenuItemConstructorOptions>();
|
|
const separator: Electron.MenuItemConstructorOptions = { type: 'separator' };
|
|
|
|
if (__DARWIN__) {
|
|
template.push({
|
|
label: 'overFlow Scanner',
|
|
submenu: [
|
|
{
|
|
label: 'About overFlow Scanner',
|
|
click: emit('show-about'),
|
|
id: 'about',
|
|
},
|
|
separator,
|
|
{
|
|
label: 'Preferences…',
|
|
id: 'preferences',
|
|
accelerator: 'CmdOrCtrl+,',
|
|
click: emit('show-preferences'),
|
|
},
|
|
separator,
|
|
{
|
|
role: 'services',
|
|
submenu: [],
|
|
},
|
|
separator,
|
|
{ role: 'hide' },
|
|
{ role: 'hideothers' },
|
|
{ role: 'unhide' },
|
|
separator,
|
|
{ role: 'quit' },
|
|
],
|
|
});
|
|
}
|
|
|
|
const fileMenu: Electron.MenuItemConstructorOptions = {
|
|
label: __DARWIN__ ? 'File' : '&File',
|
|
submenu: [
|
|
{
|
|
label: __DARWIN__ ? 'Save…' : 'Save…',
|
|
id: 'save',
|
|
click: emit('save'),
|
|
accelerator: 'CmdOrCtrl+S',
|
|
},
|
|
separator,
|
|
{
|
|
label: __DARWIN__ ? 'Export as…' : 'Export as…',
|
|
submenu: [
|
|
{
|
|
label: __DARWIN__ ? 'PNG…' : 'PNG…',
|
|
id: 'export-png',
|
|
click: emit('show-export-png'),
|
|
},
|
|
{
|
|
label: __DARWIN__ ? 'JPG…' : 'JPG…',
|
|
id: 'export-jpg',
|
|
click: emit('show-export-jpg'),
|
|
enabled: false,
|
|
},
|
|
{
|
|
label: __DARWIN__ ? 'svg…' : 'svg…',
|
|
id: 'export-svg',
|
|
click: emit('show-export-svg'),
|
|
enabled: false,
|
|
},
|
|
{
|
|
label: __DARWIN__ ? 'CSV…' : 'CSV…',
|
|
id: 'export-csv',
|
|
click: emit('show-export-csv'),
|
|
enabled: false,
|
|
},
|
|
],
|
|
},
|
|
separator,
|
|
{
|
|
label: __DARWIN__ ? 'Print…' : 'Print…',
|
|
id: 'print',
|
|
click: emit('show-print'),
|
|
enabled: false,
|
|
},
|
|
],
|
|
};
|
|
template.push(fileMenu);
|
|
|
|
template.push({
|
|
label: __DARWIN__ ? 'View' : '&View',
|
|
submenu: [
|
|
{
|
|
label: __DARWIN__ ? 'Toggle Full Screen' : 'Toggle &full screen',
|
|
role: 'togglefullscreen',
|
|
},
|
|
separator,
|
|
{
|
|
label: __DARWIN__ ? 'Reset Zoom' : 'Reset zoom',
|
|
accelerator: 'CmdOrCtrl+0',
|
|
click: zoom(ZoomDirection.Reset),
|
|
},
|
|
{
|
|
label: __DARWIN__ ? 'Zoom In' : 'Zoom in',
|
|
accelerator: 'CmdOrCtrl+=',
|
|
click: zoom(ZoomDirection.In),
|
|
},
|
|
{
|
|
label: __DARWIN__ ? 'Zoom Out' : 'Zoom out',
|
|
accelerator: 'CmdOrCtrl+-',
|
|
click: zoom(ZoomDirection.Out),
|
|
},
|
|
separator,
|
|
{
|
|
label: '&Reload',
|
|
id: 'reload-window',
|
|
// Ctrl+Alt is interpreted as AltGr on international keyboards and this
|
|
// can clash with other shortcuts. We should always use Ctrl+Shift for
|
|
// chorded shortcuts, but this menu item is not a user-facing feature
|
|
// so we are going to keep this one around and save Ctrl+Shift+R for
|
|
// a different shortcut in the future...
|
|
accelerator: 'CmdOrCtrl+Alt+R',
|
|
click(item: any, focusedWindow: Electron.BrowserWindow) {
|
|
if (focusedWindow) {
|
|
focusedWindow.reload();
|
|
}
|
|
},
|
|
visible: __RELEASE_CHANNEL__ === 'development',
|
|
},
|
|
{
|
|
id: 'show-devtools',
|
|
label: __DARWIN__
|
|
? 'Toggle Developer Tools'
|
|
: '&Toggle developer tools',
|
|
accelerator: (() => {
|
|
return __DARWIN__ ? 'Alt+Command+I' : 'Ctrl+Shift+I';
|
|
})(),
|
|
click(item: any, focusedWindow: Electron.BrowserWindow) {
|
|
if (focusedWindow) {
|
|
focusedWindow.webContents.toggleDevTools();
|
|
}
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
if (__DARWIN__) {
|
|
template.push({
|
|
role: 'window',
|
|
submenu: [
|
|
{ role: 'minimize' },
|
|
{ role: 'zoom' },
|
|
{ role: 'close' },
|
|
separator,
|
|
{ role: 'front' },
|
|
],
|
|
});
|
|
}
|
|
|
|
const settingMenu: Electron.MenuItemConstructorOptions = {
|
|
label: __DARWIN__ ? 'Discovery' : '&Discovery',
|
|
submenu: [
|
|
{
|
|
label: __DARWIN__ ? 'Setting…' : 'Setting…',
|
|
id: 'preferences',
|
|
accelerator: 'CmdOrCtrl+,',
|
|
click: emit('show-preferences'),
|
|
},
|
|
{
|
|
label: __DARWIN__ ? 'Language…' : 'Language…',
|
|
submenu: [
|
|
{
|
|
label: __DARWIN__ ? 'English…' : 'English…',
|
|
id: 'language-english',
|
|
click: emit('change-language-english'),
|
|
},
|
|
{
|
|
label: __DARWIN__ ? 'Korean…' : 'Korean…',
|
|
id: 'language-korean',
|
|
click: emit('change-language-korean'),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
template.push(settingMenu);
|
|
|
|
const submitIssueItem: Electron.MenuItemConstructorOptions = {
|
|
label: __DARWIN__ ? 'Report Issue…' : 'Report issue…',
|
|
click() {
|
|
shell.openExternal('https://github.com/desktop/desktop/issues/new/choose');
|
|
},
|
|
};
|
|
|
|
const contactSupportItem: Electron.MenuItemConstructorOptions = {
|
|
label: __DARWIN__ ? 'Contact overFlow Scanner Support…' : '&Contact overFlow Scanner support…',
|
|
click() {
|
|
shell.openExternal(
|
|
`https://github.com/contact?from_desktop_app=1&app_version=${app.getVersion()}`
|
|
);
|
|
},
|
|
};
|
|
|
|
const showUserGuides: Electron.MenuItemConstructorOptions = {
|
|
label: 'Show User Guides',
|
|
click() {
|
|
shell.openExternal('https://help.github.com/desktop/guides/');
|
|
},
|
|
};
|
|
|
|
const helpItems = [
|
|
submitIssueItem,
|
|
contactSupportItem,
|
|
showUserGuides,
|
|
];
|
|
|
|
if (__DARWIN__) {
|
|
template.push({
|
|
role: 'help',
|
|
submenu: helpItems,
|
|
});
|
|
} else {
|
|
template.push({
|
|
label: '&Help',
|
|
submenu: [
|
|
...helpItems,
|
|
separator,
|
|
{
|
|
label: '&About overFlow Scanner',
|
|
click: emit('show-about'),
|
|
id: 'about',
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
ensureItemIds(template);
|
|
|
|
return Menu.buildFromTemplate(template);
|
|
}
|
|
|
|
type ClickHandler = (
|
|
menuItem: Electron.MenuItem,
|
|
browserWindow: Electron.BrowserWindow,
|
|
event: Electron.Event
|
|
) => void;
|
|
|
|
/**
|
|
* Utility function returning a Click event handler which, when invoked, emits
|
|
* the provided menu event over IPC.
|
|
*/
|
|
function emit(name: MenuEvent): ClickHandler {
|
|
return (menuItem, window) => {
|
|
if (window) {
|
|
window.webContents.send('menu-event', { name });
|
|
} else {
|
|
ipcMain.emit('menu-event', { name });
|
|
}
|
|
};
|
|
}
|
|
|
|
enum ZoomDirection {
|
|
Reset,
|
|
In,
|
|
Out,
|
|
}
|
|
|
|
/** The zoom steps that we support, these factors must sorted */
|
|
const ZoomInFactors = [1, 1.1, 1.25, 1.5, 1.75, 2];
|
|
const ZoomOutFactors = ZoomInFactors.slice().reverse();
|
|
|
|
/**
|
|
* Returns the element in the array that's closest to the value parameter. Note
|
|
* that this function will throw if passed an empty array.
|
|
*/
|
|
function findClosestValue(arr: Array<number>, value: number) {
|
|
return arr.reduce((previous, current) => {
|
|
return Math.abs(current - value) < Math.abs(previous - value)
|
|
? current
|
|
: previous;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Figure out the next zoom level for the given direction and alert the renderer
|
|
* about a change in zoom factor if necessary.
|
|
*/
|
|
function zoom(direction: ZoomDirection): ClickHandler {
|
|
return (menuItem, window) => {
|
|
if (!window) {
|
|
return;
|
|
}
|
|
|
|
const { webContents } = window;
|
|
|
|
if (direction === ZoomDirection.Reset) {
|
|
webContents.setZoomFactor(1);
|
|
webContents.send('zoom-factor-changed', 1);
|
|
} else {
|
|
webContents.getZoomFactor(rawZoom => {
|
|
const zoomFactors =
|
|
direction === ZoomDirection.In ? ZoomInFactors : ZoomOutFactors;
|
|
|
|
// So the values that we get from getZoomFactor are floating point
|
|
// precision numbers from chromium that don't always round nicely so
|
|
// we'll have to do a little trick to figure out which of our supported
|
|
// zoom factors the value is referring to.
|
|
const currentZoom = findClosestValue(zoomFactors, rawZoom);
|
|
|
|
const nextZoomLevel = zoomFactors.find(
|
|
f =>
|
|
direction === ZoomDirection.In ? f > currentZoom : f < currentZoom
|
|
);
|
|
|
|
// If we couldn't find a zoom level (likely due to manual manipulation
|
|
// of the zoom factor in devtools) we'll just snap to the closest valid
|
|
// factor we've got.
|
|
const newZoom =
|
|
nextZoomLevel === undefined ? currentZoom : nextZoomLevel;
|
|
|
|
webContents.setZoomFactor(newZoom);
|
|
webContents.send('zoom-factor-changed', newZoom);
|
|
});
|
|
}
|
|
};
|
|
}
|