File browser overview page improvements

This commit is contained in:
crschnick 2023-05-29 00:33:34 +00:00
parent 40a5c6a306
commit 52cdcfa0aa
21 changed files with 367 additions and 113 deletions

View file

@ -0,0 +1,41 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.icon.BrowserIcons;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.core.store.FileSystem;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class BrowserFileOverviewComp extends SimpleComp {
OpenFileSystemModel model;
ObservableList<FileSystem.FileEntry> list;
@Override
protected Region createSimple() {
var c = new ListBoxViewComp<>(list, list, entry -> {
return Comp.of(() -> {
var icon = BrowserIcons.createIcon(entry);
var l = new Button(entry.getPath(), icon.createRegion());
l.setOnAction(event -> {
model.cd(entry.getPath());
event.consume();
});
l.setAlignment(Pos.CENTER_LEFT);
GrowAugment.create(true,false).augment(l);
return l;
});
})
.styleClass("overview-file-list");
return c.createRegion();
}
}

View file

@ -48,6 +48,10 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
var fi = new FontIcon("mdi2m-magnify"); var fi = new FontIcon("mdi2m-magnify");
button.setGraphic(fi); button.setGraphic(fi);
button.setOnAction(event -> { button.setOnAction(event -> {
if (model.getCurrentDirectory() == null) {
return;
}
if (expanded.get()) { if (expanded.get()) {
if (filterString.getValue() == null) { if (filterString.getValue() == null) {
expanded.set(false); expanded.set(false);
@ -62,6 +66,7 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
text.setPrefWidth(0); text.setPrefWidth(0);
button.getStyleClass().add(Styles.FLAT); button.getStyleClass().add(Styles.FLAT);
button.disableProperty().bind(model.getInOverview());
expanded.addListener((observable, oldValue, val) -> { expanded.addListener((observable, oldValue, val) -> {
if (val) { if (val) {
text.setPrefWidth(250); text.setPrefWidth(250);
@ -88,9 +93,11 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
} }
} }
private final OpenFileSystemModel model;
private final Property<String> filterString; private final Property<String> filterString;
public BrowserFilterComp(Property<String> filterString) { public BrowserFilterComp(OpenFileSystemModel model, Property<String> filterString) {
this.model = model;
this.filterString = filterString; this.filterString = filterString;
} }
} }

View file

@ -36,7 +36,7 @@ public class BrowserNavBar extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var path = new SimpleStringProperty(model.getCurrentPath().get()); var path = new SimpleStringProperty(model.getCurrentPath().get());
model.getCurrentPath().addListener((observable, oldValue, newValue) -> { SimpleChangeListener.apply(model.getCurrentPath(), (newValue) -> {
path.set(newValue); path.set(newValue);
}); });
path.addListener((observable, oldValue, newValue) -> { path.addListener((observable, oldValue, newValue) -> {
@ -49,7 +49,11 @@ public class BrowserNavBar extends SimpleComp {
.styleClass("path-text") .styleClass("path-text")
.apply(struc -> { .apply(struc -> {
SimpleChangeListener.apply(struc.get().focusedProperty(), val -> { SimpleChangeListener.apply(struc.get().focusedProperty(), val -> {
struc.get().pseudoClassStateChanged(INVISIBLE, !val); struc.get().pseudoClassStateChanged(INVISIBLE, !val && !model.getInOverview().get());
});
SimpleChangeListener.apply(model.getInOverview(), val -> {
struc.get().pseudoClassStateChanged(INVISIBLE, !val && !struc.get().isFocused());
}); });
struc.get().setOnMouseClicked(event -> { struc.get().setOnMouseClicked(event -> {
@ -61,13 +65,15 @@ public class BrowserNavBar extends SimpleComp {
struc.get().selectAll(); struc.get().selectAll();
struc.get().requestFocus(); struc.get().requestFocus();
}); });
struc.get().setPromptText("Overview of " + model.getName());
}); });
var graphic = Bindings.createStringBinding( var graphic = Bindings.createStringBinding(
() -> { () -> {
var icon = model.getCurrentDirectory() != null var icon = model.getCurrentDirectory() != null
? FileIconManager.getFileIcon(model.getCurrentDirectory(), false) ? FileIconManager.getFileIcon(model.getCurrentDirectory(), false)
: null; : "home_icon.png";
return icon; return icon;
}, },
model.getCurrentPath()); model.getCurrentPath());
@ -80,7 +86,9 @@ public class BrowserNavBar extends SimpleComp {
graphicButton.getStyleClass().add(Styles.LEFT_PILL); graphicButton.getStyleClass().add(Styles.LEFT_PILL);
graphicButton.getStyleClass().add("path-graphic-button"); graphicButton.getStyleClass().add("path-graphic-button");
new ContextMenuAugment<>( new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, () -> new BrowserContextMenu(model, null)) event -> event.getButton() == MouseButton.PRIMARY, () -> {
return model.getInOverview().get() ? null : new BrowserContextMenu(model, null);
})
.augment(new SimpleCompStructure<>(graphicButton)); .augment(new SimpleCompStructure<>(graphicButton));
GrowAugment.create(false, true).augment(graphicButton); GrowAugment.create(false, true).augment(graphicButton);
@ -92,7 +100,9 @@ public class BrowserNavBar extends SimpleComp {
.apply(struc -> { .apply(struc -> {
var t = struc.get().getChildren().get(0); var t = struc.get().getChildren().get(0);
var b = struc.get().getChildren().get(1); var b = struc.get().getChildren().get(1);
b.visibleProperty().bind(t.focusedProperty().not()); b.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
return !t.isFocused() && !model.getInOverview().get();
}, t.focusedProperty(), model.getInOverview()));
}) })
.grow(false, true); .grow(false, true);

View file

@ -4,11 +4,13 @@ import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import lombok.SneakyThrows;
import java.time.Instant;
import java.util.List; import java.util.List;
public class BrowserOverviewComp extends SimpleComp { public class BrowserOverviewComp extends SimpleComp {
@ -20,16 +22,23 @@ public class BrowserOverviewComp extends SimpleComp {
} }
@Override @Override
@SneakyThrows
protected Region createSimple() { protected Region createSimple() {
var commonList = new BrowserSelectionListComp(FXCollections.observableArrayList( ShellControl sc = model.getFileSystem().getShell().orElseThrow();
new FileSystem.FileEntry(model.getFileSystem(), "C:\\", Instant.now(), true, false, false, 0, null))); var common = sc.getOsType().determineInterestingPaths(sc).stream().map(s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s)).toList();
var common = new SimpleTitledPaneComp(AppI18n.observable("a"), commonList); var commonOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(common));
var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview);
var recentList = new BrowserSelectionListComp(FXCollections.observableArrayList( var roots = sc.getShellDialect().listRoots(sc).map(s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s)).toList();
new FileSystem.FileEntry(model.getFileSystem(), "C:\\", Instant.now(), true, false, false, 0, null))); var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots));
var recent = new SimpleTitledPaneComp(AppI18n.observable("Recent"), recentList); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var vbox = new VerticalComp(List.of(common, recent)).styleClass("home");
var recent = BindingsHelper.mappedContentBinding(model.getSavedState().getRecentDirectories(), s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
var recentOverview = new BrowserFileOverviewComp(model, recent);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview).vgrow();
var vbox = new VerticalComp(List.of(commonPane, rootsPane, recentPane)).styleClass("overview");
return vbox.createRegion(); return vbox.createRegion();
} }
} }

View file

@ -1,6 +1,7 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
@ -9,7 +10,6 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts; import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -44,6 +44,7 @@ public class OpenFileSystemComp extends SimpleComp {
private Region createContent() { private Region createContent() {
var overview = new Button(null, new FontIcon("mdi2m-monitor")); var overview = new Button(null, new FontIcon("mdi2m-monitor"));
overview.setOnAction(e -> model.cd(null)); overview.setOnAction(e -> model.cd(null));
overview.disableProperty().bind(model.getInOverview());
var backBtn = new Button(null, new FontIcon("fth-arrow-left")); var backBtn = new Button(null, new FontIcon("fth-arrow-left"));
backBtn.setOnAction(e -> model.back()); backBtn.setOnAction(e -> model.back());
@ -56,18 +57,20 @@ public class OpenFileSystemComp extends SimpleComp {
var refreshBtn = new Button(null, new FontIcon("mdmz-refresh")); var refreshBtn = new Button(null, new FontIcon("mdmz-refresh"));
refreshBtn.setOnAction(e -> model.refresh()); refreshBtn.setOnAction(e -> model.refresh());
Shortcuts.addShortcut(refreshBtn, new KeyCodeCombination(KeyCode.F5)); Shortcuts.addShortcut(refreshBtn, new KeyCodeCombination(KeyCode.F5));
refreshBtn.disableProperty().bind(model.getInOverview());
var terminalBtn = new Button(null, new FontIcon("mdi2c-code-greater-than")); var terminalBtn = BrowserAction.byId("openTerminal").toButton(model, List.of());
terminalBtn.setOnAction( terminalBtn.setOnAction(
e -> model.openTerminalAsync(model.getCurrentPath().get())); e -> model.openTerminalAsync(model.getCurrentPath().get()));
terminalBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory())); terminalBtn.disableProperty().bind(model.getInOverview());
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open")); var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
new ContextMenuAugment<>( new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, () -> new BrowserContextMenu(model, null)) event -> event.getButton() == MouseButton.PRIMARY, () -> new BrowserContextMenu(model, null))
.augment(new SimpleCompStructure<>(menuButton)); .augment(new SimpleCompStructure<>(menuButton));
menuButton.disableProperty().bind(model.getInOverview());
var filter = new BrowserFilterComp(model.getFilter()).createStructure(); var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)); Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));
var topBar = new ToolBar(); var topBar = new ToolBar();

View file

@ -8,11 +8,10 @@ import javafx.beans.property.SimpleIntegerProperty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
final class BrowserHistory { final class OpenFileSystemHistory {
private final IntegerProperty cursor = new SimpleIntegerProperty(0); private final IntegerProperty cursor = new SimpleIntegerProperty(-1);
private final List<String> history = new ArrayList<>(); private final List<String> history = new ArrayList<>();
private final BooleanBinding canGoBack = Bindings.createBooleanBinding( private final BooleanBinding canGoBack = Bindings.createBooleanBinding(
() -> cursor.get() > 0 && history.size() > 1, cursor); () -> cursor.get() > 0 && history.size() > 1, cursor);
@ -24,35 +23,33 @@ final class BrowserHistory {
} }
public void updateCurrent(String s) { public void updateCurrent(String s) {
if (s == null) {
return;
}
var lastString = getCurrent(); var lastString = getCurrent();
if (Objects.equals(lastString, s)) { if (cursor.get() != -1 && Objects.equals(lastString, s)) {
return; return;
} }
if (canGoForth.get()) { if (canGoForth.get()) {
history.subList(cursor.get() + 1, history.size()).clear(); history.subList(cursor.get() + 1, history.size()).clear();
} }
history.add(s); history.add(s);
cursor.set(history.size() - 1); cursor.set(history.size() - 1);
} }
public Optional<String> back() { public String back() {
if (!canGoBack.get()) { if (!canGoBack.get()) {
return Optional.empty(); return null;
} }
cursor.set(cursor.get() - 1); cursor.set(cursor.get() - 1);
return Optional.of(history.get(cursor.get())); return history.get(cursor.get());
} }
public Optional<String> forth() { public String forth() {
if (!canGoForth.get()) { if (!canGoForth.get()) {
return Optional.empty(); return null;
} }
cursor.set(cursor.get() + 1); cursor.set(cursor.get() + 1);
return Optional.of(history.get(cursor.get())); return history.get(cursor.get());
} }
public BooleanBinding canGoBackProperty() { public BooleanBinding canGoBackProperty() {

View file

@ -1,7 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty; import io.xpipe.app.util.BusyProperty;
@ -15,6 +14,7 @@ import io.xpipe.core.store.ConnectionFileSystem;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -26,7 +26,6 @@ import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream; import java.util.stream.Stream;
@Getter @Getter
@ -37,13 +36,13 @@ public final class OpenFileSystemModel {
private final Property<String> filter = new SimpleStringProperty(); private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList; private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>(); private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final BrowserHistory history = new BrowserHistory(); private final OpenFileSystemHistory history = new OpenFileSystemHistory();
private final BooleanProperty busy = new SimpleBooleanProperty(); private final BooleanProperty busy = new SimpleBooleanProperty();
private final BrowserModel browserModel; private final BrowserModel browserModel;
private final BooleanProperty noDirectory = new SimpleBooleanProperty(); private OpenFileSystemSavedState savedState;
private final Property<OpenFileSystemSavedState> savedState = new SimpleObjectProperty<>();
private final OpenFileSystemCache cache = new OpenFileSystemCache(this); private final OpenFileSystemCache cache = new OpenFileSystemCache(this);
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>(); private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final String name; private final String name;
private boolean local; private boolean local;
@ -51,6 +50,9 @@ public final class OpenFileSystemModel {
this.browserModel = browserModel; this.browserModel = browserModel;
this.store = store; this.store = store;
this.name = name != null ? name : DataStorage.get().getStoreEntry(store).getName(); this.name = name != null ? name : DataStorage.get().getStoreEntry(store).getName();
this.inOverview.bind(Bindings.createBooleanBinding(() -> {
return currentPath.get() == null;
}, currentPath));
fileList = new BrowserFileListModel(this); fileList = new BrowserFileListModel(this);
addListeners(); addListeners();
} }
@ -73,18 +75,14 @@ public final class OpenFileSystemModel {
} }
private void addListeners() { private void addListeners() {
savedState.addListener((observable, oldValue, newValue) -> { // savedState.addListener((observable, oldValue, newValue) -> {
if (store == null) { // if (store == null) {
return; // return;
} // }
//
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store); // var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
storageEntry.ifPresent(entry -> AppCache.update("browser-state-" + entry.getUuid(), newValue)); // storageEntry.ifPresent(entry -> AppCache.update("browser-state-" + entry.getUuid(), newValue));
}); // });
currentPath.addListener((observable, oldValue, newValue) -> {
savedState.setValue(savedState.getValue().withLastDirectory(newValue));
});
} }
@SneakyThrows @SneakyThrows
@ -132,12 +130,15 @@ public final class OpenFileSystemModel {
} }
// Handle commands typed into navigation bar // Handle commands typed into navigation bar
if (normalizedPath != null && !FileNames.isAbsolute(normalizedPath) && fileSystem.getShell().isPresent()) { if (normalizedPath != null
&& !FileNames.isAbsolute(normalizedPath)
&& fileSystem.getShell().isPresent()) {
var directory = currentPath.get(); var directory = currentPath.get();
var name = normalizedPath + " - " var name = normalizedPath + " - "
+ XPipeDaemon.getInstance().getStoreName(store).orElse("?"); + XPipeDaemon.getInstance().getStoreName(store).orElse("?");
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream().anyMatch(dialect -> normalizedPath.startsWith(dialect.getOpenCommand()))) { if (ShellDialects.ALL.stream()
.anyMatch(dialect -> normalizedPath.startsWith(dialect.getOpenCommand()))) {
var cmd = fileSystem var cmd = fileSystem
.getShell() .getShell()
.get() .get()
@ -167,7 +168,7 @@ public final class OpenFileSystemModel {
dirPath = FileSystemHelper.validateDirectoryPath(this, normalizedPath); dirPath = FileSystemHelper.validateDirectoryPath(this, normalizedPath);
} catch (Exception ex) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle(); ErrorEvent.fromThrowable(ex).handle();
return Optional.of(currentPath.get()); return Optional.ofNullable(currentPath.get());
} }
if (!Objects.equals(path, dirPath)) { if (!Objects.equals(path, dirPath)) {
@ -194,7 +195,7 @@ public final class OpenFileSystemModel {
filter.setValue(null); filter.setValue(null);
currentPath.set(path); currentPath.set(path);
savedState.setValue(savedState.getValue().withLastDirectory(path)); savedState.cd(path);
history.updateCurrent(path); history.updateCurrent(path);
loadFilesSync(path); loadFilesSync(path);
} }
@ -203,13 +204,11 @@ public final class OpenFileSystemModel {
try { try {
if (dir != null) { if (dir != null) {
var stream = getFileSystem().listFiles(dir); var stream = getFileSystem().listFiles(dir);
noDirectory.set(false);
fileList.setAll(stream); fileList.setAll(stream);
} else { } else {
var stream = getFileSystem().listRoots().stream() var stream = getFileSystem().listRoots().stream()
.map(s -> new FileSystem.FileEntry( .map(s -> new FileSystem.FileEntry(
getFileSystem(), s, Instant.now(), true, false, false, 0, null)); getFileSystem(), s, Instant.now(), true, false, false, 0, null));
noDirectory.set(true);
fileList.setAll(stream); fileList.setAll(stream);
} }
return true; return true;
@ -302,23 +301,6 @@ public final class OpenFileSystemModel {
}); });
} }
public void deleteSelectionAsync() {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
if (fileSystem == null) {
return;
}
if (!BrowserAlerts.showDeleteAlert(fileList.getSelectedRaw())) {
return;
}
FileSystemHelper.delete(fileList.getSelectedRaw());
refreshSync();
});
});
}
void closeSync() { void closeSync() {
if (fileSystem == null) { if (fileSystem == null) {
return; return;
@ -337,38 +319,25 @@ public final class OpenFileSystemModel {
var fs = store.createFileSystem(); var fs = store.createFileSystem();
fs.open(); fs.open();
this.fileSystem = fs; this.fileSystem = fs;
this.local = fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false); this.local =
fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false);
this.cache.init(); this.cache.init();
}); });
} }
public void initWithGivenDirectory(String dir) throws Exception { public void initWithGivenDirectory(String dir) throws Exception {
initSavedState(dir); initState();
cdSyncWithoutCheck(dir); cdSyncWithoutCheck(dir);
} }
public void initWithDefaultDirectory() throws Exception { public void initWithDefaultDirectory() throws Exception {
var dir = FileSystemHelper.getStartDirectory(this); initState();
initSavedState(dir); savedState.cd(null);
cdSyncWithoutCheck(dir); history.updateCurrent(null);
} }
private void initSavedState(String path) { private void initState() {
var storageEntry = DataStorage.get() this.savedState = OpenFileSystemSavedState.loadForStore(store);
.getStoreEntryIfPresent(store)
.map(entry -> entry.getUuid())
.orElse(UUID.randomUUID());
this.savedState.setValue(
AppCache.get("browser-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
try {
return OpenFileSystemSavedState.builder()
.lastDirectory(path)
.build();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return null;
}
}));
} }
public void openTerminalAsync(String directory) { public void openTerminalAsync(String directory) {
@ -392,19 +361,19 @@ public final class OpenFileSystemModel {
}); });
} }
public BrowserHistory getHistory() { public OpenFileSystemHistory getHistory() {
return history; return history;
} }
public void back() { public void back() {
try (var ignored = new BusyProperty(busy)) { try (var ignored = new BusyProperty(busy)) {
history.back().ifPresent(s -> cd(s)); cd(history.back());
} }
} }
public void forth() { public void forth() {
try (var ignored = new BusyProperty(busy)) { try (var ignored = new BusyProperty(busy)) {
history.forth().ifPresent(s -> cd(s)); cd(history.forth());
} }
} }
} }

