File browser improvements

This commit is contained in:
crschnick 2023-03-14 22:02:40 +00:00
parent 9e505c68ed
commit 355de8e2fc
9 changed files with 256 additions and 136 deletions

View file

@ -136,7 +136,7 @@ run {
systemProperty 'io.xpipe.app.writeLogs', "true"
systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "trace"
systemProperty 'io.xpipe.app.logLevel', "debug"
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
// systemProperty "io.xpipe.beacon.port", "21724"
// systemProperty "io.xpipe.beacon.printMessages", "true"

View file

@ -15,6 +15,7 @@ public class FileBrowserClipboard {
@Value
public static class Instance {
UUID uuid;
FileSystem.FileEntry baseDirectory;
List<FileSystem.FileEntry> entries;
}
@ -22,18 +23,18 @@ public class FileBrowserClipboard {
public static Instance currentDragClipboard;
@SneakyThrows
public static ClipboardContent startDrag(List<FileSystem.FileEntry> selected) {
public static ClipboardContent startDrag(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
var content = new ClipboardContent();
var idea = UUID.randomUUID();
currentDragClipboard = new Instance(idea, selected);
currentDragClipboard = new Instance(idea, base, new ArrayList<>(selected));
content.putString(idea.toString());
return content;
}
@SneakyThrows
public static void startCopy(List<FileSystem.FileEntry> selected) {
public static void startCopy(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
var id = UUID.randomUUID();
currentCopyClipboard = new Instance(id, new ArrayList<>(selected));
currentCopyClipboard = new Instance(id, base, new ArrayList<>(selected));
}
public static Instance retrieveCopy() {

View file

@ -55,9 +55,9 @@ final class FileContextMenu extends ContextMenu {
private final OpenFileSystemModel model;
private final FileSystem.FileEntry entry;
private final Property<String> editing;
private final Property<FileSystem.FileEntry> editing;
public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property<String> editing) {
public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property<FileSystem.FileEntry> editing) {
super();
this.model = model;
this.entry = entry;
@ -154,7 +154,7 @@ final class FileContextMenu extends ContextMenu {
var rename = new MenuItem("Rename");
rename.setOnAction(event -> {
event.consume();
editing.setValue(entry.getPath());
editing.setValue(entry);
});
getItems().addAll(new SeparatorMenuItem(), rename, delete);

View file

@ -5,7 +5,6 @@ package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
@ -13,6 +12,7 @@ import io.xpipe.app.util.Containers;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
@ -21,11 +21,11 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.input.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import java.io.File;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Comparator;
@ -39,6 +39,8 @@ final class FileListComp extends AnchorPane {
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
private static final PseudoClass FOLDER = PseudoClass.getPseudoClass("folder");
private static final PseudoClass DRAG = PseudoClass.getPseudoClass("drag");
private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass("drag-over");
private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current");
private static final String UNKNOWN = "unknown";
private final FileListModel fileList;
@ -57,7 +59,7 @@ final class FileListComp extends AnchorPane {
@SuppressWarnings("unchecked")
private TableView<FileSystem.FileEntry> createTable() {
var editing = new SimpleObjectProperty<String>();
var editing = new SimpleObjectProperty<FileSystem.FileEntry>();
var filenameCol = new TableColumn<FileSystem.FileEntry, String>("Name");
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
param.getValue() != null
@ -96,16 +98,15 @@ final class FileListComp extends AnchorPane {
}
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileSystem.FileEntry>) c -> {
fileList.getModel().getBrowserModel().getSelectedFiles().setAll(c.getList());
fileList.getSelected().setAll(c.getList());
fileList.getFileSystemModel().getBrowserModel().getSelectedFiles().setAll(c.getList());
});
var draggedOverDirectory = new SimpleBooleanProperty();
table.setOnKeyPressed(event -> {
if (event.isControlDown()
&& event.getCode().equals(KeyCode.C)
&& table.getSelectionModel().getSelectedItems().size() > 0) {
FileBrowserClipboard.startCopy(table.getSelectionModel().getSelectedItems());
FileBrowserClipboard.startCopy(fileList.getFileSystemModel().getCurrentDirectory(), table.getSelectionModel().getSelectedItems());
event.consume();
}
@ -113,15 +114,38 @@ final class FileListComp extends AnchorPane {
var clipboard = FileBrowserClipboard.retrieveCopy();
if (clipboard != null) {
var files = clipboard.getEntries();
var target = fileList.getModel().getCurrentDirectory();
fileList.getModel().dropFilesIntoAsync(target, files, true);
var target = fileList.getFileSystemModel().getCurrentDirectory();
fileList.getFileSystemModel().dropFilesIntoAsync(target, files, true);
event.consume();
}
}
});
var emptyEntry = new FileListEntry(table, null, fileList);
table.setOnDragOver(event -> {
emptyEntry.onDragOver(event);
});
table.setOnDragEntered(event -> {
emptyEntry.onDragEntered(event);
});
table.setOnDragDetected(event -> {
emptyEntry.startDrag(event);
});
table.setOnDragExited(event -> {
emptyEntry.onDragExited(event);
});
table.setOnDragDropped(event -> {
emptyEntry.onDragDrop(event);
});
table.setRowFactory(param -> {
TableRow<FileSystem.FileEntry> row = new TableRow<>();
var listEntry = Bindings.createObjectBinding(() -> new FileListEntry(row, row.getItem(), fileList), row.itemProperty());
row.itemProperty().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG, false);
row.pseudoClassStateChanged(DRAG_OVER, false);
});
row.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> {
t.consume();
@ -129,7 +153,7 @@ final class FileListComp extends AnchorPane {
return;
}
var cm = new FileContextMenu(fileList.getModel(), row.getItem(), editing);
var cm = new FileContextMenu(fileList.getFileSystemModel(), row.getItem(), editing);
if (t.getButton() == MouseButton.SECONDARY) {
cm.show(row, t.getScreenX(), t.getScreenY());
}
@ -141,113 +165,28 @@ final class FileListComp extends AnchorPane {
}
});
draggedOverDirectory.addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG, newValue);
fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG_OVER, newValue != null && newValue == row.getItem());
});
fileList.getDraggedOverEmpty().addListener((observable, oldValue, newValue) -> {
table.pseudoClassStateChanged(DRAG_INTO_CURRENT, newValue);
});
row.setOnDragEntered(event -> {
listEntry.get().onDragEntered(event);
});
row.setOnDragOver(event -> {
if (row.equals(event.getGestureSource())) {
return;
}
if (row.getItem() == null || !row.getItem().isDirectory()) {
draggedOverDirectory.set(true);
} else {
row.pseudoClassStateChanged(DRAG, true);
}
event.acceptTransferModes(TransferMode.ANY);
event.consume();
listEntry.get().onDragOver(event);
});
row.setOnDragDetected(event -> {
if (row.isEmpty()) {
return;
}
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "img/file_drag_icon.png")
.orElseThrow();
var image = new Image(url.toString(), 80, 80, true, false);
var selected = table.getSelectionModel().getSelectedItems();
Dragboard db = row.startDragAndDrop(TransferMode.COPY);
db.setContent(FileBrowserClipboard.startDrag(selected));
db.setDragView(image, 30, 60);
event.setDragDetect(true);
event.consume();
listEntry.get().startDrag(event);
});
row.setOnDragExited(event -> {
if (row.getItem() != null && row.getItem().isDirectory()) {
row.pseudoClassStateChanged(DRAG, false);
} else {
draggedOverDirectory.set(false);
}
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
}
// if (event.getGestureSource() != null) {
// try {
// var f = Files.createTempFile(null, null);
// var cc = new ClipboardContent();
// cc.putFiles(List.of(f.toFile()));
// Dragboard db = row.startDragAndDrop(TransferMode.COPY);
// db.setContent(cc);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// }
listEntry.get().onDragExited(event);
});
//
// row.setEventDispatcher((event, chain) -> {
// if (event.getEventType().getName().equals("MOUSE_DRAGGED")) {
// MouseEvent drag = (MouseEvent) event;
//
// if (drag.isDragDetect()) {
// return chain.dispatchEvent(event);
// }
//
// Rectangle area = new Rectangle(
// App.getApp().getStage().getX(),
// App.getApp().getStage().getY(),
// App.getApp().getStage().getWidth(),
// App.getApp().getStage().getHeight()
// );
// if (!area.intersects(drag.getScreenX(), drag.getScreenY(), 20, 20)) {
// System.out.println("->Drag down");
// drag.setDragDetect(true);
// }
// }
//
// return chain.dispatchEvent(event);
// });
row.setOnDragDropped(event -> {
draggedOverDirectory.set(false);
// Accept drops from outside the app window
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Dragboard db = event.getDragboard();
var list = db.getFiles().stream().map(File::toPath).toList();
var target = row.getItem() != null && row.getItem().isDirectory()
? row.getItem()
: fileList.getModel().getCurrentDirectory();
fileList.getModel().dropLocalFilesIntoAsync(target, list);
event.setDropCompleted(true);
event.consume();
}
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var files = FileBrowserClipboard.retrieveDrag(event.getDragboard())
.getEntries();
var target = row.getItem() != null
? row.getItem()
: fileList.getModel().getCurrentDirectory();
fileList.getModel().dropFilesIntoAsync(target, files, false);
event.setDropCompleted(true);
event.consume();
}
listEntry.get().onDragDrop(event);
});
return row;
@ -276,10 +215,10 @@ final class FileListComp extends AnchorPane {
new LazyTextFieldComp(text).createStructure().get();
private final ChangeListener<String> listener;
public FilenameCell(Property<String> editing) {
public FilenameCell(Property<FileSystem.FileEntry> editing) {
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null
&& getTableRow().getItem().getPath().equals(newValue)) {
&& getTableRow().getItem().equals(newValue)) {
textField.requestFocus();
}
});

