I have set up my application to change its function based on an enum. The value of a variable linked to this enum will determine how the program interprets certain actions like mouse clicks and so on. I would like a Label (perhaps in the status area in the bottom left) to reflect what the current "mode" the application is in, and display a readable message for the user to see.
Here's my enum:
enum Mode {
defaultMode, // Example states that will determine
alternativeMode; // how the program interprets mouse clicks
// My attempt at making a property that a label could bind to
private SimpleStringProperty property = new SimpleStringProperty(this, "myEnumProp", "Initial Text");
public SimpleStringProperty getProperty() {return property;}
// Override of the toString() method to display prettier text
#Override
public String toString()
{
switch(this) {
case defaultMode:
return "Default mode";
default:
return "Alternative mode";
}
}
}
From what I've gathered, what I'm looking for is a way to bind an enum's toString() property (which I overrode into more digestable form) to this label. The binding would be so that whenever I set something like
applicationState = Mode.alternativeMode;
the label will display the toString() results automatically, without me needing to place a leftStatus.setText(applicationState.toString()) every time I do that.
Here's what I've tried: (in my main controller class):
leftStatus.textProperty().bind(applicationState.getProperty());
That sets the label to the initial text, but won't update when I update the applicationState enum.
What am I doing wrong?
Instead of adding a property to the enum class, why not use a ObjectProperty for the application state? Have a look at this MCVE:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class Example extends Application {
private ObjectProperty<Mode> appState = new SimpleObjectProperty<>(Mode.DEFAULT);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
Button btn = new Button("Toggle mode");
btn.setOnMouseClicked((event) -> appState.setValue(appState.get() == Mode.DEFAULT ? Mode.ALTERNATIVE : Mode.DEFAULT));
Label lbl = new Label();
lbl.textProperty().bind(appState.asString());
FlowPane pane = new FlowPane();
pane.getChildren().addAll(btn, lbl);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
public enum Mode {
DEFAULT("Default mode"),
ALTERNATIVE("Alternative mode");
private String description;
private Mode(String description) {
this.description = description;
}
#Override
public String toString() {
return description;
}
}
}
Use asString to get a StringBinding from a Property<Mode> containing the value of the property converted to String using the object's toString method.
Example:
#Override
public void start(Stage primaryStage) {
ComboBox<Mode> combo = new ComboBox<>();
combo.getItems().setAll(Mode.values());
Label label = new Label();
// use "state" property from combo box
// (you could replace combo.valueProperty() with your own property)
label.textProperty().bind(combo.valueProperty().asString());
Scene scene = new Scene(new VBox(combo, label), 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
Otherwise, if you want the property value contained in the enum, you could use Bindings.selectString, provided you rename the getProperty() method to propertyProperty() to adhere the naming conventions:
enum Mode {
...
public StringProperty propertyProperty() {return property;}
...
}
private final Random random = new Random();
#Override
public void start(Stage primaryStage) {
ComboBox<Mode> combo = new ComboBox<>();
combo.getItems().setAll(Mode.values());
Label label = new Label();
// use "state" property from combo box
// (you could replace combo.valueProperty() with your own property)
label.textProperty().bind(Bindings.selectString(combo.valueProperty(), "property"));
Scene scene = new Scene(new VBox(combo, label), 200, 200);
scene.setOnMouseClicked(evt -> {
// change property values at random
Mode.defaultMode.propertyProperty().set(random.nextBoolean() ? "a" : "b");
Mode.alternativeMode.propertyProperty().set(random.nextBoolean() ? "c" : "d");
});
primaryStage.setScene(scene);
primaryStage.show();
}
Related
I am having trouble understanding how to apply the mvc pattern with JavaFX.
Here are my questions with respect to the code below, since I need to follow the pattern given in the code:
a) How can I attach an event handler of the button which is present in my ViewA to the code in my ControllerA (specifically, attachEventHandlers() method). For example, I want my button to populate the comboBox in ViewA with the results of getModelItems() method from controller.
Note that the method getModelItems() is private.
b) I would have multiple buttons and event handlers in my view. How will I bind each one of them uniquely to the controller?
c) I want to invoke setName(String name) on my model in the controller, and the parameter I want to pass is the selected value on the comboBox in viewA. How can I achieve this?
Thank you so much for any help!
Below is the code referred in the description.
Controller:
import model.ModelA;
import view.ViewA;
public class ControllerA {
private ViewA view;
private ModelA model;
public ControllerA(ViewA view, ModelA model) {
//initialise model and view fields
this.model = model;
this.view = view;
//populate combobox in ViewB, e.g. if viewB represented your ViewB you could invoke the line below
//viewB.populateComboBoxWithCourses(setupAndRetrieveCourses());
this.attachEventHandlers();
}
private void attachEventHandlers() {
}
private String[] getModelItems() {
String[] it = new String[2];
it[0] = "0";
it[1] = "1";
return it;
}
}
Model:
public class ModelA {
private String name;
public Name() {
name = "";
}
public void setName(String name) {
this.name = name;
}
#Override
public String toString() {
return "Name = " + name;
}
}
View:
import javafx.scene.layout.BorderPane;
//You may change this class to extend another type if you wish
public class ViewA extends BorderPane {
public BorderPane bp;
public ViewA(){
this.bp = new BorderPane();
ComboBox comboBox = new ComboBox();
Button button1 = new Button("Populate");
bp.setTop(button1);
bp.setBottom(comboBox);
}
}
Loader:
public class ApplicationLoader extends Application {
private ViewA view;
#Override
public void init() {
//create model and view and pass their references to the controller
ModelA model = new ModelA();
view = new ViewA();
new ControllerA(view, model);
}
#Override
public void start(Stage stage) throws Exception {
//whilst you can set a min width and height (example shown below) for the stage window,
//you should not set a max width or height and the application should
//be able to be maximised to fill the screen and ideally behave sensibly when resized
stage.setMinWidth(530);
stage.setMinHeight(500);
stage.setTitle("Final Year Module Chooser Tool");
stage.setScene(new Scene(view));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You add delegates to your ViewA to allow for access:
public class ViewA extends BorderPane {
ComboBox comboBox;
Button button1;
public ViewA(){
comboBox = new ComboBox();
button1 = new Button("Populate");
setTop(button1);
setBottom(comboBox);
}
// Add delegates for all functionality you want to make available through ViewA
public ObservableList<String> getItems() { return comboBox.getItems(); }
public void setOnButton1Action(...) { ... }
public void setOnButton2Action(...) { ... }
...
}
You can go as broad or as narrow as you like, based on how much you want to manage through ViewA.
I have a TableView with a CellFactory that places a ComboBox into one of the columns. The TableView has SelectionMode.MULTIPLE enabled but it is acting odd with the ComboBox cell.
When the users clicks on the ComboBox to select a value, that row is added to the list of selected rows. Instead, clicking on the ComboBox should either select that row and deselect all others (unless CTRL is being held), or it should not select the row at all, but only allow for interaction with the ComboBox.
I am not sure how to achieve this.
Here is a complete example to demonstrate the issue:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
enum Manufacturer {
HP, DELL, LENOVO, ASUS, ACER;
}
public class TableViewSelectionIssue extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Simple TableView
TableView<ComputerPart> tableView = new TableView<>();
TableColumn<ComputerPart, Manufacturer> colManufacturer = new TableColumn<>("Manufacturer");
TableColumn<ComputerPart, String> colItem = new TableColumn<>("Item");
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
colManufacturer.setCellValueFactory(t -> t.getValue().manufacturerProperty());
colItem.setCellValueFactory(t -> t.getValue().itemNameProperty());
tableView.getColumns().addAll(colManufacturer, colItem);
// CellFactory to display ComboBox in colManufacturer
colManufacturer.setCellFactory(param -> new ManufacturerTableCell(colManufacturer, FXCollections.observableArrayList(Manufacturer.values())));
// Add sample items
tableView.getItems().addAll(
new ComputerPart("Keyboard"),
new ComputerPart("Mouse"),
new ComputerPart("Monitor"),
new ComputerPart("Motherboard"),
new ComputerPart("Hard Drive")
);
root.getChildren().add(tableView);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
class ComputerPart {
private final ObjectProperty<Manufacturer> manufacturer = new SimpleObjectProperty<>();
private final StringProperty itemName = new SimpleStringProperty();
public ComputerPart(String itemName) {
this.itemName.set(itemName);
}
public Manufacturer getManufacturer() {
return manufacturer.get();
}
public void setManufacturer(Manufacturer manufacturer) {
this.manufacturer.set(manufacturer);
}
public ObjectProperty<Manufacturer> manufacturerProperty() {
return manufacturer;
}
public String getItemName() {
return itemName.get();
}
public void setItemName(String itemName) {
this.itemName.set(itemName);
}
public StringProperty itemNameProperty() {
return itemName;
}
}
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
ManufacturerTableCell(TableColumn<ComputerPart, Manufacturer> column, ObservableList<Manufacturer> items) {
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
this.cboStatus.setConverter(new StringConverter<Manufacturer>() {
#Override
public String toString(Manufacturer object) {
return object.name();
}
#Override
public Manufacturer fromString(String string) {
return null;
}
});
this.cboStatus.disableProperty().bind(column.editableProperty().not());
this.cboStatus.setOnShowing(event -> {
final TableView<ComputerPart> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
this.cboStatus.valueProperty().addListener((observable, oldValue, newValue) -> {
if (isEditing()) {
commitEdit(newValue);
column.getTableView().refresh();
}
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (empty) {
setGraphic(null);
} else {
this.cboStatus.setValue(item);
this.setGraphic(this.cboStatus);
}
}
}
The example begins with a predictable UI:
However, when interacting with the ComboBox in the Manufacturer column, the corresponding row is selected. This is expected for the first row, but it does not get deselected when interacting with another ComboBox.
How can I prevent subsequent interactions with a ComboBox from adding to the selected rows? It should behave like any other click on a TableRow, should it not?
I am using JDK 8u161.
Note: I understand there is a ComboBoxTableCell class available, but I've not been able to find any examples of how to use one properly; that is irrelevant to my question, though, unless the ComboBoxTableCell behaves differently.
Since you want an "always editing" cell, your implementation should behave more like CheckBoxTableCell than ComboBoxTableCell. The former bypasses the normal editing mechanism of the TableView. As a guess, I think it's your use of the normal editing mechanism that causes the selection issues—why exactly, I'm not sure.
Modifying your ManufactureTableCell to be more like CheckBoxTableCell, it'd look something like:
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
private final IntFunction<Property<Manufacturer>> extractor;
private Property<Manufacturer> property;
ManufacturerTableCell(IntFunction<Property<Manufacturer>> extractor, ObservableList<Manufacturer> items) {
this.extractor = extractor;
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
// removed StringConverter for brevity (accidentally)
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
cboStatus.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (event.isShortcutDown()) {
getTableView().getSelectionModel().select(getIndex(), getTableColumn());
} else {
getTableView().getSelectionModel().clearAndSelect(getIndex(), getTableColumn());
}
event.consume();
});
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
clearProperty();
if (empty) {
setGraphic(null);
} else {
property = extractor.apply(getIndex());
Bindings.bindBidirectional(cboStatus.valueProperty(), property);
setGraphic(cboStatus);
}
}
private void clearProperty() {
setGraphic(null);
if (property != null) {
Bindings.unbindBidirectional(cboStatus.valueProperty(), property);
}
}
}
And you'd install it like so:
// note you could probably share the same ObservableList between all cells
colManufacturer.setCellFactory(param ->
new ManufacturerTableCell(i -> tableView.getItems().get(i).manufacturerProperty(),
FXCollections.observableArrayList(Manufacturer.values())));
As already mentioned, the above implementation bypasses the normal editing mechanism; it ties the value of the ComboBox directly to the model item's property. The implementation also adds a MOUSE_PRESSED handler to the ComboBox that selects the row (or cell if using cell selection) as appropriate. Unfortunately, I'm not quite understanding how to implement selection when Shift is down so only "Press" and "Shortcut+Press" is handled.
The above works how I believe you want it to, but I could only test it out using JavaFX 12.
Sorry, but I must have a mental lapsus right now, because I don't see where the problem is, and should be trivial. I've prepared a simple scenario where I bind a field to a bean property using the BeanFieldGroup, and when I click the Change and Reset buttons, the model is set with the correct values, but the textfield in the UI is not being updated.
I'm using Vaadin4Spring, but should not be the issue.
import com.vaadin.data.fieldgroup.BeanFieldGroup;
import com.vaadin.navigator.View;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.spring.annotation.SpringView;
import com.vaadin.ui.Button;
import com.vaadin.ui.Notification;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import java.io.Serializable;
#SpringView(name = "test")
public class TestView extends VerticalLayout implements View {
private TextField txtTest = new TextField("Test");
private Button btnChange = new Button("Click!");
private Button btnReset = new Button("Reset");
private TestBean testBean = new TestBean();
public TestView() {
txtTest.setImmediate(true);
addComponent(txtTest);
addComponent(btnChange);
addComponent(btnReset);
BeanFieldGroup<TestBean> binder = new BeanFieldGroup<>(TestBean.class);
binder.setItemDataSource(testBean);
binder.setBuffered(false);
binder.bind(txtTest, "text");
initComponents();
}
private void initComponents() {
btnChange.addClickListener(new Button.ClickListener() {
#Override
public void buttonClick(Button.ClickEvent event) {
testBean.setText("Hello world!");
}
});
btnReset.addClickListener(new Button.ClickListener() {
#Override
public void buttonClick(Button.ClickEvent event) {
testBean.setText("");
}
});
}
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
Notification.show("Test");
}
public class TestBean implements Serializable {
private String text;
public TestBean() {
text = "";
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
}
The closest thing I have found is binder.discard(), which forces all bound fields to re-read its value from the bean. Yes, it still has to be called manually, but is still far less painful than getItemDataSource().getItemProperty(...).setValue(...). If there are any concerns with this brute-force approach then of course one can call Field.discard() directly on the fields that should be affected.
You are calling a bean setter directly and because Java doesn't provide any way to listen that kind of changes, the Vaadin property (or a TextField) doesn't know that the value has been changed. If you change the value through a Vaadin property by saying
binder.getItemDataSource().getItemProperty("text").setValue("new value");
then you see "new value" on the TextField, and because buffering is disabled, testBean.getText() also returns "new value".
When I press the Right Mouse button on a JavaFX TreeTableView it selects the node I clicked on. I can understand why this may be desirable in most cases, however not in all cases.
I am working on an application where I have multiple columns in the tree table with one column using a canvas widget for custom graphics (waveforms). The graphics column needs to be able to be interacted with for various reasons (setting markers, zooming, etc.) with the mouse. Because of this I don't want the mouse buttons to select the row in the table (or interact with the table).
I was able filter out the click with the first mouse button by putting a addEventFilter() for MouseEvent.MOUSE_CLICKED with a event.consume(). I can then process the mouse click the way I want.
However when I right click on the cell the selection in the table changes to that row. I have tried putting event filters on the cell, the table, the row, the column, nothing seems to work to filter the right click selection change. Note the canvas widget is set to be mouse transparent.
Here is an example using the standard address book example, except I replaced the email column with a canvas widget.
package sample;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;
public class Main extends Application {
private TableView<Person> table = new TableView<Person>();
private ContextMenu menu = new ContextMenu();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
class MyTableCell extends TableCell<Person, String> {
public MyTableCell(ContextMenu menu) {
super();
setContextMenu(menu);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(item);
setGraphic(null);
}
}
class MySpecialCell extends MyTableCell {
Canvas canvas = new Canvas(200.0, 12.0);
public MySpecialCell() {
super(null);
canvas.setMouseTransparent(true);
addEventFilter(MouseEvent.ANY, e -> e.consume());
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (! empty) {
canvas.getGraphicsContext2D().strokeText(item, 5.0, 10.0);
setGraphic(canvas);
} else {
setGraphic(null);
}
}
}
#Override
public void start(Stage stage) throws Exception{
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(450);
stage.setHeight(500);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
menu.getItems().add(new MenuItem("Hello World"));
Callback cellFactory = param -> new MyTableCell(menu);
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName"));
firstNameCol.setCellFactory(cellFactory);
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName"));
lastNameCol.setCellFactory(cellFactory);
TableColumn emailCol = new TableColumn("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email"));
emailCol.setCellFactory(param -> new MySpecialCell());
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
This is using Java 8u20. In this example the MyTableCell class is used for the cells in the first two columns. It adds a context menu to those columns. The MySpecialCell class is for the last column and it puts a canvas in the table with the email text in it. It also goes way overboard and filters ANY mouse event.
If you run this you will see that if you use mouse button 1 in the first two columns you can change the selection in the table. Mouse button 1 in the third column does not change selection. However Mouse button 2 (right mouse button) DOES change selection in the third column.
I want it to not change selection. Can someone give me a hint on how to prevent selection from changing when using the right mouse button in the column?
NOTE: I have tried having the canvas process the mouse input (actually not make it mouse transparent and having it filter all mouse events) and the table still changes selection on right click. For various reasons (where the data is, the fact that the canvas is actually a stack of canvas widgets, the difficulty of figuring out what node the mouse is interacting with in the table, etc) in my real application I wanted the cell to handle the mouse input.
Thanks!
To repeat my comments: I think the behaviour might be rather a bug than a feature - I would expect that consuming any mouseEvent would prevent the right (secondary) button just the same as the left (primary) button normal actions.
Was evaluated as a feature: the missing piece was to consume the contextMenuEvents in addition to the mouseEvents, something like
public MySpecialCell() {
super(null);
canvas.setMouseTransparent(true);
addEventFilter(MouseEvent.ANY, e -> e.consume());
// move Jonathan's code from table to cell level:
// need to consume contextMenuEvents as well
addEventFilter(ContextMenuEvent.ANY, e -> e.consume());
}
Back to disagreeing with the evaluation:
all mouse buttons should behave consistently to consuming all mouseEvents
contextMenuEvent is basically unrelated to selection, so consuming it should have no effect (in either way) on selection
The hack below might still be needed if contextMenu's triggered by keyboard on the special column are required, didn't dig further, though.
Anyway, a quick hack-around is to replace the default behaviour by a custom implementation. The collaborators
a custom TableCellBehaviour that overrides doSelect to do nothing on receiving a secondary button event
a custom TableCellSkin: that's only needed to plug-in the custom behaviour - needs to extend TableCellSkinBase (and c&p the implementation of its abstract methods from TableCellSkin) because only then we have a constructor taking our custom behaviour
let the custom TableCell create the custom skin instead of the default
Something like (note: while working, it's not properly tested):
class MySpecialCellBehavior extends TableCellBehavior {
public MySpecialCellBehavior(TableCell control) {
super(control);
}
#Override
protected void doSelect(double x, double y, MouseButton button,
int clickCount, boolean shiftDown, boolean shortcutDown) {
// do nothing on secondary button
if (button == MouseButton.SECONDARY) return;
super.doSelect(x, y, button, clickCount, shiftDown, shortcutDown);
}
}
class MySpecialCellSkin extends TableCellSkinBase {
private final TableColumn tableColumn;
public MySpecialCellSkin(TableCell tableCell) {
super(tableCell, new MySpecialCellBehavior(tableCell));
// doesn't make a difference
//consumeMouseEvents(true);
this.tableColumn = tableCell.getTableColumn();
super.init(tableCell);
}
#Override protected BooleanProperty columnVisibleProperty() {
return tableColumn.visibleProperty();
}
#Override protected ReadOnlyDoubleProperty columnWidthProperty() {
return tableColumn.widthProperty();
}
}
class MySpecialCell extends MyTableCell {
Canvas canvas = new Canvas(200.0, 12.0);
public MySpecialCell() {
super(null);
canvas.setMouseTransparent(true);
addEventFilter(MouseEvent.ANY, e -> e.consume());
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (! empty) {
canvas.getGraphicsContext2D().strokeText(item, 5.0, 10.0);
setGraphic(canvas);
} else {
setGraphic(null);
}
}
// create and return the custom skin
#Override
protected Skin<?> createDefaultSkin() {
return new MySpecialCellSkin(this);
}
}
I found these examples:
http://www.jeasyui.com/tutorial/datagrid/datagrid21.php\
Can a table row expand and close?
Basically I want to create a JavaFX table which I can expand in order to see more data. Is there any similar example written in JavaFX?
EDIT
So, after reworking the problem with tableView specifics, I (sort of) quickly hacked together this example. Keep in mind, I didn't use the animation mentioned in the original answer, although it would be easy enough to adapt, and I didn't replicate the provided example exactly at all, since I honestly, didn't have time. But this gives the basic accordion feel, where you would just need to spend time messing around with various width and height properties of different fields to achieve something that was exactly that. (in the handler you might want to even insert a row where the first column has a huge width and a nested table view to achieve sort of exactly what they were doing). again, this is with 1 column, and it shows the basics of adding a bit of added information on expansion, you could take this as far as you want:
fileChooserExample.java:
package filechooserexample;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Callback;
public class FileChooserExample extends Application {
public static void main(String[] args) { launch(args); }
#Override public void start(final Stage stage) {
stage.setTitle("People");
// stage.getIcons().add(new Image("http://icons.iconarchive.com/icons/icons-land/vista-people/72/Historical-Viking-Female-icon.png")); // icon license: Linkware (Backlink to http://www.icons-land.com required)
// create a table.
final TableView<Person> table = new TableView<>(
FXCollections.observableArrayList(
new Person("Jacob", "Smith"),
new Person("Isabella", "Johnson"),
new Person("Ethan", "Williams"),
new Person("Emma", "Jones"),
new Person("Michael", "Brown")
)
);
// define the table columns.
TableColumn<Person, Boolean> actionCol = new TableColumn<>("Action");
actionCol.setSortable(false);
actionCol.setPrefWidth(1000);
// define a simple boolean cell value for the action column so that the column will only be shown for non-empty rows.
actionCol.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person, Boolean>, ObservableValue<Boolean>>() {
#Override public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<Person, Boolean> features) {
return new SimpleBooleanProperty(features.getValue() != null);
}
});
// create a cell value factory with an add button for each row in the table.
actionCol.setCellFactory(new Callback<TableColumn<Person, Boolean>, TableCell<Person, Boolean>>() {
#Override public TableCell<Person, Boolean> call(TableColumn<Person, Boolean> personBooleanTableColumn) {
return new AddPersonCell(stage, table);
}
});
table.getColumns().setAll(actionCol);
table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
stage.setScene(new Scene(table));
stage.show();
}
/** A table cell containing a button for adding a new person. */
private class AddPersonCell extends TableCell<Person, Boolean> {
// a button for adding a new person.
final Button addButton = new Button("Add");
// pads and centers the add button in the cell.
final VBox paddedButton = new VBox();
final HBox mainHolder = new HBox();
// records the y pos of the last button press so that the add person dialog can be shown next to the cell.
final DoubleProperty buttonY = new SimpleDoubleProperty();
/**
* AddPersonCell constructor
* #param stage the stage in which the table is placed.
* #param table the table to which a new person can be added.
*/
AddPersonCell(final Stage stage, final TableView table) {
paddedButton.setPadding(new Insets(3));
paddedButton.getChildren().add(addButton);
mainHolder.getChildren().add(paddedButton);
addButton.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
buttonY.set(mouseEvent.getScreenY());
if (getTableRow().getPrefHeight() == 100){
getTableRow().setPrefHeight(35);
paddedButton.getChildren().remove(1);
getTableRow().autosize();
}
else{
getTableRow().setPrefHeight(100);
Label myLabel = new Label();
myLabel.setText("This is new label text!");
myLabel.setTextFill(Color.BLACK);
paddedButton.getChildren().add(myLabel);
getTableRow().autosize();
}
}
});
addButton.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent actionEvent) {
table.getSelectionModel().select(getTableRow().getIndex());
}
});
}
/** places an add button in the row only if the row is not empty. */
#Override protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(paddedButton);
}
}
}
}
Person.java:
package filechooserexample;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Person {
private StringProperty firstName;
private StringProperty lastName;
public Person(String firstName, String lastName) {
setFirstName(firstName);
setLastName(lastName);
}
public final void setFirstName(String value) { firstNameProperty().set(value); }
public final void setLastName(String value) { lastNameProperty().set(value); }
public String getFirstName() { return firstNameProperty().get(); }
public String getLastName() { return lastNameProperty().get(); }
public StringProperty firstNameProperty() {
if (firstName == null) firstName = new SimpleStringProperty(this, "firstName");
return firstName;
}
public StringProperty lastNameProperty() {
if (lastName == null) lastName = new SimpleStringProperty(this, "lastName");
return lastName;
}
}
Again, pardon the seemingly hackery of adding the various buttons with the named columns that do nothing, It just got super busy here so I borrowed the main table structure from :
original SO table dynamic row addition question
Who did a wonderful job of adding additional rows to a table.
again, if this is not at all what you need let me know, and I'll try to help as best I can.