Background with 2 colors in JavaFX? - css

In JavaFX 2, using CSS, is it possible to create a background with 2 colors? Think of e.g. a TableCell with a height of 10 px. I want to the first 2 px (vertically) to be red, the remaining 8 px (vertically) shall stay at the default background color. Is that possible using CSS in JavaFX 2? How?
Example:
Original background:
Desired result:
(the upper 2 pixels were replaced by red)
Thanks for any hint on this!

I used a simple layer of background colors to produce a red highlight (similar to Stefan' suggested solution).
/**
* file: table.css
* Place in same directory as TableViewPropertyEditorWithCSS.java.
* Have your build system copy this file to your build output directory.
**/
.highlighted-cell {
-fx-text-fill: -fx-text-inner-color;
-fx-background-color: firebrick, gainsboro;
-fx-background-insets: 0, 2 0 0 0;
}
For a standard region like a stackpane, all you really need to do is apply the above css (less the -fx-text-fill) to get the desired result.
Here is another tricky way to define the color using a gradient:
-fx-background-color:
linear-gradient(
from 0px 0px to 0px 2px,
firebrick, firebrick 99%,
gainsboro
);
In the screenshot below, the value cells are highlighted (by having the highlighted-cell css class applied to them) if have the value false.
Highlight cell style class switch logic:
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
....
getStyleClass().remove("highlighted-cell");
} else {
if (getItem() instanceof Boolean && (Boolean.FALSE.equals((Boolean) getItem()))) {
getStyleClass().add("highlighted-cell");
} else {
getStyleClass().remove("highlighted-cell");
}
...
}
}
It looks good when the highlighted-cell style class applied to a standard table cell (during an updateItem call for a custom cell) but does have a couple of drawbacks. The table coloring scheme is very subtle and complex. It has highlights for odd/even values, highlights for selected rows, highlights for selected hovered rows, highlights for focused rows and cells, etc. Plus it has various combinations of all of the above. Just setting the background-color directly in the highlight-cell class is a kind of brute force way to achieve what you want because it does not take all these other subtleties into account and just overrides them, so a cell which has been highlighted using this style always looks the same no matter what temporary css psuedo-class state has been applied to it.
It's fine really, but a nicer solution would color the highlighted cell differently depending on psuedo-class states. That is quite a tricky thing to do though and you could waste a lot of time playing around with various states and css selector combinations to try to get the nice changing highlight. In all, for this example it didn't seem worth that extra effort for me, though it may be for you.
Test program (apologies for length and complexity of this, it was just easier for me to integrate the style highlighting logic into an existing program):
import java.lang.reflect.*;
import java.util.logging.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Callback;
// click in the value column (a couple of times) to edit the value in the column.
// property editors are defined only for String and Boolean properties.
// change focus to something else to commit the edit.
public class TableViewPropertyEditorWithCSS extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
final Person aPerson = new Person("Fred", false, false, "Much Ado About Nothing");
final Label currentObjectValue = new Label(aPerson.toString());
TableView<NamedProperty> table = new TableView();
table.setEditable(true);
table.setItems(createNamedProperties(aPerson));
TableColumn<NamedProperty, String> nameCol = new TableColumn("Name");
nameCol.setCellValueFactory(new PropertyValueFactory<NamedProperty, String>("name"));
TableColumn<NamedProperty, Object> valueCol = new TableColumn("Value");
valueCol.setCellValueFactory(new PropertyValueFactory<NamedProperty, Object>("value"));
valueCol.setCellFactory(new Callback<TableColumn<NamedProperty, Object>, TableCell<NamedProperty, Object>>() {
#Override
public TableCell<NamedProperty, Object> call(TableColumn<NamedProperty, Object> param) {
return new EditingCell();
}
});
valueCol.setOnEditCommit(
new EventHandler<CellEditEvent<NamedProperty, Object>>() {
#Override
public void handle(CellEditEvent<NamedProperty, Object> t) {
int row = t.getTablePosition().getRow();
NamedProperty property = (NamedProperty) t.getTableView().getItems().get(row);
property.setValue(t.getNewValue());
currentObjectValue.setText(aPerson.toString());
}
});
table.getColumns().setAll(nameCol, valueCol);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
VBox layout = new VBox(10);
layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10;");
layout.getChildren().setAll(
currentObjectValue,
table);
VBox.setVgrow(table, Priority.ALWAYS);
Scene scene = new Scene(layout, 650, 600);
scene.getStylesheets().add(getClass().getResource("table.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
private ObservableList<NamedProperty> createNamedProperties(Object object) {
ObservableList<NamedProperty> properties = FXCollections.observableArrayList();
for (Method method : object.getClass().getMethods()) {
String name = method.getName();
Class type = method.getReturnType();
if (type.getName().endsWith("Property")) {
try {
properties.add(new NamedProperty(name, (Property) method.invoke(object)));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(TableViewPropertyEditorWithCSS.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
return properties;
}
public class NamedProperty {
public NamedProperty(String name, Property value) {
nameProperty.set(name);
valueProperty = value;
}
private StringProperty nameProperty = new SimpleStringProperty();
public StringProperty nameProperty() {
return nameProperty;
}
public StringProperty getName() {
return nameProperty;
}
public void setName(String name) {
nameProperty.set(name);
}
private Property valueProperty;
public Property valueProperty() {
return valueProperty;
}
public Object getValue() {
return valueProperty.getValue();
}
public void setValue(Object value) {
valueProperty.setValue(value);
}
}
public class Person {
private final SimpleStringProperty firstName;
private final SimpleBooleanProperty married;
private final SimpleBooleanProperty hasChildren;
private final SimpleStringProperty favoriteMovie;
private Person(String firstName, Boolean isMarried, Boolean hasChildren, String favoriteMovie) {
this.firstName = new SimpleStringProperty(firstName);
this.married = new SimpleBooleanProperty(isMarried);
this.hasChildren = new SimpleBooleanProperty(hasChildren);
this.favoriteMovie = new SimpleStringProperty(favoriteMovie);
}
public SimpleStringProperty firstNameProperty() {
return firstName;
}
public SimpleBooleanProperty marriedProperty() {
return married;
}
public SimpleBooleanProperty hasChildrenProperty() {
return hasChildren;
}
public SimpleStringProperty favoriteMovieProperty() {
return favoriteMovie;
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public Boolean getMarried() {
return married.get();
}
public void setMarried(Boolean isMarried) {
married.set(isMarried);
}
public Boolean getHasChildren() {
return hasChildren.get();
}
public void setHasChildren(Boolean hasChildren) {
this.hasChildren.set(hasChildren);
}
public String getFavoriteMovie() {
return favoriteMovie.get();
}
public void setFavoriteMovie(String movie) {
favoriteMovie.set(movie);
}
#Override
public String toString() {
return firstName.getValue() + ", isMarried? " + married.getValue() + ", hasChildren? " + hasChildren.getValue() + ", favoriteMovie: " + favoriteMovie.get();
}
}
class EditingCell extends TableCell<NamedProperty, Object> {
private TextField textField;
private CheckBox checkBox;
public EditingCell() {
}
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
if (getItem() instanceof Boolean) {
createCheckBox();
setText(null);
setGraphic(checkBox);
} else {
createTextField();
setText(null);
setGraphic(textField);
textField.selectAll();
}
}
}
#Override
public void cancelEdit() {
super.cancelEdit();
if (getItem() instanceof Boolean) {
setText(getItem().toString());
} else {
setText((String) getItem());
}
setGraphic(null);
}
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
getStyleClass().remove("highlighted-cell");
} else {
if (getItem() instanceof Boolean && (Boolean.FALSE.equals((Boolean) getItem()))) {
getStyleClass().add("highlighted-cell");
} else {
getStyleClass().remove("highlighted-cell");
}
if (isEditing()) {
if (getItem() instanceof Boolean) {
if (checkBox != null) {
checkBox.setSelected(getBoolean());
}
setText(null);
setGraphic(checkBox);
} else {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
}
} else {
setText(getString());
setGraphic(null);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (!newValue) {
commitEdit(textField.getText());
}
}
});
}
private void createCheckBox() {
checkBox = new CheckBox();
checkBox.setSelected(getBoolean());
checkBox.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
checkBox.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (!newValue) {
commitEdit(checkBox.isSelected());
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
private Boolean getBoolean() {
return getItem() == null ? false : (Boolean) getItem();
}
}
}

Look, how to understand the CSSRef:
http://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html
Look at
-fx-background-image :
uri [ , uri ]*
A series of image URIs separated by commas.
Look at
-fx-background-repeat
repeat-style [ , repeat-style ]*
where repeat-style = repeat-x | repeat-y | [repeat | space | round | stretch | no-repeat]{1,2}
A series of values separated by commas. Each repeat-style item in the series applies to the corresponding image in the background-image series.
Look at :
-fx-background-position
bg-position [ , bg-position ]*
where = [
[ [ size | left | center | right ] [ size | top | center | bottom ]? ]
| [ [ center | [ left | right ] size? ] || [ center | [ top | bottom ] size? ]
]
A series of values separated by commas. Each bg-position item in the series applies to the corresponding image in the background-image series.
So, what can you see : you should describe 2 images, (2x2 pixels each - one red and one - grey)
Two bg positions, and two repeat styles for each of them corresponding.
How?
example :
{
-fx-backdround-image : "path_to_red", "path_to_grey";
-fx-background-repeat : repeat-x, stretch;
-fx-background-position : 0px 0px, 0px 2px;
}
I don't give a garantee on workness of the code, but the idea seems correct.
Maybe possible with only colors instead of images when using insets. Example from original JavaFX CSS:
.table-row-cell:odd {
-fx-background-color: -fx-table-cell-border-color, derive(-fx-control-inner-background,-5%);
-fx-background-insets: 0, 0 0 1 0;
}
[6 characters...]

Related

How to set text and color of multiple ListView items when clicked

I know there are many related questions about this but maybe I'm missing something because I can't get the behavior I'm expecting, to work.
#FXML
private ListView<String> guiList;
void performAction(Actions action) {
try {
Task<String> task = new Task<>() {
#Override
public String call() {
String mySelection = Context.getInstance().getSelected();
ArrayList<String> selectedList = Context.getInstance().getItemsClicked();
if (selectedList == null) {
selectedList = new ArrayList<>();
}
selectedList.add(mySelection);
Context.getInstance().setItemsClicked(selectedList);
guiList.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> param) {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item != null && item.matches(mySelection)) {
setText(mySelection + " [" + action + "]");
setFont(Font.font(Font.getDefault().getFamily(), FontWeight.BOLD, Font.getDefault().getSize()));
setStyle("-fx-text-fill: green;");
} else {
setText(item);
}
}
};
return cell;
}
});
return "";
}
};
} catch (Exception e) {
}
}
When I click in an item of guiList, the text is changed, gets bold and shows in green color but I don't understand why I need the else statement. If I don't use it, all the other items of the list disappear.
I ask this because I want to change ALL of the items I click and in the current behavior, the changes are only made in the last one clicked.
Here is on approach. Use an object that has a Boolean variable to keeps up with if the item has been selected.
KeyCode 1
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) - > {
if (t1 != null) {
t1.setSelected(true);
}
}));
Key Code 2
lvMain.setCellFactory(lv - > new ListCell < MyItem > () {
#Override
public void updateItem(MyItem item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item.getText());
if (item.isSelected()) {
setTextFill(Color.RED);
}
}
}
});
Main
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class App extends Application {
#Override
public void start(Stage primaryStage) {
ListView<MyItem> lvMain = new ListView();//Create ListView
lvMain.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);//Change ListView selection mode to multiple
ObservableList<MyItem> items = FXCollections.observableArrayList(new MyItem("Julia"), new MyItem("Ian"), new MyItem("Sue"), new MyItem("Matthew"), new MyItem("Hannah"));//ObseravableList that will be used to set the ListView
lvMain.setItems(items);//Set the ListView's items
lvMain.setCellFactory(lv -> new ListCell<MyItem>()
{
#Override
public void updateItem(MyItem item, boolean empty)
{
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
}
else {
setText(item.getText());
if(item.isSelected())
{
setTextFill(Color.RED);
}
}
}
});
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) -> {
if(t1 != null)
{
t1.setSelected(true);
}
}));
VBox vbox = new VBox();
vbox.getChildren().addAll(lvMain);
StackPane root = new StackPane();
root.getChildren().add(vbox);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
MyItem
/**
*
* #author Sed
*/
public class MyItem {
private String text;
private boolean selected;
public MyItem(String text) {
this.text = text;
this.selected = false;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean isSelected) {
this.selected = isSelected;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
Output
I think a better solution would be to use the ListView's built in multiple selection or have your cells have a ToggleButton. When the ToggleButton is on, change the color of the text. When it is off, change the color back to it's original state.

Strikethough on table row in javafx

I want to update a row to be strikethrough in a tableview when the user deletes it. I'm somewhat new to javafx and have been searching with no luck.
donationsTable.setRowFactory(tv -> {
TableRow<Donation> row = new TableRow<Donation>() {
// to force updateItem called
#Override
protected boolean isItemChanged(Donation d,
Donation d2) {
return true;
}
#Override
public void updateItem(Donation d, boolean empty) {
super.updateItem(d, empty) ;
if (d == null) {
setStyle("");
} else if (d.getAction().equals(Donation.DELETE_DONATION)) {
setStyle("delete-row");
} else if (d.getAction().equals(Donation.NEW_DONATION)) {
setStyle("-fx-font-weight: bold;");
} else {
setStyle("");
}
}
};
row.setOnMouseClicked(event -> {
deleteDonation.setDisable(false);
});
return row;
});
The bold works for new donations, but I can't get the strikethrough to work. I did see that it needs to be set on the text, not the row so my css is:
.delete-row .text {
-fx-strikethrough: true;
}
However, I'm getting a warning: WARNING CSS Error parsing '*{delete-row}: Expected COLON at [1,12]
I only have a very basic understanding of css. This is what I have seen in other answers, but I don't understand why it is not working for me.
Any help is much appreciated.
Based on James_D's suggestion, I changed updateItem:
public void updateItem(Donation d, boolean empty) {
super.updateItem(d, empty) ;
PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
pseudoClassStateChanged(delete, d != null && d.getAction().equals(Donation.DELETE_DONATION));
PseudoClass add = PseudoClass.getPseudoClass("add-row");
pseudoClassStateChanged(add, d != null && d.getAction().equals(Donation.NEW_DONATION));
}
css has
.table-row-cell:delete-row .text {
-fx-strikethrough: true;
}
.table-row-cell:add-row {
-fx-font-weight: bold;
}
strikethrough still not working and bold stopped working.
The setStyle method will set an inline style on a Node; this style is in the form of a CSS rule. This is what you do with the bold case:
if (d.getAction().equals(Donation.NEW_DONATION)) {
setStyle("-fx-font-weight: bold;");
}
To add a CSS class to the list of classes for a node, get the list of the node's CSS classes with getStyleClass(), and manipulate it.
You have to be a little careful here, as the list can contain multiple copies of the same value, and additionally you have no control over how many times updateItem() is called and with which Donations as a parameter. The best option is to remove all instances of the class delete-row and add one back in under the correct conditions:
#Override
public void updateItem(Donation d, boolean empty) {
super.updateItem(d, empty) ;
getStyleClass().removeAll(Collections.singleton("delete-row"));
if (d == null) {
setStyle("");
} else if (d.getAction().equals(Donation.DELETE_DONATION)) {
setStyle("");
getStyleClass().add("delete-row");
} else if (d.getAction().equals(Donation.NEW_DONATION)) {
setStyle("-fx-font-weight: bold;");
} else {
setStyle("");
}
}
Another option is to use a CSS pseudoclass instead:
#Override
public void updateItem(Donation d, boolean empty) {
super.updateItem(d, empty) ;
PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
pseudoClassStateChanged(delete, d != null && d.getAction().equals(Donation.DELETE_DONATION));
if (d != null && d.getAction().equals(Donation.NEW_DONATION)) {
setStyle("-fx-font-weight: bold;");
} else {
setStyle("");
}
}
with
.table-row-cell:delete-row .text {
-fx-strikethrough: true;
}
I would probably refactor the NEW_DONATION style as a pseudoclass as well in this scenario, for consistency.
Here's a complete example using pseudoclasses. Note that I changed the CSS for bold (as I understand it, using font-weight depends on the system having a bold font for the currently-selected font; using something generic (sans-serif) with a -fx-font rule is more robust.)
Donation.java
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Donation {
public enum Action { NEW_DONATION, DELETE_DONATION, NO_ACTION }
private final StringProperty name = new SimpleStringProperty() ;
private final ObjectProperty<Action> action = new SimpleObjectProperty<>() ;
public Donation(String name, Action action) {
setName(name);
setAction(action);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final ObjectProperty<Action> actionProperty() {
return this.action;
}
public final Action getAction() {
return this.actionProperty().get();
}
public final void setAction(final Action action) {
this.actionProperty().set(action);
}
}
App.java
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
TableView<Donation> table = new TableView<>();
table.setRowFactory(tv -> {
TableRow<Donation> row = new TableRow<>() {
#Override
protected void updateItem(Donation donation, boolean empty) {
super.updateItem(donation, empty);
PseudoClass add = PseudoClass.getPseudoClass("add-row");
pseudoClassStateChanged(add,
donation != null && donation.getAction() == Donation.Action.NEW_DONATION);
PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
pseudoClassStateChanged(delete,
donation != null && donation.getAction() == Donation.Action.DELETE_DONATION);
}
};
return row ;
});
Random rng = new Random();
for (int i = 1 ; i <= 40 ; i++) {
table.getItems().add(new Donation("Donation "+i, Donation.Action.values()[rng.nextInt(3)]));
}
table.getColumns().add(column("Donation", Donation::nameProperty));
table.getColumns().add(column("Action", Donation::actionProperty));
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
private static <S,T> TableColumn<S,T> column(String name, Function<S, Property<T>> prop) {
TableColumn<S,T> col = new TableColumn<>(name);
col.setCellValueFactory(data -> prop.apply(data.getValue()));
return col ;
}
public static void main(String[] args) {
launch();
}
}
style.css:
.table-row-cell:delete-row .text {
-fx-strikethrough: true;
}
.table-row-cell:add-row {
/* -fx-font-weight: bold; */
-fx-font: bold 1em sans-serif ;
}
Update:
If the property determining the style of the table row is not being observed by one of the columns (e.g. in the above example, the "action" column is not present), you need to arrange for the row to observe that property itself. This is a little tricky, as the row is reused for different table items, so you need to add and remove the listener from the correct property when that happens. This looks like:
table.setRowFactory(tv -> {
TableRow<Donation> row = new TableRow<>() {
// Listener that updates style when the actionProperty() changes
private final ChangeListener<Donation.Action> listener =
(obs, oldAction, newAction) -> updateStyle();
{
// make sure listener above is registered
// with the correct actionProperty()
itemProperty().addListener((obs, oldDonation, newDonation) -> {
if (oldDonation != null) {
oldDonation.actionProperty().removeListener(listener);
}
if (newDonation != null) {
newDonation.actionProperty().addListener(listener);
}
});
}
#Override
protected void updateItem(Donation donation, boolean empty) {
super.updateItem(donation, empty);
updateStyle();
}
private void updateStyle() {
Donation donation = getItem();
PseudoClass add = PseudoClass.getPseudoClass("add-row");
pseudoClassStateChanged(add, donation != null && donation.getAction() == Donation.Action.NEW_DONATION);
PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
pseudoClassStateChanged(delete, donation != null && donation.getAction() == Donation.Action.DELETE_DONATION);
}
};
return row ;
});

JavaFx - String and FlowPane (row?) within tableView?

I'm currently trying to implement the following:
A TableView with an ObservableList as dataset, with two columns, each of which contains Strings (names of the players). This part is easy enough.
Once a Player(name) is clicked, a custom FlowPane should be injected below the selected player. If another player is clicked, the flowpane should disappear and be injected below the currently clicked player.
The below code implements the TableView (minus the mouse listener part). Please help me let the FlowPane span the entire row. I'm guessing I need a RowFactory but have no clue how to make it work for my purposes :)
Also, apparently both my columns now show the same data. Confusing :) Is there a way to tell one column to use half the data set and the other column the other half? I obviously don't want my data shown twice.
public class main extends Application
{
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage) throws Exception
{
try
{
FlowPane f = new FlowPane();
Scene scene = new Scene(f, 300, 200);
Player p1 = new Player("player 1 ");
Player p2 = new Player("player 2 ");
Player p3 = new Player("player 3 ");
ArrayList<Object> players = new ArrayList<>();
players.add(p1);
players.add(p2);
players.add(p3);
ObservableList<Object> observableList = FXCollections.observableArrayList(players);
TableView<Object> table = createTableView(observableList, 300, 200);
f.getChildren().add(table);
injectFlowPane(table);
stage.setScene(scene);
stage.show();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public TableView<Object> createTableView(ObservableList<Object> items, double width, double height)
{
TableView<Object> table = new TableView<>();
table.setItems(items);
table.getColumns().add(createTableColumn(width / 2));
table.getColumns().add(createTableColumn(width / 2));
table.setMinSize(width, height);
table.setPrefSize(width, height);
table.setMaxSize(width, height);
return table;
}
private TableColumn<Object, Object> createTableColumn(double width)
{
TableColumn<Object, Object> tableColumn = new TableColumn<>();
tableColumn.setCellFactory(
new Callback<TableColumn<Object, Object>, TableCell<Object, Object>>() {
#Override
public TableCell<Object, Object> call(TableColumn<Object, Object> arg0)
{
return new PlayerCell();
}
});
tableColumn.setCellValueFactory(cellDataFeatures -> {
Object item = cellDataFeatures.getValue();
return new SimpleObjectProperty<>(item);
});
tableColumn.setMinWidth(width);
return tableColumn;
}
private void injectFlowPane(TableView<Object> table)
{
FlowPane f = new FlowPane();
f.setMinSize(50, 50);
f.setBackground(new Background(new BackgroundFill(Color.DARKGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
table.getItems().add(1, f);
}
}
public class PlayerCell extends TableCell<Object, Object>
{
#Override
protected void updateItem(Object item, boolean empty)
{
super.updateItem(item, false);
// if (empty)
if (item != null)
{
if (item instanceof Player)
{
setText(((Player) item).getName());
setGraphic(null);
}
else if (item instanceof FlowPane)
{
setGraphic((FlowPane) item);
}
else
{
setText("N/A");
setGraphic(null);
}
}
else
{
setText(null);
setGraphic(null);
}
}
}
public class Player
{
private String name;
public Player(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
EDIT:
I have now implemented James_D's ExpandingTableRow, which works neatly as far as showing the FlowPane below the selected TableRow is concerned. I have also managed to change my datastructures so that each column now shows different players instead of the same ones in each column.
However, the FlowPane that is created should actually depend on the actual player(cell) that is clicked within the row. In James' example: a different FlowPane would be created if the FirstName or LastName was selected (even for the same row). The FlowPane should be shown the same way - below the selected row - but it's a different, new FlowPane depending on if FirstName was clicked, or if LastName was clicked. How can I manage to do this?
I've looked at using:
table.getSelectionModel().setCellSelectionEnabled(true);
But this actually seems to disable James_d's solution.
This solution works only in Java 9 and later.
The display of a row is managed by a TableRow, and the actual layout of that row is performed by its skin (a TableRowSkin). So to manage this, you need a subclass of TableRow that installs a custom skin.
The row implementation is pretty straightforward: in this example I added a property for the "additional content" to be displayed when the row is selected. It also overrides the createDefaultSkin() method to specify a custom skin implementation.
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TableRow;
public class ExpandingTableRow<T> extends TableRow<T> {
private final ObjectProperty<Node> selectedRowContent = new SimpleObjectProperty<>();
public final ObjectProperty<Node> selectedRowContentProperty() {
return this.selectedRowContent;
}
public final Node getSelectedRowContent() {
return this.selectedRowContentProperty().get();
}
public final void setSelectedRowContent(final Node selectedRowContent) {
this.selectedRowContentProperty().set(selectedRowContent);
}
public ExpandingTableRow(Node selectedRowContent) {
super();
setSelectedRowContent(selectedRowContent);
}
public ExpandingTableRow() {
this(null);
}
#Override
protected Skin<?> createDefaultSkin() {
return new ExpandingTableRowSkin<T>(this);
}
}
The skin implementation has to do the layout work. It needs to override the methods that compute the height, accounting for the height of the extra content if needed, and it needs to override the layoutChildren() method, to position the additional content, if needed. Finally, it must manage the additional content, adding or removing the additional content if the selected state of the row changes (or if the additional content itself is changed).
import javafx.scene.control.skin.TableRowSkin;
public class ExpandingTableRowSkin<T> extends TableRowSkin<T> {
private ExpandingTableRow<T> row;
public ExpandingTableRowSkin(ExpandingTableRow<T> row) {
super(row);
this.row = row;
row.selectedRowContentProperty().addListener((obs, oldContent, newContent) -> {
if (oldContent != null) {
getChildren().remove(oldContent);
}
if (newContent != null && row.isSelected()) {
getChildren().add(newContent);
}
if (row.getTableView() != null) {
row.getTableView().requestLayout();
}
});
row.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected && row.getSelectedRowContent() != null
&& !getChildren().contains(row.getSelectedRowContent())) {
getChildren().add(row.getSelectedRowContent());
} else {
getChildren().remove(row.getSelectedRowContent());
}
});
}
#Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().maxHeight(width);
}
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().minHeight(width);
}
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().prefHeight(width);
}
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected void layoutChildren(double x, double y, double w, double h) {
if (row.isSelected()) {
double rowHeight = super.computePrefHeight(w, snappedTopInset(), snappedRightInset(), snappedBottomInset(),
snappedLeftInset());
super.layoutChildren(x, y, w, rowHeight);
row.getSelectedRowContent().resizeRelocate(x, y + rowHeight, w, h - rowHeight);
} else {
super.layoutChildren(x, y, w, h);
}
}
}
Finally, a test (using the usual example from Oracle, or a version of it):
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class ExpandingTableRowTest extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getColumns().add(column("First Name", Person::firstNameProperty));
table.getColumns().add(column("Last Name", Person::lastNameProperty));
table.setRowFactory(tv -> {
Label label = new Label();
FlowPane flowPane = new FlowPane(label);
TableRow<Person> row = new ExpandingTableRow<>(flowPane) {
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
label.setText(null);
} else {
label.setText(String.format("Some additional information about %s %s here",
person.getFirstName(), person.getLastName()));
}
}
};
return row;
});
table.getItems().addAll(
new Person("Jacob", "Smith"),
new Person("Isabella", "Johnson"),
new Person("Ethan", "Williams"),
new Person("Emma", "Jones"),
new Person("Michael", "Brown")
);
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S, T> TableColumn<S, T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S, T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName) {
setFirstName(firstName);
setLastName(lastName);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final String lastName) {
this.lastNameProperty().set(lastName);
}
}
public static void main(String[] args) {
launch(args);
}
}
As you can see, a little refinement of the style and sizing may be needed to get this production-ready, but this shows the approach that will work.

