File browser improvements

This commit is contained in:
crschnick 2023-05-18 09:48:31 +00:00
parent 21da5bd828
commit 9cb0b7f494
11 changed files with 281 additions and 133 deletions

View file

@ -1,19 +1,23 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.storage.store.StoreEntryFlatMiniSectionComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.comp.storage.store.StoreEntryTree;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.DragPseudoClassAugment;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Point2D;
import javafx.scene.control.Button;
import javafx.scene.Node;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import java.util.Timer;
@ -33,34 +37,101 @@ final class BookmarkList extends SimpleComp {
@Override
protected Region createSimple() {
var observableList = BindingsHelper.filteredContentBinding(StoreEntryFlatMiniSectionComp.ALL, e -> e.getEntry().getState().isUsable());
var list = new ListBoxViewComp<>(observableList, observableList, e -> {
return Comp.of(() -> {
var button = new Button(null, e.createRegion());
var root = StoreEntryTree.createTree();
var view = new TreeView<StoreEntryWrapper>(root);
view.setShowRoot(false);
view.getStyleClass().add("bookmark-list");
view.setCellFactory(param -> {
return new StoreCell();
});
if (!(e.getEntry().getStore() instanceof ShellStore)) {
button.setDisable(true);
model.getSelected().addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
view.getSelectionModel().clearSelection();
return;
}
view.getSelectionModel()
.select(getTreeViewItem(
root,
StoreViewState.get().getAllEntries().stream()
.filter(storeEntryWrapper -> storeEntryWrapper
.getState()
.getValue()
.isUsable()
&& storeEntryWrapper
.getEntry()
.getStore()
.equals(newValue.getStore()))
.findAny()
.orElse(null)));
});
return view;
}
private static TreeItem<StoreEntryWrapper> getTreeViewItem(
TreeItem<StoreEntryWrapper> item, StoreEntryWrapper value) {
if (item.getValue() != null && item.getValue().equals(value)) {
return item;
}
for (TreeItem<StoreEntryWrapper> child : item.getChildren()) {
TreeItem<StoreEntryWrapper> s = getTreeViewItem(child, value);
if (s != null) {
return s;
}
}
return null;
}
private final class StoreCell extends TreeCell<StoreEntryWrapper> {
private final StringProperty img = new SimpleStringProperty();
private final Node imageView = new PrettyImageComp(img, 20, 20).createRegion();
private StoreCell() {
setGraphic(imageView);
addEventHandler(DragEvent.DRAG_OVER, mouseEvent -> {
if (getItem() == null) {
return;
}
button.setOnAction(event -> {
var fileSystem = ((ShellStore) e.getEntry().getStore());
model.openFileSystemAsync(fileSystem);
event.consume();
});
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(button));
DragPseudoClassAugment.create().augment(new SimpleCompStructure<>(button));
button.addEventHandler(
DragEvent.DRAG_OVER,
mouseEvent -> handleHoverTimer(e.getEntry().getStore(), mouseEvent));
button.addEventHandler(
DragEvent.DRAG_EXITED,
mouseEvent -> activeTask = null);
return button;
handleHoverTimer(getItem().getEntry().getStore(), mouseEvent);
mouseEvent.consume();
});
}).styleClass("bookmark-list").createRegion();
return list;
addEventHandler(DragEvent.DRAG_EXITED, mouseEvent -> {
activeTask = null;
mouseEvent.consume();
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (getItem() == null || event.getButton() != MouseButton.PRIMARY) {
return;
}
var fileSystem = ((ShellStore) getItem().getEntry().getStore());
model.openFileSystemAsync(fileSystem, null);
event.consume();
});
}
@Override
public void updateItem(StoreEntryWrapper item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
// Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
setGraphic(null);
} else {
setText(item.getName());
img.set(item.getEntry()
.getProvider()
.getDisplayIconFileName(item.getEntry().getStore()));
setGraphic(imageView);
}
}
}
private void handleHoverTimer(DataStore store, DragEvent event) {

View file

@ -11,13 +11,13 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
@ -46,27 +46,32 @@ public class FileBrowserComp extends SimpleComp {
protected Region createSimple() {
FileType.loadDefinitions();
DirectoryType.loadDefinitions();
ThreadHelper.runAsync( () -> {
ThreadHelper.runAsync(() -> {
FileIconManager.loadIfNecessary();
});
var bookmarksList = new BookmarkList(model).createRegion();
VBox.setVgrow(bookmarksList, Priority.ALWAYS);
var localDownloadStage = new LocalFileTransferComp(model.getLocalTransfersStage()).hide(Bindings.createBooleanBinding(() -> {
if (model.getOpenFileSystems().size() == 0) {
return true;
}
var localDownloadStage = new LocalFileTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
if (model.getOpenFileSystems().size() == 0) {
return true;
}
return !model.getMode().equals(FileBrowserModel.Mode.BROWSER);
}, PlatformThread.sync(model.getOpenFileSystems()))).createRegion();
SimpleChangeListener.apply(model.getSelected(), val -> {
localDownloadStage.visibleProperty().unbind();
if (val == null) {
return;
}
if (!model.getMode().equals(FileBrowserModel.Mode.BROWSER)) {
return true;
}
localDownloadStage.visibleProperty().bind(PlatformThread.sync(val.getLocal().not()));
});
if (model.getSelected().getValue() != null) {
return model.getSelected().getValue().isLocal();
}
return false;
},
model.getOpenFileSystems(),
model.getSelected())))
.createRegion();
var vertical = new VBox(bookmarksList, localDownloadStage);
vertical.setFillWidth(true);
@ -75,9 +80,10 @@ public class FileBrowserComp extends SimpleComp {
.widthProperty()
.addListener(
// set sidebar width in pixels depending on split pane width
(obs, old, val) -> splitPane.setDividerPosition(0, 230 / splitPane.getWidth()));
(obs, old, val) -> splitPane.setDividerPosition(0, 280 / splitPane.getWidth()));
var r = addBottomBar(splitPane);
r.getStyleClass().add("browser");
// AppFont.small(r);
return r;
}
@ -92,14 +98,14 @@ public class FileBrowserComp extends SimpleComp {
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
selected.setSpacing(10);
// model.getSelected().addListener((ListChangeListener<? super FileSystem.FileEntry>) c -> {
// selected.getChildren().setAll(c.getList().stream().map(s -> {
// var field = new TextField(s.getPath());
// field.setEditable(false);
// field.setPrefWidth(400);
// return field;
// }).toList());
// });
// model.getSelected().addListener((ListChangeListener<? super FileSystem.FileEntry>) c -> {
// selected.getChildren().setAll(c.getList().stream().map(s -> {
// var field = new TextField(s.getPath());
// field.setEditable(false);
// field.setPrefWidth(400);
// return field;
// }).toList());
// });
var spacer = new Spacer(Orientation.HORIZONTAL);
var button = new Button("Select");
button.setOnAction(event -> model.finishChooser());
@ -128,7 +134,8 @@ public class FileBrowserComp extends SimpleComp {
map.put(v, t);
tabs.getTabs().add(t);
});
tabs.getSelectionModel().select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
tabs.getSelectionModel()
.select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
var modifying = new SimpleBooleanProperty();
@ -195,8 +202,6 @@ public class FileBrowserComp extends SimpleComp {
}
}
});
stack.getStyleClass().add("browser");
return stack;
}
@ -228,29 +233,14 @@ public class FileBrowserComp extends SimpleComp {
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
var name = Bindings.createStringBinding(
() -> {
return model.getStore().getValue() != null
? DataStorage.get()
.getStoreEntry(model.getStore().getValue())
.getName()
: null;
},
PlatformThread.sync(model.getStore()));
var image = Bindings.createStringBinding(
() -> {
return model.getStore().getValue() != null
? DataStorage.get()
.getStoreEntry(model.getStore().getValue())
.getProvider()
.getDisplayIconFileName(model.getStore().getValue())
: null;
},
model.getStore());
var logo = new PrettyImageComp(image, 20, 20).createRegion();
var name = DataStorage.get().getStoreEntry(model.getStore()).getName();
var image = DataStorage.get()
.getStoreEntry(model.getStore())
.getProvider()
.getDisplayIconFileName(model.getStore());
var logo = new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion();
var label = new Label();
label.textProperty().bind(name);
var label = new Label(name);
label.addEventHandler(DragEvent.DRAG_ENTERED, new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent mouseEvent) {

View file

@ -71,46 +71,43 @@ public class FileBrowserModel {
public void openExistingFileSystemIfPresent(ShellStore store) {
var found = openFileSystems.stream()
.filter(model -> Objects.equals(model.getStore().getValue(), store))
.filter(model -> Objects.equals(model.getStore(), store))
.findFirst();
if (found.isPresent()) {
selected.setValue(found.get());
} else {
openFileSystemAsync(store);
openFileSystemAsync(store, null);
}
}
public void openFileSystemSync(ShellStore store, String path) throws Exception {
var model = new OpenFileSystemModel(this);
openFileSystems.add(model);
selected.setValue(model);
model.switchSync(store);
model.cd(path);
}
public void openFileSystemAsync(ShellStore store) {
// Prevent multiple tabs in non browser modes
if (!mode.equals(Mode.BROWSER)) {
ThreadHelper.runFailableAsync(() -> {
var open = openFileSystems.size() > 0 ? openFileSystems.get(0) : null;
if (open != null) {
open.closeSync();
openFileSystems.remove(open);
}
var model = new OpenFileSystemModel(this);
openFileSystems.add(model);
selected.setValue(model);
model.switchSync(store);
});
return;
}
public void openFileSystemAsync(ShellStore store, String path) {
// // Prevent multiple tabs in non browser modes
// if (!mode.equals(Mode.BROWSER)) {
// ThreadHelper.runFailableAsync(() -> {
// var open = openFileSystems.size() > 0 ? openFileSystems.get(0) : null;
// if (open != null) {
// open.closeSync();
// openFileSystems.remove(open);
// }
//
// var model = new OpenFileSystemModel(this, store);
// openFileSystems.add(model);
// selected.setValue(model);
// model.switchSync(store);
// });
// return;
// }
ThreadHelper.runFailableAsync(() -> {
var model = new OpenFileSystemModel(this);
var model = new OpenFileSystemModel(this, store);
model.initFileSystem();
openFileSystems.add(model);
selected.setValue(model);
model.switchSync(store);
if (path != null) {
model.cd(path);
} else {
model.initDirectory();
}
});
}
}

View file

@ -21,7 +21,7 @@ public class FileSystemHelper {
}
ConnectionFileSystem fileSystem = (ConnectionFileSystem) model.getFileSystem();
var current = !(model.getStore().getValue() instanceof LocalStore)
var current = !model.isLocal()
? fileSystem
.getShellControl()
.executeSimpleStringCommand(

View file

@ -34,7 +34,7 @@ import java.util.stream.Stream;
@Getter
public final class OpenFileSystemModel {
private Property<FileSystemStore> store = new SimpleObjectProperty<>();
private final FileSystemStore store;
private FileSystem fileSystem;
private final Property<String> filter = new SimpleStringProperty();
private final FileListModel fileList;
@ -46,10 +46,11 @@ public final class OpenFileSystemModel {
private final Property<OpenFileSystemSavedState> savedState = new SimpleObjectProperty<>();
private final OpenFileSystemCache cache = new OpenFileSystemCache(this);
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty local = new SimpleBooleanProperty();
private boolean local;
public OpenFileSystemModel(FileBrowserModel browserModel) {
public OpenFileSystemModel(FileBrowserModel browserModel, FileSystemStore store) {
this.browserModel = browserModel;
this.store = store;
fileList = new FileListModel(this);
addListeners();
}
@ -61,7 +62,7 @@ public final class OpenFileSystemModel {
}
BusyProperty.execute(busy, () -> {
if (store.getValue() instanceof ShellStore s) {
if (store instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow());
if (refresh) {
refreshSync();
@ -73,11 +74,11 @@ public final class OpenFileSystemModel {
private void addListeners() {
savedState.addListener((observable, oldValue, newValue) -> {
if (store.getValue() == null) {
if (store == null) {
return;
}
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store.getValue());
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
storageEntry.ifPresent(entry -> AppCache.update("browser-state-" + entry.getUuid(), newValue));
});
@ -128,7 +129,7 @@ public final class OpenFileSystemModel {
if (!FileNames.isAbsolute(path) && fileSystem.getShell().isPresent()) {
var directory = currentPath.get();
var name = path + " - "
+ XPipeDaemon.getInstance().getStoreName(store.getValue()).orElse("?");
+ XPipeDaemon.getInstance().getStoreName(store).orElse("?");
ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream().anyMatch(dialect -> path.startsWith(dialect.getOpenCommand()))) {
var cmd = fileSystem
@ -177,7 +178,7 @@ public final class OpenFileSystemModel {
private void cdSyncWithoutCheck(String path) throws Exception {
if (fileSystem == null) {
var fs = store.getValue().createFileSystem();
var fs = store.createFileSystem();
fs.open();
this.fileSystem = fs;
}
@ -323,21 +324,21 @@ public final class OpenFileSystemModel {
ErrorEvent.fromThrowable(e).handle();
}
fileSystem = null;
store = null;
}
public void switchSync(FileSystemStore fileSystem) throws Exception {
public void initFileSystem() throws Exception {
BusyProperty.execute(busy, () -> {
closeSync();
this.store.setValue(fileSystem);
var fs = fileSystem.createFileSystem();
var fs = store.createFileSystem();
fs.open();
this.fileSystem = fs;
this.local.set(
fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false));
this.local = fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false);
});
}
public void initDirectory() throws Exception {
BusyProperty.execute(busy, () -> {
var storageEntry = DataStorage.get()
.getStoreEntryIfPresent(fileSystem)
.getStoreEntryIfPresent(store)
.map(entry -> entry.getUuid())
.orElse(UUID.randomUUID());
this.savedState.setValue(
@ -362,13 +363,13 @@ public final class OpenFileSystemModel {
}
BusyProperty.execute(busy, () -> {
if (store.getValue() instanceof ShellStore s) {
if (store instanceof ShellStore s) {
var connection = ((ConnectionFileSystem) fileSystem).getShellControl();
var command = s.control()
.initWith(connection.getShellDialect().getCdCommand(directory))
.prepareTerminalOpen(directory + " - "
+ XPipeDaemon.getInstance()
.getStoreName(store.getValue())
.getStoreName(store)
.orElse("?"));
TerminalHelper.open(directory, command);
}

View file

@ -0,0 +1,36 @@
package io.xpipe.app.comp.storage.store;
import javafx.collections.ListChangeListener;
import javafx.scene.control.TreeItem;
public class StoreEntryTree {
public static TreeItem<StoreEntryWrapper> createTree() {
var topLevel = StoreSection.createTopLevel();
var root = new TreeItem<StoreEntryWrapper>();
root.setExpanded(true);
// Listen for any entry list change, not only top level changes
StoreViewState.get().getAllEntries().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
root.getChildren().clear();
for (StoreSection v : topLevel.getChildren()) {
add(root, v);
}
});
for (StoreSection v : topLevel.getChildren()) {
add(root, v);
}
return root;
}
private static void add(TreeItem<StoreEntryWrapper> parent, StoreSection section) {
var item = new TreeItem<>(section.getWrapper());
item.setExpanded(section.getWrapper().getExpanded().getValue());
parent.getChildren().add(item);
for (StoreSection child : section.getChildren()) {
add(item, child);
}
}
}

View file

@ -8,12 +8,30 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Window;
import javafx.util.Duration;
import lombok.AllArgsConstructor;
import lombok.Getter;
public class AppTheme {
public record AccentColor(Color primaryColor, PseudoClass pseudoClass) {
public static AccentColor xpipeBlue() {
return new AccentColor(Color.web("#11B4B4"), PseudoClass.getPseudoClass("accent-primer-purple"));
}
}
public static void init() {
if (AppPrefs.get() == null) {
return;
@ -60,6 +78,23 @@ public class AppTheme {
private static void changeTheme(Theme newTheme) {
PlatformThread.runLaterIfNeeded(() -> {
for (Window window : Window.getWindows()) {
var scene = window.getScene();
Image snapshot = scene.snapshot(null);
Pane root = (Pane) scene.getRoot();
ImageView imageView = new ImageView(snapshot);
root.getChildren().add(imageView);
// Animate!
var transition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(imageView.opacityProperty(), 1, Interpolator.EASE_OUT)),
new KeyFrame(
Duration.millis(1250), new KeyValue(imageView.opacityProperty(), 0, Interpolator.EASE_OUT)));
transition.setOnFinished(e -> root.getChildren().remove(imageView));
transition.play();
}
Application.setUserAgentStylesheet(newTheme.getTheme().getUserAgentStylesheet());
TrackEvent.debug("Set theme " + newTheme.getId() + " for scene");
});

View file

@ -52,7 +52,7 @@ public class SvgView {
var widthProperty = new SimpleIntegerProperty();
var heightProperty = new SimpleIntegerProperty();
SimpleChangeListener.apply(content, val -> {
if (val == null) {
if (val == null || val.isBlank()) {
return;
}

View file

@ -1,9 +1,11 @@
package io.xpipe.app.launcher;
import io.xpipe.app.browser.FileBrowserModel;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.store.ShellStore;
import lombok.Getter;
import lombok.Value;
@ -114,7 +116,8 @@ public abstract class LauncherInput {
return;
}
// GuiDsCreatorMultiStep.showForStore(DataSourceProvider.Category.STREAM, FileStore.local(file), null);
var dir = Files.isDirectory(file) ? file : file.getParent();
FileBrowserModel.DEFAULT.openFileSystemAsync(ShellStore.createLocal(), dir.toString());
}
@Override

View file

@ -49,7 +49,22 @@
}
.browser .bookmark-list {
-fx-border-width: 0 0 1 1;
-fx-border-width: 0;
}
.browser .bookmark-list *.scroll-bar:horizontal,
.browser .bookmark-list *.scroll-bar:horizontal *.track,
.browser .bookmark-list *.scroll-bar:horizontal *.track-background,
.browser .bookmark-list *.scroll-bar:horizontal *.thumb,
.browser .bookmark-list *.scroll-bar:horizontal *.increment-button,
.browser .bookmark-list *.scroll-bar:horizontal *.decrement-button,
.browser .bookmark-list *.scroll-bar:horizontal *.increment-arrow,
.browser .bookmark-list *.scroll-bar:horizontal *.decrement-arrow {
-fx-background-color: null;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-padding: 0;
-fx-shape: null;
}
.browser .tool-bar {

View file

@ -16,7 +16,7 @@ public class OpenDirectoryInNewTabAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
model.getBrowserModel().openFileSystemSync(model.getStore().getValue().asNeeded(), entries.get(0).getRawFileEntry().getPath());
model.getBrowserModel().openFileSystemAsync(model.getStore().asNeeded(), entries.get(0).getRawFileEntry().getPath());
}
@Override