JavaFX Collection of Shapes - events

I am trying to create a JavaFX control that is essentially a bunch of interactable shapes. This is what I have so far:
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class ScenarioViewer extends Group {
// I want 1mm == 1px so scale everything so that 0.001 == 1px
private static final int DEFAULT_SCALE = 1000;
private static final int DEFAULT_SENSITIVITY = 100;
private double sensitivity;
private Rectangle testRect;
public ScenarioViewer() {
sensitivity = DEFAULT_SENSITIVITY;
testRect = new Rectangle(0.0, 0.0, 0.005, 0.01);
testRect.setStroke(Color.BLACK);
testRect.setFill(null);
testRect.setStrokeWidth(0.001);
getChildren().add(testRect);
setupScale();
setupEventHandlers();
}
private void scale(double change) {
setScaleX(getScaleX() + change);
setScaleY(getScaleY() + change);
}
private void translate(double x, double y) {
setTranslateX(getTranslateX() + x);
setTranslateY(getTranslateY() + y);
}
private void setupScale() {
setScaleX(DEFAULT_SCALE);
setScaleY(DEFAULT_SCALE);
}
private EventHandler<ScrollEvent> onScroll = new EventHandler<ScrollEvent>() {
public void handle(ScrollEvent event) {
scale(event.getDeltaY() * sensitivity);
}
};
private EventHandler<MouseEvent> onDrag = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
if(event.isPrimaryButtonDown()) {
translate(event.getX() * sensitivity,
event.getY() * sensitivity);
}
}
};
private void setupEventHandlers() {
addEventHandler(ScrollEvent.SCROLL, onScroll);
addEventHandler(MouseEvent.MOUSE_DRAGGED, onDrag);
}
}
The problem with the above is that the events are only triggered if the mouse is over the rectangle, more specifically, the edges of the rectangle as it is not filled. I want the opposite, such that, the events are only triggered when the mouse is not over a shape so that specific event handlers can be registered to each shape.
Why is this happening?
I believe it is because my control extends Group, and this relevant line in the documentation:
Any transform, effect, or state applied to a Group will be applied to
all children of that group. Such transforms and effects will NOT be
included in this Group's layout bounds, however if transforms and
effects are set directly on children of this Group, those will be
included in this Group's layout bounds.
Is there a more appropriate class to extend?
Update
I've changed the base class to Region, this makes events trigger properly but now the scaling and translations don't work like before. I have to instead iterate over everything and scale/translate each individual shape? Why does calling getScaleX/Y and setTranslateX/Y act differently between Region and Group?

On your group, call setPickOnBounds(true), this will allow the group to intercept any mouse/touch/input events within it's bounds.
specific event handlers can be registered to each shape.
Just invoke shape.setOnMouseClicked(eventHandler), etc (you can also use such setters on your enclosing group to handle events at the group level). You can consume the events in the event handler if you don't want them to bubble up to the enclosing group, similarly you can add filters to the parent group if you don't want the events to reach the children.
It may help if you review the Oracle tutorials on JavaFX event handling.

Related

JavaFX weird (Key)EventBehavior

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)

How can I set another value in property listener?

