diff --git a/js/common/view/AbstractCoinTermNode.js b/js/common/view/AbstractCoinTermNode.js index 7e918497..bb907aff 100644 --- a/js/common/view/AbstractCoinTermNode.js +++ b/js/common/view/AbstractCoinTermNode.js @@ -31,8 +31,6 @@ define( function( require ) { * @constructor */ function AbstractCoinTermNode( coinTerm, options ) { - //REVIEW: Would highly recommend changing this to set more properties on the object and used methods. - // A 240+ line constructor with 12 functions that would be methods typically would be cleaner if broken up. options = _.extend( { addDragHandler: true, @@ -63,141 +61,242 @@ define( function( require ) { this.coinAndTextRootNode = new Node(); this.addChild( this.coinAndTextRootNode ); - // add a listener that will update the opacity based on the coin term's existence strength - function handleExistenceStrengthChanged( existenceStrength ) { - assert && assert( existenceStrength >= 0 && existenceStrength <= 1, 'existence strength must be between 0 and 1' ); - self.opacity = existenceStrength; - self.pickable = existenceStrength === 1; // prevent interaction with fading coin term - } - - coinTerm.existenceStrengthProperty.link( handleExistenceStrengthChanged ); + // add a listener that will adjust opacity as existence strength changes + var existenceStrengthListener = this.handleExistenceStrengthChanged.bind( this ); + coinTerm.existenceStrengthProperty.link( existenceStrengthListener ); - // timer that will be used to hide the break apart button if user doesn't use it - var hideButtonTimer = null; + // @private {function} - timer callback will be used to hide the break apart button if user doesn't use it + this.hideButtonTimer = null; - // Add the button that will allow composite coins to be decomposed. This is done outside of the root node so that - // it doesn't affect the bounds used in the model. - var breakApartButton = new BreakApartButton( { visible: false, mode: options.breakApartButtonMode } ); - this.addChild( breakApartButton ); + // @private {BreakApartButton} - the button that will allow composite coins to be decomposed, added outside of the + // root node so that it doesn't affect the bounds used in the model. + this.breakApartButton = new BreakApartButton( { visible: false, mode: options.breakApartButtonMode } ); + this.addChild( this.breakApartButton ); // adjust the touch area of the break apart button to make it easier to use on touch devices - breakApartButton.touchArea = breakApartButton.localBounds.dilatedX( breakApartButton.width / 2 ) - .withOffsets( 0, breakApartButton.height, 0, 0 ); - - // define helper functions for managing the button timers - function clearHideButtonTimer() { - if ( hideButtonTimer ) { - Timer.clearTimeout( hideButtonTimer ); - hideButtonTimer = null; - } - } - - function startHideButtonTimer() { - clearHideButtonTimer(); // just in case one is already running - hideButtonTimer = Timer.setTimeout( function() { - hideBreakApartButton(); - hideButtonTimer = null; - }, EESharedConstants.POPUP_BUTTON_SHOW_TIME * 1000 ); - } + this.breakApartButton.touchArea = this.breakApartButton.localBounds.dilatedX( this.breakApartButton.width / 2 ) + .withOffsets( 0, this.breakApartButton.height, 0, 0 ); // add the listener to the break apart button - breakApartButton.addListener( function() { + this.breakApartButton.addListener( function() { coinTerm.breakApart(); // hide the button after clicking - hideBreakApartButton(); + self.hideBreakApartButton(); // cancel timer if running - clearHideButtonTimer(); + self.clearHideButtonTimer(); } ); - // keep the button showing if the user is over it - function handleOverBreakApartButtonChanged( overButton ) { + // add a listener for changes to the 'break apart allowed' state + var breakApartButtonOverListener = this.handleOverBreakApartButtonChanged.bind( this ); + this.breakApartButton.buttonModel.overProperty.lazyLink( breakApartButtonOverListener ); - // make sure the coin terms isn't user controlled (this helps prevent some multi-touch problems) - if ( !coinTerm.userControlledProperty.get() ) { - if ( overButton ) { + // move this node as the model representation moves + function handlePositionChanged( position ) { - // the mouse just moved over the button, so stop the timer in order to make sure the button stays visible - assert && assert( !!hideButtonTimer, 'hide button timer should be running' ); - clearHideButtonTimer(); - } - else { + // the intent here is to position the center of the coin at the position, NOT the center of the node + self.translation = position; + } - // the mouse just moved away from the button, so start a timer to hide it - startHideButtonTimer(); - } - } + coinTerm.positionProperty.link( handlePositionChanged ); + + // add a listener for updating the break apart button state based on the user controlled state of this coin term + var userControlledListener = this.handleUserControlledChanged.bind( this ); + coinTerm.userControlledProperty.lazyLink( userControlledListener ); + + // add a listener to handle changes to the 'break apart allowed' state + var breakApartAllowedListener = this.handleBreakApartAllowedChanged.bind( this ); + coinTerm.breakApartAllowedProperty.link( breakApartAllowedListener ); + + // add a drag handler if specified + if ( options.addDragHandler ) { + this.addDragHandler( options.dragBounds ); } - breakApartButton.buttonModel.overProperty.lazyLink( handleOverBreakApartButtonChanged ); + // add a listener that will pop this node to the front when selected by the user + coinTerm.userControlledProperty.onValue( true, function() { self.moveToFront(); } ); - // define a function that will position and show the break apart button - function showBreakApartButton() { - breakApartButton.centerX = 0; - breakApartButton.bottom = self.coinAndTextRootNode.visibleLocalBounds.minY - 3; // just above the coin term - breakApartButton.visible = true; + // add a listener that will pop this node to the front when another coin term is combined with it + var totalCountListener = this.handleCombinedCountChanged.bind( this ); + coinTerm.totalCountProperty.link( totalCountListener ); + + // Add a listener that will make this node non-pickable when animating or when collected. Doing this when + // animating prevents a number of multi-touch issues. + function updatePickability() { + self.pickable = ( coinTerm.inProgressAnimationProperty.get() === null && !coinTerm.collectedProperty.get() ); } + coinTerm.inProgressAnimationProperty.link( updatePickability ); + coinTerm.collectedProperty.link( updatePickability ); + + // internal dispose function, reference in inherit block + this.disposeAbstractCoinTermNode = function() { + coinTerm.positionProperty.unlink( handlePositionChanged ); + coinTerm.existenceStrengthProperty.unlink( existenceStrengthListener ); + self.breakApartButton.buttonModel.overProperty.unlink( breakApartButtonOverListener ); + coinTerm.userControlledProperty.unlink( userControlledListener ); + coinTerm.breakApartAllowedProperty.unlink( breakApartAllowedListener ); + coinTerm.totalCountProperty.unlink( totalCountListener ); + coinTerm.inProgressAnimationProperty.unlink( updatePickability ); + }; + + this.mutate( options ); + } + + expressionExchange.register( 'AbstractCoinTermNode', AbstractCoinTermNode ); + + return inherit( Node, AbstractCoinTermNode, { + + // add a listener that will update the opacity based on the coin term's existence strength + /** + * listener function that will adjust opacity as existence strength changes + * @param existenceStrength + * @private + */ + handleExistenceStrengthChanged: function( existenceStrength ) { + assert && assert( existenceStrength >= 0 && existenceStrength <= 1, 'existence strength must be between 0 and 1' ); + this.opacity = existenceStrength; + this.pickable = existenceStrength === 1; // prevent interaction with fading coin term + }, + + /** + * @private + */ + clearHideButtonTimer: function() { + if ( this.hideButtonTimer ) { + Timer.clearTimeout( this.hideButtonTimer ); + this.hideButtonTimer = null; + } + }, + + /** + * start the timer for hiding the break-apart button + * @private + */ + startHideButtonTimer: function() { + var self = this; + this.clearHideButtonTimer(); // just in case one is already running + this.hideButtonTimer = Timer.setTimeout( function() { + self.hideBreakApartButton(); + self.hideButtonTimer = null; + }, EESharedConstants.POPUP_BUTTON_SHOW_TIME * 1000 ); + }, + + // define a function that will position and show the break apart button + /** + * position and show the break apart button + * @private + */ + showBreakApartButton: function() { + this.breakApartButton.centerX = 0; + this.breakApartButton.bottom = this.coinAndTextRootNode.visibleLocalBounds.minY - 3; // just above the coin term + this.breakApartButton.visible = true; + }, + // define a function that will position and hide the break apart button - function hideBreakApartButton() { - breakApartButton.center = Vector2.ZERO; // position within coin term so bounds aren't affected - breakApartButton.visible = false; - } + hideBreakApartButton: function() { + this.breakApartButton.center = Vector2.ZERO; // position within coin term so bounds aren't affected + this.breakApartButton.visible = false; + }, + + /** + * listener for the 'over' state of the break-apart button + * @param {boolean} overButton + * @private + */ + handleOverBreakApartButtonChanged: function( overButton ) { - // move this node as the model representation moves - function handlePositionChanged( position ) { - // the intent here is to position the center of the coin at the position, NOT the center of the node - self.translation = position; - } + // make sure the coin terms isn't user controlled (this helps prevent some multi-touch problems) + if ( !this.coinTerm.userControlledProperty.get() ) { + if ( overButton ) { - coinTerm.positionProperty.link( handlePositionChanged ); + // the mouse just moved over the button, so stop the timer in order to make sure the button stays visible + assert && assert( !!this.hideButtonTimer, 'hide button timer should be running' ); + this.clearHideButtonTimer(); + } + else { - // update the state of the break apart button when the userControlled state changes - function handleUserControlledChanged( userControlled ) { - if ( Math.abs( coinTerm.composition.length ) > 1 && coinTerm.breakApartAllowedProperty.get() ) { + // the mouse just moved away from the button, so start a timer to hide it + this.startHideButtonTimer(); + } + } + }, + + /** + * listener that updates the state of the break-apart button when the user controlled state of the coin term changes + * @param {boolean} userControlled + * @private + */ + handleUserControlledChanged: function( userControlled ) { + if ( Math.abs( this.coinTerm.composition.length ) > 1 && this.coinTerm.breakApartAllowedProperty.get() ) { if ( userControlled ) { - clearHideButtonTimer(); // called in case the timer was running - showBreakApartButton(); + this.clearHideButtonTimer(); // called in case the timer was running + this.showBreakApartButton(); } - else if ( breakApartButton.visible ) { + else if ( this.breakApartButton.visible ) { // the userControlled flag transitioned to false while the button was visible, start the time to hide it - startHideButtonTimer(); + this.startHideButtonTimer(); } } - } + }, + + /** + * listener that updates the state of the break-apart button when the breakApartAllowed state changes + * @param {boolean} breakApartAllowed + * @private + */ + handleBreakApartAllowedChanged: function( breakApartAllowed ) { + if ( this.breakApartButton.visible && !breakApartAllowed ) { + this.clearHideButtonTimer(); + this.hideBreakApartButton(); + } + }, + + /** + * listener for handling changes to the combined count (i.e. the number of coin terms combined together) + * @param {number} newCount + * @param {number} oldCount + */ + handleCombinedCountChanged: function( newCount, oldCount ) { + if ( newCount > oldCount ) { + this.moveToFront(); + } - coinTerm.userControlledProperty.lazyLink( handleUserControlledChanged ); + if ( this.breakApartButton.visible && Math.abs( newCount ) < 2 ) { - // hide the break apart button if break apart becomes disabled, generally if the coin term joins an expression - function handleBreakApartAllowedChanged( breakApartAllowed ) { - if ( breakApartButton.visible && !breakApartAllowed ) { - clearHideButtonTimer(); - hideBreakApartButton(); + // if combined count was reduced through cancellation while the break apart button was visible, hide it, see + // https://github.com/phetsims/expression-exchange/issues/29 + this.hideBreakApartButton(); } - } + }, - coinTerm.breakApartAllowedProperty.link( handleBreakApartAllowedChanged ); + /** + * add a drag handler + * {Bounds2} dragBounds + * @private + */ + addDragHandler: function( dragBounds ) { - if ( options.addDragHandler ) { + var self = this; // create a position property and link it to the coin term, necessary because coin term has both position and // destination properties, both of which must be set when dragging occurs - var coinTermPositionAndDestination = new Property( coinTerm.positionProperty.get() ); + var coinTermPositionAndDestination = new Property( this.coinTerm.positionProperty.get() ); coinTermPositionAndDestination.lazyLink( function( positionAndDestination ) { - coinTerm.setPositionAndDestination( positionAndDestination ); + self.coinTerm.setPositionAndDestination( positionAndDestination ); } ); - // @public - drag handler, public in support of even forwarding from creator nodes + // @public - drag handler, public in support of event forwarding from creator nodes this.dragHandler = new MovableDragHandler( coinTermPositionAndDestination, { // allow moving a finger (touch) across a node to pick it up allowTouchSnag: true, // bound the area where the coin terms can go - dragBounds: options.dragBounds, + dragBounds: dragBounds, // set the target node so that MovableDragHandler knows where to get the coordinate transform, supports event // forwarding @@ -211,72 +310,29 @@ define( function( require ) { coinTermPositionAndDestination.set( position.plusXY( 0, TOUCH_DRAG_Y_OFFSET ) ); } else { - coinTermPositionAndDestination.set( coinTerm.positionProperty.get() ); + coinTermPositionAndDestination.set( self.coinTerm.positionProperty.get() ); } - coinTerm.userControlledProperty.set( true ); + self.coinTerm.userControlledProperty.set( true ); }, endDrag: function() { - coinTerm.userControlledProperty.set( false ); + self.coinTerm.userControlledProperty.set( false ); } } ); // Add the listener that will allow the user to drag the coin around. This is added only to the node that // contains the term elements, not the button, so that the button won't affect userControlled or be draggable. this.coinAndTextRootNode.addInputListener( this.dragHandler ); - } - - // add a listener that will pop this node to the front when selected by the user - coinTerm.userControlledProperty.onValue( true, function() { self.moveToFront(); } ); - - // add a listener that will pop this node to the front when another coin term is combined with it - function handleCombinedCountChanged( newCount, oldCount ) { - if ( newCount > oldCount ) { - self.moveToFront(); - } + }, - if ( breakApartButton.visible && Math.abs( newCount ) < 2 ) { - - // if combined count was reduced through cancellation while the break apart button was visible, hide it, see - // https://github.com/phetsims/expression-exchange/issues/29 - hideBreakApartButton(); - } - } - - coinTerm.totalCountProperty.link( handleCombinedCountChanged ); - - // Add a listener that will make this node non-pickable when animating or when collected. Doing this when - // animating prevents a number of multi-touch issues. - function updatePickability() { - self.pickable = ( coinTerm.inProgressAnimationProperty.get() === null && !coinTerm.collectedProperty.get() ); - } - - coinTerm.inProgressAnimationProperty.link( updatePickability ); - coinTerm.collectedProperty.link( updatePickability ); - - // internal dispose function, reference in inherit block - this.disposeAbstractCoinTermNode = function() { - coinTerm.positionProperty.unlink( handlePositionChanged ); - coinTerm.existenceStrengthProperty.unlink( handleExistenceStrengthChanged ); - breakApartButton.buttonModel.overProperty.unlink( handleOverBreakApartButtonChanged ); - coinTerm.userControlledProperty.unlink( handleUserControlledChanged ); - coinTerm.breakApartAllowedProperty.unlink( handleBreakApartAllowedChanged ); - coinTerm.totalCountProperty.unlink( handleCombinedCountChanged ); - coinTerm.inProgressAnimationProperty.unlink( updatePickability ); - }; - - this.mutate( options ); - } - - expressionExchange.register( 'AbstractCoinTermNode', AbstractCoinTermNode ); - - return inherit( Node, AbstractCoinTermNode, { - - // @public + /** + * @public + */ dispose: function() { this.disposeAbstractCoinTermNode(); Node.prototype.dispose.call( this ); } + }, { // To look correct in equations, the text all needs to be on the same baseline. The value was empirically