View file

@ -0,0 +1,164 @@
package io.xpipe.app.browser;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileSystem;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import lombok.Getter;
import java.io.File;
import java.util.Timer;
import java.util.TimerTask;
@Getter
public class FileListEntry {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private final Node row;
private final FileSystem.FileEntry item;
private final FileListModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
public FileListEntry(Node row, FileSystem.FileEntry item, FileListModel model) {
this.row = row;
this.item = item;
this.model = model;
}
private boolean acceptsDrop(DragEvent event) {
if (FileBrowserClipboard.currentDragClipboard == null) {
return false;
}
// Prevent drag and drops of files into the current directory
if (FileBrowserClipboard.currentDragClipboard
.getBaseDirectory().getPath()
.equals(model.getFileSystemModel().getCurrentDirectory().getPath()) && (item == null || !item.isDirectory())) {
return false;
}
// Prevent dropping items onto themselves
if (item != null && FileBrowserClipboard.currentDragClipboard.getEntries().contains(item)) {
return false;
}
return true;
}
public void onDragDrop(DragEvent event) {
model.getDraggedOverEmpty().setValue(false);
model.getDraggedOverDirectory().setValue(null);
// Accept drops from outside the app window
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Dragboard db = event.getDragboard();
var list = db.getFiles().stream().map(File::toPath).toList();
var target = item != null && item.isDirectory()
? item
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropLocalFilesIntoAsync(target, list);
event.setDropCompleted(true);
event.consume();
}
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var files = FileBrowserClipboard.retrieveDrag(event.getDragboard()).getEntries();
var target = item != null
? item
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropFilesIntoAsync(target, files, false);
event.setDropCompleted(true);
event.consume();
}
}
public void onDragExited(DragEvent event) {
if (item != null && item.isDirectory()) {
model.getDraggedOverDirectory().setValue(null);
} else {
model.getDraggedOverEmpty().setValue(false);
}
event.consume();
}
public void startDrag(MouseEvent event) {
if (item == null) {
return;
}
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "img/file_drag_icon.png")
.orElseThrow();
var image = new Image(url.toString(), 80, 80, true, false);
var selected = model.getSelected();
Dragboard db = row.startDragAndDrop(TransferMode.COPY);
db.setContent(FileBrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected));
db.setDragView(image, 30, 60);
event.setDragDetect(true);
event.consume();
}
private void acceptDrag(DragEvent event) {
if (item == null || !item.isDirectory()) {
model.getDraggedOverEmpty().setValue(true);
} else {
model.getDraggedOverDirectory().setValue(item);
}
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
private void handleHoverTimer(DragEvent event) {
if (item == null || !item.isDirectory()) {
return;
}
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
@Override
public void run() {
if (activeTask != this) {
return;
}
if (item != model.getDraggedOverDirectory().getValue()) {
return;
}
model.getFileSystemModel().cd(item.getPath());
}
};
DROP_TIMER.schedule(activeTask, 1000);
}
public void onDragEntered(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {
return;
}
acceptDrag(event);
}
public void onDragOver(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {
return;
}
acceptDrag(event);
handleHoverTimer(event);
}
}

