How to create a default dialog.

Content:
  1. Introduction
  2. NodeDialog code example
  3. NodeModel code example
  4. Table of all default dialog components
  5. Disabling components
  6. Renaming the default tab
  7. Creating new tabs
  8. Using DialogComponents in a custom NodeDialogPane
  9. FAQs

Introduction

"Default dialog components" provide a convenient way to get user input for a few basic settings through a dialog that can be easily created, without going through the pain of creating swing components and layouts.

We provide a couple of components that can be added to a default settings pane, and that allow the user to enter values. These values can be easily retrieved in the NodeModel then. There is no way (and no need) to layout these components - they will automatically show in the dialog below each other (or next to each other, depending on the currently active orientation, see setHorizontalPlacement), in the order they have been added. You can surround them with a border to make the dialog look nicer and you can create new tabs to organize them. But that's all.
Well, there is also support for enabling/disabling the components through the mechanism of registered ChangeListeners.

Each default component uses a swing component (like a JTextField or JSpinner) as input field, and allows for adding a label to that field, to add an explanatory text for the user. Besides that each dialog component needs a SettingsModel. This settings model holds the actual value of the component and does the storing, loading and validation. When you instantiate the component you must provide a settings model - when you want to load the entered value in your NodeModel, you use an instance of that same SettingsModel. To identify the value handled by the settings model, you must provide a unique configName (i.e. a string ID).

Here is an example of a default dialog with a few components grouped into two groups:
an example default dialog
An example of a default dialog. It shows a selection of available components, grouped into two groups, each surrounded by titled border.

For a complete list of available components (and settings models they require), please refer to the table below.

Code example NodeDialog

 Here is the code that creates the above dialog:

/**
 * The new default dialog class must be derived from DefaultNodeSettingsPane.
 */
public class DemoNodeDialogPane extends DefaultNodeSettingsPane {

    /**
     * Instantiate and add all needed components here in the constructor.
     * (The suppress warnings is used to avoid compiler warnings from the
     * constructor with generic varargs.)
     */
    @SuppressWarnings("unchecked")
    DemoNodeDialogPane() {
        createNewGroup("Group 1:"); // following components are bordered
        addDialogComponent(new DialogComponentString(new SettingsModelString(
                DemoNodeModel.STR, null), "Enter a string value:"));
        addDialogComponent(new DialogComponentNumber(new SettingsModelInteger(
                DemoNodeModel.INT, 3), "Enter an integer (1-17):", 1));
        addDialogComponent(new DialogComponentNumber(new SettingsModelDouble(
                DemoNodeModel.DBL, 3.0),
                "Enter a floating point number:", 0.1));
        createNewGroup("Group 2:"); // closes the prev group, opens a new one
        addDialogComponent(new DialogComponentBoolean(new SettingsModelBoolean(
                DemoNodeModel.BOOL, false), "Checkit or leave it:"));
        addDialogComponent(new DialogComponentStringSelection(
                new SettingsModelString(DemoNodeModel.STRSEL, null),
                "Your choice:", "First", "Second", "Third"));
        closeCurrentGroup();
        addDialogComponent(new DialogComponentColumnNameSelection(
                new SettingsModelString(DemoNodeModel.COLSEL, ""),
                "Select a column", 0, true, IntValue.class, DoubleValue.class));
    }
}
The code adds 6 components grouped by two borders. All components following the createNewGroup(<Title>) command will be put into the frame. The next createNewGroup command closes the previous group and opens a new one. If you want to add components below the last border, you can use closeCurrentGroup. Components added after this command will be placed outside any border.

Each component's constructor requires a new instance of a SettingsModel. The settings model expects a string identifier that it uses to store and load the value of the component, and a default value which it holds until a new value is loaded. Additional parameters are necessary, depending on the type of component, most require a string, that will be displayed in front of the component as label. For more details, please refer to the documentation of the specific component.

Code example NodeModel

The following code reads the user input through SettingsModels into the NodeModel:
/**
 * The node model to the demo default dialog.
 */
public class DemoNodeModel extends NodeModel {

    /**
     * The following strings are used by the dialog and the model
     * as key to store settings in the settings object.
     */
    static final String STR = "str";
    static final String INT = "int";
    static final String DBL = "dbl";
    static final String BOOL = "bool";
    static final String STRSEL = "selStr";
    static final String COLSEL = "selCol";

