JavaFX removeEventHandler not working as expected - javafx

I'm trying to remove the event handler from a FontAwesomeIconView object but it's not working. The event handler keeps working and I don't know why.
EventHandler<MouseEvent> glowIcon = (e) -> {
FontAwesomeIconView icon = (FontAwesomeIconView) e.getSource();
icon.setFill(Color.web("#ffb521"));
scene.setCursor(Cursor.HAND);
};
EventHandler<MouseEvent> unglowIcon = (e) -> {
FontAwesomeIconView icon = (FontAwesomeIconView) e.getSource();
icon.setFill(Color.web("#000000"));
scene.setCursor(Cursor.DEFAULT);
};
if (Session.getSession().isProjectCreator()) {
newIcon.setFill(Color.web("#000000"));
newIcon.addEventHandler(MouseEvent.MOUSE_ENTERED, glowIcon);
newIcon.addEventHandler(MouseEvent.MOUSE_EXITED, unglowIcon);
}else {
newIcon.setFill(Color.web("#e8e8e8")); //It's changed to this color
newIcon.removeEventHandler(MouseEvent.MOUSE_ENTERED, glowIcon);
newIcon.removeEventHandler(MouseEvent.MOUSE_EXITED, unglowIcon);
}
In the beginning the event handlers are ADDED, but when I remove them they keep working, when they shouldn't.

Without seeing a minimal reproducible example it is hard to tell. The most likely scenario from the code that you have posted is that glowIcon/unglowIncon are pointing at different lambdas when you add the handler vs remove it.
Every time this code runs it will assign a different lambda reference to those variables, even if everything is exactly the same.
EventHandler<MouseEvent> glowIcon = (e) -> {
FontAwesomeIconView icon = (FontAwesomeIconView) e.getSource();
icon.setFill(Color.web("#ffb521"));
scene.setCursor(Cursor.HAND);
};
The solution is to make sure they are only called once and the reference to them is kept in your controller for as long as they are needed. Using final is a good defensive technique to make sure you don't accidentally reassign it somewhere and lose the reference you need to remove the handler.
Something like:
public class GlowController {
private final EventHandler<MouseEvent> glowIcon = (e) -> {
FontAwesomeIconView icon = (FontAwesomeIconView) e.getSource();
icon.setFill(Color.web("#ffb521"));
scene.setCursor(Cursor.HAND);
};
private final EventHandler<MouseEvent> unglowIcon = (e) -> {
FontAwesomeIconView icon = (FontAwesomeIconView) e.getSource();
icon.setFill(Color.web("#000000"));
scene.setCursor(Cursor.DEFAULT);
};
public void doSomething() {
if (Session.getSession().isProjectCreator()) {
newIcon.setFill(Color.web("#000000"));
newIcon.addEventHandler(MouseEvent.MOUSE_ENTERED, glowIcon);
newIcon.addEventHandler(MouseEvent.MOUSE_EXITED, unglowIcon);
}else {
newIcon.setFill(Color.web("#e8e8e8")); //It's changed to this color
newIcon.removeEventHandler(MouseEvent.MOUSE_ENTERED, glowIcon);
newIcon.removeEventHandler(MouseEvent.MOUSE_EXITED, unglowIcon);
}
}
}

In case someone stumbles upon this when trying to figure out why removing event listeners add with:
vbox.setOnMouseClicked(eventHandler);
do not get removed with removeEventHandler. It turns out that you need to add your event handler using:
vbox.addEventHandler(MouseEvent.MOUSE_CLICKED, eventHandler);
for the remove call to work.
I would have thought that it would work regardless of the way you added your handler, but it doesn't :|

Related

How do I stop TextArea from listening to Shortcut KeyCombinations as KeyEvents?

