So I have been experimenting with it a litle bit with javaFX and I came across some rather weird behavior which might be linked to the TableView#edit() method.
I'll post a working example on the bottom of this post again, so you can see what exactually is happening on which cell (debuging included!).
I'll try to explain all the behavior myself, though its way easier to see it for yourself. Basically the events are messed up when using the TableView#edit() method.
1:
If you are using the contextMenu to add a new item, the keyEvents for the the keys 'escape' and 'Enter' (and propably the arrow keys, though I dont use them right now) are consumed before they fire the events on the Cells (e.g. textField and cell KeyEvents!) Though it is firing the keyEvent on the Parent node. (the AnchorPane in this case).
Now I know for a fact that these keys are captured and consumed by the contextMenu default behavior. Though it shouldn't be happening since the contextMenu is already hidden after the new item is added. further more the textField should recieve the events, especially when it is focused!
2:
When you use the button at the bottom of the TableView to add a new Item, The keyEvents are fired on the Parent node (the AnchorPane) and the Cell. Though the textField (even when focused) recieve no keyEvents at all. I cannot explain why the TextField wouldn't recieve any event even when typed in, so I assume that would definitely be a bug?
3:
When editing a cell through double click, it updates the editingCellProperty of the TableView correctly (which I check for several times). Though when start editing though the contextMenu Item (which only calls startEdit() for testpurpose) It doesnt update the editing state correctly! Funny enough it allows the keyEvents to continue as usual, unlike situation 1 & 2.
4:
When you edit an item, and then add an item (either way will cause this problem) it will update the editingCellProperty to the current cell, though when stop editing, it somehow revert back to the last Cell?!? Thats the part where funny things are happening, which I really cannot explain.
Note that the startEdit() & cancelEdit() methods are called in weird moments, and on the wrong Cells!
Right now I dont understand any of this logic. If this is intended behavior, some explanation of it would be greatly appreciated!
This is the example:
package testpacket;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class EditStateTest extends Application
{
private static ObservableList<SimpleStringProperty> exampleList = FXCollections.observableArrayList();
//Placeholder for the button
private static SimpleStringProperty PlaceHolder = new SimpleStringProperty();
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception
{
// basic ui setup
AnchorPane parent = new AnchorPane();
Scene scene = new Scene(parent);
primaryStage.setScene(scene);
//fill backinglist with data
for(int i = 0 ; i < 20; i++)
exampleList.add(new SimpleStringProperty("Hello Test"));
exampleList.add(PlaceHolder);
//create a basic tableView
TableView<SimpleStringProperty> listView = new TableView<SimpleStringProperty>();
listView.setEditable(true);
TableColumn<SimpleStringProperty, String> column = new TableColumn<SimpleStringProperty, String>();
column.setCellFactory(E -> new TableCellTest<SimpleStringProperty, String>());
column.setCellValueFactory(E -> E.getValue());
column.setEditable(true);
// set listViews' backing list
listView.setItems(exampleList);
listView.getColumns().clear();
listView.getColumns().add(column);
parent.getChildren().add(listView);
parent.setOnKeyReleased(E -> System.out.println("Parent - KeyEvent"));
primaryStage.show();
}
// basic editable cell example
public static class TableCellTest<S, T> extends TableCell<S, T>
{
// The editing textField.
protected static Button addButton = new Button("Add");
protected TextField textField = new TextField();;
protected ContextMenu menu;
public TableCellTest()
{
this.setOnContextMenuRequested(E -> {
if(this.getTableView().editingCellProperty().get() == null)
this.menu.show(this, E.getScreenX(), E.getScreenY());
});
this.menu = new ContextMenu();
MenuItem createNew = new MenuItem("create New");
createNew.setOnAction(E -> {
System.out.println("Cell ContextMenu " + this.getIndex() + " - createNew: onAction");
this.onNewItem(this.getIndex() + 1);
});
MenuItem edit = new MenuItem("edit");
edit.setOnAction(E -> {
System.out.println("Cell ContextMenu " + this.getIndex() + " - edit: onAction");
this.startEdit();
});
this.menu.getItems().setAll(createNew, edit);
addButton.addEventHandler(ActionEvent.ACTION, E -> {
if(this.getIndex() == EditStateTest.exampleList.size() - 1)
{
System.out.println("Cell " + this.getIndex() + " - Button: onAction");
this.onNewItem(this.getIndex());
}
});
addButton.prefWidthProperty().bind(this.widthProperty());
this.setOnKeyReleased(E -> System.out.println("Cell " + this.getIndex() + " - KeyEvent"));
}
public void onNewItem(int index)
{
EditStateTest.exampleList.add(index, new SimpleStringProperty("New Item"));
this.getTableView().edit(index, this.getTableColumn());
textField.requestFocus();
}
#Override
public void startEdit()
{
if (!isEditable()
|| (this.getTableView() != null && !this.getTableView().isEditable())
|| (this.getTableColumn() != null && !this.getTableColumn().isEditable()))
return;
System.out.println("Cell " + this.getIndex() + " - StartEdit");
super.startEdit();
this.createTextField();
textField.setText((String)this.getItem());
this.setGraphic(textField);
textField.selectAll();
this.setText(null);
}
#Override
public void cancelEdit()
{
if (!this.isEditing())
return;
System.out.println("Cell " + this.getIndex() + " - CancelEdit");
super.cancelEdit();
this.setText((String)this.getItem());
this.setGraphic(null);
}
#Override
protected void updateItem(T item, boolean empty)
{
System.out.println("Cell " + this.getIndex() + " - UpdateItem");
super.updateItem(item, empty);
if(empty || item == null)
{
if(this.getIndex() == EditStateTest.exampleList.size() - 1)
{
this.setText("");
this.setGraphic(addButton);
}
else
{
this.setText(null);
this.setGraphic(null);
}
}
else
{
// These checks are needed to make sure this cell is the specific cell that is in editing mode.
// Technically this#isEditing() can be left out, as it is not accurate enough at this point.
if(this.getTableView().getEditingCell() != null
&& this.getTableView().getEditingCell().getRow() == this.getIndex())
{
//change to TextField
this.setText(null);
this.setGraphic(textField);
}
else
{
//change to actual value
this.setText((String)this.getItem());
this.setGraphic(null);
}
}
}
#SuppressWarnings("unchecked")
public void createTextField()
{
textField.setOnKeyReleased(E -> {
System.out.println("TextField " + this.getIndex() + " - KeyEvent");
System.out.println(this.getTableView().getEditingCell());
// if(this.getTableView().getEditingCell().getRow() == this.getIndex())
if(E.getCode() == KeyCode.ENTER)
{
this.setItem((T) textField.getText());
this.commitEdit(this.getItem());
}
else if(E.getCode() == KeyCode.ESCAPE)
this.cancelEdit();
});
}
}
}
I hope somebody could help me further with this. If you have suggestions/solutions or workarounds for this, please let me know!
Thanks for your time!
This is kind of the poster child for Josh Bloch's "Inheritance breaks Encapsulation" mantra. What I mean by that is that when you create a subclass of an existing class (TableCell in this case), you need to know a lot about the implementation of that class in order to make the subclass play nicely with the superclass. You make a lot of assumptions in your code about the interaction between the TableView and its cells that are not true, and that (along with some bugs and general weird implementations of event handling in some controls) is why your code is breaking.
I don't think I can address every single issue, but I can give some general pointers here and provide what I think is working code that achieves what you are trying to achieve.
First, cells are reused. This is a good thing, because it makes the table perform very efficiently when there is a large amount of data, but it makes it complicated. The basic idea is that cells are essentially only created for the visible items in the table. As the user scrolls around, or as the table content changes, cells that are no longer needed are reused for different items that become visible. This massively saves on memory consumption and CPU time (if used properly). In order to be able to improve the implementation, the JavaFX team deliberately don't specify how this works, and how and when cells are likely to be reused. So you have to be careful about making assumptions about the continuity of the item or index fields of a cell (and conversely, which cell is assigned to a given item or index), particularly if you change the structure of the table.
What you are basically guaranteed is:
Any time the cell is reused for a different item, the updateItem() method is invoked before the cell is rendered.
Any time the index of the cell changes (which may be because an item is inserted in the list, or may be because the cell is reused, or both), the updateIndex() method is invoked before the cell is rendered.
However, note that in the case where both change, there is no guarantee of the order in which these are invoked. So, if your cell rendering depends on both the item and the index (which is the case here: you check both the item and the index in your updateItem(...) method), you need to ensure the cell is updated when either of those properties change. The best way (imo) to achieve this is to create a private method to perform the update, and to delegate to it from both updateItem() and updateIndex(). This way, when the second of those is invoked, your update method is invoked with consistent state.
If you change the structure of the table, say by adding a new row, the cells will need to be rearranged, and some of them are likely to be reused for different items (and indexes). However, this rearrangement only happens when the table is laid out, which by default will not happen until the next frame rendering. (This makes sense from a performance perspective: imagine you make 1000 different changes to a table in a loop; you don't want the cells to be recalculated on every change, you just want them recalculated once the next time the table is rendered to the screen.) This means, if you add rows to the table, you cannot rely on the index or item of any cell being correct. This is why your call to table.edit(...) immediately after adding a new row is so unpredictable. The trick here is to force a layout of the table by calling TableView.layout() after adding the row.
Note that pressing "Enter" when a table cell is focused will cause that cell to go into editing mode. If you handle commits on the text field in a cell with a key released event handler, these handlers will interact in an unpredictable way. I think this is why you see the strange key handling effects you see (also note that text fields consume the key events they process internally). The workaround for that is to use an onAction handler on the text field (which is arguably more semantic anyway).
Don't make the button static (I have no idea why you would want to do this anyway). "Static" means that the button is a property of the class as a whole, not of the instances of that class. So in this case, all the cells share a reference to a single button. Since the cell reuse mechanism is unspecified, you don't know that only one cell will have the button set as its graphic. This can cause disaster. For example, if you scroll the cell with the button out of view and then back into view, there is no guarantee the same cell will be used to display that last item when it comes back into view. It is possible (I don't know the implementation) that the cell that previously displayed the last item is sitting unused (perhaps part of the virtual flow container, but clipped out of view) and is not updated. In that case, the button would then appear twice in the scene graph, which would either throw an exception or cause unpredictable behavior. There's basically no valid reason to ever make a scene graph node static, and here it's a particularly bad idea.
To code functionality like this, you should read extensively the documentation for the cell mechanism and for TableView, TableColumn, and TableCell. At some point you might find you need to dig into the source code to see how the provided cell implementations work.
Here's (I think, I'm not sure I've fully tested) a working version of what I think you were looking for. I made some slight changes to the structure (no need for StringPropertys as the data type, String works just fine as long as you have no identical duplicates), added an onEditCommit handler, etc.
import javafx.application.Application;
import javafx.beans.value.ObservableValueBase;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewWithAddAtEnd extends Application {
#Override
public void start(Stage primaryStage) {
TableView<String> table = new TableView<>();
table.setEditable(true);
TableColumn<String, String> column = new TableColumn<>("Data");
column.setPrefWidth(150);
table.getColumns().add(column);
// use trivial wrapper for string data:
column.setCellValueFactory(cellData -> new ObservableValueBase<String>() {
#Override
public String getValue() {
return cellData.getValue();
}
});
column.setCellFactory(col -> new EditingCellWithMenuEtc());
column.setOnEditCommit(e ->
table.getItems().set(e.getTablePosition().getRow(), e.getNewValue()));
for (int i = 1 ; i <= 20; i++) {
table.getItems().add("Item "+i);
}
// blank for "add" button:
table.getItems().add("");
BorderPane root = new BorderPane(table);
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
public static class EditingCellWithMenuEtc extends TableCell<String, String> {
private TextField textField ;
private Button button ;
private ContextMenu contextMenu ;
// The update relies on knowing both the item and the index
// Since we don't know (or at least shouldn't rely on) the order
// in which the item and index are updated, we just delegate
// implementations of both updateItem and updateIndex to a general
// method. This way doUpdate() is always called last with consistent
// state, so we are guaranteed to be in a consistent state when the
// cell is rendered, even if we are temporarily in an inconsistent
// state between the calls to updateItem and updateIndex.
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
doUpdate(item, getIndex(), empty);
}
#Override
public void updateIndex(int index) {
super.updateIndex(index);
doUpdate(getItem(), index, isEmpty());
}
// update the cell. This updates the text, graphic, context menu
// (empty cells and the special button cell don't have context menus)
// and editable state (empty cells and the special button cell can't
// be edited)
private void doUpdate(String item, int index, boolean empty) {
if (empty) {
setText(null);
setGraphic(null);
setContextMenu(null);
setEditable(false);
} else {
if (index == getTableView().getItems().size() - 1) {
setText(null);
setGraphic(getButton());
setContextMenu(null);
setEditable(false);
} else if (isEditing()) {
setText(null);
getTextField().setText(item);
setGraphic(getTextField());
getTextField().requestFocus();
setContextMenu(null);
setEditable(true);
} else {
setText(item);
setGraphic(null);
setContextMenu(getMenu());
setEditable(true);
}
}
}
#Override
public void startEdit() {
if (! isEditable()
|| ! getTableColumn().isEditable()
|| ! getTableView().isEditable()) {
return ;
}
super.startEdit();
getTextField().setText(getItem());
setText(null);
setGraphic(getTextField());
setContextMenu(null);
textField.selectAll();
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem());
setGraphic(null);
setContextMenu(getMenu());
}
#Override
public void commitEdit(String newValue) {
// note this fires onEditCommit handler on column:
super.commitEdit(newValue);
setText(getItem());
setGraphic(null);
setContextMenu(getMenu());
}
private void addNewItem(int index) {
getTableView().getItems().add(index, "New Item");
// force recomputation of cells:
getTableView().layout();
// start edit:
getTableView().edit(index, getTableColumn());
}
private ContextMenu getMenu() {
if (contextMenu == null) {
createContextMenu();
}
return contextMenu ;
}
private void createContextMenu() {
MenuItem addNew = new MenuItem("Add new");
addNew.setOnAction(e -> addNewItem(getIndex() + 1));
MenuItem edit = new MenuItem("Edit");
// note we call TableView.edit(), not this.startEdit() to ensure
// table's editing state is kept consistent:
edit.setOnAction(e -> getTableView().edit(getIndex(), getTableColumn()));
contextMenu = new ContextMenu(addNew, edit);
}
private Button getButton() {
if (button == null) {
createButton();
}
return button ;
}
private void createButton() {
button = new Button("Add");
button.prefWidthProperty().bind(widthProperty());
button.setOnAction(e -> addNewItem(getTableView().getItems().size() - 1));
}
private TextField getTextField() {
if (textField == null) {
createTextField();
}
return textField ;
}
private void createTextField() {
textField = new TextField();
// use setOnAction for enter, to avoid conflict with enter on cell:
textField.setOnAction(e -> commitEdit(textField.getText()));
// use key released for escape: note text fields do note consume
// key releases they don't handle:
textField.setOnKeyReleased(e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
}
public static void main(String[] args) {
launch(args);
}
}
My big learn item of the day (freely summarized and slightly extended from James' answer):
view.edit(...) is safe to call only if all cells are in a stable state and the target cell is visible. Most of the time we can force the stable state by calling view.layout()
Below is yet another example to play with:
as already mentioned in one of my comments, it differs from James' in starting the edit in a listener to the items: might not always be the best place, has the advantage of a single location (at least as far as list mutations are involved) for the layout call. A drawback is that we need to be certain that the viewSkin's listener to the items is called before ours. To guarantee that, our own listener is re/registered whenever the skin changes.
as an exercise in re-use, I extended TextFieldTableCell to additionally handle the button/menu and update the cell's editability based on the row item.
there are also buttons outside the table to experiment with: addAndEdit and scrollAndEdit. The latter is to demonstrate that "instable cell state" can be reached by paths different from modifying the items.
Currently, I tend to subclass TableView and override its edit(...) to force the re-layout. Something like:
public static class TTableView<S> extends TableView<S> {
/**
* Overridden to force a layout before calling super.
*/
#Override
public void edit(int row, TableColumn<S, ?> column) {
layout();
super.edit(row, column);
}
}
Doing, relieves the burden on client code. What's left for them is to make sure the target cell is scrolled into the visible area, though.
The example:
public class TablePersonAddRowAndEdit extends Application {
private PersonStandIn standIn = new PersonStandIn();
private final ObservableList<Person> data =
// Person from Tutorial - with Properties exposed!
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")
, standIn
);
private Parent getContent() {
TableView<Person> table = new TableView<>();
table.setItems(data);
table.setEditable(true);
TableColumn<Person, String> firstName = new TableColumn<>("First Name");
firstName.setCellValueFactory(new PropertyValueFactory<>("firstName"));
firstName.setCellFactory(v -> new MyTextFieldCell<>());
ListChangeListener l = c -> {
while (c.next()) {
// true added only
if (c.wasAdded() && ! c.wasRemoved()) {
// force the re-layout before starting the edit
table.layout();
table.edit(c.getFrom(), firstName);
return;
}
};
};
// install the listener to the items after the skin has registered
// its own
ChangeListener skinListener = (src, ov, nv) -> {
table.getItems().removeListener(l);
table.getItems().addListener(l);
};
table.skinProperty().addListener(skinListener);
table.getColumns().addAll(firstName);
Button add = new Button("AddAndEdit");
add.setOnAction(e -> {
int standInIndex = table.getItems().indexOf(standIn);
int index = standInIndex < 0 ? table.getItems().size() : standInIndex;
index =1;
Person person = createNewItem("edit", index);
table.getItems().add(index, person);
});
Button edit = new Button("Edit");
edit.setOnAction(e -> {
int index = 1;//table.getItems().size() -2;
table.scrollTo(index);
table.requestFocus();
table.edit(index, firstName);
});
HBox buttons = new HBox(10, add, edit);
BorderPane content = new BorderPane(table);
content.setBottom(buttons);
return content;
}
/**
* A cell that can handle not-editable items. Has to update its
* editability based on the rowItem. Must be done in updateItem
* (tried a listener to the tableRow's item, wasn't good enough - doesn't
* get notified reliably)
*
*/
public static class MyTextFieldCell<S> extends TextFieldTableCell<S, String> {
private Button button;
public MyTextFieldCell() {
super(new DefaultStringConverter());
ContextMenu menu = new ContextMenu();
menu.getItems().add(createMenuItem());
setContextMenu(menu);
}
private boolean isStandIn() {
return getTableRow() != null && getTableRow().getItem() instanceof StandIn;
}
/**
* Update cell's editable based on the rowItem.
*/
private void doUpdateEditable() {
if (isEmpty() || isStandIn()) {
setEditable(false);
} else {
setEditable(true);
}
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
doUpdateEditable();
if (isStandIn()) {
if (isEditing()) {
LOG.info("shouldn't be editing - has StandIn");
}
if (button == null) {
button = createButton();
}
setText(null);
setGraphic(button);
}
}
private Button createButton() {
Button b = new Button("Add");
b.setOnAction(e -> {
int index = getTableView().getItems().size() -1;
getTableView().getItems().add(index, createNewItem("button", index));
});
return b;
}
private MenuItem createMenuItem() {
MenuItem item = new MenuItem("Add");
item.setOnAction(e -> {
if (isStandIn()) return;
int index = getIndex();
getTableView().getItems().add(index, createNewItem("menu", index));
});
return item;
}
private S createNewItem(String text, int index) {
return (S) new Person(text + index, text + index, text);
}
}
private Person createNewItem(String text, int index) {
return new Person(text + index, text + index, text);
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.setTitle(FXUtils.version());
primaryStage.show();
}
/**
* Marker-Interface to denote a class as not mutable.
*/
public static interface StandIn {
}
public static class PersonStandIn extends Person implements StandIn{
public PersonStandIn() {
super("standIn", "", "");
}
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TablePersonAddRowAndEdit.class.getName());
}
Update
shouldn't have been too surprised - a related problem was discussed half a year ago (and produced a bug report)
I am trying to add a certain behavior to the TableView, so it is more user friendly. Though I got a problem getting one of those things to work.
What I am trying to do is adding a new Item to the TableView (I use a custom BackingList), and automatically start editing that new Item. Here is an example snippit on how I try to accomplish this.
public void onNewItem(Object newItem)
{
// I use the index of this cell, to make sure it is inserted at this
// index in the TableView instead of at the end.
this.BackingList.add(this.getIndex(), newItem);
// Start editing the previous cell, assuming the index of this cell is already updated.
this.getTableView().edit(this.getIndex(), this.getTableColumn());
}
So I add the item to the backingList, which should fire an UpdateEvent inside the TableView. Though against my expectations that update is happening way after this method is exited. This means I cannot start editing that specific cell (since the item doesn't exist yet) at this point in time.
So the question is, is there a way to 'force' an update, so I can start editing the specific cell? Or are there any other workarounds for this problem?
If there are any suggestion/idea's/solutions, please let me know.
Thanks in advance!
Edit 01:
I tried some things to accomplish this, and one method that is working is the following. Though the downside is, that it breaks the EventSystem somehow. So it is not usefull.(In case if you are wondering, this.startEdit() isn't working either, so I have no options left unfortunatly.)
public void onNewItem(Object newItem)
{
this.BackingList.add(this.getIndex(), newItem);
this.getTableRow().updateIndex(this.getIndex());//TODO fishy fix!
this.getTableView().edit(this.getIndex(), this.getTableColumn());
}
EDIT 02:
On request Im adding this example so people can see what is happening. I added two ways to add ellements. one is via a contextMenu, which works perfectly. second is the button at the bottom of the list.
package testpacket;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class EditStateTest extends Application
{
private static ObservableList<SimpleStringProperty> exampleList = FXCollections.observableArrayList();
//Placeholder for the button
private static SimpleStringProperty PlaceHolder = new SimpleStringProperty();
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception
{
// basic ui setup
AnchorPane parent = new AnchorPane();
Scene scene = new Scene(parent);
primaryStage.setScene(scene);
//fill backinglist with data
for(int i = 0 ; i < 20; i++)
exampleList.add(new SimpleStringProperty("Hello Test"));
exampleList.add(PlaceHolder);
//create a basic tableView
TableView<SimpleStringProperty> listView = new TableView<SimpleStringProperty>();
listView.setEditable(true);
TableColumn<SimpleStringProperty, String> column = new TableColumn<SimpleStringProperty, String>();
column.setCellFactory(E -> new TableCellTest<SimpleStringProperty, String>());
column.setCellValueFactory(E -> E.getValue());
column.setEditable(true);
// set listViews' backing list
listView.setItems(exampleList);
listView.getColumns().clear();
listView.getColumns().add(column);
parent.getChildren().add(listView);
parent.setOnKeyReleased(E -> System.out.println("KeyRelease Captuered: Parent"));
primaryStage.show();
}
// basic editable cell example
public static class TableCellTest<S, T> extends TableCell<S, T>
{
// The editing textField.
protected static TextField textField = new TextField();;
protected Button addButton;
protected ContextMenu menu;
public TableCellTest()
{
this.setOnContextMenuRequested(E -> {
if(this.getTableView().editingCellProperty().get() == null)
this.menu.show(this, E.getScreenX(), E.getScreenY());
});
this.menu = new ContextMenu();
MenuItem createNew = new MenuItem("create New");
createNew.setOnAction(E -> {
this.onNewItem(this.getIndex());
});
this.menu.getItems().add(createNew);
addButton = new Button("Add");
addButton.setOnAction(E -> this.onNewItem(exampleList.size() - 1));
addButton.prefWidthProperty().bind(this.widthProperty());
}
public void onNewItem(int index)
{
EditStateTest.exampleList.add(index, new SimpleStringProperty("New Item"));
this.getTableView().edit(index, this.getTableColumn());
textField.requestFocus();
}
#Override
public void startEdit()
{
if (!isEditable()
|| (this.getTableView() != null && !this.getTableView().isEditable())
|| (this.getTableColumn() != null && !this.getTableColumn().isEditable()))
return;
super.startEdit();
if(textField == null)
this.createTextField();
textField.setText((String)this.getItem());
this.setGraphic(textField);
textField.selectAll();
this.setText(null);
}
#Override
public void cancelEdit()
{
if (!this.isEditing())
return;
super.cancelEdit();
this.setText((String)this.getItem());
this.setGraphic(null);
}
#Override
protected void updateItem(T item, boolean empty)
{
super.updateItem(item, empty);
// Checks if visuals need an update.
if(this.getIndex() == EditStateTest.exampleList.size() - 1)
{
this.setText("");
this.setGraphic(addButton);
}
else if(empty || item == null)
{
this.setText(null);
this.setGraphic(null);
}
else
{
// These checks are needed to make sure this cell is the specific cell that is in editing mode.
// Technically this#isEditing() can be left out, as it is not accurate enough at this point.
if(this.isEditing() && this.getTableView().getEditingCell() != null
&& this.getTableView().getEditingCell().getRow() == this.getIndex())
{
//change to TextField
this.setText(null);
this.setGraphic(textField);
}
else
{
//change to actual value
this.setText((String)this.getItem());
this.setGraphic(null);
}
}
}
#SuppressWarnings("unchecked")
public void createTextField()
{
// A keyEvent for the textField. which is called when there is no keyEvent set to this cellObject.
textField.addEventHandler(KeyEvent.KEY_RELEASED, E -> {
if(this.getTableView().getEditingCell().getRow() == this.getIndex())
if(E.getCode() == KeyCode.ENTER)
{
this.setItem((T) textField.getText());
this.commitEdit(this.getItem());
}
else if(E.getCode() == KeyCode.ESCAPE)
this.cancelEdit();
});
}
}
}
The idea is that there is a new item added (which works perfectly) and that it will automatically start editing the newly added Item. When using the contextMenu it works perfectly, though when using the button it works only in some cases. (about every 3-5 new Items it works once)
I hope that there is some way to solve this wierd problem.
On a side note, when making the button static (which I prefer) the button somehow disapears every few new Items. And it seems like it is in the same patern as I mentioned above.
I have been digging quite a lot into this problem right now, and the main problem why this was rather confusing is because the eventSystem is messing up when using the 'normal' way of starting an edit on a certain cell.
This problem (e.g. editing state updating) was visually caused by a flaw inside the onUpdateItem method. Basically the placeHolder for the button had a null value, causing the button to disapear. Unexplainable is that the button still appeared sporadically.
This should do the trick for the onUpdateItem() method.
if(empty || item == null)
{
if(this.getIndex() == EditStateTest.exampleList.size() - 1)
{
this.setText("");
this.setGraphic(addButton);
}
else
{
this.setText(null);
this.setGraphic(null);
}
}
else
{
// These checks are needed to make sure this cell is the specific cell that is in editing mode.
// Technically this#isEditing() can be left out, as it is not accurate enough at this point.
if(this.getTableView().getEditingCell() != null
&& this.getTableView().getEditingCell().getRow() == this.getIndex())
{
//change to TextField
this.setText(null);
this.setGraphic(textField);
}
else
{
//change to actual value
this.setText((String)this.getItem());
this.setGraphic(null);
}
}
Anyway, since the method in question is updated, the editing state is entered flawelessly. But there are still problems with the EventSystem behind the TableView unfortunatly. Meaning that the example (even with the changes above) is not functional. But as the problem is out of the scope of this question, I consider this problem solved. If you are interested in the solution for the EventSystem, this Question is dedicated to that problem.
Everyone who helped me out with this problem, many thanks!
in MainActivity.java I am reading a list from a txt file then on a show list button I am able to display the list, with list items, images and checkbox using custom grid view. Now with another button click I am trying to get checked item's position so as I can use these selected items. the code I have is giving me a null pointer exception error on . view.getCheckedItemPositions();
MainActivity.java
if(v.getId()==R.id.toast)
{
Toast.makeText(this,"positions" + adapter.getCheckedItemPositions(view), Toast.LENGTH_SHORT).show();
}
}
public class CustomAdapter extends BaseAdapter {
#Override
public View getView(int position, View view, ViewGroup parent) {
LinearLayout layout = new LinearLayout(getApplicationContext());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
view = new View(getApplicationContext());
TextView text=new TextView(getApplicationContext());
text.setTextColor(Color.BLACK);
text.setText(content.get(position));
CheckBox checkBox = new CheckBox(getApplicationContext());
checkBox.setChecked(!checkBox.isChecked());
checkBox.setChecked(true);
layout.addView(checkBox);
layout.addView(text);
return layout;
}
public List<Integer> getCheckedItemPositions(GridView view){
SparseBooleanArray checked = new SparseBooleanArray();
checked = view.getCheckedItemPositions();
List<Integer> positions = new ArrayList<>();
int checksize=checked.size();
for(int i=0; i<checksize; i++) {
if (checked.valueAt(i)) {
positions.add(checked.keyAt(i));
}
}
return positions;
}
}
}
Activity_main.xml
<GridView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/gridView"
android:layout_below="#+id/button"
android:layout_alignParentStart="true"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Toast"
android:id="#+id/toast"
android:layout_alignParentTop="true"
android:layout_alignEnd="#+id/gridView"
android:layout_marginEnd="49dp" />
</RelativeLayout>
logcat
--------- beginning of crash
09-18 18:31:49.144 31751-31751/com.example.administrator.myapplication E/AndroidRuntime﹕ FATAL EXCEPTION: main
Process: com.example.administrator.myapplication, PID: 31751
java.lang.NullPointerException: Attempt to invoke virtual method 'int android.util.SparseBooleanArray.size()' on a null object reference
at com.example.administrator.myapplication.MainActivity$CustomAdapter.getCheckedItemPositions(MainActivity.java:158)
at com.example.administrator.myapplication.MainActivity.onClick(MainActivity.java:96)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5257)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
09-18 18:32:26.135 31751-31758/com.example.administrator.myapplication W/art﹕ Suspending all threads took: 102.748ms
09-18 18:32:27.078 31751-31758/com.example.administrator.myapplication W/art﹕ Suspending all threads took: 78.288ms
I got the answer..
I created a link hash map with string as position of the check box and value as the boolean value at that position. Like
LinkedHashMap mylist = new LinkedHashMap<>();
Now in order to get the status of the checked item and monitor it in runntime we have to set a Listener on checkbox either using
checkBox.setOnCheckedChangeListener();
or //
checkbox.setOnClickListener()
either of these two works.
Earlier I was just setting up the checkbox but not setting up a listener on the check box as above
checkBox.setChecked(!checkBox.isChecked());
checkBox.setChecked(true);
Now with setOnClickListener all I have to do is store the position and boolean value of the checkbox to the LinkHashmap.
checkBox.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (((CheckBox) v).isChecked()) {
saveValues("check" + position, true);
Toast.makeText(MainActivity.this,"position: "+getValues("check"+position), Toast.LENGTH_SHORT).show();
}
else {
saveValues("check"+position,false);
}
}
});
Later on I can see the checked items using
public Boolean getValues(String a){
return mylist.get(a).booleanValue();
}
I am really thankful to the contributors of the below post
Android: checkbox listener
I have a problem and It's 10 days that I am working and can't solve it.I made a layout for each row for ListView.This Layout Contains a linearLayout that there is a TextView and a WebView inside it.Now I Need a C# Project that I can add a new Row to the ListView with new text and url whenever I want.For Example: button.click { ListView.add(Resource.Layout.Items, "Text","Url")}..I know this command is wrong. Just I wanted to clear the problem for you.
I khnow it's custom Row layout and I read manny examples at this site other sites and Xamarin site about that,adapters,... but I can't do it. :(
Please answer me correctly.
It is very important for me.
Thanks a lot.
You need to create an adapter that can work with you custom objects as items. It could look like the following sample:
public class MyAdapter : BaseAdapter<MyItem>
{
readonly LayoutInflater inflater;
List<MyItem> myItemList;
public MyAdapter(Context context)
{
inflater = LayoutInflater.FromContext(context);
myItemList = YOUR_DATASOURCE.GetMyItems();
}
public override MyItem this [int index]
{
get { return myItemList[index]; }
}
public override int Count
{
get { return myItemList.Count; }
}
public override long GetItemId(int position)
{
return position;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View view = convertView ?? inflater.Inflate(Resource.Layout.MyItemLayout, parent, false);
var item = myItemList[position];
var viewHolder = view.Tag as MyViewHolder;
if (viewHolder == null)
{
viewHolder = new MyViewHolder();
viewHolder.Web = view.FindViewById<WebView>(Resource.Id.MyItemLayout_Icon);
viewHolder.Name = view.FindViewById<TextView>(Resource.Id.MyItemLayout_Title);
view.Tag = viewHolder;
}
viewHolder.Web.Url = item.Url; //You need to check how you have to set the url for a WebView
viewHolder.Name.Text = item.Text;
return view;
}
public override void NotifyDataSetChanged()
{
myItemList = YOUR_DATASOURCE.GetMyItems();
base.NotifyDataSetChanged();
}
}
class MyViewHolder : Java.Lang.Object
{
public WebView Web { get; set; }
public TextView Name { get; set; }
}
You apply the adapter to your ListView with ListView.Adapter = new MyAdapter(Activity);. Each time you change an item in you button click event, you tricker (ListView.Adapter as MyAdapter).NotifyDataSetChanged(); which will force the adapter to reload and refresh the data.
YOUR_DATASOURCE represents the point in your code where you store the informations like the url or text of all your items. This could typically be a database or something similar. While GetMyItems() is a method for example to query your database.
Hope this clears things up.
If I create a ListView in JavaFX like this:
ObservableList<String> elements = FXCollections.observableArrayList("John", "Doe");
ListView<String> lView = new ListView<String>(elements);
What I want to do is draw a line starting from the end of a row in the ListView, say from "John"
To do this, I need the location(x,y) of the row "John". Is it possible to get the location?
Update
This is a sample interface that I got using Swing and Piccolo2D. However, using that library is painful. I am wondering if I can do the same in JavaFX
It is possible, but it may not be as straight forward as you hoped. In order to determine the layout coordinates for a particular Cell within a ListView (or TableView/TreeView) you need to have access to that particular Cell object. The best way (and maybe only way in JavaFX 2.2) is to provide the container with a custom Cell and CellFactory that exposes each Cell. How you expose the Cell depends on what your triggers are for drawing the line.
Bases on your illustration, you'll need access to each cell once the ListViews are populated. You can do this with a List<ListCell<String>> field in the CellFactory. I'll mention one caveat here about ListCells. The ListViewSkin will reuse Cells whenever possible. That means that if you are going to try to populate and connect a list that ends up scrolling, then keeping your lines in the right place will be much more difficult. I'd recommend trying to ensure that all your list items fit on screen.
Below is an example with some notes in the comments. Take note that getting the correct coordinates for drawing your Line will probably require calculating the offset of your SceneGraph which I didn't do in this example.
package listviewcellposition;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewCellPosition extends Application {
// CustomCellFactory for creating CustomCells
public class CustomCellFactory implements
Callback<ListView<String>, ListCell<String>> {
List<ListCell<String>> allCells = new ArrayList<>();
#Override
public ListCell<String> call(final ListView<String> p) {
final CustomCell cell = new CustomCell();
allCells.add(cell);
return cell;
}
public List<ListCell<String>> getAllCells() {
return allCells;
}
}
// CustomCell is where the exposure occurs. Here, it's based on the
// Cell being selected in the ListView. You could choose a different
// trigger here but you'll need to explore.
public class CustomCell extends ListCell<String> {
// General display stuff
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item == null ? "" : item);
setGraphic(null);
}
}
}
#Override
public void start(Stage primaryStage) {
// This pane will contain the lines after they are created.
// I set it into an AnchorPane to avoid having to deal with
// resizing.
Pane linePane = new Pane();
AnchorPane pane = new AnchorPane();
pane.setPrefSize(100, 250);
AnchorPane.setBottomAnchor(linePane, 0.0);
AnchorPane.setLeftAnchor(linePane, 0.0);
AnchorPane.setRightAnchor(linePane, 0.0);
AnchorPane.setTopAnchor(linePane, 0.0);
pane.getChildren().add(linePane);
ListView<String> lView = new ListView<>();
lView.setPrefSize(100, 250);
CustomCellFactory lCellFactory = new CustomCellFactory();
lView.setCellFactory(lCellFactory);
ListView<String> rView = new ListView<>();
rView.setPrefSize(100, 250);
CustomCellFactory rCellFactory = new CustomCellFactory();
rView.setCellFactory(rCellFactory);
lView.getItems().addAll("Bill", "Doctor", "Steve", "Joanne");
rView.getItems().addAll("Seuss", "Rowling", "King", "Shakespeare");
HBox root = new HBox();
root.getChildren().addAll(lView, pane, rView);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
connectCells(lCellFactory, "Bill", rCellFactory, "Shakespeare", linePane);
connectCells(lCellFactory, "Doctor", rCellFactory, "Seuss", linePane);
connectCells(lCellFactory, "Steve", rCellFactory, "King", linePane);
connectCells(lCellFactory, "Joanne", rCellFactory, "Rowling", linePane);
}
// Looks up the ListCell<> for each String and creates a Line
// with the coordinates from each Cell. The calculation is very
// contrived because I know that all the components have the same
// x-coordinate. You'll need more complicated calculations if your
// containers are not aligned this way.
private void connectCells(CustomCellFactory lCellFactory, String lVal,
CustomCellFactory rCellFactory, String rVal, Pane linePane) {
List<ListCell<String>> lList = lCellFactory.getAllCells();
ListCell<String> lCell = null;
for (ListCell<String> lc : lList) {
if (lc.getItem() != null && lc.getItem().equals(lVal)) {
lCell = lc;
break;
}
}
List<ListCell<String>> rList = rCellFactory.getAllCells();
ListCell<String> rCell = null;
for (ListCell<String> rc : rList) {
if (rc.getItem() != null && rc.getItem().equals(rVal)) {
rCell = rc;
break;
}
}
if (lCell != null && rCell != null) {
double startY = lCell.getLayoutY() +
(lCell.getBoundsInLocal().getHeight() / 2);
double endY = rCell.getLayoutY() +
(rCell.getBoundsInLocal().getHeight() / 2);
Line line = new Line(0, startY,
linePane.getBoundsInParent().getWidth(), endY);
line.setStrokeWidth(2);
line.setStroke(Color.BLACK);
linePane.getChildren().add(line);
}
}
public static void main(String[] args) {
launch(args);
}
}