I'm new to JavaFX and I just can't seem to find how to do this.
I have a ListView inside a Vbox that I populate with an ObservableList of Strings. I've set the SelectionMode of the ListView to MULTIPLE and that has allowed me to select multiple items while holding the Ctrl or Shift keys.
I'd like to be able to click on a row and drag the mouse down and select multiple rows but I can't figure out how to do this. I've tried several searches and seem to only find Drag and Drop and that's not what I need.
@FXML private ListView availableColumnList;
private ObservableList<String> availableColumns = FXCollections.<String>observableArrayList("One","Two","Three","Four");
availableColumnList.getItems().addAll(availableColumns);
availableColumnList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
If you're using JavaFX 10+ then you can extend ListViewSkin
and add the functionality there. The reason you need JavaFX 10 or later is because that's when the VirtualContainerBase
class, which ListViewSkin
extends, had the getVirtualFlow()
method added. You can then use the animation API, such as an AnimationTimer
, to scroll the ListView
via the VirtualFlow#scrollPixels(double)
method.
Below is a proof-of-concept. All it does is auto-scroll the ListView
when the mouse is near the top (or left) or near the bottom (or right) of the ListView
. When the mouse enters a cell, the item is selected (crudely). If you want to deselect items if you start dragging the mouse in the opposite direction then you need to implement that yourself. Another thing you probably want to implement is stopping the AnimationTimer
if the ListView
is hidden or removed from the scene.
Note: The below uses a "full press-drag-release" gesture. In other words, there's a mixture of MouseEvent
handlers and MouseDragEvent
handlers. The reason for using MouseDragEvent
s is because they can be delivered to other nodes, not just the original (unlike with a "simple press-drag-release" gesture). Check out this documentation for more information.
Main.java
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.stage.Stage;
public final class Main extends Application {
@Override
public void start(Stage primaryStage) {
var listView = IntStream.range(0, 1000)
.mapToObj(Integer::toString)
.collect(Collectors.collectingAndThen(
Collectors.toCollection(FXCollections::observableArrayList),
ListView::new
));
// Sets the custom skin. Can also be set via CSS.
listView.setSkin(new CustomListViewSkin<>(listView));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
primaryStage.setScene(new Scene(listView, 600, 400));
primaryStage.show();
}
}
CustomListViewSkin.java
import javafx.animation.AnimationTimer;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.skin.ListViewSkin;
import javafx.scene.input.MouseDragEvent;
import javafx.scene.input.MouseEvent;
public class CustomListViewSkin<T> extends ListViewSkin<T> {
private static final double DISTANCE = 10;
private static final double PERCENTAGE = 0.05;
private AnimationTimer scrollAnimation = new AnimationTimer() {
@Override
public void handle(long now) {
if (direction == -1) {
getVirtualFlow().scrollPixels(-DISTANCE);
} else if (direction == 1) {
getVirtualFlow().scrollPixels(DISTANCE);
}
}
};
private Rectangle2D leftUpArea;
private Rectangle2D rightDownArea;
private int direction = 0;
private int anchorIndex = -1;
public CustomListViewSkin(final ListView<T> control) {
super(control);
final var flow = getVirtualFlow();
final var factory = flow.getCellFactory();
// decorate the actual cell factory
flow.setCellFactory(vf -> {
final var cell = factory.call(flow);
// handle drag start
cell.addEventHandler(MouseEvent.DRAG_DETECTED, event -> {
if (control.getSelectionModel().getSelectionMode() == SelectionMode.MULTIPLE) {
event.consume();
cell.startFullDrag();
anchorIndex = cell.getIndex();
}
});
// handle selecting items when the mouse-drag enters the cell
cell.addEventHandler(MouseDragEvent.MOUSE_DRAG_ENTERED, event -> {
event.consume();
if (event.getGestureSource() != cell) {
final var model = control.getSelectionModel();
if (anchorIndex < cell.getIndex()) {
model.selectRange(anchorIndex, cell.getIndex() + 1);
} else {
model.selectRange(cell.getIndex(), anchorIndex + 1);
}
}
});
return cell;
});
// handle the auto-scroll functionality
flow.addEventHandler(MouseDragEvent.MOUSE_DRAG_OVER, event -> {
event.consume();
if (leftUpArea.contains(event.getX(), event.getY())) {
direction = -1;
scrollAnimation.start();
} else if (rightDownArea.contains(event.getX(), event.getY())) {
direction = 1;
scrollAnimation.start();
} else {
direction = 0;
scrollAnimation.stop();
}
});
// stop the animation when the mouse exits the flow/list (desired?)
flow.addEventHandler(MouseDragEvent.MOUSE_DRAG_EXITED, event -> {
event.consume();
scrollAnimation.stop();
});
// handle stopping the animation and reset the state when the mouse
// is released. Added to VirtualFlow because it doesn't matter
// which cell receives the event.
flow.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
if (anchorIndex != -1) {
event.consume();
anchorIndex = -1;
scrollAnimation.stop();
}
});
updateAutoScrollAreas();
registerChangeListener(control.orientationProperty(), obs -> updateAutoScrollAreas());
registerChangeListener(flow.widthProperty(), obs -> updateAutoScrollAreas());
registerChangeListener(flow.heightProperty(), obs -> updateAutoScrollAreas());
}
// computes the regions where the mouse needs to be
// in order to start auto-scrolling. The regions depend
// on the orientation of the ListView.
private void updateAutoScrollAreas() {
final var flow = getVirtualFlow();
switch (getSkinnable().getOrientation()) {
case HORIZONTAL:
final double width = flow.getWidth() * PERCENTAGE;
leftUpArea = new Rectangle2D(0, 0, width, flow.getHeight());
rightDownArea = new Rectangle2D(flow.getWidth() - width, 0, width, flow.getHeight());
break;
case VERTICAL:
final double height = flow.getHeight() * PERCENTAGE;
leftUpArea = new Rectangle2D(0, 0, flow.getWidth(), height);
rightDownArea = new Rectangle2D(0, flow.getHeight() - height, flow.getWidth(), height);
break;
default:
throw new AssertionError();
}
}
@Override
public void dispose() {
unregisterChangeListeners(getSkinnable().orientationProperty());
unregisterChangeListeners(getVirtualFlow().widthProperty());
unregisterChangeListeners(getVirtualFlow().heightProperty());
super.dispose();
scrollAnimation.stop();
scrollAnimation = null;
}
}
Note: As mentioned by kleopatra, at least some of this functionality is better suited for the behavior class. However, for simplicity's sake, I decided to only use the existing, public skin class (by extending it). Again, the above is only a proof-of-concept.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With