Prechádzať zdrojové kódy

- allow reordering of directories via drag'n'drop

Sebastian Stenzel 10 rokov pred
rodič
commit
4f91adb822

+ 1 - 2
main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java

@@ -3,7 +3,6 @@ package org.cryptomator.ui.controls;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 import javafx.scene.control.ContentDisplay;
-import javafx.scene.control.ListCell;
 import javafx.scene.control.Tooltip;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
@@ -11,7 +10,7 @@ import javafx.scene.shape.Circle;
 
 import org.cryptomator.ui.model.Directory;
 
-public class DirectoryListCell extends ListCell<Directory> implements ChangeListener<Boolean> {
+public class DirectoryListCell extends DraggableListCell<Directory> implements ChangeListener<Boolean> {
 
 	// fill: #FD4943, stroke: #E1443F
 	private static final Color RED_FILL = Color.rgb(253, 73, 67);

+ 132 - 0
main/ui/src/main/java/org/cryptomator/ui/controls/DraggableListCell.java

@@ -0,0 +1,132 @@
+package org.cryptomator.ui.controls;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.geometry.Insets;
+import javafx.scene.SnapshotParameters;
+import javafx.scene.control.ListCell;
+import javafx.scene.image.Image;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.DragEvent;
+import javafx.scene.input.Dragboard;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.TransferMode;
+import javafx.scene.layout.Border;
+import javafx.scene.layout.BorderImage;
+import javafx.scene.layout.BorderStroke;
+import javafx.scene.layout.BorderStrokeStyle;
+import javafx.scene.layout.BorderWidths;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+class DraggableListCell<T> extends ListCell<T> {
+
+	private static final double DROP_LINE_WIDTH = 4.0;
+	private static final Paint DROP_LINE_COLOR = Color.gray(0.0, 0.6);
+
+	private final List<BorderStroke> defaultBorderStrokes;
+	private final List<BorderImage> defaultBorderImages;
+
+	public DraggableListCell() {
+		setOnDragDetected(this::onDragDetected);
+		setOnDragOver(this::onDragOver);
+		setOnDragEntered(this::onDragEntered);
+		setOnDragExited(this::onDragExited);
+		setOnDragDropped(this::onDragDropped);
+		setOnDragDone(DragEvent::consume);
+		this.defaultBorderStrokes = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getStrokes();
+		this.defaultBorderImages = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getImages();
+	}
+
+	private Border createDropPositionBorder(double verticalCursorPosition) {
+		boolean isUpperHalf = verticalCursorPosition < this.getHeight() / 2.0;
+		final double topBorder = isUpperHalf ? DROP_LINE_WIDTH : 0.0;
+		final double bottomBorder = !isUpperHalf ? DROP_LINE_WIDTH : 0.0;
+		final BorderWidths borderWidths = new BorderWidths(topBorder, 0.0, bottomBorder, 0.0);
+		final BorderStroke dragStroke = new BorderStroke(DROP_LINE_COLOR, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, borderWidths, Insets.EMPTY);
+		final List<BorderStroke> strokes = new ArrayList<BorderStroke>(defaultBorderStrokes);
+		strokes.add(0, dragStroke);
+		return new Border(strokes, defaultBorderImages);
+	}
+
+	private void onDragDetected(MouseEvent event) {
+		if (getItem() == null) {
+			return;
+		}
+
+		final ClipboardContent content = new ClipboardContent();
+		content.putString(Integer.toString(getIndex()));
+		final Image snapshot = this.snapshot(new SnapshotParameters(), null);
+		final Dragboard dragboard = startDragAndDrop(TransferMode.MOVE);
+		dragboard.setDragView(snapshot);
+		dragboard.setContent(content);
+
+		event.consume();
+	}
+
+	private void onDragOver(DragEvent event) {
+		if (getItem() == null) {
+			return;
+		}
+
+		if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
+			event.acceptTransferModes(TransferMode.MOVE);
+			setBorder(createDropPositionBorder(event.getY()));
+		}
+
+		event.consume();
+	}
+
+	private void onDragEntered(DragEvent event) {
+		if (getItem() == null) {
+			return;
+		}
+
+		if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
+			setBorder(createDropPositionBorder(event.getY()));
+		}
+	}
+
+	private void onDragExited(DragEvent event) {
+		if (getItem() == null) {
+			return;
+		}
+
+		if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
+			setBorder(new Border(defaultBorderStrokes, defaultBorderImages));
+		}
+	}
+
+	private void onDragDropped(DragEvent event) {
+		if (getItem() == null) {
+			return;
+		}
+
+		if (event.getGestureSource() instanceof DraggableListCell<?> && event.getDragboard().hasString()) {
+			final List<T> list = getListView().getItems();
+			try {
+				// where to insert what?
+				final int draggedIdx = Integer.parseInt(event.getDragboard().getString());
+				final T currentItem = this.getItem();
+				final T draggedItem = list.remove(draggedIdx);
+				final int currentItemIdx = list.indexOf(currentItem);
+
+				// insert before or after currentItem?
+				boolean insertBefore = event.getY() < this.getHeight() / 2.0;
+				final int insertPosition = insertBefore ? currentItemIdx : currentItemIdx + 1;
+
+				// insert!
+				getListView().getItems().add(insertPosition, draggedItem);
+				getListView().getSelectionModel().select(insertPosition);
+				event.setDropCompleted(true);
+			} catch (NumberFormatException e) {
+				event.setDropCompleted(false);
+			}
+		}
+
+		event.consume();
+	}
+}