<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * User-interface component for picking a number value from a range.
 * This is essentially a value with integrated up/down spinners.
 * But PhET has been calling it a 'picker', so that's what this type is named.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Color = require( 'SCENERY/util/Color' );
  var DerivedProperty = require( 'AXON/DerivedProperty' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Timer = require( 'JOIST/Timer' );
  var Util = require( 'DOT/Util' );

  // creates a vertical gradient where with color1 at the top and bottom, color2 in the center
  var createBackgroundGradient = function( color1, color2, height ) {
    return new LinearGradient( 0, 0, 0, height )
      .addColorStop( 0, color1 )
      .addColorStop( 0.5, color2 )
      .addColorStop( 1, color1 );
  };

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<String>} stateProperty 'up'|'over'|'down'|'out'
   * @param {Property<Boolean>} enabledProperty
   * @param {Function} fireFunction
   * @param {Number} timerDelay start to fire continuously after pressing for this long (milliseconds)
   * @param {Number} intervalDelay // fire continuously at this frequency (milliseconds)
   * @constructor
   */
  function PickerListener( stateProperty, enabledProperty, fireFunction, timerDelay, intervalDelay ) {

    // stuff related to press-&-hold feature
    var fired = false;
    var timeoutID = null;
    var intervalID = null;
    var cleanupTimer = function() {
      if ( timeoutID ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
    };

    ButtonListener.call( this, {

      up: function() {
        stateProperty.set( 'up' );
        cleanupTimer();
      },

      over: function() {
        stateProperty.set( 'over' );
      },

      down: function() {
        if ( intervalID === null ) {
          stateProperty.set( 'down' );
          fired = false;
          timeoutID = Timer.setTimeout( function() {
            timeoutID = null;
            fired = true;
            intervalID = Timer.setInterval( function() {
              if ( enabledProperty.get() ) {
                fireFunction();
              }
            }, intervalDelay );
          }, timerDelay );
        }
      },

      out: function() {
        stateProperty.set( 'out' );
      },

      fire: function() {
        cleanupTimer();
        if ( !fired && enabledProperty.get() ) {
          fireFunction();
        }
      }
    } );
  }

  inherit( ButtonListener, PickerListener );

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<Number>} valueProperty
   * @param {Property<Range>} rangeProperty
   * @param {*} options
   * @constructor
   */
  function NumberPicker( valueProperty, rangeProperty, options ) {

    options = _.extend( {
      color: new Color( 0, 0, 255 ), // must be a Color, not a CSS string
      cornerRadius: 6,
      xMargin: 3,
      yMargin: 3,
      decimalPlaces: 0,
      font: new PhetFont( 24 ),
      upFunction: function() { return valueProperty.get() + 1; },
      downFunction: function() { return valueProperty.get() - 1; },
      timerDelay: 400, // start to fire continuously after pressing for this long (milliseconds)
      intervalDelay: 100, // fire continuously at this frequency (milliseconds),
      noValueString: '-', // string to display if valueProperty.get is null or undefined
      touchAreaExpandX: 10,
      touchAreaExpandY: 20,
      mouseAreaExpandX: 0,
      mouseAreaExpandY: 14
    }, options );

    var thisNode = this;
    Node.call( thisNode, { cursor: 'pointer' } );

    // properties for the "up" (increment) control
    var upStateProperty = new Property( 'up' );
    var upEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value < range.max );
    } );

    // properties for the "down" (decrement) control
    var downStateProperty = new Property( 'up' );
    var downEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value > range.min );
    } );

    // callbacks for changing the value
    var fireUp = function() {
      valueProperty.set( Math.min( options.upFunction(), rangeProperty.get().max ) );
    };
    var fireDown = function() {
      valueProperty.set( Math.max( options.downFunction(), rangeProperty.get().min ) );
    };

    // displays the value
    var valueNode = new Text( "", { font: options.font, pickable: false } );

    // compute max width of text based on value range
    valueNode.text = Util.toFixed( rangeProperty.get().min, options.decimalPlaces );
    var maxWidth = valueNode.width;
    valueNode.text = Util.toFixed( rangeProperty.get().max, options.decimalPlaces );
    maxWidth = Math.max( maxWidth, valueNode.width );

    // compute shape of the background behind the numeric value
    var backgroundWidth = maxWidth + ( 2 * options.xMargin );
    var backgroundHeight = valueNode.height + ( 2 * options.yMargin );
    var backgroundOverlap = 1;
    var backgroundCornerRadius = options.cornerRadius;

    // top half of the background, for "up". Shape computed starting at upper-left, going clockwise.
    var upBackground = new Path( new Shape()
      .arc( backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, Math.PI, Math.PI * 3 / 2, false )
      .arc( backgroundWidth - backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, -Math.PI / 2, 0, false )
      .lineTo( backgroundWidth, ( backgroundHeight / 2 ) + backgroundOverlap )
      .lineTo( 0, ( backgroundHeight / 2 ) + backgroundOverlap )
      .close() );
    upBackground.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // bottom half of the background, for "down". Shape computed starting at bottom-right, going clockwise.
    var downBackground = new Path( new Shape()
      .arc( backgroundWidth - backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, 0, Math.PI / 2, false )
      .arc( backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, Math.PI / 2, Math.PI, false )
      .lineTo( 0, backgroundHeight / 2 )
      .lineTo( backgroundWidth, backgroundHeight / 2 )
      .close() );
    downBackground.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // touch area for buttons
    upBackground.touchArea = Shape.rectangle(
      upBackground.left - ( options.touchAreaExpandX / 2 ), upBackground.top - options.touchAreaExpandY,
      upBackground.width + options.touchAreaExpandX, upBackground.height + options.touchAreaExpandY );
    downBackground.touchArea = Shape.rectangle(
      downBackground.left - ( options.touchAreaExpandX / 2 ), downBackground.top,
      downBackground.width + options.touchAreaExpandX, downBackground.height + options.touchAreaExpandY );

    // mouse area for buttons
    upBackground.mouseArea = Shape.rectangle(
      upBackground.left - ( options.mouseAreaExpandX / 2 ), upBackground.top - options.mouseAreaExpandY,
      upBackground.width + options.mouseAreaExpandX, upBackground.height + options.mouseAreaExpandY );
    downBackground.mouseArea = Shape.rectangle(
      downBackground.left - ( options.mouseAreaExpandX / 2 ), downBackground.top,
      downBackground.width + options.mouseAreaExpandX, downBackground.height + options.mouseAreaExpandY );

    // compute colors
    var arrowColors = {
      up: options.color,
      over: options.color,
      down: options.color.darkerColor(),
      out: options.color,
      disabled: 'rgb(176,176,176)'
    };
    var centerColor = 'white';
    var highlightGradient = createBackgroundGradient( options.color, centerColor, backgroundHeight );
    var pressedGradient = createBackgroundGradient( options.color.darkerColor(), centerColor, backgroundHeight );
    var backgroundColors = {
      up: 'white',
      over: highlightGradient,
      down: pressedGradient,
      out: pressedGradient,
      disabled: 'white'
    };

    // compute size of arrows
    var arrowButtonSize = new Dimension2( 0.5 * backgroundWidth, 0.1 * backgroundWidth );

    // 'up' arrow
    var arrowOptions = { fill: 'white', stroke: 'black', lineWidth: 0.25 };
    var upArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, 0 )
      .lineTo( arrowButtonSize.width, arrowButtonSize.height )
      .lineTo( 0, arrowButtonSize.height )
      .close();
    var upArrow = new Path( upArrowShape, arrowOptions );
    upArrow.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // 'down' arrow
    var downArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, arrowButtonSize.height )
      .lineTo( 0, 0 )
      .lineTo( arrowButtonSize.width, 0 )
      .close();
    var downArrow = new Path( downArrowShape, arrowOptions );
    downArrow.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // rendering order
    thisNode.addChild( upBackground );
    thisNode.addChild( downBackground );
    thisNode.addChild( upArrow );
    thisNode.addChild( downArrow );
    thisNode.addChild( valueNode );

    // layout, background nodes are already drawn in the local coordinate frame
    var ySpacing = 3;
    valueNode.x = 0;
    valueNode.centerY = backgroundHeight / 2;
    upArrow.centerX = upBackground.centerX;
    upArrow.bottom = upBackground.top - ySpacing;
    downArrow.centerX = downBackground.centerX;
    downArrow.top = downBackground.bottom + ySpacing;

    // Update text to match the value
    valueProperty.link( function( value ) {
      if ( value === null || value === undefined ) {
        valueNode.text = options.noValueString;
        valueNode.x = ( backgroundWidth - valueNode.width ) / 2; // horizontally centered
      }
      else {
        valueNode.text = Util.toFixed( value, options.decimalPlaces );
        valueNode.right = backgroundWidth - options.xMargin; // right aligned
      }
    } );

    // Update button colors
    var updateColors = function( stateProperty, enabledProperty, background, arrow ) {
      if ( enabledProperty.get() ) {
        arrow.stroke = 'black';
        var state = stateProperty.get();
        if ( state === 'up' ) {
          background.fill = backgroundColors.up;
          arrow.fill = arrowColors.up;
        }
        else if ( state === 'over' ) {
          background.fill = backgroundColors.over;
          arrow.fill = arrowColors.over;
        }
        else if ( state === 'down' ) {
          background.fill = backgroundColors.down;
          arrow.fill = arrowColors.down;
        }
        else if ( state === 'out' ) {
          background.fill = backgroundColors.out;
          arrow.fill = arrowColors.out;
        }
        else {
          throw new Error( 'unsupported state: ' + state );
        }
      }
      else {
        background.fill = backgroundColors.disabled;
        arrow.fill = arrowColors.disabled;
        arrow.stroke = arrowColors.disabled; // stroke so that arrow size will look the same when it's enabled/disabled
      }
    };
    var updateUpColors = function() {
      updateColors( upStateProperty, upEnabledProperty, upBackground, upArrow );
    };
    var updateDownColors = function() {
      updateColors( downStateProperty, downEnabledProperty, downBackground, downArrow );
    };
    upStateProperty.link( updateUpColors.bind( thisNode ) );
    upEnabledProperty.link( updateUpColors.bind( thisNode ) );
    downStateProperty.link( updateDownColors.bind( thisNode ) );
    downEnabledProperty.link( updateDownColors.bind( thisNode ) );

    thisNode.mutate( options );
  }

  return inherit( Node, NumberPicker );
} );
<commit_msg>allow colors to be specified as either {Color|String} in scenery-phet.NumberPicker<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * User-interface component for picking a number value from a range.
 * This is essentially a value with integrated up/down spinners.
 * But PhET has been calling it a 'picker', so that's what this type is named.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Color = require( 'SCENERY/util/Color' );
  var DerivedProperty = require( 'AXON/DerivedProperty' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Timer = require( 'JOIST/Timer' );
  var Util = require( 'DOT/Util' );

  // creates a vertical gradient where with color1 at the top and bottom, color2 in the center
  var createBackgroundGradient = function( color1, color2, height ) {
    return new LinearGradient( 0, 0, 0, height )
      .addColorStop( 0, color1 )
      .addColorStop( 0.5, color2 )
      .addColorStop( 1, color1 );
  };

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<String>} stateProperty 'up'|'over'|'down'|'out'
   * @param {Property<Boolean>} enabledProperty
   * @param {Function} fireFunction
   * @param {Number} timerDelay start to fire continuously after pressing for this long (milliseconds)
   * @param {Number} intervalDelay // fire continuously at this frequency (milliseconds)
   * @constructor
   */
  function PickerListener( stateProperty, enabledProperty, fireFunction, timerDelay, intervalDelay ) {

    // stuff related to press-&-hold feature
    var fired = false;
    var timeoutID = null;
    var intervalID = null;
    var cleanupTimer = function() {
      if ( timeoutID ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
    };

    ButtonListener.call( this, {

      up: function() {
        stateProperty.set( 'up' );
        cleanupTimer();
      },

      over: function() {
        stateProperty.set( 'over' );
      },

      down: function() {
        if ( intervalID === null ) {
          stateProperty.set( 'down' );
          fired = false;
          timeoutID = Timer.setTimeout( function() {
            timeoutID = null;
            fired = true;
            intervalID = Timer.setInterval( function() {
              if ( enabledProperty.get() ) {
                fireFunction();
              }
            }, intervalDelay );
          }, timerDelay );
        }
      },

      out: function() {
        stateProperty.set( 'out' );
      },

      fire: function() {
        cleanupTimer();
        if ( !fired && enabledProperty.get() ) {
          fireFunction();
        }
      }
    } );
  }

  inherit( ButtonListener, PickerListener );

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<Number>} valueProperty
   * @param {Property<Range>} rangeProperty
   * @param {*} options
   * @constructor
   */
  function NumberPicker( valueProperty, rangeProperty, options ) {

    options = _.extend( {
      color: new Color( 0, 0, 255 ), // {Color|String}
      cornerRadius: 6,
      xMargin: 3,
      yMargin: 3,
      decimalPlaces: 0,
      font: new PhetFont( 24 ),
      upFunction: function() { return valueProperty.get() + 1; },
      downFunction: function() { return valueProperty.get() - 1; },
      timerDelay: 400, // start to fire continuously after pressing for this long (milliseconds)
      intervalDelay: 100, // fire continuously at this frequency (milliseconds),
      noValueString: '-', // string to display if valueProperty.get is null or undefined
      touchAreaExpandX: 10,
      touchAreaExpandY: 20,
      mouseAreaExpandX: 0,
      mouseAreaExpandY: 14
    }, options );

    var thisNode = this;
    Node.call( thisNode, { cursor: 'pointer' } );

    // properties for the "up" (increment) control
    var upStateProperty = new Property( 'up' );
    var upEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value < range.max );
    } );

    // properties for the "down" (decrement) control
    var downStateProperty = new Property( 'up' );
    var downEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value > range.min );
    } );

    // callbacks for changing the value
    var fireUp = function() {
      valueProperty.set( Math.min( options.upFunction(), rangeProperty.get().max ) );
    };
    var fireDown = function() {
      valueProperty.set( Math.max( options.downFunction(), rangeProperty.get().min ) );
    };

    // displays the value
    var valueNode = new Text( "", { font: options.font, pickable: false } );

    // compute max width of text based on value range
    valueNode.text = Util.toFixed( rangeProperty.get().min, options.decimalPlaces );
    var maxWidth = valueNode.width;
    valueNode.text = Util.toFixed( rangeProperty.get().max, options.decimalPlaces );
    maxWidth = Math.max( maxWidth, valueNode.width );

    // compute shape of the background behind the numeric value
    var backgroundWidth = maxWidth + ( 2 * options.xMargin );
    var backgroundHeight = valueNode.height + ( 2 * options.yMargin );
    var backgroundOverlap = 1;
    var backgroundCornerRadius = options.cornerRadius;

    // top half of the background, for "up". Shape computed starting at upper-left, going clockwise.
    var upBackground = new Path( new Shape()
      .arc( backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, Math.PI, Math.PI * 3 / 2, false )
      .arc( backgroundWidth - backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, -Math.PI / 2, 0, false )
      .lineTo( backgroundWidth, ( backgroundHeight / 2 ) + backgroundOverlap )
      .lineTo( 0, ( backgroundHeight / 2 ) + backgroundOverlap )
      .close() );
    upBackground.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // bottom half of the background, for "down". Shape computed starting at bottom-right, going clockwise.
    var downBackground = new Path( new Shape()
      .arc( backgroundWidth - backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, 0, Math.PI / 2, false )
      .arc( backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, Math.PI / 2, Math.PI, false )
      .lineTo( 0, backgroundHeight / 2 )
      .lineTo( backgroundWidth, backgroundHeight / 2 )
      .close() );
    downBackground.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // touch area for buttons
    upBackground.touchArea = Shape.rectangle(
      upBackground.left - ( options.touchAreaExpandX / 2 ), upBackground.top - options.touchAreaExpandY,
      upBackground.width + options.touchAreaExpandX, upBackground.height + options.touchAreaExpandY );
    downBackground.touchArea = Shape.rectangle(
      downBackground.left - ( options.touchAreaExpandX / 2 ), downBackground.top,
      downBackground.width + options.touchAreaExpandX, downBackground.height + options.touchAreaExpandY );

    // mouse area for buttons
    upBackground.mouseArea = Shape.rectangle(
      upBackground.left - ( options.mouseAreaExpandX / 2 ), upBackground.top - options.mouseAreaExpandY,
      upBackground.width + options.mouseAreaExpandX, upBackground.height + options.mouseAreaExpandY );
    downBackground.mouseArea = Shape.rectangle(
      downBackground.left - ( options.mouseAreaExpandX / 2 ), downBackground.top,
      downBackground.width + options.mouseAreaExpandX, downBackground.height + options.mouseAreaExpandY );

    // compute colors
    var baseColor = Color.toColor( options.color );
    var arrowColors = {
      up: baseColor,
      over: baseColor,
      down: baseColor.darkerColor(),
      out: baseColor,
      disabled: 'rgb(176,176,176)'
    };
    var centerColor = 'white';
    var highlightGradient = createBackgroundGradient( baseColor, centerColor, backgroundHeight );
    var pressedGradient = createBackgroundGradient( baseColor.darkerColor(), centerColor, backgroundHeight );
    var backgroundColors = {
      up: 'white',
      over: highlightGradient,
      down: pressedGradient,
      out: pressedGradient,
      disabled: 'white'
    };

    // compute size of arrows
    var arrowButtonSize = new Dimension2( 0.5 * backgroundWidth, 0.1 * backgroundWidth );

    // 'up' arrow
    var arrowOptions = { fill: 'white', stroke: 'black', lineWidth: 0.25 };
    var upArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, 0 )
      .lineTo( arrowButtonSize.width, arrowButtonSize.height )
      .lineTo( 0, arrowButtonSize.height )
      .close();
    var upArrow = new Path( upArrowShape, arrowOptions );
    upArrow.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // 'down' arrow
    var downArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, arrowButtonSize.height )
      .lineTo( 0, 0 )
      .lineTo( arrowButtonSize.width, 0 )
      .close();
    var downArrow = new Path( downArrowShape, arrowOptions );
    downArrow.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // rendering order
    thisNode.addChild( upBackground );
    thisNode.addChild( downBackground );
    thisNode.addChild( upArrow );
    thisNode.addChild( downArrow );
    thisNode.addChild( valueNode );

    // layout, background nodes are already drawn in the local coordinate frame
    var ySpacing = 3;
    valueNode.x = 0;
    valueNode.centerY = backgroundHeight / 2;
    upArrow.centerX = upBackground.centerX;
    upArrow.bottom = upBackground.top - ySpacing;
    downArrow.centerX = downBackground.centerX;
    downArrow.top = downBackground.bottom + ySpacing;

    // Update text to match the value
    valueProperty.link( function( value ) {
      if ( value === null || value === undefined ) {
        valueNode.text = options.noValueString;
        valueNode.x = ( backgroundWidth - valueNode.width ) / 2; // horizontally centered
      }
      else {
        valueNode.text = Util.toFixed( value, options.decimalPlaces );
        valueNode.right = backgroundWidth - options.xMargin; // right aligned
      }
    } );

    // Update button colors
    var updateColors = function( stateProperty, enabledProperty, background, arrow ) {
      if ( enabledProperty.get() ) {
        arrow.stroke = 'black';
        var state = stateProperty.get();
        if ( state === 'up' ) {
          background.fill = backgroundColors.up;
          arrow.fill = arrowColors.up;
        }
        else if ( state === 'over' ) {
          background.fill = backgroundColors.over;
          arrow.fill = arrowColors.over;
        }
        else if ( state === 'down' ) {
          background.fill = backgroundColors.down;
          arrow.fill = arrowColors.down;
        }
        else if ( state === 'out' ) {
          background.fill = backgroundColors.out;
          arrow.fill = arrowColors.out;
        }
        else {
          throw new Error( 'unsupported state: ' + state );
        }
      }
      else {
        background.fill = backgroundColors.disabled;
        arrow.fill = arrowColors.disabled;
        arrow.stroke = arrowColors.disabled; // stroke so that arrow size will look the same when it's enabled/disabled
      }
    };
    var updateUpColors = function() {
      updateColors( upStateProperty, upEnabledProperty, upBackground, upArrow );
    };
    var updateDownColors = function() {
      updateColors( downStateProperty, downEnabledProperty, downBackground, downArrow );
    };
    upStateProperty.link( updateUpColors.bind( thisNode ) );
    upEnabledProperty.link( updateUpColors.bind( thisNode ) );
    downStateProperty.link( updateDownColors.bind( thisNode ) );
    downEnabledProperty.link( updateDownColors.bind( thisNode ) );

    thisNode.mutate( options );
  }

  return inherit( Node, NumberPicker );
} );
<|endoftext|>