View file

@ -1,15 +1,147 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import lombok.Builder; import com.fasterxml.jackson.core.JacksonException;
import lombok.Value; import com.fasterxml.jackson.core.JsonGenerator;
import lombok.With; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.JacksonMapper;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.*;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
@Value import java.io.IOException;
@With import java.time.Instant;
@Jacksonized import java.util.*;
@Builder
@AllArgsConstructor
@Getter
@JsonSerialize(using = OpenFileSystemSavedState.Serializer.class)
@JsonDeserialize(using = OpenFileSystemSavedState.Deserializer.class)
public class OpenFileSystemSavedState { public class OpenFileSystemSavedState {
String lastDirectory; public static class Serializer extends StdSerializer<OpenFileSystemSavedState> {
protected Serializer() {
super(OpenFileSystemSavedState.class);
}
@Override
public void serialize(OpenFileSystemSavedState value, JsonGenerator gen, SerializerProvider provider) throws IOException {
var node = JsonNodeFactory.instance.objectNode();
node.set("recentDirectories", JacksonMapper.getDefault().valueToTree(value.getRecentDirectories()));
gen.writeTree(node);
}
}
public static class Deserializer extends StdDeserializer<OpenFileSystemSavedState> {
protected Deserializer() {
super(OpenFileSystemSavedState.class);
}
@Override
@SneakyThrows
public OpenFileSystemSavedState deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JacksonException {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
JavaType javaType = JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, RecentEntry.class);
List<RecentEntry> recentDirectories = JacksonMapper.getDefault().treeToValue(tree.remove("recentDirectories"), javaType);
return new OpenFileSystemSavedState(null, FXCollections.observableList(recentDirectories));
}
}
static OpenFileSystemSavedState loadForStore(FileSystemStore store) {
var storageEntry = DataStorage.get()
.getStoreEntryIfPresent(store)
.map(entry -> entry.getUuid())
.orElse(UUID.randomUUID());
var state = AppCache.get("fs-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState();
});
state.store = store;
return state;
}
@Value
@Jacksonized
@Builder
public static class RecentEntry {
String directory;
Instant time;
}
private FileSystemStore store;
private String lastDirectory;
@NonNull
private ObservableList<RecentEntry> recentDirectories;
public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
this.lastDirectory = lastDirectory;
this.recentDirectories = recentDirectories;
}
private static final Timer TIMEOUT_TIMER = new Timer(true);
private static final int STORED = 10;
public OpenFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
}
public void save() {
if (store == null) {
return;
}
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
storageEntry.ifPresent(entry -> AppCache.update("fs-state-" + entry.getUuid(), this));
}
public void cd(String dir) {
if (dir == null) {
return;
}
lastDirectory = dir;
TIMEOUT_TIMER.schedule(
new TimerTask() {
@Override
public void run() {
// Synchronize with platform thread
Platform.runLater(() -> {
if (Objects.equals(lastDirectory, dir)) {
updateRecent(dir);
save();
}
});
}
},
20000);
}
private void updateRecent(String dir) {
recentDirectories.removeIf(recentEntry -> Objects.equals(recentEntry.directory, dir));
var o = new RecentEntry(dir, Instant.now());
if (recentDirectories.size() < STORED) {
recentDirectories.add(0, o);
} else {
recentDirectories.remove(recentDirectories.size() - 1);
recentDirectories.add(o);
}
}
} }

