Open ariel-phet opened 8 years ago
@amanda-phet do you mind getting a design doc started?
Also labeling for developer meeting, so we can sort out what needs to be done on NumberKeypad sooner vs later, who is lead developer on the design, who is doing the work, etc.
11/10/16 design meeting notes:
• We need 2 separate keypads: one for numbers, one for entering equations • Focus first on numbers, then on equations • Currently missing from numbers keypad is sign (+/-) button (see #274)
We should edit the bullets above to use "expressions", "strings", "text", or something along those lines, rather than "equations" (which implies that there is an equal sign being typed).
"strings" and "text" is way too general, we're not designing a text editor. How about "mathematical expressions"?
Revised 11/10/16 design meeting notes: (replaces https://github.com/phetsims/scenery-phet/issues/283#issuecomment-260809625)
• We need 2 separate keypads: one for numbers, one for entering mathematical expressions • Focus first on numbers, then on mathematical expressions • Currently missing from numbers keypad is sign (+/-) button (see #274)
@pixelzoom and @jonathanolson ,
@aadish and I (mostly Aadish) have done some work on the Keypad. I think the current form captures most of the architecture that we discussed. I’m mostly out for the next couple of weeks, but since this is going to be needed around the end of January, I thought it would be good for you to take a look and see if you think we’re generally on the right track. There are a number of things that can be improved – for instance, there is a ‘value’ field in AbstractKey
that can probably be removed. Please take a look at it from a more of a high level and give us your comments, and we will address them in the new year.
The bulk of the code can be found in scenery-phet/keypad, and there is demo code in scenery-phet/demo.
Comments on scenery-phet demo (prior to looking at code):
[x] When nothing has been entered on the keypad, the default "arithmetic value" should not be 0
. That makes it impossible to distinguish between no entry and an explicit 0
entry without consulting the string value. It would be preferable to use null
to represent the absence of an arithmetic value.
[x] The demo itself is a little annoying, with stuff horizontally shifting around. This is probably just due to bad layout choices in the demo, but it's distracting, and potentially masks genuine problems.
[x] In the demo, "Clear on next keypress" should be a checkbox, not a button. It's a boolean state that's set, not an action that's performed. See the demo for NumberKeypad. (And if it doesn't already, the implementation must provide an API that allows the client to turn this on _and* off.)
[x] The demo doesn't demonstrate the decimal key. It should, because it's essential to sims in development (e.g. unit-rates). Again, see the demo of NumberKeypad.
My first pass at the code was a general review, look mainly at conformance to PhET guidelines.
Issues:
[x] Keypad.createKeyNode - Private functions like this should be located after the constructor.
[x] Keypad.createKeyNode has an options
parameter (documented as optional), but has no _.extend
call to define defaults. So buttonFont
and buttonColor
are not really options, they are effectively required parameters. Either add an _.extend
call or function parameters.
[x] Keypad and IntegerAccumulator constructor parameter options
is not properly annotated as being optional. It should be {Object} [options]
.
[x] We generally put _.extend
as the first line in the constructor, since options are an important part of the public API. This is currently not the case in Keypad and IntegerAccumulator, where _.extend
follows the supertype constructor call.
[x] IntegerAccumulator is missing visibility annotation for validateAndProcessInput
.
[x] AbstractKey has insufficient documentation for constructor parameter identifier
.
[x] AbstractKey has no visibility annotations for properties defined in its constructor.
[x] AbstractKey handleKeyPressed
is an abstract function. It should be annotated as @abstract
and the default implementation should throw Error
, not assert
.
[x] AbstractKey handleKeyPressed
is missing the keyAccumulator
parameter, which I only discovered by looking at subtype implementations.
[x] AbstractKeyhandleKeyPressed
is missing documentation of the keyAccumulator
parameter.
[x] The documentation that is provided for AbstractKey handleKeyPressed
is mostly redundant ("Function that is called by the key accumulator when this key is pressed, must be implemented in descendant classes."). It's obviously a function, a descendent class (sic) is a subtype (or subclass, if you must), and the @abstract
annotation would indicate that it needs to be implemented by subtypes.
[x] PlusMinusKey is missing @public
and @override
annotations for handleKeyPressed
.
[x] Redundant documentation like "PlusMinusKey Class derived from AbstractKey class" is not very useful. The reader needs a quick overview of this type's role and responsibilities, not the specifics of the class (sic) hierarchy.
[x] AbstractKeyAccumulator validateAndProcessInput
is an abstract function. It should be annotated as @abstract
and the default implementation should throw Error
, not assert
.
[x] Some functions are missing documentation, see TODO items that I've added in code.
I corrected many of the issues related to code guidelines (check off in the above check list). I also added some TODO
items in the code, where something still needs to be addressed.
On to review of the code architecture...
First round of architectural review:
The constructor signature of AbstractKey is:
function AbstractKey( displayNode, value, identifier )
... where value
is {number}
. Looking at non-numeric subtypes of AbstractKey (i.e. BackspaceKey, PlusMinusKey), I see that the value
argument is null
. This is a fundamental problem - a numeric value has no business being in the base type, it is specific to keys that have a numeric value (i.e. DigitKey).
The constructor signature is:
function BackspaceKey( width, height )
Why can't width and height be provided via options? There are surely reasonable defaults (as there apparently are for other keys). Also wondering... Why does BackspaceKey have these options when Keypad is responsible for button size (via buttonWidth
and buttonHeight
options)? Why is Backspace key instantiated as new BackspaceKey( DEFAULT_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT )
in various Keypad layouts, when the button size might be changed via Keypad options?
... and if BackspaceKey is responsible for its dimensions, then why aren't the other subtypes of AbstractKey also responsible for their dimensions? Or the number of cells that they occupy in the grid?
This is apparently done in AbstractKeyAccumulator subtypes via:
validateAndProcessInput: function( accumulatedKeys )
I was surprised to see that this function has only one parameter, accumulatedKeys
. How does it "validate and process input" if the input is not provided? Is this function mis-named? Is validation being done after the accumulator is modified?
NOTE: Validation was revised in https://github.com/phetsims/scenery-phet/commit/b8d3bb96fc725cdfae1572554347283e88e392c4, see also https://github.com/phetsims/scenery-phet/issues/283#issuecomment-268633910.
clear on next key press
the responsibility of the base type?IntegerAccumulator defines _clearOnNextKeyPress
and its associated setter/getter. Why aren't these defined in AbstractKeyAccumulator? I see nothing in the implementation to indicate that these are specific to IntegerAccumulator.
There's a lot of stuff in IntegerAccumulator that would apply to (for example) DecimalAccumulator. Do you need to factor that out into a more general NumberAccumulator?
Highly recommended that you create DecimalAccumulator, and add it to scenery-phet demo. It will illuminate a lot of issues (like this one).
This is another fundamental issue.
In the scenery-phet demo, I see:
555 var accumulator = new IntegerAccumulator( { maxLength: 5 } );
568 var keyPad = new Keypad( Keypad.PositiveAndNegativeIntegerLayout, accumulator
576 accumulator.clear();
583 accumulator.setClearOnNextKeyPress( true );
The accumulator should be an internal implementation detail, transparent to the client. Clients shouldn't need to instantiate it, and certainly shouldn't need to explicitly clear it, etc. Keypad (or its subtypes) should provide a complete API.
For example... Create NumberKeypad (Keypad subtype), which creates its own NumberAccumulator based on options, and (transparently) provides access to the string and numeric representations of the value. Could also possibly select a default layout based on options. Then scenery-phet demo might look like:
var keypad = new NumberKeypad( {
plusMinus: true,
maxDigits: 5
} );
keypad.clear();
keypad.setClearOnNextKeyPress( true );
0
.IntegerAccumulator.updateNumericalValue:
72 return stringRepresentation.length > 0 ? parseInt( stringRepresentation, 10 ) : 0;
Absence of a value should most definitely not be mapped to a value of 0
. This makes it impossible for the client to determine whether 0
was actually entered without consulting the string value. Recommended to change to:
72 return ( stringRepresentation.length > 0 ) ? parseInt( stringRepresentation, 10 ) : null;
The current specification of only 2 layouts requires 145 lines! (See Keypad PositiveIntegerLayout and PositiveAndNegativeIntegerLayout.) That specification is verbose, redundant, and makes it very difficult to see the structure of the keypad - especially when keys are specified out of order, as they currently are in both layouts.
Suggestions:
• Use a 2-dimensional array instead of row
and column
fields.
• Make horizontalSpan
and verticalSpan
properties of the keys, with default 1
.
Then the specification of layouts would look like this:
PositiveIntegerLayout: [
[ new DigitKey( 7 ), new DigitKey( 8 ), new DigitKey( 9 ) ],
[ new DigitKey( 4 ), new DigitKey( 5 ), new DigitKey( 6 ) ],
[ new DigitKey( 1 ), new DigitKey( 2 ), new DigitKey( 3 ) ],
[ new DigitKey( 0, { horizontalSpan: 2 } ), new BackspaceKey() ]
],
PositiveAndNegativeIntegerLayout: [
[ new DigitKey( 7 ), new DigitKey( 8 ), new DigitKey( 9 ) ],
[ new DigitKey( 4 ), new DigitKey( 5 ), new DigitKey( 6 ) ],
[ new DigitKey( 1 ), new DigitKey( 2 ), new DigitKey( 3 ) ],
[ new PlusMinusKey(), new DigitKey( 0 ), new BackspaceKey() ]
],
This makes it very easy to see the structure of the keypad, and which keys occupy more than 1 grid cell. It also makes it very easy for clients to specify new (custom) layouts.
In PlusMinusKey:
20 AbstractKey.call( this, '+/-', null, 'PlusMinus' );
... should be:
20 AbstractKey.call( this, '\u002b/\u2212', null, 'PlusMinus' );
After addressing the issue of the accumulator being transparent to the client... You need to address the issue of disposing of a Keypad. In scenery-phet demo (ComponentsView), you have the client linking to Properties and there is no dispose
anywhere in the keypad implementation.
In AbstractAccumulator:
23 // @public - array property that tracks the accumulated key presses
24 this.accumulatedKeysProperty = new Property( [] );
I see no calls to accumulatedKeysProperty.link
, so apparently nothing is observing it. So why does it need to be a Property? And if it does need to be observable, why is it not an ObservableArray?
updateStringValue
and updateNumericalValue
are poorly named.These functions don't actually update anything. They compute values, which are used elsewhere to update Properties. More appropriate names would be computeStringValue
and computeNumericalValue
.
[NOTE: Renamed to keysToString
and stringToInteger
.
The first if
statement in the current implementation is redundant, and can be deleted:
updateNumericalValue: function( accumulatedKeys, index ) {
if ( accumulatedKeys.length === 0 ) {
return 0;
}
var stringRepresentation = this.updateStringValue( accumulatedKeys, index );
return stringRepresentation.length > 0 ? parseInt( stringRepresentation, 10 ) : 0;
},
var length = accumulatedKeys.length;
var multiplier = 1;
var maxLength = this.options.maxLength;
var startIndex = 0;
var startString = '';
if ( length > 0 && accumulatedKeys[ 0 ] instanceof PlusMinusKey ) {
multiplier = -1;
maxLength += 1;
startIndex = 1;
startString = '-';
}
All of these local vars are unnecessary. The minus character should be Unicode, but requires special care when converting from string to number.
I'm going to go ahead and change this. Ask me if you have questions.
This was a problem before I made the above changes (git checkout 0c4958c
to confirm). If you enter a '0', it should be overwritten by subsequent digits. The displayed string should not look like this:
OK, I'm done with this review. There are a lot of open issues, represented by check-box items in the above comments.
The big issues are:
(1) The client shouldn't need to know about accumulator; Keypad (and its subtypes) should provide the complete API.
(2) AbstractKey should not have an associated {number} value
, which is relevant only for DigitKey.
(3) Specification of layouts should be simplified/clarified.
(4) DecimalAccumulator would help to flush out type hierarchy issues.
(5) Leading zeros are not handled properly.
In retrospect... A separate issue (something like "initial review") should have been created for this review, rather than putting this in the "design requirements" issue. Let's keep that in mind for the next review.
A couple of other things that occurred to me, after ruminating on the current implementation. These are (imo) both big architectural flaws.
In https://github.com/phetsims/scenery-phet/issues/272 we discussed the importance on being able to customize the constraints on what can be entered using the keypad. And that issue resulted in the addition of options.validateKey
in NumberKeypad. I see no evidence of any similar API in this new implementation. In fact, the only validation is a maximum number of digits that is hardwired into IntegerAccumulator.validateKeys
:
65 if ( this.getNumberOfDigits( proposedKeys ) <= this.options.maxLength ) {
Validation should be encapsulated in one place, and it should be customizable.
My first red flag related to this was in AbstractKey.handleKeyPress, whose signature is:
39 handleKeyPressed: function( keyAccumulator )
If feels wrong that keys must know about the accumulator. handleKeyPressed
creates a "proposed" set of keys, based on the current accumulated keys. And then that proposed set is what is validated. Keys are also responsible for handling the "clear on next pressed feature" (which results in duplicated code, btw.) And keys know the structure of how the accumulator stores keys - e.g., the keys are manipulating arrays. This all seems very wrong to me. Keys should know nothing about the accumulator. The accumulator should make decisions based on which key is pressed, and how keys are accumulated should be private to the accumulator.
Another note on the validation issue mentioned in https://github.com/phetsims/scenery-phet/issues/283#issuecomment-268678103... The current implementation of IntegerAccumulator only allows you to constrain the number of digits entered. As an example of why this isn't sufficient... Unit Rates needs to constrain the number of digits and requires that the entry is a positive integer (i.e., 0
is not valid.) Future sims may have other constraints, and trying to foresee all constraints is impossible, so Keypad must allow clients to specify their own constraint algorithm.
In https://github.com/phetsims/scenery-phet/issues/283#issuecomment-268389241, @jbphet asked for feedback from me and @jonathanolson. I don't see any feedback from @jonathanolson. So I'm wondering if we should defer the design phone call scheduled for this morning?
@jbphet and I discussed status via Skype, and we decided to defer our design meeting until @jonathanolson has provided feedback.
Here's the feedback I'd like to hear from @jonathanolson:
I reviewed independently first, and came to the same high-level conclusions as @pixelzoom. Review here starts with higher-level points and moves towards low-level things I noticed. Feel free to convert to a checklist if it helps.
I was anticipating a structure where there was an enumeration of values:
KeypadValue = {
0: ...
1: ...
// ...
9: ...
MINUS: ...
DECIMAL: ...
};
and the accumulator stored the current values (similar to what is current):
// @public {Property.<Array.<KeypadValue>>}
this.valuesProperty = new Property( [] );
where each key contained logic just based on the keypad values:
getValuesAfterPress: function( values: {Array.<KeypadValue>} ) : {Array.<KeypadValue}
The current structure is similar, but keys themselves are used in place of the values requiring extra documentation like:
// PlusMinusKey (if present) will be first key, and indicates that the number is negative
if ( keys.length > 0 && keys[ i ] instanceof PlusMinusKey ) {
returnValue = PlusMinusKey.MINUS_CHAR;
i++;
}
and each key is instead given the full accumulator (and the responsibility to have to handle accumulator responsibilites, like clearing on next keypress):
if ( keyAccumulator.getClearOnNextKeyPress() ) {
newArray = [];
keyAccumulator.setClearOnNextKeyPress( false );
}
else {
// actually do things
}
This would prevent bugs like the BackspaceKey not fully clearing everything on the next keypress with clearOnNextKeyPress is set (it only removes one character and removes the flag).
Ideally, this would be handled in the accumulator:
onPress: function( key ) {
// Handle clearing on key presses in one place
if ( this.clearOnNextKeyPress ) {
this.valuesProperty.value = [];
this.clearOnNextKeyPress = false;
}
else {
var proposedValues = key.getValuesAfterPress( this.valuesProperty.value );
if ( this.validate( proposedValues ) ) {
this.valuesProperty.value = proposedValues;
}
}
}
So that keys could have a slightly simpler implementation, e.g. for DigitKey:
getValuesAfterPress: function( currentValues ) {
return Keypad.removeLeadingZero( currentValues ).concat( [ this.value ] );
}
instead of the current:
handleKeyPressed: function( keyAccumulator ) {
var newArray;
if ( keyAccumulator.getClearOnNextKeyPress() ) {
newArray = [];
keyAccumulator.setClearOnNextKeyPress( false );
}
else{
newArray = _.clone( keyAccumulator.accumulatedKeysProperty.get() );
if ( keyAccumulator.valueProperty.get() === 0 ){
newArray.pop();
}
}
newArray.push( this );
return newArray;
}
or for PlusMinusKey:
getValuesAfterPress: function( currentValues ) {
var isNegative = currentValues.length > 0 && currentValues[ 0 ] === KeypadValue.MINUS;
// Remove minus sign if the value is already negative
if ( isNegative ) {
return currentValues.slice( 1 );
}
// Otherwise add a minus sign on the front
else {
return [ KeypadValue.MINUS ].concat( currentValues );
}
}
Additionally, it would be ideal for the accumulator to have a custom validate() function given in the options (which may include maxDigits handling). I agree with @pixelzoom on the need to have custom validation functions. Having a library (like Keypad.validateMaxDigits, Keypad.validateNonzero, etc.) that can be composed would be great, so you could:
new Keypad( ..., function validate( values ) {
// Allow up to 4 digits, don't allow 0 on its own, and don't allow the digit 5
return Keypad.validateMaxDigits( values, 4 ) &&
Keypad.validateNonzero( values ) &&
!_.includes( values, KeypadValue[ 5 ] );
} );
I didn't see anything about clearOnNextKeyPress in the AbstractKeyAccumulator, but it is assumed by all keys (which only require an AbstractKeyAccumulator). Can this move to the AbstractKeyAccumulator?
I like having the currentValues be a property (or ObservableArray as @pixelzoom notes), since the stringValue and numberValue can be DerivedProperties. However, I'd have the numberValue created directly from the values, so we don't have to handle stripping out Unicode minus signs or other display complexities. Thus I'd like to see valuesToString( values ) and valuesToNumber( values ), and:
// in Keypad
this.stringProperty = new DerivedProperty( [ accumulator.valuesProperty ], accumulator.valuesToString.bind( accumulator ) );
this.numberProperty = new DerivedProperty( [ accumulator.valuesProperty ], accumulator.valuesToNumber.bind( accumulator ) );
Documentation of layout (and possibly simplification of that as noted by @pixelzoom) would help a lot. Took a bit to figure out it was {Array.<Array.<AbstractKey|null>>}
. Also, it should be further simplified, as I think rows like [ new DigitKey( 5, { horizontalSpan: 5 } ), null, new DigitKey( 3 ) ]
will improperly position the 3 key. There seem to be a lot of edge cases, so accumulating row/column offsets (instead of computing for each value) in layout would be more error-proof.
For loops for rows/columns (or multiple dimensions handled in general) are more readable with names:
for ( var row = 0, ... ) {
for ( var column = 0, ... ) {
doSomething( values[ row ][ column ], values[ row ][ column - 1 ] );
}
}
There's a lot of inconsistent formatting, particularly with array brackets, e.g. layout[ numRows - 1][i]
should be layout[ numRows - 1 ][ i ]
.
createKeyNode types should have {AbstractKey}
for keys and {AbstractKeyAccumulator}
for the accumulator.
Default buttonColor and buttonFont are specified in two places.
It's weird that displayNode can be a {string}
or {Node}
, although AbstractKey only allows Node. Can we remove the ability to have strings, or fully support it? If we keep the string support, I'm worried about the asymmetric auto-scaling to a fixed dimension.
Noting that stringToInteger can return null is quite important.
Also I want to note that having an enumeration for values isn't a strict requirement, but the abstraction sounds helpful in general. It may be overgeneralizing, and keys as values may be fine.
In the future, what happens if you have a key that conditionally wants to store one of a few values? It probably won't come up, but having a concrete list of what values can be in the array may just help comprehension.
Either way, thinks like isDigit() is probably a helpful utility function (currently done with instanceof DigitKey).
Self-assigning to review for discussion on 2/20/17 @ 10am.
Over the last few days I've discussed the design of the keypad with @pixelzoom, @jonathanolson, and @aadish . Below is a summary of the decisions made based on those discussions.
On the topic of the Keypad API, we agreed that the client shouldn't need to know about accumulator. The Keypad (and its subtypes) should provide a complete API. The fields should be accessible like this:
keypad.stringProperty.link
keypad.valueProperty.link
keypad.accumulatedKeysProperty.link
...versus something like this:
keypad.accumulator.stringProperty.link
Since we may sometimes have a need for custom accumulators, the keypads should have default accumulators with the ability to provide a different one.
On the topic of custom validation, we need to support this, so we decided to use a combination of general options so that clients don't have to create a validator any time they want any sort of variation, but also should allow the client to supply a completely different validator. We are thinking that the API will offer options like this:
maxDigitsLeftOfMantissa
maxDigitsRightOfMantissa
...but also these:
additionalValidator
alternativeValidator
additionalValidator
will be a function that was just in addition to the default validation. alternativeValidator
will be a different function and, if provided, would override all default and additional validation.
To make it easier to create new keypads, we could have a library of parameterized validators, creating them something like this:
options.additionalValidation = Keypad.currencyValidator( { maxDigitsLeftOfMantissa: 4 } )
We should be sure to note in the documentation that the validator can have side effects. Write in use case where decimal point is added.
On the topic of whether the keys should encapsulate the knowledge of how they are accumulated, we decided that the accumulator should have that knowledge. This would allow keys to have different behaviors in different keypads. For example, we may want the +/- key to work differently in number accumulator versus an equation accumulator. The tradeoff is that we will need to change more files to add a new key.
We discussed using an observable array for the accumulated keys instead of a Propery<Array>
, but decided that the latter will work better.
@pixelzoom suggested that we implement a floating point accumulator, determine what is common between this and the integer accumulator and consider putting it into a common base class. @aadish will do this.
@pixelzoom and @jonathanolson - please comment if you think I've left anything out or misrepresented any of the things that we discussed.
@aadish is going to move forward with these changes and we will re-review once the main elements are working.
@jbphet What's the status of the new keypad?
@ariel-phet FYI, the lack of closure on this issue is resulting in work-around solutions that are being copy-pasted between sims. See https://github.com/phetsims/projectile-motion/issues/118
The fields should be accessible like this:
keypad.stringProperty.link keypad.valueProperty.link keypad.accumulatedKeysProperty.link
AbstractKeyAccumulator doesn't have stringProperty/valueProperty, that seems like an implementation detail in NumberAccumulator? (It's also not marked @override
in NumberAccumulator).
TermAccumulator (in Area Model) has richStringProperty and termProperty (which somewhat map over), but initially there was no stringProperty equivalent (where I used termProperty.value.toRichString()
) before I needed to handle the display slightly differently than the "value". I don't think assuming there's always a stringProperty equivalent is a safe assumption.
Are we sure we want to put restrictions on accumulators by pushing the client to forward things through Keypad itself?
We should be sure to note in the documentation that the validator can have side effects.
That seems like an anti-pattern. Presumably validating would have no side effects?
As you refactor, please keep Area Model in mind (3 custom keypad layouts, custom accumulator).
Also, it looks like these are already being forwarded in Keypad (but some are undefined for Area Model's accumulator):
this.stringProperty = this.keyAccumulator.stringProperty; // @public (read-only)
this.valueProperty = this.keyAccumulator.valueProperty; // @public (read-only)
this.accumulatedKeysProperty = this.keyAccumulator.accumulatedKeysProperty; // @public (read-only)
Documentation for what the accumulator API is should probably be in the abstract accumulator, as if these were part of the API, it was undocumented.
@jonathanolson said:
AbstractKeyAccumulator doesn't have stringProperty/valueProperty, that seems like an implementation detail in NumberAccumulator?
and
Are we sure we want to put restrictions on accumulators by pushing the client to forward things through Keypad itself?
This is a good point, and in fact, that's why I designed it this way in the first place - there will be different accumulators needed in different circumstances. This makes it difficult to make these properties available directly from the keypad. In a previous comment about design discussions with @pixelzoom and @jonathanolson I said, "we agreed that the client shouldn't need to know about accumulator". I'd like to reverse this decision - it's a lot easier to implement and use this if the client knows about and hooks up to the accumulator. It also makes sense to me - you have a keypad, and the keypad has a place where the keys get collected. I think there will be enough examples available in the code so that new users will be able to make sense of this pretty quickly.
On the topic of validators, @pixelzoom said:
We should be sure to note in the documentation that the validator can have side effects.
..and @jonathanolson said:
That seems like an anti-pattern. Presumably validating would have no side effects?
I just spent some time looking at this, and I think it is no longer the case. It may have been at one time - I honestly don't remember - but AbstractKeyAccumulator.validateAndUpdate
is just expecting a boolean value from the validators. I suppose there isn't anything that actively prevents the validators from modifying the proposed keys, but the default validator in NumberAccumulator
doesn't do it, and there aren't any examples in the demo that do it either. So, in short, I think this is no longer an issue. Feel free to let me know if I've missed anything.
I think the issues raised above have been addressed at this point, but there were a lot of them, so I have only a moderate degree of confidence that this is true. I'm going to give @andrea-phet the go-ahead to use this in Projectile Motion, see https://github.com/phetsims/projectile-motion/issues/127. I think the API is unlikely to change much at this point, so it should be safe. I'm going to assign to @jonathanolson to comment in case he thinks it's not ready for this, since he has used it in Area Model. If he's okay with it and doesn't veto, it should be assigned to @pixelzoom for another look when he returns. If all looks good to him, it can be migrated into unit rates. If not, I'll work with him to iron out any issues.
Scanning back through the list of comments, I realized that I missed an item: implement dispose. I'll do this shortly, but I think any review can occur in parallel.
I'm going to assign to @jonathanolson to comment in case he thinks it's not ready for this, since he has used it in Area Model. If he's okay with it and doesn't veto, it should be assigned to @pixelzoom for another look when he returns.
Sounds ready for projectile-motion usage to me. Assigning as described.
@jbphet In addition to the lack of dispose
... NumberAccumulator does not propagate options to AbstractKeyAccumulator, looks like a bug. I'm also guessing that the alternativeValidator
and additionalValidator
features of AbstractKeyAccumulator are therefore untested.
@jbphet said:
Scanning back through the list of comments, I realized that I missed an item: implement dispose. I'll do this shortly, but I think any review can occur in parallel.
Integration of Keypad into projectile-motion is completed. Publication of that sim is blocked until dispose
is implemented and options
propagation is fixed (see https://github.com/phetsims/scenery-phet/issues/283#issuecomment-321404395). High priority.
@jonathanolson said he might be able to take care of this quickly. Since it is a blocking issue for projectile motion I ask him to move forward.
As a side note, things would be more convenient if we had an array of validators, instead of having to branch to check for default/alternative/additional. It would also allow greater flexibility in the future.
Added the options pass-through.
It seems like the only thing that needs disposal are the key nodes (as if the key labels are Nodes themselves, like in Area Model's case, they need the parent removed).
Am I missing something? (I double-checked all files in the keypad directory and really didn't see any complicated disposal that would be required).
I added disposal of the properties that are created by the keypad. I also added a test harness to the scenery-phet demo that removes one of the demo keypads and invokes the dispose function.
@jonathanolson added the options pass through, and he and I added the dispose function. However, I also added a test case to the scenery-phet demo that tests the disposal of the keypad, but that test case isn't passing yet. The keypad is being successfully removed from the scene graph, and the dispose function is being called, and as far as I can tell all references from the demo are being cleaned up, yet the keypad is not being garbage collected. I'll leave this open and investigate early next week.
The issue that was causing the keypad to not be garbage collected has been resolved, and was unrelated to the dispose function. It was an odd and subtle case, and a separate issue will be logged on it since @jonathanolson wanted to investigate it more after he and I looked at it. I'm going to leave the test harness in the scenery-phet demo so that we can easily regression test the dispose function. The test harness consists of a button that, when pressed, removes one of the keypads from the scene graph and calls its dispose function. When using this, the idea is to load the keypad components, take a memory snapshot, then press the removal button and take another snapshot. There should be three keypads in the first memory snapshot and two in the second.
This is what the keypad test/demo page looks like as of this writing, and the green button at the lower right is the one that tests the disposal:
At this point the options pass-through and dispose function should be taken care of. Back to @pixelzoom for another check.
Several things still to be addressed.
Little things:
[x] (1) Documentation in NumberAccumulator
refers to non-existent PlusMinusKey
in 2 places.
[x] (2) defaultValidator
should be @protected
in both AbstractKeyAccumulator
and NumberAccumulator
. It’s intended to be overridden by subtypes, and shouldn’t be called by clients.
[x] (3) validateAndUpdate
should be @protected
in AbstractKeyAccumulator
. It’s called by the subtype, not intended to be called by clients.
[ ] (4) There are several unused features in the implementation: alternativeValidator
, additionalValidator
, KeyID.X
, KeyID.X_SQUARED
, … Have these features been tested? If not, why not? And should ‘untested’ be noted in the internal documentation?
[x] (5) Copyright headers are out of sync. Run 'grunt update-copyright-dates'.
Big things:
[x] (1) A string representation seems useful to (required by?) all accumulator subtypes. So why are stringProperty
and keyToString
specific to NumberAccumulator
? I believe that this functionality should be in AbstractKeyAccumulator
. (@jonathanolson also commented on this on 7/26/17 in https://github.com/phetsims/scenery-phet/issues/283#issuecomment-318182334. I don’t see stringProperty
addressed specifically in @jbphet ’s response https://github.com/phetsims/scenery-phet/issues/283#issuecomment-320366093.)
[x] (2) Related to the preceding item... As mentioned in past reviews, I think it’s difficult to get the accumulator hierarchy correct when there is only 1 concrete accumulator type (NumberAccumulator
). I’d really like to see another subtype, if only to flush out issues of where responsibilities should reside.
[x] (3) @jonathanolson said “Presumably validating would have no side effects?” @jbphet replied “I just spent some time looking at this, and I think it is no longer the case.” Validation certainly does have side-effects, see AbstractKeyAccumulator.validateAndUpdate
. Validation of proposedKeys
and update of accumulatedKeysProperty
are tightly integrated into this one function. If proposedKeys
is rejected, it’s rejected silently, which seems like something that could be an issue in the future. Highly recommended to decouple validation of proposedKeys
and updating of accumulatedKeysProperty
.
[ ] (4) In https://github.com/phetsims/scenery-phet/issues/283#issuecomment-321714316, JO suggested “things would be more convenient if we had an array of validators, instead of having to branch to check for default/alternative/additional. It would also allow greater flexibility in the future.” I agree. And I don’t see any comment that addresses this.
[EDIT: I numbered the items above, to make it easier to refer to them in subsequent comments.]
KeyID.X, KeyID.X_SQUARED, … Have these features been tested? If not, why not? And should ‘untested’ be noted in the internal documentation?
These are used in Area Model (see TermAccumulator). It was noted that any sim-specific Key IDs should be placed in KeyID, should that change?
I’d really like to see another subtype, if only to flush out issues of where responsibilities should reside.
TermAccumulator (not common code) is another subtype.
Several of the "little things" have been addressed.
In the "big things" subcategory...
@pixelzoom said:
A string representation seems useful to (required by?) all accumulator subtypes.
Maybe. It just seemed to me that making any assumptions about what representations are needed makes for a brittle design, and I wanted to keep the abstract accumulator as simple and general as possible, so it only accumulates keys. I could envision a case where only a numeric value is needed, for instance. So, I'd prefer to keep it this way. In the grand scheme of things, if there is a string representation in the base type, it probably won't ever mess anything up, so if others feel strongly I'll capitulate and move it there.
Also, @jonathanolson pointed out that there is a second accumulator sub-type - TermAccumulator
. @pixelzoom - do you feel that this sufficiently addresses the 2nd "Big Thing"?
I'll work on Little Thing 4 and Big Things 3 and 4 at a later time. I'm not going to assign back @pixelzoom until I've done that, though he's welcome to comment when he sees this update if so inclined.
Re questions in the above comment...
Perhaps @jonathanolson can chime in on whether he ran into hierarchy issues when creating TermAccumulator
, and whether having stringProperty
available in AbstractKeyAccumulator
would have been useful.
Perhaps @jonathanolson can chime in on whether he ran into hierarchy issues when creating TermAccumulator, and whether having stringProperty available in AbstractKeyAccumulator would have been useful.
As noted in https://github.com/phetsims/scenery-phet/issues/283#issuecomment-318182334 above:
TermAccumulator (in Area Model) has richStringProperty and termProperty (which somewhat map over), but initially there was no stringProperty equivalent (where I used termProperty.value.toRichString()) before I needed to handle the display slightly differently than the "value". I don't think assuming there's always a stringProperty equivalent is a safe assumption.
So having to require a stringProperty implementation for all accumulator subtypes doesn't "feel" correct to me.
The general accumulation and validation seemed quite general, like it should work for any subtype.
@pixelzoom and @amanda-phet have pointed out the need for a general NumberKeypad design meeting. We should look into addressing the following questions as well as fleshing out and prioritizing other anticipated needs