I would like to change the disclosure node of expanding/unexpanding in Tree View without using -fx-background-image of CSS .arrow, because eventhough the image is 9*9 pixel, it shows so bad. I want to use the setCellFactory, but I don't know how.
I have several questions :
in setCellFactory, what is the purpose of overriding call or updateItem method? which one to override in this case?
what is the different in item == null or boolean empty = true in updateItem? what case does the boolean empty = true handles?
I want my view to be like this
since TreeView can only contain one type of Item and Group A label is not a Person, other than putting Group A as a name of a Person object I guess I can only display the tree using a loop and put the TreeItems under a Box according to which Group they belong to. However, I don't know how to make expandable Box (use VBox?Hbox?StackPane?) please give me a hint about this
package DummyGUI;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.*;
import javafx.event.ActionEvent;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
public class App extends Application
{
private final Image male = new Image(getClass().getResourceAsStream("male.png"), 16, 16, true, true);
private final Node maleIcon = new ImageView(male);
private final Image female = new Image(getClass().getResourceAsStream("female.png"), 16, 16, true, true);
private final Node femaleIcon = new ImageView(female);
private final Image plus = new Image(getClass().getResourceAsStream("plus-button.png"), 16, 16, true, true);
private final Node plusIcon = new ImageView(plus);
private final Image minus = new Image(getClass().getResourceAsStream("minus-button.png"), 16, 16, true, true);
private final Node minusIcon = new ImageView(minus);
private TreeView<Person> tree;
public static void main(String[] args)
{
launch();
}
public void start(Stage topView)
{
createGUI(topView);
}
private void createGUI(Stage topView)
{
topView.setTitle("Dummy App");
initTree();
VBox vb = new VBox(tree);
topView.setScene(new Scene(vb));
topView.show();
}
private void initTree()
{
Person person1 = new Person("Charles", 'M', '0');
Person person2 = new Person("John", 'M', 'A');
Person person3 = new Person("Pearl", 'M', 'A');
TreeItem<Person> root = new TreeItem<>(person1);
TreeItem<Person> child1 = new TreeItem<>(person2);
TreeItem<Person> child2 = new TreeItem<>(person3);
tree = new TreeView<>(root);
root.setExpanded(true);
root.getChildren().addAll(child1, child2);
tree.setCellFactory(tv ->
{
HBox hb = new HBox();
TreeCell<Person> cell = new TreeCell<Person>()
{
#Override
public void updateItem(Person item, boolean empty)
{
super.updateItem(item, empty);
if(empty)
{
setGraphic(null);
setText(null);
}
else
{
Node icon = (item.getGender() == 'M' ? maleIcon : femaleIcon);
setGraphic(icon);
setText(item.toString());
}
}
};
cell.setDisclosureNode(plusIcon);
return cell;
});
}
}
package DummyGUI;
public class Person
{
String name;
char gender;
char group;
public Person(String name, char gender, char group)
{
this.name = name;
this.gender = gender;
this.group = group;
}
public String getName()
{
return name;
}
public char getGender()
{
return gender;
}
public char getGroup()
{
return group;
}
public String toString()
{
return name;
}
}
I tried to change the arrow icon to a plus icon but it has bug because I don't understand how to override updateItem.
I don't know why the gender icon for John and Charles does not show when the tree is expanded and I dont know how to add minus icon.
Thank you
Minimalistic answer:
The basic problem is the re-use of the same instance of the nodes in all cells - nodes must have exactly one parent. Using the same, silently removes it from its former parent (or throws an exception)
The adapted cell
tree.setCellFactory(tv -> {
// per-cell icons
Node maleIcon = new Button("m");
Node femaleIcon = new Button("f");
Node expanded = new Label("-");
Node collapsed = new Label("+");
TreeCell<Person> cell = new TreeCell<Person>() {
#Override
public void updateItem(Person item, boolean empty) {
super.updateItem(item, empty);
// check if the cell is empty (no data) or if the data is null
if (item == null || empty) {
setGraphic(null);
setText(null);
} else {
Node icon = (item.getGender() == 'M' ? maleIcon
: femaleIcon);
setGraphic(icon);
// never-ever override toString for application reasons
// instead set the text from data properties
setText(item.getName());
}
if (getTreeItem() != null) {
// update disclosureNode depending on expanded state
setDisclosureNode(getTreeItem().isExpanded() ? expanded : collapsed);
}
}
};
return cell;
});
if (getTreeItem() != null)
{
setDisclosureNode(getTreeItem().isExpanded() ? expanded : collapsed);
setOnMouseClicked(event -> {
setDisclosureNode(getTreeItem().isExpanded() ? expanded : collapsed);
});
}
its ok, i think with setOnMouseClicked it becomes good x0 thanks!
Related
I have a request to display a string in various colours in a table cell, that is one portion of a string in one colour and the rest in another colour (either the background or the text). I have found an article on changing the cell background colour, but not a portion of a cell. That is close to the requirement, but don't meet the requirement.
The only possible solution, I can think of, is to use the Text type which can be set with various colours after splitting a string into two parts. But, how to use the Text type data with the TableView setup as the following?
aColumn.setCellValueFactory(p -> new SimpleStringProperty(...) );
...
aTalbeView.setItems(FXcollections.observableArrayList(...));
I am still new to JavaFX. Is it doable? If so, how shall I approach a solution?
A mock up table is attached below.
The cellValueFactory is used to tell the cell what data to display. To tell the cell how to display its data, use a cellFactory. The two are more or less independent.
So you can do
aColumn.setCellValueFactory(p -> new SimpleStringProperty(...));
and then something like:
aColumn.setCellFactory(tc -> new TableCell<>() {
private final String[] palette = new String[] {
"#1B9E77", "#D95F02", "#7570B3", "#E7298A",
"#66A61E", "#E6AB02", "#A6761D", "#666666" };
private TextFlow flow = new TextFlow();
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
flow.getChildren().clear();
int i = 0 ;
for (String word : item.split("\\s")) {
Text text = new Text(word);
text.setFill(Color.web(palette[i++ % palette.length]);
flow.getChildren().add(text);
flow.getChildren().add(new Text(" "));
}
setGraphic(flow);
}
}
});
This assumes each cell has multiple words (separated by whitespace) and colors each word a different color. You can implement the different colors any way you like; this shows the basic idea.
The approach used in this answer
An additional range parameter is added to the backing model to indicate the highlight range for text in the cell.
The cellValueFactory uses a binding statement to allow the cell to respond to updates to either the text in the cell or the highlight range.
Labels within an HBox are used for the cell graphic rather than a
TextFlow as labels have more styling options (e.g. for text
background) than text nodes in TextFlow.
Using multiple labels within the cells does change some of the eliding behavior of the cell when not enough room is available in the column to include all text, this could be customized by setting properties on the HBOX or label to configure this behavior how you want.
CSS stylesheet for styling is included in the code but could be
extracted to a separate stylesheet if desired.
I didn't thoroughly test the solution, so there may be logic errors around some of the boundary conditions.
Screenshots
Highlighted a subset of text within a cell in a non-selected row:
Highlighted a subset of text within a cell in a selected row:
Example code
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class HighlightedTextTableViewer extends Application {
private static final String CSS_DATA_URL = "data:text/css,";
private static final String HIGHLIGHTABLE_LABEL_CSS = CSS_DATA_URL + // language=CSS
"""
.highlightable {
-fx-font-family: monospace;
-fx-font-weight: bold;
}
.highlight {
-fx-background-color: cornflowerblue;
-fx-text-fill: white;
}
""";
private static final String HIGHLIGHTABLE_STYLE_CLASS = "highlightable";
private static final String HIGHLIGHTED_STYLE_CLASS = "highlight";
#Override
public void start(Stage stage) {
TableView<Field> table = createTable();
populateTable(table);
VBox layout = new VBox(
10,
table
);
layout.setPadding(new Insets(10));
layout.setPrefHeight(100);
stage.setScene(new Scene(layout));
stage.show();
}
private TableView<Field> createTable() {
TableView<Field> table = new TableView<>();
TableColumn<Field, String> nameColumn = new TableColumn<>("Name");
nameColumn.setCellValueFactory(
p -> p.getValue().nameProperty()
);
TableColumn<Field, Field> valueColumn = new TableColumn<>("Value");
valueColumn.setCellValueFactory(
p -> Bindings.createObjectBinding(
p::getValue,
p.getValue().valueProperty(), p.getValue().highlightRangeProperty()
)
);
valueColumn.setCellFactory(param -> new HighlightableTextCell());
//noinspection unchecked
table.getColumns().setAll(nameColumn, valueColumn);
return table;
}
public static class HighlightableTextCell extends TableCell<Field, Field> {
protected void updateItem(Field item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty || item.getValue() == null) {
setGraphic(null);
} else {
setGraphic(constructTextBox(item));
}
}
private Node constructTextBox(Field item) {
HBox textBox = new HBox();
textBox.getStylesheets().setAll(HIGHLIGHTABLE_LABEL_CSS);
textBox.getStyleClass().add(HIGHLIGHTABLE_STYLE_CLASS);
int from = item.getHighlightRange() != null ? item.getHighlightRange().from() : -1;
int valueLen = item.getValue() != null ? item.getValue().length() : -1;
int to = item.getHighlightRange() != null ? item.getHighlightRange().to() : -1;
if (item.highlightRangeProperty() == null
|| from >= to
|| from > valueLen
) { // no highlight specified or no highlight in range.
textBox.getChildren().add(
createStyledLabel(
item.getValue()
)
);
} else {
textBox.getChildren().add(
createStyledLabel(
item.getValue().substring(
0,
from
)
)
);
if (from >= valueLen) {
return textBox;
}
textBox.getChildren().add(
createStyledLabel(
item.getValue().substring(
from,
Math.min(valueLen, to)
), HIGHLIGHTED_STYLE_CLASS
)
);
if (to >= valueLen) {
return textBox;
}
textBox.getChildren().add(
createStyledLabel(
item.getValue().substring(
to
)
)
);
}
return textBox;
}
private Label createStyledLabel(String value, String... styleClasses) {
Label label = new Label(value);
label.getStyleClass().setAll(styleClasses);
return label;
}
}
private void populateTable(TableView<Field> table) {
table.getItems().addAll(
new Field("Dragon", "93 6d 6d da", null),
new Field("Rainbow", "0c fb ff 1c", new Range(3, 8))
);
}
}
class Field {
private final StringProperty name;
private final StringProperty value;
private final ObjectProperty<Range> highlightRange;
public Field(String name, String value, Range highlightRange) {
this.name = new SimpleStringProperty(name);
this.value = new SimpleStringProperty(value);
this.highlightRange = new SimpleObjectProperty<>(highlightRange);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getValue() {
return value.get();
}
public StringProperty valueProperty() {
return value;
}
public void setValue(String value) {
this.value.set(value);
}
public Range getHighlightRange() {
return highlightRange.get();
}
public ObjectProperty<Range> highlightRangeProperty() {
return highlightRange;
}
public void setHighlightRange(Range highlightRange) {
this.highlightRange.set(highlightRange);
}
}
record Range(int from, int to) {}
Alternative using TextField
An alternative to the HBox for displaying highlighted text would be to use a TextField (non-editable), which allows a selection to be set (via APIs on the text field), however, I did not attempt a solution with a TextField approach. A TextField may allow a user to drag the mouse to select text (perhaps could be disabled if desired by making the field mouse transparent).
Related Questions (uses TextFlow)
Highlight text in TableView with TextFlow
JavaFX TableView with highlighted text
JavaFX: setting background color for Text controls
I have looked days on any ready solution for the subject of having TOGETHER in javafx (pure) :
Combobox
Multiselect of items through Checkboxes
Filter items by the "editable" part of the Combobox
I have had no luck finding what I was looking for so I have now a working solution taken from different separate solution... Thank you to all for this !
Now I would like to know if what I have done follows the best practices or not... It's working... but is it "ugly" solution ? Or would that be a sort of base anyone could use ?
I tied to comment as much as I could, and also kept the basic comment of the sources :
user:2436221 (Jonatan Stenbacka) --> https://stackoverflow.com/a/34609439/14021197
user:5844477 (Sai Dandem) --> https://stackoverflow.com/a/52471561/14021197
Thank you for your opinions, and suggestions...
Here is the working example :
package application;
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;
#SuppressWarnings ("restriction") // Only applies for PROTECTD library : com.sun.javafx.scene.control.skin.ComboBoxListViewSkin
public class MultiSelectFiltered2 extends Application {
// These 2 next fields are used in order to keep the FILTERED TEXT entered by user.
private String aFilterText = "";
private boolean isUserChangeText = true;
public void start(Stage stage) {
Text txt = new Text(); // A place where to expose the result of checked items.
HBox vbxRoot = new HBox(); // A basic root to order the GUI
ComboBox<ChbxItems> cb = new ComboBox<ChbxItems>() {
// This part is needed in order to NOT have the list hided when an item is selected...
// TODO --> Seems a little ugly to me since this part is the PROTECTED part !
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new ComboBoxListViewSkin<ChbxItems>(this) {
#Override
protected boolean isHideOnClickEnabled() {
return false;
}
};
}
};
cb.setEditable(true);
// Create a list with some dummy values.
ObservableList<ChbxItems> items = FXCollections.observableArrayList();
items.add(new ChbxItems("One"));
items.add(new ChbxItems("Two"));
items.add(new ChbxItems("Three"));
items.add(new ChbxItems("Four"));
items.add(new ChbxItems("Five"));
items.add(new ChbxItems("Six"));
items.add(new ChbxItems("Seven"));
items.add(new ChbxItems("Eight"));
items.add(new ChbxItems("Nine"));
items.add(new ChbxItems("Ten"));
// Create a FilteredList wrapping the ObservableList.
FilteredList<ChbxItems> filteredItems = new FilteredList<ChbxItems>(items, p -> true);
// Add a listener to the textProperty of the combo box editor. The
// listener will simply filter the list every time the input is changed
// as long as the user hasn't selected an item in the list.
cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
// This needs to run on the GUI thread to avoid the error described here:
// https://bugs.openjdk.java.net/browse/JDK-8081700.
Platform.runLater(() -> {
if (isUserChangeText) {
aFilterText = cb.getEditor().getText();
}
// If the no item in the list is selected or the selected item
// isn't equal to the current input, we re-filter the list.
filteredItems.setPredicate(item -> {
boolean isPartOfFilter = true;
// We return true for any items that starts with the
// same letters as the input. We use toUpperCase to
// avoid case sensitivity.
if (!item.getText().toUpperCase().startsWith(newValue.toUpperCase())) {
isPartOfFilter = false;
}
return isPartOfFilter;
});
isUserChangeText = true;
});
});
cb.setCellFactory(new Callback<ListView<ChbxItems>, ListCell<ChbxItems>>() {
#Override
public ListCell<ChbxItems> call(ListView<ChbxItems> param) {
return new ListCell<ChbxItems>() {
private CheckBox chbx = new CheckBox();
// This 'just open bracket' opens the newly CheckBox Class specifics
{
chbx.setOnAction(new EventHandler<ActionEvent>() {
// This VERY IMPORTANT part will effectively set the ChbxItems item
// The argument is never used, thus left as 'arg0'
#Override
public void handle(ActionEvent arg0) {
// This is where the usual update of the check box refreshes the editor' text of the parent combo box... we want to avoid this ;-)
isUserChangeText = false;
// The one line without which your check boxes are going to be checked depending on the position in the list... which changes when the list gets filtered.
getListView().getSelectionModel().select(getItem());
// Updating the exposed text from the list of checked items... This is added here to have a 'live' update.
txt.setText(updateListOfValuesChosen(items));
}
});
}
private BooleanProperty booleanProperty; //Will be used for binding... explained bellow.
#Override
protected void updateItem(ChbxItems item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
// Binding is used in order to link the checking (selecting) of the item, with the actual 'isSelected' field of the ChbxItems object.
if (booleanProperty != null) {
chbx.selectedProperty().unbindBidirectional(booleanProperty);
}
booleanProperty = item.isSelectedProperty();
chbx.selectedProperty().bindBidirectional(booleanProperty);
// This is the usual part for the look of the cell
setGraphic(chbx);
setText(item.getText() + "");
} else {
// Look of the cell, which has to be "reseted" if no item is attached (empty is true).
setGraphic(null);
setText("");
}
// Setting the 'editable' part of the combo box to what the USER wanted
// --> When 'onAction' of the check box, the 'behind the scene' update will refresh the combo box editor with the selected object reference otherwise.
cb.getEditor().setText(aFilterText);
cb.getEditor().positionCaret(aFilterText.length());
}
};
}
});
// Yes, it's the filtered items we want to show in the combo box...
// ...but we want to run through the original items to find out if they are checked or not.
cb.setItems(filteredItems);
// Some basic cosmetics
vbxRoot.setSpacing(15);
vbxRoot.setPadding(new Insets(25));
vbxRoot.setAlignment(Pos.TOP_LEFT);
// Adding the visual children to root VBOX
vbxRoot.getChildren().addAll(txt, cb);
// Ordinary Scene & Stage settings and initialization
Scene scene = new Scene(vbxRoot);
stage.setScene(scene);
stage.show();
}
// Just a method to expose the list of items checked...
// This is the result that will be probably the input for following code.
// -->
// If the class ChbxItems had a custom object rather than 'text' field,
// the resulting checked items from here could be a list of these custom objects --> VERY USEFUL
private String updateListOfValuesChosen(ObservableList<ChbxItems> items) {
StringBuilder sb = new StringBuilder();
items.stream().filter(ChbxItems::getIsSelected).forEach(cbitem -> {
sb.append(cbitem.getText()).append("\n");
});
return sb.toString();
}
// The CHECKBOX object, with 2 fields :
// - The boolean part (checked ot not)
// - The text part which is shown --> Could be a custom object with 'toString()' overridden ;-)
class ChbxItems {
private SimpleStringProperty text = new SimpleStringProperty();
private BooleanProperty isSelected = new SimpleBooleanProperty();
public ChbxItems(String sText) {
setText(sText);
}
public void setText(String text) {
this.text.set(text);
}
public String getText() {
return text.get();
}
public SimpleStringProperty textProperty() {
return text;
}
public void setIsSelected(boolean isSelected) {
this.isSelected.set(isSelected);
}
public boolean getIsSelected() {
return isSelected.get();
}
public BooleanProperty isSelectedProperty() {
return isSelected;
}
}
public static void main(String[] args) {
launch();
}
}
I have following problem: When i create a TableView and put data in the columns, the columns fit to content automatically. But if there many rows (more than 30) JavaFX optimize the columnwidth to the average length of all content.
In the first example i put the long strings first in the table and everything is fine.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
// Init table
TableView<Person> table = new TableView<Person>();
table.setMaxWidth(Double.MAX_VALUE);
table.setMaxHeight(Double.MAX_VALUE);
table.setItems(persons);
// Init columns
TableColumn<Person, String> firstname = new TableColumn<Person, String>("Firstname");
firstname.setCellValueFactory(new PropertyValueFactory<Person, String>("firstname"));
table.getColumns().add(firstname);
TableColumn<Person, String> lastname = new TableColumn<Person, String>("Lastname");
lastname.setCellValueFactory(new PropertyValueFactory<Person, String>("lastname"));
table.getColumns().add(lastname);
TableColumn<Person, String> email = new TableColumn<Person, String>("E-Mail");
email.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
table.getColumns().add(email);
// Init Stage
Scene scene = new Scene(table, 400, 150);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public class Person {
private String firstname;
private String lastname;
private String email;
public Person(String firstname, String lastname, String email) {
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
}
// Getters and Setters
}
}
Looks good...
In the second example i put short strings first in the table and at least the long strings. In this example JavaFX choose columnwidth to small and the content gets cut.
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
[...]
Looks bad...
How can i avoid this?
EDIT 19.05.2018
The links in the comments didn`t work.
So, i have found the problem in the source of JavaFx:
In the updateScene() method the maxRows are set to 30. There is no way to change the value because every methods that are involved are protected or private.
The comment is right. It can be take much time to create a table if there many rows in the table. But sometimes a developer knows the much possible rows of a table or it´s okay to risk higher loadingtime.
One solution is to contact Oracle to create a setter() for the value of max. rows. So the developer can choose the max. rows for each column individually.
public class TableColumnHeader extends Region {
[...]
private void updateScene() {
// RT-17684: If the TableColumn widths are all currently the default,
// we attempt to 'auto-size' based on the preferred width of the first
// n rows (we can't do all rows, as that could conceivably be an unlimited
// number of rows retrieved from a very slow (e.g. remote) data source.
// Obviously, the bigger the value of n, the more likely the default
// width will be suitable for most values in the column
final int n = 30; // ------------------------------------------> This is the problem!
if (! autoSizeComplete) {
if (getTableColumn() == null || getTableColumn().getWidth() != DEFAULT_COLUMN_WIDTH || getScene() == null) {
return;
}
doColumnAutoSize(getTableColumn(), n);
autoSizeComplete = true;
}
}
[...]
private void doColumnAutoSize(TableColumnBase<?,?> column, int cellsToMeasure) {
double prefWidth = column.getPrefWidth();
// if the prefWidth has been set, we do _not_ autosize columns
if (prefWidth == DEFAULT_COLUMN_WIDTH) {
getTableViewSkin().resizeColumnToFitContent(column, cellsToMeasure);
}
}
[...]
}
public class TableViewSkin<T> extends TableViewSkinBase<T, T, TableView<T>, TableViewBehavior<T>, TableRow<T>, TableColumn<T, ?>> {
[...]
#Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
if (!tc.isResizable()) return;
// final TableColumn<T, ?> col = tc;
List<?> items = itemsProperty().get();
if (items == null || items.isEmpty()) return;
Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
if (cellFactory == null) return;
TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumnBase
cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.snappedLeftInset() + r.snappedRightInset();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows); // ------------------> if maxRows equals -1 every item will be checked
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(tc);
cell.updateTableView(tableView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
// dispose of the cell to prevent it retaining listeners (see RT-31015)
cell.updateIndex(-1);
// RT-36855 - take into account the column header text / graphic widths.
// Magic 10 is to allow for sort arrow to appear without text truncation.
TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
Node graphic = header.label.getGraphic();
double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
maxWidth = Math.max(maxWidth, headerWidth);
// RT-23486
maxWidth += padding;
if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
maxWidth = Math.max(maxWidth, tc.getWidth());
}
tc.impl_setWidth(maxWidth);
}
[...]
}
Another solution is to fire a MouseEvent on the header of the rect of the TableColumn.
If there a MouseEvent with a ClickCount equals 2 and the PrimaryButton is down the resizeColumnToFitContent() method is called with a value for maxRows of -1.
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
-1 means all rows that are in the TableView.
public class NestedTableColumnHeader extends TableColumnHeader {
[...]
private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent me) {
Rectangle rect = (Rectangle) me.getSource();
TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
if (! header.isColumnResizingEnabled()) return;
if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
// the user wants to resize the column such that its
// width is equal to the widest element in the column
header.getTableViewSkin().resizeColumnToFitContent(column, -1); // -----------------------> this method should be call and everything is fine
} else {
// rather than refer to the rect variable, we just grab
// it from the source to prevent a small memory leak.
Rectangle innerRect = (Rectangle) me.getSource();
double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
header.dragAnchorX = me.getSceneX();
header.columnResizingStarted(startX);
}
me.consume();
}
};
[...]
}
So, is it possible to create a new MouseEvent with a ClickCount of 2 and the PrimaryButtonDown-boolean is true and fire this to the TableColumn?
And: How can i contact Oracle to please them to create a setter() for the maxRows in the next release?
As mentioned here and here there is no easy way to determine the required height of a webview, until "RT-25005 Automatic preferred sizing of WebView" is implemented.
Are there any workarounds to this issue? I couldn't find a solution in SO or elsewhere. However since i think this is no uncommon problem, there needs to be a workaround. Any idea?
For Webviewsembeded in a stage I found the following solution (see here):
webView.getEngine().executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('height')"
);
or
Double.parseDouble(webView.getEngine().executeScript("document.height").toString())
However this doesn't seem to work for Webviews embedded in a treecell, like here. In this case I always get too big numbers as a result.
Minimal running example (including recommendation of jewelsea):
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Callback;
import org.w3c.dom.Document;
public class TableViewSampleHTML extends Application {
private final ObservableList<MyData> data = FXCollections.observableArrayList(new MyData(1L), new MyData(3L), new MyData(2L), new MyData(4L), new MyData(1L));
public static void main(final String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
final Scene scene = new Scene(new Group());
TableView<MyData> table = new TableView<>();
table.setPrefHeight(700);
final TableColumn<MyData, Long> nameCol = new TableColumn("So So");
nameCol.setMinWidth(200);
nameCol.setCellValueFactory(new PropertyValueFactory<>("i"));
// Allow to display Textflow in Column
nameCol.setCellFactory(new Callback<TableColumn<MyData, Long>, TableCell<MyData, Long>>() {
#Override
public TableCell<MyData, Long> call(TableColumn<MyData, Long> column) {
return new TableCell<MyData, Long>() {
#Override
protected void updateItem(Long item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
setStyle("");
} else {
WebView webview = new WebView();
webview.setPrefWidth(700.0);
WebEngine engine = webview.getEngine();
String textHTML = new String(new char[item.intValue()]).replace("\0", " <b> bold </b> normal, ");
// textHTML = "<body>"
// + textHTML + "</body>";
engine.loadContent(textHTML);
setGraphic(webview);
engine.documentProperty().addListener((obj, prev, newv) -> {
String heightText = engine.executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('height')"
).toString();
System.out.println("heighttext: " + heightText);
webview.setPrefHeight(Double.parseDouble(heightText.replace("px", "")));
this.setPrefHeight(Double.parseDouble(heightText.replace("px", "")));
setGraphic(webview);
});
}
}
};
}
});
table.setItems(data);
table.getColumns().addAll(nameCol);
((Group) scene.getRoot()).getChildren().addAll(table);
stage.setScene(scene);
stage.show();
}
public static class MyData {
private Long i;
public MyData(Long i) {
this.i = i;
}
public Long getI() {
return i;
}
}
}
Now the outout is
heighttext: 581px
heighttext: 581px
However these values seem to be too big. See screeenshot:
Some progress has been made and cell heights are now calculated more realisticly. Kindly see the relevant the adapted code below.
Relevant changens:
Seems like it is mandatory to call webview.setPrefHeight(-1); before executing the jevascript.
Javascript has been modified. No big change seen, but maybe the result is more general
Open points:
For some reason i still have to add + 15.0 to the calculated height. This is a hack. Seems like some additional lenght has to be considered somewhere.
Functionality on recalculation after resize of column has is not optimal. Using table.refresh() causes significant delay in rendering.
public class TableViewSampleHTML extends Application {
private final ObservableList<MyData> data = FXCollections.observableArrayList(new MyData(1L), new MyData(14L), new MyData(2L), new MyData(3L), new MyData(15L));
public static void main(final String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
final Scene scene = new Scene(new Group(), 400, 800);
TableView<MyData> table = new TableView<>();
table.setPrefWidth(400);
table.setPrefHeight(800);
final TableColumn<MyData, Long> nameCol = new TableColumn("So So");
final TableColumn<MyData, Long> col2 = new TableColumn("la la");
nameCol.setPrefWidth(200);
col2.setCellValueFactory(new PropertyValueFactory<>("i"));
nameCol.setCellValueFactory(new PropertyValueFactory<>("i"));
nameCol.widthProperty().addListener((ob,oldV,newV) -> {table.refresh();} );
// Allow to display Textflow in Column
nameCol.setCellFactory(new Callback<TableColumn<MyData, Long>, TableCell<MyData, Long>>() {
#Override
public TableCell<MyData, Long> call(TableColumn<MyData, Long> column) {
return new TableCell<MyData, Long>() {
#Override
protected void updateItem(Long item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
setStyle("");
} else {
WebView webview = new WebView();
WebEngine engine = webview.getEngine();
webview.setPrefHeight(-1); // <- Absolute must at this position (before calling the Javascript)
setGraphic(webview);
String textHTML = new String(new char[item.intValue()]).replace("\0", " <b> bold </b> normal, ");
textHTML = "<body>"
+ textHTML + "</body>";
engine.loadContent(textHTML);
engine.documentProperty().addListener((obj, prev, newv) -> {
String heightText = engine.executeScript( // <- Some modification, which gives moreless the same result than the original
"var body = document.body,"
+ "html = document.documentElement;"
+ "Math.max( body.scrollHeight , body.offsetHeight, "
+ "html.clientHeight, html.scrollHeight , html.offsetHeight );"
).toString();
System.out.println("heighttext: " + heightText);
Double height = Double.parseDouble(heightText.replace("px", "")) + 15.0; // <- Why are this 15.0 required??
webview.setPrefHeight(height);
this.setPrefHeight(height);
});
}
}
};
}
});
table.setItems(data);
table.getColumns().addAll(nameCol);
table.getColumns().addAll(col2);
((Group) scene.getRoot()).getChildren().addAll(table);
stage.setScene(scene);
stage.show();
}
public static class MyData {
private Long i;
public MyData(Long i) {
this.i = i;
}
public Long getI() {
return i;
}
}
}
Output now looks like:
From the example you linked (JavaFX webview, get document height) the height of the document is computed in a ChangeListener on the document:
engine.documentProperty().addListener((prop, oldDoc, newDoc) -> {
String heightText = engine.executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('height')"
).toString();
System.out.println("heighttext: " + heightText);
});
Output:
heighttext: 36px
heighttext: 581px
heighttext: 581px
In the code in your question you are not executing the height check based upon a ChangeListener. So you are querying the height of the WebView document before the document has been loaded (which is why it is returning zero for your code).
Basedon BerndGirt's answer.
public class WebviewCellFactory<S,T> implements Callback<TableColumn<S,T>, TableCell<S,T>> {
#Override
public TableCell<S, T> call(TableColumn<S, T> column) {
return new TableCell<S, T>() {
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
setStyle("");
} else {
WebView webview = new WebView();
WebEngine engine = webview.getEngine();
webview.setPrefHeight(-1); // <- Absolute must at this position (before calling the Javascript)
webview.setBlendMode(BlendMode.DARKEN);
setGraphic(webview);
engine.loadContent("<body topmargin=0 leftmargin=0 style=\"background-color: transparent;\">"+item+"</body>");
engine.documentProperty().addListener((obj, prev, newv) -> {
String heightText = engine.executeScript( // <- Some modification, which gives moreless the same result than the original
"var body = document.body,"
+ "html = document.documentElement;"
+ "Math.max( body.scrollHeight , body.offsetHeight, "
+ "html.clientHeight, html.scrollHeight , html.offsetHeight );"
).toString();
Double height = Double.parseDouble(heightText.replace("px", "")) + 10 ; // <- Why are this 15.0 required??
webview.setPrefHeight(height);
this.setPrefHeight(height);
});
}
}
};
}
}
then you just need to set
tableColumn.setCellFactory(new WebviewCellFactory());
if there are any mistakes please tell me.
I´m using a FXML to set my form, but I need to set the limit of characters in textfields. How can I made this ?
You can't directly set a limit to number of characters. But you can add a listener to lengthProperty() of the textfield
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TextFieldLimit extends Application {
private static final int LIMIT = 10;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage primaryStage) {
final TextField textField = new TextField();
textField.lengthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (newValue.intValue() > oldValue.intValue()) {
// Check if the new character is greater than LIMIT
if (textField.getText().length() >= LIMIT) {
// if it's 11th character then just setText to previous
// one
textField.setText(textField.getText().substring(0, LIMIT));
}
}
}
});
VBox vbox = new VBox(20);
vbox.getChildren().add(textField);
Scene scene = new Scene(vbox, 400, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
}
One more elegance solution
Pattern pattern = Pattern.compile(".{0,25}");
TextFormatter formatter = new TextFormatter((UnaryOperator<TextFormatter.Change>) change -> {
return pattern.matcher(change.getControlNewText()).matches() ? change : null;
});
textField.setTextFormatter(formatter);
where 0 and 25 - min and max amount of chars. + ability to set a pattern of input text
Here is my solution to limit the length of a textfield.
I would not recommend solutions which use a listener (on text property or on length property), they do not behave correctly in all situations (for what I have seen).
I create an HTML input text with a max length, and compare it to my textfield in JavaFX. I had the same behavior with paste operations (Ctrl + V), cancel operations (Ctrl + Z) in both cases. The goal here is to check if the text is valid BEFORE modifying the textfield.
We could use a similar approach for a numeric text field.
import java.util.Objects;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.control.TextField;
public class LimitedTextField extends TextField {
private final IntegerProperty maxLength;
public LimitedTextField() {
super();
this.maxLength = new SimpleIntegerProperty(-1);
}
public IntegerProperty maxLengthProperty() {
return this.maxLength;
}
public final Integer getMaxLength() {
return this.maxLength.getValue();
}
public final void setMaxLength(Integer maxLength) {
Objects.requireNonNull(maxLength, "Max length cannot be null, -1 for no limit");
this.maxLength.setValue(maxLength);
}
#Override
public void replaceText(int start, int end, String insertedText) {
if (this.getMaxLength() <= 0) {
// Default behavior, in case of no max length
super.replaceText(start, end, insertedText);
}
else {
// Get the text in the textfield, before the user enters something
String currentText = this.getText() == null ? "" : this.getText();
// Compute the text that should normally be in the textfield now
String finalText = currentText.substring(0, start) + insertedText + currentText.substring(end);
// If the max length is not excedeed
int numberOfexceedingCharacters = finalText.length() - this.getMaxLength();
if (numberOfexceedingCharacters <= 0) {
// Normal behavior
super.replaceText(start, end, insertedText);
}
else {
// Otherwise, cut the the text that was going to be inserted
String cutInsertedText = insertedText.substring(
0,
insertedText.length() - numberOfexceedingCharacters
);
// And replace this text
super.replaceText(start, end, cutInsertedText);
}
}
}
}
Tested with JavaFX 8 and Java 8u45
This is a very simple solution that seems to work for me.
textfield.setOnKeyTyped(event ->{
int maxCharacters = 5;
if(tfInput.getText().length() > maxCharacters) event.consume();
});
I use a simple call to ChangeListener, where I test the condition to perform stops.
textFild.addListener((observable, oldValue, newValue) -> {
if (newValue.length() == MAX_SIZE) {
textField.setText(oldValue);
}
});
This is a better way to do the job on a generic text field:
public static void addTextLimiter(final TextField tf, final int maxLength) {
tf.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(final ObservableValue<? extends String> ov, final String oldValue, final String newValue) {
if (tf.getText().length() > maxLength) {
String s = tf.getText().substring(0, maxLength);
tf.setText(s);
}
}
});
}
Works perfectly, except for that Undo bug.
the following 1-liner will exactly do it, wheras 5 is the limit of the TextField tf:
tf.setTextFormatter(new TextFormatter<>(c -> c.getControlNewText().matches(".{0,5}") ? c : null));
This is a solution that works well:
#FXML
void limitTextFields(KeyEvent event) {
int maxLength = 5;
TextField tf = (TextField) event.getSource();
if (tf.getText().length() > maxLength) {
tf.deletePreviousChar();
}
}