From 1e1bf328164a6d89720100e37a8feb587f19afc5 Mon Sep 17 00:00:00 2001 From: Jonathan Olson Date: Wed, 24 Mar 2021 20:11:26 -0600 Subject: [PATCH] Track pointer capture events for working around when pointer capture doesn't work (e.g. Chrome outside of the iframe), see https://github.com/phetsims/scenery/issues/1186 Adding interrupt to SimpleDragHandler pointer listener, see https://github.com/phetsims/scenery/issues/1186 Adding pointer ID to mouseDown and mouseDownAction, see https://github.com/phetsims/natural-selection/issues/264 Oops fix --- js/input/BatchedDOMEvent.js | 7 ++- js/input/BrowserEvents.js | 36 ++++++++++++- js/input/Input.js | 87 ++++++++++++++++++++++++++++--- js/input/InputFuzzer.js | 2 +- js/input/Pointer.js | 27 ++++++++++ js/input/SimpleDragHandler.js | 5 ++ js/listeners/ListenerTestUtils.js | 2 +- 7 files changed, 155 insertions(+), 11 deletions(-) diff --git a/js/input/BatchedDOMEvent.js b/js/input/BatchedDOMEvent.js index 1c881cf20..1187c8c29 100644 --- a/js/input/BatchedDOMEvent.js +++ b/js/input/BatchedDOMEvent.js @@ -75,7 +75,12 @@ define( require => { } } else if ( this.type === BatchedDOMEvent.MOUSE_TYPE ) { - callback.call( input, input.pointFromEvent( domEvent ), domEvent ); + if ( callback === input.mouseDown ) { + callback.call( input, null, input.pointFromEvent( domEvent ), domEvent ); + } + else { + callback.call( input, input.pointFromEvent( domEvent ), domEvent ); + } } else if ( this.type === BatchedDOMEvent.WHEEL_TYPE ) { callback.call( input, domEvent ); diff --git a/js/input/BrowserEvents.js b/js/input/BrowserEvents.js index 4ee46c1cf..435ed9156 100644 --- a/js/input/BrowserEvents.js +++ b/js/input/BrowserEvents.js @@ -147,7 +147,9 @@ define( require => { 'pointermove', 'pointerover', 'pointerout', - 'pointercancel' + 'pointercancel', + 'gotpointercapture', + 'lostpointercapture' ], /** @@ -426,6 +428,38 @@ define( require => { sceneryLog && sceneryLog.OnInput && sceneryLog.pop(); }, + /** + * Listener for window's gotpointercapture event. + * @private + * + * @param {Event} domEvent + */ + ongotpointercapture: function ongotpointercapture( domEvent ) { + sceneryLog && sceneryLog.OnInput && sceneryLog.OnInput( 'gotpointercapture' ); + sceneryLog && sceneryLog.OnInput && sceneryLog.push(); + + // NOTE: Will be called without a proper 'this' reference. Do NOT rely on it here. + BrowserEvents.batchWindowEvent( domEvent, BatchedDOMEvent.POINTER_TYPE, 'gotPointerCapture', false ); + + sceneryLog && sceneryLog.OnInput && sceneryLog.pop(); + }, + + /** + * Listener for window's lostpointercapture event. + * @private + * + * @param {Event} domEvent + */ + onlostpointercapture: function onlostpointercapture( domEvent ) { + sceneryLog && sceneryLog.OnInput && sceneryLog.OnInput( 'lostpointercapture' ); + sceneryLog && sceneryLog.OnInput && sceneryLog.push(); + + // NOTE: Will be called without a proper 'this' reference. Do NOT rely on it here. + BrowserEvents.batchWindowEvent( domEvent, BatchedDOMEvent.POINTER_TYPE, 'lostPointerCapture', false ); + + sceneryLog && sceneryLog.OnInput && sceneryLog.pop(); + }, + /** * Listener for window's MSPointerDown event. * @private diff --git a/js/input/Input.js b/js/input/Input.js index 42c8e933f..e42832b37 100644 --- a/js/input/Input.js +++ b/js/input/Input.js @@ -172,6 +172,7 @@ define( require => { const KeyboardUtils = require( 'SCENERY/accessibility/KeyboardUtils' ); const merge = require( 'PHET_CORE/merge' ); const Mouse = require( 'SCENERY/input/Mouse' ); + const NullableIO = require( 'TANDEM/types/NullableIO' ); const NumberIO = require( 'TANDEM/types/NumberIO' ); const Pen = require( 'SCENERY/input/Pen' ); const platform = require( 'PHET_CORE/platform' ); @@ -277,6 +278,7 @@ define( require => { this.mouseUpAction = new Action( ( point, event ) => { if ( !this.mouse ) { this.initMouse(); } const pointChanged = this.mouse.up( point, event ); + this.mouse.id = null; this.upEvent( this.mouse, event, pointChanged ); }, { phetioPlayback: true, @@ -290,14 +292,16 @@ define( require => { } ); // @private {Action} - Emits to the PhET-iO data stream. - this.mouseDownAction = new Action( ( point, event ) => { + this.mouseDownAction = new Action( ( id, point, event ) => { if ( !this.mouse ) { this.initMouse(); } + this.mouse.id = id; const pointChanged = this.mouse.down( point, event ); this.downEvent( this.mouse, event, pointChanged ); }, { phetioPlayback: true, tandem: options.tandem.createTandem( 'mouseDownAction' ), parameters: [ + { name: 'id', phetioType: NullableIO( NumberIO ) }, { name: 'point', phetioType: Vector2IO }, { name: 'event', phetioType: EventIO } ], @@ -530,6 +534,44 @@ define( require => { phetioDocumentation: 'Emits when a pen is canceled' } ); + // @private {Action} - Emits to the PhET-iO data stream. + this.gotPointerCaptureAction = new Action( ( id, event ) => { + const pointer = this.findPointerById( id ); + + if ( pointer ) { + pointer.onGotPointerCapture(); + } + }, { + phetioPlayback: true, + tandem: options.tandem.createTandem( 'gotPointerCaptureAction' ), + parameters: [ + { name: 'id', phetioType: NumberIO }, + { name: 'event', phetioType: DOMEventIO } + ], + phetioEventType: EventType.USER, + phetioDocumentation: 'Emits when a pointer is captured (normally at the start of an interaction)', + phetioHighFrequency: true + } ); + + // @private {Action} - Emits to the PhET-iO data stream. + this.lostPointerCaptureAction = new Action( ( id, event ) => { + const pointer = this.findPointerById( id ); + + if ( pointer ) { + pointer.onLostPointerCapture(); + } + }, { + phetioPlayback: true, + tandem: options.tandem.createTandem( 'lostPointerCaptureAction' ), + parameters: [ + { name: 'id', phetioType: NumberIO }, + { name: 'event', phetioType: DOMEventIO } + ], + phetioEventType: EventType.USER, + phetioDocumentation: 'Emits when a pointer loses its capture (normally at the end of an interaction)', + phetioHighFrequency: true + } ); + // wire up accessibility listeners on the display's root accessible DOM element. if ( this.display._accessible ) { @@ -1040,13 +1082,14 @@ define( require => { * NOTE: This may also be called from the pointer event handler (pointerDown) or from things like fuzzing or * playback. The event may be "faked" for certain purposes. * + * @param {number|null} id * @param {Vector2} point * @param {Event} event */ - mouseDown( point, event ) { - sceneryLog && sceneryLog.Input && sceneryLog.Input( 'mouseDown(' + Input.debugText( point, event ) + ');' ); + mouseDown( id, point, event ) { + sceneryLog && sceneryLog.Input && sceneryLog.Input( 'mouseDown(' + id + ', ' + Input.debugText( point, event ) + ');' ); sceneryLog && sceneryLog.Input && sceneryLog.push(); - this.mouseDownAction.execute( point, event ); + this.mouseDownAction.execute( id, point, event ); sceneryLog && sceneryLog.Input && sceneryLog.pop(); } @@ -1291,8 +1334,7 @@ define( require => { switch( type ) { case 'mouse': // The actual event afterwards - this.mouseDown( point, event ); - this.mouse.id = id; + this.mouseDown( id, point, event ); break; case 'touch': this.touchStart( id, point, event ); @@ -1321,7 +1363,6 @@ define( require => { switch( type ) { case 'mouse': this.mouseUp( point, event ); - this.mouse.id = null; break; case 'touch': this.touchEnd( id, point, event ); @@ -1394,6 +1435,38 @@ define( require => { } } + /** + * Handles a gotpointercapture event, forwarding it to the proper logical event. + * @public (scenery-internal) + * + * @param {number} id + * @param {string} type + * @param {Vector2} point + * @param {Event} event + */ + gotPointerCapture( id, type, point, event ) { + sceneryLog && sceneryLog.Input && sceneryLog.Input( `gotPointerCapture('${id}',${Input.debugText( null, event )});` ); + sceneryLog && sceneryLog.Input && sceneryLog.push(); + this.gotPointerCaptureAction.execute( id, event ); + sceneryLog && sceneryLog.Input && sceneryLog.pop(); + } + + /** + * Handles a lostpointercapture event, forwarding it to the proper logical event. + * @public (scenery-internal) + * + * @param {number} id + * @param {string} type + * @param {Vector2} point + * @param {Event} event + */ + lostPointerCapture( id, type, point, event ) { + sceneryLog && sceneryLog.Input && sceneryLog.Input( `lostPointerCapture('${id}',${Input.debugText( null, event )});` ); + sceneryLog && sceneryLog.Input && sceneryLog.push(); + this.lostPointerCaptureAction.execute( id, event ); + sceneryLog && sceneryLog.Input && sceneryLog.pop(); + } + /** * Handles a pointerover event, forwarding it to the proper logical event. * @public (scenery-internal) diff --git a/js/input/InputFuzzer.js b/js/input/InputFuzzer.js index 7644c4cc7..5af18f2a4 100644 --- a/js/input/InputFuzzer.js +++ b/js/input/InputFuzzer.js @@ -280,7 +280,7 @@ define( require => { this.isMouseDown = false; } else { - this.display._input.mouseDown( this.mousePosition, domEvent ); + this.display._input.mouseDown( null, this.mousePosition, domEvent ); this.isMouseDown = true; } }, diff --git a/js/input/Pointer.js b/js/input/Pointer.js index a37d0caa9..9e8a6ec1a 100644 --- a/js/input/Pointer.js +++ b/js/input/Pointer.js @@ -96,6 +96,9 @@ define( require => { // certain behavior for the life of the listener. Other listeners can observe the Intent on the Pointer and // react accordingly this._intent = null; + + // @private {boolean} + this._pointerCaptured = false; } scenery.register( 'Pointer', Pointer ); @@ -315,6 +318,30 @@ define( require => { }, get intent() { return this.getIntent(); }, + /* + * This is called when a capture starts on this pointer. We request it on pointerstart, and if received, we should + * generally receive events outside the window. + * @public + */ + onGotPointerCapture() { + this._pointerCaptured = true; + }, + + /** + * This is called when a capture ends on this pointer. This happens normally when the user releases the pointer above + * the sim or outside, but also in cases where we have NOT received an up/end. + * @public + * + * See https://github.com/phetsims/scenery/issues/1186 for more information. We'll want to interrupt the pointer + * on this case regardless, + */ + onLostPointerCapture() { + if ( this._pointerCaptured ) { + this.interruptAll(); + } + this._pointerCaptured = false; + }, + /** * Releases references so it can be garbage collected. * @public diff --git a/js/input/SimpleDragHandler.js b/js/input/SimpleDragHandler.js index 1ed56e32c..c49fb54d5 100644 --- a/js/input/SimpleDragHandler.js +++ b/js/input/SimpleDragHandler.js @@ -314,6 +314,11 @@ define( require => { // mouse/touch move move: function( event ) { self.dragAction.execute( event.pointer.point, event ); + }, + + // pointer interruption + interrupt: () => { + self.interrupt(); } }; PhetioObject.call( this, options ); diff --git a/js/listeners/ListenerTestUtils.js b/js/listeners/ListenerTestUtils.js index d520c45e9..107e84ccf 100644 --- a/js/listeners/ListenerTestUtils.js +++ b/js/listeners/ListenerTestUtils.js @@ -35,7 +35,7 @@ define( require => { null ); display._input.validatePointers(); - display._input.mouseDown( new Vector2( x, y ), domEvent ); + display._input.mouseDown( null, new Vector2( x, y ), domEvent ); }, /**