Just as the title says, how do I stop shortcut keys (accelerators) being picked up as key events in TextArea? I have tried the method suggested here with different modifications: TextArea ignore KeyEvent in JavaFX with no luck.
If you want to stop specific accelerators from working when the TextArea has focus simply add an event filter for KEY_PRESSED events.
public class AcceleratorFilter implements EventHandler<KeyEvent> {
// blacklist of KeyCombinations
private final Set<KeyCombination> combinations;
public AcceleratorFilter(KeyCombination... combinations) {
this.combinations = Set.of(combinations);
}
#Override
public void handle(Event event) {
if (combinations.stream().anyMatch(combo -> combo.match(event)) {
event.consume();
}
}
}
TextArea area = new TextArea();
area.addEventFilter(KeyEvent.KEY_PRESSED, new AcceleratorFilter(
KeyCombination.valueOf("shortcut+o"),
KeyCombination.valueOf("shortcut+s") // etc...
));
If you want to indiscriminately block all accelerators registered with the Scene then you can query the Scenes accelerators and consume the KeyEvent if appropriate.
TextArea area = new TextArea();
area.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var scene = ((Node) event.getSource()).getScene();
// #getAccelerators() = ObservableMap<KeyCombination, Runnable>
var combos = scene.getAccelerators().keySet();
if (combos.stream().anyMatch(combo -> combo.match(event)) {
event.consume();
}
});
This latter option may cause issues if you're not careful. For instance, if you have a default Button in the Scene then the above event filter may interfere with the ENTER key. Also, this option won't necessarily stop things like shortcut+c, shortcut+v, etc. because those shortcuts are registered with the TextInputControl, not the Scene.

Javafx : ComboBoxTableCell - how to select a value in one click?

I have a TableView with a ComboBoxTableCell, when using the default implementation the user have to click three times to select a value from of the ComboBox's list.
I want when the user clicks on the cell to show the combo box list. I based my solution on this one:
JavaFX editable ComboBox in a table view
The cell does get into edit mode (startEdit() is called) but it takes another click to show the list of values, what am I missing?
table.addEventHandler(MouseEvent.MOUSE_CLICKED, (e) ->
{
if (table.getEditingCell() == null)
{
TablePosition focusedCellPos = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPos.getRow(), focusedCellPos.getTableColumn());
}
});
Thanks.
Interesting problem - bubbling up again after quite a while :)
Looks like the approach of the OP is indeed working (as of fx11, some bugs around its editing seem to be fixed) - with a little help from the combo cell:
start editing in a single click handler on the tableView (from OP)
extend ComboBoxTableCell and override its startEdit to open the dropDown
Code snippet:
// set editable to see the combo
table.setEditable(true);
// keep approach by OP
table.addEventHandler(MouseEvent.MOUSE_CLICKED, (e) -> {
TablePosition<Person, ?> focusedCellPos = table.getFocusModel()
.getFocusedCell();
if (table.getEditingCell() == null) {
table.edit(focusedCellPos.getRow(),
focusedCellPos.getTableColumn());
}
});
// use modified standard combo cell shows its popup on startEdit
firstName.setCellFactory(cb -> new ComboBoxTableCell<>(firstNames) {
#Override
public void startEdit() {
super.startEdit();
if (isEditing() && getGraphic() instanceof ComboBox) {
// needs focus for proper working of esc/enter
getGraphic().requestFocus();
((ComboBox<?>) getGraphic()).show();
}
}
});
Maybe not the cleanest solution to this problem, but I found a workaround to make the ComboBoxTableCells drop down its menu in just 1 click:
column.setCellFactory(new Callback<TableColumn<Person, String>, TableCell<Person, String>>() {
#Override
public TableCell<Person, String> call(TableColumn<Person, String> column) {
ComboBoxTableCell cbtCell = new ComboBoxTableCell<>(cbValues);
cbtCell.setOnMouseEntered(new EventHandler<Event>() {
#Override
public void handle(Event event) {
// Without a Person object, a combobox shouldn't open in that row
if (((Person)((TableRow)cbtCell.getParent()).getItem()) != null) {
Robot r = new Robot();
r.mouseClick(MouseButton.PRIMARY);
r.mouseClick(MouseButton.PRIMARY);
}
}
});
return cbtCell;
}
});
PS: I know that this topic is a bit old, but I also stumbled upon this problem recently and could not find any working solution to it online. As I sad, it's not the cleanest workaround, but at least it does its job. ;)

JavaFX - Progress properties don't work?