<commit_before>'use strict';

/**
 * Module Dependences
 */

var passportConf  = require('../config/passport');
var Docker        = require('dockerode');
var docker        = new Docker({ host: '127.0.0.1', port: 4243 });

/**
 * Docker Controller
 */

module.exports.controller = function (app) {

 /**
   * GET /docker*
   * *ALL* docker routes must be authenticated first
   */

  app.all('/docker*', passportConf.isAuthenticated);

  /**
   * GET /images
   * JSON images api
   */

  app.get('/docker/images', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    docker.listImages(function (err, images) {
      if (err) {
        return (err, null);
      }
      res.json(images);
    });
  });

  /**
   * GET /dockercontainers
   * JSON containers api
   */

  app.get('/docker/containers', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    docker.listContainers({ all:true }, function (err, containers) {
      if (err) {
        return (err, null);
      }
      res.json(containers);
    });
  });

  /**
   * DEL /dockerdeleteimages/:name
   * JSON docker image delete api
   */

  app.delete('/docker/deleteimage', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Name of image to be removed
    var name = req.body.name;

    // Get the image
    var image = docker.getImage(name);

    // Remove image
    image.remove(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });

  });

  /**
   * GET /dockerstartcontainer
   * JSON startcontainer api
   */

  app.post('/docker/createcontainer', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Dictionay for Volumes
    var volumes = {};
    if (req.body.volume !== '') {
      var mountPoint = req.body.volume.split(':')[1];
      volumes[mountPoint] = {};
    }
    else {
      volumes = null;
    }

    // Create container options
    var createOpts = {
      Image:req.body.name,
      Cmd:req.body.cmd.split(' '),
      Tty:req.body.tty,
      // If tty is true open streams
      AttachStderr:(req.body.tty === 'true') ? true : false,
      AttachStdin:(req.body.tty === 'true') ? true : false,
      AttachStdout:(req.body.tty === 'true') ? true : false,
      OpenStdin:(req.body.tty === 'true') ? true : false,
      StdinOnce:(req.body.tty === 'true') ? true : false,
      Volumes:volumes
    };

    // Dictionary for out Ports
    var ports = {};
    ports[req.body.containerPort] = [{ HostPort:req.body.hostPort }];

    // Start container options
    var startOpts = {
      PortBindings: ports,
      Binds:(req.body.volume === '') ? null : [req.body.volume]
    };

    docker.createContainer(createOpts, function (err, container) {
      container.start(startOpts, function (err, data) {
        res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
      });
    });
  });

  /**
   * POST /dockerstartcontainer/:id
   * JSON start docker container api
   */

  app.post('/docker/startcontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be started
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Start Container
    container.start(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });
  });

  /**
   * POST /dockerstopcontainer/:id
   * JSON stop docker container api
   */

  app.post('/docker/stopcontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be stopped
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Restart Container
    container.stop(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });
  });

  /**
   * DEL /dockerremovecontainer/:id
   * JSON remove docker container api
   */

  app.delete('/docker/removecontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be restarted
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Remove Container
    container.remove(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: String(err) });
    });
  });

  /**
   * POST /dockerrestartcontainer/:id
   * JSON restart docker container api
   */

  app.post('/dockerrestartcontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be restarted
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Restart Container
    container.restart(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });
  });
};
<commit_msg>Added API for Inspect Container<commit_after>'use strict';

