phetsims / circuit-construction-kit-common

"Circuit Construction Kit: Basics" is an educational simulation in HTML5, by PhET Interactive Simulations.
GNU General Public License v3.0
10 stars 10 forks source link

Make it so that studio doesn't leave "holes" in the carousel during customization #630

Closed samreid closed 1 year ago

samreid commented 3 years ago

Make it so that studio doesn't leave "holes" in the carousel during customization, but dragging out the last item should leave a hole.

samreid commented 2 years ago

At today's design meeting, we agreed we need to make the distinction between "all the batteries are currently gone and there should be a placeholder blank spot for them" vs "the customized sim doesn't have any batteries so there shouldn't be a spot for them".

samreid commented 2 years ago

@kathy-phet said this may be beyond the scope of our January 2022 sprint.

samreid commented 2 years ago

The pagination is very explicit at the moment, with wires at the top of each page.

    const circuitElementToolNodes = [
      circuitElementToolFactory.createWireToolNode(),
      circuitElementToolFactory.createRightBatteryToolNode(),
      circuitElementToolFactory.createACVoltageToolNode(),
      circuitElementToolFactory.createLightBulbToolNode(),
      circuitElementToolFactory.createResistorToolNode(),
      circuitElementToolFactory.createSwitchToolNode(),

      circuitElementToolFactory.createWireToolNode(),
      circuitElementToolFactory.createFuseToolNode(),
      circuitElementToolFactory.createDollarBillToolNode(),
      circuitElementToolFactory.createPaperClipToolNode(),
      circuitElementToolFactory.createCoinToolNode(),
      circuitElementToolFactory.createEraserToolNode(),

      circuitElementToolFactory.createWireToolNode(),
      circuitElementToolFactory.createPencilToolNode(),
      circuitElementToolFactory.createHandToolNode(),
      circuitElementToolFactory.createDogToolNode()
    ];

It's unclear what the desired behavior should be during customization. Should the client be able to set up different pagination during startup? Or dynamically?

@kathy-phet said this may be beyond the scope of our January 2022 sprint.

Is the current behavior acceptable for now?

kathy-phet commented 2 years ago

@samreid @arouinfar - I would say that this is not a priority for the Jan 2022 sprint, and can defer for a later time.

samreid commented 2 years ago

@arouinfar should this be part of the Full initial PhET-iO release: DC Milestone? How should we proceed?

arouinfar commented 2 years ago

This is definitely worth investigating before the full publication milestone, but I won't insist on having this feature if it proves too difficult.

samreid commented 2 years ago

Before starting on this, I'd like to clarify the design requirements. ComboBoxes have the ability to "move up" and "move down" elements:

image

Is that what we want here? Do we want that generally for all carousels? Or generally for more layout containers? Do we want to use the visibleProperty to determine which are included and not included? Do we like that studio UX?

arouinfar commented 1 year ago

11/30/22 discussion with @samreid @matthew-blackman @arouinfar

matthew-blackman commented 1 year ago

@samreid and I have been discussing this and ran into some edge cases. Currently there is a wire as the top element on every page of the carousel. We are asking about the desired behavior in the following cases:

  1. The user wants to remove a wire from a carousel page
  2. The user does not want wires as the first item of each carousel page

If the user, for example, hides the fuse, do we still want a wire at the top of each page? @arouinfar

matthew-blackman commented 1 year ago

Currently the carousel has only one global 'Wire Tool Node' that is being shown at the top of each carousel page. If the user sets the wire tool node invisible, this will remove the wire tool at the top of each carousel page. Is this the intended behavior, or do we want separate wire tool nodes for each carousel page to allow more customizability? @arouinfar

arouinfar commented 1 year ago

This is a fun edge case. The wire appears at the top of each carousel page as a convenience since there are generally far more wires in a circuit than other element types.

If the user, for example, hides the fuse, do we still want a wire at the top of each page?

Yes, I think so. The goal is to make wires very easy to find, and keeping them in a consistent position maintains this.

If the user sets the wire tool node invisible, this will remove the wire tool at the top of each carousel page. Is this the intended behavior, or do we want separate wire tool nodes for each carousel page to allow more customizability?

I don't think we need to provide clients with the ability to decide which of the wires to keep and which to hide. I'm fine with using a global approach -- setting wireToolNode.visibleProperty to false will hide all instances of the wire in the carousel.

samreid commented 1 year ago

@arouinfar what do you think about having the wire part of the carousel be non-scrolling? Would that be ok, or perhaps awkward and confusing?

samreid commented 1 year ago

The layout of the carousel is hard-coded and will be difficult to generalize. Especially repagination, and especially if we have sim-specific constraints like "a wire should appear at the top of every page". If we do change carousel to support removing and adding back items, can that be done solely using visibleProperty? It seems carousels may need to sometimes leave a space for a (temporarily?) invisible icon, if we don't follow an approach like https://github.com/phetsims/circuit-construction-kit-common/issues/903. Or do we need a separate API like Carousel.setItems() or making it so each item has an optional isIncludedProperty?

samreid commented 1 year ago

I opened a common code issue and requested advice from @pixelzoom

arouinfar commented 1 year ago

Some thoughts

@arouinfar what do you think about having the wire part of the carousel be non-scrolling?

@samreid I'm trying to understand what this would look like. Would that mean that the previous button would appear below the wire? Or would the carousel would basically look the same, but the wire would simply not scroll with the rest of the contents? The latter is acceptable, the former is not. I would rather remove repeated wire icons than visually factor it out of the carousel.

EDIT: I just saw the proposal in #903, and disabling the icon when reaching the limit is also fine with me! If we go this route we may be able to use visibleProperty instead of creating a new isIncludedProperty.

samreid commented 1 year ago

@arouinfar please review the time estimate from @pixelzoom in https://github.com/phetsims/sun/issues/814#issuecomment-1334463348 and recommend whether this should be included in the first release or not.

samreid commented 1 year ago

@matthew-blackman and I prototyped an alternative that adds an enabledProperty so that phet-io clients can "gray out" items in the toolbox without leaving layout holes.

```diff Subject: [PATCH] message --- Index: js/view/CircuitElementToolNode.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/view/CircuitElementToolNode.ts b/js/view/CircuitElementToolNode.ts --- a/js/view/CircuitElementToolNode.ts (revision e8535ac5ee30fd3f6398235b672f404fb4163e3b) +++ b/js/view/CircuitElementToolNode.ts (date 1670446022742) @@ -109,23 +109,18 @@ allowTouchSnag: true } ) ); - // Make the tool icon visible if we can create more of the item. But be careful to only update visibility when - // the specific count for this item changes, so we can support PhET-iO hiding the icons via visibility. - let lastCount: number | null = null; - let lastValue: boolean | null = null; + const enabledProperty = new BooleanProperty( true, { + tandem: providedOptions!.tandem!.createTandem( 'enabledProperty' ) + } ); - Multilink.multilink( [ circuit.circuitElements.lengthProperty, options.additionalProperty ], ( length, additionalValue: boolean ) => { + Multilink.multilink( [ enabledProperty, circuit.circuitElements.lengthProperty, options.additionalProperty ], ( enabled, length, additionalValue: boolean ) => { const currentCount = count(); - if ( lastCount !== currentCount || lastValue !== additionalValue ) { - // Gray out circuit elements that cannot be dragged out, this includes the real bulb when it is not selected - const isAvailable = ( currentCount < maxNumber ) && additionalValue; - this.setOpacity( isAvailable ? 1 : 0.4 ); - this.filters = isAvailable ? [] : [ Grayscale.FULL ]; - this.inputEnabled = isAvailable; - } - lastCount = currentCount; - lastValue = additionalValue; + // Gray out circuit elements that cannot be dragged out, this includes the real bulb when it is not selected + const isAvailable = ( currentCount < maxNumber ) && additionalValue && enabled; + this.setOpacity( isAvailable ? 1 : 0.4 ); + this.filters = isAvailable ? [] : [ Grayscale.FULL ]; + this.inputEnabled = isAvailable; } ); // Update touch areas when lifelike/schematic changes ```

For instance, I disabled the resistor like so:

image

We do not believe it to be a great user interface to have a bunch of disabled components that cannot be enabled, but we thought this was worth consideration in light of the extreme cost of implementing repagination and relayout for carousels.

arouinfar commented 1 year ago

@arouinfar please review the time estimate from @pixelzoom in https://github.com/phetsims/sun/issues/814#issuecomment-1334463348 and recommend whether this should be included in the first release or not.

My personal preference would be to invest the time into carousel because it's not going away, but I cannot make this call. I added the sun issue to the PhET-iO project board for further discussion.

@matthew-blackman and I prototyped an alternative

Thanks! I think it's really helpful to see alternative solutions.

We do not believe it to be a great user interface to have a bunch of disabled components that cannot be enabled,

I completely agree.

but we thought this was worth consideration in light of the extreme cost of implementing repagination and relayout for carousels.

I'm not a fan of the grayscale solution, but I appreciate the effort in looking into alternatives. At this point, I would prefer the empty holes to the grayscale icons. I find it less confusing.

samreid commented 1 year ago

I experimented to see if there was a simple solution to the carousel layout problem, and I developed this prototype:

```diff Subject: [PATCH] Hide the realistic bulb tool icon until selected in the advanced control panel, see https://github.com/phetsims/circuit-construction-kit-common/issues/903 --- Index: main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts (revision 040b8f82a1382dae701cb36a72a70c99c3e28be5) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts (date 1670478695368) @@ -11,7 +11,7 @@ import Property from '../../../axon/js/Property.js'; import ReadOnlyProperty from '../../../axon/js/ReadOnlyProperty.js'; import Vector2 from '../../../dot/js/Vector2.js'; -import { DragListener, Grayscale, Node, SceneryEvent, Text, VBox, VBoxOptions } from '../../../scenery/js/imports.js'; +import { DragListener, Grayscale, IndexedNodeIO, Node, SceneryEvent, Text, VBox, VBoxOptions } from '../../../scenery/js/imports.js'; import CCKCConstants from '../CCKCConstants.js'; import circuitConstructionKitCommon from '../circuitConstructionKitCommon.js'; import Circuit from '../model/Circuit.js'; @@ -81,6 +81,8 @@ excludeInvisibleChildrenFromBounds: false, additionalProperty: new BooleanProperty( true ), + phetioType: IndexedNodeIO, + phetioState: true, visiblePropertyOptions: { phetioFeatured: true } Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 4ee88a07bb2c211e8576bc50692be96fae731084) +++ b/main/sun/js/Carousel.ts (date 1670478212343) @@ -24,7 +24,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -204,11 +204,11 @@ }, buttonOptions ) ); // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); + // const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; + // const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); + // const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); + // const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; + // let itemCenter = options.spacing + ( maxItemLength / 2 ); // Options common to all separators const separatorOptions = { @@ -224,22 +224,29 @@ // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + const scrollingNode = isHorizontal ? new HBox( { + spacing: options.spacing - options.margin, + margin: options.margin + } ) : new VBox( { + spacing: options.spacing - options.margin, + margin: options.margin + } ); + // const scrollingNode = new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); items.forEach( item => { // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } + // if ( isHorizontal ) { + // item.centerX = itemCenter; + // item.centerY = options.margin + ( maxItemHeight / 2 ); + // } + // else { + // item.centerX = options.margin + ( maxItemWidth / 2 ); + // item.centerY = itemCenter; + // } scrollingNode.addChild( item ); // center for the next item - itemCenter += ( options.spacing + maxItemLength ); + // itemCenter += ( options.spacing + maxItemLength ); // add optional separator if ( options.separatorsVisible ) { @@ -248,27 +255,27 @@ // vertical separator, to the left of the item separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, + // preferredHeight: scrollingHeight, centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, centerY: item.centerY }, separatorOptions ) ); scrollingNode.addChild( separator ); // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); + // itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); } else { // horizontal separator, below the item separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, + // preferredWidth: scrollingWidth, centerX: item.centerX, centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing }, separatorOptions ) ); scrollingNode.addChild( separator ); // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); + // itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); } } } ); Index: main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts (revision 040b8f82a1382dae701cb36a72a70c99c3e28be5) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts (date 1670478600850) @@ -75,6 +75,8 @@ const LIFELIKE_PROPERTY = new EnumerationProperty( CircuitElementViewType.LIFELIKE ); const SCHEMATIC_PROPERTY = new EnumerationProperty( CircuitElementViewType.SCHEMATIC ); +let index = 0; + // createCircuitElementToolNode type CreateCircuitElementToolNodeSelfOptions = { @@ -191,30 +193,24 @@ } public createWireToolNode(): Node { - if ( !this.wireToolNode ) { - - // Cache a single instance to simplify PhET-iO - this.wireToolNode = this.createCircuitElementToolNode( wireStringProperty, CCKCConstants.NUMBER_OF_WIRES, - ( tandem: Tandem, viewTypeProperty: Property ) => { - return viewTypeProperty.value === CircuitElementViewType.LIFELIKE ? ( new Image( wireIcon_png, { - tandem: tandem - } ) ) : new Line( 0, 0, 120, 0, { - stroke: Color.BLACK, - lineWidth: 4.5, // match with other toolbox icons - tandem: tandem - } ); - }, - circuitElement => circuitElement instanceof Wire, - ( position: Vector2 ) => this.circuit.wireGroup.createNextElement( ...this.circuit.createVertexPairArray( position, WIRE_LENGTH ) ), { - tandem: this.parentTandem.createTandem( 'wireToolNode' ), - lifelikeIconHeight: 9, - schematicIconHeight: 2 - } ); - } + return this.createCircuitElementToolNode( wireStringProperty, CCKCConstants.NUMBER_OF_WIRES, + ( tandem: Tandem, viewTypeProperty: Property ) => { + return viewTypeProperty.value === CircuitElementViewType.LIFELIKE ? ( new Image( wireIcon_png, { + tandem: tandem + } ) ) : new Line( 0, 0, 120, 0, { + stroke: Color.BLACK, + lineWidth: 4.5, // match with other toolbox icons + tandem: tandem + } ); + }, + circuitElement => circuitElement instanceof Wire, + ( position: Vector2 ) => this.circuit.wireGroup.createNextElement( ...this.circuit.createVertexPairArray( position, WIRE_LENGTH ) ), { + tandem: this.parentTandem.createTandem( 'wireToolNode' + ( index++ ) ), + lifelikeIconHeight: 9, + schematicIconHeight: 2 + } ); + } - return new Node( { children: [ this.wireToolNode ] } ); - } - /** * @param count - the number that can be dragged out at once */ ```

