Search code examples
javafx

JavaFX Auto Scroll Table Up or Down When Dragging Rows Outside Of Viewport


I've got a table view which you can drag rows to re-position the data. The issue is getting the table view to auto scroll up or down when dragging the row above or below the records within the view port.

Any ideas how this can be achieved within JavaFX?

categoryProductsTable.setRowFactory(tv -> {

        TableRow<EasyCatalogueRow> row = new TableRow<EasyCatalogueRow>();

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

        row.setOnDragOver(event -> {
            Dragboard db = event.getDragboard();
            if (db.hasContent(SERIALIZED_MIME_TYPE)) {
                event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                event.consume();

            }
        });

        row.setOnDragDropped(event -> {
            Dragboard db = event.getDragboard();
            if (db.hasContent(SERIALIZED_MIME_TYPE)) {
                int dropIndex;

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

                ArrayList<Integer> indexes = (ArrayList<Integer>) db.getContent(SERIALIZED_MIME_TYPE);

                for (int index : indexes) {
                    EasyCatalogueRow draggedProduct = categoryProductsTable.getItems().remove(index);
                    categoryProductsTable.getItems().add(dropIndex, draggedProduct);
                    dropIndex++;
                }

                event.setDropCompleted(true);
                categoryProductsTable.getSelectionModel().select(null);
                event.consume();

                updateSortIndicies();

            }
        });

        return row;
    });