/**
 * Module Dependences
 */

var passportConf  = require('../config/passport');
var Docker        = require('dockerode');
var docker        = new Docker({ host: '127.0.0.1', port: 4243 });

/**
 * Docker Controller
 */

module.exports.controller = function (app) {

 /**
   * GET /docker*
   * *ALL* docker routes must be authenticated first
   */

  app.all('/docker*', passportConf.isAuthenticated);

  /**
   * GET /images
   * JSON images api
   */

  app.get('/docker/images', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    docker.listImages(function (err, images) {
      if (err) {
        return (err, null);
      }
      res.json(images);
    });
  });

  /**
   * GET /dockercontainers
   * JSON containers api
   */

  app.get('/docker/containers', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    docker.listContainers({ all:true }, function (err, containers) {
      if (err) {
        return (err, null);
      }
      res.json(containers);
    });
  });

  /**
   * DEL /dockerdeleteimages/:name
   * JSON docker image delete api
   */

  app.delete('/docker/deleteimage', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Name of image to be removed
    var name = req.body.name;

    // Get the image
    var image = docker.getImage(name);

    // Remove image
    image.remove(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });

  });

  /**
   * GET /dockerstartcontainer
   * JSON startcontainer api
   */

  app.post('/docker/createcontainer', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Dictionay for Volumes
    var volumes = {};
    if (req.body.volume !== '') {
      var mountPoint = req.body.volume.split(':')[1];
      volumes[mountPoint] = {};
    }
    else {
      volumes = null;
    }

    // Create container options
    var createOpts = {
      Image:req.body.image,
      Cmd:req.body.cmd.split(' '),
      name:req.body.name,
      Tty:req.body.tty,
      // If tty is true open streams
      AttachStderr:(req.body.tty === 'true') ? true : false,
      AttachStdin:(req.body.tty === 'true') ? true : false,
      AttachStdout:(req.body.tty === 'true') ? true : false,
      OpenStdin:(req.body.tty === 'true') ? true : false,
      StdinOnce:(req.body.tty === 'true') ? true : false,
      Volumes:volumes,
      WorkingDir:mountPoint
    };

    // Dictionary for out Ports
    var ports = {};
    ports[req.body.containerPort] = [{ HostPort:req.body.hostPort }];

    // Start container options
    var startOpts = {
      PortBindings: ports,
      Binds:(req.body.volume === '') ? null : [req.body.volume]
    };

    docker.createContainer(createOpts, function (err, container) {
      container.start(startOpts, function (err, data) {
        res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
      });
    });
  });

  /**
   * GET /dockerinspectcontainer/:id
   * JSON inspect docker container api
   */

  app.get('/docker/inspectcontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be started
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Inpsect Container
    container.inspect(function (err, container) {
      if (err) {
        return (err, null);
      }
      res.json(container);
    });
  });

  /**
   * POST /dockerstartcontainer/:id
   * JSON start docker container api
   */

  app.post('/docker/startcontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be started
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Start Container
    container.start(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });
  });

  /**
   * POST /dockerstopcontainer/:id
   * JSON stop docker container api
   */

  app.post('/docker/stopcontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be stopped
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Restart Container
    container.stop(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });
  });

  /**
   * DEL /dockerremovecontainer/:id
   * JSON remove docker container api
   */

  app.delete('/docker/removecontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be restarted
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Remove Container
    container.remove(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: String(err) });
    });
  });

  /**
   * POST /dockerrestartcontainer/:id
   * JSON restart docker container api
   */

  app.post('/dockerrestartcontainer/:id', passportConf.isAuthenticated, passportConf.isAdministrator, function (req, res) {
    // Id of container to be restarted
    var id = req.params.id;

    // Get the container
    var container = docker.getContainer(id);

    // Restart Container
    container.restart(function (err, result) {
      res.send((err === null) ? { msg: '' } : { msg: 'error' + err });
    });
  });
};
<|endoftext|>