It allows you to remove items from the carousel, and the layout reflows eliminating holes. You can also re-order items in the carousel the same way we reorder items in the combo box. I tested and all of this customization is preserved when launching the test sim.

Necessary steps to take this to production:

samreid commented 1 year ago

Here's a better patch that is opt-in (less disruptive for other sims) and minimal changes, and improves layout.

```diff Subject: [PATCH] Hide the realistic bulb tool icon until selected in the advanced control panel, see https://github.com/phetsims/circuit-construction-kit-common/issues/903 --- Index: main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts (revision 040b8f82a1382dae701cb36a72a70c99c3e28be5) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolNode.ts (date 1670478695368) @@ -11,7 +11,7 @@ import Property from '../../../axon/js/Property.js'; import ReadOnlyProperty from '../../../axon/js/ReadOnlyProperty.js'; import Vector2 from '../../../dot/js/Vector2.js'; -import { DragListener, Grayscale, Node, SceneryEvent, Text, VBox, VBoxOptions } from '../../../scenery/js/imports.js'; +import { DragListener, Grayscale, IndexedNodeIO, Node, SceneryEvent, Text, VBox, VBoxOptions } from '../../../scenery/js/imports.js'; import CCKCConstants from '../CCKCConstants.js'; import circuitConstructionKitCommon from '../circuitConstructionKitCommon.js'; import Circuit from '../model/Circuit.js'; @@ -81,6 +81,8 @@ excludeInvisibleChildrenFromBounds: false, additionalProperty: new BooleanProperty( true ), + phetioType: IndexedNodeIO, + phetioState: true, visiblePropertyOptions: { phetioFeatured: true } Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 040b8f82a1382dae701cb36a72a70c99c3e28be5) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1670480786008) @@ -8,7 +8,7 @@ */ import Property from '../../../axon/js/Property.js'; -import merge from '../../../phet-core/js/merge.js'; +import optionize from '../../../phet-core/js/optionize.js'; import { Node, Color, HBox, HBoxOptions } from '../../../scenery/js/imports.js'; import Carousel, { CarouselOptions } from '../../../sun/js/Carousel.js'; import PageControl from '../../../sun/js/PageControl.js'; @@ -38,7 +38,7 @@ */ public constructor( viewTypeProperty: Property, circuitElementToolNodes: Node[], tandem: Tandem, providedOptions?: CircuitElementToolboxOptions ) { - providedOptions = merge( { + providedOptions = optionize()( { carouselOptions: { itemsPerPage: 5, @@ -54,7 +54,9 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ) + tandem: tandem.createTandem( 'carousel' ), + + isScrollingNodeLayoutBox: true, } }, providedOptions ); Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 4ee88a07bb2c211e8576bc50692be96fae731084) +++ b/main/sun/js/Carousel.ts (date 1670480581098) @@ -24,7 +24,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -45,6 +45,7 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible + isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is done manually // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -118,6 +119,7 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, + isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -224,7 +226,15 @@ // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + const scrollingNode = options.isScrollingNodeLayoutBox ? + ( isHorizontal ? new HBox( { + spacing: options.spacing, + yMargin: options.margin + } ) : new VBox( { + spacing: options.spacing, + xMargin: options.margin + } ) ) : + new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); items.forEach( item => { // add the item Index: main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts (revision 040b8f82a1382dae701cb36a72a70c99c3e28be5) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolFactory.ts (date 1670480817356) @@ -75,6 +75,9 @@ const LIFELIKE_PROPERTY = new EnumerationProperty( CircuitElementViewType.LIFELIKE ); const SCHEMATIC_PROPERTY = new EnumerationProperty( CircuitElementViewType.SCHEMATIC ); +// Increment tandems for wire tool nodes +let wireToolNodeCount = 0; + // createCircuitElementToolNode type CreateCircuitElementToolNodeSelfOptions = { @@ -191,30 +194,24 @@ } public createWireToolNode(): Node { - if ( !this.wireToolNode ) { - - // Cache a single instance to simplify PhET-iO - this.wireToolNode = this.createCircuitElementToolNode( wireStringProperty, CCKCConstants.NUMBER_OF_WIRES, - ( tandem: Tandem, viewTypeProperty: Property ) => { - return viewTypeProperty.value === CircuitElementViewType.LIFELIKE ? ( new Image( wireIcon_png, { - tandem: tandem - } ) ) : new Line( 0, 0, 120, 0, { - stroke: Color.BLACK, - lineWidth: 4.5, // match with other toolbox icons - tandem: tandem - } ); - }, - circuitElement => circuitElement instanceof Wire, - ( position: Vector2 ) => this.circuit.wireGroup.createNextElement( ...this.circuit.createVertexPairArray( position, WIRE_LENGTH ) ), { - tandem: this.parentTandem.createTandem( 'wireToolNode' ), - lifelikeIconHeight: 9, - schematicIconHeight: 2 - } ); - } + return this.createCircuitElementToolNode( wireStringProperty, CCKCConstants.NUMBER_OF_WIRES, + ( tandem: Tandem, viewTypeProperty: Property ) => { + return viewTypeProperty.value === CircuitElementViewType.LIFELIKE ? ( new Image( wireIcon_png, { + tandem: tandem + } ) ) : new Line( 0, 0, 120, 0, { + stroke: Color.BLACK, + lineWidth: 4.5, // match with other toolbox icons + tandem: tandem + } ); + }, + circuitElement => circuitElement instanceof Wire, + ( position: Vector2 ) => this.circuit.wireGroup.createNextElement( ...this.circuit.createVertexPairArray( position, WIRE_LENGTH ) ), { + tandem: this.parentTandem.createTandem( 'wireToolNode' + ( wireToolNodeCount++ ) ), + lifelikeIconHeight: 9, + schematicIconHeight: 2 + } ); + } - return new Node( { children: [ this.wireToolNode ] } ); - } - /** * @param count - the number that can be dragged out at once */ ```
samreid commented 1 year ago

I committed a version that can be tested on phettest.

arouinfar commented 1 year ago

12/15/22 design meeting

samreid commented 1 year ago

@marlitas and I fixed the overlap/offset/clipping problem in the commit. Since the PageControl can be manually hidden, it seems the next important part of this issue would be getting rid of blank pages.

samreid commented 1 year ago

This patch is working but not yet ready to commit:

```diff Subject: [PATCH] Use combineOptions, see https://github.com/phetsims/circuit-construction-kit-common/issues/914 --- Index: js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/Carousel.ts b/js/Carousel.ts --- a/js/Carousel.ts (revision b934901464bda0c7a93de76ed1826d5f3829bb72) +++ b/js/Carousel.ts (date 1671554530927) @@ -33,6 +33,7 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import Utils from '../../dot/js/Utils.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -237,6 +238,26 @@ } ) ) : new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + // Number of pages + let numberOfPages = items.length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + + if (this.pageNumberProperty.value >=numberOfPages){ + + } + } + + if ( options.isScrollingNodeLayoutBox ) { + const updatePageCount = () => { + const count = items.filter( item => item.visible ).length; + console.log( count ); + + numberOfPages = Utils.roundSymmetric( count / options.itemsPerPage ); + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + } + this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; items.forEach( item => { @@ -335,12 +356,6 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - // Number of the page that is visible in the carousel. assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); ```
samreid commented 1 year ago

Current patch:

```diff Subject: [PATCH] Remove incorrect comment, see https://github.com/phetsims/ph-scale/issues/240 --- Index: main/axon/js/DerivedProperty.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/axon/js/DerivedProperty.ts b/main/axon/js/DerivedProperty.ts --- a/main/axon/js/DerivedProperty.ts (revision 8274b6e240cbf1ad0f9427eb59ecc51e22ef5636) +++ b/main/axon/js/DerivedProperty.ts (date 1671728338426) @@ -206,6 +206,14 @@ return DerivedProperty.deriveAny( properties, () => _.reduce( properties, andFunction, true ), options ); } + /** + * Creates a derived boolean Property whose value is true iff every input Property value is true. + */ + public static count( properties: TReadOnlyProperty[], options?: PropertyOptions ): UnknownDerivedProperty { + assert && assert( properties.length > 0, 'must provide a dependency' ); + return DerivedProperty.deriveAny( properties, () => properties.map( property => property.value ).length, options ); + } + /** * Creates a derived boolean Property whose value is true iff any input Property value is true. */ Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 21bd965d24a31fa4a878c3c4532e3eb58cda0408) +++ b/main/sun/js/PageControl.ts (date 1671728819515) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -46,7 +47,7 @@ * @param numberOfPages - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPages: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision a5dee69bc0a464466a56e98505d79db2af059b83) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1671728819509) @@ -64,7 +64,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 21bd965d24a31fa4a878c3c4532e3eb58cda0408) +++ b/main/sun/js/Carousel.ts (date 1671728636952) @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -89,7 +91,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -237,6 +239,46 @@ } ) ) : new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( items.map( item => item.visibleProperty ), () => { + let numberOfPages = items.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } + + return numberOfPages; + }, options ); + + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true + } ); + + if ( options.isScrollingNodeLayoutBox ) { + const updatePageCount = () => { + + // const numberOfPages = getPageCount(); + // if ( pageNumberProperty.value >= numberOfPages ) { + // pageNumberProperty.value = numberOfPages - 1; + // } + // const count = items.filter( item => item.visible ).length; + // console.log( count ); + + // numberOfPages = Utils.roundSymmetric( count / options.itemsPerPage ); + // console.log( pageNumberProperty.value, numberOfPages ); + + // if ( pageNumberProperty.value >= numberOfPages ) { + // pageNumberProperty.value = numberOfPages - 1; + // } + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + } + this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; items.forEach( item => { @@ -335,27 +377,13 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { + const numberOfPages = this.numberOfPagesProperty.value; + assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); // button state @@ -422,7 +450,7 @@ this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; + // this.numberOfPages = getPageCount(); this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; ```
samreid commented 1 year ago

@matthew-blackman and @samreid can spend 30 minutes to see if there is low-hanging fruit for improvements, but we can live with things as they are now if there aren't any quick improvements to be made.

In discussions, @jbphet and @arouinfar indicated that avoiding blank pages and fixing the page control are worth more time than the 30 minutes allotted. We would also like to understand the scope of what would be necessary to make all sims have flexible layout carousels (getting rid of the opt-in flag).

samreid commented 1 year ago

This patch is working well, hides and shows dots, hides and shows pages.