Solution

  • Ok, so I figured it out. Not sure it's the best way to do it but it works. Basically I added an event listener to the table view which handles the DragOver event. This event is fired whilst dragging the rows within the table view.

    Essentially, whilst the drag is being performed, I work out if we need to scroll up or down or not scroll at all. This is done by working out if the items being dragged are within either the upper or lower proximity areas of the table view.

    A separate thread controlled by the DragOver event listener then handles the scrolling.

    public class CategoryProductsReportController extends ReportController implements Initializable {
    
        @FXML
        private TableView<EasyCatalogueRow> categoryProductsTable;
    
        private ObservableList<EasyCatalogueRow> categoryProducts = FXCollections.observableArrayList();
    
        public enum ScrollMode {
            UP, DOWN, NONE
        }
    
        private AutoScrollableTableThread autoScrollThread = null;
    
        /**
         * Initializes the controller class.
         */
        @Override
        public void initialize(URL url, ResourceBundle rb) {
    
            initProductTable();
    
        }
    
        private void initProductTable() {
    
            categoryProductsTable.setItems(categoryProducts);
            ...
    
            ...
    
            // Multi Row Drag And Drop To Allow Items To Be Re-Positioned Within 
            // Table
            categoryProductsTable.setRowFactory(tv -> {
    
                TableRow<EasyCatalogueRow> row = new TableRow<EasyCatalogueRow>();
    
                row.setOnDragDetected(event -> {
                    if (!row.isEmpty()) {
                        Dragboard db = row.startDragAndDrop(TransferMode.MOVE);
                        db.setDragView(row.snapshot(null, null));
                        ClipboardContent cc = new ClipboardContent();
                        cc.put(SERIALIZED_MIME_TYPE, new ArrayList<Integer>(categoryProductsTable.getSelectionModel().getSelectedIndices()));
                        db.setContent(cc);
                        event.consume();
                    }
                });
    
                row.setOnDragOver(event -> {
                    Dragboard db = event.getDragboard();
                    if (db.hasContent(SERIALIZED_MIME_TYPE)) {
                        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                        event.consume();
    
                    }
                });
    
                row.setOnDragDropped(event -> {
                    Dragboard db = event.getDragboard();
                    if (db.hasContent(SERIALIZED_MIME_TYPE)) {
                        int dropIndex;
    
                        if (row.isEmpty()) {
                            dropIndex = categoryProductsTable.getItems().size();
                        } else {
                            dropIndex = row.getIndex();
                        }
    
                        ArrayList<Integer> indexes = (ArrayList<Integer>) db.getContent(SERIALIZED_MIME_TYPE);
    
                        for (int index : indexes) {
                            EasyCatalogueRow draggedProduct = categoryProductsTable.getItems().remove(index);
                            categoryProductsTable.getItems().add(dropIndex, draggedProduct);
                            dropIndex++;
                        }
    
                        event.setDropCompleted(true);
                        categoryProductsTable.getSelectionModel().select(null);
                        event.consume();
    
                        updateSortIndicies();
    
                    }
                });
    
                return row;
            });
    
            categoryProductsTable.addEventFilter(DragEvent.DRAG_DROPPED, event -> {
                if (autoScrollThread != null) {
                    autoScrollThread.stopScrolling();
                    autoScrollThread = null;
                }
            });
    
            categoryProductsTable.addEventFilter(DragEvent.DRAG_OVER, event -> {
    
                double proximity = 100;
    
                Bounds tableBounds = categoryProductsTable.getLayoutBounds();
    
                double dragY = event.getY();
    
                //System.out.println(tableBounds.getMinY() + " --> " + tableBounds.getMaxY() + " --> " + dragY);
                // Area At Top Of Table View. i.e Initiate Upwards Auto Scroll If
                // We Detect Anything Being Dragged Above This Line.
                double topYProximity = tableBounds.getMinY() + proximity;
    
                // Area At Bottom Of Table View. i.e Initiate Downwards Auto Scroll If
                // We Detect Anything Being Dragged Below This Line.
                double bottomYProximity = tableBounds.getMaxY() - proximity;
    
                // We Now Make Use Of A Thread To Scroll The Table Up Or Down If
                // The Objects Being Dragged Are Within The Upper Or Lower
                // Proximity Areas
                if (dragY < topYProximity) {
                    // We Need To Scroll Up
                    if (autoScrollThread == null) {
                        autoScrollThread = new AutoScrollableTableThread(categoryProductsTable);
                        autoScrollThread.scrollUp();
                        autoScrollThread.start();
                    }
    
                } else if (dragY > bottomYProximity) {
                    // We Need To Scroll Down
                    if (autoScrollThread == null) {
                        autoScrollThread = new AutoScrollableTableThread(categoryProductsTable);
                        autoScrollThread.scrollDown();
                        autoScrollThread.start();
                    }
    
                } else {
                    // No Auto Scroll Required We Are Within Bounds
                    if (autoScrollThread != null) {
                        autoScrollThread.stopScrolling();
                        autoScrollThread = null;
                    }
                }
    
            });
    
        }
    }
    
    class AutoScrollableTableThread extends Thread {
    
        private boolean running = true;
        private ScrollMode scrollMode = ScrollMode.NONE;
        private ScrollBar verticalScrollBar = null;
    
        public AutoScrollableTableThread(TableView tableView) {
            super();
            setDaemon(true);
            verticalScrollBar = (ScrollBar) tableView.lookup(".scroll-bar:vertical");
    
        }
    
        @Override
        public void run() {
    
            try {
                Thread.sleep(300);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
    
            while (running) {
    
                Platform.runLater(() -> {
                    if (verticalScrollBar != null && scrollMode == ScrollMode.UP) {
                        verticalScrollBar.setValue(verticalScrollBar.getValue() - 0.01);
                    } else if (verticalScrollBar != null && scrollMode == ScrollMode.DOWN) {
                        verticalScrollBar.setValue(verticalScrollBar.getValue() + 0.01);
                    }
                });
    
                try {
                    sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void scrollUp() {
            System.out.println("Start To Scroll Up");
            scrollMode = ScrollMode.UP;
            running = true;
        }
    
        public void scrollDown() {
            System.out.println("Start To Scroll Down");
            scrollMode = ScrollMode.DOWN;
            running = true;
        }
    
        public void stopScrolling() {
            System.out.println("Stop Scrolling");
            running = false;
            scrollMode = ScrollMode.NONE;
        }
    
    }