JavaFX TableView with highlighted text
Asked Answered
F

2

3

I want to highlight parts of text displayed in a JavaFX TableView. Till now i'm using Text objects in TextFlow objects. To highlight specific parts in a text i am using tags to cut the text in parts (javafx.scene.text objects) to highlight or not to highlight with the following code.

col3.setCellValueFactory(new PropertyValueFactory<RegexMatch, String>("text"));
col3.setCellFactory(new Callback<TableColumn, TableCell>() {
    @Override
    public TableCell call(TableColumn param) {
        TableCell cell = new TableCell() {
            @Override
            protected void updateItem(Object text, boolean empty) {
                if (text != null && text instanceof String) {
                    String str = (String) text;
                    TextFlow flow = new TextFlow();
                    if (txtSearchField.getText().length() > 3 && str.contains(HIGHLIGHT_START)) {
                        // Something to highlight
                        flow.getChildren().clear();
                        while (str.contains(HIGHLIGHT_START)) {
                            // First part
                            Text starttext = new Text(str.substring(0, str.indexOf(HIGHLIGHT_START)));
                            starttext.setWrappingWidth(Double.MAX_VALUE);
                            flow.getChildren().add(starttext);
                            str = str.substring(str.indexOf(HIGHLIGHT_START) + HIGHLIGHT_START.length(), str.length());
                            // Part to highlight
                            Text highlightedText = new Text(str.substring(0, str.indexOf(HIGHLIGHT_END)));
                            highlightedText.setStyle("-fx-text-background-color: yellow;");
                            highlightedText.setFill(Color.BLUE);
                            highlightedText.setWrappingWidth(Double.MAX_VALUE);
                            flow.getChildren().add(highlightedText);
                            // Last part
                            str = str.substring(str.indexOf(HIGHLIGHT_END) + HIGHLIGHT_END.length(), str.length());
                            if (!str.contains(HIGHLIGHT_START)) {
                                Text endtext = new Text(str);
                                endtext.setWrappingWidth(Double.MAX_VALUE);
                                flow.getChildren().add(endtext);
                            }
                        }
                    }else if (txtSearchField.getText().length() < 1) {
                        // Remove former highlightings and show simple text
                        str = str.replaceAll(HIGHLIGHT_START, "");
                        str = str.replaceAll(HIGHLIGHT_END, "");
                        flow.getChildren().clear();
                        Text textModule = new Text(str);
                        textModule.setWrappingWidth(Double.MAX_VALUE);
                        flow.getChildren().add(textModule);
                    } else {
                        // show simple text
                        flow.getChildren().clear();
                        Text textModule = new Text(str);
                        textModule.setWrappingWidth(Double.MAX_VALUE);
                        flow.getChildren().add(textModule);
                    }
                    flow.setPrefHeight(bigIcons ? BIG_SIZE : SMALL_SIZE);
                    setGraphic(flow);
                }
            }
        };
        return cell;
    }
});

Unfortunately the background highlighting does not work and i have strange linebreaks as shown in the picture. The text does not contain any linebreaks.

scrennshot oft table

(Sorry for the picture quality, its a real screenshot :))

Any help is appreciated.