Automatically updating/styling TreeView's TreeCells when BooleanProperty is true in seperate TableView

What's the easiest way to have a TreeView's cells auto-refresh with a new style when a condition is met in a separate TableView?
I'm currently setting the TreeCells' styles in the updateItem() method in the TreeView cell factory, but this only fires off if the user adds or removes something in the TreeView. I want to be able to change the style of a given TreeCell if I check off all 3 checkboxes in a separate dialog box.
I'm currently able to monitor the number of checked checkboxes with a BooleanProperty and an IntegerProperty, but I have no idea how I'm supposed to "auto-update" or call a TreeView refresh when a TreeItem's Object's BooleanProperty changes.
Any help is greatly appreciated.
You can set the style in the TreeCell whenever a boolean property on the value underlying the TreeCell is updated (via a binding).
return new TreeCell<Message>() {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
styleProperty.set(null);
} else {
setText(item.getText());
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
Full Sample
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TreeViewSample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
ObservableList<Message> messages = FXCollections.observableArrayList();
TreeItem<Message> rootItem = new TreeItem<> (new Message("Inbox"));
rootItem.setExpanded(true);
for (int i = 1; i < 6; i++) {
Message message = new Message("Message" + i);
messages.add(message);
TreeItem<Message> item = new TreeItem<> (message);
rootItem.getChildren().add(item);
}
TreeView<Message> tree = new TreeView<> (rootItem);
tree.setCellFactory(new Callback<TreeView<Message>, TreeCell<Message>>() {
#Override
public TreeCell<Message> call(TreeView<Message> param) {
return new TreeCell<Message>() {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
styleProperty.set(null);
} else {
setText(item.getText());
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
}
});
TableView<Message> tableView = new TableView<>();
tableView.setEditable(true);
TableColumn<Message, String> textCol = new TableColumn<>("Text");
textCol.setCellValueFactory(new PropertyValueFactory<>("text"));
tableView.getColumns().add(textCol);
TableColumn<Message, Boolean> readCol = new TableColumn<>("Read");
readCol.setCellValueFactory(new PropertyValueFactory<>("read"));
readCol.setCellFactory(CheckBoxTableCell.forTableColumn(readCol));
readCol.setEditable(true);
tableView.getColumns().add(readCol);
tableView.setItems(messages);
VBox root = new VBox(10, tree, tableView);
root.setPadding(new Insets(10));
stage.setScene(new Scene(root, 300, 250));
stage.show();
}
public class Message {
private StringProperty text = new SimpleStringProperty();
private BooleanProperty read = new SimpleBooleanProperty(false);
public Message(String msgText) {
text.set(msgText);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public boolean isRead() {
return read.get();
}
public BooleanProperty readProperty() {
return read;
}
public void setRead(boolean read) {
this.read.set(read);
}
}
}
I'm trying to the bind the graphicProperty to the same BooleanProperty and change the image based on the value.
Example using a binding of an Image within an ImageView associated with the cell.
Image unreadImage = new Image("http://icons.iconarchive.com/icons/oxygen-icons.org/oxygen/16/Status-mail-unread-new-icon.png");
Image readImage = new Image("http://icons.iconarchive.com/icons/icons8/ios7/16/Messaging-Read-Message-icon.png");
. . .
return new TreeCell<Message>() {
ImageView imageView = new ImageView();
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
imageView.imageProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
setGraphic(null);
styleProperty().set(null);
} else {
setText(item.getText());
setGraphic(imageView);
imageView.imageProperty().bind(
Bindings.when(
item.readProperty()
).then(readImage)
.otherwise(unreadImage)
);
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
An alternate (and possibly preferable) way to handle this from above is to instead get the style class or psuedoclass of the cell and update that based upon the boolean property. Then define the style in a separate CSS stylesheet. The output of the sample below is the same as the graphic based sample above.
mail.css
.readable:read {
-fx-background-color: red;
-fx-graphic: url(
"http://icons.iconarchive.com/icons/icons8/ios7/16/Messaging-Read-Message-icon.png"
);
}
.readable:unread {
-fx-graphic: url(
"http://icons.iconarchive.com/icons/oxygen-icons.org/oxygen/16/Status-mail-unread-new-icon.png"
);
}
Pseudo-class based code snippet:
PseudoClass READ_PSEUDO_CLASS = PseudoClass.getPseudoClass("read");
PseudoClass UNREAD_PSEUDO_CLASS = PseudoClass.getPseudoClass("unread");
tree.setCellFactory(new Callback<TreeView<Message>, TreeCell<Message>>() {
#Override
public TreeCell<Message> call(TreeView<Message> param) {
return new TreeCell<Message>() {
private ChangeListener<Boolean> readChangeListener = (observable, oldValue, newValue) -> {
pseudoClassStateChanged(READ_PSEUDO_CLASS, newValue);
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, !newValue);
};
Message priorItem = null;
{
getStyleClass().add("readable");
}
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
if (priorItem != null) {
priorItem.readProperty().removeListener(readChangeListener);
}
priorItem = item;
if (empty || item == null || item.getText() == null) {
setText(null);
pseudoClassStateChanged(READ_PSEUDO_CLASS, false);
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, false);
} else {
item.readProperty().addListener(readChangeListener);
setText(item.getText());
pseudoClassStateChanged(READ_PSEUDO_CLASS, item.isRead());
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, !item.isRead());
}
}
};
}
});

