Skip to content

Commit

Permalink
Merge pull request #567 from IdelsTak/master
Browse files Browse the repository at this point in the history
Improve file ordering on DnD for SelectionTable
  • Loading branch information
torakiki committed Dec 14, 2023
2 parents c3fd95b + 34cc1bf commit 9acef48
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 19 deletions.
8 changes: 8 additions & 0 deletions pdfsam-themes/src/main/resources/themes/light/theme.css
Expand Up @@ -476,6 +476,14 @@ btn
-fx-min-height: 15.0em;
}


.table-row-cell:drag-hovered-row {
-fx-border-color: -category-others-color;
-fx-border-radius: 0.583333em;
-fx-border-style: dashed;
-fx-border-width: 0.11111em;
}

/**
* Selection table toolbar
*/
Expand Down
Expand Up @@ -68,18 +68,13 @@
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.beans.property.*;
import javafx.css.PseudoClass;
import javafx.scene.*;

import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
Expand All @@ -100,13 +95,14 @@
public class SelectionTable extends TableView<SelectionTableRowData> implements ToolBound, RestorableView {

private static final Logger LOG = LoggerFactory.getLogger(SelectionTable.class);

private static final PseudoClass DRAG_HOVERED_ROW_PSEUDO_CLASS = PseudoClass.getPseudoClass("drag-hovered-row");
private static final DataFormat DND_TABLE_SELECTION_MIME_TYPE = new DataFormat(
"application/x-java-table-selection-list");

private String toolBinding = StringUtils.EMPTY;
private final Label placeHolder = new Label(i18n().tr("Drag and drop PDF files here"));
private final PasswordFieldPopup passwordPopup;
private final IntegerProperty hoverIndex = new SimpleIntegerProperty(-1);
private Consumer<SelectionChangedEvent> selectionChangedConsumer;

public SelectionTable(String toolBinding, boolean canDuplicateItems, boolean canMove,
Expand Down Expand Up @@ -265,6 +261,24 @@ private MenuItem createMenuItem(String text, Ikon icon) {
}

private void initDragAndDrop(boolean canMove) {

hoverIndex.addListener((observable, oldIndex, newIndex) -> {
List<Node> cells = new ArrayList<>(lookupAll(".table-row-cell"));
int newIndexValue = newIndex.intValue();

if (newIndexValue == -1) {
cells.forEach(cell -> cell.pseudoClassStateChanged(DRAG_HOVERED_ROW_PSEUDO_CLASS, false));
return;
}

for (int i = 0; i < cells.size(); i++) {
TableRow<?> row = (TableRow<?>) cells.get(i);
boolean hovered = i == newIndexValue;

row.pseudoClassStateChanged(DRAG_HOVERED_ROW_PSEUDO_CLASS, hovered);
}
});

setOnDragOver(e -> dragConsume(e, this.onDragOverConsumer()));
setOnDragEntered(e -> dragConsume(e, this.onDragEnteredConsumer()));
setOnDragExited(this::onDragExited);
Expand Down Expand Up @@ -358,7 +372,10 @@ private void dragConsume(DragEvent e, Consumer<DragEvent> c) {
}

private Consumer<DragEvent> onDragOverConsumer() {
return (DragEvent e) -> e.acceptTransferModes(TransferMode.COPY_OR_MOVE);
return (DragEvent e) -> {
hoverIndex.set(calculateHoverIndex(e));
e.acceptTransferModes(TransferMode.COPY_OR_MOVE);
};
}

private Consumer<DragEvent> onDragEnteredConsumer() {
Expand Down Expand Up @@ -386,8 +403,20 @@ public String toolBinding() {

@EventListener(priority = Integer.MIN_VALUE)
public void onLoadDocumentsRequest(PdfLoadRequest loadEvent) {
getItems().addAll(loadEvent.getDocuments().stream().map(SelectionTableRowData::new).toList());
var focus = ofNullable(getFocusModel().getFocusedItem());
var toDrop = loadEvent.getDocuments().stream().map(SelectionTableRowData::new).toList();
var hoverIndexValue = hoverIndex.get();
var itemsCount = getItems().size();
var dropIndex = (hoverIndexValue < 0 || hoverIndexValue > itemsCount)
? itemsCount
: hoverIndexValue;

getSortOrder().clear();
getItems().addAll(dropIndex, toDrop);
focus.map(getItems()::indexOf).ifPresent(getFocusModel()::focus);
hoverIndex.setValue(-1);
this.sort();

loadEvent.getDocuments().stream().findFirst().ifPresent(
f -> eventStudio().broadcast(requestFallbackDestination(f.getFile(), toolBinding()), toolBinding()));
eventStudio().broadcast(loadEvent);
Expand Down Expand Up @@ -456,7 +485,7 @@ private void copySelectedToClipboard() {
ClipboardContent content = new ClipboardContent();
writeContent(getSelectionModel().getSelectedItems().stream()
.map(item -> item.descriptor().getFile().getAbsolutePath() + ", " + item.descriptor().getFile().length()
+ ", " + item.descriptor().pages().getValue()).collect(Collectors.toList())).to(content);
+ ", " + item.descriptor().pages().getValue()).collect(Collectors.toList())).to(content);
Clipboard.getSystemClipboard().setContent(content);
}

Expand Down Expand Up @@ -502,4 +531,24 @@ public void restoreStateFrom(Map<String, String> data) {
}

}

private int calculateHoverIndex(DragEvent event) {
var mouseY = event.getY();
var totalHeight = 0.0;
var cells = new ArrayList<>(lookupAll(".table-row-cell"));

for (var i = 0; i < cells.size(); i++) {
var row = (TableRow<?>) cells.get(i);

if (row != null) {
totalHeight += row.getHeight();

if (mouseY <= totalHeight) {
return i - 1;
}
}
}

return - 1;
}
}
@@ -0,0 +1,86 @@
/*
* This file is part of the PDF Split And Merge source code
* Created on 27/nov/2013
* Copyright 2017 by Sober Lemur S.r.l. (info@soberlemur.com).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.pdfsam.ui.components.selection.multiple;

import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.Collections;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.VBox;

class MockFileExplorer extends VBox {

MockFileExplorer(Path tempFolder) throws URISyntaxException, IOException {
var fileList = new ListView<File>();

fileList.setId("file-list");
fileList.setCellFactory(param -> new FileListCell());
fileList.setOnDragDetected(event -> onDragDetected(event, fileList));
fileList.setOnDragOver(this::onDragOver);

var pdfs = new File[]{
Files.createFile(tempFolder.resolve("1.pdf")).toFile(),
Files.createFile(tempFolder.resolve("2.pdf")).toFile()
};

fileList.getItems().addAll(pdfs);

getChildren().add(fileList);
}

private void onDragDetected(MouseEvent event, ListView<File> fileList) {
var selectedIndex = fileList.getSelectionModel().getSelectedIndex();

if (selectedIndex >= 0) {
var file = fileList.getItems().get(selectedIndex);
var dragboard = fileList.startDragAndDrop(TransferMode.COPY);
var content = new ClipboardContent();

content.put(DataFormat.FILES, Collections.singletonList(file));
dragboard.setContent(content);

event.consume();
}
}

private void onDragOver(DragEvent event) {
var dragboard = event.getDragboard();

if (dragboard.hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
}
}

private static class FileListCell extends ListCell<File> {

@Override
protected void updateItem(File item, boolean empty) {
super.updateItem(item, empty);

if (empty) {
setText(null);
} else {
setText(item.getName());
}
}
}

}
Expand Up @@ -58,13 +58,16 @@
import org.testfx.util.WaitForAsyncUtils;

import java.io.File;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import javafx.application.Platform;
import javafx.scene.*;
import javafx.scene.control.*;
import org.pdfsam.model.ui.dnd.FilesDroppedEvent;

import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand All @@ -79,6 +82,7 @@
import static org.pdfsam.core.context.ApplicationContext.app;
import static org.pdfsam.eventstudio.StaticStudio.eventStudio;
import static org.pdfsam.i18n.I18nContext.i18n;
import static org.pdfsam.model.pdf.PdfDocumentDescriptor.newDescriptorNoPassword;
import static org.testfx.api.FxAssert.verifyThat;

/**
Expand All @@ -105,8 +109,8 @@ public static void setUp() {
@Start
public void start(Stage stage) throws Exception {
victim = new SelectionTable(MODULE, true, true,
new SelectionTableColumn<?>[] { new LoadingColumn(MODULE), FileColumn.NAME, LongColumn.SIZE,
IntColumn.PAGES, LongColumn.LAST_MODIFIED, new PageRangesColumn() });
new SelectionTableColumn<?>[]{new LoadingColumn(MODULE), FileColumn.NAME, LongColumn.SIZE,
IntColumn.PAGES, LongColumn.LAST_MODIFIED, new PageRangesColumn()});
victim.setId("victim");
firstItem = populate();
Scene scene = new Scene(victim);
Expand Down Expand Up @@ -380,6 +384,65 @@ public void duplicate() {
victim.getItems().stream().filter(i -> "temp.pdf".equals(i.descriptor().getFileName())).count());
}

@Test
@Tag("NoHeadless")
public void dropsOnFirstIndex() throws Exception {
MockFileExplorer mockFileExplorer;

try {
mockFileExplorer = new MockFileExplorer(folder);
} catch (URISyntaxException ex) {
throw new RuntimeException(ex);
}

Platform.runLater(() -> {
var stage = new Stage();
var ownerStage = (Stage) victim.getScene().getWindow();
var offset = 20;
var ownerX = ownerStage.getX();
var ownerWidth = ownerStage.getWidth();
var stageX = ownerX + ownerWidth + offset;
var scene = new Scene(mockFileExplorer);

stage.setX(stageX);
stage.setScene(scene);
stage.show();
});

WaitForAsyncUtils.waitForFxEvents();

var listView = robot.lookup("#file-list").queryAs(ListView.class);
var cells = listView.lookupAll(".list-cell");
var cell = cells.stream()
.filter(ListCell.class::isInstance)
.map(ListCell.class::cast)
.filter(c -> Objects.nonNull(c.getItem()))
.findFirst()
.orElseThrow();

robot.drag(cell).dropTo("temp.pdf");

WaitForAsyncUtils.waitForFxEvents();

var request = new PdfLoadRequest(MODULE);
var descriptor = newDescriptorNoPassword((File) cell.getItem());
request.add(descriptor);

WaitForAsyncUtils.waitForFxEvents();

victim.onLoadDocumentsRequest(request);

WaitForAsyncUtils.waitForFxEvents();

var actualDescriptor = victim.getItems()
.stream()
.map(SelectionTableRowData::descriptor)
.findFirst()
.orElseThrow();

assertThat(actualDescriptor).isEqualTo(descriptor);
}

@Test
public void moveSelected() {
robot.clickOn("temp.pdf");
Expand Down

0 comments on commit 9acef48

Please sign in to comment.