<commit_before>#!/bin/env node

var express         = require('express');
var bodyParser      = require('body-parser');
var cookieParser    = require('cookie-parser');
var methodOverride  = require('method-override');
var morgan          = require('morgan');
var config          = require('./config/config');

var server = module.exports = this;

server.setupNetworkConfig = function() {
  server.port = config.server.port;
  server.ip = config.server.ip;
};

server.setExpressApp = function(){
  server.express = express();
  server.express.use(bodyParser.json());       // get information from html forms
  server.express.use(methodOverride());        // simulate DELETE and PUT
  server.express.use(cookieParser());          // read cookies (needed for auth)
  server.express.use(morgan('dev'));
};

server.setRoutes = function(){
  require('./controllers/index.js')(server.express);
};

server.terminatorHandler = function(signal){
  if (typeof signal === 'string') {
    console.log('%s: Received %s - terminating app ...', Date(Date.now()), signal);
    process.exit(1);
  }
  console.log('%s: Node server stopped.', Date(Date.now()) );
};

//  Process on exit and signals.
server.setupTerminationHandlers = function(){
  process.on('exit', function() { server.terminatorHandler(); });
  var termSignals = ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGBUS', 'SIGFPE', 'SIGUSR1',
  'SIGSEGV', 'SIGUSR2', 'SIGTERM'];
  termSignals.forEach(function(element) {
    process.on(element, function() { server.terminatorHandler(element); });
  });
};

server.setup = function() {
  server.setupNetworkConfig();
  server.setExpressApp();
  server.setRoutes();
  server.setupTerminationHandlers();
};

server.start = function() {
  server.express.listen(server.port, server.ipaddress, function() {
    console.log('%s: Node server started on %s:%d ...', Date(Date.now()), server.ip, server.port);
  });
};

server.setup();
server.start();
<commit_msg>Removing unnecesary function<commit_after>#!/bin/env node

var express         = require('express');
var bodyParser      = require('body-parser');
var cookieParser    = require('cookie-parser');
var methodOverride  = require('method-override');
var morgan          = require('morgan');
var config          = require('./config/config');

var server = module.exports = this;