    /**
     * these are the members storing user settings.
     * Use the same settings model (but a new instance) as in
     * the node dialog for that value.
     */
    private final SettingsModelString m_str =
            new SettingsModelString(STR, null);

    private final SettingsModelIntegerBounded m_intBounded =
            new SettingsModelIntegerBounded(INT, 4, 1, 17);

    private final SettingsModelDouble m_dbl =
            new SettingsModelDouble(DBL, .4);

    private final SettingsModelBoolean m_bool =
            new SettingsModelBoolean(BOOL, true);

    private final SettingsModelString m_selStr =
            new SettingsModelString(STRSEL, null);

    private final SettingsModelString m_colSel =
        new SettingsModelString(COLSEL, "none");

    /** The constructor */
    DemoNodeModel() {
        super(1, 0);
    }

    [....]

    /**
     * @see org.knime.core.node.NodeModel
     *      #loadValidatedSettingsFrom(org.knime.core.node.NodeSettingsRO)
     */
    @Override
    protected void loadValidatedSettingsFrom(final NodeSettingsRO settings)
            throws InvalidSettingsException {
        m_bool.loadSettingsFrom(settings);
        m_dbl.loadSettingsFrom(settings);
        m_intBounded.loadSettingsFrom(settings);
        m_selStr.loadSettingsFrom(settings);
        m_str.loadSettingsFrom(settings);
        m_colSel.loadSettingsFrom(settings);
    }


    /**
     * @see org.knime.core.node.NodeModel
     *      #saveSettingsTo(org.knime.core.node.NodeSettingsWO)
     */
    @Override
    protected void saveSettingsTo(final NodeSettingsWO settings) {
        m_bool.saveSettingsTo(settings);
        m_dbl.saveSettingsTo(settings);
        m_intBounded.saveSettingsTo(settings);
        m_selStr.saveSettingsTo(settings);
        m_str.saveSettingsTo(settings);
        m_colSel.saveSettingsTo(settings);
       
    }


    /**
     * @see org.knime.core.node.NodeModel
     *      #validateSettings(org.knime.core.node.NodeSettingsRO)
     */
    @Override
    protected void validateSettings(final NodeSettingsRO settings)
            throws InvalidSettingsException {
        m_bool.validateSettings(settings);
        m_dbl.validateSettings(settings);
        m_intBounded.validateSettings(settings);
        m_selStr.validateSettings(settings);
        m_str.validateSettings(settings);
        m_colSel.validateSettings(settings);
    }


