New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code Review: Polish for MSModalInputSheet (#20833) #276

Merged
merged 2 commits into from Nov 9, 2018
File filter...
Filter file types
Jump to file or symbol
Failed to load files and symbols.
+226 −48
Diff settings

Always

Just for now

Copy path View file
@@ -35,7 +35,8 @@ globals:
AppController: false
MSImageData: false
NSImage: false
MSModalInputSheet: false
NSTextField: false
NSSlider: false
MSStyleFill: false
MSStyleBorder: false
MSColor: false
Copy path View file
@@ -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": [
Copy path View file
@@ -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) {

This comment has been minimized.

@mathieudutour

mathieudutour Nov 5, 2018

Contributor

I chose to merge the different input methods into a single one so to reduce the API surface and make sure we have the same signature for all of them (the selection input method was super weird).

The result is now provided in a callback so that we can support making that modal a sheet in the future (needs to be async) and so that the fact the user cancels is very clear (error)

/* 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)
Copy path View file
@@ -36,47 +36,61 @@ Show an alert with a custom title and message. The alert is modal, so it will st
| title<span class="arg-type">string - required</span> | The title of the alert. |
| text<span class="arg-type">string - required</span> | 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 | |
| ------------------------------------------------------ | -------------------------------------- |
| message<span class="arg-type">string - required</span> | The prompt message to show. |
| initialValue<span class="arg-type">string</span> | 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 | |
| ------------------------------------------------------ | ------------------------------------------ |
| message<span class="arg-type">string - required</span> | The prompt message to show. |
| items<span class="arg-type">string[] - required</span> | An array of option items. |
| selectedIndex<span class="arg-type">number</span> | 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 | |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| message<span class="arg-type">string - required</span> | The prompt message to show. |
| options<span class="arg-type">object</span> | Options to customize the input sheet. Most of the options depends on the type of the input. |
| option.description<span class="arg-type">string</span> | A secondary text to describe with more details the input. |
| option.type<span class="arg-type">[Input Type](#inputtype)</span> | The type of the input. |
| option.initialValue<span class="arg-type">string | number</span> | The initial value of the input. |
| option.possibleValues<span class="arg-type">string[] - required with a selection</span> | The possible choices that the user can make. Only used for a `selection` input. |
| option.maxValue<span class="arg-type">number</span> | The maximal value. Only used for a `slider` input. Defaults to `1`. |
| option.minValue<span class="arg-type">number</span> | The maximal value. Only used for a `slider` input. Defaults to `0`. |
| option.increment<span class="arg-type">number</span> | Restricts the possible values to multiple of the increment. Only used for a `slider` input. |
| callback<span class="arg-type">function</span> | 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`). |
ProTip! Use n and p to navigate between commits in a pull request.