Package com.scriptographer.ui

Source Code of com.scriptographer.ui.Component$OptionList

/*
* Scriptographer
*
* This file is part of Scriptographer, a Scripting Plugin for Adobe Illustrator
* http://scriptographer.org/
*
* Copyright (c) 2002-2010, Juerg Lehni
* http://scratchdisk.com/
*
* All rights reserved. See LICENSE file for details.
*
* File created on 23.10.2005.
*/

package com.scriptographer.ui;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import com.scratchdisk.list.ExtendedArrayList;
import com.scratchdisk.list.Lists;
import com.scratchdisk.list.ReadOnlyList;
import com.scratchdisk.script.ArgumentReader;
import com.scratchdisk.script.Callable;
import com.scratchdisk.script.ChangeEmitter;
import com.scratchdisk.script.ChangeReceiver;
import com.scratchdisk.script.MapArgumentReader;
import com.scratchdisk.script.ScriptEngine;
import com.scratchdisk.util.ConversionUtils;
import com.scriptographer.ScriptographerEngine;

/**
* @author lehni
*/
public class Component implements ChangeReceiver {

  private String label;
  private String name; // for preferences
  private ComponentType type;
  private Object defaultValue;
  private Object options[];
  private boolean visible = true;
  private boolean enabled = true;
  private boolean fullSize = false;
  private Integer width;
  private Integer height;
  private Integer length;
  private Integer maxLength;
  private Boolean multiline;
  private Integer rows;
  private Integer columns;
  private Double min;
  private Double max;
  private Double increment;
  private Integer fractionDigits;
  private TextUnits units;
  private Boolean steppers;

  protected ComponentProxy proxy;
  protected Palette palette;
  private boolean initialized;
  /**
   * A reference to the original object that defined this componenet, to
   * be able to replace it again in components at the end of execution.
   * (Needed by LiveEffect scope restauration).
   */
  private Map<String, Object> definition;

  /**
   * @jshide
   */
  public Component(ArgumentReader reader)
      throws IllegalArgumentException {
    if (reader.isMap()) {
      type = reader.readEnum("type", ComponentType.class);
      defaultValue = reader.readObject("value");
      if (type == null) {
        // Determine type form options and value
        if (reader.has("options"))
          type = ComponentType.LIST;
        else if (reader.has("onClick"))
          type = ComponentType.BUTTON;
        else if (reader.has("onSelect"))
          type = ComponentType.MENU_ENTRY;
        else if (defaultValue instanceof Number)
          type = ComponentType.NUMBER;
        else if (defaultValue instanceof Boolean)
          type = ComponentType.BOOLEAN;
        else if (defaultValue instanceof String)
          type = ComponentType.STRING;
      }
      if (type != null) {
        setType(type);
        // Call setMultiline to set default value for length
        setMultiline(false);
        // Tell the framework to set the properties from the map on the
        // object after creating through ArgumentReader
        reader.setProperties(this);
        // Turn on steppers for number components with units by default
        if (steppers == null && type == ComponentType.NUMBER
            && units != null && units != TextUnits.NONE)
          setSteppers(true);
      }
    }
    if (type == null)
      throw new IllegalArgumentException();
  }

  protected void initialize() {
    // Now set all the values again, so the item reflects them:
    if (!visible)
      proxy.setVisible(false);
    if (!enabled)
      proxy.setEnabled(false);
    // Calculate a default range for sliders of none was defined
    if (min == null && max == null && type == ComponentType.SLIDER) {
      min = 0d;
      max = defaultValue == null ? 1d
          : ConversionUtils.toDouble(defaultValue);
    }
    // Set these values not straight through proxy but through the public
    // methods here, since they perform type filtering for us.
    setRange(min, max);
    setFractionDigits(fractionDigits);
    // Setting range internally updates increments, so no need to set it
    // again here.
    setMaxLength(maxLength);
    setOptions(options);
    setValue(defaultValue);
    setUnits(units);
    // Now update the size of the item.
    updateSize();
    initialized = true;
  }

