Menus

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.

Menus (Form.Field.Option)

Instances of the form menu element are created using the new 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:

new Form.Field.Option( key:String, displayName:String or null, options:Array of Object, names:Array of String or null, selected:Object or null )

This 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.

IMPORTANT: Recent Changes to the Form.Field.Option Class

Form.Field.Option now defaults to not allowing null values, but has two additional properties allowsNull and nullOptionTitle. These are not included in the constructor arguments, but can be set after creating the field. If allowsNull is true, then an option will be included to pick null (no value). If a nullOptionTitle is specified that will be used as the title, otherwise a default "No Value" string will be used.

The Form validate function previously was required to return a boolean 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.

Menu of Objects

The use of objects as the source for the menu’s contents is very useful and easy-to-create.

horizontal-alignment-menu

 1  The new constructor is called on the Form.Field.Option class and the created form element is stored in a variable, in this example alignmentMenu. This variable can be used in the parent action as the parameter for the addField() method of the Form class to add the menu to a form.

 2  The menu is assigned a unique identifying key (string) that is used to extract the current displayed menu item object when the form is validated or processed.

 3  The text to use as the label for the menu.

 4  An array of Omni application objects. In this example, calling the all property of the HorizontalAlignment class returns an array of HorizontalAlignment options representing left, center, and right alignments. This array of objects will be used to populate the menu.

 5  Since a null value is used instead of an array of menu titles, the localized built-in titles for the objects will be displayed as the contents of the menu.

 6  Indicate which menu object is to be pre-selected when the form is displayed, by providing one of the objects from the source list of objects as the value of this parameter.

 X  The result of the creation of the menu element is an object reference to the form element.

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.

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.

weekday-names-menu

 1  An array of strings, in this case the names of the days of the week, are stored in a variable: weekdays

 2  The new constructor is called on the Form.Field.Option class and the created form element is stored in a variable, in this example weekdayMenu. This variable can be used in the parent action as the parameter for the addField() method of the Form class to add the menu to a form.

 3  The menu is assigned a unique identifying key (string) that is used to extract the current displayed menu item object when the form is validated or processed.

 4  The text to use as the label for the menu.

 5  Since the content of the menu will be a list of weekday names, the required object list is used instead as an array of index values, indicating the chosen weekday’s position in the array of weekday names. Since JavaScript considers the first item in an array to have a position (index) of 0, begin the sequential array of indexes with 0.

 6  The content of the menu (menu items) is the weekday list, represented by the variable: weekdays

 7  Indicate which menu object is to be pre-selected when the form is displayed, by providing one of the indexes from the objects array as the value of this parameter. In this example, the index 0 is used to indicate that the first menu item should be pre-selected when the form is displayed.

 9  The result of the creation of the menu element is an object reference to the form element.

