I'm fairly new to the JavaFX world, and I can't seem to figure out how to listen for text-modify events in the HTMLEditor component.
I need this since I'm hooking this widget to a model, which needs updating.
The addEventFilter API, with a KeyEvent.KEY_TYPED event type doesn't seem to be working as it should. When its handler is called, the getHTMLText() isn't updated yet with the most recent character (if someone doesn't understand this paragraph, I'll provide a step-by-step example).
The TextField has a textProperty() on which a listener can be attached.
Now what about the HTMLEditor?
Also, it would be nice to have the listener called ONLY on text modify events (and not on CTRL+A, for example). You know... like SWT Text's addModifyListener().
While using JavaFX HTMLEditor in one of my project application, I also faced a similar situation. I ended up adding a button, upon whose click the parsing of the HTML text would happen, and further tasks executed. With AnchorPane, I was able to add the button on the HTMLEditor seamlessly, and it looked like a part of it.
Anyways, here's a little example of how you can achieve what you want without any extra button:
package application;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.web.HTMLEditor;
import javafx.stage.Stage;
public class Main extends Application
{
#Override
public void start(Stage primaryStage)
{
try
{
final HTMLEditor editor = new HTMLEditor();
Scene scene = new Scene(editor);
primaryStage.setScene(scene);
editor.setOnKeyReleased(new EventHandler<KeyEvent>()
{
#Override
public void handle(KeyEvent event)
{
if (isValidEvent(event))
{
System.out.println(editor.getHtmlText());
}
}
private boolean isValidEvent(KeyEvent event)
{
return !isSelectAllEvent(event)
&& ((isPasteEvent(event)) || isCharacterKeyReleased(event));
}
private boolean isSelectAllEvent(KeyEvent event)
{
return event.isShortcutDown() && event.getCode() == KeyCode.A;
}
private boolean isPasteEvent(KeyEvent event)
{
return event.isShortcutDown() && event.getCode() == KeyCode.V;
}
private boolean isCharacterKeyReleased(KeyEvent event)
{
// Make custom changes here..
switch (event.getCode())
{
case ALT:
case COMMAND:
case CONTROL:
case SHIFT:
return false;
default:
return true;
}
}
});
primaryStage.show();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String[] args)
{
launch(args);
}
}
UPDATE:
Upon a bit more of thinking, I found a way to get event handling done even on those button clicks. Here's how:
EventHandler<MouseEvent> onMouseExitedHandler = new EventHandler<MouseEvent>()
{
#Override
public void handle(MouseEvent event)
{
System.out.println(editor.getHtmlText());
}
};
for (Node node : editor.lookupAll("ToolBar"))
{
node.setOnMouseExited(onMouseExitedHandler);
}
If you see the HTMLEditor, it has two ToolBars.
What I'm doing in the code is looking up for those two toolbars, and setting an onMouseExited event handler. The analogy is that if the user enters and makes some changes on the HTML Text and exits the toolbar, an event will be fired, which can then be handled.
You can even set different kind of event handlers on these two toolbars, based on your needs, but in my opinion, these onMouseExited event handlers provide a very wide coverage when used with the onKeyReleased event handlers. The coverage based on onMouseExited handler is not exact though.
here is a simple one
public class HtmlEditorListener {
private final BooleanProperty editedProperty;
private String htmlRef;
public HtmlEditorListener(final HTMLEditor editor) {
editedProperty = new SimpleBooleanProperty();
editedProperty.addListener((ov, o, n) -> htmlRef = n? null: editor.getHtmlText());
editedProperty.set(false);
editor.setOnMouseClicked(e -> checkEdition(editor.getHtmlText()));
editor.addEventFilter(KeyEvent.KEY_TYPED, e -> checkEdition(editor.getHtmlText()));
}
public BooleanProperty editedProperty() {
return editedProperty;
}
private void checkEdition(final String html) {
if (editedProperty.get()) {
return;
}
editedProperty.set(htmlRef != null
&& html.length() != htmlRef.length()
|| !html.equals(htmlRef));
}
}
HtmlEditor is based on Web view
HTMLEditor editor = getEditor();
WebView webView = (WebView) getEditor().lookup("WebView");
new WebViewEditorListener(webView, new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
}
});
Add Callback for tracking html changes.
public static class WebViewEditorListener {
private final ChangeListener<String> listener;
private final WebPage webPage;
private String htmlRef, innerText;
public WebViewEditorListener(final WebView editor, ChangeListener<String> listener) {
this.listener = listener;
webPage = Accessor.getPageFor(editor.getEngine());
editor.setOnMouseClicked(e -> onKeyTyped(webPage.getHtml(webPage.getMainFrame())));
editor.addEventFilter(KeyEvent.KEY_TYPED, e -> onKeyTyped(webPage.getHtml(webPage.getMainFrame())));
}
public String getHtmlContent(){
return htmlRef == null ? "" : htmlRef ;
}
private void onKeyTyped(final String html) {
boolean isEqual = htmlRef != null ? htmlRef.length() == html.length() : html == null;
if (!isEqual){
String text = webPage.getInnerText(webPage.getMainFrame());
listener.changed(null, innerText, text);
innerText = text;
htmlRef = html;
}
}
}
Related
Consider the following MCVE. Of course, the functionality of this MCVE is completely pointless, but I need it to work this way in the real implementation.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
#SuppressWarnings("all")
public class MCVE extends Application {
private static final String OPTION_1 = "Option 1 (www.option1.com)";
private static final String OPTION_2 = "Option 2 (www.option2.com)";
private static final String OPTION_3 = "Option 3 (www.option3.com)";
private static final String OPTION_4 = "Option 4 (www.option4.com)";
private static final String OPTION_5 = "Option 5 (www.option5.com)";
ComboBox<String> cb;
#Override
public void start(Stage primaryStage) throws Exception {
VBox outer = new VBox();
cb = new ComboBox<String>();
outer.getChildren().add(cb);
Scene scene = new Scene(outer, 640, 480);
primaryStage.setScene(scene);
Task<Void> task = new Task<Void>() {
#Override
public Void call() {
cb.getItems().addAll(OPTION_1, OPTION_2, OPTION_3, OPTION_4, OPTION_5);
cb.setEditable(true);
// Adds a listener to the selectedItemProperty that gets the
// value inside the parenthesis of the selected item and sets
// this as the text of the ComboBox.
cb.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
String[] valSplit = newValue.split("[\\(\\)]");
if (valSplit.length > 1) {
Platform.runLater(() -> cb.getEditor().setText(valSplit[1]));
}
});
cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
System.out.println("CB value: " + newValue);
});
setURL("www.option2.com");
return null;
}
};
task.setOnSucceeded(e -> {
primaryStage.show();
});
new Thread(task).start();
}
public void setURL(String url) {
// First we check if the classValue is the URL of one of the options in
// the ComboBox. If it is we select that option.
for (String option : cb.getItems()) {
// We retrieve the URL of the option.
String opURL = option.split("[\\(\\)]")[1];
// If the URL of the option is equals to the provided URL, we select
// this option and break the for loop.
if (opURL.equals(url)) {
cb.getSelectionModel().select(option);
break;
}
}
}
public static void main(String[] args) {
launch(args);
}
}
Since I invoke setURL("www.option2.com"), I expect it to first select the option in the ComboBox with that URL, and then get the value inside the parenthesis and set that as the text of the ComboBox. So I except the final value of the ComboBox to be "www.option2.com". But this doesn't happen. Instead the final value is "Option 2 (www.option2.com)".
Since I have added a listener to the textProperty of the ComboBox, I can see that the value is first the expected "www.option2.com", but then changes back to "Option 2 (www.option2.com)". After some further investigation, I've found out that it's the invocation of primaryStage.show() that changes the value. More specifically, it's the invocation of the deprecated Parent.impl_processCSS that changes the value.
So if I set the URL after primaryStage.show(), everything works as I except. But if I want to do all of the work before I show the dialog, like I do now, it doesn't.
So why does primaryStage.show() change the value of my ComboBox, and how can I prevent this? Should I maybe use another approach when trying to set the value of a ComboBox?
You could exchange the part of you code which sets the text of the editor of the ComboBox with some code that sets up a cell factory and a converter.
cb.setConverter(new StringConverter<String>(){
#Override
public String toString(String object) {
if(object != null) {
String[] valSplit = object.split("[\\(\\)]");
return valSplit[1];
} else
return null;
}
#Override
public String fromString(String string) {
List<String> collect = cb.getItems().stream().filter(s -> s.contains(string)).collect(Collectors.toList());
if(collect.size() == 1)
return collect.get(0);
else
return null;
}
});
cb.setCellFactory(item -> {
return new ListCell<String>(){
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty)
setText("");
else
setText(item);
}
};
});
The toString method of your converter will format the selected item in the needed form, and the cell factory ensures that the items in the drop down list are displayed in the original format.
Note: I have also filled the fromString method of the converter. This method is executed, when the user types into the editor then presses enter. This implementation checks all the items in the list, and if there is only one single item which contains the typed string, that item will be selected.
I've got a problem with a Java project I'm working on: I'm creating a grid of buttons via code in javafx on a pane. The buttons are all types of a subclass of the javafx Button class that i wrote.
Here's the header of the class:
private final String BASIC_STYLE = "-fx-font: 6 arial;";
private final String CLICKED_STYLE = "-fx-background-color: #0f0";
private int row;
private int col;
private String category;
private boolean selected = false;
Within the constructor i do the follwing:
this.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
toggleSelected();
}
});
Here's the toggleSelected() Method:
public void toggleSelected() {
this.selected = !selected;
this.setStyle(selected ? this.BASIC_STYLE : this.BASIC_STYLE+this.CLICKED_STYLE);
}
It's basically supposed to swap the style everytime you click the button. When i click the button, the button first gets selected by the OS (the border is becoming blue) and only after i click a second time on the exact same button it'll become green (the style that i'm giving it via setStyle).
However, the selected property becomes true on the first click and false on the second click, which means i click once on the button and it gets a blue border and selected = true, if i click on it a second time it becomes green and selected = false and if i click on it a third time it becomes normal again but selected will be true again.
I find it to be really strange that the first click on a button changes the "selected" variable correctly but not the style. Why is this happening and how can i avoid that i've to select the button first before i can click it?
You initialize
selected = false ;
and
setStyle(BASIC_STYLE);
But your event handler enforces the rule
selected == true -> setStyle(BASIC_STYLE);
selected == false -> setStyle(CLICKED_STYLE);
So your initial state is inconsistent with the state your handler enforces.
From the initial state, the first time you click, selected is set to true which causes setStyle(BASIC_STYLE) (which is the value it already has, so nothing changes). From then on, everything will switch as required.
You either need to change the initial state, or switch the logic of the setStyle(...) call in the handler.
public class ButtonEnterAction extends Button {
boolean selected = true;
public ButtonEnterAction(String connect) {
setText(connect);
action();
}
public ButtonEnterAction() {
action();
}
private void action() {
EventHandler<KeyEvent> enterEvent = (KeyEvent event) -> {
if (event.getCode() == KeyCode.ENTER) {
fire();
}
};
addEventFilter(KeyEvent.KEY_PRESSED, enterEvent);
// setOnMouseEntered(new EventHandler<MouseEvent>() {
// #Override
// public void handle(MouseEvent me) {
// SepiaTone st = new SepiaTone();
// setEffect(st);
// }
// });
// setOnMouseExited(new EventHandler<MouseEvent>() {
// #Override
// public void handle(MouseEvent me) {
// setEffect(null);
// }
// });
}
#Override
public void fire() {
super.fire(); //To change body of generated methods, choose Tools | Templates.
if (selected) {
SepiaTone st = new SepiaTone();
setEffect(st);
} else {
setEffect(null);
}
selected = !selected;
}
}
Create the Instant Class in ButtonEnterAction is like.
ButtonEnterAction bea = new ButtonEnterAction("TestButton");
bea.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
System.out.println("hello");
}
});
I have a TableView that uses a ColorPicker to (display/edit) colors in a cell.
The table display the ColorPicker in the desired field, but edits aren't working.
TableColumn<SeriesPreferences, Color> c2 = new TableColumn<SeriesPreferences, Color>("Color");
c2.setCellValueFactory(new PropertyValueFactory<SeriesPreferences, Color>("color"));
c2.setCellFactory(new Callback<TableColumn<SeriesPreferences, Color>,
TableCell<SeriesPreferences, Color>>()
{
#Override
public TableCell<SeriesPreferences, Color>
call(final TableColumn<SeriesPreferences, Color> param)
{
TableCell<SeriesPreferences, Color> cell =
new TableCell<SeriesPreferences, Color>()
{
#Override
public void updateItem(Color c, boolean empty)
{
if(c != null)
{
final ColorPicker cp = new ColorPicker();
cp.setValue(c);
setGraphic(cp);
cp.setOnAction(new EventHandler<javafx.event.ActionEvent>()
{
public void
handle(javafx.event.ActionEvent t)
{
getTableView().edit(getTableRow().getIndex(), param);
commitEdit(cp.getValue());
}
});
}
}
};
return cell;
}
});
c2.setOnEditCommit(new EventHandler<CellEditEvent<SeriesPreferences, Color>>()
{
#Override
public void handle(CellEditEvent<SeriesPreferences, Color> t)
{
((SeriesPreferences) t.getTableView().getItems().get(t.getTablePosition().
getRow())).setColor(t.getNewValue());
}
});
The edit event handler isn't being called when i change the color in the color picker, any ideas?
There's no need to access the JavaFX POJO (or JavaFX Bean) directly if its properties are correctly bound to the table and also it isn't necessary to call anything other than commitEdit.
The answer from Max Beikirch is misleading, because it causes the color picker (and with it the color) to disappear when the table is not in edit mode. It's a workaround to put the table into edit mode, but a bad one. So do this before showing the color picker popup when click on the button:
Write your cell with a color picker like this:
public class ColorTableCell<T> extends TableCell<T, Color> {
private final ColorPicker colorPicker;
public ColorTableCell(TableColumn<T, Color> column) {
this.colorPicker = new ColorPicker();
this.colorPicker.editableProperty().bind(column.editableProperty());
this.colorPicker.disableProperty().bind(column.editableProperty().not());
this.colorPicker.setOnShowing(event -> {
final TableView<T> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
this.colorPicker.valueProperty().addListener((observable, oldValue, newValue) -> {
if(isEditing()) {
commitEdit(newValue);
}
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Color item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if(empty) {
setGraphic(null);
} else {
this.colorPicker.setValue(item);
this.setGraphic(this.colorPicker);
}
}
}
If you're on Java 7, replace the lambdas with anonymous inner classes, but it should work as well. Full blog post is here.
I had the same problem for CheckBoxTableCell and DatePickerTableCell and ColorPickerTableCells :-(
I deal it like that: on the events of the controls I get back the POJO objects in use by the "((Inputs)getTableView().getItems().get(getTableRow().getIndex()" and I update similary like is it done in the OnEditCommit method...
So for me it's look like this (update the color):
((Inputs) getTableView().getItems().get(
getTableRow().getIndex())
).setColor(cp.getValue());
Here is example with ColorPickerCell
:
public class ColorPickerTableCell<Inputs> extends TableCell<Inputs, Color>{
private ColorPicker cp;
public ColorPickerTableCell(){
cp = new ColorPicker();
cp.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
commitEdit(cp.getValue());
updateItem(cp.getValue(), isEmpty());
((Inputs) getTableView().getItems().get(
getTableRow().getIndex())
).setColor(cp.getValue());
}
});
setGraphic(cp);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setEditable(true);
}
#Override
protected void updateItem(Color item, boolean empty) {
super.updateItem(item, empty);
cp.setVisible(!empty);
this.setItem(item);
cp.setValue(item);
}
}
With this simple JavaFX's POJO:
public ObjectProperty<Color> color = new SimpleObjectProperty<Color>();
this.color = new SimpleObjectProperty(color);
public ObjectProperty<Color> colorProperty() {
return color;
}
public void setColor(Color color2) {
color.set(color2);
}
I do not know if it's a good way to achive that but it worked for me... Note that the JavaFX's POJO is only accessible within an "ActionEvent" request (combobox, datepicker, colorpicker, etc..)
Regards,
Well, I investigated that topic a bit as I have had the same problem. I am afraid to say that JavaFX is just unusable.
I took a look at how others implemented their cells and the key was that were all using something that is representable by a string.
Now, it's the way it always is with Java: Do it the Java-way or be left alone in the rain. The docs for JavaFX are extremely bad at the moment, so I had to try until it works.
So: To trigger the editCommit-event, you have to call setContentDisplay(ContentDisplay. TEXT_ONLY) in updateItem(). That works well if want to display your data as string, but fails completely in cases like these, where a colorpicker just does the job.
Alternatively, it might be possible to fire the event manually. But how do you get the table-position? I don't know.
It like Michael Simons said in the comment on the OP. You need to be in edit mode. When creating your own custom cells you can trigger edit mode manually by calling startEdit(); from inside the TableCell.
for example using the focusProperty of your control:
cp.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
startEdit();
}
});
I would like to know if it was possible to detect the double-click in JavaFX 2 ? and how ?
I would like to make different event between a click and a double click.
Thanks
Yes you can detect single, double even multiple clicks:
myNode.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2){
System.out.println("Double clicked");
}
}
}
});
MouseButton.PRIMARY is used to determine if the left (commonly) mouse button is triggered the event. Read the api of getClickCount() to conclude that there maybe multiple click counts other than single or double. However I find it hard to distinguish between single and double click events. Because the first click count of the double click will rise a single event as well.
Here is another piece of code which can be used if you have to distinguish between a single- and a double-click and have to take a specific action in either case.
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class DoubleClickDetectionTest extends Application {
boolean dragFlag = false;
int clickCounter = 0;
ScheduledThreadPoolExecutor executor;
ScheduledFuture<?> scheduledFuture;
public DoubleClickDetectionTest() {
executor = new ScheduledThreadPoolExecutor(1);
executor.setRemoveOnCancelPolicy(true);
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
StackPane root = new StackPane();
primaryStage.setScene(new Scene(root, 400, 400));
primaryStage.show();
root.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getButton().equals(MouseButton.PRIMARY)) {
dragFlag = true;
}
}
});
root.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getButton().equals(MouseButton.PRIMARY)) {
if (!dragFlag) {
System.out.println(++clickCounter + " " + e.getClickCount());
if (e.getClickCount() == 1) {
scheduledFuture = executor.schedule(() -> singleClickAction(), 500, TimeUnit.MILLISECONDS);
} else if (e.getClickCount() > 1) {
if (scheduledFuture != null && !scheduledFuture.isCancelled() && !scheduledFuture.isDone()) {
scheduledFuture.cancel(false);
doubleClickAction();
}
}
}
dragFlag = false;
}
}
});
}
#Override
public void stop() {
executor.shutdown();
}
private void singleClickAction() {
System.out.println("Single-click action executed.");
}
private void doubleClickAction() {
System.out.println("Double-click action executed.");
}
}
Adhering to Java SE 8 lambda expressions would look something like this:
node.setOnMouseClicked(event -> {
if(event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2) {
handleSomeAction();
}
});
Once you get used to lambda expressions - they end up being more understandable than the original class instantiation and overriding (x) method. -In my opinion-
The response by P. Pandey is the simplest approach which actually distinguishes between single and double click, but it did not work for me. For one, the function "currentTimeMillis" already returns milliseconds, so dividing it by 1000 does not seem to be necessary. The version below worked for me in a more consistent fashion.
#Override
public void handle(MouseEvent t) {
long diff = 0;
currentTime=System.currentTimeMillis();
if(lastTime!=0 && currentTime!=0){
diff=currentTime-lastTime;
if( diff<=215)
isdblClicked=true;
else
isdblClicked=false;
}
lastTime=currentTime;
System.out.println("IsDblClicked()"+isdblClicked);
//use the isdblClicked flag...
}
Not sure if someone still follows this OP or refer it, but below is my version of differentiating single click to double click. While most of the answers are quite acceptable, it would be really useful if it can be done in a proper resuable way.
One of the challenge I encountered is the need to have the single-double click differentiation on multiple nodes at multiple places. I cannot do the same repetitive cumbersome logic on each and every node. It should be done in a generic way.
So I opted to implement a custom EventDispatcher and use this dispatcher on node level or I can apply it directly to Scene to make it applicable for all child nodes.
For this I created a new MouseEvent namely 'MOUSE_DOUBLE_CLICKED", so tthat I am still sticking with the standard JavaFX practises. Now I can include the double_clicked event filters/handlers just like other mouse event types.
node.addEventFilter(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e->{..<code to handle double_click>..});
node.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e->{..<code to handle double_click>..});
Below is the implementation and complete working demo of this custom event dispatcher.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.*;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DoubleClickEventDispatcherDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
Rectangle box1 = new Rectangle(150, 150);
box1.setStyle("-fx-fill:red;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box1, "Red Box");
Rectangle box2 = new Rectangle(150, 150);
box2.setStyle("-fx-fill:yellow;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box2, "Yellow Box");
HBox pane = new HBox(box1, box2);
pane.setSpacing(10);
pane.setAlignment(Pos.CENTER);
addEventHandlers(pane, "HBox");
Scene scene = new Scene(new StackPane(pane), 450, 300);
stage.setScene(scene);
stage.show();
// SETTING CUSTOM EVENT DISPATCHER TO SCENE
scene.setEventDispatcher(new DoubleClickEventDispatcher(scene.getEventDispatcher()));
}
private void addEventHandlers(Node node, String nodeId) {
node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked filter"));
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked handler"));
node.addEventFilter(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println("" + nodeId + " mouse double clicked filter"));
node.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println(nodeId + " mouse double clicked handler"));
}
/**
* Custom MouseEvent
*/
interface CustomMouseEvent {
EventType<MouseEvent> MOUSE_DOUBLE_CLICKED = new EventType<>(MouseEvent.ANY, "MOUSE_DBL_CLICKED");
}
/**
* Custom EventDispatcher to differentiate from single click with double click.
*/
class DoubleClickEventDispatcher implements EventDispatcher {
/**
* Default delay to fire a double click event in milliseconds.
*/
private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;
/**
* Default event dispatcher of a node.
*/
private final EventDispatcher defaultEventDispatcher;
/**
* Timeline for dispatching mouse clicked event.
*/
private Timeline clickedTimeline;
/**
* Constructor.
*
* #param initial Default event dispatcher of a node
*/
public DoubleClickEventDispatcher(final EventDispatcher initial) {
defaultEventDispatcher = initial;
}
#Override
public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
final EventType<? extends Event> type = event.getEventType();
if (type == MouseEvent.MOUSE_CLICKED) {
final MouseEvent mouseEvent = (MouseEvent) event;
final EventTarget eventTarget = event.getTarget();
if (mouseEvent.getClickCount() > 1) {
if (clickedTimeline != null) {
clickedTimeline.stop();
clickedTimeline = null;
final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_CLICKED);
Event.fireEvent(eventTarget, dblClickedEvent);
}
return mouseEvent;
}
if (clickedTimeline == null) {
final MouseEvent clickedEvent = copy(mouseEvent, mouseEvent.getEventType());
clickedTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
Event.fireEvent(eventTarget, clickedEvent);
clickedTimeline = null;
}));
clickedTimeline.play();
return mouseEvent;
}
}
return defaultEventDispatcher.dispatchEvent(event, tail);
}
/**
* Creates a copy of the provided mouse event type with the mouse event.
*
* #param e MouseEvent
* #param eventType Event type that need to be created
* #return New mouse event instance
*/
private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
e.isStillSincePress(), e.getPickResult());
}
}
}
Here is how I have implemented double click
if (e.getEventType().equals(MouseEvent.MOUSE_CLICKED) && !drag_Flag) {
long diff = 0;
if(time1==0)
time1=System.currentTimeMillis();
else
time2=System.currentTimeMillis();
if(time1!=0 && time2!=0)
diff=time2-time1;
if((diff/1000)<=215 && diff>0)
{
isdblClicked=true;
}
else
{
isdblClicked=false;
}
System.out.println("IsDblClicked()"+isdblClicked);
}
Since it is not possible to distinguish between single-click and double-click by default, we use the following approach:
On single-click, we wrap the single-click operation in an abortable runnable. This runnable waits a certain amount of time (i.e., SINGLE_CLICK_DELAY) before being executed.
In the meantime, if a second click, i.e., a double-click, occurs, the single-click operation gets aborted and only the double-click operation is performed.
This way, either the single-click or the double-click operation is performed, but never both.
Following is the full code. To use it, only the three TODO lines have to be replaced by the wanted handlers.
private static final int SINGLE_CLICK_DELAY = 250;
private ClickRunner latestClickRunner = null;
private class ClickRunner implements Runnable {
private final Runnable onSingleClick;
private boolean aborted = false;
public ClickRunner(Runnable onSingleClick) {
this.onSingleClick = onSingleClick;
}
public void abort() {
this.aborted = true;
}
#Override
public void run() {
try {
Thread.sleep(SINGLE_CLICK_DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!aborted) {
System.out.println("Execute Single Click");
Platform.runLater(() -> onSingleClick.run());
}
}
}
private void init() {
container.setOnMouseClicked(me -> {
switch (me.getButton()) {
case PRIMARY:
if (me.getClickCount() == 1) {
System.out.println("Single Click");
latestClickRunner = new ClickRunner(() -> {
// TODO: Single-left-click operation
});
CompletableFuture.runAsync(latestClickRunner);
}
if (me.getClickCount() == 2) {
System.out.println("Double Click");
if (latestClickRunner != null) {
System.out.println("-> Abort Single Click");
latestClickRunner.abort();
}
// TODO: Double-left-click operation
}
break;
case SECONDARY:
// TODO: Right-click operation
break;
default:
break;
}
});
}
A solution using PauseTransition:
PauseTransition singlePressPause = new PauseTransition(Duration.millis(500));
singlePressPause.setOnFinished(e -> {
// single press
});
node.setOnMousePressed(e -> {
if (e.isPrimaryButtonDown() && e.getClickCount() == 1) {
singlePressPause.play();
}
if (e.isPrimaryButtonDown() && e.getClickCount() == 2) {
singlePressPause.stop();
// double press
}
});
An alternative to single click vs. double click that I'm using is single click vs. press-and-hold (for about a quarter to a half second or so), then release the button. The technique can use a threaded abortable timer as in some of the code snippets above to distinguish between the two. Assuming that the actual event handling happens on the button release, this alternative has the advantage that single click works normally (i.e., without any delay), and for press-and-hold you can give the user some visual feedback when the button has been held long enough to be released (so there's never any ambiguity about which action was performed).
If you are testing how many mouse buttons (==2) are pressed, do not code it in sub-method! The next is working:
listView.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if( mouseEvent.getButton().equals(MouseButton.SECONDARY)) {
System.out.println("isSecondaryButtonDown");
mouseEvent.consume();
// ....
}
else
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2){
System.out.println("Double clicked");
// mousePressedInListViewDC(mouseEvent);
}
else
if(mouseEvent.getClickCount() == 1){
System.out.println("1 clicked");
mousePressedInListView1C(mouseEvent);
}
}
}
})
;
I ran in the same problem, and what I noticed is that single and double click ARE distinguished with basic :
Button btn = new Button("Double click me too");
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");
});
But a 'single' click is catched as part of the double click. So you will see on the console :
Using mainly the answer of #markus-weninger, I built up a Class extending Button to expose 2 new EventHandlers :
setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler)
setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler)
So with the full example code bellow, when double clicking on last button, we get :
Keep in mind :
The obvious drawback is that even a single click caught with setOnMouseSingleClicked will be delayed with the singleClickDelayMillis (exposed variable which should be set accordingly to the OS, as mentioned by Kleopatra).
Another noticeable fact, is that I extended Button, and not Node where it should be : The Class where the onMouseClicked(...) is implemented.
As a last comment, I decided to add a new EventHandler rather than using the existing setOnMousePressed, setOnMouseReleased or setOnMouseClicked so that the developer can still fully implement these convenience EventHandlers. For example in order to have immediate response from a click on the button without waiting for the singleClickDelayMillis. But this means that if you implement both, the setOnMouseClicked will be fired even on a double click... beware.
Here comes the code :
import java.util.concurrent.CompletableFuture;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.beans.property.ObjectProperty;
import javafx.event.EventHandler;
import javafx.beans.property.SimpleObjectProperty;
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(singleClickDelayMillis);
} 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).
EventHandler<MouseEvent> eventHandler = 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;
}
};
//Adding the event handler
addEventHandler(MouseEvent.MOUSE_CLICKED, eventHandler);
}
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();
Label lbl = new Label("Double click me");
lbl.setOnMouseClicked(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 2) {
System.out.println("Label double clicked");
} else if (mouseEvent.getClickCount() == 1)
System.out.println("Label clicked");
});
Button btn = new Button("Double click me too");
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 three ;-)");
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(lbl);
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();
}
}
Right now I have the following code working:
#UiHandler("usernameTextBox")
void onUsernameTextBoxKeyPress(KeyPressEvent event) {
keyPress(event);
}
#UiHandler("passwordTextBox")
void onPasswordTextBoxKeyPress(KeyPressEvent event) {
keyPress(event);
}
void keyPress(KeyPressEvent event) {
if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
submit();
}
}
I would like the ability to have just one listener for all elements on the view without duplicating an event for each textbox.
The end goal is that if they press enter, regardless of where they are on the page, it should submit the form.
Thanks!
What works, but still requires you to specify it for each widget, but doesn't require duplicate code:
#UiHandler({"usernameTextBox", "passwordTextBox"})
void onPasswordTextBoxKeyPress(KeyPressEvent event) {
keyPress(event);
}
Yes jackcrews is correct. Also you can try the following. It may be VerticalPanel, DockLayoutPanel etc....
UiBinder.ui.xml
<gwt:VerticalPanel ui:field="mainPanel">
<gwt:Label>Name</gwt:TextBox>
<gwt:TextBox ui:field="textBox">
</gwt:VerticalPanel>
Main.java
#UiField
VerticalPanel mainPanel;
public Main() {
focushandler();
}
void focusHandler() {
mainPanel.addDomHandler(new Handler(), KeyPressEvent.getType());
}
final class Handler implements KeyPressHandler {
#Override
public void onKeyPress(KeyPressEvent event) {
//Code what you expect
}
}
Actually this has more number of lines. But it is good practice.
Regards,
Gnik
I found out that the g:FocusPanel allows me to capture events for everything inside the panel.
#UiHandler("focusPanel")
void onFocusPanelKeyPress(KeyPressEvent event) {
if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
submit();
}
}