Solution
As @eckig suggested, using multiple Labels in a HBox is an good idea, because each 'Label' can have its own background color and you can use as much Labels in line in a HBox as needed:

  col3.setCellValueFactory(new PropertyValueFactory<RegexMatch, String("excerptLineTable"));
  col3.setCellFactory(new Callback<TableColumn, TableCell>() {
    @Override
    public TableCell call(TableColumn param) {
        TableCell cell = new TableCell() {
            @Override
            protected void updateItem(Object text, boolean empty) {
                if (text != null && text instanceof String) {
                    HBox hbox = new HBox();


                    String str = (String) text;
                    if (txtSearchField.getText().length() > 3 && str.contains(HIGHLIGHT_START)) {
                        // Something to highlight
                        hbox.getChildren().clear();
                        while (str.contains(HIGHLIGHT_START)) {
                            // First part
                            Label label = new Label(str.substring(0, str.indexOf(HIGHLIGHT_START)));
                            hbox.getChildren().add(label);
                            str = str.substring(str.indexOf(HIGHLIGHT_START) + HIGHLIGHT_START.length(), str.length());
                            // Part to highlight
                            Label label2 = new Label(str.substring(0, str.indexOf(HIGHLIGHT_END)));
                            label2.setStyle("-fx-background-color: blue;");
                            hbox.getChildren().add(label2);
                            // Last part
                            str = str.substring(str.indexOf(HIGHLIGHT_END) + HIGHLIGHT_END.length(), str.length());
                            if (!str.contains(HIGHLIGHT_START)) {
                                Label label3 = new Label(str);
                                hbox.getChildren().add(label3);
                            }
                        }
                    } else if (txtSearchField.getText().length() < 1) {
                        // Remove former highlightings and show simple text
                        str = str.replaceAll(HIGHLIGHT_START, "");
                        str = str.replaceAll(HIGHLIGHT_END, "");
                        hbox.getChildren().clear();
                        Label label = new Label(str);
                        hbox.getChildren().add(label);
                    } else {
                        // show simple text
                        hbox.getChildren().clear();
                        Label label = new Label(str);
                        hbox.getChildren().add(label);
                    }
                    setGraphic(hbox);
                }
            }
        };
        return cell;
    }
});
Footcloth answered 13/11, 2014 at 10:35 Comment(0)
L
3

After double reading your problem I think we can resume this as follows:

  • A rather long text in a TableColumn
  • The user should be able to filter this text
  • Each text-fragment which matches the current search term is to be highlighted.

Now you have two options:

  1. Create a HBox and add Labels containg the text fragments. A Label extends Region and so may have a background color.
  2. Use TextFlow and add Texts as text fragments. There you can "only" change the foreground color.

Ah and before I forget: To disable TextFlow text wrapping you must not call textModule.setWrappingWidth(Double.MAX_VALUE); but instead textModule.setPrefWidth(Double.MAX_VALUE);

And another hint: Try to recycle as much as possible. Recreating the whole TextFlow on each update is not really a good idea (instead store it as member variable in the cell).

Lyra answered 13/11, 2014 at 19:16 Comment(12)
Unfortunately setPrefWidth is not available for Text. I have to rebuild each TextFlows because the highlighted areas do change since the user wants to highlight a different patternFootcloth
First: I meant setPrefWidth on TextFlow, not Text. And secondly: You could store the TextFlow and rebuild only its contained Text elements.Lyra
1) Doesn't change anythink, the text becomes wrapped as before 2) Yes, to keep the TextFlow element would be possible but since i have to delete alls Text children and create new ones i am in doubt about improving the performance with this...Footcloth
As I said in your other question regarding TextFlow: The wrapping width seems to behave differently depending on its parent level element. In this case the TableCellSkin seems to be the problem. I assume you are using the default skin which is just a Label, where you set the TextFlow as graphic element? I'm just guessing here, but I think writing a custom skin may help if you still stick to the TextFlow. And regarding the performance: I don't know how big your table is, but updateItem() gets called rather often..Lyra
And a third option just got to my mind: Using a Canvas element! You can draw the text fragments to the Canvas and as you then know where each fragment starts and ends, you can paint a background rectangle behind a fragment that is to be highlighted.Lyra
The hint to use Labels did it for me! Works like a charm! I posted my solution above.Footcloth
Great to hear that you could solve it. I am still interested how this solution performs with large data sets in your table.. :-)Lyra
Around 200 lines are now Problem at all, rendering >300,000 lines really takes too long (~ 1 minute). I think i have to focus on the visible part of the table in that case...Footcloth
You can move the logic part of the search & split functionality to the data class being displayed in the table. If you lazy initialize these properties, it should speed things up.Lyra
Ok, 1) i understand, but how would you do 2). Can you please provide an example?Footcloth
I dont think, that the comments section is appropriate for this, create a follow up question for that (speeding up your table).Lyra
Ok, i splitted it up in two questions: #26992021 and #26992219Footcloth
D
3

