diff --git a/.eslintrc.yml b/.eslintrc.yml index 96383c1da..b19c5391f 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -35,7 +35,8 @@ globals: AppController: false MSImageData: false NSImage: false - MSModalInputSheet: false + NSTextField: false + NSSlider: false MSStyleFill: false MSStyleBorder: false MSColor: false diff --git a/CHANGELOG.json b/CHANGELOG.json index 7080b1f10..a3302508d 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -1,5 +1,7 @@ { - "unreleased": [], + "unreleased": [ + "[New] Add UI.getInputFromUser method and deprecate the other input methods" + ], "releases": { "52.1": ["[New] Add basic support for Shape path"], "52": [ diff --git a/Source/ui/UI.js b/Source/ui/UI.js index 9bcdf516e..d981f9f41 100644 --- a/Source/ui/UI.js +++ b/Source/ui/UI.js @@ -1,5 +1,5 @@ /* globals NSAlertFirstButtonReturn */ - +import util from 'util' import { isNativeObject } from '../dom/utils' function getPluginAlertIcon() { @@ -48,6 +48,146 @@ export function alert(title, text) { return dialog.runModal() } +export const INPUT_TYPE = { + string: 'string', + slider: 'slider', + selection: 'selection', + // coming soon + // number: 'number', + // color: 'color', + // path: 'path' +} + +export function getInputFromUser(messageText, options, callback) { + /* eslint-disable no-param-reassign */ + if (!options) { + options = {} + callback = () => {} + } else if (util.isFunction(options)) { + callback = options + options = {} + } + /* eslint-enable */ + + const type = String(options.type || INPUT_TYPE.string) + + if (options.type && !INPUT_TYPE[type]) { + throw new Error('unknown input type') + } + if (!messageText || typeof messageText !== 'string') { + throw new Error('input description missing') + } + + let accessory + switch (type) { + case INPUT_TYPE.string: + accessory = NSTextField.alloc().initWithFrame(NSMakeRect(0, 0, 200, 25)) + accessory.setStringValue( + String( + typeof options.initialValue === 'undefined' + ? '' + : options.initialValue + ) + ) + break + // case INPUT_TYPE.number: + // accessory = NSStepper.alloc().initWithFrame(NSMakeRect(0, 0, 200, 25)) + // accessory.setFloatValue(Number(options.initialValue || 0)) + // if (typeof options.maxValue !== 'undefined') { + // accessory.setMaxValue(options.maxValue) + // } + // if (typeof options.minValue !== 'undefined') { + // accessory.setMinValue(options.minValue) + // } + // if (typeof options.increment !== 'undefined') { + // accessory.setIncrement(options.increment) + // } + // break + case INPUT_TYPE.slider: { + accessory = NSSlider.alloc().initWithFrame(NSMakeRect(0, 0, 200, 25)) + accessory.setFloatValue(Number(options.initialValue || 0)) + if (typeof options.maxValue !== 'undefined') { + accessory.setMaxValue(options.maxValue) + } + if (typeof options.minValue !== 'undefined') { + accessory.setMinValue(options.minValue) + } + if (typeof options.increment !== 'undefined') { + accessory.setAllowsTickMarkValuesOnly(true) + accessory.setNumberOfTickMarks( + parseInt( + 1 + + ((typeof options.maxValue !== 'undefined' + ? options.maxValue + : 1) - + (typeof options.minValue !== 'undefined' + ? options.minValue + : 0)) / + options.increment, + 10 + ) + ) + } + break + } + case INPUT_TYPE.selection: { + if (!util.isArray(options.possibleValues)) { + throw new Error( + 'When the input type is `selection`, you need to provide the array of possible choices.' + ) + } + accessory = NSComboBox.alloc().initWithFrame(NSMakeRect(0, 0, 200, 25)) + accessory.addItemsWithObjectValues(options.possibleValues) + const initialIndex = options.possibleValues.indexOf(options.initialValue) + accessory.selectItemAtIndex(initialIndex !== -1 ? initialIndex : 0) + accessory.editable = false + break + } + default: + break + } + + const dialog = NSAlert.alloc().init() + dialog.setMessageText(messageText) + if (options.description) { + dialog.setInformativeText(options.description) + } + dialog.addButtonWithTitle(options.okButton || 'OK') + dialog.addButtonWithTitle(options.cancelButton || 'Cancel') + dialog.setAccessoryView(accessory) + dialog.icon = getPluginAlertIcon() + + const responseCode = dialog.runModal() + + if (responseCode !== NSAlertFirstButtonReturn) { + callback(new Error('user canceled input')) + return + } + + switch (type) { + case INPUT_TYPE.string: + callback(null, String(accessory.stringValue())) + return + // case INPUT_TYPE.number: + // return Number(accessory.stringValue()) + case INPUT_TYPE.slider: { + const value = + typeof options.increment !== 'undefined' + ? accessory.closestTickMarkValueToValue(accessory.floatValue()) + : accessory.floatValue() + callback(null, Number(value)) + return + } + case INPUT_TYPE.selection: { + const selectedIndex = accessory.indexOfSelectedItem() + callback(null, options.possibleValues[selectedIndex]) + return + } + default: + callback(null, undefined) + } +} + /** * Shows a simple input sheet which displays a message, and asks for a single string * input. @@ -56,14 +196,27 @@ export function alert(title, text) { * @return The string that the user input. */ export function getStringFromUser(msg, initial) { - const panel = MSModalInputSheet.alloc().init() - const result = panel.runPanelWithNibName_ofType_initialString_label_( - 'MSModalInputSheet', - 0, - String(typeof initial === 'undefined' ? '' : initial), - msg + console.warn( + `\`UI.getStringFromUser(message, initialValue)\` is deprecated. +Use \`UI.getInputFromUser( + message, + { initialValue }, + (error, value) => {} +)\` instead.` ) - return String(result) + const accessory = NSTextField.alloc().initWithFrame(NSMakeRect(0, 0, 200, 25)) + accessory.setStringValue( + String(typeof initial === 'undefined' ? '' : initial) + ) + const dialog = NSAlert.alloc().init() + dialog.setMessageText('User input') + dialog.setInformativeText(msg) + dialog.addButtonWithTitle('OK') + dialog.addButtonWithTitle('Cancel') + dialog.setAccessoryView(accessory) + dialog.icon = getPluginAlertIcon() + dialog.runModal() + return String(accessory.stringValue()) } /** @@ -76,6 +229,14 @@ export function getStringFromUser(msg, initial) { * @return An array with three items: [responseCode, selection, ok]. */ export function getSelectionFromUser(msg, items, selectedItemIndex = 0) { + console.warn( + `\`UI.getSelectionFromUser(message, items, selectedItemIndex)\` is deprecated. +Use \`UI.getInputFromUser( + message, + { type: UI.INPUT_TYPE.selection, items, initialValue }, + (error, value) => {} +)\` instead.` + ) const accessory = NSComboBox.alloc().initWithFrame(NSMakeRect(0, 0, 200, 25)) accessory.addItemsWithObjectValues(items) accessory.selectItemAtIndex(selectedItemIndex) diff --git a/docs/api/UI.md b/docs/api/UI.md index 2dfb57949..488b1dd76 100644 --- a/docs/api/UI.md +++ b/docs/api/UI.md @@ -36,47 +36,61 @@ Show an alert with a custom title and message. The alert is modal, so it will st | titlestring - required | The title of the alert. | | textstring - required | The text of the message. | -## Get a string input from the user - -```js -var string = UI.getStringFromUser("What's your name?", 'Appleseed') +## Get an input from the user + +```javascript +UI.getInputFromUser( + "What's your name?", + { + initialValue: 'Appleseed', + }, + (err, value) => { + if (err) { + // most likely the user canceled the input + return + } + } +) ``` -Shows a simple input sheet which displays a message, and asks for a single string input. - -| Parameters | | -| ------------------------------------------------------ | -------------------------------------- | -| messagestring - required | The prompt message to show. | -| initialValuestring | The initial value of the input string. | - -### Returns - -The string that the user input, or "null" (String) if the user clicked 'Cancel'. - -## Make the user select an option +```javascript +UI.getInputFromUser("What's your favorite design tool?", { + type: UI.INPUT_TYPE.selection + possibleValues: ['Sketch'] +}, (err, value) => { + if (err) { + // most likely the user canceled the input + return + } +}) +``` -```js -var options = ['Sketch'] -var selection = UI.getSelectionFromUser( - "What's your favorite design tool?", - options +```javascript +UI.getInputFromUser( + "What's the opacity of the new layer?", + { + type: UI.INPUT_TYPE.slider, + }, + (err, value) => { + if (err) { + // most likely the user canceled the input + return + } + } ) - -var ok = selection[2] -var value = options[selection[1]] -if (ok) { - // do something -} ``` -Shows an input sheet which displays a popup with a series of options, from which the user is asked to choose. - -| Parameters | | -| ------------------------------------------------------ | ------------------------------------------ | -| messagestring - required | The prompt message to show. | -| itemsstring[] - required | An array of option items. | -| selectedIndexnumber | The index of the item to select initially. | - -### Returns - -An array with a response code, the selected index and `ok`. The code will be one of `NSAlertFirstButtonReturn` or `NSAlertSecondButtonReturn`. The selection will be the integer index of the selected item. `ok` is the boolean `code === NSAlertFirstButtonReturn`. +Shows a simple input sheet which displays a message, and asks for an input from the user. + +| Parameters | | +| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| messagestring - required | The prompt message to show. | +| optionsobject | Options to customize the input sheet. Most of the options depends on the type of the input. | +| option.descriptionstring | A secondary text to describe with more details the input. | +| option.type[Input Type](#inputtype) | The type of the input. | +| option.initialValuestring | number | The initial value of the input. | +| option.possibleValuesstring[] - required with a selection | The possible choices that the user can make. Only used for a `selection` input. | +| option.maxValuenumber | The maximal value. Only used for a `slider` input. Defaults to `1`. | +| option.minValuenumber | The maximal value. Only used for a `slider` input. Defaults to `0`. | +| option.incrementnumber | Restricts the possible values to multiple of the increment. Only used for a `slider` input. | +| callbackfunction | A function called after the user entered the input. It is called with an `Error` if the user canceled the input and a `string` or `number` depending on the input type (or `undefined`). |