Behavior Proposal v2

John Hendrikx john.hendrikx at gmail.com
Fri Nov 17 13:35:13 UTC 2023


I'm working on a new version of the behavior proposal after some 
fruitful discussions here on this list, and a meeting with Andy.

In this meeting a few problems I was unable to address came to light, 
and which I've now addressed in this post:

1) TextAreaBehavior is relying heavily on internal functions provided by 
the corresponding Skin. These are all related to how Text is visually 
laid out and actions that you can take that would need this layout 
information of which the Control is unaware; the simplest example is 
pressing the "HOME" or "END" key which needs to move to the start or end 
of current line depending on its visual bounds.

2) Having Behaviors as a separate concept didn't seem all that useful.

3) The potential for having a HUGE number of semantic events in Skin -> 
Behavior communication

TextAreaBehavior
==============

I think its fair to say that this is definitely one of the more unique 
behaviors in JavaFX.  Together with its Skin it is doing things that I 
don't think any of the other skins do.  The Skin for example provides 
styleable CSS properties, and provides a lot of call backs for the 
behavior.  This latter part I've not seen in any other skins (I did a 
quick search).  I didn't investigate if any other Skins are also 
providing CSS stylable properties.

Now I think this can mean that perhaps the TextArea control is lacking 
some functions that it should be providing. TextArea is aware that its 
content will be wrapped and split into lines, that it has a view port, 
and provides ways to move the caret.  What it doesn't provide is more 
precise caret control.  It doesn't seem like a stretch to also provide 
for functions that can navigate the caret to the start or end of the 
current line, or to the start or end of the current visible page, etc.

Lacking that, we could also accept that perhaps a Behavior should have 
some more awareness of a related Skin.  In MVC, the Controller 
(Behavior) has a reference to the View (Skin) and also has some 
knowledge of how the View operates. A solution here can be to have Skins 
with very specific needs implement an interface. The Behavior can then 
be attuned to that interface, which would allow a new Skin to be 
constructed which reuses a lot of an existing Behavior without the 
requirement to subclass a specific Skin.  Note that the Behavior can 
fairly easily access the Skin already through Control.  A simple 
instanceof check will suffice to see if it can expand its behavior to 
support visual operations.

     void moveCaretToEndOfLine(TextArea control) {  // called in 
response to a KeyEvent
           if (control.getSkinnable() instanceof 
VisuallyCaretAwareInterface x) {
                x.moveCaret(...);
           }
           // do nothing, wanted behavior not supported by Skin
     }

Usefulness of Behavior Concept
==========================

The point was made that creating a new Behavior is still very limited in 
what it allows the user to change how a control "feels".  This is 
because the Skin does a lot of filtering when it is passing things to 
the associated behavior.  For example, the SpinnerSkin will call the 
behavior when the spinner buttons are pressed (with any mouse 
button(!!!)), but the Behavior can't limit this to just the left mouse 
button (like Spinners on Windows do). Other Skins will only call the 
Behavior for specific mouse buttons, or for specific events.  In effect, 
the Skin is already dictating part of the behavior (either by omission 
or by generalization) which should be part of the Behavior impementation.

For example, if I want to create a SpinnerBehavior that would react to 
the scroll wheel when it is hovering above the Spinner's text field, I'm 
out of luck; the Skin is not calling the Behavior for SCROLL events.

So perhaps we can leverage something here that is already public 
knowledge: the Substructure of Controls. Spinner for example has the 
substructure:

  * text-field — TextField
  * increment-arrow-button — StackPane
      o increment-arrow — Region
  * decrement-arrow-button — StackPane
      o decrement-arrow — Region

The names here are part of the CSS public API and indicate interesting 
parts of the spinner control.

What if we completely forbid the Skin from installing event handlers, 
even on its children (or at least the ones that are part of the 
substructure), and let those events bubble up for Behavior to catch?  An 
immediate problem would be how the Behavior would know whether I pressed 
the mouse button on the UP or DOWN button, or if I pressed the button on 
the Text Field portion.  We could look at the event's target, but even 
though you can see which specific Node was targetted, the Behavior 
doesn't know what that node means.  So, instead we could ask the Skin 
this (or alternatively, put this in a fixed Key on the Node in its 
mappings, or use the CSS id field for this purpose):

      interface Skin<T> {
           default String determineSubstructureElement(Event<?> event) { 
return null; }
      }

