dialog.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. 'use strict'
  2. const {app, BrowserWindow} = require('electron')
  3. const binding = process.atomBinding('dialog')
  4. const v8Util = process.atomBinding('v8_util')
  5. const fileDialogProperties = {
  6. openFile: 1 << 0,
  7. openDirectory: 1 << 1,
  8. multiSelections: 1 << 2,
  9. createDirectory: 1 << 3,
  10. showHiddenFiles: 1 << 4,
  11. promptToCreate: 1 << 5
  12. }
  13. const messageBoxTypes = ['none', 'info', 'warning', 'error', 'question']
  14. const messageBoxOptions = {
  15. noLink: 1 << 0
  16. }
  17. const parseArgs = function (window, options, callback, ...args) {
  18. if (window != null && window.constructor !== BrowserWindow) {
  19. // Shift.
  20. [callback, options, window] = [options, window, null]
  21. }
  22. if ((callback == null) && typeof options === 'function') {
  23. // Shift.
  24. [callback, options] = [options, null]
  25. }
  26. // Fallback to using very last argument as the callback function
  27. const lastArgument = args[args.length - 1]
  28. if ((callback == null) && typeof lastArgument === 'function') {
  29. callback = lastArgument
  30. }
  31. return [window, options, callback]
  32. }
  33. const normalizeAccessKey = (text) => {
  34. if (typeof text !== 'string') return text
  35. // macOS does not have access keys so remove single ampersands
  36. // and replace double ampersands with a single ampersand
  37. if (process.platform === 'darwin') {
  38. return text.replace(/&(&?)/g, '$1')
  39. }
  40. // Linux uses a single underscore as an access key prefix so escape
  41. // existing single underscores with a second underscore, replace double
  42. // ampersands with a single ampersand, and replace a single ampersand with
  43. // a single underscore
  44. if (process.platform === 'linux') {
  45. return text.replace(/_/g, '__').replace(/&(.?)/g, (match, after) => {
  46. if (after === '&') return after
  47. return `_${after}`
  48. })
  49. }
  50. return text
  51. }
  52. const checkAppInitialized = function () {
  53. if (!app.isReady()) {
  54. throw new Error('dialog module can only be used after app is ready')
  55. }
  56. }
  57. module.exports = {
  58. showOpenDialog: function (...args) {
  59. checkAppInitialized()
  60. let [window, options, callback] = parseArgs(...args)
  61. if (options == null) {
  62. options = {
  63. title: 'Open',
  64. properties: ['openFile']
  65. }
  66. }
  67. let {buttonLabel, defaultPath, filters, properties, title} = options
  68. if (properties == null) {
  69. properties = ['openFile']
  70. } else if (!Array.isArray(properties)) {
  71. throw new TypeError('Properties must be an array')
  72. }
  73. let dialogProperties = 0
  74. for (const prop in fileDialogProperties) {
  75. if (properties.includes(prop)) {
  76. dialogProperties |= fileDialogProperties[prop]
  77. }
  78. }
  79. if (title == null) {
  80. title = ''
  81. } else if (typeof title !== 'string') {
  82. throw new TypeError('Title must be a string')
  83. }
  84. if (buttonLabel == null) {
  85. buttonLabel = ''
  86. } else if (typeof buttonLabel !== 'string') {
  87. throw new TypeError('Button label must be a string')
  88. }
  89. if (defaultPath == null) {
  90. defaultPath = ''
  91. } else if (typeof defaultPath !== 'string') {
  92. throw new TypeError('Default path must be a string')
  93. }
  94. if (filters == null) {
  95. filters = []
  96. }
  97. const wrappedCallback = typeof callback === 'function' ? function (success, result) {
  98. return callback(success ? result : void 0)
  99. } : null
  100. return binding.showOpenDialog(title, buttonLabel, defaultPath, filters,
  101. dialogProperties, window, wrappedCallback)
  102. },
  103. showSaveDialog: function (...args) {
  104. checkAppInitialized()
  105. let [window, options, callback] = parseArgs(...args)
  106. if (options == null) {
  107. options = {
  108. title: 'Save'
  109. }
  110. }
  111. let {buttonLabel, defaultPath, filters, title} = options
  112. if (title == null) {
  113. title = ''
  114. } else if (typeof title !== 'string') {
  115. throw new TypeError('Title must be a string')
  116. }
  117. if (buttonLabel == null) {
  118. buttonLabel = ''
  119. } else if (typeof buttonLabel !== 'string') {
  120. throw new TypeError('Button label must be a string')
  121. }
  122. if (defaultPath == null) {
  123. defaultPath = ''
  124. } else if (typeof defaultPath !== 'string') {
  125. throw new TypeError('Default path must be a string')
  126. }
  127. if (filters == null) {
  128. filters = []
  129. }
  130. const wrappedCallback = typeof callback === 'function' ? function (success, result) {
  131. return callback(success ? result : void 0)
  132. } : null
  133. return binding.showSaveDialog(title, buttonLabel, defaultPath, filters,
  134. window, wrappedCallback)
  135. },
  136. showMessageBox: function (...args) {
  137. checkAppInitialized()
  138. let [window, options, callback] = parseArgs(...args)
  139. if (options == null) {
  140. options = {
  141. type: 'none'
  142. }
  143. }
  144. let {buttons, cancelId, defaultId, detail, icon, message, title, type} = options
  145. if (type == null) {
  146. type = 'none'
  147. }
  148. const messageBoxType = messageBoxTypes.indexOf(type)
  149. if (messageBoxType === -1) {
  150. throw new TypeError('Invalid message box type')
  151. }
  152. if (buttons == null) {
  153. buttons = []
  154. } else if (!Array.isArray(buttons)) {
  155. throw new TypeError('Buttons must be an array')
  156. }
  157. if (options.normalizeAccessKeys) {
  158. buttons = buttons.map(normalizeAccessKey)
  159. }
  160. if (title == null) {
  161. title = ''
  162. } else if (typeof title !== 'string') {
  163. throw new TypeError('Title must be a string')
  164. }
  165. if (message == null) {
  166. message = ''
  167. } else if (typeof message !== 'string') {
  168. throw new TypeError('Message must be a string')
  169. }
  170. if (detail == null) {
  171. detail = ''
  172. } else if (typeof detail !== 'string') {
  173. throw new TypeError('Detail must be a string')
  174. }
  175. if (icon == null) {
  176. icon = null
  177. }
  178. if (defaultId == null) {
  179. defaultId = -1
  180. }
  181. // Choose a default button to get selected when dialog is cancelled.
  182. if (cancelId == null) {
  183. cancelId = 0
  184. for (let i = 0; i < buttons.length; i++) {
  185. const text = buttons[i].toLowerCase()
  186. if (text === 'cancel' || text === 'no') {
  187. cancelId = i
  188. break
  189. }
  190. }
  191. }
  192. const flags = options.noLink ? messageBoxOptions.noLink : 0
  193. return binding.showMessageBox(messageBoxType, buttons, defaultId, cancelId,
  194. flags, title, message, detail, icon, window,
  195. callback)
  196. },
  197. showErrorBox: function (...args) {
  198. return binding.showErrorBox(...args)
  199. }
  200. }
  201. // Mark standard asynchronous functions.
  202. v8Util.setHiddenValue(module.exports.showMessageBox, 'asynchronous', true)
  203. v8Util.setHiddenValue(module.exports.showOpenDialog, 'asynchronous', true)
  204. v8Util.setHiddenValue(module.exports.showSaveDialog, 'asynchronous', true)