I want to speed-up my ScrollPane scrolling. I need something like:
scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> {
scrollPane.setVvalue(oldValue.doubleValue() + (newValue.doubleValue() - oldValue.doubleValue()) * 2);
});
but without stackowerflow exections and working..
May be there is a way to consume this like an event?
P.S. BTW, why does setOnScroll() fire only when scrolling reaches max (top) or min (bot) position?
I don't really recommend modifying a property while it is already changing, but if you want to do it you need to set a flag to suppress recursive calls. Here's an example:
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;
public class ModifyScrollSpeed extends Application {
#Override
public void start(Stage primaryStage) {
ScrollPane scrollPane = new ScrollPane(createContent());
DoublingListener.register(scrollPane.vvalueProperty());
Scene scene = new Scene(scrollPane, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private static class DoublingListener implements ChangeListener<Number> {
private boolean doubling ;
private Property<Number> target ;
private DoublingListener(Property<Number> target) {
this.target = target ;
}
public static void register(Property<Number> target) {
target.addListener(new DoublingListener(target));
}
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (! doubling) {
doubling = true ;
target.setValue(oldValue.doubleValue() + 2 * (newValue.doubleValue() - oldValue.doubleValue()));
}
doubling = false ;
}
}
private Node createContent() {
TilePane tilePane = new TilePane();
Random rng = new Random();
for (int i = 0; i < 200; i++) {
Region region = new Region();
region.setMinSize(40, 40);
region.setPrefSize(40, 40);
region.setMaxSize(40, 40);
region.setStyle(String.format("-fx-background-color: #%02x%02x%02x;",
rng.nextInt(256), rng.nextInt(256), rng.nextInt(256)));
tilePane.getChildren().add(region);
}
return tilePane ;
}
public static void main(String[] args) {
launch(args);
}
}
I don't actually see any other way to change the increment amounts for a scroll pane. Note that ScrollBar has API for changing the increment amounts via its unitIncrement and blockIncrement properties, however ScrollPane does not have equivalent properties.
There is a comment in the source code for ScrollPane which says
/*
* TODO The unit increment and block increment variables have been
* removed from the public API. These are intended to be mapped to
* the corresponding variables of the scrollbars. However, the problem
* is that they are specified in terms of the logical corrdinate space
* of the ScrollPane (that is, [hmin..hmax] by [vmin..vmax]. This is
* incorrect. Scrolling is a user action and should properly be based
* on how much of the content is visible, not on some abstract
* coordinate space. At some later date we may add a finer-grained
* API to allow applications to control this. Meanwhile, the skin should
* set unit and block increments for the scroll bars to do something
* reasonable based on the viewport size, e.g. the block increment
* should scroll 90% of the pixel size of the viewport, and the unit
* increment should scroll 10% of the pixel size of the viewport.
*/
The current skin for the scroll pane hard codes the unit and block increments for its scroll bars (in the updateHorizontalSB and updateVerticalSB methods) in the manner described in this comment (i.e. 10% and 90% of the visible amount), so I see no real way to get at these. In Java 9 the skin class will become a public class, and so at a minimum you could subclass it and modify this behavior.
You can try something like this -
private boolean scrolllChanging = false;
private void myScroll(ObservableDoubleValue observable, Double oldValue, Double newValue) {
if (!scrollChanging) {
try {
scrollChanging = true;
// Insert logic here. Any subsequent changes won't reach here until `scrollChanging` is set to false again.
} finally {
scrollChanging = false;
}
}
}
scrollPane.vvalueProperty().addListener(this::myScroll);
Forgive any minor type errors, I have not compiled this.

JavaFX Transition animation waiting

so quicky, I am doing program which demonstrate methods used for computer graph drawing. I need to create timeline or history of actions like ( placeVertex(x,y), moveVertex(newX,newY) etc. ) and iterate through (forward and backwards, automatically or manual)
I already achieved that by using command design pattern but few of these commands are using transitions. First idea was to use Condition interface's lock, await and signal in setOnFinished between each commands but it led to gui freezing.
I tryed SequentialTransition but it's no use for my problem - can't change properties dynamically between transitions.
Is there a possibility to somehow inform generation that one transition ended and next can run without GUI freezing and drawing?
Thanks!
edit: I ll try to simplify it all
Here is my Command interface and one of these commands:
public interface Command {
public void execute();
}
public class MoveVertex implements Command {
public MoveVertex(Data d, Vertex v, double changedX, double changedY){..};
#Override
public void execute() {
Path path = new Path();
path.getElements().add(new MoveTo(v.getCenterX(), v.getCenterY()));
path.getElements().add(new LineTo(changedX, changedY));
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(velocity));
pathTransition.setPath(path);
pathTransition.setNode(v.getVertex());
pathTransition.play(); }
}
These Commands are stored in my history class which is basically
private List<Command> history;
And I do going through the list and executing Commands
public boolean executeNext() {
if (history.size() != position) {
history.get(position).execute();
position++;
return true;
}
return false;
}
And I am trying to achieve state when next Command is started only if previous finished. Tryed to put await/signal in between without success.
The solution below uses Itachi's suggestion of providing an onFinished handler to move to a node to a new (random) location after we get to the next location.
It could probably be made more efficient (and simpler to understand) by re-using a single Transition rather than using recursion within the event handler. It is probably unnecessary to create a new Transition for each movement - but, as long as there aren't hundreds of thousands of movement iterations, it should be acceptable as is.
import javafx.animation.*;
import javafx.application.Application;
import javafx.event.*;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.Random;
// animates moving a node forever in a random walk pattern.
public class RandomWalk extends Application {
private static final Random random = new Random(42);
private static final double W = 200;
private static final double H = 200;
private static final double R = 10;
private static final Node node = new Circle(
R, Color.FORESTGREEN
);
#Override
public void start(Stage stage) {
// start from the center of the screen.
node.relocate(W / 2 - R, H / 2 - R);
stage.setScene(new Scene(new Pane(node), W, H));
stage.show();
walk();
}
// start walking from the current position to random points in sequence.
private void walk() {
final Point2D to = getRandomPoint();
final Transition transition = createMovementTransition(
node,
to
);
transition.setOnFinished(
walkFrom(to)
);
transition.play();
}
private EventHandler<ActionEvent> walkFrom(final Point2D from) {
return event -> {
// Finished handler might be called a frame before transition complete,
// leading to glitches if we relocate in the handler.
// The transition works by manipulating translation values,
// so zero the translation out before relocating the node.
node.setTranslateX(0);
node.setTranslateY(0);
// After the transition is complete, move the node to the new location.
// Relocation co-ordinates are adjusted by the circle's radius.
// For a standard node, the R adjustment would be unnecessary
// as most nodes are located at the top left corner of the node
// rather than at the center like a circle is.
node.relocate(
from.getX() - R,
from.getY() - R
);
// Generate the next random point and play a transition to walk to it.
// I'd rather not use recursion here as if you recurse long enough,
// then you will end up with a stack overflow, but I'm not quite sure
// how to do this without recursion.
final Point2D next = getRandomPoint();
final Transition transition = createMovementTransition(node, next);
transition.setOnFinished(walkFrom(next));
transition.play();
};
}
// We use a PathTransition to move from the current position to the next.
// For the simple straight-line movement we are doing,
// a straight TranslateTransition would have been fine.
// A PathTransition is just used to demonstrate that this
// can work for the generic path case, not just straight line movement.
private Transition createMovementTransition(Node node, Point2D to) {
Path path = new Path(
new MoveTo(
0,
0
),
new LineTo(
to.getX() - node.getLayoutX(),
to.getY() - node.getLayoutY()
)
);
return new PathTransition(
Duration.seconds(2),
path,
node
);
}
// #return a random location within a bounding rectangle (0, 0, W, H)
// with a margin of R kept between the point and the bounding rectangle edge.
private Point2D getRandomPoint() {
return new Point2D(
random.nextInt((int) (W - 2*R)) + R,
random.nextInt((int) (H - 2*R)) + R
);
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX 2.2: Hooking Slider Drag n Drop Events

I am trying to catch the events on the JavaFX Slider especially the one which indicates that the drag stopped and was released. At first I used the valueProperty with mock-up code like this
slider.valueProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> ov, Number oldValue, Number newValue) {
log.fine(newValue.toString());
}
});
but with this it update too often. So I searched within SceneBuilder and the API and found some interessting like
slider.setOnMouseDragReleased(new EventHandler<MouseDragEvent>() {
#Override
public void handle(MouseDragEvent event) {
System.out.println("setOnMouseDragReleased");
}
});
but they never get fired. There only some like setOnMouseReleased I get some output, but this for example count for the whole Node like the labels etc.
So my question is, which is the correct hook to know the value is not changing anymore (if possible after release of the mouse like drag'n'drop gesture) and maybe with a small example to see its interfaces working.
Add a change listener to the slider's valueChangingProperty to know when the slider's value is changing, and take whatever action you want on the value change.
The sample below will log the slider's value when it starts to change and again when it finishes changing.
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class SliderChangeLog extends Application {
private final ListView<String> startLog = new ListView<>();
private final ListView<String> endLog = new ListView<>();
#Override public void start(Stage stage) throws Exception {
Pane logsPane = createLogsPane();
Slider slider = createMonitoredSlider();
VBox layout = new VBox(10);
layout.setAlignment(Pos.CENTER);
layout.setPadding(new Insets(10));
layout.getChildren().setAll(
slider,
logsPane
);
VBox.setVgrow(logsPane, Priority.ALWAYS);
stage.setTitle("Slider Value Change Logger");
stage.setScene(new Scene(layout));
stage.show();
}
private Slider createMonitoredSlider() {
final Slider slider = new Slider(0, 1, 0.5);
slider.setMajorTickUnit(0.5);
slider.setMinorTickCount(0);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMinHeight(Slider.USE_PREF_SIZE);
slider.valueChangingProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(
ObservableValue<? extends Boolean> observableValue,
Boolean wasChanging,
Boolean changing) {
String valueString = String.format("%1$.3f", slider.getValue());
if (changing) {
startLog.getItems().add(
valueString
);
} else {
endLog.getItems().add(
valueString
);
}
}
});
return slider;
}
private HBox createLogsPane() {
HBox logs = new HBox(10);
logs.getChildren().addAll(
createLabeledLog("Start", startLog),
createLabeledLog("End", endLog)
);
return logs;
}
public Pane createLabeledLog(String logName, ListView<String> log) {
Label label = new Label(logName);
label.setLabelFor(log);
VBox logPane = new VBox(5);
logPane.getChildren().setAll(
label,
log
);
logPane.setAlignment(Pos.TOP_LEFT);
return logPane;
}
public static void main(String[] args) { launch(args); }
}
There could be times when you want to know when the user is moving the slider versus the slider value changing due to a binding to a property. One example is a slider that is used on a media player view to show the media timeline. The slider not only displays the time but also allows the user to fast forward or rewind. The slider is bound to the media player's current time which fires the change value on the slider. If the user moves the slider, you may want to detect the drag so as to stop the media player, have the media player seek to the new time and resume playing. Unfortunately the only drag event that seems to fire on the slider is the setOnDragDetected event. So I used the following two methods to check for a slider drag.
slider.setOnDragDetected(new EventHandler<Event>() {
#Override
public void handle(Event event) {
currentPlayer.pause();
isDragged=true;
}
});
slider.setOnMouseReleased(new EventHandler<Event>() {
#Override
public void handle(Event event) {
if(isDragged){
currentPlayer.seek(Duration.seconds((double) slider.getValue()));
currentPlayer.play();
isDragged=false;
}
}
});
jewelsea's answer was very helpful for setting me on the right track, however if "snapToTicks" is on, undesired behavior results. The "end" value as captured by jewelsea's listener is before the snap takes place, and the post-snap value is never captured.
My solution sets a listener on value but uses valueChanging as a sentinel. Something like:
slider.valueProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(
ObservableValue<? extends Number> observableValue,
Number previous,
Number now) {
if (!slider.isValueChanging()
|| now.doubleValue() == slider.getMax()
|| now.doubleValue() == slider.getMin()) {
// This only fires when we're done
// or when the slider is dragged to its max/min.
}
}
});
I found that checking for the max and min value was necessary to catch the corner case where the user drags the slider all the way past its left or right bounds before letting go of the mouse. For some reason, that doesn't fire an event like I'd expect, so this seems like an okay work-around.
Note: Unlike jewelsea, I'm ignoring the starting value for the sake of simplicity.
Note 2: I'm actually using ScalaFX 2, so I'm not sure if this Java translation compiles as-written.