The result of the form will be the object index value corresponding to the chosen menu item:

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 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.1", "label": "Stroke Properties", "shortLabel": "Stroke" }*/ (() => { var action = new PlugIn.Action(function(selection, sender){ // DIALOG TITLE AND OK BUTTON TITLE let formPrompt = "Choose the stroke settings for the selected graphics:" let buttonTitle = "Continue" // CONSTRUCT THE FORM var form = new Form() let pointSizes = ["0.5","1","2","3","4","5","6","7","8","9","10","11","12"] let thicknessMenu = new Form.Field.Option( "strokeThickness", "Thickness", [0,1,2,3,4,5,6,7,8,9,10,11,12], pointSizes, 1 ) let firstJoinType = LineJoin.all[0] // LineJoin['Bevel'] let joinMenu = new Form.Field.Option( "strokeJoin", "Join Type", LineJoin.all, null, firstJoinType ) let firstStrokeType = StrokeType.all[0] // StrokeType['Single'] let typeMenu = new Form.Field.Option( "strokeType", "Stroke Type", StrokeType.all, null, firstStrokeType ) let firstStrokePattern = StrokeDash.all[0] // StrokeDash['Dash4on4off'] let 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 formPromise = form.show(formPrompt, buttonTitle) // PROCESS THE FORM RESULTS formPromise.then(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'] // PERFORM TASKS selection.graphics.forEach(function(graphic){ graphic.strokeThickness = strokeThickness graphic.strokeType = strokeTypeValue graphic.strokePattern = strokePatternValue graphic.strokeJoin = strokeJoinValue }) }) // PROCESS FORM CANCELLATION formPromise.catch(function(error){ console.log("form cancelled", error); }) }); // VALIDATE GRAPHIC(S) SELECTION action.validate = function(selection, sender){ return (selection.graphics.length > 0) }; return action; })();

 20-27  A new menu form element is created using a list of strings (numeric strings representing stroke thicknesses) as the source of the menu’s content.

 29-36  A new form element is created using an array of object options from the LineJoin class. The built-in localized option titles for this class will be displayed as the menu’s contents.

 38-45  A new form element is created using an array of object options from the Stroketype class. The built-in localized option titles for this class will be displayed as the menu’s contents.

 47-54  A new form element is created using an array of object options from the StrokeDash class. The built-in localized option titles for this class will be displayed as the menu’s contents.

 56-59  The created form elements are added to the blank form object using the addField(…) method of the Form class.

 62-64  The form validation function is used to determine the state of the form’s approval button. In this example, the validation value is set to always return true, leaving the button always active.

 67  The form is displayed using the show(…) method of Form class.

 70-84  The form settings are extracted from the form values record returned from the stored promise object, and the extracted stroke settings are applied to the selected graphics. NOTE: the value returned by the strokeThickness key is the array index of the chosen menu item. The next statement (73) uses that index to retrieve the stored numeric string value, and then convert it to a number.

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.

/*{ "type": "action", "targets": ["omnigraffle"], "author": "Otto Automator", "identifier": "com.omni-automation.og.stroke-form-live", "description": "Applies chosen stroke settings to selected graphics.", "version": "1.1", "label": "Stroke Properties (live)", "shortLabel": "Form" }*/ (() => { var action = new PlugIn.Action(function(selection, sender){ var selectedGraphic = selection.graphics[0] let storedStrokeThickness = selectedGraphic.strokeThickness let storedStrokeType = selectedGraphic.strokeType let storedStrokePattern = selectedGraphic.strokePattern let storedStrokeJoin = selectedGraphic.strokeJoin // DIALOG TITLE AND OK BUTTON TITLE let formPrompt = "Choose the stroke settings for the selected graphics:" let buttonTitle = "Continue" // CONSTRUCT THE FORM var form = new Form() initialValue = parseInt(storedStrokeThickness) initialIndex = (initialValue > 18) ? 18:initialValue let pointSizes = ["0.5","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18"] let 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 ) let joinMenu = new Form.Field.Option( "strokeJoin", "Join Type", LineJoin.all, null, storedStrokeJoin ) let typeMenu = new Form.Field.Option( "strokeType", "Stroke Type", StrokeType.all, null, storedStrokeType ) let 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; })();

 105-107  Due to the complexity of dealing with multiple graphics simultaneously, the action validation function is set to only be enabled if a single graphic is selected in the document.

 13  A reference to the selected graphic is stored in the variable: selectedGraphic

 15-18  The stroke properties of the selected graphic are stored in variables prior to the form being displayed to the user.

 68-83  The form settings are extracted and applied to the selected graphic from within the form’s validate function rather than from within the form promise’s then(…) function. Every time the settings of any of the form elements are changed, the validation handler will be called and the selected graphics property values will be altered accordingly.

 94-100  Should the user cancel the form dialog, the promise’s error handler (catch) is called and the stroke settings of the selected graphic are restored to the settings stored in the variables declared at the beginning of the 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 on this page.

UNDER CONSTRUCTION

This webpage is in the process of being developed. Any content may change and may not be accurate or complete at this time.

DISCLAIMER