More file browser improvements [release]

This commit is contained in:
crschnick 2023-03-15 17:33:07 +00:00
parent 355de8e2fc
commit d5e024c43e
8 changed files with 101 additions and 57 deletions

View file

@ -22,14 +22,13 @@ import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.stream.Stream;
import static io.xpipe.app.util.HumanReadableFormat.byteCount; import static io.xpipe.app.util.HumanReadableFormat.byteCount;
import static javafx.scene.control.TableColumn.SortType.ASCENDING; import static javafx.scene.control.TableColumn.SortType.ASCENDING;
@ -41,7 +40,6 @@ final class FileListComp extends AnchorPane {
private static final PseudoClass DRAG = PseudoClass.getPseudoClass("drag"); private static final PseudoClass DRAG = PseudoClass.getPseudoClass("drag");
private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass("drag-over"); 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 PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current");
private static final String UNKNOWN = "unknown";
private final FileListModel fileList; private final FileListModel fileList;
@ -59,7 +57,6 @@ final class FileListComp extends AnchorPane {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private TableView<FileSystem.FileEntry> createTable() { private TableView<FileSystem.FileEntry> createTable() {
var editing = new SimpleObjectProperty<FileSystem.FileEntry>();
var filenameCol = new TableColumn<FileSystem.FileEntry, String>("Name"); var filenameCol = new TableColumn<FileSystem.FileEntry, String>("Name");
filenameCol.setCellValueFactory(param -> new SimpleStringProperty( filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
param.getValue() != null param.getValue() != null
@ -67,7 +64,7 @@ final class FileListComp extends AnchorPane {
: null)); : null));
filenameCol.setComparator(Comparator.comparing(String::toLowerCase)); filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
filenameCol.setSortType(ASCENDING); filenameCol.setSortType(ASCENDING);
filenameCol.setCellFactory(col -> new FilenameCell(editing)); filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
var sizeCol = new TableColumn<FileSystem.FileEntry, Number>("Size"); var sizeCol = new TableColumn<FileSystem.FileEntry, Number>("Size");
sizeCol.setCellValueFactory( sizeCol.setCellValueFactory(
@ -91,22 +88,29 @@ final class FileListComp extends AnchorPane {
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5)); filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5));
if (fileList.getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER) || fileList.getMode().equals(FileBrowserModel.Mode.DIRECTORY_CHOOSER)) { if (fileList.getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)
|| fileList.getMode().equals(FileBrowserModel.Mode.DIRECTORY_CHOOSER)) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
} else { } else {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
} }
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileSystem.FileEntry>) c -> { table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileSystem.FileEntry>)
fileList.getSelected().setAll(c.getList()); c -> {
fileList.getFileSystemModel().getBrowserModel().getSelectedFiles().setAll(c.getList()); fileList.getSelected().setAll(c.getList());
}); fileList.getFileSystemModel()
.getBrowserModel()
.getSelectedFiles()
.setAll(c.getList());
});
table.setOnKeyPressed(event -> { table.setOnKeyPressed(event -> {
if (event.isControlDown() if (event.isControlDown()
&& event.getCode().equals(KeyCode.C) && event.getCode().equals(KeyCode.C)
&& table.getSelectionModel().getSelectedItems().size() > 0) { && table.getSelectionModel().getSelectedItems().size() > 0) {
FileBrowserClipboard.startCopy(fileList.getFileSystemModel().getCurrentDirectory(), table.getSelectionModel().getSelectedItems()); FileBrowserClipboard.startCopy(
fileList.getFileSystemModel().getCurrentDirectory(),
table.getSelectionModel().getSelectedItems());
event.consume(); event.consume();
} }
@ -140,31 +144,20 @@ final class FileListComp extends AnchorPane {
table.setRowFactory(param -> { table.setRowFactory(param -> {
TableRow<FileSystem.FileEntry> row = new TableRow<>(); TableRow<FileSystem.FileEntry> row = new TableRow<>();
var listEntry = Bindings.createObjectBinding(() -> new FileListEntry(row, row.getItem(), fileList), row.itemProperty()); var listEntry = Bindings.createObjectBinding(
() -> new FileListEntry(row, row.getItem(), fileList), row.itemProperty());
row.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue && listEntry.get().isSynthetic()) {
row.updateSelected(false);
}
});
row.itemProperty().addListener((observable, oldValue, newValue) -> { row.itemProperty().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG, false); row.pseudoClassStateChanged(DRAG, false);
row.pseudoClassStateChanged(DRAG_OVER, false); row.pseudoClassStateChanged(DRAG_OVER, false);
}); });
row.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> {
t.consume();
if (row.isEmpty()) {
return;
}
var cm = new FileContextMenu(fileList.getFileSystemModel(), row.getItem(), editing);
if (t.getButton() == MouseButton.SECONDARY) {
cm.show(row, t.getScreenX(), t.getScreenY());
}
});
row.setOnMouseClicked(e -> {
if (e.getClickCount() == 2 && !row.isEmpty()) {
fileList.onDoubleClick(row.getItem());
}
});
fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> { fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG_OVER, newValue != null && newValue == row.getItem()); row.pseudoClassStateChanged(DRAG_OVER, newValue != null && newValue == row.getItem());
}); });
@ -173,6 +166,16 @@ final class FileListComp extends AnchorPane {
table.pseudoClassStateChanged(DRAG_INTO_CURRENT, newValue); table.pseudoClassStateChanged(DRAG_INTO_CURRENT, newValue);
}); });
row.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> {
listEntry.get().onMouseClick(t);
});
row.setOnMouseClicked(e -> {
if (e.getClickCount() == 2 && !row.isEmpty()) {
fileList.onDoubleClick(row.getItem());
}
});
row.setOnDragEntered(event -> { row.setOnDragEntered(event -> {
listEntry.get().onDragEntered(event); listEntry.get().onDragEntered(event);
}); });
@ -194,7 +197,13 @@ final class FileListComp extends AnchorPane {
SimpleChangeListener.apply(fileList.getShown(), (newValue) -> { SimpleChangeListener.apply(fileList.getShown(), (newValue) -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
table.getItems().setAll(newValue); var newItems = new ArrayList<FileSystem.FileEntry>();
var parentEntry = fileList.getFileSystemModel().getCurrentParentDirectory();
if (parentEntry != null) {
newItems.add(parentEntry);
}
newItems.addAll(newValue);
table.getItems().setAll(newItems);
if (newValue.size() > 0) { if (newValue.size() > 0) {
table.scrollTo(0); table.scrollTo(0);
} }
@ -217,8 +226,7 @@ final class FileListComp extends AnchorPane {
public FilenameCell(Property<FileSystem.FileEntry> editing) { public FilenameCell(Property<FileSystem.FileEntry> editing) {
editing.addListener((observable, oldValue, newValue) -> { editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
&& getTableRow().getItem().equals(newValue)) {
textField.requestFocus(); textField.requestFocus();
} }
}); });
@ -257,7 +265,7 @@ final class FileListComp extends AnchorPane {
pseudoClassStateChanged(FOLDER, isDirectory); pseudoClassStateChanged(FOLDER, isDirectory);
var fileName = FileNames.getFileName(fullPath); var fileName = getTableRow().getItem().equals(fileList.getFileSystemModel().getCurrentParentDirectory()) ? ".." : FileNames.getFileName(fullPath);
var hidden = getTableRow().getItem().isHidden() || fileName.startsWith("."); var hidden = getTableRow().getItem().isHidden() || fileName.startsWith(".");
getTableRow().pseudoClassStateChanged(HIDDEN, hidden); getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName); text.set(fileName);
@ -277,17 +285,7 @@ final class FileListComp extends AnchorPane {
} else { } else {
var path = getTableRow().getItem(); var path = getTableRow().getItem();
if (path.isDirectory()) { if (path.isDirectory()) {
if (true) { setText("");
setText("");
return;
}
try (Stream<FileSystem.FileEntry> stream =
path.getFileSystem().listFiles(path.getPath())) {
setText(stream.count() + " items");
} catch (Exception e) {
setText(UNKNOWN);
}
} else { } else {
setText(byteCount(fileSize.longValue())); setText(byteCount(fileSize.longValue()));
} }
@ -307,7 +305,7 @@ final class FileListComp extends AnchorPane {
fileTime != null fileTime != null
? HumanReadableFormat.date( ? HumanReadableFormat.date(
fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime()) fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime())
: UNKNOWN); : "");
} }
} }
} }