The Skin will reply here with the publically known style class name that 
it assigns to its substructure elements, or `null` if it doesn't support 
this feature or if the Event passed in doesn't match to any (public) 
substructure element or if the Event just didn't belong to this Skins 
substructure at all.

The Behavior can then use this "name" or "annotation" as an extra piece 
of information to determine the desired action:

      void mousePressed(MouseEvent event, Spinner control) {
            if (event.getButton() == MouseButton.PRIMARY) {
                control.requestFocus();

                switch 
(control.getSkinnable().determineSubstructureElement(event)) {
                     case "increment-arrow-button" -> startSpinningUp();
                     case "increment-arrow" -> startSpinningUp();
                     case "decrement-arrow-button" -> startSpinningDown();
                     case "decrement-arrow" -> startSpinningDown();
                }
           }
      }

In this way Behaviors can support many more things than the Skin 
initially thought of providing.  You could have the Spinner buttons only 
react on LMB, have the text field respond to SCROLL events, have the 
spinner buttons react differently on long or short presses, on drags or 
gestures, etc...

Note, the above suggested API fpr determineSubstructureElement returns a 
String with the element name; it could also be a simple object that is 
aware of the nested substructure, so that it can be queried "did this 
Event involve the 'increment-arrow-button' or any of its children?" more 
easily.

Semantic Events
==============

Note that the above change sort of eliminates the need for Semantic 
Events between Skins and Behavior, which I think will severely cut down 
on how many Semantic Events will be needed.  As the events are now 
limited to specifying the communication needed between Behavior and 
Control (or only an indirection between Behavior and itself), they can 
be more directly related to state changes.  Instead of having 4 events 
for START/STOP_SPINNING_UP/DOWN, we could just have a 
SPINNER_VALUE_CHANGE event which is parameterized.

The value in still having the semantic events lies in:

- Being able to indirect several low-level sets of events to a single 
high-level action, allowing the high-level function to be remapped or 
accessed directly
- Being able to intercept and block events using the normal event system 
before they're acted upon, or even to generate a high level event directly

Who does what?
================

This changed a little bit from my earlier definitions, but I'll adjust 
them here:

# Controls (no changes)

- Controls refer to a Skin and Behavior
- Controls define semantic events
- Controls can generate semantic events (via mappings)
- Controls never modify their own (user writable) state on their own 
accord (the user is in full control)
- Controls provide an override based key mapping system

# Skins (stop acting on events)

- Skins refer to a Control (legacy inheritance) but are limited in their 
allowed interactions (unwritten rule since the beginning)
- Skins never interprete any events, not even on their children, with 
perhaps the one exception if the handling of the event only changes Skin 
internal state
- Skins never act upon semantic events
- Skins provide information about their substructure, either on the Node 
or when asked

# Behaviors (also generates semantic events, and reacts on more events)

- Behaviors refer to nothing
     - Control (and indirectly Skin) reference is received via event 
handler and listeners only
     - Skins can be checked to see if they support special behavior via 
an Interface
- Behaviors define base key mappings
     - Controls refer to these, after first checking for any user overrides
     - Base key mappings can be global and immutable, user overrides 
(maintained in Control) are mutable
- Behaviors generate semantic events in response to (combinations of) 
low-level events (or via timers)
- Behaviors can act upon events of any type (if unconsumed by the user) 
that are targetted at (or bubble up to) the control (enforced as that's 
the only place they can install handlers)
- Behaviors are allowed to modify control state and their own state

The above definitions seperate the Look & Feel part even better than my 
earlier proposal, and makes Behaviors far more powerful than before at 
only a slight cost to Skins.  Skins now truly only provide visuals, and 
never interprete events, while Behaviors have more options to determine 
what events they are interested in and how to respond to them.

The balance between the amount of code in Skins and Behaviors will shift 
a bit.  More event handling will move to Behaviors (where it actually 
belongs), which may go a long way to also making Skins easier to 
subclass or reimplement.

--John






-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20231117/2b4fc63c/attachment-0001.htm>


More information about the openjfx-dev mailing list