server.setupNetworkConfig = function() {
  server.port = config.server.port;
  server.ip = config.server.ip;
};

server.setExpressApp = function(){
  server.express = express();
  server.express.use(bodyParser.json());       // get information from html forms
  server.express.use(methodOverride());        // simulate DELETE and PUT
  server.express.use(cookieParser());          // read cookies (needed for auth)
  server.express.use(morgan('dev'));
};

server.setRoutes = function(){
  require('./controllers/index.js')(server.express);
};

server.terminatorHandler = function(signal){
  if (typeof signal === 'string') {
    console.log('%s: Received %s - terminating app ...', Date(Date.now()), signal);
    process.exit(1);
  }
  console.log('%s: Node server stopped.', Date(Date.now()) );
};

//  Process on exit and signals.
server.setupTerminationHandlers = function(){
  process.on('exit', server.terminatorHandler);
  var termSignals = ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGBUS', 'SIGFPE', 'SIGUSR1',
  'SIGSEGV', 'SIGUSR2', 'SIGTERM'];
  termSignals.forEach(function(element) {
    process.on(element, function() { server.terminatorHandler(element); });
  });
};

server.setup = function() {
  server.setupNetworkConfig();
  server.setExpressApp();
  server.setRoutes();
  server.setupTerminationHandlers();
};

server.start = function() {
  server.express.listen(server.port, server.ipaddress, function() {
    console.log('%s: Node server started on %s:%d ...', Date(Date.now()), server.ip, server.port);
  });
};

server.setup();
server.start();
<|endoftext|>


<commit_before>import React from 'react';
import express from 'express';
import exphbs  from 'express-handlebars';
import { match, RouterContext } from 'react-router';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import routes from './app/routes';
import chalk from 'chalk';
import configureStore from './app/store/configureStore';
import compression from 'compression';
import helmet from 'helmet';
import hpp from 'hpp';
import morgan from 'morgan';
import fs from 'fs';
import unirest from 'unirest';
import cheerio from 'cheerio';
import schedule from 'node-schedule';

const app = express();
const port = 5000;
const jsPath = '/assets/js';

// Using helmet to secure Express with various HTTP headers
app.use(helmet());
// Prevent HTTP parameter pollution.
app.use(hpp());
// Compress all requests
app.use(compression());
// Use morgan for http request debug (only show error)
app.use(morgan('dev', { skip: (req, res) => res.statusCode < 400 }));

app.use(express.static('./public'));
app.use(jsPath, express.static('./dist/client'));

app.engine('hbs', exphbs({extname: '.hbs'}));
app.set('view engine', 'hbs');

let pluginSiteVersion = null;
let pluginSiteApiVersion = null;

const downloadHeader = () => {
  var headerFile = __HEADER_FILE__;
  if (headerFile !== null && headerFile !== undefined) {
    console.info(`Downloading header file from '${headerFile}'`);
    unirest.get(headerFile).end((response) => {
      if (response.statusCode == 200) {
        var $ = cheerio.load(response.body, { decodeEntities: false });
        $('img, script').each(function() {
          var src = $(this).attr('src');
          if (src !== undefined && src.startsWith('/')) {
            $(this).attr('src', 'https://jenkins.io' + src);
          }
        });
        $('a, link').each(function() {
          var href = $(this).attr('href');
          if (href !== undefined && href.startsWith('/')) {
            $(this).attr('href', 'https://jenkins.io' + href);
          }
        });
        $('head').prepend('{{> header }}');
        $('head').append('');
        $('#grid-box').append('{{{rendered}}}');
        $('#grid-box').after('');
        if (fs.existsSync('./git.json')) {
          try {
            var versions = JSON.parse(fs.readFileSync('./git.json'));
            pluginSiteVersion = versions["plugin-site"];
            pluginSiteApiVersion = versions["plugin-site-api"];
            if (pluginSiteVersion !== undefined && pluginSiteApiVersion !== undefined) {
              $('#creativecommons').append('{{> version }}');
            }
          } catch (err) {
            console.error(chalk.red("Problem processing git.json"), err);
          }
        }
        fs.writeFileSync('./views/index.hbs', $.html());
      } else {
        console.error(response.statusCode);
        console.error(error);
      }
    });
  } else {
    console.info("HEADER_FILE environment variable null");
  }
}

downloadHeader();

app.get('*', (req, res, next) => {
  match({ routes: routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (!renderProps) {
      res.sendStatus(404);
    } else {
      const store = configureStore();
      const { location, params, history } = renderProps;
      const promises = renderProps.components
        .filter(component => component.fetchData)
        // Should the component have a static method `fetchData({ store, location, params, history })` then
        // call it.
        .map(component => component.fetchData({ store, location, params, history }));

      Promise.all(promises).then(() => {
        const rendered = renderToString(

            <Provider store={store}>
              <RouterContext {...renderProps} />
            </Provider>

        );
        const finalState = JSON.stringify(store.getState()).replace(/</g, '\\x3c');
        const pluginNotFound = req.url !== '/' && store.getState().ui.plugin === null;
        res.status(pluginNotFound ? 404 : 200).render('index', {
          rendered: rendered,
          reduxState: finalState,
          jsPath: jsPath,
          pluginSiteVersion,
          pluginSiteApiVersion
        });
      }).catch((err) => {
        console.error(chalk.red(error));
        res.sendStatus(404);
      });
    }
  });
});

app.listen(port, (error) => {
  if (error) {
    console.error(chalk.red(error)); // eslint-disable-line no-console
  } else {
    console.info(chalk.green(`==>  Listening on port ${port}`)); // eslint-disable-line no-console
    schedule.scheduleJob('*/15 * * * *', () => {
      downloadHeader();
    });
  }
});
<commit_msg>[WEBSITE-293] - Fix CSS import<commit_after>import React from 'react';
import express from 'express';
import exphbs  from 'express-handlebars';
import { match, RouterContext } from 'react-router';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import routes from './app/routes';
import chalk from 'chalk';
import configureStore from './app/store/configureStore';
import compression from 'compression';
import helmet from 'helmet';
import hpp from 'hpp';
import morgan from 'morgan';
import fs from 'fs';
import unirest from 'unirest';
import cheerio from 'cheerio';
import schedule from 'node-schedule';

const app = express();
const port = 5000;
const jsPath = '/assets/js';

// Using helmet to secure Express with various HTTP headers
app.use(helmet());
// Prevent HTTP parameter pollution.
app.use(hpp());
// Compress all requests
app.use(compression());
// Use morgan for http request debug (only show error)
app.use(morgan('dev', { skip: (req, res) => res.statusCode < 400 }));

app.use(express.static('./public'));
app.use(jsPath, express.static('./dist/client'));

app.engine('hbs', exphbs({extname: '.hbs'}));
app.set('view engine', 'hbs');

let pluginSiteVersion = null;
let pluginSiteApiVersion = null;

