JavaFX: Right click on TableColumn disables resizing - java

Reproduced in OpenJFX 11.0.2 & 12.0.1 SDK (Windows 10, x64), not reproducible in JavaFX 8
Right-click on a table-column, then try to resize the column. No resize cursor is shown and column can't be resized until you manually click on the column again.
Any ideas for a workaround? I need to usecontextMenu for TableColumns, so potential workarounds that make the header ignore right mouse click aren't possible.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class Foo extends Application {
#Override
public void start(Stage stage) throws Exception {
TableView<Object> testView = new TableView<>();
testView.getColumns().addAll(new TableColumn<Object, Object>("C1"), new TableColumn<Object, Object>("C2"), new TableColumn<Object, Object>("C3"));
stage.setScene(new Scene(testView));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Ok I found the following (very, very dirty) workaround. I never tried this before because I assumend it would prevent the context menu from showing (as I noted in my original question), but apprently simply consuming the mouse event of every TableColumnHeader works and the context menu is still shown correctly (also works with TableColumns without context menus).
Not sure if anything internal could go wrong with this, but as the right click doesn't seem to be doing anything useful by default, I hope not.
Of course lookupAll needs to be called after it has been rendered.
Note 1: If you have TableMenuButtonVisible set to true, you need to do this every time a column is set to visible.
Note 2: Its getting dirtier and dirtier. Simply calling this again after a column has been set to visible (see note 1) doesn't always suffice (also not with a Platform.runLater call). I assume that's because the column header hasn't been rendered at that point. You either
need to wait until the Set<Node> is fully filled, i.e. the size of
it must be amountOfVisibleColumns + 1. If its equal to the amount
of visible columns, it won't work for the newly shown column.
or call layout() on the TableView before lookupAll
or if you have a class that extends TableView, override layoutChildren and execute the lookup if the amount of visible columns has changed
Note 3: You need to keep track of the old onMousePressed and execute it if the button isn't SECONDARY, otherwise the reordering of columns won't work.
Please let me know if you can think of any cleaner way.
import java.util.Set;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableColumnHeader;
import javafx.scene.input.MouseButton;
import javafx.stage.Stage;
public class Foo extends Application {
#Override
public void start(Stage stage) throws Exception {
TableView<Object> testView = new TableView<>();
testView.getColumns().addAll(createColumn("C1"), createColumn("C2"), createColumn("C3"));
stage.setOnShown(ev -> {
Set<Node> headers = testView.lookupAll("TableColumnHeader");
for (Node header : headers) {
if (header != null) {
((TableColumnHeader) header).setOnMousePressed(e -> {
if (e.getButton() == MouseButton.SECONDARY) {
e.consume();
}
});
}
}
});
stage.setScene(new Scene(testView));
stage.show();
}
private TableColumn<Object, Object> createColumn(String text) {
MenuItem item = new MenuItem("Context");
item.setOnAction(e -> {
System.out.println("Action");
});
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().add(item);
TableColumn<Object, Object> column = new TableColumn<>(text);
column.setContextMenu(contextMenu);
return column;
}
public static void main(String[] args) {
launch(args);
}
}

EDIT: Found the described bug in the Java bug tracker and filed a PR with the fix:
https://github.com/openjdk/jfx/pull/483
EDIT 2: My PR was accepted and merged back. The bug is fixed now, you can test it by using 17-ea+11. :-)
I have the same problem. This bug is caused by the mousePressedHandler added in TableColumnHeader. This class has even more problems, for example if I close a PopupControl with setConsumeAutoHidingEvents(true) by a click on a column, the sorting will be triggered. Those methods needs to be changed, maybe the addEventHandler methods should be used instead of the convenience setOn... methods.
I fixed it by consuming the event when I'm about to show my PopupControl:
public class MyTableColumnHeader extends TableColumnHeader {
public MyTableColumnHeader(TableColumnBase tc) {
super(tc);
addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed);
}
private void onMousePressed(MouseEvent mouseEvent) {
if (mouseEvent.getButton() == MouseButton.SECONDARY) {
showPopup();
// Consume here, so the column won't get 'stuck'.
mouseEvent.consume();
}
}
private void showPopup() {
...
}
}
Eventually, someone should open at least a bug. I may will also have a look in the not too distant future.

Related

Problem with extending SimpleStringProperty