```diff Subject: [PATCH] Improve default value for listener, see https://github.com/phetsims/axon/issues/421 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672876539753) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision e46a6bb6c08dea9d85bbfb9067664c57c8891edf) +++ b/main/sun/js/CarouselComboBox.ts (date 1672876137345) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672876539762) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, Index: main/axon/js/DerivedProperty.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/axon/js/DerivedProperty.ts b/main/axon/js/DerivedProperty.ts --- a/main/axon/js/DerivedProperty.ts (revision a76bc17bcc8d6717789720abdfb81bdcd2009c63) +++ b/main/axon/js/DerivedProperty.ts (date 1672875381035) @@ -206,6 +206,14 @@ return DerivedProperty.deriveAny( properties, () => _.reduce( properties, andFunction, true ), options ); } + /** + * Creates a derived boolean Property whose value is true iff every input Property value is true. + */ + public static count( properties: TReadOnlyProperty[], options?: PropertyOptions ): UnknownDerivedProperty { + assert && assert( properties.length > 0, 'must provide a dependency' ); + return DerivedProperty.deriveAny( properties, () => properties.map( property => property.value ).length, options ); + } + /** * Creates a derived boolean Property whose value is true iff any input Property value is true. */ Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672876539772) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision e46a6bb6c08dea9d85bbfb9067664c57c8891edf) +++ b/main/sun/js/PageControl.ts (date 1672876702485) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 44d7215b50ecde2a51e99927939cac7f7b6b4564) +++ b/main/studio/js/Select.ts (date 1672876934899) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,21 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'content window' ); + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'outer window' ); + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +202,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 3bd5f25954e779ee90b00d5c53640dcf610e4031) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672875381041) @@ -64,7 +64,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision e46a6bb6c08dea9d85bbfb9067664c57c8891edf) +++ b/main/sun/js/Carousel.ts (date 1672877449072) @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -89,7 +91,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -237,6 +239,32 @@ } ) ) : new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( items.map( item => item.visibleProperty ), () => { + let numberOfPages = items.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + + numberOfPages = Math.floor( numberOfPages + 1 ); + } + + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); + + this.numberOfPagesProperty.debug( 'number of pages' ); + + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true + } ); + this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; items.forEach( item => { @@ -335,27 +363,13 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { + const numberOfPages = this.numberOfPagesProperty.value; + assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); // button state @@ -416,13 +430,25 @@ pageNumberProperty.link( pageNumberListener ); + if ( options.isScrollingNodeLayoutBox ) { + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + } + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision e46a6bb6c08dea9d85bbfb9067664c57c8891edf) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672876137348) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```

This also includes the patch from https://github.com/phetsims/studio/issues/283 for ease of testing. (Select.ts only). Next steps:

samreid commented 1 year ago

Design questions:

samreid commented 1 year ago

Current patch:

```diff Subject: [PATCH] Mark vertex positionProperty as phetioHighFrequency: true and update APIs, see https://github.com/phetsims/circuit-construction-kit-common/issues/898 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672946411558) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, Index: main/axon/js/DerivedProperty.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/axon/js/DerivedProperty.ts b/main/axon/js/DerivedProperty.ts --- a/main/axon/js/DerivedProperty.ts (revision b12c25b97d5cfee4429b3c93ff0ded3bd19f4dcd) +++ b/main/axon/js/DerivedProperty.ts (date 1672946411546) @@ -206,6 +206,14 @@ return DerivedProperty.deriveAny( properties, () => _.reduce( properties, andFunction, true ), options ); } + /** + * Creates a derived boolean Property whose value is true iff every input Property value is true. + */ + public static count( properties: TReadOnlyProperty[], options?: PropertyOptions ): UnknownDerivedProperty { + assert && assert( properties.length > 0, 'must provide a dependency' ); + return DerivedProperty.deriveAny( properties, () => properties.map( property => property.value ).length, options ); + } + /** * Creates a derived boolean Property whose value is true iff any input Property value is true. */ Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672946411563) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,21 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'content window' ); + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'outer window' ); + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +202,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672946411554) @@ -64,7 +64,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672948030087) @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -89,7 +91,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -120,7 +122,7 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, + isScrollingNodeLayoutBox: true, // items itemsPerPage: 4, @@ -237,53 +239,36 @@ } ) ) : new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( items.map( item => item.visibleProperty ), () => { + let numberOfPages = items.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + + numberOfPages = Math.floor( numberOfPages + 1 ); + } + + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); + + this.numberOfPagesProperty.debug( 'number of pages' ); + + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true + } ); + this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { - - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // scrollingNode.addChild( new HSeparator() ); } ); // How much to translate scrollingNode each time a next/previous button is pressed @@ -335,27 +320,13 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { + const numberOfPages = this.numberOfPagesProperty.value; + assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); // button state @@ -366,13 +337,22 @@ previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; + const scrollingNodeMargin = options.spacing;//options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + + // const targetValue = -pageNumber * scrollingDelta + scrollingNodeMargin; + const itemsInLayout = this.isScrollingNodeLayoutBox ? items.filter( item => item.visible ) : items; + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + const targetValue = isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top; // TODO: margin? + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +367,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,26 +386,38 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + if ( options.isScrollingNodeLayoutBox ) { + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + } + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; - options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; + options.children = [ backgroundNode, windowNode, foregroundNode ]; this.disposeCarousel = () => { pageNumberProperty.unlink( pageNumberListener ); Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
samreid commented 1 year ago

Progress with @jbphet and @matthew-blackman. Function builder is getting in better shape, but we still have margin problems when not using separators.

```diff Subject: [PATCH] Mark vertex positionProperty as phetioHighFrequency: true and update APIs, see https://github.com/phetsims/circuit-construction-kit-common/issues/898 --- Index: main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts b/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts --- a/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts (revision 9051cf3e44517216c3036437eab78010a593ae96) +++ b/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts (date 1672950274697) @@ -36,9 +36,11 @@ const range = radiusOfCurvatureMagnitudeProperty.range; + + debugger; const titleStringProperty = new DerivedProperty( [ radiusOfCurvatureProperty, - GeometricOpticsStrings.radiusOfCurvaturePositiveStringProperty, + GeometricOpticsStrings.radiusOfCurvaturePositive, GeometricOpticsStrings.radiusOfCurvatureNegativeStringProperty ], ( radiusOfCurvature: number, radiusOfCurvaturePositiveString: string, radiusOfCurvatureNegativeString: string ) => ( radiusOfCurvature >= 0 ) ? radiusOfCurvaturePositiveString : radiusOfCurvatureNegativeString, { Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672946411558) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, Index: main/axon/js/DerivedProperty.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/axon/js/DerivedProperty.ts b/main/axon/js/DerivedProperty.ts --- a/main/axon/js/DerivedProperty.ts (revision b12c25b97d5cfee4429b3c93ff0ded3bd19f4dcd) +++ b/main/axon/js/DerivedProperty.ts (date 1672946411546) @@ -206,6 +206,14 @@ return DerivedProperty.deriveAny( properties, () => _.reduce( properties, andFunction, true ), options ); } + /** + * Creates a derived boolean Property whose value is true iff every input Property value is true. + */ + public static count( properties: TReadOnlyProperty[], options?: PropertyOptions ): UnknownDerivedProperty { + assert && assert( properties.length > 0, 'must provide a dependency' ); + return DerivedProperty.deriveAny( properties, () => properties.map( property => property.value ).length, options ); + } + /** * Creates a derived boolean Property whose value is true iff any input Property value is true. */ Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672946411563) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,21 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'content window' ); + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'outer window' ); + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +202,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672946411554) @@ -64,7 +64,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672953986272) @@ -24,7 +24,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Path, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -89,7 +91,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -120,7 +122,7 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, + isScrollingNodeLayoutBox: true, // items itemsPerPage: 4, @@ -208,10 +210,6 @@ // Computations related to layout of items const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); // Options common to all separators const separatorOptions = { @@ -219,6 +217,10 @@ lineWidth: options.separatorLineWidth }; + const scrollingLength = items.length * ( maxItemLength + options.spacing ); + const scrollingWidth = isHorizontal ? scrollingLength : maxItemWidth; + const scrollingHeight = isHorizontal ? maxItemHeight : scrollingLength; + super(); // enables animation when scrolling between pages @@ -230,59 +232,49 @@ const scrollingNode = options.isScrollingNodeLayoutBox ? ( isHorizontal ? new HBox( { spacing: options.spacing, - yMargin: options.margin + // yMargin: options.margin } ) : new VBox( { spacing: options.spacing, - xMargin: options.margin + // xMargin: options.margin } ) ) : new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( items.map( item => item.visibleProperty ), () => { + let numberOfPages = items.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + + numberOfPages = Math.floor( numberOfPages + 1 ); + } + + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); + + this.numberOfPagesProperty.debug( 'number of pages' ); + + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true + } ); + this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } scrollingNode.addChild( item ); - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { - - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } + scrollingNode.addChild( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: scrollingHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: scrollingWidth + 2 * options.margin + } ) ) ); } } ); @@ -304,7 +296,7 @@ Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode, new Path( clipArea, { stroke: 'red' } ) ], clipArea: clipArea } ); @@ -335,27 +327,13 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { + const numberOfPages = this.numberOfPagesProperty.value; + assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); // button state @@ -366,13 +344,22 @@ previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; + // const scrollingNodeMargin = options.spacing / 2; // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + + // const targetValue = -pageNumber * scrollingDelta + scrollingNodeMargin; + const itemsInLayout = this.isScrollingNodeLayoutBox ? items.filter( item => item.visible ) : items; + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + const targetValue = ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin; // TODO: margin? + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +374,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,23 +393,35 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + if ( options.isScrollingNodeLayoutBox ) { + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + } + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
arouinfar commented 1 year ago

I noticed @samreid's design questions in https://github.com/phetsims/circuit-construction-kit-common/issues/630#issuecomment-1372592176

What if there is only one page in the carousel? Should we hide the carousel arrow buttons? (they are grayed out). Should we hide the dots? Clients can easily remove them manually. So maybe that is OK.

I don't think we need to automatically hide the previous/next buttons or the remaining page control dot. Clients can do it manually if they want to.

What if you remove all elements from the carousel? Should the carousel auto-hide? But the client can already set carousel.visibleProperty to false if they want. So maybe that is OK.

No need to auto hide the entire carousel. Clients should be using carousel.visibleProperty if that is their goal.

samreid commented 1 year ago

Fixes the margin when there are no separators:

```diff Subject: [PATCH] Mark vertex positionProperty as phetioHighFrequency: true and update APIs, see https://github.com/phetsims/circuit-construction-kit-common/issues/898 --- Index: main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts b/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts --- a/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts (revision 9051cf3e44517216c3036437eab78010a593ae96) +++ b/main/geometric-optics/js/common/view/RadiusOfCurvatureControl.ts (date 1672950274697) @@ -36,9 +36,11 @@ const range = radiusOfCurvatureMagnitudeProperty.range; + + debugger; const titleStringProperty = new DerivedProperty( [ radiusOfCurvatureProperty, - GeometricOpticsStrings.radiusOfCurvaturePositiveStringProperty, + GeometricOpticsStrings.radiusOfCurvaturePositive, GeometricOpticsStrings.radiusOfCurvatureNegativeStringProperty ], ( radiusOfCurvature: number, radiusOfCurvaturePositiveString: string, radiusOfCurvatureNegativeString: string ) => ( radiusOfCurvature >= 0 ) ? radiusOfCurvaturePositiveString : radiusOfCurvatureNegativeString, { Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672946411558) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, Index: main/axon/js/DerivedProperty.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/axon/js/DerivedProperty.ts b/main/axon/js/DerivedProperty.ts --- a/main/axon/js/DerivedProperty.ts (revision b12c25b97d5cfee4429b3c93ff0ded3bd19f4dcd) +++ b/main/axon/js/DerivedProperty.ts (date 1672946411546) @@ -206,6 +206,14 @@ return DerivedProperty.deriveAny( properties, () => _.reduce( properties, andFunction, true ), options ); } + /** + * Creates a derived boolean Property whose value is true iff every input Property value is true. + */ + public static count( properties: TReadOnlyProperty[], options?: PropertyOptions ): UnknownDerivedProperty { + assert && assert( properties.length > 0, 'must provide a dependency' ); + return DerivedProperty.deriveAny( properties, () => properties.map( property => property.value ).length, options ); + } + /** * Creates a derived boolean Property whose value is true iff any input Property value is true. */ Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672946411563) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,21 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'content window' ); + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + console.log( 'outer window' ); + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +202,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672946411554) @@ -64,7 +64,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672956827013) @@ -24,7 +24,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Path, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -89,7 +91,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -120,7 +122,7 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, + isScrollingNodeLayoutBox: true, // items itemsPerPage: 4, @@ -206,19 +208,16 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, lineWidth: options.separatorLineWidth }; + const scrollingLength = items.length * ( maxItemLength + options.spacing ); + const scrollingWidth = isHorizontal ? scrollingLength : maxItemWidth; + const scrollingHeight = isHorizontal ? maxItemHeight : scrollingLength; + super(); // enables animation when scrolling between pages @@ -227,62 +226,50 @@ // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + const scrollingNode = isHorizontal ? new HBox( { + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); + + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( items.map( item => item.visibleProperty ), () => { + let numberOfPages = items.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + + numberOfPages = Math.floor( numberOfPages + 1 ); + } + + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); + + this.numberOfPagesProperty.debug( 'number of pages' ); + + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true + } ); this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } scrollingNode.addChild( item ); - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { - - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } + scrollingNode.addChild( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: scrollingHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: scrollingWidth + 2 * options.margin + } ) ) ); } } ); @@ -304,7 +291,7 @@ Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode, new Path( clipArea, { stroke: 'red' } ) ], clipArea: clipArea } ); @@ -335,27 +322,13 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { + const numberOfPages = this.numberOfPagesProperty.value; + assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); // button state @@ -366,13 +339,23 @@ previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; + // const scrollingNodeMargin = options.spacing / 2; // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + + // const targetValue = -pageNumber * scrollingDelta + scrollingNodeMargin; + const itemsInLayout = this.isScrollingNodeLayoutBox ? items.filter( item => item.visible ) : items; + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin; + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +370,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,23 +389,35 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + if ( options.isScrollingNodeLayoutBox ) { + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + } + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
samreid commented 1 year ago

Removes the opt-in flag:

```diff Subject: [PATCH] Mark vertex positionProperty as phetioHighFrequency: true and update APIs, see https://github.com/phetsims/circuit-construction-kit-common/issues/898 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672946411558) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, Index: main/axon/js/DerivedProperty.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/axon/js/DerivedProperty.ts b/main/axon/js/DerivedProperty.ts --- a/main/axon/js/DerivedProperty.ts (revision b12c25b97d5cfee4429b3c93ff0ded3bd19f4dcd) +++ b/main/axon/js/DerivedProperty.ts (date 1672946411546) @@ -206,6 +206,14 @@ return DerivedProperty.deriveAny( properties, () => _.reduce( properties, andFunction, true ), options ); } + /** + * Creates a derived boolean Property whose value is true iff every input Property value is true. + */ + public static count( properties: TReadOnlyProperty[], options?: PropertyOptions ): UnknownDerivedProperty { + assert && assert( properties.length > 0, 'must provide a dependency' ); + return DerivedProperty.deriveAny( properties, () => properties.map( property => property.value ).length, options ); + } + /** * Creates a derived boolean Property whose value is true iff any input Property value is true. */ Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672946411554) @@ -64,7 +64,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672957870801) @@ -24,7 +24,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Path, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +47,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +90,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +103,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -120,7 +120,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -206,13 +205,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,66 +216,57 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + items.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( items.map( item => item.visibleProperty ), () => { + let numberOfPages = items.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; + numberOfPages = Math.floor( numberOfPages + 1 ); } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + this.numberOfPagesProperty.debug( 'number of pages' ); - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); // How much to translate scrollingNode each time a next/previous button is pressed @@ -304,7 +287,11 @@ Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode, + + // For debugging + new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,27 +322,13 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { + const numberOfPages = this.numberOfPagesProperty.value; + assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); // button state @@ -366,13 +339,22 @@ previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; + // const scrollingNodeMargin = options.spacing / 2; // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + const itemsInLayout = items.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +369,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,23 +388,33 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; @@ -466,7 +458,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
samreid commented 1 year ago

