[
https://issues.apache.org/jira/browse/GROOVY-11434?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18128906#comment-18128906
]
ASF GitHub Bot commented on GROOVY-11434:
-----------------------------------------
paulk-asert commented on PR #2989:
URL: https://github.com/apache/groovy/pull/2989#issuecomment-6277936793
> # Code review:
[GROOVY-11434](https://issues.apache.org/jira/browse/GROOVY-11434)
groovy-groovyfx
> Reviewed commits:
>
> * `fcb630cc045950beb13d98ba4b016165db83590f` — add the optional
`groovy-groovyfx` module
> * `229870fa64c56ce30986170a5e38340287177612` — put the compile-only JavaFX
jars on the reachability-metadata generator classpath
>
> This note is from reading those commits and the surrounding builder and
transform code. The test suite was not re-run for this review.
>
> ## Recommendation
> Please address the scene-root behavior before merging `fcb630cc04`. The
suggested scene change is small, and it brings the implementation in line with
`SceneFactory`'s own contract. The notes on numeric extensions and
`@FXBindable` are smaller; they are worth doing in the same change if that
stays convenient. `229870fa64` looks right as it stands.
>
> ## Design that fits the codebase
> The module follows the `SwingBuilder` shape in the right places.
>
> * `SceneGraphBuilder` registers one reflective `NodeFactory` for the
common node, and a separate factory only where construction itself differs:
stage, scene, charts, table columns, and `bind`.
> * Child placement stays in `FXChildren`. That decision depends on the
parent, so one parent dispatch is easier to follow than a factory per control.
> * `FXRuntime` names no JavaFX type, so a missing runtime can be reported
before those classes are linked.
> * Toolkit startup distinguishes "already running" from "already shut down".
> * JavaFX remains compile-only and is not bundled. The
GPL-2.0-with-Classpath-Exception dependency stays off the runtime classpath.
> * No new source file approaches a thousand lines. `SceneGraphBuilder` is
544 lines.
>
> Please keep that split. Moving child placement back into per-control
factories would add types without removing rules.
>
> ## Scene root is chosen before the sibling list is known
> `SceneFactory` documents this rule: the first nested `Parent` becomes the
scene root; when the first child is not a `Parent`, or when further children
follow, those children belong to a `Group` root.
>
> `setChild` applies a different rule. On the first `Parent` it clears
`groovyfx.placeholderRoot` and replaces `scene.root` immediately. A later
sibling is then appended to that root when the root is a `Group` or a `Pane`.
`VBox`, `HBox`, `BorderPane`, and `GridPane` are all `Pane`s, so this builder
nests the second child inside the first:
>
> ```groovy
> scene {
> vbox { label('a') }
> label('b')
> }
> ```
>
> `label('b')` becomes a child of the `vbox`, beside `'a'`. Two panes, such
as `vbox` followed by `hbox`, nest the second pane inside the first. A later
sibling throws only when the replacement root is neither a `Group` nor a `Pane`.
>
> `setChild` sees one child at a time, so it cannot know whether another
sibling will follow. The swap in `setChild` therefore cannot implement the
documented rule. `onNodeCompleted` already runs after the whole body.
>
> Suggested behavior:
>
> 1. When no explicit `root:` is given, keep the placeholder `Group` for the
entire body and add every nested `Node` to it.
> 2. In `onNodeCompleted`, if that group contains exactly one child and that
child is a `Parent`, remove the child from the group and then set it as
`scene.root`. A node cannot have two parents, so the removal has to happen
first.
> 3. When the caller passed `root:`, keep adding children to that root, and
skip the promotion.
>
> That leaves one completion rule in place of the mid-build swap, the
"append to a pane" branch, and the placeholder flag being cleared on the first
parent.
>
> Please add tests for a `vbox` followed by a `label`, and for two panes. In
both cases the children should be siblings under a `Group`. The current tests
cover a single `vbox` (`buildsAStageSceneAndControls`) and rectangles only
(`sceneWithoutParentRootGetsAGroup`). They do not cover a `Parent` with a
following sibling.
>
> ## Numeric extensions repeat one type dispatch
> `FXExtensions` uses the same ladder in seven methods: `Integer`, `Short`,
or `Byte`, then `Long`, then `Float`, otherwise `doubleValue()`. The ladder
appears in `plus`, `minus`, and `div` for a constant on the right, and in
`plus`, `minus`, `multiply`, and `div` for a constant on the left.
>
> The public overloads need to remain. Extension methods are selected by
signature. The bodies can share one private method that accepts the four
primitive operations (`int`, `long`, `float`, and `double`). `negative`, and
the overloads that only forward to `add` or `concat`, are already direct.
>
> That ladder is also the numeric policy. `BigDecimal` and `BigInteger`
match none of the branches, so both become `doubleValue()`. An expression such
as `1.5g + property` therefore yields a `double` binding. Please make that
choice once, in the shared helper: either call the `Bindings` overload that
preserves the operand type, or reject a non-primitive `Number` with an
exception that states the conversion.
>
> ## `@FXBindable` maps types by simple name after resolution
> `FXPropertyTypes.forType` classifies a property by its simple name. That
is appropriate for `FXBindableASTStubber`, which runs at `CONVERSION`, before
names are resolved. `FXBindableASTTransformation` runs at `CANONICALIZATION`,
after resolution. Please map resolved types there from the resolved `ClassNode`
(`ClassHelper` primitives, `java.lang.String`, `ObservableList`,
`ObservableMap`, and `ObservableSet`), and keep the simple-name table in the
stubber.
>
> With the current shared function, any class whose simple name is `String`
becomes a `StringProperty`. The annotation contract lists `boolean`, `int`,
`long`, `float`, `double`, `String`, and their wrappers. `forType` also maps
`short`, `byte`, and `char` to `IntegerProperty`, because those names are in
`INTEGER_NAMES`. Storing a `char` as an `int` should be stated in that
contract, or the mapping should stop.
>
> The generated `nameProperty()` method publishes the property with a
check-then-act on a non-volatile field: if `name$property` is null, it assigns
a new `Simple*Property`. Two threads can each construct a property, and the
write that loses drops listeners attached to the discarded instance. A model
filled off the JavaFX thread and then bound on it is a normal use of this
annotation. Please make the property field volatile and publish it under
synchronization, or generate the accessor as a synchronized method. The
getter's read of `name$property == null ? name : name$property.get()` has the
same visibility gap.
>
> There is also no joint-compilation test showing that a Java caller can see
`nameProperty()` through the stub. The stubber exists for that phase.
>
> ## Smaller contracts
> `XYChartFactory.newInstance` retains a positional value only when that
value is already an instance of the chart class. A call such as
`lineChart('Sales')` drops `'Sales'`. `StageFactory`, `SceneFactory`, and
`NodeFactory` either use a positional value or reject it. Please reject a
non-chart positional value the same way.
>
> `NodeFactory.construct` catches every `GroovyRuntimeException` from
`invokeConstructorOf`. A constructor that runs and throws is reported as an
`InvokerInvocationException`, which is a `GroovyRuntimeException`. If the bean
also has a `text` property and the value is a `CharSequence`, that failure is
discarded and a no-argument instance receives `text`. Please catch
`MissingMethodException` for the text fallback, and let a constructor that has
already run propagate its failure.
>
> ## Classpath fix
> `229870fa64` is the right fix, and it does not need a redesign.
`nativeImageMetadata` is a `JavaExec` task whose classpath is
`sourceSets.main.runtimeClasspath`. That classpath omits `compileOnly` and
`compileOnlyApi`. `FXExtensions` names `NumberBinding` in its signatures, so
`Class.getMethods()` needs the JavaFX jars. Appending
`configurations.compileClasspath` adds the configuration that contains them.
Thanks for the careful read, Daniel. Addressed in 16f8cff466; notes on each
point below, including a couple where I went a different way.
**Scene root.** Agreed, that was a real bug and the test gap you spotted was
exactly where it hid. `setChild` now only ever appends to the current root
(placeholder `Group`, or an explicit `root:`), and `onNodeCompleted` promotes a
sole nested `Parent` once the body is complete, detaching it from the group
first. The Javadoc now states the rule, including that `scene.root` is the
placeholder while the body is still being built. Added the two tests you asked
for (`vbox` followed by `label`; two panes) plus one for an explicit `root:`;
both new sibling tests fail against the old factory.
**`XYChartFactory` positional value.** Now rejected with the same wording as
`SceneFactory` (`The value of 'lineChart' must be a LineChart. Found:
java.lang.String`).
**`NodeFactory` constructor catch.** Agreed the blanket catch was wrong, but
catching `MissingMethodException` wouldn't work: when no constructor matches,
`MetaClassImpl` throws a plain `GroovyRuntimeException("Could not find matching
constructor…")`, not a `MissingMethodException`, so the text fallback would
never trigger. I've made the fallback a pre-check instead: a `CharSequence`
value goes to the no-arg constructor + `text` property only when the bean has
no constructor accepting a `String`; everything else calls
`invokeConstructorOf` with no catch, so a constructor that runs and fails
reports its own failure, and a missing constructor surfaces Groovy's own
message (wrapped by `FactoryBuilderSupport` as before). `NodeFactoryTest`
covers the fallback, a `GroovyRuntimeException` from a `String` constructor
propagating, and the no-match case.
**Numeric constants.** I've documented the policy (class Javadoc and the
adoc) rather than changing it, because neither suggested alternative is
available: JavaFX only has `int`/`long`/`float`/`double` overloads, so there's
no "preserve the operand type" call for `BigDecimal`/`BigInteger`; and
rejecting non-primitive `Number`s would break the most common case, since
Groovy's decimal literals are `BigDecimal` (`width * 0.5` would throw). Going
to `double` for both is the only sensible mapping; it's now stated. I left the
seven overload bodies as they are: a shared helper taking four lambdas per call
didn't read better than the four-line bodies.
**`@FXBindable` type mapping by simple name.** This one is deliberate (the
`FXPropertyTypes` Javadoc notes it): the stubber at CONVERSION and the
transform at CANONICALIZATION must give the same answer, or a user class that
happens to be called `String` would get a stub returning `StringProperty` and a
class returning `ObjectProperty<…>`, which javac would compile against and then
fail to link. Consistency across the two phases matters more than exactness in
one of them, and the colliding case is contrived.
**`short`/`byte`/`char`.** The contract now documents `short` and `byte`
widening to `IntegerProperty`. I've dropped `char`: it now maps to
`ObjectProperty<Character>` like any other unlisted type, which round-trips
cleanly rather than storing a char as an int. Test added for all three.
**Thread-safety of `nameProperty()`.** I'd rather keep the unsynchronized
lazy check-then-act: it's the standard JavaFX lazy-property idiom (JavaFX's own
`Node` and `Control` properties are written exactly this way), and JavaFX
properties and their listener lists aren't thread-safe in any case, so a
synchronized accessor would cost every call without making off-thread binding
safe. The scenario you describe (fill the model off the FX thread, bind on it)
already works: before the first `nameProperty()` call the setter writes the
plain field, and handing over via `Platform.runLater` gives the happens-before.
Happy to add a sentence to the Javadoc saying the usual JavaFX
thread-confinement rules apply if you think it's worth calling out.
**Joint compilation.** Fair gap. Added `FXBindableJointCompilationTest`,
which compiles a class-level `@FXBindable` class together with a Java caller
through `JavaAwareCompilationUnit` with `keepStubs`, asserts the stub declares
`StringProperty nameProperty()` / `IntegerProperty ageProperty()`, then loads
and runs the Java class.
**Classpath fix.** No change.
> Include GroovyFx in Apache Groovy
> ---------------------------------
>
> Key: GROOVY-11434
> URL: https://issues.apache.org/jira/browse/GROOVY-11434
> Project: Groovy
> Issue Type: New Feature
> Components: Swing
> Affects Versions: 4.0.22
> Reporter: Pruteanu Dragos
> Assignee: Paul King
> Priority: Major
> Fix For: 7.x
>
>
> Please include the project
> [GroovyFX|[http://groovyfx.org|http://groovyfx.org/]] into the Apache Groovy
> repository. The public repository is
> [here|https://github.com/groovyfx-project/groovyfx]. The project is very
> nice, something similar to groovy-swing. There has been no updates to that
> project, and there are compatibility issues with latest OpenJDKs.
> JavaFX is the highest standard regarding User Interfaces. The project is very
> nice and there are users interested in it.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)