10

my aim is to sort a tableview with drag and drop. I followed this example: http://docs.oracle.com/javafx/2/fxml_get_started/fxml_tutorial_intermediate.htm

For drag and drop I added the fxml via Scene Builder

<TableView fx:id="tableView" onDragDetected="#dragDetected" onDragDropped="#dragDropped" onDragOver="#dragOver"

and made the controllers

@FXML
    private void dragDetected(MouseEvent event) {
        System.out.println("dragDetected");

        Integer idx;
        idx = tableView.getSelectionModel().getFocusedIndex();
        Dragboard db = tableView.startDragAndDrop(TransferMode.MOVE);
        ClipboardContent content = new ClipboardContent();
        content.putString(idx.toString());
        db.setContent(content);

        System.out.println(idx);
//        System.out.println(event.getPickResult());
        event.consume();
    }

    @FXML
    private void dragOver(DragEvent event) {
        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
        event.consume();
    }

    @FXML
    private void dragDropped(DragEvent event) {
        System.out.println("dragDropped");

        System.out.println(event.getTarget());
        System.out.println(event.getPickResult());
    }

but at drag dropped I cant get the row of the place where I dropped the object. All I get is the cell Information. Text[text="Smith", x=0.0, y=0.0, ...

How do I get this work? Maybe Class TableRow<T> could help, but I do not understand how to use it proberly.

schasoli
  • 317
  • 1
  • 3
  • 12

3 Answers3

37

As you suspected, the answer is to use a TableRow. You do this by setting a row factory on your table, which is used to create the table rows as they are needed. You can create them and set the drag handlers on them before returning them.

So, remove the onDragDetected, onDragDropped etc attributes from the FXML, and in the initialize method in the controller set the drag handlers on the row.

Here is a complete example, using the usual example from the Oracle tutorials. I didn't use FXML in this example (I just created the table view directly in the Java class), but you can just move all the table view configuration to the initialize method.

import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class TableViewDragRows extends Application {

    private static final DataFormat SERIALIZED_MIME_TYPE = new DataFormat("application/x-java-serialized-object");

    @Override
    public void start(Stage primaryStage) {
        TableView<Person> tableView = new TableView<>();
        tableView.getColumns().add(createCol("First Name", Person::firstNameProperty, 150));
        tableView.getColumns().add(createCol("Last Name", Person::lastNameProperty, 150));
        tableView.getColumns().add(createCol("Email", Person::emailProperty, 200));

        tableView.getItems().addAll(
            new Person("Jacob", "Smith", "jacob.smith@example.com"),
            new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
            new Person("Ethan", "Williams", "ethan.williams@example.com"),
            new Person("Emma", "Jones", "emma.jones@example.com"),
            new Person("Michael", "Brown", "michael.brown@example.com")
        );

        tableView.setRowFactory(tv -> {
            TableRow<Person> row = new TableRow<>();

            row.setOnDragDetected(event -> {
                if (! row.isEmpty()) {
                    Integer index = row.getIndex();
                    Dragboard db = row.startDragAndDrop(TransferMode.MOVE);
                    db.setDragView(row.snapshot(null, null));
                    ClipboardContent cc = new ClipboardContent();
                    cc.put(SERIALIZED_MIME_TYPE, index);
                    db.setContent(cc);
                    event.consume();
                }
            });

            row.setOnDragOver(event -> {
                Dragboard db = event.getDragboard();
                if (db.hasContent(SERIALIZED_MIME_TYPE)) {
                    if (row.getIndex() != ((Integer)db.getContent(SERIALIZED_MIME_TYPE)).intValue()) {
                        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                        event.consume();
                    }
                }
            });

            row.setOnDragDropped(event -> {
                Dragboard db = event.getDragboard();
                if (db.hasContent(SERIALIZED_MIME_TYPE)) {
                    int draggedIndex = (Integer) db.getContent(SERIALIZED_MIME_TYPE);
                    Person draggedPerson = tableView.getItems().remove(draggedIndex);

                    int dropIndex ; 

                    if (row.isEmpty()) {
                        dropIndex = tableView.getItems().size() ;
                    } else {
                        dropIndex = row.getIndex();
                    }

                    tableView.getItems().add(dropIndex, draggedPerson);

                    event.setDropCompleted(true);
                    tableView.getSelectionModel().select(dropIndex);
                    event.consume();
                }
            });

            return row ;
        });


        Scene scene = new Scene(new BorderPane(tableView), 600, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private TableColumn<Person, String> createCol(String title, 
            Function<Person, ObservableValue<String>> mapper, double size) {

        TableColumn<Person, String> col = new TableColumn<>(title);
        col.setCellValueFactory(cellData -> mapper.apply(cellData.getValue()));
        col.setPrefWidth(size);

        return col ;
    }


   public class Person {
        private final StringProperty firstName = new SimpleStringProperty(this, "firstName");
        private final StringProperty lastName = new SimpleStringProperty(this, "lastName");
        private final StringProperty email = new SimpleStringProperty(this, "email");

        public Person(String firstName, String lastName, String email) {
            this.firstName.set(firstName);
            this.lastName.set(lastName);
            this.email.set(email);
        }

        public final StringProperty firstNameProperty() {
            return this.firstName;
        }

        public final String getFirstName() {
            return this.firstNameProperty().get();
        }

        public final void setFirstName(final String firstName) {
            this.firstNameProperty().set(firstName);
        }

        public final StringProperty lastNameProperty() {
            return this.lastName;
        }

        public final String getLastName() {
            return this.lastNameProperty().get();
        }

        public final void setLastName(final String lastName) {
            this.lastNameProperty().set(lastName);
        }

        public final StringProperty emailProperty() {
            return this.email;
        }

        public final String getEmail() {
            return this.emailProperty().get();
        }

        public final void setEmail(final String email) {
            this.emailProperty().set(email);
        }

    }

    public static void main(String[] args) {
        launch(args);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Could you please explain more detailed where to put what. (Stupid question probably.) My IDE likes to create a class `tv` if I put the handler after `@FXML private TableView tableView;` – schasoli Feb 19 '15 at 22:29
  • Put the handlers in the controller's `initialize()` method. – James_D Feb 20 '15 at 03:33
  • 3
    Hi @James_D, I would like to ask: why are we using `SERIALIZED_MIME_TYPE ` ? What is it for ? – mynameisJEFF Aug 16 '15 at 10:19
  • That was an easy and quick implementation example. – Wesos de Queso Aug 11 '17 at 22:39
  • Thanks for the example @James_D! Question for anyone: If the number of table rows exceeds the table’s maximum height, and the dragged row Y-coordinate is below (or above) the table, is there a way to get the table's vertical scrollbar to auto-scroll? – mcs75 Oct 12 '19 at 03:20
  • I'm having problem with that implementation. In JavaFX 11 it seems like event.setDropCompleted(true) in onDragDropped doesn't work. After I drop a row the onDragOver event is still fired and the Dragboard still have it's contents. I even tried doing db.clear() in onDragDropped but it still doesn't work. – FilipK Apr 17 '20 at 09:32
7

using the answer from @James_D, I also created a multi-select verion.

import java.util.ArrayList;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewDragRows2 extends Application {

    private static final DataFormat SERIALIZED_MIME_TYPE = new DataFormat("application/x-java-serialized-object");
    private ArrayList<Person> selections = new ArrayList<>();
    @Override
    public void start(Stage primaryStage) {
        TableView<Person> tableView = new TableView<>();
        tableView.getColumns().add(createCol("First Name", Person::firstNameProperty, 150));
        tableView.getColumns().add(createCol("Last Name", Person::lastNameProperty, 150));
        tableView.getColumns().add(createCol("Email", Person::emailProperty, 200));
        tableView.getColumns().add(createCol("Country", Person::countryProperty, 200));
        tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        tableView.getItems().addAll(
            new Person("Jacob", "Smith", "jacob.smith@example.com","A"),
            new Person("Isabella", "Johnson", "isabella.johnson@example.com","A"),
            new Person("Ethan", "Williams", "ethan.williams@example.com","A"),
            new Person("Emma", "Jones", "emma.jones@example.com","B"),
            new Person("da", "Jones", "emma.jones@example.com","B"),
            new Person("csd", "Jones", "emma.jones@example.com","B"),
            new Person("dsf", "Jones", "emma.jones@example.com","B"),
            new Person("fsd", "Jones", "emma.jones@example.com","B"),
            new Person("feferef", "Jones", "emma.jones@example.com","B"),
            new Person("Michael", "Brown", "michael.brown@example.com","C"),
            new Person("XMan", "Brown", "michael.brown@example.com","C"),
            new Person("ZMan", "Brown", "michael.brown@example.com","D"),
            new Person("YMan", "Brown", "michael.brown@example.com","D"),
            new Person("DDDMan", "Brown", "michael.brown@example.com","D")
        );

        tableView.setRowFactory(tv -> {
            TableRow<Person> row = new TableRow<>();

            row.setOnDragDetected(event -> {
                if (! row.isEmpty()) {
                    Integer index = row.getIndex();

                    selections.clear();//important...

                    ObservableList<Person> items = tableView.getSelectionModel().getSelectedItems();

                    for(Person iI:items) {
                        selections.add(iI);
                    }


                    Dragboard db = row.startDragAndDrop(TransferMode.MOVE);
                    db.setDragView(row.snapshot(null, null));
                    ClipboardContent cc = new ClipboardContent();
                    cc.put(SERIALIZED_MIME_TYPE, index);
                    db.setContent(cc);
                    event.consume();
                }
            });

            row.setOnDragOver(event -> {
                Dragboard db = event.getDragboard();
                if (db.hasContent(SERIALIZED_MIME_TYPE)) {
                    if (row.getIndex() != ((Integer)db.getContent(SERIALIZED_MIME_TYPE)).intValue()) {
                        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                        event.consume();
                    }
                }
            });

            row.setOnDragDropped(event -> {
                Dragboard db = event.getDragboard();

                if (db.hasContent(SERIALIZED_MIME_TYPE)) {

                    int dropIndex;Person dI=null; 

                    if (row.isEmpty()) {
                        dropIndex = tableView.getItems().size() ;
                    } else {
                        dropIndex = row.getIndex();
                        dI = tableView.getItems().get(dropIndex);
                    }
                    int delta=0;
                    if(dI!=null)
                    while(selections.contains(dI)) {
                        delta=1;
                        --dropIndex;
                        if(dropIndex<0) {
                            dI=null;dropIndex=0;
                            break;
                        }
                        dI = tableView.getItems().get(dropIndex);
                    }

                    for(Person sI:selections) {
                        tableView.getItems().remove(sI);
                    }

                    if(dI!=null)
                        dropIndex=tableView.getItems().indexOf(dI)+delta;
                    else if(dropIndex!=0)
                        dropIndex=tableView.getItems().size();



                    tableView.getSelectionModel().clearSelection();

                    for(Person sI:selections) {
                        //draggedIndex = selections.get(i);
                        tableView.getItems().add(dropIndex, sI);
                        tableView.getSelectionModel().select(dropIndex);
                        dropIndex++;

                    }

                    event.setDropCompleted(true);
                    selections.clear();
                    event.consume();
                }
            });

            return row ;
        });


        Scene scene = new Scene(new BorderPane(tableView), 600, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private TableColumn<Person, String> createCol(String title, 
            Function<Person, ObservableValue<String>> mapper, double size) {

        TableColumn<Person, String> col = new TableColumn<>(title);
        col.setCellValueFactory(cellData -> mapper.apply(cellData.getValue()));
        col.setPrefWidth(size);

        return col ;
    }


   public class Person {
        private final StringProperty firstName = new SimpleStringProperty(this, "firstName");
        private final StringProperty lastName = new SimpleStringProperty(this, "lastName");
        private final StringProperty email = new SimpleStringProperty(this, "email");
        private final StringProperty country = new SimpleStringProperty(this, "country");;

        public Person(String firstName, String lastName, String email, String country) {
            this.firstName.set(firstName);
            this.lastName.set(lastName);
            this.email.set(email);
            this.country.set(country);
        }

        public final StringProperty firstNameProperty() {
            return this.firstName;
        }

        public final String getFirstName() {
            return this.firstNameProperty().get();
        }

        public final void setFirstName(final String firstName) {
            this.firstNameProperty().set(firstName);
        }

        public final StringProperty lastNameProperty() {
            return this.lastName;
        }

        public final String getLastName() {
            return this.lastNameProperty().get();
        }

        public final void setLastName(final String lastName) {
            this.lastNameProperty().set(lastName);
        }

        public final StringProperty emailProperty() {
            return this.email;
        }
        public final StringProperty countryProperty() {
            return this.country;
        }

        public final String getEmail() {
            return this.emailProperty().get();
        }

        public final void setEmail(final String email) {
            this.emailProperty().set(email);
        }

    }

    public static void main(String[] args) {
        launch(args);
    }
}

behave like this

KnIfER
  • 712
  • 7
  • 13
2

working the 1 answer (from James_D) into FMXL:

<TableView fx:id="tableView" GridPane.columnIndex="0" GridPane.rowIndex="1">

public class FXMLTableViewController implements Initializable {
    private static final DataFormat SERIALIZED_MIME_TYPE = new DataFormat("application/x-java-serialized-object");

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        tableView.setRowFactory(tv -> {
        ...
            return row ;
        });
    }

    @FXML
    private TableView<Person> tableView;
    ...
schasoli
  • 317
  • 1
  • 3
  • 12
  • I was just coming back to edit the answer to show the FXML-based version, but yes, that's exactly correct. – James_D Feb 20 '15 at 20:05