I'm trying to get the progress value of my Picture Viewer when another picture is loading. I've tried two simple ways to do it, but it didn't work out for me.
First I was using the progressProperty from the Image class:
public boolean nextImageClicked()
{
if(PICTURE_INDEX < picturePaths.size() - 1)
{
String path = picturePaths.get(PICTURE_INDEX + 1).toURI().toString();
Image newImage = new Image(path);
newImage.progressProperty().addListener((observable, oldValue, newValue) -> System.out.println("Current progress: "+newValue));
GUI.getImageView().setImage(newImage);
adjustImageViewBounds();
PICTURE_INDEX += 1;
return true;
}
else return false;
}
It didn't print out anything on the console because the progress value doesn't change bizarrely. So I've tried to put all the work stuff in a Task<Void> and getting the progress value through the Task:
public boolean nextClicked()
{
if(PICTURE_INDEX < picturePaths.size() - 1)
{
Task<Void> task = new Task<Void>()
{
#Override protected Void call() throws Exception
{
String path = picturePaths.get(PICTURE_INDEX + 1).toURI().toString();
Image newImage = new Image(path);
GUI.getImageView().setImage(newImage);
adjustImageViewBounds();
PICTURE_INDEX += 1;
return null;
}
};
task.setOnRunning(e -> System.out.println(task.getProgress()));
task.progressProperty().addListener((observable, oldValue, newValue) ->
{
System.out.println(newValue);
});
task.run();
return true;
}
else return false;
}
Also didn't work out as hoped.
task.setOnRunning(e -> System.out.println(task.getProgress()));
I implemented this to see the default value, it printed out "-1".
What have I to change to let the console return single progress values like "0.1", "0.14" ?
You need the Image to load in the background, so that the call to the constructor returns before the image is completely loaded. By default, it will block until it is loaded (so the progress property will be 1 by the time you add the listener to it):
public boolean nextImageClicked()
{
if(PICTURE_INDEX < picturePaths.size() - 1)
{
String path = picturePaths.get(PICTURE_INDEX + 1).toURI().toString();
// note additional parameter:
Image newImage = new Image(path, true);
newImage.progressProperty().addListener((observable, oldValue, newValue) -> System.out.println("Current progress: "+newValue));
GUI.getImageView().setImage(newImage);
adjustImageViewBounds();
PICTURE_INDEX += 1;
return true;
}
else return false;
}
For a Task's progress to change, you need to explicitly call updateProgress(...) on the task. The only way to know what to pass in would be to observe the image's progress and pass it to the task's progress, so you would just have a more convoluted version of the code above. This is not a good use case for a task, since Image already supports background loading out of the box.
Don't try to do this on your own. You've got no idea, how much of the image has been loaded, unless you find the size of the image before loading and load the image from a steam observing the progress of the stream, which would be unnecessarily complicated. BTW: The Image constructor you use returns when the image is completely loaded. You can specify the image to be loaded asynchronically by using the right constructor however. Image provides a progress property to observe the loading progress:
#Override
public void start(Stage primaryStage) {
ImageView iv = new ImageView();
ProgressBar pb = new ProgressBar();
Button btn = new Button("Load Image");
btn.setOnAction((ActionEvent event) -> {
// ca. 6 MB image loaded from web
Image image = new Image("http://eoimages.gsfc.nasa.gov/images/imagerecords/79000/79793/city_lights_africa_8k.jpg", true);
pb.progressProperty().bind(image.progressProperty());
iv.setImage(image);
});
ScrollPane sp = new ScrollPane(iv);
VBox.setVgrow(sp, Priority.ALWAYS);
VBox root = new VBox(btn, pb, sp);
root.setFillWidth(true);
Scene scene = new Scene(root);
primaryStage.setMaximized(true);
primaryStage.setScene(scene);
primaryStage.show();
}

javafx have an eventfilter remove itself

