menu-item-roles.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import { app, BrowserWindow, session, webContents, WebContents, MenuItemConstructorOptions } from 'electron/main';
  2. const isMac = process.platform === 'darwin';
  3. const isWindows = process.platform === 'win32';
  4. const isLinux = process.platform === 'linux';
  5. type RoleId = 'about' | 'close' | 'copy' | 'cut' | 'delete' | 'forcereload' | 'front' | 'help' | 'hide' | 'hideothers' | 'minimize' |
  6. 'paste' | 'pasteandmatchstyle' | 'quit' | 'redo' | 'reload' | 'resetzoom' | 'selectall' | 'services' | 'recentdocuments' | 'clearrecentdocuments' | 'startspeaking' | 'stopspeaking' |
  7. 'toggledevtools' | 'togglefullscreen' | 'undo' | 'unhide' | 'window' | 'zoom' | 'zoomin' | 'zoomout' | 'togglespellchecker' |
  8. 'appmenu' | 'filemenu' | 'editmenu' | 'viewmenu' | 'windowmenu' | 'sharemenu'
  9. interface Role {
  10. label: string;
  11. accelerator?: string;
  12. checked?: boolean;
  13. windowMethod?: ((window: BrowserWindow) => void);
  14. webContentsMethod?: ((webContents: WebContents) => void);
  15. appMethod?: () => void;
  16. registerAccelerator?: boolean;
  17. nonNativeMacOSRole?: boolean;
  18. submenu?: MenuItemConstructorOptions[];
  19. }
  20. export const roleList: Record<RoleId, Role> = {
  21. about: {
  22. get label () {
  23. return isLinux ? 'About' : `About ${app.name}`;
  24. },
  25. ...(isWindows && { appMethod: () => app.showAboutPanel() })
  26. },
  27. close: {
  28. label: isMac ? 'Close Window' : 'Close',
  29. accelerator: 'CommandOrControl+W',
  30. windowMethod: w => w.close()
  31. },
  32. copy: {
  33. label: 'Copy',
  34. accelerator: 'CommandOrControl+C',
  35. webContentsMethod: wc => wc.copy(),
  36. registerAccelerator: false
  37. },
  38. cut: {
  39. label: 'Cut',
  40. accelerator: 'CommandOrControl+X',
  41. webContentsMethod: wc => wc.cut(),
  42. registerAccelerator: false
  43. },
  44. delete: {
  45. label: 'Delete',
  46. webContentsMethod: wc => wc.delete()
  47. },
  48. forcereload: {
  49. label: 'Force Reload',
  50. accelerator: 'Shift+CmdOrCtrl+R',
  51. nonNativeMacOSRole: true,
  52. windowMethod: (window: BrowserWindow) => {
  53. window.webContents.reloadIgnoringCache();
  54. }
  55. },
  56. front: {
  57. label: 'Bring All to Front'
  58. },
  59. help: {
  60. label: 'Help'
  61. },
  62. hide: {
  63. get label () {
  64. return `Hide ${app.name}`;
  65. },
  66. accelerator: 'Command+H'
  67. },
  68. hideothers: {
  69. label: 'Hide Others',
  70. accelerator: 'Command+Alt+H'
  71. },
  72. minimize: {
  73. label: 'Minimize',
  74. accelerator: 'CommandOrControl+M',
  75. windowMethod: w => w.minimize()
  76. },
  77. paste: {
  78. label: 'Paste',
  79. accelerator: 'CommandOrControl+V',
  80. webContentsMethod: wc => wc.paste(),
  81. registerAccelerator: false
  82. },
  83. pasteandmatchstyle: {
  84. label: 'Paste and Match Style',
  85. accelerator: isMac ? 'Cmd+Option+Shift+V' : 'Shift+CommandOrControl+V',
  86. webContentsMethod: wc => wc.pasteAndMatchStyle(),
  87. registerAccelerator: false
  88. },
  89. quit: {
  90. get label () {
  91. switch (process.platform) {
  92. case 'darwin': return `Quit ${app.name}`;
  93. case 'win32': return 'Exit';
  94. default: return 'Quit';
  95. }
  96. },
  97. accelerator: isWindows ? undefined : 'CommandOrControl+Q',
  98. appMethod: () => app.quit()
  99. },
  100. redo: {
  101. label: 'Redo',
  102. accelerator: isWindows ? 'Control+Y' : 'Shift+CommandOrControl+Z',
  103. webContentsMethod: wc => wc.redo()
  104. },
  105. reload: {
  106. label: 'Reload',
  107. accelerator: 'CmdOrCtrl+R',
  108. nonNativeMacOSRole: true,
  109. windowMethod: w => w.reload()
  110. },
  111. resetzoom: {
  112. label: 'Actual Size',
  113. accelerator: 'CommandOrControl+0',
  114. nonNativeMacOSRole: true,
  115. webContentsMethod: (webContents: WebContents) => {
  116. webContents.zoomLevel = 0;
  117. }
  118. },
  119. selectall: {
  120. label: 'Select All',
  121. accelerator: 'CommandOrControl+A',
  122. webContentsMethod: wc => wc.selectAll()
  123. },
  124. services: {
  125. label: 'Services'
  126. },
  127. recentdocuments: {
  128. label: 'Open Recent'
  129. },
  130. clearrecentdocuments: {
  131. label: 'Clear Menu'
  132. },
  133. startspeaking: {
  134. label: 'Start Speaking'
  135. },
  136. stopspeaking: {
  137. label: 'Stop Speaking'
  138. },
  139. toggledevtools: {
  140. label: 'Toggle Developer Tools',
  141. accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I',
  142. nonNativeMacOSRole: true,
  143. windowMethod: w => w.webContents.toggleDevTools()
  144. },
  145. togglefullscreen: {
  146. label: 'Toggle Full Screen',
  147. accelerator: isMac ? 'Control+Command+F' : 'F11',
  148. windowMethod: (window: BrowserWindow) => {
  149. window.setFullScreen(!window.isFullScreen());
  150. }
  151. },
  152. undo: {
  153. label: 'Undo',
  154. accelerator: 'CommandOrControl+Z',
  155. webContentsMethod: wc => wc.undo()
  156. },
  157. unhide: {
  158. label: 'Show All'
  159. },
  160. window: {
  161. label: 'Window'
  162. },
  163. zoom: {
  164. label: 'Zoom'
  165. },
  166. zoomin: {
  167. label: 'Zoom In',
  168. accelerator: 'CommandOrControl+Plus',
  169. nonNativeMacOSRole: true,
  170. webContentsMethod: (webContents: WebContents) => {
  171. webContents.zoomLevel += 0.5;
  172. }
  173. },
  174. zoomout: {
  175. label: 'Zoom Out',
  176. accelerator: 'CommandOrControl+-',
  177. nonNativeMacOSRole: true,
  178. webContentsMethod: (webContents: WebContents) => {
  179. webContents.zoomLevel -= 0.5;
  180. }
  181. },
  182. togglespellchecker: {
  183. label: 'Check Spelling While Typing',
  184. get checked () {
  185. const wc = webContents.getFocusedWebContents();
  186. const ses = wc ? wc.session : session.defaultSession;
  187. return ses.spellCheckerEnabled;
  188. },
  189. nonNativeMacOSRole: true,
  190. webContentsMethod: (wc: WebContents) => {
  191. const ses = wc ? wc.session : session.defaultSession;
  192. ses.spellCheckerEnabled = !ses.spellCheckerEnabled;
  193. }
  194. },
  195. // App submenu should be used for Mac only
  196. appmenu: {
  197. get label () {
  198. return app.name;
  199. },
  200. submenu: [
  201. { role: 'about' },
  202. { type: 'separator' },
  203. { role: 'services' },
  204. { type: 'separator' },
  205. { role: 'hide' },
  206. { role: 'hideOthers' },
  207. { role: 'unhide' },
  208. { type: 'separator' },
  209. { role: 'quit' }
  210. ]
  211. },
  212. // File submenu
  213. filemenu: {
  214. label: 'File',
  215. submenu: [
  216. isMac ? { role: 'close' } : { role: 'quit' }
  217. ]
  218. },
  219. // Edit submenu
  220. editmenu: {
  221. label: 'Edit',
  222. submenu: [
  223. { role: 'undo' },
  224. { role: 'redo' },
  225. { type: 'separator' },
  226. { role: 'cut' },
  227. { role: 'copy' },
  228. { role: 'paste' },
  229. ...(isMac ? [
  230. { role: 'pasteAndMatchStyle' },
  231. { role: 'delete' },
  232. { role: 'selectAll' },
  233. { type: 'separator' },
  234. {
  235. label: 'Speech',
  236. submenu: [
  237. { role: 'startSpeaking' },
  238. { role: 'stopSpeaking' }
  239. ]
  240. }
  241. ] as MenuItemConstructorOptions[] : [
  242. { role: 'delete' },
  243. { type: 'separator' },
  244. { role: 'selectAll' }
  245. ] as MenuItemConstructorOptions[])
  246. ]
  247. },
  248. // View submenu
  249. viewmenu: {
  250. label: 'View',
  251. submenu: [
  252. { role: 'reload' },
  253. { role: 'forceReload' },
  254. { role: 'toggleDevTools' },
  255. { type: 'separator' },
  256. { role: 'resetZoom' },
  257. { role: 'zoomIn' },
  258. { role: 'zoomOut' },
  259. { type: 'separator' },
  260. { role: 'togglefullscreen' }
  261. ]
  262. },
  263. // Window submenu
  264. windowmenu: {
  265. label: 'Window',
  266. submenu: [
  267. { role: 'minimize' },
  268. { role: 'zoom' },
  269. ...(isMac ? [
  270. { type: 'separator' },
  271. { role: 'front' }
  272. ] as MenuItemConstructorOptions[] : [
  273. { role: 'close' }
  274. ] as MenuItemConstructorOptions[])
  275. ]
  276. },
  277. // Share submenu
  278. sharemenu: {
  279. label: 'Share',
  280. submenu: []
  281. }
  282. };
  283. const hasRole = (role: keyof typeof roleList) => {
  284. return Object.prototype.hasOwnProperty.call(roleList, role);
  285. };
  286. const canExecuteRole = (role: keyof typeof roleList) => {
  287. if (!hasRole(role)) return false;
  288. if (!isMac) return true;
  289. // macOS handles all roles natively except for a few
  290. return roleList[role].nonNativeMacOSRole;
  291. };
  292. export function getDefaultType (role: RoleId) {
  293. if (shouldOverrideCheckStatus(role)) return 'checkbox';
  294. return 'normal';
  295. }
  296. export function getDefaultLabel (role: RoleId) {
  297. return hasRole(role) ? roleList[role].label : '';
  298. }
  299. export function getCheckStatus (role: RoleId) {
  300. if (hasRole(role)) return roleList[role].checked;
  301. }
  302. export function shouldOverrideCheckStatus (role: RoleId) {
  303. return hasRole(role) && Object.prototype.hasOwnProperty.call(roleList[role], 'checked');
  304. }
  305. export function getDefaultAccelerator (role: RoleId) {
  306. if (hasRole(role)) return roleList[role].accelerator;
  307. }
  308. export function shouldRegisterAccelerator (role: RoleId) {
  309. const hasRoleRegister = hasRole(role) && roleList[role].registerAccelerator !== undefined;
  310. return hasRoleRegister ? roleList[role].registerAccelerator : true;
  311. }
  312. export function getDefaultSubmenu (role: RoleId) {
  313. if (!hasRole(role)) return;
  314. let { submenu } = roleList[role];
  315. // remove null items from within the submenu
  316. if (Array.isArray(submenu)) {
  317. submenu = submenu.filter((item) => item != null);
  318. }
  319. return submenu;
  320. }
  321. export function execute (role: RoleId, focusedWindow: BrowserWindow, focusedWebContents: WebContents) {
  322. if (!canExecuteRole(role)) return false;
  323. const { appMethod, webContentsMethod, windowMethod } = roleList[role];
  324. if (appMethod) {
  325. appMethod();
  326. return true;
  327. }
  328. if (windowMethod && focusedWindow != null) {
  329. windowMethod(focusedWindow);
  330. return true;
  331. }
  332. if (webContentsMethod && focusedWebContents != null) {
  333. webContentsMethod(focusedWebContents);
  334. return true;
  335. }
  336. return false;
  337. }