const downloadHeader = () => {
  var headerFile = __HEADER_FILE__;
  if (headerFile !== null && headerFile !== undefined) {
    console.info(`Downloading header file from '${headerFile}'`);
    unirest.get(headerFile).end((response) => {
      if (response.statusCode == 200) {
        var $ = cheerio.load(response.body, { decodeEntities: false });
        $('img, script').each(function() {
          var src = $(this).attr('src');
          if (src !== undefined && src.startsWith('/')) {
            $(this).attr('src', 'https://jenkins.io' + src);
          }
        });
        $('a, link').each(function() {
          var href = $(this).attr('href');
          if (href !== undefined && href.startsWith('/')) {
            $(this).attr('href', 'https://jenkins.io' + href);
          }
        });
        $('head').prepend('{{> header }}');
        // Even though we're supplying our own this one still causes a conflict.
        $('link[href="https://jenkins.io/css/font-icons.css"]').remove();
        $('head').append('');
        $('#grid-box').append('{{{rendered}}}');
        $('#grid-box').after('');
        if (fs.existsSync('./git.json')) {
          try {
            var versions = JSON.parse(fs.readFileSync('./git.json'));
            pluginSiteVersion = versions["plugin-site"];
            pluginSiteApiVersion = versions["plugin-site-api"];
            if (pluginSiteVersion !== undefined && pluginSiteApiVersion !== undefined) {
              $('#creativecommons').append('{{> version }}');
            }
          } catch (err) {
            console.error(chalk.red("Problem processing git.json"), err);
          }
        }
        fs.writeFileSync('./views/index.hbs', $.html());
      } else {
        console.error(response.statusCode);
        console.error(error);
      }
    });
  } else {
    console.info("HEADER_FILE environment variable null");
  }
}

downloadHeader();

app.get('*', (req, res, next) => {
  match({ routes: routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (!renderProps) {
      res.sendStatus(404);
    } else {
      const store = configureStore();
      const { location, params, history } = renderProps;
      const promises = renderProps.components
        .filter(component => component.fetchData)
        // Should the component have a static method `fetchData({ store, location, params, history })` then
        // call it.
        .map(component => component.fetchData({ store, location, params, history }));

      Promise.all(promises).then(() => {
        const rendered = renderToString(

            <Provider store={store}>
              <RouterContext {...renderProps} />
            </Provider>

        );
        const finalState = JSON.stringify(store.getState()).replace(/</g, '\\x3c');
        const pluginNotFound = req.url !== '/' && store.getState().ui.plugin === null;
        res.status(pluginNotFound ? 404 : 200).render('index', {
          rendered: rendered,
          reduxState: finalState,
          jsPath: jsPath,
          pluginSiteVersion,
          pluginSiteApiVersion
        });
      }).catch((err) => {
        console.error(chalk.red(error));
        res.sendStatus(404);
      });
    }
  });
});

app.listen(port, (error) => {
  if (error) {
    console.error(chalk.red(error)); // eslint-disable-line no-console
  } else {
    console.info(chalk.green(`==>  Listening on port ${port}`)); // eslint-disable-line no-console
    schedule.scheduleJob('*/15 * * * *', () => {
      downloadHeader();
    });
  }
});
<|endoftext|>


<commit_before>var express = require('express');
var app = express();
var Chat = require('./models/chat')
var bodyParser = require('body-parser')

app.use(bodyParser.json())

app.post('/api/chats', function (req, res) {
    var chat = new Chat(req.body)
    chat.save(function(err,doc){
        res.send(201,doc)
    })
});

module.exports = app
<commit_msg>Fixed res.send deprecated method in express<commit_after>var express = require('express');
var app = express();
var Chat = require('./models/chat')
var bodyParser = require('body-parser')

app.use(bodyParser.json())

app.post('/api/chats', function (req, res) {
    var chat = new Chat(req.body)
    chat.save(function(err,doc){
        res.status(201).send(doc)
    })
});