    /**
     * @see org.knime.core.node.NodeModel
     *      #execute(org.knime.core.node.BufferedDataTable[],
     *      org.knime.core.node.ExecutionContext)
     */
    @Override
    protected BufferedDataTable[] execute(final BufferedDataTable[] inData,
            final ExecutionContext exec) throws Exception {

        if (m_bool.getBooleanValue()) {
            double d = m_dbl.getDoubleValue() * 2.5;
        }
        int i = m_intBounded.getIntValue();
        // i is larger than 0 and smaller than 18 (because the model used
        // is a SettingsModelIntBounded with these limits).

        if (m_selStr.getStringValue() != null) {
            m_str.setStringValue(m_colSel.getStringValue());
        }

        return new BufferedDataTable[0];
    }

For each component/setting value add a member holding the corresponding SettingsModel. Use exactly the same type of SettingsModel as the component in the dialog. The SettingsModels provide load, save, and validate methods that just need to be called in the corrsponding method of the NodeModel. This takes care of the saving and loading of these values and also transfers them into the dialog and the new user values back from the dialog. To access the current value in the SettingsModel, call the coresponding getter-method. SettingModels also provide a public setter-method to change their value.

If  there are dependencies to check between two settings values, the validateSettings method becomes a bit more complicated: If a settings model validates a value, it does not store that value, thus you have no access to that value you are supposed to validate. If you need to check some values against each other (like, to ensure that the new minimum is smaller than the specified maximum, for example), you need to create new temporary settings models, load the new values in, read and verify them, and release the settings models at the end. Here is a code example:
    /**
     * @see org.knime.core.node.NodeModel
     *      #validateSettings(org.knime.core.node.NodeSettingsRO)
     */
    @Override
    protected void validateSettings(final NodeSettingsRO settings)
            throws InvalidSettingsException {

        // create new (temp) settings models
        // (if m_min and m_max are fields of the NodeModel)
       
SettingsModelInteger min =
                   m_min.createCloneWithValidatedValue(MIN, settings);
        SettingsModelInteger max =
                   m_max.createCloneWithValidatedValue(MAX, settings);

        if (min.getIntValue() >= max.getIntValue()) {
            throw new InvalidSettingsException("The specified minimum "
                   + "must be smaller than the maximum value.");
        }

        m_dbl.validateSettings(settings);
        m_intBounded.validateSettings(settings);
        m_selStr.validateSettings(settings);
        m_colSel.validateSettings(settings);
    }

With the createCloneWithValidatedValue method, create a temporary settings model with the new value from a NodeSettings object. Use this temporary object to access the new value, but make sure not to change any permanent variables in the NodeModel. Release the clone models after validateSettings finishes.

Use static create-methods to instantiate SettingsModels:
Because you need instances of identical SettingsModels in two places (in the NodeModel and the NodeDialog constructor) it is probably a good idea to implement static methods that create these instances and to just call these methods in both places. For example, add the following method to your NodeModel:
static SettingsModelIntegerBounded createRangeSettingsModel() {
    return new SettingsModelIntegerBounded("Range", 4, 1, 17);
}
Now, your dialog constructor looks like:
        addDialogComponent(new DialogComponentNumber(MyNodeModel.createRangeSettingsModel());
and the node model:
        private final SettingsModelIntegerBounded m_intBounded = createRangeSettingsModel();
The code is simpler, and more importantly, it is ensured that you really use the same SettingsModels with the same parameters and same IDs.


Table of all default dialog components

Here is a list of all currently available default components.
Looks Default Component Class Accepted SettingsModels Getter Methods Notes
ScreenShot DlgCompBoolean DialogComponentBoolean SettingsModelBoolean boolean getBooleanValue() Stores a boolean value according to the state of the checkbox.
ScreenShot ColFilter DialogComponentColumnFilter SettingsModelFilterString List<String> getIncludeList()
List<String> getExcludeList()
Implements support for an include and exclude list. Mainly used for column filtering.
ScreenShot DlgComp ColSelect DialogComponentColumnNameSelection SettingsModelString

SettingsModelColumnName
String getStringValue()

String getColumnName()
boolean useRowID()
Stores the name of the selected column. Uses the renderer of the type of the corresponding column.
Using the SettingsModelColumnName will add a RowID option to the select list.
ScreenShot DlgComp FileChooser DialogComponentFileChooser SettingsModelString String getStringValue() The stored string is the (absolut) filename of the selected file.
ScreenShot DlgComp number DialogComponentNumber SettingsModelDouble
SettingsModelInteger
SettingsModelDoubleBounded
SettingsModelIntegerBounded
double getDoubleValue()
int getIntValue()
Accepts a number (double or integer) in an editable spinner. The bounded models will not accept values outside the specified range.
ScreenShot DlgComp NumberEdit DialogComponentNumberEdit SettingsModelDouble
SettingsModelInteger
SettingsModelDoubleBounded
SettingsModelIntegerBounded
double getDoubleValue()
int getIntValue()
Accepts a number (double or integer) in an EditField. The bounded models will not accept values outside the specified range.
component with two spinners for min and max DialogComponentDoubleRange SettingsModelDoubleRange double getMaxRange()
double getMinRange()
Stores a min and a max floating point number.
ScreenShot DlgComp Password DialogComponentPasswordField SettingsModelString String getStringValue() Accepts any string in a textfield. The entered string will not be echoed - a dot appears for each character. The string stored in the settings model is the encrypted password. The component provides static encryption and decryption methods.
ScreenShot DlgComp String DialogComponentString SettingsModelString String getStringValue() Stores any string entered in the field.
Screenshot DlgComponentMultiLineString DialogComponentMultiLineString SettingsModelString String getStringValue() Stores a string, that could contain multiple lines.
ScreenShot DlgComp stringSelect DialogComponentStringSelection SettingsModelString String getStringValue() The user can select a string from the list. The combobox is not editable.
ScreenShot DlgComp stringListSelect DialogComponentStringListSelection SettingsModelStringArray String getStringArrayValue() The user can select multiple strings from the list. The select box is not editable.
ScreenShot DlgComp buttonGroup DialogComponentButtonGroup SettingsModelString String getStringValue() The user can choose one option. The action command of the selected RadioButton is stored in the SettingsModelString.


Disabling components

All components can be disabled (grayed-out) by calling setEnabled(false) on their SettingsModel. You can register a ChangeListener with a SettingsModel and change the enable status of another one depending on the value of the first one, for example. Disabled SettingsModels still store, save and load their value (this way a value entered in the enabled component is preserved). When you start using the values from the SettingsModels in your NodeModel implementation, you need to check the enable status by calling isEnabled(). When changing the values in the SettingsModels (or when instantiating them and assigning an initial value), you need to carefully maintain a consistent state between those SettingsModels whose state depend on each other. For example, if you have a Model that holds a string value and whose enable status depends on a boolean SettingsModel, you need to make sure that you disable the string model, whenever you assign false to the boolean model (and, if you initialize the boolean model with false, disable the string model!). An example is available here.

Renaming the default tab

You can set a new title on the default "Options" tab, the one that is always created and the components are added to by default. Call setDefaultTabTitle(String) in your NodeDialogPane (that is derived from the DefaultNodeSettingsPane). The specified string appears in the tab's title.

Creating new tabs

Additional tabs can be created by calling createNewTab(String). The specified string is displayed as title of the new tab. Components added after this call will be placed into the new tab. Also groups work in the new tab. Previous tabs are not accessible anymore. New tabs are placed right of and behind already existing tabs. Or, you can call createNewTab(String, int) to specify the position the new tab should be placed at. The specified title must be unique. A specific tab can be selected (brought to front) by calling  selectedTab(String).

Placement of dialog components

The placement (horizontal/vertical) of the dialog components can be changed by calling setHorizontalPlacement(boolean). If set to true the next components will be added next to each other. If set to false the next component is place below the previous one.

Using DialogComponents in a custom NodeDialogPane

The provided DefaultNodeSettingsPane is a convenient way to build simple dialogs without the need to implement any additional methods or arranging components. Its layout is very restricted though, components can be arranged only below/next to each other. If you need a more complex dialog with a different layout, you need to create your own swing panel (directly derived from NodeDialogPane). You can still use the DialogComponents, arrange them in your custom panel and benefit from their loading and saving capabilities: Each component provides a getComponentPanel() method that returns the panel holding the component. This panel can be placed into your dialog pane. In the dialog's loadSettingsFrom and saveSettingsTo methods, you need to call the component's loadSettingsFrom and saveSettingsTo respectively. The usage of the corresponding SettingsModels in the NodeModel is the same as if they were used in a DefaultNodeSettingsPane.

FAQs

Q: There are two places to specify a default value (with the settings model, when adding a component, and in the one used in the NodeModel). Which one is used?
A: The one specified in the settings model  used by the NodeModel. Before the dialog opens, the NodeModel's settings are transfered into the dialog. In some cases the default value in the dialog constructor is used to determine the components width, if it is not set explicitly.

Q: When the user enters an invalid value some components are colored red, others aren't. Why?
A: If you use the same SettingsModel in the dialog and the NodeModel it should work. Especially if you expect numbers in a certain range, make sure you use bounded models in the dialog and the NodeModel - with the same upper and lower bounds! The coloring doesn't work if the NodeModel has a more restricted settings model than the dialog component, if it is the NodeModel that rejects the value.

Q: Why do I need to instantiate two SettingsModels for each component, why can't I just transfer the object from the NodeModel to the NodeDialog and get the user input stored in there?
A: That's how the Nodes in KNIME are designed. The NodeModel doesn't know it's dialog, and vice versa. Values are transfered through settings objects. You need to implement a mechanism to store and load your current settings anyway - we re-use it to transfer the current values into the dialog. Also, the NodeModel must be able to reject invalid/inconsistent values (in the validateSettings method). If we would have only one object, the invalid settings would be set to let the NodeModel validate them and canceling the dialog then would leave these  invalid or inconsistent values in the NodeModel.

Q: How can I influence the width of the components?
A: There are some components that calculate there width automatically (like ComboBoxes). Others have a constructor that allow setting the width (in columns/characters) explicitly. If you don't use this constructor, the value in the SettingsModel passed will be used to determine the width.