I am just trying to extend a SimpleStringProperty in OpenJFX 11.0.1 to add some extra functionality. But ist seems not so easy, I experienced strange behavior of my extended Property and I don't know why. I think it should work.
My in this sample code simplified SimpleStringProperty extension contains another readonly string property which should be updated every time the the user types into a bound TextField. In this case remove all not allowed characters and convert the prefix. (I know this is not perfect but short enough to show)
After starting the sample code you will get a window with a rows of Controls. Typing in a String like "001 (242) 555666" the label should show the normalized phone number like "+1242555666".
The initial conversion works correcty.
I never get any exceptions.
The conversion is called when I type in new digits.
But if you play around with typing and deleting after a few seconds the set() method of my property isn't longer triggered by the bidirectional binding to the TextField.
To simplify the example I didn't use a TextFormatter. If I use one the problem doesn't change.
Can anyone help me figure out the problem?
Windows and OS X show the same behavior with OpenJFX 11 and OpenJFX 11.0.1
I tried the same code with JDK 1.8 and there it works fine.
package testproperty;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class TestProperty extends Application {
// attempt to create an own property
public class myPhoneNumberProperty extends SimpleStringProperty {
private final ReadOnlyStringWrapper normalizedNumber = new ReadOnlyStringWrapper("");
public ReadOnlyStringProperty normalizedNumberProperty() { return normalizedNumber.getReadOnlyProperty(); }
public String getNormalizedNumber() { return normalizedNumber.get(); }
public myPhoneNumberProperty() {
super();
}
public myPhoneNumberProperty(String s) {
super(s);
calculate();
}
#Override
public void set(String s) {
super.set(s);
calculate();
}
private void calculate() {
// some calculations (only for test purposes)
String original = this.get();
String result = original.replaceAll("[^0123456789]","");
if (result.startsWith("00")) result = result.replaceFirst("00", "+");
if (original.startsWith("+")) result = "+".concat(result);
normalizedNumber.set(result);
}
}
#Override
public void start(Stage primaryStage) {
// create my property
myPhoneNumberProperty phoneNumberA = new myPhoneNumberProperty("+34 952 111 222");
// set up grid pane
GridPane grid = new GridPane();
grid.setPadding(new Insets(5,5,5,5));
grid.setVgap(20);
grid.setHgap(20);
// set up the row
Label labelA = new Label("Enter phone number");
TextField textFieldA = new TextField();
textFieldA.textProperty().bindBidirectional(phoneNumberA);
Label labelB = new Label("Normalized number");
Label labelN = new Label();
labelN.textProperty().bind(phoneNumberA.normalizedNumberProperty());
grid.addRow(0, labelA, textFieldA, labelB, labelN);
// complete scene
Scene scene = new Scene(grid, 1000, 100);
primaryStage.setTitle("PhoneNumberProperty TestProg");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Your phoneNumberA property object is being garbage collected. To fix this you must keep a strong reference to the object. One option is to make it an instance field.
JavaFX implements bindings using weak listeners/references. Bidirectional bindings have no strong references to the other property. This is different from unidirectional bindings where a reference to the observable value must be kept in order to unbind from it later.

JavaFX ComboBox becomes unclickable after removal and re-adding

I think I perhaps have found a bug in Java, or maybe I am doing something wrong.
I populate a container based on some received data. The container has one or more ComboBoxes. On ComboBox selection change I receive new data. I then clear the GridPane and re-add the nodes (that still exist in the new data, and/or add new nodes).
The ComboBox still has focus, but I am unable to activate it again on click. Anything which causes the ComboBox to lose focus (such as focusing another component) will cause it to work again.
This is an simplified example. Tried with jdk1.8.0_162 and jdk-9.0.4
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxTest extends Application {
public static void main(String[] args) {
ComboBoxTest.launch(args);
}
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
root.getChildren().clear();
root.getChildren().add(choices);
});
Platform.setImplicitExit(true);
stage.setScene(new Scene(root));
stage.show();
}
}
The design is dynamic. I have a list of values received from a server. This is used to create and place ComboBox on a grid. When the user changes a selection in a ComboBox it receive a new list of values from the server. This list may still contain values that corresponds to existing nodes in the grid. They are reused rather than re-created.
Just to not loose reason and solution posted as comment to the deleted answer by sillyfly (post your own and I'll delete this :)
A little guess as to the underlying cause/issue - the change causes the ComboBox to disappear while its list (which is technically a different stage) is showing. My guess is that leaves it in an indefinite state where it thinks the list is still showing, but it never hides so it doesn't reset. In this case, maybe calling ComboBox::hide will also work
This assumption is correct as you can see if you change the selection by keyboard (in which case the dropdown is not open): the combo is still accessible by keyboard and mouse. So hiding the dropdown before removing indeed is the solution.
In code (the simplified example in the Michael's edit)
public class ReaddFocusedCombo extends Application {
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
// guess by sillyfly: combo gets confused if popup still open
choices.hide();
root.getChildren().clear();
root.getChildren().add(choices);
// suggested in answer: working but then the choice isn't focused
//root.requestFocus();
// doesn't work
// choices.requestFocus();
});
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Update: a little search in the bug parade turned up a similar misbehaviour on adding a showing combo which was fixed on initial attaching to a scene, but missed the dynamic use case. Filed a new issue for the latter.

JavaFX TextField cancelEdit not working as expected

I have a TextField and I would like the contents of the field to be restored to their previous value when I press Esc which is expected behaviour on most systems (I have no idea why JavaFX doesn't do this by default).
To this end I tried to use TextField.cancelEdit() but it appears that this is doing nothing.
Here is a SSCCE
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.stage.Stage;
public class UiTest2Controller extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage aPrimaryStage) throws Exception {
final TextField field = new TextField();
field.setOnAction(aEvent -> {
System.out.println("Action");
});
field.setOnKeyPressed(aEvent -> {
if (aEvent.getCode() == KeyCode.ESCAPE) {
System.out.println("Escape");
field.cancelEdit();
field.getParent().requestFocus();
}
});
field.setPromptText("Hello World...");
aPrimaryStage.setScene(new Scene(new Group(field)));
aPrimaryStage.show();
}
}
Steps to reproduce:
Type something in the text field
Press Esc
Expected behaviour: The field returns to previous value (empty first time) and focus is lost.
Actual behaviour: The fiels retains its value at the time and focus is lost.
I am not sure about the specifics, but it seems to work as expected if the TextField has a TextFormatter assigned:
field.setTextFormatter(new TextFormatter<>(TextFormatter.IDENTITY_STRING_CONVERTER));

