dialog.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import { app, BaseWindow } from 'electron/main';
  2. import type { OpenDialogOptions, OpenDialogReturnValue, MessageBoxOptions, SaveDialogOptions, SaveDialogReturnValue, MessageBoxReturnValue, CertificateTrustDialogOptions } from 'electron/main';
  3. const dialogBinding = process._linkedBinding('electron_browser_dialog');
  4. enum SaveFileDialogProperties {
  5. createDirectory = 1 << 0,
  6. showHiddenFiles = 1 << 1,
  7. treatPackageAsDirectory = 1 << 2,
  8. showOverwriteConfirmation = 1 << 3,
  9. dontAddToRecent = 1 << 4
  10. }
  11. enum OpenFileDialogProperties {
  12. openFile = 1 << 0,
  13. openDirectory = 1 << 1,
  14. multiSelections = 1 << 2,
  15. createDirectory = 1 << 3, // macOS
  16. showHiddenFiles = 1 << 4,
  17. promptToCreate = 1 << 5, // Windows
  18. noResolveAliases = 1 << 6, // macOS
  19. treatPackageAsDirectory = 1 << 7, // macOS
  20. dontAddToRecent = 1 << 8 // Windows
  21. }
  22. let nextId = 0;
  23. const getNextId = function () {
  24. return ++nextId;
  25. };
  26. const normalizeAccessKey = (text: string) => {
  27. if (typeof text !== 'string') return text;
  28. // macOS does not have access keys so remove single ampersands
  29. // and replace double ampersands with a single ampersand
  30. if (process.platform === 'darwin') {
  31. return text.replaceAll(/&(&?)/g, '$1');
  32. }
  33. // Linux uses a single underscore as an access key prefix so escape
  34. // existing single underscores with a second underscore, replace double
  35. // ampersands with a single ampersand, and replace a single ampersand with
  36. // a single underscore
  37. if (process.platform === 'linux') {
  38. return text.replaceAll('_', '__').replaceAll(/&(.?)/g, (match, after) => {
  39. if (after === '&') return after;
  40. return `_${after}`;
  41. });
  42. }
  43. return text;
  44. };
  45. const checkAppInitialized = function () {
  46. if (!app.isReady()) {
  47. throw new Error('dialog module can only be used after app is ready');
  48. }
  49. };
  50. const setupOpenDialogProperties = (properties: (keyof typeof OpenFileDialogProperties)[]): number => {
  51. let dialogProperties = 0;
  52. for (const property of properties) {
  53. if (Object.hasOwn(OpenFileDialogProperties, property)) { dialogProperties |= OpenFileDialogProperties[property]; }
  54. }
  55. return dialogProperties;
  56. };
  57. const setupSaveDialogProperties = (properties: (keyof typeof SaveFileDialogProperties)[]): number => {
  58. let dialogProperties = 0;
  59. for (const property of properties) {
  60. if (Object.hasOwn(SaveFileDialogProperties, property)) { dialogProperties |= SaveFileDialogProperties[property]; }
  61. }
  62. return dialogProperties;
  63. };
  64. const saveDialog = (sync: boolean, window: BaseWindow | null, options?: SaveDialogOptions) => {
  65. checkAppInitialized();
  66. if (options == null) options = { title: 'Save' };
  67. const {
  68. buttonLabel = '',
  69. defaultPath = '',
  70. filters = [],
  71. properties = [],
  72. title = '',
  73. message = '',
  74. securityScopedBookmarks = false,
  75. nameFieldLabel = '',
  76. showsTagField = true
  77. } = options;
  78. if (typeof title !== 'string') throw new TypeError('Title must be a string');
  79. if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string');
  80. if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string');
  81. if (typeof message !== 'string') throw new TypeError('Message must be a string');
  82. if (typeof nameFieldLabel !== 'string') throw new TypeError('Name field label must be a string');
  83. const settings = {
  84. buttonLabel,
  85. defaultPath,
  86. filters,
  87. title,
  88. message,
  89. securityScopedBookmarks,
  90. nameFieldLabel,
  91. showsTagField,
  92. window,
  93. properties: setupSaveDialogProperties(properties)
  94. };
  95. return sync ? dialogBinding.showSaveDialogSync(settings) : dialogBinding.showSaveDialog(settings);
  96. };
  97. const openDialog = (sync: boolean, window: BaseWindow | null, options?: OpenDialogOptions) => {
  98. checkAppInitialized();
  99. if (options == null) {
  100. options = {
  101. title: 'Open',
  102. properties: ['openFile']
  103. };
  104. }
  105. const {
  106. buttonLabel = '',
  107. defaultPath = '',
  108. filters = [],
  109. properties = ['openFile'],
  110. title = '',
  111. message = '',
  112. securityScopedBookmarks = false
  113. } = options;
  114. if (!Array.isArray(properties)) throw new TypeError('Properties must be an array');
  115. if (typeof title !== 'string') throw new TypeError('Title must be a string');
  116. if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string');
  117. if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string');
  118. if (typeof message !== 'string') throw new TypeError('Message must be a string');
  119. const settings = {
  120. title,
  121. buttonLabel,
  122. defaultPath,
  123. filters,
  124. message,
  125. securityScopedBookmarks,
  126. window,
  127. properties: setupOpenDialogProperties(properties)
  128. };
  129. return (sync) ? dialogBinding.showOpenDialogSync(settings) : dialogBinding.showOpenDialog(settings);
  130. };
  131. const messageBox = (sync: boolean, window: BaseWindow | null, options?: MessageBoxOptions) => {
  132. checkAppInitialized();
  133. if (options == null) options = { type: 'none', message: '' };
  134. const messageBoxTypes = ['none', 'info', 'warning', 'error', 'question'];
  135. let {
  136. buttons = [],
  137. cancelId,
  138. signal,
  139. checkboxLabel = '',
  140. checkboxChecked,
  141. defaultId = -1,
  142. detail = '',
  143. icon = null,
  144. textWidth = 0,
  145. noLink = false,
  146. message = '',
  147. title = '',
  148. type = 'none'
  149. } = options;
  150. const messageBoxType = messageBoxTypes.indexOf(type);
  151. if (messageBoxType === -1) throw new TypeError('Invalid message box type');
  152. if (!Array.isArray(buttons)) throw new TypeError('Buttons must be an array');
  153. if (options.normalizeAccessKeys) buttons = buttons.map(normalizeAccessKey);
  154. if (typeof title !== 'string') throw new TypeError('Title must be a string');
  155. if (typeof noLink !== 'boolean') throw new TypeError('noLink must be a boolean');
  156. if (typeof message !== 'string') throw new TypeError('Message must be a string');
  157. if (typeof detail !== 'string') throw new TypeError('Detail must be a string');
  158. if (typeof checkboxLabel !== 'string') throw new TypeError('checkboxLabel must be a string');
  159. checkboxChecked = !!checkboxChecked;
  160. if (checkboxChecked && !checkboxLabel) {
  161. throw new Error('checkboxChecked requires that checkboxLabel also be passed');
  162. }
  163. // Choose a default button to get selected when dialog is cancelled.
  164. if (cancelId == null) {
  165. // If the defaultId is set to 0, ensure the cancel button is a different index (1)
  166. cancelId = (defaultId === 0 && buttons.length > 1) ? 1 : 0;
  167. for (const [i, button] of buttons.entries()) {
  168. const text = button.toLowerCase();
  169. if (text === 'cancel' || text === 'no') {
  170. cancelId = i;
  171. break;
  172. }
  173. }
  174. }
  175. // AbortSignal processing.
  176. let id: number | undefined;
  177. if (signal) {
  178. // Generate an ID used for closing the message box.
  179. id = getNextId();
  180. // Close the message box when signal is aborted.
  181. if (signal.aborted) { return Promise.resolve({ cancelId, checkboxChecked }); }
  182. signal.addEventListener('abort', () => dialogBinding._closeMessageBox(id));
  183. }
  184. const settings = {
  185. window,
  186. messageBoxType,
  187. buttons,
  188. id,
  189. defaultId,
  190. cancelId,
  191. noLink,
  192. title,
  193. message,
  194. detail,
  195. checkboxLabel,
  196. checkboxChecked,
  197. icon,
  198. textWidth
  199. };
  200. if (sync) {
  201. return dialogBinding.showMessageBoxSync(settings);
  202. } else {
  203. return dialogBinding.showMessageBox(settings);
  204. }
  205. };
  206. export function showOpenDialog(window: BaseWindow, options: OpenDialogOptions): OpenDialogReturnValue;
  207. export function showOpenDialog(options: OpenDialogOptions): OpenDialogReturnValue;
  208. export function showOpenDialog (windowOrOptions: BaseWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue {
  209. const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
  210. const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
  211. return openDialog(false, window, options);
  212. }
  213. export function showOpenDialogSync(window: BaseWindow, options: OpenDialogOptions): OpenDialogReturnValue;
  214. export function showOpenDialogSync(options: OpenDialogOptions): OpenDialogReturnValue;
  215. export function showOpenDialogSync (windowOrOptions: BaseWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue {
  216. const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
  217. const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
  218. return openDialog(true, window, options);
  219. }
  220. export function showSaveDialog(window: BaseWindow, options: SaveDialogOptions): SaveDialogReturnValue;
  221. export function showSaveDialog(options: SaveDialogOptions): SaveDialogReturnValue;
  222. export function showSaveDialog (windowOrOptions: BaseWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue {
  223. const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
  224. const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
  225. return saveDialog(false, window, options);
  226. }
  227. export function showSaveDialogSync(window: BaseWindow, options: SaveDialogOptions): SaveDialogReturnValue;
  228. export function showSaveDialogSync(options: SaveDialogOptions): SaveDialogReturnValue;
  229. export function showSaveDialogSync (windowOrOptions: BaseWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue {
  230. const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
  231. const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
  232. return saveDialog(true, window, options);
  233. }
  234. export function showMessageBox(window: BaseWindow, options: MessageBoxOptions): MessageBoxReturnValue;
  235. export function showMessageBox(options: MessageBoxOptions): MessageBoxReturnValue;
  236. export function showMessageBox (windowOrOptions: BaseWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue {
  237. const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
  238. const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
  239. return messageBox(false, window, options);
  240. }
  241. export function showMessageBoxSync(window: BaseWindow, options: MessageBoxOptions): MessageBoxReturnValue;
  242. export function showMessageBoxSync(options: MessageBoxOptions): MessageBoxReturnValue;
  243. export function showMessageBoxSync (windowOrOptions: BaseWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue {
  244. const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
  245. const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
  246. return messageBox(true, window, options);
  247. }
  248. export function showErrorBox (...args: any[]) {
  249. return dialogBinding.showErrorBox(...args);
  250. }
  251. export function showCertificateTrustDialog (windowOrOptions: BaseWindow | CertificateTrustDialogOptions, maybeOptions?: CertificateTrustDialogOptions) {
  252. const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
  253. const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
  254. if (options == null || typeof options !== 'object') {
  255. throw new TypeError('options must be an object');
  256. }
  257. const { certificate, message = '' } = options;
  258. if (certificate == null || typeof certificate !== 'object') {
  259. throw new TypeError('certificate must be an object');
  260. }
  261. if (typeof message !== 'string') throw new TypeError('message must be a string');
  262. return dialogBinding.showCertificateTrustDialog(window, certificate, message);
  263. }