menu-item-roles.ts 10 KB

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