JavaFx Create Table Cell Accepts numbers only?

I have TableView with column inside it that must only accept numbers.
and I added onMouseClickListener to enter edit mode on the mouse click instead of double click on the cell
I want a way to not allowing the user to enter any character except numbers. My code is:
Callback<TableColumn<DailyDetails, String>, TableCell<DailyDetails, String>> defaultCellFactory
= TextFieldTableCell.<DailyDetails>forTableColumn();
dailyCredit.setCellFactory(column -> {
TableCell<DailyDetails, String> cell = defaultCellFactory.call(column);
cell.setOnMouseClicked(e -> {
if (!cell.isEditing() && !cell.isEmpty()) {
cell.getTableView().edit(cell.getIndex(), column);
}
});
return cell;
});
I implemented Table cell from the scratch:
class NumberCell extends TableCell<DailyDetails, String> {
private TextField textField;
public NumberCell() {
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(String.valueOf(getItem()));
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
setText(getString());
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
private void createTextField() {
textField = new TextField(getString());
//textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.lengthProperty().addListener(new ChangeListener<Number>(){
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.intValue() > oldValue.intValue()) {
char ch = textField.getText().charAt(oldValue.intValue());
// Check if the new character is the number or other's
if (!(ch >= '0' && ch <= '9' )) {
// if it's not number then just setText to previous one
textField.setText(textField.getText().substring(0,textField.getText().length()-1));
}
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
Callback<TableColumn<DailyDetails, String>,
TableCell<DailyDetails, String>> cellFactory
= (TableColumn<DailyDetails, String> p) -> new NumberCell();
dailyDebit.setCellFactory(cellFactory);
the problem is i lost the on mouse listener cell.setOnMouseClicked!!!
how do i get the cell again to assign the listener ???
Just for driving the new api into everybody's brain: a full example with a slightly different TextFormatter (than in the other answer) that is Locale-aware and (dirtily!) hooked into core TextFieldTableCell, can be used in any custom editing TableCell as well:
/**
* Example of how-to use a TextFormatter in a editing TableCell.
*/
public class CellFormatting extends Application {
private Parent getContent() {
ObservableList<IntData> data = FXCollections.observableArrayList(
new IntData(1), new IntData(2), new IntData(3)
);
TableView<IntData> table = new TableView<>(data);
table.setEditable(true);
TableColumn<IntData, Integer> column = new TableColumn<>("Data");
column.setCellValueFactory(new PropertyValueFactory("data"));
// core default: will throw exception on illegal values
// column.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
NumberFormat format = NumberFormat.getIntegerInstance();
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.isContentChange()) {
ParsePosition parsePosition = new ParsePosition(0);
// NumberFormat evaluates the beginning of the text
format.parse(c.getControlNewText(), parsePosition);
if (parsePosition.getIndex() == 0 ||
parsePosition.getIndex() < c.getControlNewText().length()) {
// reject parsing the complete text failed
return null;
}
}
return c;
};
column.setCellFactory(c -> new ValidatingTextFieldTableCell<>(
// note: each cell needs its own formatter
// see comment by #SurprisedCoconut
new TextFormatter<Integer>(
// note: should use local-aware converter instead of core!
new IntegerStringConverter(), 0,
filter)));
table.getColumns().add(column);
VBox box = new VBox(table);
return box;
}
/**
* TextFieldTableCell that validates input with a TextFormatter.
* <p>
* Extends TextFieldTableCell, accesses super's private field reflectively.
*
*/
public static class ValidatingTextFieldTableCell<S, T> extends TextFieldTableCell<S, T> {
private TextFormatter<T> formatter;
private TextField textAlias;
public ValidatingTextFieldTableCell() {
this((StringConverter<T>)null);
}
public ValidatingTextFieldTableCell(StringConverter<T> converter) {
super(converter);
}
public ValidatingTextFieldTableCell(TextFormatter<T> formatter) {
super(formatter.getValueConverter());
this.formatter = formatter;
}
/**
* Overridden to install the formatter. <p>
*
* Beware: implementation detail! super creates and configures
* the textField lazy on first access, so have to install after
* calling super.
*/
#Override
public void startEdit() {
super.startEdit();
installFormatter();
}
private void installFormatter() {
if (formatter != null && isEditing() && textAlias == null) {
textAlias = invokeTextField();
textAlias.setTextFormatter(formatter);
}
}
private TextField invokeTextField() {
Class<?> clazz = TextFieldTableCell.class;
try {
Field field = clazz.getDeclaredField("textField");
field.setAccessible(true);
return (TextField) field.get(this);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
public static class IntData {
IntegerProperty data = new SimpleIntegerProperty(this, "data");
public IntData(int value) {
setData(value);
}
public void setData(int value) {
data.set(value);
}
public int getData() {
return data.get();
}
public IntegerProperty dataProperty() {
return data;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
BTW, the formatter is re-used from another question where the task at hand was to restrict input into a Spinner.
Use a TextFormatter on the TextField like this:
TextFormatter<String> formatter = new TextFormatter<String>( change -> {
change.setText(change.getText().replaceAll("[^0-9.,]", ""));
return change;
});
textField.setTextFormatter(formatter);
Works with Java8u40 upwards. Use e. g. the TableView example from the Oracle site as base.

Resources