Use alignBox so that all items are the same size:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672946411558) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672946411554) @@ -64,7 +64,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672959458701) @@ -24,7 +24,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Path, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +47,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +90,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +103,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +110,18 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + const origItems = items; + + const alignGroup = new AlignGroup(); + items = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +132,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -206,13 +217,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,66 +228,57 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + items.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( items.map( item => item.visibleProperty ), () => { + let numberOfPages = items.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; + numberOfPages = Math.floor( numberOfPages + 1 ); } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + this.numberOfPagesProperty.debug( 'number of pages' ); - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); // How much to translate scrollingNode each time a next/previous button is pressed @@ -304,7 +299,11 @@ Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode, + + // For debugging + new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,27 +334,13 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { + const numberOfPages = this.numberOfPagesProperty.value; + assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); // button state @@ -366,13 +351,20 @@ previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + const itemsInLayout = items.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +379,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,23 +398,33 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + items.forEach( item => item.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); - this.items = items; + this.items = origItems; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; @@ -466,7 +468,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
samreid commented 1 year ago

Expression Exchange coins are not centered:

Current patch:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672946411558) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672961464116) @@ -54,9 +54,7 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), - - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +62,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672961672305) @@ -24,7 +24,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +33,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +47,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +90,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +103,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +110,16 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +130,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,8 +174,8 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // This quantity is used make some other computations independent of orientation. const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; @@ -206,13 +215,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,66 +226,54 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { - - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); - - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); // How much to translate scrollingNode each time a next/previous button is pressed @@ -304,7 +294,11 @@ Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode + + // For debugging + // new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,44 +329,35 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; if ( options.hideDisabledButtons ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +372,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,23 +391,33 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + alignBoxes.forEach( item => item.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; @@ -466,7 +461,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +470,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
image
samreid commented 1 year ago

Number play not quite right:

image

Everything else seems good, including function builder.

samreid commented 1 year ago

Expression exchange got a lot better in this patch:

image

Number play still cuts off a bit:

image

This patch computes the window size by beginning of item to end of item.

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672972876559) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, @@ -340,6 +340,7 @@ this.functionCarousel.animationEnabled = false; + // TODO: This calls a private attribute `items` this.functionCarousel.items.forEach( functionContainer => { // function container's position Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672961464116) @@ -54,9 +54,7 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), - - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +62,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672975370407) @@ -7,11 +7,10 @@ * Pressing the next and previous buttons moves through the pages. * Movement through the pages is animated, so that items appear to scroll by. * - * Note that Carousel performs layout directly on the items (Nodes) that it is provided. - * If those Nodes appear in multiple places in the scenegraph, then it's the client's - * responsibility to provide the Carousel with wrapped Nodes. + * Note that Carousel wraps each item (Node) in an alignBox to ensure all items have an equal "footprint" dimension. * * @author Chris Malley (PixelZoom, Inc.) + * @author Sam Reid (PhET Interactive Simulations) */ import NumberProperty from '../../axon/js/NumberProperty.js'; @@ -24,7 +23,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Path, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +32,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +46,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +89,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +102,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +109,16 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +129,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,11 +173,11 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // This quantity is used make some other computations independent of orientation. - const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; + // const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; // Options common to both buttons const buttonOptions = { @@ -206,13 +214,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,87 +225,69 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); - // How much to translate scrollingNode each time a next/previous button is pressed - let scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); - if ( options.separatorsVisible ) { - scrollingDelta += ( options.itemsPerPage * options.spacing ); - } - - // Clipping window, to show one page at a time. - // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. - let windowLength = ( scrollingDelta + options.spacing ); - if ( options.separatorsVisible ) { - windowLength -= options.spacing; - } + // Measure from the beginning of the first item to the end of the last item on the 1st page + const windowLength = isHorizontal ? + alignBoxes[ options.itemsPerPage - 1 ].right - alignBoxes[ 0 ].left + options.margin * 2 : + alignBoxes[ options.itemsPerPage - 1 ].bottom - alignBoxes[ 0 ].top + options.margin * 2; const windowWidth = isHorizontal ? windowLength : scrollingNode.width; const windowHeight = isHorizontal ? scrollingNode.height : windowLength; - const clipArea = isHorizontal ? - Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : - Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); + const clipArea = Shape.rectangle( 0, 0, windowWidth, windowHeight ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode, + + // For debugging + new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,44 +318,35 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; if ( options.hideDisabledButtons ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +361,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,23 +380,33 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + alignBoxes.forEach( item => item.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; @@ -466,7 +450,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +459,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
samreid commented 1 year ago

When the margin matches the spacing, it is better:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672972876559) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, @@ -340,6 +340,7 @@ this.functionCarousel.animationEnabled = false; + // TODO: This calls a private attribute `items` this.functionCarousel.items.forEach( functionContainer => { // function container's position Index: main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts --- a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (revision 9c8e8cf8bdffd476b011de5540055586484475ae) +++ b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (date 1672984038870) @@ -52,8 +52,8 @@ return new Node().addChild( numberCardCreatorNode ); } ), { itemsPerPage: 10, - margin: 14, - spacing: 8, + margin: 10, + spacing: 10, animationDuration: 0.4 } ); Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672946411577) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,7 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +114,11 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + // TODO dispose here and in Carousel + numberOfPagesProperty.link( numberOfPages => { + dotNode.visible = pageNumber < numberOfPages; + } ); } // Indicate which page is selected Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672961464116) @@ -54,9 +54,7 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), - - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +62,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672975370407) @@ -7,11 +7,10 @@ * Pressing the next and previous buttons moves through the pages. * Movement through the pages is animated, so that items appear to scroll by. * - * Note that Carousel performs layout directly on the items (Nodes) that it is provided. - * If those Nodes appear in multiple places in the scenegraph, then it's the client's - * responsibility to provide the Carousel with wrapped Nodes. + * Note that Carousel wraps each item (Node) in an alignBox to ensure all items have an equal "footprint" dimension. * * @author Chris Malley (PixelZoom, Inc.) + * @author Sam Reid (PhET Interactive Simulations) */ import NumberProperty from '../../axon/js/NumberProperty.js'; @@ -24,7 +23,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Path, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +32,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +46,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +89,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +102,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +109,16 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +129,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,11 +173,11 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // This quantity is used make some other computations independent of orientation. - const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; + // const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; // Options common to both buttons const buttonOptions = { @@ -206,13 +214,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,87 +225,69 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); - // How much to translate scrollingNode each time a next/previous button is pressed - let scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); - if ( options.separatorsVisible ) { - scrollingDelta += ( options.itemsPerPage * options.spacing ); - } - - // Clipping window, to show one page at a time. - // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. - let windowLength = ( scrollingDelta + options.spacing ); - if ( options.separatorsVisible ) { - windowLength -= options.spacing; - } + // Measure from the beginning of the first item to the end of the last item on the 1st page + const windowLength = isHorizontal ? + alignBoxes[ options.itemsPerPage - 1 ].right - alignBoxes[ 0 ].left + options.margin * 2 : + alignBoxes[ options.itemsPerPage - 1 ].bottom - alignBoxes[ 0 ].top + options.margin * 2; const windowWidth = isHorizontal ? windowLength : scrollingNode.width; const windowHeight = isHorizontal ? scrollingNode.height : windowLength; - const clipArea = isHorizontal ? - Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : - Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); + const clipArea = Shape.rectangle( 0, 0, windowWidth, windowHeight ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode, + + // For debugging + new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,44 +318,35 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; if ( options.hideDisabledButtons ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { // options that are independent of orientation @@ -387,14 +361,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,23 +380,33 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + alignBoxes.forEach( item => item.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; @@ -466,7 +450,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +459,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
image
samreid commented 1 year ago

Implement disposal and avoid animation during initialization:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672985040916) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, @@ -340,6 +340,7 @@ this.functionCarousel.animationEnabled = false; + // TODO: This calls a private attribute `items`, see https://github.com/phetsims/function-builder/issues/152 this.functionCarousel.items.forEach( functionContainer => { // function container's position Index: main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts --- a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (revision 9c8e8cf8bdffd476b011de5540055586484475ae) +++ b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (date 1672984038870) @@ -52,8 +52,8 @@ return new Node().addChild( numberCardCreatorNode ); } ), { itemsPerPage: 10, - margin: 14, - spacing: 8, + margin: 10, + spacing: 10, animationDuration: 0.4 } ); Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672984396876) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,8 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + const dotListeners: ( ( numberOfPages: number ) => void )[] = []; + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +115,12 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + const dotListener = ( numberOfPages: number ) => { + dotNode.visible = pageNumber < numberOfPages; + }; + numberOfPagesProperty.link( dotListener ); + dotListeners.push( dotListener ); } // Indicate which page is selected @@ -136,6 +144,7 @@ this.disposePageControl = () => { pageNumberProperty.unlink( pageNumberObserver ); + dotListeners.forEach( dotListener => numberOfPagesProperty.unlink( dotListener ) ); }; } Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672961464116) @@ -54,9 +54,7 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), - - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +62,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672985430630) @@ -7,11 +7,10 @@ * Pressing the next and previous buttons moves through the pages. * Movement through the pages is animated, so that items appear to scroll by. * - * Note that Carousel performs layout directly on the items (Nodes) that it is provided. - * If those Nodes appear in multiple places in the scenegraph, then it's the client's - * responsibility to provide the Carousel with wrapped Nodes. + * Note that Carousel wraps each item (Node) in an alignBox to ensure all items have an equal "footprint" dimension. * * @author Chris Malley (PixelZoom, Inc.) + * @author Sam Reid (PhET Interactive Simulations) */ import NumberProperty from '../../axon/js/NumberProperty.js'; @@ -24,7 +23,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Path, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +32,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +46,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +89,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +102,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +109,19 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + // Don't animate layout during initialization + let isInitialized = false; + + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +132,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,11 +176,8 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; - - // This quantity is used make some other computations independent of orientation. - const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // Options common to both buttons const buttonOptions = { @@ -206,13 +214,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,87 +225,69 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); - // How much to translate scrollingNode each time a next/previous button is pressed - let scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); - if ( options.separatorsVisible ) { - scrollingDelta += ( options.itemsPerPage * options.spacing ); - } - - // Clipping window, to show one page at a time. - // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. - let windowLength = ( scrollingDelta + options.spacing ); - if ( options.separatorsVisible ) { - windowLength -= options.spacing; - } + // Measure from the beginning of the first item to the end of the last item on the 1st page + const windowLength = isHorizontal ? + alignBoxes[ options.itemsPerPage - 1 ].right - alignBoxes[ 0 ].left + options.margin * 2 : + alignBoxes[ options.itemsPerPage - 1 ].bottom - alignBoxes[ 0 ].top + options.margin * 2; const windowWidth = isHorizontal ? windowLength : scrollingNode.width; const windowHeight = isHorizontal ? scrollingNode.height : windowLength; - const clipArea = isHorizontal ? - Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : - Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); + const clipArea = Shape.rectangle( 0, 0, windowWidth, windowHeight ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode, + + // For debugging + new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,45 +318,37 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; if ( options.hideDisabledButtons ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page - if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + + // Do not animate during initialization. + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value && isInitialized ) { // options that are independent of orientation let animationOptions = { @@ -387,14 +362,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,33 +381,55 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + // const numberOfPages = this.numberOfPagesProperty.value; + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + + // NOTE: the alignBox visibleProperty is the same as the item Node visibleProperty + alignBoxes.forEach( alignBox => alignBox.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; this.disposeCarousel = () => { pageNumberProperty.unlink( pageNumberListener ); + + // There are 2 problems to be aware of for the alignBox disposal. + // 1. Each alignBox has a visibleProperty of the wrapped item Node, so that must be disconnected + // 2. We link to the updatePageCount method above, so we must unlink here anyways + alignBoxes.forEach( alignBox => { + alignBox.visibleProperty.unlink( updatePageCount ); + alignBox.dispose(); + } ); }; this.mutate( options ); + isInitialized = true; + // support for binder documentation, stripped out in builds and only runs when ?binder is specified assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'Carousel', this ); } @@ -466,7 +463,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +472,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```