module.exports = app
<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * User-interface component for picking a number value from a range.
 * This is essentially a value with integrated up/down spinners.
 * But PhET has been calling it a 'picker', so that's what this type is named.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // imports
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Color = require( 'SCENERY/util/Color' );
  var DerivedProperty = require( 'AXON/DerivedProperty' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Timer = require( 'JOIST/Timer' );
  var Util = require( 'DOT/Util' );

  // creates a vertical gradient where with color1 at the top and bottom, color2 in the center
  var createBackgroundGradient = function( color1, color2, height ) {
    return new LinearGradient( 0, 0, 0, height )
      .addColorStop( 0, color1 )
      .addColorStop( 0.5, color2 )
      .addColorStop( 1, color1 );
  };

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<String>} stateProperty 'up'|'over'|'down'|'out'
   * @param {Property<Boolean>} enabledProperty
   * @param {Function} fireFunction
   * @param {Number} timerDelay start to fire continuously after pressing for this long (milliseconds)
   * @param {Number} intervalDelay // fire continuously at this frequency (milliseconds)
   * @constructor
   */
  function PickerListener( stateProperty, enabledProperty, fireFunction, timerDelay, intervalDelay ) {

    // stuff related to press-&-hold feature
    var fired = false;
    var timeoutID = null;
    var intervalID = null;
    var cleanupTimer = function() {
      if ( timeoutID ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
    };

    ButtonListener.call( this, {

      up: function() {
        stateProperty.set( 'up' );
        cleanupTimer();
      },

      over: function() {
        stateProperty.set( 'over' );
      },

      down: function() {
        stateProperty.set( 'down' );
        fired = false;
        timeoutID = Timer.setTimeout( function() {
          timeoutID = null;
          fired = true;
          intervalID = Timer.setInterval( function() {
            if ( enabledProperty.get() ) {
              fireFunction();
            }
          }, intervalDelay );
        }, timerDelay );
      },

      out: function() {
        stateProperty.set( 'out' );
      },

      fire: function() {
        cleanupTimer();
        if ( !fired && enabledProperty.get() ) {
          fireFunction();
        }
      }
    } );
  }

  inherit( ButtonListener, PickerListener );

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<Number>} valueProperty
   * @param {Property<Range>} rangeProperty
   * @param {*} options
   * @constructor
   */
  function NumberPicker( valueProperty, rangeProperty, options ) {

    options = _.extend( {
      color: new Color( 0, 0, 255 ), // must be a Color, not a CSS string
      cornerRadius: 6,
      xMargin: 3,
      yMargin: 3,
      decimalPlaces: 0,
      font: new PhetFont( 24 ),
      upFunction: function() { return valueProperty.get() + 1; },
      downFunction: function() { return valueProperty.get() - 1; },
      timerDelay: 400, // start to fire continuously after pressing for this long (milliseconds)
      intervalDelay: 100, // fire continuously at this frequency (milliseconds),
      noValueString: '-' // string to display if valueProperty.get is null or undefined
    }, options );

    var thisNode = this;
    Node.call( thisNode, { cursor: 'pointer' } );

    // properties for the "up" (increment) control
    var upStateProperty = new Property( 'up' );
    var upEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value < range.max );
    } );

    // properties for the "down" (decrement) control
    var downStateProperty = new Property( 'up' );
    var downEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value > range.min );
    } );

    // callbacks for changing the value
    var fireUp = function() {
      valueProperty.set( options.upFunction() );
    };
    var fireDown = function() {
      valueProperty.set( options.downFunction() );
    };

    // displays the value
    var valueNode = new Text( "", { font: options.font, pickable: false } );

    // compute max width of text based on value range
    valueNode.text = Util.toFixed( rangeProperty.get().min, options.decimalPlaces );
    var maxWidth = valueNode.width;
    valueNode.text = Util.toFixed( rangeProperty.get().max, options.decimalPlaces );
    maxWidth = Math.max( maxWidth, valueNode.width );

    // compute shape of the background behind the numeric value
    var backgroundWidth = maxWidth + ( 2 * options.xMargin );
    var backgroundHeight = valueNode.height + ( 2 * options.yMargin );
    var backgroundOverlap = 1;
    var backgroundCornerRadius = options.cornerRadius;

    // top half of the background, for "up". Shape computed starting at upper-left, going clockwise.
    var upBackground = new Path( new Shape()
      .arc( backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, Math.PI, Math.PI * 3 / 2, false )
      .arc( backgroundWidth - backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, -Math.PI / 2, 0, false )
      .lineTo( backgroundWidth, ( backgroundHeight / 2 ) + backgroundOverlap )
      .lineTo( 0, ( backgroundHeight / 2 ) + backgroundOverlap )
      .close() );
    upBackground.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // bottom half of the background, for "down". Shape computed starting at bottom-right, going clockwise.
    var downBackground = new Path( new Shape()
      .arc( backgroundWidth - backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, 0, Math.PI / 2, false )
      .arc( backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, Math.PI / 2, Math.PI, false )
      .lineTo( 0, backgroundHeight / 2 )
      .lineTo( backgroundWidth, backgroundHeight / 2 )
      .close() );
    downBackground.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // compute colors
    var arrowColors = {
      up: options.color,
      over: options.color,
      down: options.color.darkerColor(),
      out: options.color,
      disabled: 'rgb(176,176,176)'
    };
    var centerColor = 'white';
    var highlightGradient = createBackgroundGradient( options.color, centerColor, backgroundHeight );
    var pressedGradient = createBackgroundGradient( options.color.darkerColor(), centerColor, backgroundHeight );
    var backgroundColors = {
      up: 'white',
      over: highlightGradient,
      down: pressedGradient,
      out: pressedGradient,
      disabled: 'white'
    };

    // compute size of arrows
    var arrowButtonSize = new Dimension2( 0.5 * backgroundWidth, 0.1 * backgroundWidth );

    // 'up' arrow
    var arrowOptions = { fill: 'white', stroke: 'black', lineWidth: 0.25 };
    var upArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, 0 )
      .lineTo( arrowButtonSize.width, arrowButtonSize.height )
      .lineTo( 0, arrowButtonSize.height )
      .close();
    var upArrow = new Path( upArrowShape, arrowOptions );
    upArrow.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // 'down' arrow
    var downArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, arrowButtonSize.height )
      .lineTo( 0, 0 )
      .lineTo( arrowButtonSize.width, 0 )
      .close();
    var downArrow = new Path( downArrowShape, arrowOptions );
    downArrow.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // rendering order
    thisNode.addChild( upBackground );
    thisNode.addChild( downBackground );
    thisNode.addChild( upArrow );
    thisNode.addChild( downArrow );
    thisNode.addChild( valueNode );

    // layout, background nodes are already drawn in the local coordinate frame
    var ySpacing = 3;
    valueNode.x = 0;
    valueNode.centerY = backgroundHeight / 2;
    upArrow.centerX = upBackground.centerX;
    upArrow.bottom = upBackground.top - ySpacing;
    downArrow.centerX = downBackground.centerX;
    downArrow.top = downBackground.bottom + ySpacing;

    // Update text to match the value
    valueProperty.link( function( value ) {
      if ( value === null || value === undefined ) {
        valueNode.text = options.noValueString;
        valueNode.x = ( backgroundWidth - valueNode.width ) / 2; // horizontally centered
      }
      else {
        valueNode.text = Util.toFixed( value, options.decimalPlaces );
        valueNode.right = backgroundWidth - options.xMargin; // right aligned
      }
    } );

    // Update button colors
    var updateColors = function( stateProperty, enabledProperty, background, arrow ) {
      if ( enabledProperty.get() ) {
        arrow.stroke = 'black';
        var state = stateProperty.get();
        if ( state === 'up' ) {
          background.fill = backgroundColors.up;
          arrow.fill = arrowColors.up;
        }
        else if ( state === 'over' ) {
          background.fill = backgroundColors.over;
          arrow.fill = arrowColors.over;
        }
        else if ( state === 'down' ) {
          background.fill = backgroundColors.down;
          arrow.fill = arrowColors.down;
        }
        else if ( state === 'out' ) {
          background.fill = backgroundColors.out;
          arrow.fill = arrowColors.out;
        }
        else {
          throw new Error( 'unsupported state: ' + state );
        }
      }
      else {
        background.fill = backgroundColors.disabled;
        arrow.fill = arrowColors.disabled;
        arrow.stroke = arrowColors.disabled; // stroke so that arrow size will look the same when it's enabled/disabled
      }
    };
    var updateUpColors = function() {
      updateColors( upStateProperty, upEnabledProperty, upBackground, upArrow );
    };
    var updateDownColors = function() {
      updateColors( downStateProperty, downEnabledProperty, downBackground, downArrow );
    };
    upStateProperty.link( updateUpColors.bind( thisNode ) );
    upEnabledProperty.link( updateUpColors.bind( thisNode ) );
    downStateProperty.link( updateDownColors.bind( thisNode ) );
    downEnabledProperty.link( updateDownColors.bind( thisNode ) );

    thisNode.mutate( options );
  }

  return inherit( Node, NumberPicker );
} );
<commit_msg>add option for expanding touch area of picker<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * User-interface component for picking a number value from a range.
 * This is essentially a value with integrated up/down spinners.
 * But PhET has been calling it a 'picker', so that's what this type is named.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // imports
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Color = require( 'SCENERY/util/Color' );
  var DerivedProperty = require( 'AXON/DerivedProperty' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Timer = require( 'JOIST/Timer' );
  var Util = require( 'DOT/Util' );

  // creates a vertical gradient where with color1 at the top and bottom, color2 in the center
  var createBackgroundGradient = function( color1, color2, height ) {
    return new LinearGradient( 0, 0, 0, height )
      .addColorStop( 0, color1 )
      .addColorStop( 0.5, color2 )
      .addColorStop( 1, color1 );
  };

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<String>} stateProperty 'up'|'over'|'down'|'out'
   * @param {Property<Boolean>} enabledProperty
   * @param {Function} fireFunction
   * @param {Number} timerDelay start to fire continuously after pressing for this long (milliseconds)
   * @param {Number} intervalDelay // fire continuously at this frequency (milliseconds)
   * @constructor
   */
  function PickerListener( stateProperty, enabledProperty, fireFunction, timerDelay, intervalDelay ) {

    // stuff related to press-&-hold feature
    var fired = false;
    var timeoutID = null;
    var intervalID = null;
    var cleanupTimer = function() {
      if ( timeoutID ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
    };

    ButtonListener.call( this, {

      up: function() {
        stateProperty.set( 'up' );
        cleanupTimer();
      },

      over: function() {
        stateProperty.set( 'over' );
      },

      down: function() {
        stateProperty.set( 'down' );
        fired = false;
        timeoutID = Timer.setTimeout( function() {
          timeoutID = null;
          fired = true;
          intervalID = Timer.setInterval( function() {
            if ( enabledProperty.get() ) {
              fireFunction();
            }
          }, intervalDelay );
        }, timerDelay );
      },

      out: function() {
        stateProperty.set( 'out' );
      },

      fire: function() {
        cleanupTimer();
        if ( !fired && enabledProperty.get() ) {
          fireFunction();
        }
      }
    } );
  }

  inherit( ButtonListener, PickerListener );

  //-------------------------------------------------------------------------------------------

  /**
   * @param {Property<Number>} valueProperty
   * @param {Property<Range>} rangeProperty
   * @param {*} options
   * @constructor
   */
  function NumberPicker( valueProperty, rangeProperty, options ) {

    options = _.extend( {
      color: new Color( 0, 0, 255 ), // must be a Color, not a CSS string
      cornerRadius: 6,
      xMargin: 3,
      yMargin: 3,
      decimalPlaces: 0,
      font: new PhetFont( 24 ),
      upFunction: function() { return valueProperty.get() + 1; },
      downFunction: function() { return valueProperty.get() - 1; },
      timerDelay: 400, // start to fire continuously after pressing for this long (milliseconds)
      intervalDelay: 100, // fire continuously at this frequency (milliseconds),
      noValueString: '-', // string to display if valueProperty.get is null or undefined
      touchAreaExpandX: 10,
      touchAreaExpandY: 12
    }, options );

    var thisNode = this;
    Node.call( thisNode, { cursor: 'pointer' } );

    // properties for the "up" (increment) control
    var upStateProperty = new Property( 'up' );
    var upEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value < range.max );
    } );

    // properties for the "down" (decrement) control
    var downStateProperty = new Property( 'up' );
    var downEnabledProperty = new DerivedProperty( [ valueProperty, rangeProperty ], function( value, range ) {
      return ( value !== null && value !== undefined && value > range.min );
    } );

    // callbacks for changing the value
    var fireUp = function() {
      valueProperty.set( options.upFunction() );
    };
    var fireDown = function() {
      valueProperty.set( options.downFunction() );
    };

    // displays the value
    var valueNode = new Text( "", { font: options.font, pickable: false } );

    // compute max width of text based on value range
    valueNode.text = Util.toFixed( rangeProperty.get().min, options.decimalPlaces );
    var maxWidth = valueNode.width;
    valueNode.text = Util.toFixed( rangeProperty.get().max, options.decimalPlaces );
    maxWidth = Math.max( maxWidth, valueNode.width );

    // compute shape of the background behind the numeric value
    var backgroundWidth = maxWidth + ( 2 * options.xMargin );
    var backgroundHeight = valueNode.height + ( 2 * options.yMargin );
    var backgroundOverlap = 1;
    var backgroundCornerRadius = options.cornerRadius;

    // top half of the background, for "up". Shape computed starting at upper-left, going clockwise.
    var upBackground = new Path( new Shape()
      .arc( backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, Math.PI, Math.PI * 3 / 2, false )
      .arc( backgroundWidth - backgroundCornerRadius, backgroundCornerRadius, backgroundCornerRadius, -Math.PI / 2, 0, false )
      .lineTo( backgroundWidth, ( backgroundHeight / 2 ) + backgroundOverlap )
      .lineTo( 0, ( backgroundHeight / 2 ) + backgroundOverlap )
      .close() );
    upBackground.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // bottom half of the background, for "down". Shape computed starting at bottom-right, going clockwise.
    var downBackground = new Path( new Shape()
      .arc( backgroundWidth - backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, 0, Math.PI / 2, false )
      .arc( backgroundCornerRadius, backgroundHeight - backgroundCornerRadius, backgroundCornerRadius, Math.PI / 2, Math.PI, false )
      .lineTo( 0, backgroundHeight / 2 )
      .lineTo( backgroundWidth, backgroundHeight / 2 )
      .close() );
    downBackground.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // expand touch area for buttons
    upBackground.touchArea = Shape.rectangle(
      upBackground.left - ( options.touchAreaExpandX / 2 ), upBackground.top - options.touchAreaExpandY,
      upBackground.width + options.touchAreaExpandX, upBackground.height + options.touchAreaExpandY );
    downBackground.touchArea = Shape.rectangle(
      downBackground.left - ( options.touchAreaExpandX / 2 ), downBackground.top,
      downBackground.width + options.touchAreaExpandX, downBackground.height + options.touchAreaExpandY );

    // compute colors
    var arrowColors = {
      up: options.color,
      over: options.color,
      down: options.color.darkerColor(),
      out: options.color,
      disabled: 'rgb(176,176,176)'
    };
    var centerColor = 'white';
    var highlightGradient = createBackgroundGradient( options.color, centerColor, backgroundHeight );
    var pressedGradient = createBackgroundGradient( options.color.darkerColor(), centerColor, backgroundHeight );
    var backgroundColors = {
      up: 'white',
      over: highlightGradient,
      down: pressedGradient,
      out: pressedGradient,
      disabled: 'white'
    };

    // compute size of arrows
    var arrowButtonSize = new Dimension2( 0.5 * backgroundWidth, 0.1 * backgroundWidth );

    // 'up' arrow
    var arrowOptions = { fill: 'white', stroke: 'black', lineWidth: 0.25 };
    var upArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, 0 )
      .lineTo( arrowButtonSize.width, arrowButtonSize.height )
      .lineTo( 0, arrowButtonSize.height )
      .close();
    var upArrow = new Path( upArrowShape, arrowOptions );
    upArrow.addInputListener( new PickerListener( upStateProperty, upEnabledProperty, fireUp, options.timerDelay, options.intervalDelay ) );

    // 'down' arrow
    var downArrowShape = new Shape()
      .moveTo( arrowButtonSize.width / 2, arrowButtonSize.height )
      .lineTo( 0, 0 )
      .lineTo( arrowButtonSize.width, 0 )
      .close();
    var downArrow = new Path( downArrowShape, arrowOptions );
    downArrow.addInputListener( new PickerListener( downStateProperty, downEnabledProperty, fireDown, options.timerDelay, options.intervalDelay ) );

    // rendering order
    thisNode.addChild( upBackground );
    thisNode.addChild( downBackground );
    thisNode.addChild( upArrow );
    thisNode.addChild( downArrow );
    thisNode.addChild( valueNode );

    // layout, background nodes are already drawn in the local coordinate frame
    var ySpacing = 3;
    valueNode.x = 0;
    valueNode.centerY = backgroundHeight / 2;
    upArrow.centerX = upBackground.centerX;
    upArrow.bottom = upBackground.top - ySpacing;
    downArrow.centerX = downBackground.centerX;
    downArrow.top = downBackground.bottom + ySpacing;

    // Update text to match the value
    valueProperty.link( function( value ) {
      if ( value === null || value === undefined ) {
        valueNode.text = options.noValueString;
        valueNode.x = ( backgroundWidth - valueNode.width ) / 2; // horizontally centered
      }
      else {
        valueNode.text = Util.toFixed( value, options.decimalPlaces );
        valueNode.right = backgroundWidth - options.xMargin; // right aligned
      }
    } );

    // Update button colors
    var updateColors = function( stateProperty, enabledProperty, background, arrow ) {
      if ( enabledProperty.get() ) {
        arrow.stroke = 'black';
        var state = stateProperty.get();
        if ( state === 'up' ) {
          background.fill = backgroundColors.up;
          arrow.fill = arrowColors.up;
        }
        else if ( state === 'over' ) {
          background.fill = backgroundColors.over;
          arrow.fill = arrowColors.over;
        }
        else if ( state === 'down' ) {
          background.fill = backgroundColors.down;
          arrow.fill = arrowColors.down;
        }
        else if ( state === 'out' ) {
          background.fill = backgroundColors.out;
          arrow.fill = arrowColors.out;
        }
        else {
          throw new Error( 'unsupported state: ' + state );
        }
      }
      else {
        background.fill = backgroundColors.disabled;
        arrow.fill = arrowColors.disabled;
        arrow.stroke = arrowColors.disabled; // stroke so that arrow size will look the same when it's enabled/disabled
      }
    };
    var updateUpColors = function() {
      updateColors( upStateProperty, upEnabledProperty, upBackground, upArrow );
    };
    var updateDownColors = function() {
      updateColors( downStateProperty, downEnabledProperty, downBackground, downArrow );
    };
    upStateProperty.link( updateUpColors.bind( thisNode ) );
    upEnabledProperty.link( updateUpColors.bind( thisNode ) );
    downStateProperty.link( updateDownColors.bind( thisNode ) );
    downEnabledProperty.link( updateDownColors.bind( thisNode ) );

    thisNode.mutate( options );
  }

  return inherit( Node, NumberPicker );
} );
<|endoftext|>