CONTEXT :
I have redundant code I want to factorize. The code allows a Node
descendant to fire a DOUBLE click EventHandler
, without firing a SINGLE click EventHandler
.
I need this feature in different ButtonBase
implementations :
Button
ToggleButton
CheckBox
For the moment, I have extended each leaf class written above with the same code allowing this feature.
PROBLEM :
I know this is smelling very bad, and I would like to factorize it, at least, to the common parent -> ButtonBase
... if not Node
where it should origin from (it is in Node
that the onMouseClicked(...)
is implemented).
[
I have thus implemented an abstract class ButtonBaseCustom
extending ButtonBase.
Knowing, and understanding, that multiple inheritage does not exist in Java, partially due to the diamond problem, I tried to follow this answer from @SCB, using :
Both as interfaces (I created
ButtonBaseCustomInterface
&ButtonInterface
for this),ButtonBaseCustomInterface
as interface, andButton
as parent (extending from it)ButtonInterface
as interface andButtonBaseCustom
as parent (extending from it)
QUESTION :
How do I get a ButtonCustom having the parent abilities implemented in ButtonBaseCustom, and still acting as a Button ?
CLOSEST SOLUTION :
I have a very high instinct that the architecture N°3 here above is the one to go with.
The next code compiles and runs... even gives me the correct behavior of SINGLE/DOUBLE click caught... But there is an issue with the SKIN part : and that's the top of the iceberg I suppose. Maybe it smells like I'm in the wrong direction ?
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.skin.ToggleButtonSkin;
public class ToggleButtonWithDblClick extends ButtonBaseCutom implements ToggleButtonInterface {
ToggleButton tglBtn = null;
public ToggleButtonWithDblClick() {
super();
tglBtn = new ToggleButton();
}
public ToggleButtonWithDblClick(String text) {
super(text);
tglBtn = new ToggleButton(text);
}
public ToggleButtonWithDblClick(String text, Node graphic) {
super(text, graphic);
tglBtn = new ToggleButton(text, graphic);
}
@Override
public ToggleGroup getToggleGroup() {
return tglBtn.getToggleGroup();
}
@Override
public boolean isSelected() {
return tglBtn.isSelected();
}
@Override
public BooleanProperty selectedProperty() {
return tglBtn.selectedProperty();
}
@Override
public void setSelected(boolean value) {
tglBtn.setSelected(value);
}
@Override
public void setToggleGroup(ToggleGroup value) {
tglBtn.setToggleGroup(value);
}
@Override
public ObjectProperty<ToggleGroup> toggleGroupProperty() {
return tglBtn.toggleGroupProperty();
}
@Override
public void fire() {
tglBtn.fire();
}
@Override
public Skin<?> createDefaultSkin() {
tglBtn.setSkin(new ToggleButtonSkin(tglBtn));
return tglBtn.getSkin();
}
}
EXAMPLE
For example, here is a full code of working button :
package application;
import java.util.concurrent.CompletableFuture;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class DblClickCatchedWithoutSingleClick extends Application {
public class ButtonWithDblClick extends Button {
private long singleClickDelayMillis = 250;
private ClickRunner latestClickRunner = null;
private ObjectProperty<EventHandler<MouseEvent>> onMouseSingleClickedProperty = new SimpleObjectProperty<>();
private ObjectProperty<EventHandler<MouseEvent>> onMouseDoubleClickedProperty = new SimpleObjectProperty<>();
// CONSTRUCTORS
public ButtonWithDblClick() {
super();
addClickedEventHandler();
}
public ButtonWithDblClick(String text) {
super(text);
addClickedEventHandler();
}
public ButtonWithDblClick(String text, Node graphic) {
super(text, graphic);
addClickedEventHandler();
}
private class ClickRunner implements Runnable {
private final Runnable onClick;
private boolean aborted = false;
public ClickRunner(Runnable onClick) {
this.onClick = onClick;
}
public void abort() {
this.aborted = true;
}
@Override
public void run() {
try {
Thread.sleep(getSingleClickDelayMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!aborted) {
Platform.runLater(onClick::run);
}
}
}
private void addClickedEventHandler() {
//Handling the mouse clicked event (not using 'onMouseClicked' so it can still be used by developer).
addEventHandler(MouseEvent.MOUSE_CLICKED, me -> {
switch (me.getButton()) {
case PRIMARY:
if (me.getClickCount() == 1) {
latestClickRunner = new ClickRunner(() -> {
System.out.println("ButtonWithDblClick : SINGLE Click fired");
onMouseSingleClickedProperty.get().handle(me);
});
CompletableFuture.runAsync(latestClickRunner);
}
if (me.getClickCount() == 2) {
if (latestClickRunner != null) {
latestClickRunner.abort();
}
System.out.println("ButtonWithDblClick : DOUBLE Click fired");
onMouseDoubleClickedProperty.get().handle(me);
}
break;
case SECONDARY:
// Right-click operation. Not implemented since usually no double RIGHT click needs to be caught.
break;
default:
break;
}
});
}
public void setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseSingleClickedProperty.set(eventHandler);
}
public void setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseDoubleClickedProperty.set(eventHandler);
}
public long getSingleClickDelayMillis() {
return singleClickDelayMillis;
}
public void setSingleClickDelayMillis(long singleClickDelayMillis) {
this.singleClickDelayMillis = singleClickDelayMillis;
}
}
public void start(Stage stage) {
VBox root = new VBox();
Button btn = new Button("Double click me");
btn.setOnMousePressed(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 1) {
System.out.println("Button clicked");
} else if (mouseEvent.getClickCount() == 2)
System.out.println("Button double clicked");
});
ButtonWithDblClick btn2 = new ButtonWithDblClick("Double click me too ;-)");
btn2.setOnMouseSingleClicked(me -> {
System.out.println("BUTTON_2 : Fire SINGLE Click");
});
btn2.setOnMouseDoubleClicked(me -> {
System.out.println("BUTTON_2 : Fire DOUBLE Click");
});
root.getChildren().add(btn);
root.getChildren().add(btn2);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
CUSTOM BUTTON_BASE :
import java.util.concurrent.CompletableFuture;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.ButtonBase;
import javafx.scene.input.MouseEvent;
public abstract class ButtonBaseCutom extends ButtonBase {
private long singleClickDelayMillis = 250;
private ClickRunner latestClickRunner = null;
private ObjectProperty<EventHandler<MouseEvent>> onMouseSingleClickedProperty = new SimpleObjectProperty<>();
private ObjectProperty<EventHandler<MouseEvent>> onMouseDoubleClickedProperty = new SimpleObjectProperty<>();
// CONSTRUCTORS
protected ButtonBaseCutom() {
super();
addClickedEventHandler();
}
protected ButtonBaseCutom(String text) {
super(text);
addClickedEventHandler();
}
protected ButtonBaseCutom(String text, Node graphic) {
super(text, graphic);
addClickedEventHandler();
}
private class ClickRunner implements Runnable {
private final Runnable onClick;
private boolean aborted = false;
public ClickRunner(Runnable onClick) {
this.onClick = onClick;
}
public void abort() {
this.aborted = true;
}
@Override
public void run() {
try {
Thread.sleep(getSingleClickDelayMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!aborted) {
Platform.runLater(onClick::run);
}
}
}
private void addClickedEventHandler() {
//Handling the mouse clicked event (not using 'onMouseClicked' so it can still be used by developer).
addEventHandler(MouseEvent.MOUSE_CLICKED , me -> {
switch (me.getButton()) {
case PRIMARY:
if (me.getClickCount() == 1) {
latestClickRunner = new ClickRunner(() -> {
System.out.println("ButtonWithDblClick : SINGLE Click fired");
onMouseSingleClickedProperty.get().handle(me);
});
CompletableFuture.runAsync(latestClickRunner);
}
if (me.getClickCount() == 2) {
if (latestClickRunner != null) {
latestClickRunner.abort();
}
System.out.println("ButtonWithDblClick : DOUBLE Click fired");
onMouseDoubleClickedProperty.get().handle(me);
}
break;
case SECONDARY:
// Right-click operation. Not implemented since usually no double RIGHT click needs to be caught.
break;
default:
break;
}
});
}
public void setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseSingleClickedProperty.set(eventHandler);
}
public void setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseDoubleClickedProperty.set(eventHandler);
}
public long getSingleClickDelayMillis() {
return singleClickDelayMillis;
}
public void setSingleClickDelayMillis(long singleClickDelayMillis) {
this.singleClickDelayMillis = singleClickDelayMillis;
}
}
INTERFACES :
Here the interfaces. The ButtonBaseCutomInterface
:
public interface ButtonBaseCutomInterface {
public void setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler) ;
public void setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler) ;
public long getSingleClickDelayMillis() ;
public void setSingleClickDelayMillis(long singleClickDelayMillis) ;
}
and for example the ToggleButton
interface :
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.scene.AccessibleAttribute;
import javafx.scene.control.ToggleGroup;
public interface ToggleButtonInterface {
//Skin<?> createDefaultSkin();
public void fire();
public ToggleGroup getToggleGroup();
public boolean isSelected();
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters);
public BooleanProperty selectedProperty();
public void setSelected(boolean value);
public void setToggleGroup(ToggleGroup value);
public ObjectProperty<ToggleGroup> toggleGroupProperty();
}
PS : Yes some methods are still missing in my Custom classes, but the question is probably not about these :
- getOnMouseSingleClicked(...)
- ...