A snippet of my code is as follows:
//button clicked method
#FXML
public void newHeatExchanger2ButtonClicked(ActionEvent event) throws Exception {
//new pane created
Pane pane = new Pane();
//method call everytime the button is clicked
create2DExchanger(pane);
}
//method declaration
private void create2DExchanger(Pane pane) {
EventHandler<MouseEvent> panePressed = (e -> {
if (e.getButton() == MouseButton.SECONDARY){
do stuff
}
if (e.getButton() == MouseButton.PRIMARY){
do stuff
}
});
EventHandler<MouseEvent> paneDragged = (e -> {
if (e.getButton() == MouseButton.PRIMARY){
do stuff
}
});
EventHandler<MouseEvent> paneReleased = (e -> {
if (e.getButton() == MouseButton.PRIMARY){
do stuff;
}
});
EventHandler<MouseEvent> paneMoved = (t -> {
do stuff;
});
EventHandler<MouseEvent> paneClicked = (t -> {
//I need this filter to remove itself right here
t.consume();
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
});
pane.removeEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.removeEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.removeEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
pane.addEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventFilter(MouseEvent.MOUSE_PRESSED, paneClicked);
}
Initially I set the pane to have only the event filters of mouse_moved, and mouse_pressed. As soon as the mouse is clicked I need the mouse filter for mouse_pressed and mouse_moved to go away and add the eventHandlers as I do in the paneClicked filter llamda. I need the first set of events to be filters because there are children nodes I do not want to receive the event (i.e. an event filter on an arc that is a child of the pane). The second set need to be handlers because the arc event filter needs to consume the event before the pane eventHandlers receive it.
Convenience events like:
pane.setOnMousePressed()
can remove themselves by calling
pane.setOnMousePressed(null);
but I need this initial event filter to remove itself. I will need the functionality of an event removing itself later as well in the code, but if I try to add
pane.removeEventFilter(MouseEvent.MOUSE_PRESSED, paneClicked);
to
EventHandler<MouseEvent> paneClicked = (t -> {
//I need this filter to remove itself right here
t.consume();
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
});
It will not compile. I have been researching for a couple of days on how to get the functionality of an eventFilter or eventHandler to remove itself but I am coming up short. I have not found anything online or on stackexchange in my Google searches either. I would really appreciate being able to figure this thing out. Thanks.
I believe the problem stems from the fact that you try to access paneClicked before it was fully declared.
You can overcome this using an anonymous class with the this keyword:
EventHandler<MouseEvent> paneClicked = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
event.consume();
pane.removeEventFilter(MouseEvent.MOUSE_PRESSED, this);
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
}
};
Or by referencing a fully qualified static function:
public class ContainingClass {
...
private static EventHandler<MouseEvent> paneClicked = (t -> {
t.consume();
pane.removeEventFilter(
MouseEvent.MOUSE_PRESSED, ContainingClass.paneClicked
);
});
}
See also:
Why do lambdas in Java 8 disallow forward reference to member variables where anonymous classes don't?
Java8 Lambdas vs Anonymous classes

TranslateTransition in JavaFX does nothing

I'm trying to use a TranslateTransition object in JavaFX to move an onscreen object in a LOGO program I am building. I have an onscreen TurtleDisplay object, which extends ImageView, and this is what I'm trying to move. The code to move it is here:
public void drawTurtle(TurtleData currentData) {
TurtleImage inList = getTurtleImage(currentData);
if (inList==null) {
TurtleImage temp = new TurtleImage(currentData.getX(),
currentData.getY(), currentData.getHeading(), turtleImage);
myTurtlesGroup.getChildren().add(temp);
myTurtlesList.add(temp);
}
else {
TranslateTransition tt = new TranslateTransition(Duration.seconds(3),inList);
tt.setFromX(inList.getX());
tt.setFromY(inList.getY());
tt.setToX(inList.getX()+currentData.getX());
tt.setToY(inList.getY()+currentData.getY());
tt.setCycleCount(Timeline.INDEFINITE);
tt.play();
}
}
This code, which is part of the front end, is called from the back end via a Listener on an ObservableList. The backend contains this ObservableList of TurtleData objects that contain the information necessary to move a turtle on screen -- which turtle to move, the coordinate to move to, and the rotation of the turtle. The code to call this is here:
ObservableList<TurtleData> myTurtles = FXCollections
.observableArrayList();
myTurtles.addListener(new ListChangeListener<TurtleData>() {
#Override
public void onChanged(Change<? extends TurtleData> c) {
myDisplay.getSelectedWorkspace().getTV().clearTurtles();
while (c.next()) {
for (TurtleData addItem : c.getAddedSubList()) {
myDisplay.getSelectedWorkspace().getTV().drawTurtle(addItem);
}
}
}
});
I have stepped through with a debugger and ensured that this code is called -- specifically, the tt.play() line is run. Nothing moves on screen. Does anyone have any idea what is wrong? Do I need to setup an Animation Timeline? Thank you for any help!

Resources