Skip to content

Commit

Permalink
initial commit for RichDragListener, see phetsims/scenery#1614
Browse files Browse the repository at this point in the history
  • Loading branch information
jessegreenberg committed May 31, 2024
1 parent 5101f1c commit 3a2ea8b
Show file tree
Hide file tree
Showing 5 changed files with 442 additions and 38 deletions.
337 changes: 337 additions & 0 deletions js/RichDragListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
// Copyright 2024, University of Colorado Boulder

/**
* A drag listener that supports both pointer and keyboard input. It is composed with a RichPointerDragListener and a
* RichKeyboardDragListener to support pointer input, alternative input, sounds, and other PhET-specific features.
*
* Be sure to dispose of this listener when it is no longer needed.
*
* Typical PhET usage will use a position Property in a model coordinate frame and look like this:
*
* // A focusable Node that can be dragged with pointer or keyboard.
* const draggableNode = new Node( {
* tagName: 'div',
* focusable: true
* } );
*
* const richDragListener = new RichDragListener( {
* positionProperty: someObject.positionProperty,
* transform: modelViewTransform
* } );
*
* draggableNode.addInputListener( richDragListener );
*
* @author Jesse Greenberg
*/

import { Hotkey, PressListenerEvent, SceneryEvent, TInputListener } from '../../scenery/js/imports.js';
import TProperty from '../../axon/js/TProperty.js';
import Vector2 from '../../dot/js/Vector2.js';
import Transform3 from '../../dot/js/Transform3.js';
import TReadOnlyProperty from '../../axon/js/TReadOnlyProperty.js';
import Bounds2 from '../../dot/js/Bounds2.js';
import RichPointerDragListener, { PressedRichPointerDragListener, RichPointerDragListenerOptions } from './RichPointerDragListener.js';
import RichKeyboardDragListener, { RichKeyboardDragListenerOptions } from './RichKeyboardDragListener.js';
import optionize, { combineOptions } from '../../phet-core/js/optionize.js';
import WrappedAudioBuffer from '../../tambo/js/WrappedAudioBuffer.js';
import { SoundClipOptions } from '../../tambo/js/sound-generators/SoundClip.js';
import { SoundGeneratorAddOptions } from '../../tambo/js/soundManager.js';
import grab_mp3 from '../../tambo/sounds/grab_mp3.js';
import release_mp3 from '../../tambo/sounds/release_mp3.js';
import SceneryPhetConstants from './SceneryPhetConstants.js';
import sceneryPhet from './sceneryPhet.js';

type SelfOptions = {

// Called when the drag is started, for any input type. If you want to determine the type of input, you can check
// SceneryEvent.isFromPDOM or SceneryEvent.type. If you need a start behavior for a specific form of input,
// provide a start callback for that listener's options. It will be called IN ADDITION to this callback.
start?: ( ( event: SceneryEvent, listener: RichPointerDragListener | RichKeyboardDragListener ) => void ) | null;

// Called during the drag event, for any input type. If you want to determine the type of input, you can check
// SceneryEvent.isFromPDOM or SceneryEvent.type. If you need a drag behavior for a specific form of input,
// provide a drag callback for that listener's options. It will be called IN ADDITION to this callback.
drag?: ( ( event: SceneryEvent, listener: RichPointerDragListener | RichKeyboardDragListener ) => void ) | null;

// Called when the drag is ended, for any input type. If you want to determine the type of input, you can check
// SceneryEvent.isFromPDOM or SceneryEvent.type. If you need an end behavior for a specific form of input,
// provide an end callback for that listener's options. It will be called IN ADDITION to this callback. The event
// may be null for cases of interruption.
end?: ( ( event: SceneryEvent | null, listener: RichPointerDragListener | RichKeyboardDragListener ) => void ) | null;

// If provided, it will be synchronized with the drag position in the model coordinate frame. The optional transform
// is applied.
positionProperty?: Pick<TProperty<Vector2>, 'value'> | null;

// If provided, this will be used to convert between the parent (view) and model coordinate frames. Most useful
// when you also provide a positionProperty.
transform?: Transform3 | TReadOnlyProperty<Transform3> | null;

// If provided, the model position will be constrained to these bounds.
dragBoundsProperty?: TReadOnlyProperty<Bounds2 | null> | null;

// If provided, this allows custom mapping from the desired position (i.e. where the pointer is, or where the
// RichKeyboardListener will set the position) to the actual position that will be used.
mapPosition?: null | ( ( point: Vector2 ) => Vector2 );

// If true, the target Node will be translated during the drag operation.
translateNode?: boolean;

// Grab and release sounds. `null` means no sound.
grabSound?: WrappedAudioBuffer | null;
releaseSound?: WrappedAudioBuffer | null;

// Passed to the grab and release SoundClip instances.
grabSoundClipOptions?: SoundClipOptions;
releaseSoundClipOptions?: SoundClipOptions;

// addSoundGeneratorOptions
grabSoundGeneratorAddOptions?: SoundGeneratorAddOptions;
releaseSoundGeneratorAddOptions?: SoundGeneratorAddOptions;

// Additional options for the RichPointerDragListener, OR any overrides for the RichPointerDragListener that should
// be used instead of the above options. For example, if the RichPointerDragListener should have different
// grab/release sounds, you can provide those options here.
richPointerDragListenerOptions?: RichPointerDragListenerOptions;

// Additional options for the RichKeyboardDragListener, OR any overrides for the RichKeyboardDragListener that should
// be used instead of the above options. For example, if the RichKeyboardDragListener should have different
// grab/release sounds, you can provide those options here.
richKeyboardDragListenerOptions?: RichKeyboardDragListenerOptions;
};

