Plug-In Forms: Menus (Form.Field.Option)
One of the most common user interface elements for Apple systems is the popup menu on macOS, and its iOS counterpart, the list menu. The Form class in Omni Automation provides the Form.Field.Option sub-class so that users can select an option from a menu or menu list.
Instances of the form menu element are created using the new item constructor with provided parameters that include the menu label (title), identifying key, and either an array of objects or strings as the display content for the menu:
Constructor
new Form.Field.Option(key: String, displayName: String or null, options: Array of Object, names: Array of String or null, selected: Object or null, nullOptionTitle: String or null) → (Form.Field.Option) • Returns a new Option field, allowing the user to pick from a list of option objects. A list of names may also be given, which must have the same length as the options array if so. If no names are given, the objects are converted to strings for display. An initially selected object (which must be a member of the options array) may also be given. If the field is not configured to allow a null value and no initially selected value is specified, the user must select a value before the field is considered valid under the default form validation.
Form Menu/List Element
menuElement = new Form.Field.Option(
"elementID", //(String) identifying key used in the form values record
"elementLabel", //(String or null) field label for the form dialog
menuItemOptions, //(Array of Object) an array of objects to display, or index values corresponding to the provided names
menuItemNames, //(Array of String or null) an array of custom menu item titles
selectedItem //(Object or null) the menu item object to be selected when the form dialog is displayed
)
Here is an example of a basic options menu/list, allowing the user to choose a day of the week:
Basic Options Menu/List
(async () => {
try {
weekDays = ["Sunday",
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] basicOptionsMenu = new Form.Field.Option(
"menuID",
null,
[0,1,2,3,4,5,6],
weekDays,
0
)
inputForm = new Form()
inputForm.addField(basicOptionsMenu)
formPrompt = "Choose a day:"
buttonTitle = "Continue"
formObject = await inputForm.show(formPrompt,buttonTitle)
index = formObject.values["menuID"]
weekday = weekDays[index]
console.log(weekday)
}
catch(err){
if(!err.causedByUserCancelling){
new Alert(err.name, err.message).show()
}
}
})();
Instance Properties
The Form.Field.Option class offers two properties for enabling the use of a “null or none” selection by the user.
allowsNull (Boolean) • If set to true, an option will be added to allow selecting null.
nullOptionTitle (String or null) • If null is allowed, this will be used for the title of that option. Otherwise a default title will be used.
NOTE: The Form validate function previously was required to return a value, but it may now return null. If there is no validation function specified or it returns null, some default per-field validation is performed. Currently all fields are considered valid except for option fields that don't allow null but haven't had a value set yet. This can occur if an option field is constructed without an initial selection.
Here is an example of an options menu/list, allowing the user to choose a day of the week or nothing:
Allowing a Null Option
(async () => {
try {
weekDays = ["Sunday",
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] optionsMenu = new Form.Field.Option(
"menuID",
null,
[0,1,2,3,4,5,6],
weekDays,
0
)
optionsMenu.allowsNull = true
optionsMenu.nullOptionTitle = "None"
inputForm = new Form()
inputForm.addField(optionsMenu)
formPrompt = "Choose a day:"
buttonTitle = "Continue"
formObject = await inputForm.show(formPrompt,buttonTitle)
index = formObject.values["menuKey"]
if (index === undefined){
var day = "NONE"
} else {
var day = weekDays[index]
}
console.log(day)
}
catch(err){
if(!err.causedByUserCancelling){
new Alert(err.name, err.message).show()
}
}
})();
In Summary
The Form.Field.Option class constructor returns an instance of the Option Field class, allowing the user to pick from a list of option objects. A list of names may also be given, which must have the same length as the options array. If no names are given, the objects are converted to strings for display or their localized titles are used if available. An initially selected object (which must be a member of the options array) may also be given.
In summary, the source contents of the Option Field (menu) can be either an array of Omni application objects, or an array of menu item titles. Let’s examine each type of menu.
Menu of Objects
The use of objects as the source for the menu’s contents is very useful and easy-to-create. Here is an example using the OmniGraffle HorizontalAlignment class:
Objects for Menu Items
alignmentMenu = new Form.Field.Option(
"horizontalAlignment",
"H-Align",
HorizontalAlignment.all,
null,
HorizontalAlignment.all[0]
)
The result of the form will be the HorizontalAlignment option object corresponding to the menu selection, which can then be used to change the value of the canvas rowAlignment property.
Form Result
//-> [object HorizontalAlignment: Right]
The technique shown works the same with many other Omni application classes as well.
Menu of Strings (names)
The other type of Form.Field.Option uses an array of strings as the source content of the menu. In this example, the user selects a weekday from a list.
Menu of Strings
weekdays = ["Monday",
"Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] weekdayMenu = new Form.Field.Option(
"weekday",
"Weekday",
[0,1,2,3,4,5,6],
weekdays,
0
)
The result of the form will be the object index value corresponding to the chosen menu item:
Form Result
//-> returns: 4
//-> use to get corresponding name: weekdays[4]
Stroke Properties Form
The following example action displays a form whose elements are all menus, and can be used to change the stroke properties of the graphics selected in the frontmost OmniGraffle document. Both menu creation options (objects and string) are used in the construction of the menu form elements.
Stroke Properties Form
/*{
"type": "action",
"targets": ["omnigraffle"],
"author": "Otto Automator",
"identifier": "com.omni-automation.og.stroke-form",
"description": "Applies chosen stroke settings to selected graphics.",
"version": "1.2",
"label": "Stroke Properties",
"shortLabel": "Stroke",
"paletteLabel": "Stroke",
"image": "text.line.first.and.arrowtriangle.forward"
}*/
(() => {
const action = new PlugIn.Action(async function(selection, sender){
// DIALOG TITLE AND OK BUTTON TITLE
formPrompt = "Choose the stroke settings for the selected graphics:"
buttonTitle = "Continue"
// CONSTRUCT THE FORM
form = new Form()
pointSizes = ["0.5","1","2","3","4","5","6","7","8","9","10","11","12"]
thicknessMenu = new Form.Field.Option(
"strokeThickness",
"Thickness",
[0,1,2,3,4,5,6,7,8,9,10,11,12],
pointSizes,
1
)
firstJoinType = LineJoin.all[0] // LineJoin['Bevel']
joinMenu = new Form.Field.Option(
"strokeJoin",
"Join Type",
LineJoin.all,
null,
firstJoinType
)
firstStrokeType = StrokeType.all[0] // StrokeType['Single']
typeMenu = new Form.Field.Option(
"strokeType",
"Stroke Type",
StrokeType.all,
null,
firstStrokeType
)
firstStrokePattern = StrokeDash.all[0] // StrokeDash['Dash4on4off']
patternMenu = new Form.Field.Option(
"strokePattern",
"Stroke Pattern",
StrokeDash.all,
null,
firstStrokePattern
)
form.addField(thicknessMenu)
form.addField(typeMenu)
form.addField(patternMenu)
form.addField(joinMenu)
// VALIDATE FORM CONTENT
form.validate = function(formObject){
return true
}
// SHOWING THE FORM RETURNS A JAVASCRIPT PROMISE
formObject = await form.show(formPrompt, buttonTitle)
// RETRIEVE CHOSEN VAUES
strokeThickness = formObject.values['strokeThickness']
strokeThickness = Number(pointSizes[strokeThickness])
strokeTypeValue = formObject.values['strokeType']
strokePatternValue = formObject.values['strokePattern']
strokeJoinValue = formObject.values['strokeJoin']
// PERFORM TASKS
selection.graphics.forEach(function(graphic){
graphic.strokeThickness = strokeThickness
graphic.strokeType = strokeTypeValue
graphic.strokePattern = strokePatternValue
graphic.strokeJoin = strokeJoinValue
})
});
// VALIDATE GRAPHIC(S) SELECTION
action.validate = function(selection, sender){
return (selection.graphics.length > 0)
};
return action;
})();
Interactive Form
The next action example is a variation of the previous action. But instead of applying the chosen stroke settings after the form has been approved, the settings are applied to the selected graphic as the settings are changed by the user in the form dialog. Should the user decide to cancel the form, the stroke settings are returned to the values that existed before the form was displayed.
|
Stroke Form (interactive)
/*{
"type": "action",
"targets": ["omnigraffle"],
"author": "Otto Automator",
"identifier": "com.omni-automation.og.stroke-form-live",
"version": "1.2",
"description": "Applies chosen stroke settings to selected graphics.",
"label": "Stroke Properties (live)",
"shortLabel": "Form",
"paletteLabel": "Form",
"image": "text.line.first.and.arrowtriangle.forward"
}*/
(() => {
const action = new PlugIn.Action(async function(selection, sender){
selectedGraphic = selection.graphics[0]
storedStrokeThickness = selectedGraphic.strokeThickness
storedStrokeType = selectedGraphic.strokeType
storedStrokePattern = selectedGraphic.strokePattern
storedStrokeJoin = selectedGraphic.strokeJoin
// DIALOG TITLE AND OK BUTTON TITLE
formPrompt = "Choose the stroke settings for the selected graphics:"
buttonTitle = "Continue"
// CONSTRUCT THE FORM
form = new Form()
initialValue = parseInt(storedStrokeThickness)
initialIndex = (initialValue > 18) ? 18:initialValue
pointSizes = ["0.5","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18"]
thicknessMenu = new Form.Field.Option(
"strokeThickness",
"Thickness",
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18],
pointSizes,
initialIndex
)
joinMenu = new Form.Field.Option(
"strokeJoin",
"Join Type",
LineJoin.all,
null,
storedStrokeJoin
)
typeMenu = new Form.Field.Option(
"strokeType",
"Stroke Type",
StrokeType.all,
null,
storedStrokeType
)
patternMenu = new Form.Field.Option(
"strokePattern",
"Stroke Pattern",
StrokeDash.all,
null,
storedStrokePattern
)
form.addField(thicknessMenu)
form.addField(typeMenu)
form.addField(patternMenu)
form.addField(joinMenu)
// VALIDATE FORM CONTENT
form.validate = function(formObject){
// RETRIEVE CHOSEN VAUES
strokeThickness = formObject.values['strokeThickness']
strokeThickness = Number(pointSizes[strokeThickness])
strokeTypeValue = formObject.values['strokeType']
strokePatternValue = formObject.values['strokePattern']
strokeJoinValue = formObject.values['strokeJoin']
// APPLY SETTINGS
selectedGraphic.strokeThickness = strokeThickness
selectedGraphic.strokeType = strokeTypeValue
selectedGraphic.strokePattern = strokePatternValue
selectedGraphic.strokeJoin = strokeJoinValue
return true
}
// SHOWING THE FORM RETURNS A JAVASCRIPT PROMISE
formPromise = form.show(formPrompt, buttonTitle)
// PROCESS THE FORM RESULTS
formPromise.then(function(formObject){
// NOTHING TO DO. SETTINGS ALREADY APPLIED!
})
// PROCESS FORM CANCELLATION
formPromise.catch(function(error){
// RESTORE GRAPHIC’S ORGINIAL SETTINGS
selectedGraphic.strokeThickness = storedStrokeThickness
selectedGraphic.strokeType = storedStrokeType
selectedGraphic.strokePattern = storedStrokePattern
selectedGraphic.strokeJoin = storedStrokeJoin
})
});
// VALIDATE GRAPHIC SELECTION
action.validate = function(selection, sender){
return (selection.graphics.length === 1 ? true:false)
};
return action;
})();
Follow-On Menus
A Follow-On Menu is one whose appearance and/or contents change based-upon the selection of another menu in the form dialog. Follow-On menus are described in detail in the section about Form Validation.
Filtered Menu
In this form, the contents of the menu is determined by filtering using the entered text. Designed for use in OmniFocus, the form filters existing projects using the entered string, and presents the results in a menu:
Filtered Menu Form (OmniFocus)
(async () => {
try {
// CREATE FORM FOR GATHERING USER INPUT
inputForm = new Form()
// CREATE TEXT FIELD
textField = new Form.Field.String(
"textInput",
"Search",
null
)
// CREATE MENU
popupMenu = new Form.Field.Option(
"menuItem",
"Results",
[],
[],
null
)
popupMenu.allowsNull = true
popupMenu.nullOptionTitle = "0 items"
// ADD THE FIELDS TO THE FORM
inputForm.addField(textField)
inputForm.addField(popupMenu)
// VALIDATE THE USER INPUT
inputForm.validate = function(formObject){
var textInput = formObject.values["textInput"]
if(textInput !== currentValue){
currentValue = textInput
// remove popup menu
if (inputForm.fields.length === 2){
inputForm.removeField(inputForm.fields[1])
}
}
if(inputForm.fields.length === 1){
// search using provided string
if (!textInput){var searchResults = []} else {
var searchResults = projectsMatching(textInput)
}
var searchResultNames = searchResults.map(item => item.name)
var searchResultIDs = searchResults.map(item => item.id.primaryKey)
var popupMenu = new Form.Field.Option(
"menuItem",
"Results",
searchResultIDs,
searchResultNames,
null
)
popupMenu.allowsNull = true
popupMenu.nullOptionTitle = String(searchResults.length + " items")
inputForm.addField(popupMenu)
return false
}
if(!textInput){return false}
if(inputForm.fields.length === 2){
menuValue = formObject.values["menuItem"]
if(menuValue === undefined || String(menuValue) === "null"){return false}
return true
}
}
// PRESENT THE FORM TO THE USER
currentValue = null
formPrompt = "Enter a project title:"
formObject = await inputForm.show(formPrompt,"Continue")
// PROCESSING USING THE DATA EXTRACTED FROM THE FORM
projectID = formObject.values["menuItem"]
projectObj = Project.byIdentifier(projectID)
console.log(projectObj)
}
catch(err){
if(!err.causedByUserCancelling){
new Alert(err.name, err.message).show()
}
}
})();