  /**
   * @jshide
   */
  public Component(ComponentType type, String label,
      Object value) {
    this.label = label;
    setType(type);
    this.defaultValue = value;
  }

  /**
   * Creates a STRING Item
   *
   * @jshide
   */
  public Component(String description, String value) {
    this(ComponentType.STRING, description, value);
  }

  /**
   * Creates a NUMBER Item
   *
   * @jshide
   */
  public Component(String description, double value) {
    this(ComponentType.NUMBER, description, value);
  }

  /**
   * Creates a BOOLEAN Item
   *
   * @jshide
   */
  public Component(String description, boolean value) {
    this(ComponentType.BOOLEAN, description, value);
  }

  /**
   * Creates a RANGE Item
   *
   * @jshide
   */
  public Component(String description, Number value, double min,
      double max, double step) {
    this(ComponentType.SLIDER, description, value);
    this.setRange(min, max);
    this.increment = step;
  }
 
  /**
   * Creates a LIST Item
   *
   * @jshide
   */
  public Component(String description, Object value, Object[] options) {
    this(ComponentType.LIST, description, value);
    this.options = options;
  }
 
  // TODO: make constructors for other types

  /**
   * Resets the value of the component to {@link #defaultValue}.
   */
  public void reset() {
    setValue(defaultValue);
  }

  protected void updateSize() {
    if (proxy != null)
      proxy.updateSize();
  }

  protected void onSizeChanged() {
    if (initialized && palette != null)
      palette.onSizeChanged();
  }

  /*
   * Beans
   */

  /**
   * The component type.
   */
  public ComponentType getType() {
    return type;
  }

  @SuppressWarnings("deprecation")
  public void setType(ComponentType type) {
    if (proxy != null)
      throw new UnsupportedOperationException(
          "The component type cannot be changed once it is created");
    switch (type) {
    case CHECKBOX:
      // Convert deprecated type CHECKBOX to BOOLEAN:
      type = ComponentType.BOOLEAN;
      break;
    case RANGE:
      // Convert deprecated type RANGE to SLIDER:
      type = ComponentType.SLIDER;
      break;
    }
    this.type = type;
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("type", type);
  }

  /**
   * The value of the component.
   */
  public Object getValue() {
    return proxy != null ? proxy.getValue() : defaultValue;
  }

  public void setValue(Object value) {
    if (proxy == null || !proxy.setValue(value))
      defaultValue = value;
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("value", value);
  }

  /**
   * The first {@link #getValue()} that was set.
   *
   * Sample code:
   * <code>
   * var values = {
   *   number: 30
   * };
   * var components = {
   *   number: {
   *     type: 'number'
   *   }
   * };
   * var palette = new Palette('Text', components, values);
   * values.number = 60;
   * print(components.number.value) // 60
   * print(components.number.defaultValue) // 30
   * </code>
   */
  public Object getDefaultValue() {
    return defaultValue;
  }