According to this, Text doesn't have background stylable properties, so that's why changing -fx-text-background-color doesn't have any effect on it.

One possible workaround for this is wrapping this text node in an HBox, where you can easily set its background to yellow. But this has several drawbacks, such as the textflow losing the ability to manage the content.

If you really need highlighting, other possible solution could be styling the background of the textflow, by finding the bounds of the text, and setting the proper insets.

This is an example I've quickly done with Scenic Builder and some css:

.textflow {
    -fx-background-color: yellow;
    -fx-background-insets: 2 139.3 10 200;
}

Highlighted text

I've taken into account the width of the textflow (510) and the widths of the different text nodes using local bounds: 200, 170.7 and 129. So the right inset is 510-200-170.7=139.3 and the left inset is given from the first node width, 200.

Now the challenge is adapt this in your method...

Demonology answered 13/11, 2014 at 13:40 Comment(2)
I ended up using Labels in an HBox as @Lyra and you suggested, thanks!Footcloth
Good idea with the insets, but it can only work with 1 match per cell, right?Ratel
L
3

After double reading your problem I think we can resume this as follows:

  • A rather long text in a TableColumn
  • The user should be able to filter this text
  • Each text-fragment which matches the current search term is to be highlighted.

Now you have two options:

  1. Create a HBox and add Labels containg the text fragments. A Label extends Region and so may have a background color.
  2. Use TextFlow and add Texts as text fragments. There you can "only" change the foreground color.

Ah and before I forget: To disable TextFlow text wrapping you must not call textModule.setWrappingWidth(Double.MAX_VALUE); but instead textModule.setPrefWidth(Double.MAX_VALUE);

And another hint: Try to recycle as much as possible. Recreating the whole TextFlow on each update is not really a good idea (instead store it as member variable in the cell).

Lyra answered 13/11, 2014 at 19:16 Comment(12)
Unfortunately setPrefWidth is not available for Text. I have to rebuild each TextFlows because the highlighted areas do change since the user wants to highlight a different patternFootcloth
First: I meant setPrefWidth on TextFlow, not Text. And secondly: You could store the TextFlow and rebuild only its contained Text elements.Lyra
1) Doesn't change anythink, the text becomes wrapped as before 2) Yes, to keep the TextFlow element would be possible but since i have to delete alls Text children and create new ones i am in doubt about improving the performance with this...Footcloth
As I said in your other question regarding TextFlow: The wrapping width seems to behave differently depending on its parent level element. In this case the TableCellSkin seems to be the problem. I assume you are using the default skin which is just a Label, where you set the TextFlow as graphic element? I'm just guessing here, but I think writing a custom skin may help if you still stick to the TextFlow. And regarding the performance: I don't know how big your table is, but updateItem() gets called rather often..Lyra
And a third option just got to my mind: Using a Canvas element! You can draw the text fragments to the Canvas and as you then know where each fragment starts and ends, you can paint a background rectangle behind a fragment that is to be highlighted.Lyra
The hint to use Labels did it for me! Works like a charm! I posted my solution above.Footcloth
Great to hear that you could solve it. I am still interested how this solution performs with large data sets in your table.. :-)Lyra
Around 200 lines are now Problem at all, rendering >300,000 lines really takes too long (~ 1 minute). I think i have to focus on the visible part of the table in that case...Footcloth
You can move the logic part of the search & split functionality to the data class being displayed in the table. If you lazy initialize these properties, it should speed things up.Lyra
Ok, 1) i understand, but how would you do 2). Can you please provide an example?Footcloth
I dont think, that the comments section is appropriate for this, create a follow up question for that (speeding up your table).Lyra
Ok, i splitted it up in two questions: #26992021 and #26992219Footcloth

© 2022 - 2024 — McMap. All rights reserved.