export type RichDragListenerOptions = SelfOptions;

export default class RichDragListener implements TInputListener {
private readonly richPointerDragListener: RichPointerDragListener;
private readonly richKeyboardDragListener: RichKeyboardDragListener;

// Implements TInputListener
public readonly hotkeys: Hotkey[];

public constructor( providedOptions?: RichDragListenerOptions ) {

const options = optionize<RichDragListenerOptions>()( {

// RichDragListenerOptions
positionProperty: null,
start: null,
end: null,
drag: null,
transform: null,
dragBoundsProperty: null,
mapPosition: null,
translateNode: false,
grabSound: grab_mp3,
releaseSound: release_mp3,
grabSoundClipOptions: SceneryPhetConstants.DEFAULT_DRAG_CLIP_OPTIONS,
releaseSoundClipOptions: SceneryPhetConstants.DEFAULT_DRAG_CLIP_OPTIONS,
grabSoundGeneratorAddOptions: SceneryPhetConstants.DEFAULT_GRAB_SOUND_GENERATOR_ADD_OPTIONS,
releaseSoundGeneratorAddOptions: SceneryPhetConstants.DEFAULT_GRAB_SOUND_GENERATOR_ADD_OPTIONS,
richPointerDragListenerOptions: {},
richKeyboardDragListenerOptions: {}
}, providedOptions );

// Options that will apply to both listeners.
const sharedOptions = {
positionProperty: options.positionProperty,
transform: options.transform,
dragBoundsProperty: options.dragBoundsProperty || undefined,
mapPosition: options.mapPosition || undefined,
translateNode: options.translateNode,
grabSound: options.grabSound,
releaseSound: options.releaseSound,
grabSoundClipOptions: options.grabSoundClipOptions,
releaseSoundClipOptions: options.releaseSoundClipOptions,
grabSoundGeneratorAddOptions: options.grabSoundGeneratorAddOptions
};

//---------------------------------------------------------------------------------
// Construct the RichPointerDragListener and combine its options.
//---------------------------------------------------------------------------------
const wrappedDragListenerStart = ( event: PressListenerEvent, listener: PressedRichPointerDragListener ) => {

// when the drag listener starts, interrupt the keyboard dragging
this.richKeyboardDragListener.interrupt();

options.start && options.start( event, listener );
options.richPointerDragListenerOptions.start && options.richPointerDragListenerOptions.start( event, listener );
};

const wrappedDragListenerDrag = ( event: PressListenerEvent, listener: PressedRichPointerDragListener ) => {
options.drag && options.drag( event, listener );
options.richPointerDragListenerOptions.drag && options.richPointerDragListenerOptions.drag( event, listener );
};

const wrappedDragListenerEnd = ( event: PressListenerEvent | null, listener: PressedRichPointerDragListener ) => {
options.end && options.end( event, listener );
options.richPointerDragListenerOptions.end && options.richPointerDragListenerOptions.end( event, listener );
};

const richPointerDragListenerOptions = combineOptions<RichPointerDragListenerOptions>(
// target object
{},
// Options that apply to both, but can be overridden by provided listener-specific options
sharedOptions,
// Provided listener-specific options
options.richPointerDragListenerOptions,
// Options that cannot be overridden - see wrapped callbacks above
{
start: wrappedDragListenerStart,
drag: wrappedDragListenerDrag,
end: wrappedDragListenerEnd
}
);

this.richPointerDragListener = new RichPointerDragListener( richPointerDragListenerOptions );

//---------------------------------------------------------------------------------
// Construct the RichKeyboardDragListener and combine its options.
//---------------------------------------------------------------------------------
const wrappedKeyboardListenerStart = ( event: SceneryEvent, listener: RichKeyboardDragListener ) => {

// when the drag listener starts, interrupt the pointer dragging
this.richPointerDragListener.interrupt();

options.start && options.start( event, listener );
options.richKeyboardDragListenerOptions.start && options.richKeyboardDragListenerOptions.start( event, listener );
};

const wrappedKeyboardListenerDrag = ( event: SceneryEvent, listener: RichKeyboardDragListener ) => {
options.drag && options.drag( event, listener );
options.richKeyboardDragListenerOptions.drag && options.richKeyboardDragListenerOptions.drag( event, listener );
};

const wrappedKeyboardListenerEnd = ( event: SceneryEvent | null, listener: RichKeyboardDragListener ) => {
options.end && options.end( event, listener );
options.richKeyboardDragListenerOptions.end && options.richKeyboardDragListenerOptions.end( event, listener );
};

const richKeyboardDragListenerOptions = combineOptions<RichKeyboardDragListenerOptions>(
// target object
{},
// Options that apply to both, but can be overridden by provided listener-specific options
sharedOptions,
// Provided listener-specific options
options.richKeyboardDragListenerOptions,
// Options that cannot be overridden - see wrapped callbacks above
{
start: wrappedKeyboardListenerStart,
drag: wrappedKeyboardListenerDrag,
end: wrappedKeyboardListenerEnd
}
);

this.richKeyboardDragListener = new RichKeyboardDragListener( richKeyboardDragListenerOptions );

// The hotkeys from the keyboard listener are assigned to this listener so that they are activated for Nodes
// where this listener is added.
this.hotkeys = this.richKeyboardDragListener.hotkeys;
}

public get isPressed(): boolean {
return this.richPointerDragListener.isPressed || this.richKeyboardDragListener.isPressed;
}

public dispose(): void {
this.richPointerDragListener.dispose();
this.richKeyboardDragListener.dispose();
}

/**
* ********************************************************************
* Forward input to both listeners
* ********************************************************************
*/
public interrupt(): void {
this.richPointerDragListener.interrupt();
this.richKeyboardDragListener.interrupt();
}

/**
* ********************************************************************
* Forward to the KeyboardListener
* ********************************************************************
*/
/**
* Forward the keydown event to the KeyboardDragListener.
*/
public keydown( event: SceneryEvent<KeyboardEvent> ): void {
this.richKeyboardDragListener.keydown( event );
}

public focusout( event: SceneryEvent ): void {
this.richKeyboardDragListener.focusout( event );
}

public focusin( event: SceneryEvent ): void {
this.richKeyboardDragListener.focusin( event );
}

public cancel(): void {
this.richKeyboardDragListener.cancel();
}

/**
* ********************************************************************
* Forward to the DragListener
* ********************************************************************
*/
public click( event: SceneryEvent<MouseEvent> ): void {
this.richPointerDragListener.click( event );
}

public touchenter( event: PressListenerEvent ): void {
this.richPointerDragListener.touchenter( event );
}

public touchmove( event: PressListenerEvent ): void {
this.richPointerDragListener.touchmove( event );
}

public focus( event: SceneryEvent<FocusEvent> ): void {
this.richPointerDragListener.focus( event );
}

public blur(): void {
this.richPointerDragListener.blur();
}

public down( event: PressListenerEvent ): void {
this.richPointerDragListener.down( event );
}

public up( event: PressListenerEvent ): void {
this.richPointerDragListener.up( event );
}

public enter( event: PressListenerEvent ): void {
this.richPointerDragListener.enter( event );
}

public move( event: PressListenerEvent ): void {
this.richPointerDragListener.move( event );
}

public exit( event: PressListenerEvent ): void {
this.richPointerDragListener.exit( event );
}

public pointerUp( event: PressListenerEvent ): void {
this.richPointerDragListener.pointerUp( event );
}

public pointerCancel( event: PressListenerEvent ): void {
this.richPointerDragListener.pointerCancel( event );
}

public pointerMove( event: PressListenerEvent ): void {
this.richPointerDragListener.pointerMove( event );
}

public pointerInterrupt(): void {
this.richPointerDragListener.pointerInterrupt();
}
}