  /**
   * The name of the component as it is referenced in
   * {@link Palette#getComponents}.
   */
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("name", name);
  }

  /**
   * Text label that appears on the left hand side of the component in the
   * palette.
   */
  public String getLabel() {
    return label;
  }

  public void setLabel(String label) {
    this.label = label;
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("label", label);
  }

  /**
   * @deprecated
   */
  public String getDescription() {
    return getLabel();
  }

  /**
   * @deprecated
   */
  public void setDescription(String description) {
    setLabel(description);
  }


  /**
   * Specifies whether the component is visible.
   *
   * Sample code:
   * <code>
   * var values = {
   *   number: 10
   * };
   * var components = {
   *   showNumber: {
   *     type: 'checkbox', label: 'Show',
   *     onChange: function(value) {
   *       components.number.visible = value;
   *     }
   *   },
   *   number: {
   *     type: 'number', label: 'Number',
   *     visible: false
   *   }
   * };
   * var palette = new Palette('Show / Hide', components, values);
   * </code>
   * @return {@true if the component is visible}
   */
  public boolean isVisible() {
    return visible;
  }

  public void setVisible(boolean visible) {
    this.visible = visible;
    if (proxy != null)
      proxy.setVisible(visible);
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("visible", visible);
  }

  /**
   * Specifies whether the component is enabled. When set to {@code false},
   * the component is grayed out and does not allow user interaction.
   *
   * Sample code:
   * <code>
   * var values = {
   *   canEdit: false,
   *   text: 'Can you edit me?'
   * }
   * var components = {
   *   canEdit: {
   *     type: 'checkbox', label: 'Allow Editing',
   *     onChange: function(value) {
   *       components.text.enabled = value;
   *     }
   *   },
   *   text: {
   *     type: 'string',
   *     enabled: false
   *   }
   * };
   * var palette = new Palette('Text', components, values);
   * </code>
   *
   * @return {@true if the component is enabled}
   */
  public boolean isEnabled() {
    return enabled;
  }

  public void setEnabled(boolean enabled) {
    this.enabled = enabled;
    if (proxy != null)
      proxy.setEnabled(enabled);
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("enabled", enabled);
  }

  /**
   * {@grouptitle Size}
   *
   * When set to {@code true}, the component is stretched over the width of
   * the palette. When no {@link #getLabel()} is set, it also eliminates the
   * margin on the left hand side.
   */
  public boolean getFullSize() {
    return fullSize;
  }

  public void setFullSize(boolean fullSize) {
    this.fullSize = fullSize;
    if (fullSize) {
      width = null;
      length = null;
    }
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("fullSize", fullSize);
  }

  /**
   * The width of the input field in pixels.
   */
  public Integer getWidth() {
    return width;
  }

  public void setWidth(Integer width) {
    this.width = width;
    // Clear columns and length when setting width and vice versa.
    fullSize = false;
    columns = null;
    length = null;
    updateSize();
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("width", width);
  }

  /**
   * The height of the input field in pixels.
   */
  public Integer getHeight() {
    return height;
  }

  public void setHeight(Integer height) {
    this.height = height;
    // Clear rows when setting height and vice versa.
    rows = null;
    updateSize();
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("height", height);
  }
 
  /**
   * {@grouptitle Number / Slider Properties}
   *
   * The range for the numeric value as an array in the form: [min, max]. The
   * first element in the array defines the allowed minimum amount, the second
   * the maximum amount, both are included in the range.
   *
   * Sample code:
   * <code>
   * var values = {
   *   percentage: 50,
   *   angle: 180
   * };
   * var components = {
   *   percentage: {
   *     type: 'slider', label: 'Percentage',
   *     range: [0, 100]
   *   },
   *   angle: {
   *     type: 'number', label: 'Angle',
   *     range: [0, 360]
   *   },
   * };
   *
   * var palette = new Palette('Range Examples', components, values);
   * </code>
   */
  public double[] getRange() {
    return hasRange() ? new double[] {
      min, max
    } : null;
  }

  public void setRange(double[] range) {
    if (range == null)
      setRange(null, null);
    else
      setRange(range[0], range[1]);
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("range", range);
  }

  /**
   * @jshide
   */
  public void setRange(Double min, Double max) {
    if (type == ComponentType.NUMBER
        || type == ComponentType.SLIDER) {
      this.min = min;
      this.max = max;
      if (proxy != null && proxy.setRange(min, max)) {
        // Setting range sets increment again as well, as it will
        // be dynamically calculated based on range in case it was
        // not set on a fixed value.
        setIncrement(increment);
      }
    }
  }

  /**
   * @jshide
   */
  public boolean hasRange() {
    return min != null && max != null;
  }

  /**
   * The minimum amount allowed.
   *
   * Sample code:
   * <code>
   * var values = {
   *   size: 5
   * };
   * var components = {
   *   size: {
   *     type: 'number', label: 'Size',
   *     min: 0, steppers: true
   *   }
   * };
   * var palette = new Palette('Minimum Value', components, values);
   * </code>
   */
  public Double getMin() {
    return min;
  }

  public void setMin(Double min) {
    setRange(min, max);
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("min", min);
  }

  /**
   * The maximum amount allowed.
   *
   * Sample code:
   * <code>
   * var values = {
   *   size: 5
   * };
   * var components = {
   *   size: {
   *     type: 'number', label: 'Size',
   *     max: 10, steppers: true
   *   }
   * };
   * var palette = new Palette('Maximum Value', components, values);
   * </code>
   */
  public Double getMax() {
    return max;
  }

  public void setMax(Double max) {
    setRange(min, max);
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("max", max);
  }

  /**
   * The amount the steppers increase / decrease the value. Even when steppers
   * are not activated, the user can still use the up/down arrow keys to step
   * by the amount defined by increment.
   *
   * Sample code:
   * <code>
   * var values = {
   *   percentage: 50
   * };
   * var components = {
   *   percentage: {
   *     type: 'number',
   *     range: [0, 100],
   *     steppers: true, increment: 10
   *   }
   * };
   * var palette = new Palette('Increment', components, values);
   * </code>
   */
  public Double getIncrement() {
    if (type == ComponentType.NUMBER
        || type == ComponentType.SLIDER) {
      // If no increment is defined, calculate a default value, based on
      // the defined range.
      if (increment == null) {
        if (min != null && max != null) {
          double range = max - min;
          if (range == 0)
            range = 1.0;
          int numDigits = Math.max(0, 2 - (int) Math.ceil(Math.log10(range)));
          double inc = 1.0 / Math.pow(10, numDigits);
          return inc > 1.0 ? 1.0 : inc;
        }
        return 1.0;
      }
      return increment;
    }
    return null;
  }

  public void setIncrement(Double increment) {
    if (type == ComponentType.NUMBER
        || type == ComponentType.SLIDER) {
      this.increment = increment;
      // If no increment is defined, use a default value, as calculated
      // by getIncrement.
      if (proxy != null)
        proxy.setIncrement(getIncrement());
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("increment", increment);
    }
  }

  /**
   * The amount of digits after the decimal point. If finer grained values are
   * entered, they are rounded to the next allowed number. The default is 3.
   */
  public Integer getFractionDigits() {
    return fractionDigits;
  }

  public void setFractionDigits(Integer fractionDigits) {
    if (type == ComponentType.NUMBER) {
      if (fractionDigits == null)
        fractionDigits = 3;
      this.fractionDigits = fractionDigits;
      if (proxy != null)
        proxy.setFractionDigits(fractionDigits);
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("fractionDigits", fractionDigits);
    }
  }
 
  /**
   * The units to be displayed behind the value.
   *
   * Sample code:
   * <code>
   * var values = {
   *   width: 10,
   *   percentage: 50,
   *   angle: 180
   * }
   *
   * var components = {
   *   width: {
   *     type: 'number', label: 'Width',
   *     units: 'point'
   *   },
   *   percentage: {
   *     type: 'number', label: 'Percentage',
   *     units: 'percent'
   *   },
   *   angle: {
   *     type: 'number', label: 'Angle',
   *     units: 'degree'
   *   }
   * }
   *
   * var palette = new Palette('Units Examples', components, values);
   * </code>
   */
  public TextUnits getUnits() {
    return units;
  }

  public void setUnits(TextUnits units) {
    if (type == ComponentType.NUMBER) {
      if (units == null)
        units = TextUnits.NONE;
      this.units = units;
      if (proxy != null)
        proxy.setUnits(units);
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("units", units);
    }
  }

  /**
   * Activates little stepping arrows on the side of the input field when set
   * to true.
   */
  public Boolean getSteppers() {
    return steppers;
  }

  public void setSteppers(Boolean steppers) {
    // No support to change this at runtime for now
    if (type == ComponentType.NUMBER) {
      this.steppers = steppers;
      if (proxy != null)
        proxy.setSteppers(steppers);
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("steppers", steppers);
    }
  }

  /**
   * OptionList is a normal ExtendedArrayList that implements ChangeEmitter so
   * changes on it get propagated to the Palette object which is a
   * ChangeReceiver automatically through mechanisms in the scripting engine.
   */
  private static class OptionList extends ExtendedArrayList<Object> implements
      ChangeEmitter {

    public OptionList(Object[] options) {
      super(options);
    }
  }

  /**
   * {@grouptitle List Properties}
   *
   * The options that the user can choose from in the list component.
   *
   * Sample code:
   * <code>
   * var values = {
   *   fruit: 'Orange'
   * };
   * var components = {
   *   fruit: {
   *     type: 'list', label: 'Fruit',
   *     options: ['Orange', 'Apple', 'Banana', 'Kiwi']
   *   }
   * };
   * var palette = new Palette('List Example', components, values);
   * </code>
   */
  public com.scratchdisk.list.List<Object> getOptions() {
    return new OptionList(options);
  }

  public void setOptions(ReadOnlyList<Object> options) {
    setOptions(options != null ? Lists.toArray(options) : new Object[0]);
  }

  public void setOptions(Object[] options) {
    if (type == ComponentType.LIST) {
      // Retrieve current value before setting new options,
      // as options is used inside getValue().
      Object current = getValue();
      this.options = options;
      if (proxy != null)
        proxy.setOptions(options, current);
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("options", options);
    }
  }

  /**
   * @jshide
   */
  public Object getOption(int index) {
    if (options != null && index >= 0 && index < options.length)
      return options[index];
    return null;
  }

  /**
   * The index of the selected value in the {@link #getOptions} array.
   *
   * Sample code:
   * <code>
   * var values = {
   *   fruit: 'Apple'
   * };
   * var components = {
   *   fruit: {
   *     type: 'list', label: 'Fruit',
   *     options: ['Orange', 'Apple', 'Banana', 'Kiwi']
   *   }
   * };
   * var palette = new Palette('List Example', components, values);
   * print(components.fruit.selectedIndex) // 1
   * </code>
   */
  public Integer getSelectedIndex() {
    return proxy != null ? proxy.getSelectedIndex() : null;
  }

  public void setSelectedIndex(Integer index) {
    // We're changing index, not value, so cause onChange callback for value
    setSelectedIndex(index, true);
  }

  protected void setSelectedIndex(Integer index, boolean callback) {
    if (type == ComponentType.LIST && index != null && index >= 0
        && (options == null || index < options.length)) {
      if (proxy != null && proxy.setSelectedIndex(index, callback))
        onChange(callback);
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("selectedIndex", index);
    }
  }

  /**
   * {@grouptitle Text and String Properties}
   *
   * The width of the text field in average amount of characters.
   */
  public Integer getLength() {
    return length;
  }

  public void setLength(Integer length) {
    if (type == ComponentType.STRING ||
        type == ComponentType.NUMBER) {
      this.multiline = false;
      this.length = length;
      // Clear width and columns when setting length and vice versa.
      fullSize = false;
      width = null;
      columns = null;
      updateSize();
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("length", length);
    }
  }

  /**
   * The maximum amount of characters that the text field may contain.
   *
   * Sample code:
   * <code>
   * var values = {
   *   name: ''
   * };
   * var components = {
   *   name: {
   *     type: 'string', label: 'Name',
   *     editable: true, maxLength: 3
   *   }
   * };
   * var palette = new Palette('Max Length', components, values);
   * values.name = '123456';
   * print(values.name) // '123'
   * </code>
   */
  public Integer getMaxLength() {
    return maxLength;
  }

  public void setMaxLength(Integer maxLength) {
    if (type == ComponentType.STRING ||
        type == ComponentType.NUMBER) {
      this.maxLength = maxLength;
      if (proxy != null)
        proxy.setMaxLength(maxLength);
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("maxLength", maxLength);
    }
  }

  /**
   * Specifies whether the field shows multiple lines of text.
   *
   * Sample code:
   * <code>
   * var components = {
   *     text: {
   *         type: 'text', label: 'Text',
   *         value: 'This is a text\nwith multiple lines',
   *         multiline: true
   *     }
   * };
   * var palette = new Palette('Text', components);
   * </code>
   */
  public Boolean getMultiline() {
    return multiline;
  }

  public void setMultiline(Boolean multiline) {
    if (type == ComponentType.STRING ||
        type == ComponentType.NUMBER) {
      multiline = multiline && type == ComponentType.STRING;
      // Set default values for length / columns, rows
      if (multiline) {
        if (rows == null)
          rows = 6;
        if (columns == null)
          columns = 32;
        length = null;
      } else {
        if (length == null)
          length = type == ComponentType.STRING ? 16 : 8;
        columns = null;
        rows = null;
      }
      this.multiline = multiline;
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("multiline", multiline);
    }
  }

  /**
   * The average amount of characters per line visible in the text area.
   *
   * Sample code:
   * <code>
   * var components = {
   *     text: {
   *         type: 'string',
   *         value: 'This is a string component\nwith 6 rows and 30 columns',
   *         rows: 6, columns: 32
   *     }
   * };
   * var palette = new Palette('Text', components);
   * </code>
   */
  public Integer getColumns() {
    return columns;
  }

  public void setColumns(Integer columns) {
    if (type == ComponentType.STRING) {
      this.setMultiline(true);
      this.columns = columns;
      // Clear width and length when setting columns and vice versa.
      fullSize = false;
      width = null;
      length = null;
      updateSize();
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("columns", columns);
    }
  }

  /**
   * The amount of visible lines of text in the text area. Due to a bug in
   * Illustrator's GUI, values below 6 cause problems with scrollbars on
   * Macintosh. The default is 6.
   *
   * Sample code:
   * <code>
   * var components = {
   *     text: {
   *         type: 'string',
   *         value: 'This is a string component\nwith 6 rows and 30 columns',
   *         rows: 6, columns: 30
   *     }
   * };
   * var palette = new Palette('Text', components);
   * </code>
   */
  public Integer getRows() {
    return rows;
  }

  public void setRows(Integer rows) {
    if (type == ComponentType.STRING) {
      this.setMultiline(rows > 1);
      this.rows = rows;
      // Clear height when setting rows and vice versa.
      height = null;
      updateSize();
      // Update definition as well, so LiveEffect scope restoration works
      if (definition != null)
        definition.put("rows", rows);
    }
  }

  private Callable onChange;

  /**
   * {@grouptitle Callback handlers}
   *
   * The function that is called whenever the value of the component changes.
   * The function receives the new value as an argument.
   *
   * Sample code:
   * <code>
   * var components = {
   *   threshold: {
   *     type: 'number', label: 'Distance Threshold',
   *     units: 'point',
   *     onChange: function(value) {
   *       print('Threshold was changed to', value);
   *       tool.distanceThreshold = value;
   *     }
   *   }
   * };
   *
   * var palette = new Palette('title', components);
   * </code>
   */
  public Callable getOnChange() {
    return onChange;
  }

  public void setOnChange(Callable onChange) {
    this.onChange = onChange;
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("onChange", onChange);
  }

  protected void onChange(boolean callback) {
    Object value = getValue();
    // First call onChange on Palette, so values get updated
    if (palette != null)
      palette.onChange(this, name, value, callback);
    // And now call onChange on the item. values will contain the same
    // new value now too.
    if (callback && onChange != null)
      ScriptographerEngine.invoke(onChange, this, value);
  }

  private Callable onClick;

  /**
   * The function that is called when a button component is clicked.
   *
   * Sample code:
   * <code>
   * var components = {
   *   button: {
   *     type: 'button',
   *     value:'Click Me', label: 'Button',
   *     onClick: function() {
   *       print('You clicked me!');
   *     }
   *   }
   * };
   * var palette = new Palette('Button Component', components);
   * </code>
   */
  public Callable getOnClick() {
    return onClick;
  }

  public void setOnClick(Callable onClick) {
    this.onClick = onClick;
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("onClick", onClick);
  }

  protected void onClick() {
    if (onClick != null)
      ScriptographerEngine.invoke(onClick, this);
  }

  private Callable onSelect;

  /**
   * The function that is called when a popup menu entry is selected.
   *
   * Sample code:
   * <code>
   * var components = {
   *   menuEntry: {
   *     type: 'menu-entry',
   *     value:'Select Me',
   *     onSelect: function() {
   *       print('You selected me!');
   *     }
   *   }
   * };
   * var values = new Palette('Menu Entry', components);
   * </code>
   */
  public Callable getOnSelect() {
    return onSelect;
  }

  public void setOnSelect(Callable onSelect) {
    this.onSelect = onSelect;
    // Update definition as well, so LiveEffect scope restoration works
    if (definition != null)
      definition.put("onSelect", onSelect);
  }

  protected void onSelect() {
    if (onSelect != null)
      ScriptographerEngine.invoke(onSelect, this);
  }

  @SuppressWarnings("unchecked")
  private static Component getComponent(Object object, String name,
      Object value) {
    if (object != null) {
      if (object instanceof Component) {
        return (Component) object;
      } else if (object instanceof Map) {
        try {
          ArgumentReader reader =
              ScriptEngine.convertToArgumentReader(object);
          if (name != null || value != null) {
            // We need to create a new map that contains all the
            // values from map, but also name and value, so the
            // ArgumentReader constructor can read these from there.
            // Just clone it.
            Map<Object, Object> clone =
                new HashMap<Object, Object>((Map) object);
            if (name != null)
              clone.put("name", name);
            if (value != null)
              clone.put("value", value);
            // Make a new ArgumentReader that inherits its converter
            // from the current reader, which was returned by
            // ScriptEngine.convertToArgumentReader. So for example
            // if map actually is a NativeObject, our new reader will
            // inherit all converter functionality from it.
            reader = new MapArgumentReader(reader, clone);
          }
          Component component = new Component(reader);
          component.definition = (Map) object;
          return component;
        } catch (IllegalArgumentException e) {
        }
      }
    }
    return null;
  }

  /**
   * @jshide
   */
  public static Component[] getComponents(
      Map<String, Object> components, Map<String, Object> values) {
    ArrayList<Component> comps =
        new ArrayList<Component>();
    for (Map.Entry<String, Object> entry : components.entrySet()) {
      String name = entry.getKey();
      Object object = entry.getValue();
      Object value = values != null ? values.get(name) : null;
      Component component = getComponent(object, name, value);
      if (component != null) {
        comps.add(component);
        // Make sure we're putting the produced components back into the
        // passed comonents Map, so they can be accessed directly from
        // the script (e.g. to toggle the enabled flag).
        components.put(name, component);
      }
    }
    return comps.toArray(new Component[comps.size()]);
  }

  /**
   * @jshide
   */
  public static void restoreComponentDefinitions(
      Map<String, Object> components) {
    for (Map.Entry<String, Object> entry : components.entrySet()) {
      String name = entry.getKey();
      Object object = entry.getValue();
      if (object instanceof Component) {
        Component component = (Component) object;
        if (component.definition != null)
          components.put(name, component.definition);
      }
    }
  }
}
TOP

Related Classes of com.scriptographer.ui.Component$OptionList

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.