JDK-8199094 : TextFlow caretShape method returns wrong coordinates when TextFlow has left inset.
  • Type: Bug
  • Component: javafx
  • Sub-Component: scenegraph
  • Affected Version: jfx11,9,10
  • Priority: P3
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2018-03-04
  • Updated: 2024-06-11
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Related Reports
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "10-ea" 2018-03-20
Java(TM) SE Runtime Environment 18.3 (build 10-ea+37)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10-ea+37, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 10.0.16299.125]

A DESCRIPTION OF THE PROBLEM :
TextFlow caretShape method returns wrong coordinates when TextFlow has left or top inset.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
See Case 1 in test code:
Add some Text nodes to a TextFlow. For one of the Text nodes, call getBoundsInParent, and note the minX location. Now count what the character index is at the start of this Text node. Call caretShape at this index, and note the MoveTo x location.  The two x locations are within 1 pixel, which is fine.

See Case 2 in test code:
Now add a left or top inset to the TextFlow, and  the caretShape MoveTo x differs from the getBoundsInParent minX by the amount of the inset.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
After adding a left inset to the TextFlow,  the caretShape MoveTo x should be 
within one pixel of the getBoundsInParent minX.
ACTUAL -
case 1 - no inset
BoundingBox [minX:60.08203125
MoveTo[x=59.08203125

case 2 - left inset = 17
BoundingBox [minX:131.408203125
MoveTo[x=113.408203125

ERROR MESSAGES/STACK TRACES THAT OCCUR :
no error message

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
//
package tools;
//
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.Node;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.PathElement;
import javafx.scene.text.TextFlow;
import javafx.scene.layout.VBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.layout.Pane;
import javafx.scene.layout.FlowPane;
//
//
public class FlowCaret extends Application {
	/*
	This app tests the caretShape method in TextFlow. When TextFlow has a
	top or left inset, it appears the caretShape coordinates are off by
	the amount of the insets.

	Is this a bug in TextFlow?
	*/

private static final Logger LOG = Logger.getLogger(FlowCaret.class.getName());
//
//init fonts
private Font  mainFont = Font.font("DejaVu Serif", FontWeight.NORMAL, 20);
//
//init panes
private BorderPane bp = new BorderPane();
private StackPane sp = new StackPane();
private VBox vbox = new VBox();
//

//
//init sample string of words
private String s1 = "Electromagnetic waves cover a wide spectrum from radio signals to gamma rays. Waves can interfere. "
    + "Constructive interference occurs when two waves are in phase. ";

private String s2 = "Electromagnetic waves cover a wide spectrum from radio signals to gamma rays. Waves can interfere. "
    + "Constructive interference occurs when two waves are in phase. ";

private String s3 = "Electromagnetic waves cover a wide spectrum from radio signals to gamma rays. Waves can interfere. "
    + "Constructive interference occurs when two waves are in phase. ";

private String s4 = "Electromagnetic waves cover a wide spectrum from radio signals to gamma rays. Waves can interfere. "
    + "Constructive interference occurs when two waves are in phase. ";

private TextFlow flow1;
private TextFlow flow2;
private TextFlow flow3;
private TextFlow flow4;



    /**
     * Default Constructor
     */
    public FlowCaret() {
    }

	public void test(){
		try{
			initTest();
			//
			initFlows();

		} catch (Exception ex) {
			LOG.log(Level.SEVERE, ex.getMessage(), ex);
			throw new RuntimeException(ex);
		}
	}

	public void initTest(){
		try{
			//insets: top, right, bottom, left
			bp.setPadding(new Insets(5, 5, 5, 5));
			bp.setStyle(" -fx-border-color: green;-fx-border-width: 2px;-fx-border-style: solid;");
			bp.setPrefSize(500,400);
			//
			sp.setPadding(new Insets(10, 5, 10, 5));
			sp.setStyle(" -fx-border-color: blue;-fx-border-width: 1px;-fx-border-style: solid;");

			//
			vbox.setSpacing(8);
			vbox.setPadding(new Insets(15, 5, 15, 5));
			vbox.setStyle(" -fx-border-color: red;-fx-border-width: 1px;-fx-border-style: solid;");
			//
		} catch (Exception ex) {
			LOG.log(Level.SEVERE, ex.getMessage(), ex);
			throw new RuntimeException(ex);
		}
	}
	public void initFlows(){
		try{
			// case 1
			// no padding
			flow1 = addText(s1);
			flow1.setId("case 1");
			vbox.getChildren().add(flow1);
			//
			// case 2
			// add padding to textflow
			flow2 = addText(s2);
			flow2.setId("case 2");
			//insets: top, right, bottom, left
			flow2.setPadding(new Insets(13, 17, 10, 17));
			vbox.getChildren().add(flow2);

			// case 3
			// workaround, put textflow inside pane container
			// no padding
			flow3 = addText(s3);
			flow3.setId("case 3");
			StackPane p3 = new StackPane();
			p3.getChildren().add(flow3);
			vbox.getChildren().add(p3);
			//
			// case 4
			// workaround, put textflow inside pane container
			// add padding to pane container
			flow4 = addText(s4);
			flow4.setId("case 4");
			StackPane p4 = new StackPane();
			p4.getChildren().add(flow4);
			//insets: top, right, bottom, left
			p4.setPadding(new Insets(13, 17, 10, 17));
			vbox.getChildren().add(p4);


		} catch (Exception ex) {
			LOG.log(Level.SEVERE, ex.getMessage(), ex);
			throw new RuntimeException(ex);
		}
	}

	public void check(){
		try{
			int nodeIndex = 8;
			checkCaret(flow1, nodeIndex);
			checkCaret(flow2, nodeIndex);
			checkCaret(flow3, nodeIndex);
			checkCaret(flow4, nodeIndex);
		} catch (Exception ex) {
			LOG.log(Level.SEVERE, ex.getMessage(), ex);
			throw new RuntimeException(ex);
		}

	}

	public TextFlow addText(String s){
		TextFlow flow = new TextFlow();
		try{
			LOG.log(Level.INFO, "\n\nText s: " + s);
			//split string into words
			String[] ss = s.split(" ");
			int len = ss.length;
			for(int i = 0; i<len; i++){
				//create a word
				Text tx = new Text(ss[i] + " ");
				//set font on each word
				tx.setFont(mainFont);
				//add one word to flow
				flow.getChildren().add(tx);
			}
			flow.setStyle(" -fx-border-color: orange;-fx-border-width: 1px;-fx-border-style: solid;");

		} catch (Exception ex) {
			LOG.log(Level.SEVERE, ex.getMessage(), ex);
			throw new RuntimeException(ex);
		}
		return flow;
	}

	public void checkCaret(TextFlow flow, int nodeIndex){
		//check caret shape at selected node
		try{
			ObservableList<Node> wordList = flow.getChildren();
			Node textNode = wordList.get(nodeIndex);
			Text t = (Text)textNode;
			LOG.log(Level.INFO, "\n\n\n flow id: " + flow.getId() + ", text: " + t.getText());
			//
			//get char index at nodeIndex
			int charIndex = 0;
			for(int k = 0; k < nodeIndex; k++){
				Node n = wordList.get(k);
				int wordLen = ((Text)n).getText().length();
				//sum characters in each word
				charIndex += wordLen;
			}
			//
			Insets pad = flow.getPadding();
			LOG.log(Level.INFO, "\n flow id: " + flow.getId() + ", flow padding: " + pad );
			Bounds b = textNode.getBoundsInParent();
			LOG.log(Level.INFO, "\n charIndex: " + charIndex + ", bounds In Parent: " + b );
			//
			//get Text caret shape
			PathElement[] cshape = flow.caretShape(charIndex, true);
			//
			MoveTo moveto = (MoveTo)cshape[0];
			LineTo lineto = (LineTo)cshape[1];

			LOG.log(Level.INFO, "\n index: " + charIndex + ", moveto: " + moveto + ", lineto: " + lineto);
			//
		} catch (Exception ex) {
			LOG.log(Level.SEVERE, ex.getMessage(), ex);
			throw new RuntimeException(ex);
		}
	}

    @Override
    public void start(Stage primaryStage) throws Exception{

		//add textflow to vbox
		test();
		// add stack pane to outer pane
		sp.setAlignment(Pos.CENTER);
		sp.getChildren().add(vbox);
		bp.setCenter(sp);
		//
        // setup scene and stage
        Scene scene = new Scene( bp, 600, 500 );

        primaryStage.setScene(scene);
        primaryStage.sizeToScene();
        primaryStage.show();

        //
        check();

    }



    public static void main(String[] args) {
        launch(args);
    }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Test code shows a workaround in case 3 and 4, in which each TextFlow is added to a StackPane before adding to the VBox. Rather than adding an inset to a TextFlow, add the inset to the TextFlow container.

Caes 3 and 4 - TextFlow inset = 0.
Case 3 - Stackpane left inset = 0,  and the x locations match as in Case1.
Case 4 - Stackpane left inset =17,  and the x locations match as in Case1.


Comments
thank you. do you mind taking a look at this as well?
16-11-2023

I checked with the latest master branch and the issue still exists.
16-11-2023

[~kpk] do we still have this issue?
15-11-2023

When ran the provided test case in JDK 9.0.4+11 and 11-ea+3 (Windows 10), program output is same as below: case 2 : When left or top inset is added to the TextFlow, the caretShape MoveTo x differs from the getBoundsInParent minX by the amount of the inset. bounds In Parent: BoundingBox [minX:18.0, .... moveto: MoveTo[x=0.0, .... Case 4 : (Workaround) But when inset is added to StackPane containing TextFlow, caretShape returns correct value. bounds In Parent: BoundingBox [minX:1.3333333730697632, .... moveto: MoveTo[x=0.0, ....
06-03-2018