From 4a42d9378b9ecff4b032e97a3e37df0b1b0ca46b Mon Sep 17 00:00:00 2001 From: AgustinVallejo Date: Tue, 12 Mar 2024 17:33:22 -0500 Subject: [PATCH] Creating ReadoutListAccordionBox and factoring out the shared logic, see https://github.com/phetsims/buoyancy/issues/112 --- .../view/BuoyancyApplicationsScreenView.ts | 13 +-- js/buoyancy/view/BuoyancyExploreScreenView.ts | 17 +++- js/buoyancy/view/BuoyancyLabScreenView.ts | 15 +-- js/buoyancy/view/BuoyancyShapesScreenView.ts | 4 +- js/buoyancy/view/DensityAccordionBox.ts | 90 +++--------------- js/buoyancy/view/ReadoutListAccordionBox.ts | 91 +++++++++++++++++++ js/buoyancy/view/SubmergedAccordionBox.ts | 88 ++++-------------- 7 files changed, 151 insertions(+), 167 deletions(-) create mode 100644 js/buoyancy/view/ReadoutListAccordionBox.ts diff --git a/js/buoyancy/view/BuoyancyApplicationsScreenView.ts b/js/buoyancy/view/BuoyancyApplicationsScreenView.ts index fef63c7b..e9f7595a 100644 --- a/js/buoyancy/view/BuoyancyApplicationsScreenView.ts +++ b/js/buoyancy/view/BuoyancyApplicationsScreenView.ts @@ -249,10 +249,12 @@ export default class BuoyancyApplicationsScreenView extends DensityBuoyancyScree margin: MARGIN } ) ); - const densityBox = new DensityAccordionBox( - [], { - expandedProperty: model.densityExpandedProperty - } ); + const displayOptionsNode = new DisplayOptionsNode( model ); + + const densityBox = new DensityAccordionBox( { + expandedProperty: model.densityExpandedProperty, + contentWidthMax: displayOptionsNode.width + } ); model.sceneProperty.link( scene => { const materials = scene === Scene.BOTTLE ? [ @@ -263,12 +265,11 @@ export default class BuoyancyApplicationsScreenView extends DensityBuoyancyScree model.boat.materialProperty ] : []; assert && assert( materials.length > 0, 'unsupported Scene', scene ); - densityBox.setMaterials( materials.map( material => { + densityBox.setReadout( materials.map( material => { return { materialProperty: material }; } ) ); } ); - const displayOptionsNode = new DisplayOptionsNode( model ); this.addChild( new AlignBox( new VBox( { spacing: 10, diff --git a/js/buoyancy/view/BuoyancyExploreScreenView.ts b/js/buoyancy/view/BuoyancyExploreScreenView.ts index 7c54f24b..60d32bb0 100644 --- a/js/buoyancy/view/BuoyancyExploreScreenView.ts +++ b/js/buoyancy/view/BuoyancyExploreScreenView.ts @@ -111,26 +111,33 @@ export default class BuoyancyExploreScreenView extends SecondaryMassScreenView { const masses = visible ? [ model.primaryMass, model.secondaryMass ] : [ model.primaryMass ]; - densityBox.setMaterials( masses.map( ( mass, index ) => { + densityBox.setReadout( masses.map( ( mass, index ) => { return { materialProperty: mass.materialProperty, customNameProperty: customExploreScreenFormatting.customNames[ index ], customFormat: customExploreScreenFormatting.customFormats[ index ] }; } ) ); - submergedBox.setSubmergedVolumes( masses, customExploreScreenFormatting ); + submergedBox.setReadout( masses.map( ( mass, index ) => { + return { + mass: mass, + customNameProperty: customExploreScreenFormatting.customNames[ index ], + customFormat: customExploreScreenFormatting.customFormats[ index ] + }; + } ) ); } ); const rightSideVBox = new VBox( { diff --git a/js/buoyancy/view/BuoyancyLabScreenView.ts b/js/buoyancy/view/BuoyancyLabScreenView.ts index 5cd8a463..9faaccd9 100644 --- a/js/buoyancy/view/BuoyancyLabScreenView.ts +++ b/js/buoyancy/view/BuoyancyLabScreenView.ts @@ -110,17 +110,18 @@ export default class BuoyancyLabScreenView extends DensityBuoyancyScreenView[] | null; - customFormats?: RichTextOptions[] | null; -}; - -type SelfOptions = { - // Provided to the constructor call of setMaterials() - setMaterialsOptions?: SetMaterialsOptions; - - // Provide the ideal max content width for the accordion box content. This is used to apply maxWidths to the Texts of the readout. - contentWidthMax?: number; -}; - -type CustomMaterial = { - materialProperty: TReadOnlyProperty; - customNameProperty?: TReadOnlyProperty; - customFormat?: RichTextOptions; -}; - -type DensityAccordionBoxOptions = SelfOptions & AccordionBoxOptions; - -export default class DensityAccordionBox extends AccordionBox { - - private readonly densityReadoutBox: VBox; - private cleanupEmitter = new TinyEmitter(); - - private readonly contentWidthMax: number; +export default class DensityAccordionBox extends ReadoutListAccordionBox { public constructor( - customMaterials: CustomMaterial[], - providedOptions?: DensityAccordionBoxOptions + providedOptions?: ReadoutListAccordionBoxOptions ) { - const options = optionize4()( {}, - DensityBuoyancyCommonConstants.ACCORDION_BOX_OPTIONS, { - titleNode: new Text( DensityBuoyancyCommonStrings.densityStringProperty, { - font: DensityBuoyancyCommonConstants.TITLE_FONT, - maxWidth: 160 - } ), - layoutOptions: { stretch: true }, - setMaterialsOptions: {}, - contentWidthMax: DEFAULT_CONTENT_WIDTH - }, providedOptions ); - - const densityReadout = new VBox( { - spacing: 5, - align: 'center' - } ); - - super( densityReadout, options ); + super( DensityBuoyancyCommonStrings.densityStringProperty, providedOptions ); - this.densityReadoutBox = densityReadout; - this.contentWidthMax = options.contentWidthMax; - this.setMaterials( customMaterials ); } /** * Overwrite the displayed densities with a new set of materialProperties. */ - public setMaterials( customMaterials: CustomMaterial[] ): void { - - // Clear the previous materials that may have been created. - this.cleanupEmitter.emit(); - this.cleanupEmitter.removeAllListeners(); + public override setReadout( customMaterials: CustomReadoutObject[] ): void { - const textOptions = { - font: DEFAULT_FONT, - maxWidth: ( this.contentWidthMax - HBOX_SPACING ) / 2 - }; + super.setReadout( customMaterials ); // Returns the filled in string for the material readout or '?' if the material is hidden const getMysteryMaterialReadoutStringProperty = ( materialProperty: TReadOnlyProperty ) => new DerivedProperty( @@ -112,9 +53,11 @@ export default class DensityAccordionBox extends AccordionBox { } ); } ); - this.densityReadoutBox.children = customMaterials.map( customMaterial => { + this.readoutBox.children = customMaterials.map( customMaterial => { - const materialProperty = customMaterial.materialProperty; + const materialProperty = customMaterial.materialProperty!; + + assert && assert( materialProperty, 'materialProperty should be defined' ); // Get the custom name from the provided options, or create a dynamic property that derives from the material's name const nameProperty = customMaterial.customNameProperty ? @@ -123,13 +66,13 @@ export default class DensityAccordionBox extends AccordionBox { derive: material => material.nameProperty } ); const nameColonProperty = new DerivedProperty( [ nameProperty ], name => name + ': ' ); - const labelText = new RichText( nameColonProperty, textOptions ); + const labelText = new RichText( nameColonProperty, this.textOptions ); // Create the derived string property for the density readout const densityDerivedStringProperty = getMysteryMaterialReadoutStringProperty( materialProperty ); const customFormat = customMaterial.customFormat ? customMaterial.customFormat : {}; const densityReadout = new RichText( densityDerivedStringProperty, - combineOptions( {}, textOptions, customFormat ) ); + combineOptions( {}, this.textOptions, customFormat ) ); this.cleanupEmitter.addListener( () => { densityDerivedStringProperty.dispose(); @@ -145,11 +88,6 @@ export default class DensityAccordionBox extends AccordionBox { } ); } ); } - - public override dispose(): void { - this.cleanupEmitter.emit(); - super.dispose(); - } } densityBuoyancyCommon.register( 'DensityAccordionBox', DensityAccordionBox ); diff --git a/js/buoyancy/view/ReadoutListAccordionBox.ts b/js/buoyancy/view/ReadoutListAccordionBox.ts new file mode 100644 index 00000000..cc268e2d --- /dev/null +++ b/js/buoyancy/view/ReadoutListAccordionBox.ts @@ -0,0 +1,91 @@ +// Copyright 2019-2024, University of Colorado Boulder + +/** + * An AccordionBox that displays the percentage of each material that is submerged. + * + * @author Agustín Vallejo + */ + +import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import { RichTextOptions, Text, TextOptions, VBox } from '../../../../scenery/js/imports.js'; +import Material from '../../common/model/Material.js'; +import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; +import TinyEmitter from '../../../../axon/js/TinyEmitter.js'; +import DensityBuoyancyCommonConstants from '../../common/DensityBuoyancyCommonConstants.js'; +import { optionize4 } from '../../../../phet-core/js/optionize.js'; +import AccordionBox, { AccordionBoxOptions } from '../../../../sun/js/AccordionBox.js'; +import Mass from '../../common/model/Mass.js'; + +const DEFAULT_FONT = new PhetFont( 14 ); +const HBOX_SPACING = 5; +const DEFAULT_CONTENT_WIDTH = ( 140 + HBOX_SPACING ) / 2; + +type SelfOptions = { + // Provide the ideal max content width for the accordion box content. This is used to apply maxWidths to the Texts of the readout. + contentWidthMax?: number; +}; + +export type CustomReadoutObject = { + mass?: Mass | null; // Masses to be passed to the SubmergedAccordionBox + materialProperty?: TReadOnlyProperty | null; // Materials to be passed to the DensityAccordionBox + customNameProperty?: TReadOnlyProperty; // Optional: Custom name for the readout + customFormat?: RichTextOptions; // Optional: Custom format for the readout +}; + +export type ReadoutListAccordionBoxOptions = SelfOptions & AccordionBoxOptions; + +export default class ReadoutListAccordionBox extends AccordionBox { + + protected cleanupEmitter = new TinyEmitter(); + protected textOptions: TextOptions = {}; + + protected readonly readoutBox: VBox; + protected readonly contentWidthMax: number; + + public constructor( + titleStringProperty: TReadOnlyProperty, + providedOptions?: ReadoutListAccordionBoxOptions + ) { + + const options = optionize4()( {}, + DensityBuoyancyCommonConstants.ACCORDION_BOX_OPTIONS, { + titleNode: new Text( titleStringProperty, { + font: DensityBuoyancyCommonConstants.TITLE_FONT, + maxWidth: 160 + } ), + layoutOptions: { stretch: true }, + contentWidthMax: DEFAULT_CONTENT_WIDTH + }, providedOptions ); + + const readoutBox = new VBox( { + spacing: 5, + align: 'center' + } ); + + super( readoutBox, options ); + + this.readoutBox = readoutBox; + this.contentWidthMax = options.contentWidthMax; + + this.textOptions = { + font: DEFAULT_FONT, + maxWidth: ( this.contentWidthMax - HBOX_SPACING ) / 2 + }; + } + + /** + */ + public setReadout( customReadoutObjects: CustomReadoutObject[] ): void { + // Clear the previous materials that may have been created. + this.cleanupEmitter.emit(); + this.cleanupEmitter.removeAllListeners(); + } + + public override dispose(): void { + this.cleanupEmitter.emit(); + super.dispose(); + } +} + +densityBuoyancyCommon.register( 'ReadoutListAccordionBox', ReadoutListAccordionBox ); diff --git a/js/buoyancy/view/SubmergedAccordionBox.ts b/js/buoyancy/view/SubmergedAccordionBox.ts index 00111ba7..0a356b0c 100644 --- a/js/buoyancy/view/SubmergedAccordionBox.ts +++ b/js/buoyancy/view/SubmergedAccordionBox.ts @@ -8,98 +8,43 @@ import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js'; -import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; -import { HBox, RichText, RichTextOptions, Text, VBox } from '../../../../scenery/js/imports.js'; +import { HBox, RichText, RichTextOptions } from '../../../../scenery/js/imports.js'; import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; -import TinyEmitter from '../../../../axon/js/TinyEmitter.js'; -import DensityBuoyancyCommonConstants from '../../common/DensityBuoyancyCommonConstants.js'; -import optionize, { optionize4 } from '../../../../phet-core/js/optionize.js'; -import AccordionBox, { AccordionBoxOptions } from '../../../../sun/js/AccordionBox.js'; +import { combineOptions } from '../../../../phet-core/js/optionize.js'; import Utils from '../../../../dot/js/Utils.js'; -import Mass from '../../common/model/Mass.js'; -import DensityBuoyancyCommonStrings from '../../DensityBuoyancyCommonStrings.js'; import Gravity from '../../common/model/Gravity.js'; import Material from '../../common/model/Material.js'; +import ReadoutListAccordionBox, { CustomReadoutObject, ReadoutListAccordionBoxOptions } from './ReadoutListAccordionBox.js'; +import DensityBuoyancyCommonStrings from '../../DensityBuoyancyCommonStrings.js'; - -type SetMaterialsOptions = { - // Arrays should correspond to the provided materialProperties - customNames?: TReadOnlyProperty[] | null; - customFormats?: RichTextOptions[] | null; -}; - -type SelfOptions = { - // Provided to the constructor call of setSubmergedVolumes() - setSubmergedVolumesOptions?: SetMaterialsOptions; -}; - -type SubmergedAccordionBoxOptions = SelfOptions & AccordionBoxOptions; - -export default class SubmergedAccordionBox extends AccordionBox { - - private readonly submergedReadoutBox: VBox; - private cleanupEmitter = new TinyEmitter(); - +export default class SubmergedAccordionBox extends ReadoutListAccordionBox { public constructor( - masses: Mass[], private readonly gravityProperty: TReadOnlyProperty, private readonly liquidMaterialProperty: TReadOnlyProperty, - providedOptions?: SubmergedAccordionBoxOptions + providedOptions?: ReadoutListAccordionBoxOptions ) { - const options = optionize4()( {}, - DensityBuoyancyCommonConstants.ACCORDION_BOX_OPTIONS, { - titleNode: new Text( DensityBuoyancyCommonStrings.percentSubmergedStringProperty, { - font: DensityBuoyancyCommonConstants.TITLE_FONT, - maxWidth: 160 - } ), - layoutOptions: { stretch: true }, - setSubmergedVolumesOptions: {} - }, providedOptions ); - - const submergedReadout = new VBox( { - spacing: 5, - align: 'center' - } ); - - super( submergedReadout, options ); - - this.submergedReadoutBox = submergedReadout; - - this.setSubmergedVolumes( masses, options.setSubmergedVolumesOptions ); + super( DensityBuoyancyCommonStrings.percentSubmergedStringProperty, providedOptions ); } /** * Overwrite the displayed densities with a new set of materialProperties. */ - public setSubmergedVolumes( masses: Mass[], providedOptions?: SetMaterialsOptions ): void { - - const options = optionize()( { - customNames: null, - customFormats: null - }, providedOptions ); + public override setReadout( customMaterials: CustomReadoutObject[] ): void { - // TODO: Commenting out the assertions, this is not always true, Explore Screen View can have different number of materials, https://github.com/phetsims/submerged-buoyancy-common/issues/103 - // assert && options.customNames && assert( options.customNames.length === materialProperties.length, 'customNames option should correspond to provided materials' ); - // assert && options.customFormats && assert( options.customFormats.length === materialProperties.length, 'customFormats option should correspond to provided materials' ); - - // Clear the previous materials that may have been created. - this.cleanupEmitter.emit(); - this.cleanupEmitter.removeAllListeners(); + super.setReadout( customMaterials ); - const DEFAULT_TEXT_OPTIONS = { - font: new PhetFont( 14 ), - maxWidth: 200 - }; + this.readoutBox.children = customMaterials.map( customMaterial => { - this.submergedReadoutBox.children = masses.map( ( mass, index ) => { + const mass = customMaterial.mass!; + assert && assert( mass, 'Mass should be defined' ); // Get the custom name from the provided options, or create a dynamic property that derives from the material's name - const nameProperty = options?.customNames ? - options.customNames[ index ] : mass.nameProperty; + const nameProperty = customMaterial.customNameProperty ? + customMaterial.customNameProperty : mass.nameProperty; const nameColonProperty = new DerivedProperty( [ nameProperty ], name => name + ': ' ); - const labelText = new RichText( nameColonProperty, DEFAULT_TEXT_OPTIONS ); + const labelText = new RichText( nameColonProperty, this.textOptions ); // Create the derived string property for the submerged readout const submergedDerivedStringProperty = new DerivedProperty( @@ -111,8 +56,9 @@ export default class SubmergedAccordionBox extends AccordionBox { ], ( volume, buoyancy, gravity, liquid ) => { return Utils.toFixed( 100 * buoyancy?.magnitude / ( volume * gravity.value * liquid.density ), 1 ) + '%'; } ); + const customFormat = customMaterial.customFormat ? customMaterial.customFormat : {}; const submergedReadout = new RichText( submergedDerivedStringProperty, - options?.customFormats ? options.customFormats[ index ] : DEFAULT_TEXT_OPTIONS ); + combineOptions( {}, this.textOptions, customFormat ) ); this.cleanupEmitter.addListener( () => { submergedDerivedStringProperty.dispose();