On Tue, 7 Jul 2026 15:19:33 GMT, Andy Goryachev <[email protected]> wrote:

>> # Support for Embedded Images, Text Highlights, Wavy Underlines
>> 
>> ## Summary
>> 
>> This PR adds new attributes to support embedded images, text highlights, and 
>> wavy underlines.
>> 
>> The `RichEditorDemoApp` gains new context menus and enables drag and drop to 
>> showcase the new functionality.
>> 
>> <img width="768" height="703" alt="Screenshot 2026-06-18 at 12 40 28" 
>> src="https://github.com/user-attachments/assets/678bdbac-6582-46ee-bd41-ea938df930bb";
>>  />
>> 
>> 
>> 
>> ## Problem
>> 
>> Among the feedback received after publishing the RichTextArea incubating 
>> module, a number of users complained about the need to support embedded 
>> images.  Other users complained about difficulty to get the text highlighted 
>> or wavy underlined, and having these decorations controlled via CSS.
>> 
>> 
>> ## Solution
>> 
>> Support for images was intentionally missing in the initial release; this PR 
>> adds the feature.
>> 
>> Adding highlights and underlines is a bit more complicated, for several 
>> reasons.  One reason is that the existing highlighting mechanism supports 
>> mixing of arbitrary number of translucent colors - something that is pretty 
>> much impossible to do with CSS alone.
>> 
>> A useful compromise is to offer a limited number of standard attributes that 
>> can be controlled via CSS: 5 highlights and 3 wavy underlines.
>> 
>> 
>> ## Specification
>> 
>> 
>> ### StyleAttributeMap Attributes
>> 
>> New attributes:
>> - EMBEDDED_IMAGE
>> - TEXT_HIGHLIGHT_1 ... TEXT_HIGHLIGHT_5
>> - UNDERLINE_WAVY_1 ... UNDERLINE_WAVY_3
>> 
>> ### CSS Styling
>> 
>> The styling of highlights and wavy underlines is determined by the 
>> stylesheet and can be changed by the user.
>> 
>> The style selectors are:
>> 
>> `.rich-text-area .text-highlight-1` ... `.rich-text-area .text-highlight-5`
>> `.rich-text-area .underline-wavy-1` ... `.rich-text-area .underline-wavy-3`
>> 
>> 
>> ### New Public APIs
>> 
>> `EmbeddedImage`
>> `FileListFormatHandler`
>> `StyleAttribute.inlineNode()`
>> `StyleAttribute.isInlineNode()`
>> `StyledSegment.ofInlineNode()`
>> `CellContext.RunDecor`
>> `CellContext.decorateRun()`
>> `RichTextArea.dropTargetProperty`
>> `RichTextArea.clearDropTarget()`
>> `RichTextArea.getStyleAttributeMap()`
>> 
>> 
>> ### File Format Changes
>> 
>> The data transfer format (also used by the demo app to save the .rich files) 
>> version string is changed, making the old files unreadable.
>> 
>> Perhaps it should remain unchanged for the duration of the incubation 
>> period.  See also [2]
>> 
>> 
>> ### Breaking API Changes
>> 
>> `DataFormatHandler.createStyl...
>
> Andy Goryachev has updated the pull request incrementally with one additional 
> commit since the last revision:
> 
>   demos

I've reviewed the public API and it generally looks good. I left mostly javadoc 
comments and questions with a couple API questions.

I haven't reviewed cssref yet, but that's next.

doc-files/controls/RichTextArea/RichTextArea_DataFormat_Incubator.md line 41:

> 39: 
> 40: ```
> 41: {@RichText-v4-incubator}{#tabs|99.5}{ff|System}{fs|12.0}tabs:     1       
> 2       3{!tabs|149.0,190.0,229.0}

Shouldn't that be "v3" -- not sure why it needs to jump from v2 to v4...

doc-files/controls/RichTextArea/RichTextArea_DataFormat_Incubator.md line 129:

> 127: |uw1     |UNDERLINE_WAVY_1      |boolean     |Wavy underline color 1
> 128: |uw2     |UNDERLINE_WAVY_2      |boolean     |Wavy underline color 2
> 129: |uw3     |UNDERLINE_WAVY_3      |boolean     |Wavy underline color 3

I think `WAVY_UNDERLINE_N` (here and in other places) would be a more 
consistent naming.

doc-files/controls/RichTextArea/RichTextArea_DataFormat_Incubator.md line 163:

> 161: Example:
> 162: 
> 163: `{img|w,138.0,h,102.0,tw,-1.0,th,-1.0,a,true,b,iVBORw0KGgoAAA...}`

The example has `th,-1.0` but the above note says that -1 is only for the width.

modules/javafx.graphics/src/main/docs/javafx/scene/doc-files/cssref.html line 
6650:

> 6648:             underline-wavy-1<br>
> 6649:             underline-wavy-1<br>
> 6650:             underline-wavy-3

Copy / paste error: the second `-1` items should be `-2`. And here is another 
place where `wavy-underline-N` would be a more consistent name.

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/RichTextArea.java
 line 526:

> 524: 
> 525:     /**
> 526:      * This property indicates the target text position during the 
> drag-and-drop operations.

s/the drag-and-drop/drag-and-drop/

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/RichTextArea.java
 line 550:

> 548:      * @since 27
> 549:      */
> 550:     public final void setDropTarget(double screenX, double screenY) {

Should there be a variant of this method that takes a `TextPos`?

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/RichTextArea.java
 line 1493:

> 1491:      * @param pos the text position
> 1492:      * @param forInsert whether to pick preceding style at the segment 
> boundary
> 1493:      * @return the style attributes, non-null

Minor: we usually say "never null"

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/DataFormatHandler.java
 line 45:

> 43:      * the implementation if the format contains no styles on its own 
> (for example, in the plain text format case).
> 44:      *
> 45:      * @param input the input data, never null

Now that you take an Object instead of a String, you need to define what 
subtypes are expected and what happens if an unexpected type is passed in. 
Presumably "IllegalArgumentException" or "ClassCastException", but maybe it is 
a no-op?

Some documentation should be on this abstract method. If the answer depends on 
the subclass, then document this with an `@implSpec` section.

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/DataFormatHandler.java
 line 51:

> 49:      * @throws UnsupportedOperationException if the copy operation is not 
> supported
> 50:      */
> 51:     public abstract StyledInput createStyledInput(Object input, 
> StyleAttributeMap attr) throws IOException;

This needs `@since 27`, since it is a binary incompatible change that is 
effectively removing the old and adding a new method.

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/EmbeddedImage.java
 line 91:

> 89: 
> 90:     /// Private constructor that DOES NOT make a defensive copy of the 
> bytes.
> 91:     private EmbeddedImage(

Minor: We don't usually put javadoc comments on private methods of public 
classes. Suggestion: consider changing this to an ordinary inline comment 
(although I see that this pattern is used elsewhere, so this could be deferred).

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/EmbeddedImage.java
 line 224:

> 222:      */
> 223:     public EmbeddedImage copy(double targetWidth, double targetHeight, 
> boolean keepAspectRatio) {
> 224:         return new EmbeddedImage(bytes, width, height, targetWidth, 
> targetHeight, keepAspectRatio);

Not an API issue, but this doesn't make a defensive copy. Is the `bytes` array 
ever modified? Or is it effectively deeply immutable?

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/FileListFormatHandler.java
 line 68:

> 66:     @Override
> 67:     public StyledInput createStyledInput(Object input, StyleAttributeMap 
> attr) throws IOException {
> 68:         List<File> files = (List<File>)input;

This is an example of what I mentioned in the superclass method. This class 
should provide docs and at least add an `@throws ClassCastException` (if that's 
what we decide is the cleanest behavior, else document the behavior you want 
and change the impl to check for instanceof and implement that behavior).

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/FileListFormatHandler.java
 line 91:

> 89:      * @param files the list of files to be inserted
> 90:      */
> 91:     public static void handleDrop(RichTextArea t, TextPos p, List<File> 
> files) {

Who calls this static method?

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/RichTextFormatHandler.java
 line 198:

> 196: 
> 197:     @Override
> 198:     public StyledInput createStyledInput(Object input, StyleAttributeMap 
> attr) {

Check to see whether this inherits the `@since 27`

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/RichTextModel.java
 line 238:

> 236: 
> 237:     @Override
> 238:     public StyleAttributeMap getStyleAttributeMap(StyleResolver 
> resolver, TextPos pos, boolean forInsert) {

Check whether `@since 27` inherits

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/RichTextModel.java
 line 427:

> 425:          * @since 27
> 426:          */
> 427:         public StyleAttributeMap getStyleAttributeMap(int charIndex, 
> boolean leading, boolean forInsert) {

This is in a private class. It probably shouldn't have any javadoc, much less 
an `@since`

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/SimpleViewOnlyStyledModel.java
 line 330:

> 328: 
> 329:     @Override
> 330:     public StyleAttributeMap getStyleAttributeMap(StyleResolver r, 
> TextPos pos, boolean forInsert) {

Check `@since 27`

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/StyleAttribute.java
 line 87:

> 85:      */
> 86:     public static <P> StyleAttribute<P> inlineNode(String name, Class<P> 
> type) {
> 87:         return new InlineNodeStyleAttribute<P>(name, type);

Rather than a separate `inlineNode` factory method, could this instead be done 
with a check on the class type in `character()` factory method? I could ask the 
same of the other factory methods, so maybe there is some value in doing this 
way?

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/StyledSegment.java
 line 291:

> 289:      * Creates a StyledSegment which consists of a single inline Node.
> 290:      * @param generator the code to create a Node instance
> 291:      * @param a the segment styles, can be null

What does "null" do? Is it different from an empty style attribute map?

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/StyledSegment.java
 line 294:

> 292:      * @return the StyledSegment instance
> 293:      */
> 294:     public static StyledSegment ofInlineNode(Supplier<Node> generator, 
> StyleAttributeMap a) {

Needs `@since 27`

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/StyledTextModel.java
 line 254:

> 252:      * to the symbol indicated by the specified position.
> 253:      * <p>
> 254:      * This method returns the attributes of the last character at the 
> end of the document.

This is a little confusing. My first reading of this sentence was that it 
always returned the attributes of the last character (that is, the character at 
the end of the document), which I'm sure isn't what you are trying to say.

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/StyledTextModel.java
 line 262:

> 260:      * @since 27
> 261:      */
> 262:     public abstract StyleAttributeMap getStyleAttributeMap(StyleResolver 
> resolver, TextPos pos, boolean forInsert);

Rather than removing the existing method, would it be better to leave it and 
have it call the new method with a default value of forInsert (probably false, 
unless you think true is more common)?

modules/jfx.incubator.richtext/src/main/java/jfx/incubator/scene/control/richtext/model/StyledTextModel.java
 line 696:

> 694:      * This is a convenience method which eventually calls
> 695:      * {@link #replace(StyleResolver, TextPos, TextPos, StyledInput)}
> 696:      * with the attributes provided by {@link 
> #getStyleAttributeMap(StyleResolver, TextPos, boolean)} at the

The doc should explicitly say that it calls `getStyleAttributeMap` with `true` 
for the last param.

-------------

PR Review: https://git.openjdk.org/jfx/pull/2196#pullrequestreview-4648009869
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3538888293
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553884131
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553914744
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553927795
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554115897
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554127399
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554145633
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553852564
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553861047
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554009685
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554041286
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553949969
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553986613
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554413735
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554419752
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554433319
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554438949
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554378327
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553873911
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3553866580
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554178986
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554166523
PR Review Comment: https://git.openjdk.org/jfx/pull/2196#discussion_r3554187312

Reply via email to