Dispose of SWT shell when cursor moves out of it

I'm implementing a custom preview/tooltip for an Eclipse plug-in. It did it using a Shell in SWT, removing all its trimmings and placing a text box inside it. It looks great. However now I need to dispose of the shell when the cursor moves out of the shell window and I ran into some issues:
Does it make sense to attach a mousemoveListener to the shell? First I was doing this but then I realized that this listener only captures mouse move Events which occur inside the shell. How will I capture the mouse going out of the shell so as I could dispose of it?
Thanks and regards,
Krt_Malta
Attach a MouseTrackListener with MouseTrackAdapter as a listener and override mouseExit() method.
As another options, you can use AbstractHoverInformationControlManager from org.eclipse.jface.text, which handles all the nasty details (e.g. when you Alt+Tab out of the application, does your tooltip disappear?). The event handling is taken care of and you can focus on the interesting things. An example:
import org.eclipse.jface.text.AbstractHoverInformationControlManager;
import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
import org.eclipse.jface.text.DefaultInformationControl;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
public class Example {
public static void main(String[] args) {
final Display display = new Display();
final Shell shell = new Shell(display);
shell.setLayout(new FillLayout());
shell.setSize(200, 200);
final ExampleHoverInformationControlManager controlManager = new ExampleHoverInformationControlManager();
// This must be done for the control for which you want the mouse
// tracking to be enabled and you possibly want to show hovers.
controlManager.install(shell);
shell.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
controlManager.dispose();
}
});
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
}
private static class ExampleHoverInformationControlCreator extends
AbstractReusableInformationControlCreator {
#Override
protected IInformationControl doCreateInformationControl(Shell parent) {
return new DefaultInformationControl(parent);
}
}
private static class ExampleHoverInformationControlManager extends
AbstractHoverInformationControlManager {
protected ExampleHoverInformationControlManager() {
super(new ExampleHoverInformationControlCreator());
}
#Override
protected void computeInformation() {
MouseEvent e = getHoverEvent();
// Just a static example area for simplicity
if (e.x >= 0 && e.x < 100 && e.y >= 0 && e.y < 20) {
Rectangle area = new Rectangle(0, 0, 100, 20);
setInformation(
"This can be a string or something else, you control it", area); //$NON-NLS-1$
return;
}
// computeInformation must setInformation in all cases
setInformation(null, null);
}
}
}
I just discovered a much better solution. It runs like this.
Shell coverup = new Shell(SWT.NO_TRIM) ;
coverup.setBounds(parentComposite.getShell().getBounds());
coverup.moveAbove( Display.getCurrent().getActiveShell() );
coverup.setAlpha( 13 );
coverup.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
popupShell = new Shell( coverup, SWT.NO_TRIM );
coverup.addMouseMoveListener(new MouseMoveListener() {
#Override
public void mouseMove( MouseEvent mouseEvent )
{
System.out.println( "coverup - mouse moves" );
coverup.close() ;
}
});
Which in English is:
Create an invisible shell the same size as the application/parent shell.
Cover the parent shell with the invisible shell.
Attach a mouse Enter listener to the covering shell.
Create the actual popup as a child of, and on top of, the covering shell
When the mouse enters the popup, activate the coverup.
This means that no matter where the mouse goes after being in the popup, it enters the coverup shell - and the coverup mouse enter event disposes everything.
We are not detecting when we the mouse exits the popup - we are detecting when the mouse enters the surroundings.
Added bonus: setting the background of the coverup to a light gray, and a low Alpha, makes hte whole application subtly 'grayed-out' - so the user is aware is is disabled.
Small gotcha: if the popup shell is not wholly contained in the appwindow, where the popup shell goes outside the appwindow the mouse can escape without triggering the coverup shell.
Other than that - this works really well!

Resources