View file

@ -8,7 +8,10 @@ import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.util.ArrayList;
@ -25,24 +28,28 @@ final class FileListModel {
static final Predicate<FileSystem.FileEntry> PREDICATE_ANY = path -> true;
static final Predicate<FileSystem.FileEntry> PREDICATE_NOT_HIDDEN = path -> true;
private final OpenFileSystemModel model;
private final OpenFileSystemModel fileSystemModel;
private final Property<Comparator<FileSystem.FileEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
private final Property<List<FileSystem.FileEntry>> all = new SimpleObjectProperty<>(List.of());
private final Property<List<FileSystem.FileEntry>> shown = new SimpleObjectProperty<>(List.of());
private final ObjectProperty<Predicate<FileSystem.FileEntry>> predicateProperty =
new SimpleObjectProperty<>(path -> true);
private final ObservableList<FileSystem.FileEntry> selected = FXCollections.observableArrayList();
public FileListModel(OpenFileSystemModel model) {
this.model = model;
private final Property<FileSystem.FileEntry> draggedOverDirectory = new SimpleObjectProperty<FileSystem.FileEntry>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
model.getFilter().addListener((observable, oldValue, newValue) -> {
public FileListModel(OpenFileSystemModel fileSystemModel) {
this.fileSystemModel = fileSystemModel;
fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {
refreshShown();
});
}
public FileBrowserModel.Mode getMode() {
return model.getBrowserModel().getMode();
return fileSystemModel.getBrowserModel().getMode();
}
public void setAll(List<FileSystem.FileEntry> newFiles) {
@ -56,9 +63,9 @@ final class FileListModel {
}
private void refreshShown() {
List<FileSystem.FileEntry> filtered = model.getFilter().getValue() != null ? all.getValue().stream().filter(entry -> {
List<FileSystem.FileEntry> filtered = fileSystemModel.getFilter().getValue() != null ? all.getValue().stream().filter(entry -> {
var name = FileNames.getFileName(entry.getPath()).toLowerCase(Locale.ROOT);
var filterString = model.getFilter().getValue().toLowerCase(Locale.ROOT);
var filterString = fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
return name.contains(filterString);
}).toList() : all.getValue();
@ -72,11 +79,11 @@ final class FileListModel {
}
public boolean rename(String filename, String newName) {
var fullPath = FileNames.join(model.getCurrentPath().get(), filename);
var newFullPath = FileNames.join(model.getCurrentPath().get(), newName);
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), filename);
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
try {
model.getFileSystem().move(fullPath, newFullPath);
model.refresh();
fileSystemModel.getFileSystem().move(fullPath, newFullPath);
fileSystemModel.refresh();
return true;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
@ -86,12 +93,12 @@ final class FileListModel {
public void onDoubleClick(FileSystem.FileEntry entry) {
if (!entry.isDirectory() && getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
getModel().getBrowserModel().finishChooser();
getFileSystemModel().getBrowserModel().finishChooser();
return;
}
if (entry.isDirectory()) {
model.navigate(entry.getPath(), true);
fileSystemModel.navigate(entry.getPath(), true);
} else {
FileOpener.openInTextEditor(entry);
}

View file

@ -63,6 +63,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ApplicationHelper.checkSupport(pc, executable, getDisplayName());
var toExecute = executable + " " + toCommand(name, command);
// In order to fix this bug which also affects us: https://askubuntu.com/questions/1148475/launching-gnome-terminal-from-vscode
toExecute = "GNOME_TERMINAL_SCREEN=\"\" nohup " + toExecute + " </dev/null &>/dev/null & disown";
pc.executeSimpleCommand(toExecute);
}

View file

@ -42,6 +42,15 @@
-fx-opacity: 0.6;
}
.browser .table-row-cell:drag {
-fx-background-color: -color-neutral-emphasis;
.browser .table-directory-view .table-view:drag-into-current {
-fx-border-color: -color-success-muted;
-fx-border-width: 2px;
}
.browser .table-row-cell:drag-over {
-fx-background-color: -color-success-muted;
}
.browser .table-row-cell:drag {
-fx-background-color: -color-accent-muted;
}

View file

@ -55,7 +55,6 @@
.list-cell:empty {
-fx-opacity: 0;
-fx-pref-height: 1px;
}
.storage-group-list-comp .list-cell {