Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ globals:
AppController: false
MSImageData: false
NSImage: false
MSModalInputSheet: false
NSTextField: false
NSSlider: false
MSStyleFill: false
MSStyleBorder: false
MSColor: false
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
177 changes: 169 additions & 8 deletions Source/ui/UI.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* globals NSAlertFirstButtonReturn */

import util from 'util'
import { isNativeObject } from '../dom/utils'

function getPluginAlertIcon() {
Expand Down Expand Up @@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Expand All @@ -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())
}

/**
Expand All @@ -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)
Expand Down
90 changes: 52 additions & 38 deletions docs/api/UI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`). |