View file

@ -32,6 +32,10 @@ public interface BrowserAction {
.toList(); .toList();
} }
static LeafAction byId(String id) {
return getFlattened().stream().filter(browserAction -> id.equals(browserAction.getId())).findAny().orElseThrow();
}
default Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) { default Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
return null; return null;
} }

View file

@ -2,8 +2,12 @@ package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.util.BusyProperty; import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import java.util.List; import java.util.List;
@ -13,6 +17,29 @@ public interface LeafAction extends BrowserAction {
public abstract void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception; public abstract void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception;
default Button toButton(OpenFileSystemModel model, List<BrowserEntry> selected) {
var b = new Button();
b.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(model.getBusy(), () -> {
execute(model, selected);
});
});
event.consume();
});
if (getShortcut() != null) {
Shortcuts.addShortcut(b, getShortcut());
}
new FancyTooltipAugment<>(new SimpleStringProperty(getName(model, selected))).augment(b);
var graphic = getIcon(model, selected);
if (graphic != null) {
b.setGraphic(graphic);
}
b.setMnemonicParsing(false);
b.setDisable(!isActive(model, selected));
return b;
}
default MenuItem toItem(OpenFileSystemModel model, List<BrowserEntry> selected, UnaryOperator<String> nameFunc) { default MenuItem toItem(OpenFileSystemModel model, List<BrowserEntry> selected, UnaryOperator<String> nameFunc) {
var mi = new MenuItem(nameFunc.apply(getName(model, selected))); var mi = new MenuItem(nameFunc.apply(getName(model, selected)));
mi.setOnAction(event -> { mi.setOnAction(event -> {
@ -35,4 +62,7 @@ public interface LeafAction extends BrowserAction {
return mi; return mi;
} }
default String getId() {
return null;
}
} }

View file

@ -22,8 +22,23 @@ public interface DirectoryType {
} }
public static void loadDefinitions() { public static void loadDefinitions() {
ALL.add(new Simple( ALL.add(new DirectoryType() {
"default", new IconVariant("default_root_folder.svg"), new IconVariant("default_root_folder_opened.svg"), ""));
@Override
public String getId() {
return "root";
}
@Override
public boolean matches(FileSystem.FileEntry entry) {
return entry.getPath().equals("/") || entry.getPath().matches("\\w:\\\\");
}
@Override
public String getIcon(FileSystem.FileEntry entry, boolean open) {
return open ? "default_root_folder_opened.svg" : "default_root_folder.svg";
}
});
AppResources.with(AppResources.XPIPE_MODULE, "folder_list.txt", path -> { AppResources.with(AppResources.XPIPE_MODULE, "folder_list.txt", path -> {
try (var reader = try (var reader =

View file

@ -13,6 +13,7 @@ import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -54,6 +55,10 @@ public abstract class Comp<S extends CompStructure<?>> {
return apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); return apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
} }
public Comp<S> vgrow() {
return apply(struc -> VBox.setVgrow(struc.get(), Priority.ALWAYS));
}
public Comp<S> visible(ObservableValue<Boolean> o) { public Comp<S> visible(ObservableValue<Boolean> o) {
return apply(struc -> struc.get().visibleProperty().bind(o)); return apply(struc -> struc.get().visibleProperty().bind(o));
} }

View file

@ -39,8 +39,7 @@ public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<
var tt = new JFXTooltip(); var tt = new JFXTooltip();
var toDisplay = text.getValue(); var toDisplay = text.getValue();
if (Shortcuts.getShortcut((Region) region) != null) { if (Shortcuts.getShortcut((Region) region) != null) {
toDisplay = toDisplay = toDisplay + " (" + Shortcuts.getShortcut((Region) region).getDisplayText() + ")";
toDisplay + " (" + Shortcuts.getShortcut((Region) region).getDisplayText() + ")";
} }
tt.textProperty().setValue(toDisplay); tt.textProperty().setValue(toDisplay);
tt.setStyle("-fx-font-size: 11pt;"); tt.setStyle("-fx-font-size: 11pt;");

View file

@ -32,6 +32,7 @@ public class Shortcuts {
}; };
AtomicReference<Scene> scene = new AtomicReference<>(); AtomicReference<Scene> scene = new AtomicReference<>();
SHORTCUTS.put(region, comb);
SimpleChangeListener.apply(region.sceneProperty(), s -> { SimpleChangeListener.apply(region.sceneProperty(), s -> {
if (Objects.equals(s, scene.get())) { if (Objects.equals(s, scene.get())) {
return; return;
@ -45,7 +46,6 @@ public class Shortcuts {
if (s != null) { if (s != null) {
scene.set(s); scene.set(s);
s.addEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.put(region, comb); SHORTCUTS.put(region, comb);
} }
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -30,6 +30,8 @@ mustNotBeEmpty=$NAME$ must not be empty
download=Drop to transfer download=Drop to transfer
dragFiles=Drag files from here dragFiles=Drag files from here
null=$VALUE$ must be not null null=$VALUE$ must be not null
roots=Roots
recent=Recent
hostFeatureUnsupported=$FEATURE$ is not available on the host hostFeatureUnsupported=$FEATURE$ is not available on the host
missingStore=$NAME$ does not exist missingStore=$NAME$ does not exist
connectionName=Connection name connectionName=Connection name

View file

@ -4,9 +4,9 @@
-fx-padding: 1em; -fx-padding: 1em;
} }
.browser .home { .browser .overview {
-fx-spacing: 1em; -fx-spacing: 1.5em;
-fx-padding: 1em; -fx-padding: 1.5em;
} }
.selected-file-list { .selected-file-list {
@ -106,9 +106,18 @@
-fx-text-fill: transparent; -fx-text-fill: transparent;
} }
.browser .path-graphic-button { .browser .path-graphic-button {
-fx-padding: 0 2px 0 7px; -fx-padding: 0 5px 0 5px;
} }
.browser .overview-file-list {
-fx-border-width: 1px;
}
.browser .overview-file-list .button {
-fx-border-width: 0;
-fx-background-radius: 0;
-fx-background-insets: 0;
}
.browser .context-menu .accelerator-text { .browser .context-menu .accelerator-text {
-fx-padding: 3px 0px 3px 50px; -fx-padding: 3px 0px 3px 50px;

View file

@ -53,7 +53,7 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO
@Override @Override
public List<String> determineInterestingPaths(ShellControl pc) throws Exception { public List<String> determineInterestingPaths(ShellControl pc) throws Exception {
var home = getHomeDirectory(pc); var home = getHomeDirectory(pc);
return List.of(FileNames.join(home, "Desktop")); return List.of(home, FileNames.join(home, "Documents"), FileNames.join(home, "Downloads"), FileNames.join(home, "Desktop"));
} }
@Override @Override

View file

@ -42,6 +42,11 @@ public interface FileSystem extends Closeable, AutoCloseable {
this.executable = executable; this.executable = executable;
this.size = size; this.size = size;
} }
public static FileEntry ofDirectory(FileSystem fileSystem, String path) {
return new FileEntry(fileSystem, path, Instant.now(), true, false, false, 0, null);
}
} }
FileSystemStore getStore(); FileSystemStore getStore();

View file

@ -7,6 +7,9 @@ import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionFormatter; import io.xpipe.app.browser.action.BrowserActionFormatter;
import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.impl.FileNames;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.Clipboard;
@ -44,6 +47,11 @@ public class CopyPathAction implements BrowserAction, BranchAction {
return "Absolute Path"; return "Absolute Path";
} }
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN);
}
@Override @Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception { public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
var s = entries.stream() var s = entries.stream()
@ -89,6 +97,11 @@ public class CopyPathAction implements BrowserAction, BranchAction {
return "File Name"; return "File Name";
} }
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.C, KeyCombination.SHIFT_DOWN, KeyCombination.SHORTCUT_DOWN);
}
@Override @Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception { public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
var s = entries.stream() var s = entries.stream()

View file

@ -13,6 +13,10 @@ import java.util.List;
public class OpenTerminalAction implements LeafAction { public class OpenTerminalAction implements LeafAction {
public String getId() {
return "openTerminal";
}
@Override @Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception { public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
if (entries.size() == 0) { if (entries.size() == 0) {