Next up (roughly in this order):

samreid commented 1 year ago

Patch: Hide the buttons if there is only one page:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672985040916) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, @@ -340,6 +340,7 @@ this.functionCarousel.animationEnabled = false; + // TODO: This calls a private attribute `items`, see https://github.com/phetsims/function-builder/issues/152 this.functionCarousel.items.forEach( functionContainer => { // function container's position Index: main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts --- a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (revision 9c8e8cf8bdffd476b011de5540055586484475ae) +++ b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (date 1672984038870) @@ -52,8 +52,8 @@ return new Node().addChild( numberCardCreatorNode ); } ), { itemsPerPage: 10, - margin: 14, - spacing: 8, + margin: 10, + spacing: 10, animationDuration: 0.4 } ); Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672984396876) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -91,7 +92,8 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + const dotListeners: ( ( numberOfPages: number ) => void )[] = []; + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +115,12 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + const dotListener = ( numberOfPages: number ) => { + dotNode.visible = pageNumber < numberOfPages; + }; + numberOfPagesProperty.link( dotListener ); + dotListeners.push( dotListener ); } // Indicate which page is selected @@ -136,6 +144,7 @@ this.disposePageControl = () => { pageNumberProperty.unlink( pageNumberObserver ); + dotListeners.forEach( dotListener => numberOfPagesProperty.unlink( dotListener ) ); }; } Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672961464116) @@ -54,9 +54,7 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), - - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +62,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672985838207) @@ -7,11 +7,10 @@ * Pressing the next and previous buttons moves through the pages. * Movement through the pages is animated, so that items appear to scroll by. * - * Note that Carousel performs layout directly on the items (Nodes) that it is provided. - * If those Nodes appear in multiple places in the scenegraph, then it's the client's - * responsibility to provide the Carousel with wrapped Nodes. + * Note that Carousel wraps each item (Node) in an alignBox to ensure all items have an equal "footprint" dimension. * * @author Chris Malley (PixelZoom, Inc.) + * @author Sam Reid (PhET Interactive Simulations) */ import NumberProperty from '../../axon/js/NumberProperty.js'; @@ -24,7 +23,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +32,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +46,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +89,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +102,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +109,19 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + // Don't animate layout during initialization + let isInitialized = false; + + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +132,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,11 +176,8 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; - - // This quantity is used make some other computations independent of orientation. - const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // Options common to both buttons const buttonOptions = { @@ -206,13 +214,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,87 +225,69 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); - // How much to translate scrollingNode each time a next/previous button is pressed - let scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); - if ( options.separatorsVisible ) { - scrollingDelta += ( options.itemsPerPage * options.spacing ); - } - - // Clipping window, to show one page at a time. - // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. - let windowLength = ( scrollingDelta + options.spacing ); - if ( options.separatorsVisible ) { - windowLength -= options.spacing; - } + // Measure from the beginning of the first item to the end of the last item on the 1st page + const windowLength = isHorizontal ? + alignBoxes[ options.itemsPerPage - 1 ].right - alignBoxes[ 0 ].left + options.margin * 2 : + alignBoxes[ options.itemsPerPage - 1 ].bottom - alignBoxes[ 0 ].top + options.margin * 2; const windowWidth = isHorizontal ? windowLength : scrollingNode.width; const windowHeight = isHorizontal ? scrollingNode.height : windowLength; - const clipArea = isHorizontal ? - Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : - Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); + const clipArea = Shape.rectangle( 0, 0, windowWidth, windowHeight ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode + + // For debugging + // ,new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,45 +318,37 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; - if ( options.hideDisabledButtons ) { + if ( options.hideDisabledButtons || this.numberOfPagesProperty.value === 1 ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page - if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + + // Do not animate during initialization. + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value && isInitialized ) { // options that are independent of orientation let animationOptions = { @@ -387,14 +362,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,33 +381,54 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + + // NOTE: the alignBox visibleProperty is the same as the item Node visibleProperty + alignBoxes.forEach( alignBox => alignBox.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; this.disposeCarousel = () => { pageNumberProperty.unlink( pageNumberListener ); + + // There are 2 problems to be aware of for the alignBox disposal. + // 1. Each alignBox has a visibleProperty of the wrapped item Node, so that must be disconnected + // 2. We link to the updatePageCount method above, so we must unlink here anyways + alignBoxes.forEach( alignBox => { + alignBox.visibleProperty.unlink( updatePageCount ); + alignBox.dispose(); + } ); }; this.mutate( options ); + isInitialized = true; + // support for binder documentation, stripped out in builds and only runs when ?binder is specified assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'Carousel', this ); } @@ -466,7 +462,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +471,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
image
samreid commented 1 year ago

Recenter the dots as the number of pages changes:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672985040916) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, @@ -340,6 +340,7 @@ this.functionCarousel.animationEnabled = false; + // TODO: This calls a private attribute `items`, see https://github.com/phetsims/function-builder/issues/152 this.functionCarousel.items.forEach( functionContainer => { // function container's position Index: main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts --- a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (revision 9c8e8cf8bdffd476b011de5540055586484475ae) +++ b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (date 1672984038870) @@ -52,8 +52,8 @@ return new Node().addChild( numberCardCreatorNode ); } ), { itemsPerPage: 10, - margin: 14, - spacing: 8, + margin: 10, + spacing: 10, animationDuration: 0.4 } ); Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672986419015) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -68,7 +69,10 @@ tandemNameSuffix: 'PageControl', visiblePropertyOptions: { phetioFeatured: true - } + }, + + // When placed in a VBox or HBox, it will re-center when the number of pages changes + excludeInvisibleChildrenFromBounds: true }, providedOptions ); // validate options @@ -91,7 +95,8 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + const dotListeners: ( ( numberOfPages: number ) => void )[] = []; + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +118,12 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + const dotListener = ( numberOfPages: number ) => { + dotNode.visible = pageNumber < numberOfPages; + }; + numberOfPagesProperty.link( dotListener ); + dotListeners.push( dotListener ); } // Indicate which page is selected @@ -136,6 +147,7 @@ this.disposePageControl = () => { pageNumberProperty.unlink( pageNumberObserver ); + dotListeners.forEach( dotListener => numberOfPagesProperty.unlink( dotListener ) ); }; } Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672986345529) @@ -54,9 +54,7 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), - - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +62,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672985838207) @@ -7,11 +7,10 @@ * Pressing the next and previous buttons moves through the pages. * Movement through the pages is animated, so that items appear to scroll by. * - * Note that Carousel performs layout directly on the items (Nodes) that it is provided. - * If those Nodes appear in multiple places in the scenegraph, then it's the client's - * responsibility to provide the Carousel with wrapped Nodes. + * Note that Carousel wraps each item (Node) in an alignBox to ensure all items have an equal "footprint" dimension. * * @author Chris Malley (PixelZoom, Inc.) + * @author Sam Reid (PhET Interactive Simulations) */ import NumberProperty from '../../axon/js/NumberProperty.js'; @@ -24,7 +23,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +32,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +46,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +89,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +102,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +109,19 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + // Don't animate layout during initialization + let isInitialized = false; + + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +132,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,11 +176,8 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; - - // This quantity is used make some other computations independent of orientation. - const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // Options common to both buttons const buttonOptions = { @@ -206,13 +214,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,87 +225,69 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); - // How much to translate scrollingNode each time a next/previous button is pressed - let scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); - if ( options.separatorsVisible ) { - scrollingDelta += ( options.itemsPerPage * options.spacing ); - } - - // Clipping window, to show one page at a time. - // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. - let windowLength = ( scrollingDelta + options.spacing ); - if ( options.separatorsVisible ) { - windowLength -= options.spacing; - } + // Measure from the beginning of the first item to the end of the last item on the 1st page + const windowLength = isHorizontal ? + alignBoxes[ options.itemsPerPage - 1 ].right - alignBoxes[ 0 ].left + options.margin * 2 : + alignBoxes[ options.itemsPerPage - 1 ].bottom - alignBoxes[ 0 ].top + options.margin * 2; const windowWidth = isHorizontal ? windowLength : scrollingNode.width; const windowHeight = isHorizontal ? scrollingNode.height : windowLength; - const clipArea = isHorizontal ? - Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : - Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); + const clipArea = Shape.rectangle( 0, 0, windowWidth, windowHeight ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode + + // For debugging + // ,new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,45 +318,37 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; - if ( options.hideDisabledButtons ) { + if ( options.hideDisabledButtons || this.numberOfPagesProperty.value === 1 ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page - if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + + // Do not animate during initialization. + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value && isInitialized ) { // options that are independent of orientation let animationOptions = { @@ -387,14 +362,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,33 +381,54 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + + // NOTE: the alignBox visibleProperty is the same as the item Node visibleProperty + alignBoxes.forEach( alignBox => alignBox.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; this.disposeCarousel = () => { pageNumberProperty.unlink( pageNumberListener ); + + // There are 2 problems to be aware of for the alignBox disposal. + // 1. Each alignBox has a visibleProperty of the wrapped item Node, so that must be disconnected + // 2. We link to the updatePageCount method above, so we must unlink here anyways + alignBoxes.forEach( alignBox => { + alignBox.visibleProperty.unlink( updatePageCount ); + alignBox.dispose(); + } ); }; this.mutate( options ); + isInitialized = true; + // support for binder documentation, stripped out in builds and only runs when ?binder is specified assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'Carousel', this ); } @@ -466,7 +462,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +471,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
samreid commented 1 year ago

