diff --git a/js/PhetButton.js b/js/PhetButton.js index b05af9fd..06bb00c6 100644 --- a/js/PhetButton.js +++ b/js/PhetButton.js @@ -10,12 +10,9 @@ define( function( require ) { var Node = require( 'SCENERY/nodes/Node' ); var Image = require( 'SCENERY/nodes/Image' ); var FontAwesomeNode = require( 'SUN/FontAwesomeNode' ); - var Plane = require( 'SCENERY/nodes/Plane' ); var inherit = require( 'PHET_CORE/inherit' ); var PhetMenu = require( 'JOIST/PhetMenu' ); var Shape = require( 'KITE/Shape' ); - var ButtonListener = require( 'SCENERY/input/ButtonListener' ); - var Vector2 = require( 'DOT/Vector2' ); var PushButtonDeprecated = require( 'SUN/PushButtonDeprecated' ); var HighlightNode = require( 'JOIST/HighlightNode' ); @@ -35,7 +32,6 @@ define( function( require ) { */ function PhetButton( sim, whiteColorScheme, homeScreen, options ) { - var phetButton = this; options = _.extend( { phetLogo: whiteColorScheme ? phetLogoDarker : phetLogo, phetLogoScale: 0.28, @@ -71,33 +67,25 @@ define( function( require ) { //When the phet button is pressed, show the phet menu var phetButtonPressed = function() { - //The PhetMenu can be embedded in different contexts, but the scale should be consistent. So look up the embedding scale here and factor it out. See #39 - var ancestor = homeScreen ? phetButton.parents[0] : phetButton.parents[0].parents[0]; - var scale = ancestor.getGlobalToLocalMatrix().getScaleVector().x; - - var global = phetButton.parentToGlobalPoint( phetButton.center ); - var local = ancestor.globalToLocalPoint( global ); var phetMenu = new PhetMenu( sim, { showSaveAndLoad: sim.options.showSaveAndLoad, - scale: scale, - right: phetButton.globalToParentPoint( new Vector2( phetButton.globalBounds.maxX, 0 ) ).x, - bottom: local.y} ); - - var rectangle = new Plane( {fill: 'black', opacity: 0.3, renderer: 'svg'} ); - var detach = function() { - rectangle.detach(); - phetMenu.detach(); - phetMenu.removeInputListener( popupMenuListener ); - rectangle.removeInputListener( rectangleListener ); - }; - var popupMenuListener = new ButtonListener( {fire: detach} ); - var rectangleListener = {down: detach}; - - phetMenu.addInputListener( popupMenuListener ); - rectangle.addInputListener( rectangleListener ); - - ancestor.addChild( rectangle ); - ancestor.addChild( phetMenu ); + closeCallback: function() { + // hides the popup and barrier background + sim.hidePopup( phetMenu ); + } + } ); + function onResize( bounds, screenBounds, scale ) { + // because it starts at null + if ( bounds ) { + phetMenu.setScaleMagnitude( Math.max( 1, scale * 0.7 ) ); // minimum size for small devices + phetMenu.right = bounds.right - 10 * scale; + phetMenu.bottom = ( bounds.bottom + screenBounds.bottom ) / 2; + } + } + sim.on( 'resized', onResize ); + onResize( sim.bounds, sim.screenBounds, sim.scale ); + + sim.showPopup( phetMenu ); }; this.addListener( phetButtonPressed ); diff --git a/js/PhetMenu.js b/js/PhetMenu.js index 4d2c5dba..f7030f73 100644 --- a/js/PhetMenu.js +++ b/js/PhetMenu.js @@ -39,7 +39,7 @@ define( function( require ) { var HIGHLIGHT_COLOR = '#a6d2f4'; // Creates a menu item that highlights and fires. - var createMenuItem = function( text, width, height, separatorBefore, callback, immediateCallback ) { + var createMenuItem = function( text, width, height, separatorBefore, closeCallback, callback, immediateCallback ) { var X_MARGIN = 5; var Y_MARGIN = 3; @@ -60,7 +60,10 @@ define( function( require ) { exit: function() { highlight.fill = null; }, upImmediate: function() { immediateCallback && immediateCallback(); } } ); - menuItem.addInputListener( new ButtonListener( {fire: callback } ) ); + menuItem.addInputListener( new ButtonListener( {fire: function( event ) { + callback( event ); + closeCallback( event ); + } } ) ); menuItem.separatorBefore = separatorBefore; @@ -109,12 +112,11 @@ define( function( require ) { var showAboutDialog = function( aboutDialog ) { var plane = new Plane( {fill: 'black', opacity: 0.3, renderer: 'svg'} );//Renderer must be specified here because the plane is added directly to the scene (instead of to some other node that already has svg renderer) - sim.addChild( plane ); - sim.addChild( aboutDialog ); + sim.showPopup( aboutDialog ); var aboutDialogListener = {up: function() { aboutDialog.removeInputListener( aboutDialogListener ); plane.addInputListener( aboutDialogListener ); - aboutDialog.detach(); + sim.hidePopup( aboutDialog ); plane.detach(); }}; aboutDialog.addInputListener( aboutDialogListener ); @@ -238,7 +240,7 @@ define( function( require ) { // Create the menu items. var items = _.map( keepItemDescriptors, function( itemDescriptor ) { - return createMenuItem( itemDescriptor.text, maxTextWidth, maxTextHeight, itemDescriptor.separatorBefore, itemDescriptor.callback, itemDescriptor.immediateCallback ); + return createMenuItem( itemDescriptor.text, maxTextWidth, maxTextHeight, itemDescriptor.separatorBefore, options.closeCallback, itemDescriptor.callback, itemDescriptor.immediateCallback ); } ); var separatorWidth = _.max( items, function( item ) {return item.width;} ).width; var itemHeight = _.max( items, function( item ) {return item.height;} ).height; diff --git a/js/Sim.js b/js/Sim.js index 1397b272..f2877975 100644 --- a/js/Sim.js +++ b/js/Sim.js @@ -10,19 +10,24 @@ define( function( require ) { 'use strict'; + var inherit = require( 'PHET_CORE/inherit' ); var Util = require( 'SCENERY/util/Util' ); var NavigationBar = require( 'JOIST/NavigationBar' ); var HomeScreen = require( 'JOIST/HomeScreen' ); var Scene = require( 'SCENERY/Scene' ); + var ButtonListener = require( 'SCENERY/input/ButtonListener' ); var Vector2 = require( 'DOT/Vector2' ); var Bounds2 = require( 'DOT/Bounds2' ); var version = require( 'version' ); var PropertySet = require( 'AXON/PropertySet' ); var Property = require( 'AXON/Property' ); + var ObservableArray = require( 'AXON/ObservableArray' ); var platform = require( 'PHET_CORE/platform' ); var Timer = require( 'JOIST/Timer' ); var SimJSON = require( 'JOIST/SimJSON' ); var Path = require( 'SCENERY/nodes/Path' ); + var Rectangle = require( 'SCENERY/nodes/Rectangle' ); + var Node = require( 'SCENERY/nodes/Node' ); var Color = require( 'SCENERY/util/Color' ); var Shape = require( 'KITE/Shape' ); var Profiler = require( 'JOIST/Profiler' ); @@ -32,9 +37,18 @@ define( function( require ) { * @param {Screen[]} screens * @param {Object} [options] * @constructor + * + * Events: + * - resized( bounds, screenBounds, scale ): Fires when the sim is resized. */ function Sim( name, screens, options ) { + PropertySet.call( this, { + scale: 1, // [read-only] how the home screen and navbar are scaled + bounds: null, // global bounds for the entire simulation + screenBounds: null // global bounds for the screen-specific part (excludes the navigation bar) + } ); + assert && assert( window.phetJoistSimLauncher, 'Sim must be launched using SimLauncher, see https://github.com/phetsims/joist/issues/142' ); options = _.extend( { @@ -236,7 +250,7 @@ define( function( require ) { window.setInterval( function() { sleep( Math.ceil( 100 + Math.random() * 200 ) ); }, Math.ceil( 100 + Math.random() * 200 ) ); }; - var whiteNavBar = new Color( screens[0].backgroundColor ).equals( Color.BLACK ); + var whiteNavBar = !!new Color( screens[0].backgroundColor ).equals( Color.BLACK ); sim.navigationBar = new NavigationBar( sim, screens, sim.simModel, whiteNavBar ); // Multi-screen sims get a home screen. @@ -360,6 +374,28 @@ define( function( require ) { } ); } + // layer for popups, dialogs, and their backgrounds and barriers + this.topLayer = new Node( { renderer: 'svg' } ); + sim.scene.addChild( this.topLayer ); + + // Semi-transparent black barrier used to block input events when a dialog (or other popup) is present, and fade + // out the background. + this.barrierStack = new ObservableArray(); + this.barrierRectangle = new Rectangle( 0, 0, 1, 1, 0, 0, { + fill:'rgba(0,0,0,0.3)', + pickable: true + } ); + this.topLayer.addChild( this.barrierRectangle ); + this.barrierStack.lengthProperty.link( function( numBarriers ) { + sim.barrierRectangle.visible = numBarriers > 0; + } ); + this.barrierRectangle.addInputListener( new ButtonListener( { + fire: function() { + assert && assert( sim.barrierStack.length > 0 ); + sim.hidePopup( sim.barrierStack.get( sim.barrierStack.length - 1 ) ); + } + } ) ); + updateBackground(); //Fit to the window and render the initial scene @@ -367,8 +403,29 @@ define( function( require ) { sim.resizeToWindow(); } - Sim.prototype = { - constructor: Sim, + return inherit( PropertySet, Sim, { + /* + * Adds a popup in the global coordinate frame, and displays a semi-transparent black input barrier behind it. + * Use hidePopup() to remove it. + * @param {Node} node + */ + showPopup: function( node ) { + assert && assert( node ); + + this.barrierStack.push( node ); + this.topLayer.addChild( node ); + }, + + /* + * Hides a popup that was previously displayed with showPopup() + * @param {Node} node + */ + hidePopup: function( node ) { + assert && assert( node && this.barrierStack.contains( node ) ); + + this.barrierStack.remove( node ); + this.topLayer.removeChild( node ); + }, resizeToWindow: function() { this.resize( window.innerWidth, window.innerHeight ); @@ -380,14 +437,19 @@ define( function( require ) { //Use Mobile Safari layout bounds to size the home screen and navigation bar var scale = Math.min( width / 768, height / 504 ); + this.barrierRectangle.rectWidth = width; + this.barrierRectangle.rectHeight = height; + //40 px high on Mobile Safari var navBarHeight = scale * 40; sim.navigationBar.layout( scale, width, navBarHeight, height ); sim.navigationBar.y = height - navBarHeight; sim.scene.resize( width, height ); + var screenHeight = height - sim.navigationBar.height; + //Layout each of the screens - _.each( sim.screens, function( m ) { m.view.layout( width, height - sim.navigationBar.height ); } ); + _.each( sim.screens, function( m ) { m.view.layout( width, screenHeight ); } ); if ( sim.homeScreen ) { sim.homeScreen.layoutWithScale( scale, width, height ); @@ -400,6 +462,13 @@ define( function( require ) { if ( platform.mobileSafari ) { window.scrollTo( 0, 0 ); } + + // update our scale and bounds properties after other changes (so listeners can be fired after screens are resized) + this.scale = scale; + this.bounds = new Bounds2( 0, 0, width, height ); + this.screenBounds = new Bounds2( 0, 0, width, screenHeight ); + + this.trigger( 'resized', this.bounds, this.screenBounds, this.scale ); }, start: function() { @@ -582,10 +651,6 @@ define( function( require ) { })(); }, - addChild: function( node ) { - this.scene.addChild( node ); - }, - // A string that should be evaluated as JavaScript containing an array of "frame" objects, with a dt and an optional fireEvents function getRecordedInputEventLogString: function() { return '[\n' + _.map( this.inputEventLog, function( item ) { @@ -710,7 +775,5 @@ define( function( require ) { } this.simModel.set( state.simModel ); } - }; - - return Sim; + } ); } );