ScriptUI windows can contain several controls (checkboxes, sliders, etc), and to setup a Preset system is a very handy way to allow users save and retrieve their own preferred configurations easily. In this tutorial I’ll show you how to implement Presets with a DropDownList menu based upon XML data.
Starting point
I’ve set up a simple demo dialog using a resource string – you can use it either in InDesign or Photoshop for instance: this is the base which I’ll be using throughout the post adding functional bits of code. (I’ve used CoffeeScript, a nice language that compiles to JavaScript, to write the whole script; in this tutorial I’m going to show the JS only, but you can find the entire CoffeScript code at the bottom of the page if you’re curious).
#target photoshop; var win, windowResource; windowResource = "dialog { \ orientation: 'column', \ alignChildren: ['fill', 'top'], \ size:[410, 210], \ text: 'XML DropDownList Presets demo - www.davidebarranca.com', \ margins:15, \ \ controlsPanel: Panel { \ orientation: 'column', \ alignChildren:'right', \ margins:15, \ text: 'Controls', \ controlsGroup: Group { \ orientation: 'row', \ alignChildren:'center', \ st: StaticText { text: 'Amount:' }, \ mySlider: Slider { minvalue:0, maxvalue:500, value:300, size:[220,20] }, \ myText: EditText { text:'300', characters:5, justify:'left'} \ } \ }, \ presetsPanel: Panel { \ orientation: 'row', \ alignChildren: 'center', \ text: 'Presets', \ margins: 14, \ presetList: DropDownList {preferredSize: [163,20] }, \ saveNewPreset: Button { text: 'New', preferredSize: [44,24]}, \ deletePreset: Button { text: 'Remove', preferredSize: [60,24]}, \ resetPresets: Button { text: 'Reset', preferredSize: [50,24]} \ }, \ buttonsGroup: Group{\ alignChildren: 'bottom',\ cancelButton: Button { text: 'Cancel', properties:{name:'cancel'},size: [60,24], alignment:['right', 'center'] }, \ applyButton: Button { text: 'Apply', properties:{name:'apply'}, size: [100,24], alignment:['right', 'center'] }, \ }\ }"; win = new Window(windowResource); // binds mySlider.value with myText.text win.controlsPanel.controlsGroup.myText.onChange = function() { this.parent.mySlider.value = Number(this.text); }; win.controlsPanel.controlsGroup.mySlider.onChange = function() { this.parent.myText.text = Math.ceil(this.value); }; win.show();
This scheme lets you:
- Save new custom presets.
- Delete custom presets.
- Reset to the default group of presets (which are protected and cannot be deleted).
The presets are stored as XML data in a file (in the example there’s a single value only, but they can be as many as you need)
<presets> <preset default="true"> <name>select...</name> <value/> </preset> <preset default="true"> <name>Default value</name> <value>100</value> </preset> <preset default="true"> <name>Low value</name> <value>10</value> </preset> <preset default="true"> <name>High value</name> <value>400</value> </preset> </presets>
The default="true"
attribute marks this presets as read-only (i.e. the user can’t delete them)
Logic
This is how I’ve decomposed the problem (click to open a bigger version)
Building the Presets system
Basically the core is an initialization routine that reads XML data from an XML file (or creates a default one if it’s missing) and fills the DropDownList (DDL from now on) with labels, so let’s start with it.
1. Initialization routine
I’ve set few globals that will be useful for other functions too – the code is commented so it should be quite self-explanatory.
// the XML object holding the XML data var xmlData = null; // the array holding the preset labels, used later on // to check for label duplicates when adding a new preset var presetNamesArray = []; // the preset file (which lives alongside this script) resPath = File($.fileName).parent; var presetFile = new File("" + resPath + "/presets.xml"); // Initialization routine var initDDL = function() { var i, nameListLength; if (!presetFile.exists) { createDefaultXML(); // recursive call, needed to read and fill the DDL initDDL(); } // retrieves XML Data xmlData = readXML(); // empties the DDL before adding new content if (win.presetsPanel.presetList.items.length !== 0) { win.presetsPanel.presetList.removeAll(); } // the number of preset labels nameListLength = xmlData.preset.name.length(); presetNamesArray.length = 0; // for each preset XML element, retrieve the <name> label // and write it in the Labels array, then add it to the DDL i = 0; while (i < nameListLength) { presetNamesArray.push(xmlData.preset.name[i].toString()); win.presetsPanel.presetList.add("item", xmlData.preset.name[i]); i++; } // set as the default the first preset // which is the placeholder "select..." win.presetsPanel.presetList.selection = win.presetsPanel.presetList.items[0]; return true; };
As you’ve seen I’ve mentioned some utility functions likeĀ readXML()
Ā andĀ writeXML()
, alongsideĀ createDefaultXML()
, here they are:
// holds the XML object containing the // Default set of Presets var defaultXML = <presets> <preset default="true"> <name>select...</name> <value></value> </preset> <preset default="true"> <name>Default value</name> <value>100</value> </preset> <preset default="true"> <name>Low value</name> <value>10</value> </preset> <preset default="true"> <name>High value</name> <value>400</value> </preset> </presets>; // writes an XML object to file writeXML = function(xml, file) { if (file == null) { // global, the preset.xml file already declared file = presetFile; } try { file.open("w"); file.write(xml); file.close(); } catch (e) { alert("" + e.message + "\nThere are problems writing the XML file!"); } return true; }; // reads an XML object from a file readXML = function(file) { var content; if (file == null) { // global, the preset.xml file already declared file = presetFile; } try { file.open('r'); content = file.read(); file.close(); return new XML(content); } catch (e) { alert("" + e.message + "\nThere are problems reading the XML file!"); } return true; }; // creates a preset.xml file // using the default set of presets createDefaultXML = function() { // global, the preset.xml file already declared if (!presetFile.exists) { writeXML(defaultXML); return true; } else { presetFile.remove(); // recursive call createDefaultXML(); } return true; };
So far the script detects whether a preset.xml
file exists: if it doesn’t, it creates a default one – otherwise it reads it and use it to fill the DDL.
2. Save a new preset
I’ve setup the following onClick()
function which makes use of createPresetChild()
, another utility that grabs values from the GUI and creates a node from them. Which node, in turn, will be appended to the existing XML data.
// used to workaround the lack of indexOf in ExtendScript var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; // creates a XML <preset> node var createPresetChild = function(name, value) { var child; // mind you, each custom node has the default attribute // set to "false", meaning that it can be deleted return child = <preset default="false"> <name>{name}</name> <value>{value}</value> </preset>; }; // onClick handler for the Save New Preset button win.presetsPanel.saveNewPreset.onClick = function() { var child, presetName; // ask for a preset name presetName = prompt("Give your preset a name!\nYou'll find it in the preset list.", "User Preset", "Save new Preset"); if (presetName == null) { return; } // in case the name already exists if (__indexOf.call(presetNamesArray, presetName) >= 0) { alert("Duplicate name!\nPlease find another one."); // make a recursive call to this onClick function win.presetsPanel.saveNewPreset.onClick.call(); } // creates a <preset> node with values grabbed // from the window controls child = createPresetChild(presetName, win.controlsPanel.controlsGroup.myText.text); // appends the XML node to the existing XML data (global variable) xmlData.appendChild(child); // writes the XML object to the preset.xml file writeXML(xmlData); // you need to initialize again, in order to populate // the DDL with new values initDDL(); // selects the last preset in the DDL win.presetsPanel.presetList.selection = win.presetsPanel.presetList.items[win.presetsPanel.presetList.items.length - 1]; };
Please notice the lack of indexOf
in ExtendScript (the used __indexOf
is courtesy of CoffeeScript)
Ā 3. Delete preset
That’s pretty straightforward, you just need to check whether the default
attribute of the preset is false (which means that the preset is not protected and can be deleted). Again, each preset’s modification needs to write the changes to disk in the presets.xml
file and re-initialize the DDL in order to populate the menu properly.
// onClick handler for the Delete preset button win.presetsPanel.deletePreset.onClick = function() { // mind you: typeof xmlData.preset[].@default = XML if (xmlData.preset[win.presetsPanel.presetList.selection.index].@default.toString() === "true") { alert("Can't delete \"" + xmlData.preset[win.presetsPanel.presetList.selection.index].name + "\"\nIt's part of the default set of Presets"); return; } // ask for confirmation if (confirm("Are you sure you want to delete \"" + xmlData.preset[win.presetsPanel.presetList.selection.index].name + "\" preset?\nYou can't undo this.")) { // delete the <preset> element delete xmlData.preset[win.presetsPanel.presetList.selection.index]; } // as usual, write to disk and initialize again writeXML(xmlData); return initDDL(); };
Ā 4. Reset presets
Another easy task, just replace presets.xml
with a default one (which is hardcoded inside the script).
// onClick handler for the Reset presets button win.presetsPanel.resetPresets.onClick = function() { if (confirm("Warning\nAre you sure you want to reset the Preset list?", true)) { createDefaultXML(); initDDL(); } };
Ā 5. Apply preset
The last thing we’ve to add is a handler for the DDL onChange
event – that is, the user selects the preset and window controls must be updated with values coming from it.
// DDL onChange handler win.presetsPanel.presetList.onChange = function() { if (this.selection !== null && this.selection.index !== 0) { // sets GUI controls win.controlsPanel.controlsGroup.myText.text = xmlData.preset[this.selection.index].value; win.controlsPanel.controlsGroup.mySlider.value = Number(xmlData.preset[this.selection.index].value); } };
Completed script
Let’s put everything together! As follows both versions (JS and CoffeeScript). Speaking of the latter, XML management in ExtendScript made me use more than I wanted theĀ backticks (which embed regular JS code), but that’s fine – I’m a big fan of CoffeeScript anyway š
Javascript
#target photoshop; var createDefaultXML, createPresetChild, defaultXML, initDDL, presetFile, presetNamesArray, readXML, resPath, win, windowResource, writeXML, xmlData, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; windowResource = "dialog { \ orientation: 'column', \ alignChildren: ['fill', 'top'], \ size:[410, 210], \ text: 'DropDownList Demo - www.davidebarranca.com', \ margins:15, \ \ controlsPanel: Panel { \ orientation: 'column', \ alignChildren:'right', \ margins:15, \ text: 'Controls', \ controlsGroup: Group { \ orientation: 'row', \ alignChildren:'center', \ st: StaticText { text: 'Amount:' }, \ mySlider: Slider { minvalue:0, maxvalue:500, value:300, size:[220,20] }, \ myText: EditText { text:'300', characters:5, justify:'left'} \ } \ }, \ presetsPanel: Panel { \ orientation: 'row', \ alignChildren: 'center', \ text: 'Presets', \ margins: 14, \ presetList: DropDownList {preferredSize: [163,20] }, \ saveNewPreset: Button { text: 'New', preferredSize: [44,24]}, \ deletePreset: Button { text: 'Remove', preferredSize: [60,24]}, \ resetPresets: Button { text: 'Reset', preferredSize: [50,24]} \ }, \ buttonsGroup: Group{\ alignChildren: 'bottom',\ cancelButton: Button { text: 'Cancel', properties:{name:'cancel'},size: [60,24], alignment:['right', 'center'] }, \ applyButton: Button { text: 'Apply', properties:{name:'apply'}, size: [100,24], alignment:['right', 'center'] }, \ }\ }"; win = new Window(windowResource); xmlData = null; presetNamesArray = []; resPath = File($.fileName).parent; presetFile = new File("" + resPath + "/presets.xml"); defaultXML = <presets> <preset default="true"> <name>select...</name> <value></value> </preset> <preset default="true"> <name>Default value</name> <value>100</value> </preset> <preset default="true"> <name>Low value</name> <value>10</value> </preset> <preset default="true"> <name>High value</name> <value>400</value> </preset> </presets>; writeXML = function(xml, file) { if (file == null) { file = presetFile; } try { file.open("w"); file.write(xml); file.close(); } catch (e) { alert("" + e.message + "\nThere are problems writing the XML file!"); } return true; }; readXML = function(file) { var content; if (file == null) { file = presetFile; } try { file.open('r'); content = file.read(); file.close(); return new XML(content); } catch (e) { alert("" + e.message + "\nThere are problems reading the XML file!"); } return true; }; createDefaultXML = function() { if (!presetFile.exists) { writeXML(defaultXML); void 0; } else { presetFile.remove(); createDefaultXML(); } return true; }; createPresetChild = function(name, value) { var child; return child = <preset default="false"> <name>{name}</name> <value>{value}</value> </preset>; }; initDDL = function() { var i, nameListLength; if (!presetFile.exists) { createDefaultXML(); initDDL(); } xmlData = readXML(); if (win.presetsPanel.presetList.items.length !== 0) { win.presetsPanel.presetList.removeAll(); } nameListLength = xmlData.preset.name.length(); presetNamesArray.length = 0; i = 0; while (i < nameListLength) { presetNamesArray.push(xmlData.preset.name[i].toString()); win.presetsPanel.presetList.add("item", xmlData.preset.name[i]); i++; } win.presetsPanel.presetList.selection = win.presetsPanel.presetList.items[0]; return true; }; win.controlsPanel.controlsGroup.myText.onChange = function() { return this.parent.mySlider.value = Number(this.text); }; win.controlsPanel.controlsGroup.mySlider.onChange = function() { return this.parent.myText.text = Math.ceil(this.value); }; win.presetsPanel.presetList.onChange = function() { if (this.selection !== null && this.selection.index !== 0) { win.controlsPanel.controlsGroup.myText.text = xmlData.preset[this.selection.index].value; win.controlsPanel.controlsGroup.mySlider.value = Number(xmlData.preset[this.selection.index].value); } return true; }; win.presetsPanel.resetPresets.onClick = function() { if (confirm("Warning\nAre you sure you want to reset the Preset list?", true)) { createDefaultXML(); return initDDL(); } }; win.presetsPanel.saveNewPreset.onClick = function() { var child, presetName; presetName = prompt("Give your preset a name!\nYou'll find it in the preset list.", "User Preset", "Save new Preset"); if (presetName == null) { return; } if (__indexOf.call(presetNamesArray, presetName) >= 0) { alert("Duplicate name!\nPlease find another one."); win.presetsPanel.saveNewPreset.onClick.call(); } child = createPresetChild(presetName, win.controlsPanel.controlsGroup.myText.text); xmlData.appendChild(child); writeXML(xmlData); initDDL(); return win.presetsPanel.presetList.selection = win.presetsPanel.presetList.items[win.presetsPanel.presetList.items.length - 1]; }; win.presetsPanel.deletePreset.onClick = function() { if (xmlData.preset[win.presetsPanel.presetList.selection.index].@default.toString() === "true") { alert("Can't delete \"" + xmlData.preset[win.presetsPanel.presetList.selection.index].name + "\"\nIt's part of the default set of Presets"); return; } if (confirm("Are you sure you want to delete \"" + xmlData.preset[win.presetsPanel.presetList.selection.index].name + "\" preset?\nYou can't undo this.")) { delete xmlData.preset[win.presetsPanel.presetList.selection.index]; } writeXML(xmlData); return initDDL(); }; initDDL(); win.show();
Ā CoffeeScript
`#target photoshop` windowResource = "dialog { \ orientation: 'column', \ alignChildren: ['fill', 'top'], \ size:[410, 210], \ text: 'DropDownList Demo - www.davidebarranca.com', \ margins:15, \ \ controlsPanel: Panel { \ orientation: 'column', \ alignChildren:'right', \ margins:15, \ text: 'Controls', \ controlsGroup: Group { \ orientation: 'row', \ alignChildren:'center', \ st: StaticText { text: 'Amount:' }, \ mySlider: Slider { minvalue:0, maxvalue:500, value:300, size:[220,20] }, \ myText: EditText { text:'300', characters:5, justify:'left'} \ } \ }, \ presetsPanel: Panel { \ orientation: 'row', \ alignChildren: 'center', \ text: 'Presets', \ margins: 14, \ presetList: DropDownList {preferredSize: [163,20] }, \ saveNewPreset: Button { text: 'New', preferredSize: [44,24]}, \ deletePreset: Button { text: 'Remove', preferredSize: [60,24]}, \ resetPresets: Button { text: 'Reset', preferredSize: [50,24]} \ }, \ buttonsGroup: Group{\ alignChildren: 'bottom',\ cancelButton: Button { text: 'Cancel', properties:{name:'cancel'},size: [60,24], alignment:['right', 'center'] }, \ applyButton: Button { text: 'Apply', properties:{name:'apply'}, size: [100,24], alignment:['right', 'center'] }, \ }\ }"; # Create Dialog win = new Window windowResource # globals xmlData = null presetNamesArray = [] resPath = File($.fileName).parent presetFile = new File "#{resPath}/presets.xml" defaultXML = `<presets> <preset default="true"> <name>select...</name> <value></value> </preset> <preset default="true"> <name>Default value</name> <value>100</value> </preset> <preset default="true"> <name>Low value</name> <value>10</value> </preset> <preset default="true"> <name>High value</name> <value>400</value> </preset> </presets>` # writes an XML object to a file writeXML = (xml, file=presetFile) -> try file.open "w" file.write xml file.close() catch e alert "#{e.message}\nThere are problems writing the XML file!" true # reads a file (default=presets.xml) and returns an XML object readXML = (file=presetFile) -> try file.open 'r' content = file.read() file.close() return new XML content catch e alert "#{e.message}\nThere are problems reading the XML file!" true # creates a Default XML file createDefaultXML = () -> # if file doesn't exist if !presetFile.exists writeXML defaultXML undefined else presetFile.remove() createDefaultXML() true # creates and returns an XML <preset> element createPresetChild = (name, value) -> child = `<preset default="false"> <name>{name}</name> <value>{value}</value> </preset>` # Initialize DropDownList initDDL = () -> # if file doesn't exist if !presetFile.exists createDefaultXML() initDDL() # file exists xmlData = readXML() # clean DropDownList win.presetsPanel.presetList.removeAll() if win.presetsPanel.presetList.items.length isnt 0 # how many presets? nameListLength = xmlData.preset.name.length() # empties the names array presetNamesArray.length = 0 # fills the DDL and the presetNamesArray i = 0 while i < nameListLength # toString() otherwise its an array of XML objects! presetNamesArray.push xmlData.preset.name[i].toString() win.presetsPanel.presetList.add "item", xmlData.preset.name[i] i++ win.presetsPanel.presetList.selection = win.presetsPanel.presetList.items[0] true # binds mySlider.value and myText.text win.controlsPanel.controlsGroup.myText.onChange = () -> @parent.mySlider.value = Number @text # binds mySlider.value and myText.text win.controlsPanel.controlsGroup.mySlider.onChange = () -> @parent.myText.text = Math.ceil @value win.presetsPanel.presetList.onChange = () -> if @selection isnt null and @selection.index isnt 0 win.controlsPanel.controlsGroup.myText.text = xmlData.preset[@selection.index].value win.controlsPanel.controlsGroup.mySlider.value = Number xmlData.preset[@selection.index].value true win.presetsPanel.resetPresets.onClick = () -> if confirm "Warning\nAre you sure you want to reset the Preset list?", true # remove existing preset file, creates a Default XML file createDefaultXML() # refresh the DropDownList! initDDL() win.presetsPanel.saveNewPreset.onClick = () -> presetName = prompt "Give your preset a name!\nYou'll find it in the preset list.", "User Preset", "Save new Preset" if !presetName? then return if presetName in presetNamesArray alert "Duplicate name!\nPlease find another one." # recursion again?! win.presetsPanel.saveNewPreset.onClick.call() # create a <preset> xml child child = createPresetChild presetName, win.controlsPanel.controlsGroup.myText.text # append the child to the xmlData xmlData.appendChild child writeXML xmlData # re-initialize the DDL in order to make changes appear initDDL() # select the last preset win.presetsPanel.presetList.selection = win.presetsPanel.presetList.items[win.presetsPanel.presetList.items.length - 1] win.presetsPanel.deletePreset.onClick = () -> # "true" is a string in the XML, not a boolean! if `xmlData.preset[win.presetsPanel.presetList.selection.index].@default.toString()` is "true" alert "Can't delete \"#{xmlData.preset[win.presetsPanel.presetList.selection.index].name}\"\nIt's part of the default set of Presets" return if confirm "Are you sure you want to delete \"#{xmlData.preset[win.presetsPanel.presetList.selection.index].name}\" preset?\nYou can't undo this." `delete xmlData.preset[win.presetsPanel.presetList.selection.index]` writeXML xmlData # re-initialize the DDL in order to make changes appear initDDL() # Init the DropDownList initDDL() # Show the window win.show()