sceneryPhet.register( 'RichDragListener', RichDragListener );
13 changes: 5 additions & 8 deletions js/RichKeyboardDragListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ import grab_mp3 from '../../tambo/sounds/grab_mp3.js';
import release_mp3 from '../../tambo/sounds/release_mp3.js';
import soundManager, { SoundGeneratorAddOptions } from '../../tambo/js/soundManager.js';
import WrappedAudioBuffer from '../../tambo/js/WrappedAudioBuffer.js';

const DEFAULT_DRAG_CLIP_OPTIONS: SoundClipOptions = {
initialOutputLevel: 0.4
};
import SceneryPhetConstants from './SceneryPhetConstants.js';

type SelfOptions = {

Expand Down Expand Up @@ -53,10 +50,10 @@ export default class RichKeyboardDragListener extends KeyboardDragListener {
// SelfOptions
grabSound: grab_mp3,
releaseSound: release_mp3,
grabSoundClipOptions: DEFAULT_DRAG_CLIP_OPTIONS,
releaseSoundClipOptions: DEFAULT_DRAG_CLIP_OPTIONS,
grabSoundGeneratorAddOptions: {},
releaseSoundGeneratorAddOptions: {}
grabSoundClipOptions: SceneryPhetConstants.DEFAULT_DRAG_CLIP_OPTIONS,
releaseSoundClipOptions: SceneryPhetConstants.DEFAULT_DRAG_CLIP_OPTIONS,
grabSoundGeneratorAddOptions: SceneryPhetConstants.DEFAULT_GRAB_SOUND_GENERATOR_ADD_OPTIONS,
releaseSoundGeneratorAddOptions: SceneryPhetConstants.DEFAULT_GRAB_SOUND_GENERATOR_ADD_OPTIONS
}, providedOptions );