JavaFX Choiceox change not updating graphics

When I change the underlying observable array list the graphics choice box doesn't update. There must be a newer solution than what I have seen suggested here for example:
JavaFX: Update of ListView if an element of ObservableList changes
int selected = productsChoiceBox.getSelectionModel().getSelectedIndex();
Product prod = products.get(selected);
prod.setName(productName.getText());
prod.setUrl(productUrl.getText());
Any thoughts? I would like to avoid removing and adding.
The "standard" answer is to use an ObservableList with an extractor. However, when I tested this out, it didn't behave as advertised, and it seems like there is a bug (my guess is that ChoiceBox is not correctly handling wasUpdated type changes fired in its ListChangedListener) which I will report at JIRA. Update: filed report at https://javafx-jira.kenai.com/browse/RT-38394
The factory method FXCollections.observableArrayList(Callback) creates an (empty) observable array list. The provided Callback is a function that maps each element in the list to an array of Observables. The list registers listeners with those observables, and if those properties change, the list fires update notifications to its listeners.
This produces strange results with a ChoiceBox, however; one possible workaround would be to use a ComboBox which seems to work fine.
Here's some sample code. Select an item: then type in the text field and press enter to change the name of the selected item. Change ChoiceBox to ComboBox to see the correct behavior:
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ChoiceBoxUpdateExample extends Application {
#Override
public void start(Stage primaryStage) {
ChoiceBox<Item> choiceBox = new ChoiceBox<>();
ObservableList<Item> items = FXCollections.observableArrayList(
item -> new Observable[] {item.nameProperty()}); // the extractor
items.addAll(
IntStream.rangeClosed(1, 10)
.mapToObj(i -> new Item("Item "+i))
.collect(Collectors.toList()));
choiceBox.setItems(items);
TextField changeSelectedField = new TextField();
changeSelectedField.disableProperty()
.bind(Bindings.isNull(choiceBox.getSelectionModel().selectedItemProperty()));
changeSelectedField.setOnAction(event ->
choiceBox.getSelectionModel().getSelectedItem().setName(changeSelectedField.getText()));
BorderPane root = new BorderPane();
root.setTop(choiceBox);
root.setBottom(changeSelectedField);
Scene scene = new Scene(root, 250, 150);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
public final StringProperty name = new SimpleStringProperty();
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty().set(name);
}
public Item(String name) {
setName(name);
}
#Override
public String toString() {
return getName();
}
}
public static void main(String[] args) {
launch(args);
}
}
The correct and proper answer is from James_D, but if you REALLY want to use ChoiceBox, then try adding and removing:
int selected = productsChoiceBox.getSelectionModel().getSelectedIndex();
products.remove(selected);
products.add(selected, prod);
I do NOT believe this is the right way, but I tested it, and it does work. The ChoiceBox stays on the the removed and selected index and looks like it updates.

Listening on system clipboard using JavaFx

As stated in the answer to this question, one can setup a Timeline to check whether there is a change in the system clipboard:
Set and use variables outside timeline in javafx 8
But is there a better way? For example, an event listener? I have searched JavaFx 8 doc and didn't find anything obviously helpful.
Solutions using JavaFx is preferred, but all answers are welcome.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
final Clipboard systemClipboard = Clipboard.getSystemClipboard();
new com.sun.glass.ui.ClipboardAssistance(com.sun.glass.ui.Clipboard.SYSTEM) {
#Override
public void contentChanged() {
System.out.print("System clipboard content changed: ");
if ( systemClipboard.hasImage() ) {
System.out.println("image");
} else if ( systemClipboard.hasString() ) {
System.out.println("string");
} else if ( systemClipboard.hasFiles() ) {
System.out.println("files");
}
}
};
primaryStage.setScene(new Scene(new StackPane()));
primaryStage.show();
}
}
Test:
Press key Print Screen
Ctrl+C for selected string
Ctrl+c for selected files
No, there isn't a better way. There are no events sent round when the clipboard contents change (especially if it's from outside of the Java application) and so only a polling approach is appropriate.

Categories