View file

@ -5,10 +5,7 @@ import io.xpipe.core.store.FileSystem;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.input.DragEvent; import javafx.scene.input.*;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import lombok.Getter; import lombok.Getter;
import java.io.File; import java.io.File;
@ -33,6 +30,22 @@ public class FileListEntry {
this.model = model; this.model = model;
} }
public void onMouseClick(MouseEvent t) {
t.consume();
if (item == null || isSynthetic()) {
return;
}
var cm = new FileContextMenu(model.getFileSystemModel(), item, model.getEditing());
if (t.getButton() == MouseButton.SECONDARY) {
cm.show(row, t.getScreenX(), t.getScreenY());
}
}
public boolean isSynthetic() {
return item != null && item.equals(model.getFileSystemModel().getCurrentParentDirectory());
}
private boolean acceptsDrop(DragEvent event) { private boolean acceptsDrop(DragEvent event) {
if (FileBrowserClipboard.currentDragClipboard == null) { if (FileBrowserClipboard.currentDragClipboard == null) {
return false; return false;
@ -95,6 +108,10 @@ public class FileListEntry {
return; return;
} }
if (isSynthetic()) {
return;
}
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "img/file_drag_icon.png") var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "img/file_drag_icon.png")
.orElseThrow(); .orElseThrow();
var image = new Image(url.toString(), 80, 80, true, false); var image = new Image(url.toString(), 80, 80, true, false);
@ -116,7 +133,6 @@ public class FileListEntry {
} }
private void handleHoverTimer(DragEvent event) { private void handleHoverTimer(DragEvent event) {
if (item == null || !item.isDirectory()) { if (item == null || !item.isDirectory()) {
return; return;
} }

View file

@ -39,6 +39,7 @@ final class FileListModel {
private final Property<FileSystem.FileEntry> draggedOverDirectory = new SimpleObjectProperty<FileSystem.FileEntry>(); private final Property<FileSystem.FileEntry> draggedOverDirectory = new SimpleObjectProperty<FileSystem.FileEntry>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty(); private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<FileSystem.FileEntry> editing = new SimpleObjectProperty<>();
public FileListModel(OpenFileSystemModel fileSystemModel) { public FileListModel(OpenFileSystemModel fileSystemModel) {
this.fileSystemModel = fileSystemModel; this.fileSystemModel = fileSystemModel;

View file

@ -6,6 +6,7 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BusyProperty; import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.LocalStore; import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.ConnectionFileSystem; import io.xpipe.core.store.ConnectionFileSystem;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
@ -52,8 +53,26 @@ final class OpenFileSystemModel {
cdSync(currentPath.get()); cdSync(currentPath.get());
} }
public FileSystem.FileEntry getCurrentParentDirectory() {
var current = getCurrentDirectory();
if (current == null) {
return null;
}
var parent = FileNames.getParent(currentPath.get());
if (parent == null) {
return null;
}
return new FileSystem.FileEntry(fileSystem, parent, null, true, false, false, 0);
}
public FileSystem.FileEntry getCurrentDirectory() { public FileSystem.FileEntry getCurrentDirectory() {
return new FileSystem.FileEntry(fileSystem, currentPath.get(), Instant.now(), true, false, false, 0); if (currentPath.get() == null) {
return null;
}
return new FileSystem.FileEntry(fileSystem, currentPath.get(), null, true, false, false, 0);
} }
public Optional<String> cd(String path) { public Optional<String> cd(String path) {

View file

@ -43,8 +43,11 @@
} }
.browser .table-directory-view .table-view:drag-into-current { .browser .table-directory-view .table-view:drag-into-current {
-fx-border-color: -color-success-muted; -fx-background-color: -color-success-muted;
-fx-border-width: 2px; }
.browser .table-directory-view .table-view:drag-into-current .table-row-cell {
-fx-opacity: 0.8;
} }
.browser .table-row-cell:drag-over { .browser .table-row-cell:drag-over {

View file

@ -65,6 +65,14 @@ public class FileNames {
} }
public static String getParent(String file) { public static String getParent(String file) {
if (split(file).size() == 0) {
return null;
}
if (split(file).size() == 1) {
return file.startsWith("/") && !file.equals("/") ? "/" : null;
}
return file.substring(0, file.length() - getFileName(file).length() - 1); return file.substring(0, file.length() - getFileName(file).length() - 1);
} }

View file

@ -21,7 +21,6 @@ public interface FileSystem extends Closeable, AutoCloseable {
FileSystem fileSystem; FileSystem fileSystem;
@NonNull @NonNull
String path; String path;
@NonNull
Instant date; Instant date;
boolean directory; boolean directory;
boolean hidden; boolean hidden;
@ -29,7 +28,7 @@ public interface FileSystem extends Closeable, AutoCloseable {
long size; long size;
public FileEntry( public FileEntry(
@NonNull FileSystem fileSystem, @NonNull String path, @NonNull Instant date, boolean directory, boolean hidden, Boolean executable, @NonNull FileSystem fileSystem, @NonNull String path, Instant date, boolean directory, boolean hidden, Boolean executable,
long size long size
) { ) {
this.fileSystem = fileSystem; this.fileSystem = fileSystem;

View file

@ -1 +1 @@
0.5.13 0.5.14