// Create the grab SoundClip and wire it into the start function for the drag cycle.
Expand Down
14 changes: 5 additions & 9 deletions js/RichPointerDragListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ import grab_mp3 from '../../tambo/sounds/grab_mp3.js';
import release_mp3 from '../../tambo/sounds/release_mp3.js';
import soundManager, { SoundGeneratorAddOptions } from '../../tambo/js/soundManager.js';
import WrappedAudioBuffer from '../../tambo/js/WrappedAudioBuffer.js';

const DEFAULT_DRAG_CLIP_OPTIONS: SoundClipOptions = {
initialOutputLevel: 0.4
};
const DEFAULT_ADD_SOUND_GENERATOR_OPTIONS: SoundGeneratorAddOptions = { categoryName: 'user-interface' };
import SceneryPhetConstants from './SceneryPhetConstants.js';

type SelfOptions = {

Expand Down Expand Up @@ -57,10 +53,10 @@ export default class RichPointerDragListener extends DragListener {
// SelfOptions
grabSound: grab_mp3,
releaseSound: release_mp3,
grabSoundClipOptions: DEFAULT_DRAG_CLIP_OPTIONS,
releaseSoundClipOptions: DEFAULT_DRAG_CLIP_OPTIONS,
grabSoundGeneratorAddOptions: DEFAULT_ADD_SOUND_GENERATOR_OPTIONS,
releaseSoundGeneratorAddOptions: DEFAULT_ADD_SOUND_GENERATOR_OPTIONS
grabSoundClipOptions: SceneryPhetConstants.DEFAULT_DRAG_CLIP_OPTIONS,
releaseSoundClipOptions: SceneryPhetConstants.DEFAULT_DRAG_CLIP_OPTIONS,
grabSoundGeneratorAddOptions: SceneryPhetConstants.DEFAULT_GRAB_SOUND_GENERATOR_ADD_OPTIONS,
releaseSoundGeneratorAddOptions: SceneryPhetConstants.DEFAULT_GRAB_SOUND_GENERATOR_ADD_OPTIONS
}, providedOptions );

// Create the grab SoundClip and wire it into the start function for the drag cycle.
Expand Down
Loading

0 comments on commit 3a2ea8b

Please sign in to comment.