JDK-8194704 : TextFlow HitTest not working as expected
  • Type: Bug
  • Component: javafx
  • Sub-Component: scenegraph
  • Affected Version: 9,10
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2018-01-05
  • Updated: 2018-09-05
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
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 :
The description of the TextFlow HitTest is:
"Maps local point to index in the content."
This does not return the node index at the given coordinates. Instead it returns the character index within the Text node.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Create TextFlow contining several nodes. Add mouse listener to each node. Use mouse coordinates as input to TextFlow HitTest. Print the HitInfo that is returned.
Also print HitInfo for the Text node HitTest. The two HitInfo are identical. Compare actual node index to TextFlow HitInfo results; they don't match.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The TextFlow HitTest should return the node index at the input coordinates.
ACTUAL -
The TextFlow HitTest returned the same results as the Text node HitTest.

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.geometry.Point2D;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.HitInfo;
import javafx.scene.text.TextFlow;
import javafx.scene.layout.VBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.collections.ObservableMap;
import javafx.event.EventHandler;
//
//
public class FlowHit extends Application {
	/*
	This app tests the HitTest method for Text and TextFlow. Both methods
	return the same HitInfo.

	Is this a bug in TextFlow?
	*/

private static final Logger LOG = Logger.getLogger(FlowHit.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();
private TextFlow flow = new TextFlow();
//
//init sample string of words
private String s1 = "Electromagnetic waves cover a wide spectrum from radio waves to gamma rays. Waves can interfere. "
    + "Constructive interference occurs when two waves are in phase. ";

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

	public void test(){
		try{
			init();

			addText(flow, s1);

			vbox.getChildren().add(flow);


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

	public void init(){
		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 addText(TextFlow flow, String s){
		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 mouse click handler
				tx.setOnMouseReleased(new EventHandler<MouseEvent>() {
					@Override
					public void handle(MouseEvent event) {
						if (event.getButton() == MouseButton.PRIMARY) {
							event.consume();
							double mx = event.getX();
							double my = event.getY();
							int index = flow.getChildren().indexOf(tx);
							//check baseline offset
							checkHit(flow, index, mx, my);
						}
					}
				});

				//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);
		}
	}

	public void checkHit(TextFlow flow, int index, double mx, double my){
		//check hit info of selected node
		try{
			//
			Node n = flow.getChildren().get(index);
			Text t = (Text)n;
			//create rectangle around selected node (may be commented out)
			addBox(n);
			//find mouse point
			Point2D pt = new Point2D(mx, my);
			//
			//get Text HitInfo
			HitInfo hitText = t.hitTest(pt);
			//
			//get TextFlow HitInfo
			HitInfo hitFlow = flow.hitTest(pt);


			LOG.log(Level.INFO, "\n index: " + index + ", text: " + t.getText() + ", pt:" + pt + ", hitText:" + hitText + ", hitFlow:" + hitFlow );
			//
		} catch (Exception ex) {
			LOG.log(Level.SEVERE, ex.getMessage(), ex);
			throw new RuntimeException(ex);
		}
	}

	public void addBox(Node n){
		//add box around of selected node
		try{
			//
			//get text bounds within flow (parent)
			Bounds parentB = n.getBoundsInParent();
		//	LOG.log(Level.INFO, "\n\n node bounds in parent: " + parentB);

			//convert to scene coords
			Bounds sceneB = flow.localToScene(parentB);
		//	LOG.log(Level.INFO, "\n\n scene bounds: " + sceneB);

			//create rectangle in scene coords
			Rectangle r = new Rectangle( sceneB.getMinX(), sceneB.getMinY(), sceneB.getWidth(), sceneB.getHeight() );
			r.setStroke(Color.GREEN);
			r.setFill(Color.TRANSPARENT);
			//add rectangle to border pane
			bp.getChildren().add(r);
			//
		} 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 baselines of selected nodes
    }



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

CUSTOMER SUBMITTED WORKAROUND :
Added mouse listener to every node within each TextFlow, and use ObservableList<Node> getChildren() -> indexOf(Object o) to determine the node index at the mouse location. Don't use the TextFlow HitTest.


Comments
'HitInfo' provided by TextFlow HitTest and Text node HitTest are same. Behavior is same in JDK 9 (9.0.1+11) and JDK 10(10-ea+38). Also same in windows 10 and in Ubuntu 16.04.3 LTS.
06-01-2018