Current patch, hopefully the same as the one above:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672985040916) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, @@ -340,6 +340,7 @@ this.functionCarousel.animationEnabled = false; + // TODO: This calls a private attribute `items`, see https://github.com/phetsims/function-builder/issues/152 this.functionCarousel.items.forEach( functionContainer => { // function container's position Index: main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts --- a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (revision 9c8e8cf8bdffd476b011de5540055586484475ae) +++ b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (date 1672984038870) @@ -52,8 +52,8 @@ return new Node().addChild( numberCardCreatorNode ); } ), { itemsPerPage: 10, - margin: 14, - spacing: 8, + margin: 10, + spacing: 10, animationDuration: 0.4 } ); Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672986419015) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -68,7 +69,10 @@ tandemNameSuffix: 'PageControl', visiblePropertyOptions: { phetioFeatured: true - } + }, + + // When placed in a VBox or HBox, it will re-center when the number of pages changes + excludeInvisibleChildrenFromBounds: true }, providedOptions ); // validate options @@ -91,7 +95,8 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + const dotListeners: ( ( numberOfPages: number ) => void )[] = []; + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +118,12 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + const dotListener = ( numberOfPages: number ) => { + dotNode.visible = pageNumber < numberOfPages; + }; + numberOfPagesProperty.link( dotListener ); + dotListeners.push( dotListener ); } // Indicate which page is selected @@ -136,6 +147,7 @@ this.disposePageControl = () => { pageNumberProperty.unlink( pageNumberObserver ); + dotListeners.forEach( dotListener => numberOfPagesProperty.unlink( dotListener ) ); }; } Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1672986345529) @@ -54,9 +54,7 @@ // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), - - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +62,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1672985838207) @@ -7,11 +7,10 @@ * Pressing the next and previous buttons moves through the pages. * Movement through the pages is animated, so that items appear to scroll by. * - * Note that Carousel performs layout directly on the items (Nodes) that it is provided. - * If those Nodes appear in multiple places in the scenegraph, then it's the client's - * responsibility to provide the Carousel with wrapped Nodes. + * Note that Carousel wraps each item (Node) in an alignBox to ensure all items have an equal "footprint" dimension. * * @author Chris Malley (PixelZoom, Inc.) + * @author Sam Reid (PhET Interactive Simulations) */ import NumberProperty from '../../axon/js/NumberProperty.js'; @@ -24,7 +23,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +32,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +46,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +89,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +102,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +109,19 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + // Don't animate layout during initialization + let isInitialized = false; + + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +132,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,11 +176,8 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; - - // This quantity is used make some other computations independent of orientation. - const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // Options common to both buttons const buttonOptions = { @@ -206,13 +214,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,87 +225,69 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); - // How much to translate scrollingNode each time a next/previous button is pressed - let scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); - if ( options.separatorsVisible ) { - scrollingDelta += ( options.itemsPerPage * options.spacing ); - } - - // Clipping window, to show one page at a time. - // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. - let windowLength = ( scrollingDelta + options.spacing ); - if ( options.separatorsVisible ) { - windowLength -= options.spacing; - } + // Measure from the beginning of the first item to the end of the last item on the 1st page + const windowLength = isHorizontal ? + alignBoxes[ options.itemsPerPage - 1 ].right - alignBoxes[ 0 ].left + options.margin * 2 : + alignBoxes[ options.itemsPerPage - 1 ].bottom - alignBoxes[ 0 ].top + options.margin * 2; const windowWidth = isHorizontal ? windowLength : scrollingNode.width; const windowHeight = isHorizontal ? scrollingNode.height : windowLength; - const clipArea = isHorizontal ? - Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : - Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); + const clipArea = Shape.rectangle( 0, 0, windowWidth, windowHeight ); const windowNode = new Node( { - children: [ scrollingNode ], + children: [ scrollingNode + + // For debugging + // ,new Path( clipArea, { stroke: 'red', pickable: false } ) + ], clipArea: clipArea } ); @@ -335,45 +318,37 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; - if ( options.hideDisabledButtons ) { + if ( options.hideDisabledButtons || this.numberOfPagesProperty.value === 1 ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page - if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + + // Do not animate during initialization. + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value && isInitialized ) { // options that are independent of orientation let animationOptions = { @@ -387,14 +362,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,33 +381,54 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + + // NOTE: the alignBox visibleProperty is the same as the item Node visibleProperty + alignBoxes.forEach( alignBox => alignBox.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; this.disposeCarousel = () => { pageNumberProperty.unlink( pageNumberListener ); + + // There are 2 problems to be aware of for the alignBox disposal. + // 1. Each alignBox has a visibleProperty of the wrapped item Node, so that must be disconnected + // 2. We link to the updatePageCount method above, so we must unlink here anyways + alignBoxes.forEach( alignBox => { + alignBox.visibleProperty.unlink( updatePageCount ); + alignBox.dispose(); + } ); }; this.mutate( options ); + isInitialized = true; + // support for binder documentation, stripped out in builds and only runs when ?binder is specified assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'Carousel', this ); } @@ -466,7 +462,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +471,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, ```
samreid commented 1 year ago

Move up/Move down on items didn't seem to work, after removing several items in studio. Could this be because it is moving the item within the alignBox instead of moving the alignBox. Also possible: separators?

@matthew-blackman and I found that the problem is wrapping each node in the Carousel. That was helpful in getting the layout right, but it interferes with IndexedNodeIO. We experimented with making IndexedNodeIO work on the grandparent index and it was mostly working OK. But would need more work to deal with separators and getting this to production. This was not a requested feature, and only added in because it was easy in the 1st draft. So here is our patch for IndexedNodeIO that works on parent index:

```diff // Copyright 2020-2022, University of Colorado Boulder /** * IO Type for Nodes that can save their own index (if phetioState: true). Can be used to customize z-order * or layout order. * * This IOType supports PhET-iO state, but only when every child within a Node's children array is an IndexedNodeIO * and is stateful (`phetioState: true`). This applyState algorithm uses Node "swaps" instead of index-based inserts * to ensure that by the end of state setting, all Nodes are in the correct order. * see https://github.com/phetsims/scenery/issues/1252#issuecomment-888014859 for more information. * * @author Sam Reid (PhET Interactive Simulations) */ import FunctionIO from '../../../tandem/js/types/FunctionIO.js'; import IOType from '../../../tandem/js/types/IOType.js'; import NullableIO from '../../../tandem/js/types/NullableIO.js'; import NumberIO from '../../../tandem/js/types/NumberIO.js'; import VoidIO from '../../../tandem/js/types/VoidIO.js'; import { Node, scenery } from '../imports.js'; // In order to support unlinking from listening to the index property, keep an indexed map to callback functions const map = {}; // The next index at which a callback will appear in the map. This always increments and we do reuse old indices let index = 0; const IndexedNodeIO = new IOType( 'IndexedNodeIO', { valueType: Node, documentation: 'Node that can be moved forward/back by index, which specifies z-order and/or layout order', supertype: Node.NodeIO, toStateObject: node => { const stateObject = {}; if ( node.parents[ 0 ] && node.parents[ 0 ].parents[ 0 ] ) { // assert && assert( node.parents.length === 1, 'IndexedNodeIO only supports nodes with a single parent' ); stateObject.index = node.parents[ 0 ].parents[ 0 ].indexOfChild( node.parents[ 0 ] ); } else { stateObject.index = null; } return stateObject; }, applyState: ( node, stateObject ) => { const nodeGrandparent = node.parents[ 0 ].parents[ 0 ]; if ( nodeGrandparent && stateObject.index ) { assert && assert( node.parents[ 0 ].parents.length === 1, 'IndexedNodeIO only supports nodes with a single parent' ); // Swap the child at the destination index with current position of this Node, that way the operation is atomic. // This implementation assumes that all children are instrumented IndexedNodeIO instances and can have state set // on them to "fix them" after this operation. Without this implementation, using Node.moveChildToIndex could blow // away another IndexedNode state set. See https://github.com/phetsims/ph-scale/issues/227 const children = nodeGrandparent.children; const currentIndex = nodeGrandparent.indexOfChild( node.parents[ 0 ] ); children[ currentIndex ] = children[ stateObject.index ]; children[ stateObject.index ] = node; nodeGrandparent.setChildren( children ); } }, stateSchema: { index: NullableIO( NumberIO ) }, methods: { linkIndex: { returnType: NumberIO, parameterTypes: [ FunctionIO( VoidIO, [ NumberIO ] ) ], documentation: 'Following the PropertyIO.link pattern, subscribe for notifications when the index in the parent ' + 'changes, and receive a callback with the current value. The return value is a numeric ID for use ' + 'with clearLinkIndex.', implementation: function( listener ) { // The callback which signifies the current index const callback = () => { // assert && assert( this.parents.length === 1, 'IndexedNodeIO only supports nodes with a single parent' ); const index = this.parents[ 0 ].parents[ 0 ].indexOfChild( this.parents[ 0 ] ); listener( index ); }; // assert && assert( this.parents.length === 1, 'IndexedNodeIO only supports nodes with a single parent' ); this.parents[ 0 ].parents[ 0 ].childrenChangedEmitter.addListener( callback ); callback(); const myIndex = index; map[ myIndex ] = callback; index++; return myIndex; } }, clearLinkIndex: { returnType: VoidIO, parameterTypes: [ NumberIO ], documentation: 'Unlink a listener that has been added using linkIndex, by its numerical ID (like setTimeout/clearTimeout)', implementation: function( index ) { const method = map[ index ]; // assert && assert( this.parents.length === 1, 'IndexedNodeIO only supports nodes with a single parent' ); this.parents[ 0 ].parents[ 0 ].childrenChangedEmitter.removeListener( method ); delete map[ index ]; } }, moveForward: { returnType: VoidIO, parameterTypes: [], implementation: function() { this.parents[ 0 ].moveForward(); this.parents[ 0 ].moveForward(); }, documentation: 'Move this node one index forward in each of its parents. If the node is already at the front, this is a no-op.' }, moveBackward: { returnType: VoidIO, parameterTypes: [], implementation: function() { this.parents[ 0 ].moveBackward(); this.parents[ 0 ].moveBackward(); }, documentation: 'Move this node one index backward in each of its parents. If the node is already at the back, this is a no-op.' } } } ); scenery.register( 'IndexedNodeIO', IndexedNodeIO ); export default IndexedNodeIO; ```
samreid commented 1 year ago

Current patch:

```diff Subject: [PATCH] Add DerivedProperty.count, see https://github.com/phetsims/circuit-construction-kit-common/issues/630 --- Index: main/joist/js/preferences/LanguageSelectionNode.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/joist/js/preferences/LanguageSelectionNode.ts b/main/joist/js/preferences/LanguageSelectionNode.ts --- a/main/joist/js/preferences/LanguageSelectionNode.ts (revision 209e7076ccf0d6a046d3655719e3779856341601) +++ b/main/joist/js/preferences/LanguageSelectionNode.ts (date 1673037450749) @@ -8,7 +8,7 @@ */ import joist from '../joist.js'; -import { FireListener, HighlightOverlay, Rectangle, Text } from '../../../scenery/js/imports.js'; +import { Color, FireListener, HighlightOverlay, Rectangle, Text } from '../../../scenery/js/imports.js'; import localeInfoModule from '../../../chipper/js/data/localeInfoModule.js'; import Tandem from '../../../tandem/js/Tandem.js'; import PreferencesDialog from './PreferencesDialog.js'; @@ -47,7 +47,7 @@ fireListener.isOverProperty.link( isOver => { // makes the mouse interactive - this.stroke = isOver ? HighlightOverlay.getInnerGroupHighlightColor() : null; + this.stroke = isOver ? HighlightOverlay.getInnerGroupHighlightColor() : Color.TRANSPARENT; } ); const localeListener = ( selectedLocale: string ) => { Index: main/number-line-operations/js/common/view/OperationEntryCarousel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-line-operations/js/common/view/OperationEntryCarousel.js b/main/number-line-operations/js/common/view/OperationEntryCarousel.js --- a/main/number-line-operations/js/common/view/OperationEntryCarousel.js (revision e4aea8713f70d4193ac8867d253b22b055287a86) +++ b/main/number-line-operations/js/common/view/OperationEntryCarousel.js (date 1672946411561) @@ -64,7 +64,7 @@ } ); // page indicator - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, centerX: carousel.centerX Index: main/sun/js/CarouselComboBox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/CarouselComboBox.ts b/main/sun/js/CarouselComboBox.ts --- a/main/sun/js/CarouselComboBox.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/CarouselComboBox.ts (date 1672946411574) @@ -127,8 +127,8 @@ // page control let pageControl: PageControl | null = null; - if ( carousel.numberOfPages > 1 ) { - pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, combineOptions( { + if ( carousel.numberOfPagesProperty.value > 1 ) { + pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, combineOptions( { orientation: options.carouselOptions.orientation }, options.pageControlOptions ) ); hBoxChildren.push( pageControl ); Index: main/function-builder/js/common/view/SceneNode.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/function-builder/js/common/view/SceneNode.js b/main/function-builder/js/common/view/SceneNode.js --- a/main/function-builder/js/common/view/SceneNode.js (revision cccd761124fef78429a8fc8ba28577133f648ed3) +++ b/main/function-builder/js/common/view/SceneNode.js (date 1672985040916) @@ -110,7 +110,7 @@ } ); // Page control for input carousel - const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPages, merge( { + const inputPageControl = new PageControl( inputCarousel.pageNumberProperty, inputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', right: inputCarousel.left - PAGE_CONTROL_SPACING, centerY: inputCarousel.centerY @@ -137,7 +137,7 @@ } ); // Page control for output carousel - const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPages, merge( { + const outputPageControl = new PageControl( outputCarousel.pageNumberProperty, outputCarousel.numberOfPagesProperty, merge( { orientation: 'vertical', left: outputCarousel.right + PAGE_CONTROL_SPACING, centerY: outputCarousel.centerY @@ -178,7 +178,7 @@ } ); // Page control for function carousel - const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPages, merge( { + const functionPageControl = new PageControl( functionCarousel.pageNumberProperty, functionCarousel.numberOfPagesProperty, merge( { visible: options.functionCarouselVisible, orientation: 'horizontal', centerX: functionCarousel.centerX, @@ -340,6 +340,7 @@ this.functionCarousel.animationEnabled = false; + // TODO: This calls a private attribute `items`, see https://github.com/phetsims/function-builder/issues/152 this.functionCarousel.items.forEach( functionContainer => { // function container's position Index: main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts --- a/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (revision 9c8e8cf8bdffd476b011de5540055586484475ae) +++ b/main/number-suite-common/js/lab/view/NumberCardCreatorCarousel.ts (date 1672984038870) @@ -52,8 +52,8 @@ return new Node().addChild( numberCardCreatorNode ); } ), { itemsPerPage: 10, - margin: 14, - spacing: 8, + margin: 10, + spacing: 10, animationDuration: 0.4 } ); Index: main/build-a-molecule/js/common/view/KitPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/build-a-molecule/js/common/view/KitPanel.js b/main/build-a-molecule/js/common/view/KitPanel.js --- a/main/build-a-molecule/js/common/view/KitPanel.js (revision 8b6bc0c2c7697a83373841cac9613fc7a0175114) +++ b/main/build-a-molecule/js/common/view/KitPanel.js (date 1672946411551) @@ -65,7 +65,7 @@ this.addChild( this.kitCarousel ); // Page control for input carousel - const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPages, { + const inputPageControl = new PageControl( this.kitCarousel.pageNumberProperty, this.kitCarousel.numberOfPagesProperty, { top: this.kitCarousel.bottom + BAMConstants.VIEW_PADDING / 2, centerX: this.kitCarousel.centerX, pageFill: Color.WHITE, Index: main/sun/js/PageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/PageControl.ts b/main/sun/js/PageControl.ts --- a/main/sun/js/PageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/PageControl.ts (date 1672986419015) @@ -14,6 +14,7 @@ import { Circle, CircleOptions, TColor, Node, NodeOptions, PressListener, PressListenerEvent } from '../../scenery/js/imports.js'; import Tandem from '../../tandem/js/Tandem.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; type SelfOptions = { interactive?: boolean; // {boolean} whether the control is interactive @@ -43,10 +44,10 @@ /** * @param pageNumberProperty - which page is currently visible - * @param numberOfPages - number of pages + * @param numberOfPagesProperty - number of pages * @param providedOptions */ - public constructor( pageNumberProperty: TProperty, numberOfPages: number, providedOptions: PageControlOptions ) { + public constructor( pageNumberProperty: TProperty, numberOfPagesProperty: ReadOnlyProperty, providedOptions: PageControlOptions ) { const options = optionize()( { @@ -68,7 +69,10 @@ tandemNameSuffix: 'PageControl', visiblePropertyOptions: { phetioFeatured: true - } + }, + + // When placed in a VBox or HBox, it will re-center when the number of pages changes + excludeInvisibleChildrenFromBounds: true }, providedOptions ); // validate options @@ -91,7 +95,8 @@ // For horizontal orientation, pages are ordered left-to-right. // For vertical orientation, pages are ordered top-to-bottom. const dotNodes: DotNode[] = []; - for ( let pageNumber = 0; pageNumber < numberOfPages; pageNumber++ ) { + const dotListeners: ( ( numberOfPages: number ) => void )[] = []; + for ( let pageNumber = 0; pageNumber < numberOfPagesProperty.value; pageNumber++ ) { // dot const dotCenter = ( pageNumber * ( 2 * options.dotRadius + options.dotSpacing ) ); @@ -113,6 +118,12 @@ dotNode.cursor = 'pointer'; dotNode.addInputListener( pressListener ); } + + const dotListener = ( numberOfPages: number ) => { + dotNode.visible = pageNumber < numberOfPages; + }; + numberOfPagesProperty.link( dotListener ); + dotListeners.push( dotListener ); } // Indicate which page is selected @@ -136,6 +147,7 @@ this.disposePageControl = () => { pageNumberProperty.unlink( pageNumberObserver ); + dotListeners.forEach( dotListener => numberOfPagesProperty.unlink( dotListener ) ); }; } Index: main/studio/js/Select.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/studio/js/Select.ts b/main/studio/js/Select.ts --- a/main/studio/js/Select.ts (revision 4651e5748f6e86526c2e3b25d7723a4cea2a3e22) +++ b/main/studio/js/Select.ts (date 1672957859074) @@ -26,6 +26,7 @@ import RootTreeNode from './RootTreeNode.js'; import PhetioElement from './PhetioElement.js'; import { ScreenState, SimInfoState } from '../../joist/js/SimInfo.js'; +import studio from './studio.js'; const simFrame = document.getElementById( 'sim-frame' ) as HTMLIFrameElement; @@ -111,6 +112,19 @@ simFrame.contentWindow!.document.addEventListener( 'keydown', storeEvent ); simFrame.contentWindow!.document.addEventListener( 'keyup', storeEvent ); + simFrame.contentWindow!.document.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + + window.addEventListener( 'keyup', async ( e: KeyboardEvent ) => { + // if the key event is a delete or backspace key or escape + if ( e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Escape' ) { + await this.deleteSelectedElement(); + } + } ); + const updateSelectedElement = () => { let phetioID = this.savedViewElementAutoselectID; @@ -186,6 +200,20 @@ } ] ); } + /** + * Toggle the visibility of the selected PhET-iO Element (if supported) + */ + public async deleteSelectedElement(): Promise { + const selectedElement = this.selectedTreeNodeProperty.value; + if ( selectedElement ) { + const visibilityPhetioID = selectedElement.phetioID + '.visibleProperty'; + if ( studio.phetioElements[ visibilityPhetioID ] && studio.phetioElements[ visibilityPhetioID ].metadata && !studio.phetioElements[ visibilityPhetioID ].metadata.phetioReadOnly ) { + const isVisible = await window.phetio.phetioClient.invokeAsync( selectedElement.phetioID + '.visibleProperty', 'getValue', [] ); + window.phetio.phetioClient.invoke( selectedElement.phetioID + '.visibleProperty', 'setValue', [ !isVisible ] ); + } + } + } + /** * Selects the TreeNode for a screen in the Studio tree. This will only happen if the user changed the screen, * not from Studio changing the screen based on a selection in the Studio tree (because that would be reciprocal Index: main/scenery/js/nodes/IndexedNodeIO.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/scenery/js/nodes/IndexedNodeIO.js b/main/scenery/js/nodes/IndexedNodeIO.js --- a/main/scenery/js/nodes/IndexedNodeIO.js (revision 2138d654748f9324e50a08a0ee68d319128b832d) +++ b/main/scenery/js/nodes/IndexedNodeIO.js (date 1673039932734) @@ -101,7 +101,10 @@ returnType: VoidIO, parameterTypes: [], implementation: function() { - return this.moveForward(); + + // remove all separators from the parent + this.moveForward(); + // add back the separators }, documentation: 'Move this node one index forward in each of its parents. If the node is already at the front, this is a no-op.' }, @@ -110,7 +113,8 @@ returnType: VoidIO, parameterTypes: [], implementation: function() { - return this.moveBackward(); + // same + this.moveBackward(); }, documentation: 'Move this node one index backward in each of its parents. If the node is already at the back, this is a no-op.' } Index: main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts --- a/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (revision 8bfdd30f22d162ab6c4a3e76ab9922434111a302) +++ b/main/circuit-construction-kit-common/js/view/CircuitElementToolbox.ts (date 1673037633094) @@ -44,19 +44,15 @@ itemsPerPage: 5, orientation: 'vertical', - - // Determines the vertical margins spacing: CAROUSEL_ITEM_SPACING, - - // this is only the horizontal margin - margin: 13, + margin: CAROUSEL_ITEM_SPACING, // Expand the touch area above the up button and below the down button buttonTouchAreaYDilation: 8, - tandem: tandem.createTandem( 'carousel' ), + separatorsVisible: true, - isScrollingNodeLayoutBox: true + tandem: tandem.createTandem( 'carousel' ) } }, providedOptions ); @@ -64,7 +60,7 @@ const carousel = new Carousel( circuitElementToolNodes, providedOptions.carouselOptions ); carousel.mutate( { scale: providedOptions.carouselScale } ); - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'vertical', pageFill: Color.WHITE, pageStroke: Color.BLACK, Index: main/sun/js/Carousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/Carousel.ts b/main/sun/js/Carousel.ts --- a/main/sun/js/Carousel.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/Carousel.ts (date 1673039641947) @@ -7,11 +7,10 @@ * Pressing the next and previous buttons moves through the pages. * Movement through the pages is animated, so that items appear to scroll by. * - * Note that Carousel performs layout directly on the items (Nodes) that it is provided. - * If those Nodes appear in multiple places in the scenegraph, then it's the client's - * responsibility to provide the Carousel with wrapped Nodes. + * Note that Carousel wraps each item (Node) in an alignBox to ensure all items have an equal "footprint" dimension. * * @author Chris Malley (PixelZoom, Inc.) + * @author Sam Reid (PhET Interactive Simulations) */ import NumberProperty from '../../axon/js/NumberProperty.js'; @@ -24,7 +23,7 @@ import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js'; import merge from '../../phet-core/js/merge.js'; import optionize, { combineOptions } from '../../phet-core/js/optionize.js'; -import { HBox, HSeparator, HSeparatorOptions, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; +import { AlignGroup, HBox, HSeparator, HSeparatorOptions, IndexedNodeIO, Node, NodeOptions, Rectangle, TColor, VBox, VSeparator, VSeparatorOptions } from '../../scenery/js/imports.js'; import TSoundPlayer from '../../tambo/js/TSoundPlayer.js'; import pushButtonSoundPlayer from '../../tambo/js/shared-sound-players/pushButtonSoundPlayer.js'; import Tandem from '../../tandem/js/Tandem.js'; @@ -33,6 +32,8 @@ import CarouselButton, { CarouselButtonOptions } from './buttons/CarouselButton.js'; import ColorConstants from './ColorConstants.js'; import sun from './sun.js'; +import ReadOnlyProperty from '../../axon/js/ReadOnlyProperty.js'; +import DerivedProperty from '../../axon/js/DerivedProperty.js'; const DEFAULT_ARROW_SIZE = new Dimension2( 20, 7 ); @@ -45,7 +46,6 @@ lineWidth?: number; // width of the border around the carousel cornerRadius?: number; // radius applied to the carousel and next/previous buttons defaultPageNumber?: number; // page that is initially visible - isScrollingNodeLayoutBox?: boolean; // if true, use HBox/VBox for the contents. If false, layout is managed by Carousel // items itemsPerPage?: number; // number of items per page, or how many items are visible at a time in the carousel @@ -89,7 +89,7 @@ private readonly itemsPerPage: number; // number of pages in the carousel - public readonly numberOfPages: number; + public readonly numberOfPagesProperty: ReadOnlyProperty; // page number that is currently visible public readonly pageNumberProperty: Property; @@ -102,7 +102,6 @@ private readonly backgroundHeight: number; private readonly disposeCarousel: () => void; - private readonly isScrollingNodeLayoutBox: boolean; /** * @param items - Nodes shown in the carousel @@ -110,6 +109,23 @@ */ public constructor( items: Node[], providedOptions?: CarouselOptions ) { + // Don't animate layout during initialization + let isInitialized = false; + + const alignGroup = new AlignGroup(); + const alignBoxes = items.map( item => { + const alignBox = alignGroup.createBox( item, { + + // The alignBoxes are in the HBox/VBox, so we must link their visibleProperties to relayout when item visibility changes + visibleProperty: item.visibleProperty, + + tandem: item.tandem.createTandem( 'alignBox' ), + phetioType: IndexedNodeIO, + phetioState: true + } ); + return alignBox; + } ); + // Override defaults with specified options const options = optionize()( { @@ -120,7 +136,6 @@ lineWidth: 1, cornerRadius: 4, defaultPageNumber: 0, - isScrollingNodeLayoutBox: false, // items itemsPerPage: 4, @@ -165,11 +180,8 @@ const isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item - const maxItemWidth = _.maxBy( items, ( item: Node ) => item.width )!.width; - const maxItemHeight = _.maxBy( items, ( item: Node ) => item.height )!.height; - - // This quantity is used make some other computations independent of orientation. - const maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; + const maxItemWidth = _.maxBy( alignBoxes, ( item: Node ) => item.width )!.width; + const maxItemHeight = _.maxBy( alignBoxes, ( item: Node ) => item.height )!.height; // Options common to both buttons const buttonOptions = { @@ -196,6 +208,9 @@ } } as const; + assert && assert( options.spacing >= options.margin, 'The spacing must be >= the margin, or you will see ' + + 'page 2 items at the end of page 1' ); + // Next/previous buttons const nextButton = new CarouselButton( combineOptions( { arrowDirection: isHorizontal ? 'right' : 'down', @@ -206,13 +221,6 @@ tandem: options.tandem.createTandem( 'previousButton' ) }, buttonOptions ) ); - // Computations related to layout of items - const numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; - const scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); - const scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); - const scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; - let itemCenter = options.spacing + ( maxItemLength / 2 ); - // Options common to all separators const separatorOptions = { stroke: options.separatorColor, @@ -224,85 +232,63 @@ // enables animation when scrolling between pages this.animationEnabled = options.animationEnabled; + const children: Node[] = []; + + alignBoxes.forEach( item => { + children.push( item ); + + if ( options.separatorsVisible ) { + children.push( isHorizontal ? new VSeparator( combineOptions( separatorOptions, { + localMinimumHeight: maxItemHeight + 2 * options.margin + } ) ) : new HSeparator( combineOptions( separatorOptions, { + localMinimumWidth: maxItemWidth + 2 * options.margin + } ) ) ); + } + } ); + // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. - const scrollingNode = options.isScrollingNodeLayoutBox ? - ( isHorizontal ? new HBox( { - spacing: options.spacing, - yMargin: options.margin - } ) : new VBox( { - spacing: options.spacing, - xMargin: options.margin - } ) ) : - new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); - - this.isScrollingNodeLayoutBox = options.isScrollingNodeLayoutBox; - items.forEach( item => { - - // add the item - if ( isHorizontal ) { - item.centerX = itemCenter; - item.centerY = options.margin + ( maxItemHeight / 2 ); - } - else { - item.centerX = options.margin + ( maxItemWidth / 2 ); - item.centerY = itemCenter; - } - scrollingNode.addChild( item ); - - // center for the next item - itemCenter += ( options.spacing + maxItemLength ); - - // add optional separator - if ( options.separatorsVisible ) { - let separator; - if ( isHorizontal ) { - - // vertical separator, to the left of the item - separator = new VSeparator( combineOptions( { - preferredHeight: scrollingHeight, - centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, - centerY: item.centerY - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + const scrollingNode = isHorizontal ? new HBox( { + children: children, + spacing: options.spacing, + yMargin: options.separatorsVisible ? 0 : options.margin + } ) : new VBox( { + children: children, + spacing: options.spacing, + xMargin: options.separatorsVisible ? 0 : options.margin + } ); - // center for the next item - itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); - } - else { + // Number of pages + this.numberOfPagesProperty = DerivedProperty.deriveAny( alignBoxes.map( item => item.visibleProperty ), () => { + let numberOfPages = alignBoxes.filter( item => item.visible ).length / options.itemsPerPage; + if ( !Number.isInteger( numberOfPages ) ) { + numberOfPages = Math.floor( numberOfPages + 1 ); + } - // horizontal separator, below the item - separator = new HSeparator( combineOptions( { - preferredWidth: scrollingWidth, - centerX: item.centerX, - centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing - }, separatorOptions ) ); - scrollingNode.addChild( separator ); + // Have to have at least one page, even if it is blank + return Math.max( numberOfPages, 1 ); + }, { + isValidValue: v => v > 0 + } ); - // center for the next item - itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); - } - } + // Number of the page that is visible in the carousel. + assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= this.numberOfPagesProperty.value - 1, + `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); + const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { + tandem: options.tandem.createTandem( 'pageNumberProperty' ), + numberType: 'Integer', + validValues: _.range( this.numberOfPagesProperty.value ), + phetioFeatured: true } ); - // How much to translate scrollingNode each time a next/previous button is pressed - let scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); - if ( options.separatorsVisible ) { - scrollingDelta += ( options.itemsPerPage * options.spacing ); - } - - // Clipping window, to show one page at a time. - // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. - let windowLength = ( scrollingDelta + options.spacing ); - if ( options.separatorsVisible ) { - windowLength -= options.spacing; - } + // Measure from the beginning of the first item to the end of the last item on the 1st page + const windowLength = isHorizontal ? + alignBoxes[ options.itemsPerPage - 1 ].right - alignBoxes[ 0 ].left + options.margin * 2 : + alignBoxes[ options.itemsPerPage - 1 ].bottom - alignBoxes[ 0 ].top + options.margin * 2; const windowWidth = isHorizontal ? windowLength : scrollingNode.width; const windowHeight = isHorizontal ? scrollingNode.height : windowLength; - const clipArea = isHorizontal ? - Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : - Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); + const clipArea = Shape.rectangle( 0, 0, windowWidth, windowHeight ); const windowNode = new Node( { children: [ scrollingNode ], clipArea: clipArea @@ -335,45 +321,38 @@ windowNode.centerY = backgroundNode.centerY; } - // Number of pages - let numberOfPages = items.length / options.itemsPerPage; - if ( !Number.isInteger( numberOfPages ) ) { - numberOfPages = Math.floor( numberOfPages + 1 ); - } - - // Number of the page that is visible in the carousel. - assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, - `defaultPageNumber is out of range: ${options.defaultPageNumber}` ); - const pageNumberProperty = new NumberProperty( options.defaultPageNumber, { - tandem: options.tandem.createTandem( 'pageNumberProperty' ), - numberType: 'Integer', - validValues: _.range( numberOfPages ), - phetioFeatured: true - } ); - // Change pages let scrollAnimation: Animation | null = null; + // This is called when pageNumberProperty changes AND when the total numberOfPagesProperty changes. const pageNumberListener = ( pageNumber: number ) => { - assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, `pageNumber out of range: ${pageNumber}` ); + assert && assert( pageNumber >= 0 && pageNumber <= this.numberOfPagesProperty.value - 1, `pageNumber out of range: ${pageNumber}` ); // button state - nextButton.enabled = pageNumber < ( numberOfPages - 1 ); + nextButton.enabled = pageNumber < ( this.numberOfPagesProperty.value - 1 ); previousButton.enabled = pageNumber > 0; - if ( options.hideDisabledButtons ) { + if ( options.hideDisabledButtons || this.numberOfPagesProperty.value === 1 ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } - const scrollingNodeMargin = options.isScrollingNodeLayoutBox ? options.spacing / 2 : 0; - // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); // Only animate if animation is enabled and PhET-iO state is not being set. When PhET-iO state is being set (as // in loading a customized state), the carousel should immediately reflect the desired page - if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value ) { + const itemsInLayout = alignBoxes.filter( item => item.visible ); + + // Find the item at the top of pageNumber page + const firstItemOnPage = itemsInLayout[ pageNumber * options.itemsPerPage ]; + + // Place we want to scroll to + const targetValue = firstItemOnPage ? ( ( isHorizontal ? -firstItemOnPage.left : -firstItemOnPage.top ) + options.margin ) + : 0; + + // Do not animate during initialization. + if ( this.animationEnabled && !phet.joist.sim.isSettingPhetioStateProperty.value && isInitialized ) { // options that are independent of orientation let animationOptions = { @@ -387,14 +366,14 @@ animationOptions = merge( { getValue: () => scrollingNode.left, setValue: ( value: number ) => { scrollingNode.left = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } else { animationOptions = merge( { getValue: () => scrollingNode.top, setValue: ( value: number ) => { scrollingNode.top = value; }, - to: -pageNumber * scrollingDelta + scrollingNodeMargin + to: targetValue }, animationOptions ); } @@ -406,33 +385,54 @@ // animation disabled, move immediate to new page if ( isHorizontal ) { - scrollingNode.left = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.left = targetValue; } else { - scrollingNode.top = -pageNumber * scrollingDelta + scrollingNodeMargin; + scrollingNode.top = targetValue; } } }; pageNumberProperty.link( pageNumberListener ); + const updatePageCount = () => { + + if ( pageNumberProperty.value >= this.numberOfPagesProperty.value ) { + pageNumberProperty.value = this.numberOfPagesProperty.value - 1; + } + + pageNumberListener( pageNumberProperty.value ); + }; + + // NOTE: the alignBox visibleProperty is the same as the item Node visibleProperty + alignBoxes.forEach( alignBox => alignBox.visibleProperty.link( updatePageCount ) ); + // Buttons modify the page number nextButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() + 1 ) ); previousButton.addListener( () => pageNumberProperty.set( pageNumberProperty.get() - 1 ) ); this.items = items; this.itemsPerPage = options.itemsPerPage; - this.numberOfPages = numberOfPages; this.pageNumberProperty = pageNumberProperty; options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; this.disposeCarousel = () => { pageNumberProperty.unlink( pageNumberListener ); + + // There are 2 problems to be aware of for the alignBox disposal. + // 1. Each alignBox has a visibleProperty of the wrapped item Node, so that must be disconnected + // 2. We link to the updatePageCount method above, so we must unlink here anyways + alignBoxes.forEach( alignBox => { + alignBox.visibleProperty.unlink( updatePageCount ); + alignBox.dispose(); + } ); }; this.mutate( options ); + isInitialized = true; + // support for binder documentation, stripped out in builds and only runs when ?binder is specified assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'Carousel', this ); } @@ -466,7 +466,7 @@ public scrollToItem( item: Node ): void { // If the layout is dynamic, then only account for the visible items - const itemsInLayout = this.isScrollingNodeLayoutBox ? this.items.filter( item => item.visible ) : this.items; + const itemsInLayout = this.items.filter( item => item.visible ); this.scrollToItemIndex( itemsInLayout.indexOf( item ) ); } @@ -475,7 +475,7 @@ * Is the specified item currently visible in the carousel? */ public isItemVisible( item: Node ): boolean { - const itemIndex = this.items.indexOf( item ); + const itemIndex = this.items.filter( item => item.visible ).indexOf( item ); assert && assert( itemIndex !== -1, 'item not found' ); return ( this.pageNumberProperty.get() === this.itemIndexToPageNumber( itemIndex ) ); } Index: main/sun/js/demo/components/demoPageControl.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/demo/components/demoPageControl.ts b/main/sun/js/demo/components/demoPageControl.ts --- a/main/sun/js/demo/components/demoPageControl.ts (revision 0e4f7479857c7ea4b97ec7ba79b4070fc174f9c5) +++ b/main/sun/js/demo/components/demoPageControl.ts (date 1672946411566) @@ -25,7 +25,7 @@ } ); // page control - const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPages, { + const pageControl = new PageControl( carousel.pageNumberProperty, carousel.numberOfPagesProperty, { orientation: 'horizontal', interactive: true, dotRadius: 10, Index: main/number-suite-common/js/common/view/SecondLocaleSelectorCarousel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/number-suite-common/js/common/view/SecondLocaleSelectorCarousel.ts b/main/number-suite-common/js/common/view/SecondLocaleSelectorCarousel.ts --- a/main/number-suite-common/js/common/view/SecondLocaleSelectorCarousel.ts (revision 9c8e8cf8bdffd476b011de5540055586484475ae) +++ b/main/number-suite-common/js/common/view/SecondLocaleSelectorCarousel.ts (date 1673037488482) @@ -9,7 +9,6 @@ import numberSuiteCommon from '../../numberSuiteCommon.js'; import Carousel from '../../../../sun/js/Carousel.js'; import localeProperty from '../../../../joist/js/i18n/localeProperty.js'; -import { GridBox } from '../../../../scenery/js/imports.js'; import LanguageSelectionNode from '../../../../joist/js/preferences/LanguageSelectionNode.js'; import NumberSuiteCommonPreferences from '../model/NumberSuiteCommonPreferences.js'; @@ -23,24 +22,10 @@ } ); }; - // A prototype where we show all languages in grid managed by a Carousel so that there aren't too many items - // displayed at one time - const chunkedLocaleItems = _.chunk( createInteractiveLocales(), 10 ); - const carouselItems = chunkedLocaleItems.map( localeItem => { - return new GridBox( { - xMargin: 5, - yMargin: 3, - xAlign: 'center', - autoRows: 10, - children: [ ...localeItem ], - resize: false - } ); - } ); - - super( carouselItems, { - itemsPerPage: 1, - spacing: 0, - margin: 0, + super( createInteractiveLocales(), { + itemsPerPage: 10, + spacing: 10, + margin: 10, orientation: 'vertical' } ); ```
samreid commented 1 year ago

@matthew-blackman suggested we show separators in the carousel, and @arouinfar and I agreed it looks really great. @arouinfar also affirmed that it is important to be able to "move up" and "move down" even after we said it could take 2-4+ hours to implement.

samreid commented 1 year ago

I think I should move this work to branches, so we can keep making improvements there and request review before moving it to master. UPDATE: Also, it's been nice to work out details in this issue, but it seems nice to continue branches and commits in the common code issue https://github.com/phetsims/sun/issues/814, see you over there.

samreid commented 1 year ago

That commit and branch were moved to https://github.com/phetsims/sun/issues/814

samreid commented 1 year ago

Addressed in https://github.com/phetsims/sun/issues/814

samreid commented 1 year ago

OK Carousel has been merged to master. @arouinfar and/or @matthew-blackman can you please test:

Please test carousel related features such as moving item indices, removing items, dragging out all of one item, etc. Please check the tandem tree and see if anything was disrupted. It was a very complex merge and I'm having trouble telling if I got everything right.

samreid commented 1 year ago

Here are some changes in the API file: https://github.com/phetsims/phet-io-sim-specific/commit/b794abd0b8aeb6adc6c9e7b10a277d0f2d8f1472