Squash merge xpipe 13 feature branches into master

This commit is contained in:
crschnick 2024-11-16 07:13:24 +00:00
parent 6dabe53011
commit 5868fcfd33
482 changed files with 7425 additions and 5709 deletions

View file

@ -27,7 +27,7 @@ You should therefore always check out the matching version tag for your local re
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags.
So for example if you currently have XPipe `11.3` installed, you should run `git reset --hard 11.3` first to properly compile against it.
You need to have JDK for Java 21 installed to compile the project.
You need to have JDK for Java 22 installed to compile the project.
If you are on Linux or macOS, you can easily accomplish that by running
```bash
curl -s "https://get.sdkman.io" | bash
@ -57,7 +57,7 @@ to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plu
## Modularity and IDEs
All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/21/) and make full use of the Java Module System (JPMS).
All XPipe components target [Java 22](https://openjdk.java.net/projects/jdk/22/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
@ -65,7 +65,7 @@ many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is IntelliJ.
When setting up the project in IntelliJ, make sure that the correct JDK (Java 21)
When setting up the project in IntelliJ, make sure that the correct JDK (Java 22)
is selected both for the project and for gradle itself.
## Contributing guide

View file

@ -23,8 +23,8 @@ dependencies {
api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:3.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.0'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.3'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.3'
api 'com.vladsch.flexmark:flexmark:0.64.8'
api 'com.vladsch.flexmark:flexmark-util:0.64.8'
@ -58,8 +58,8 @@ dependencies {
api 'org.apache.commons:commons-lang3:3.17.0'
api 'io.sentry:sentry:7.14.0'
api 'commons-io:commons-io:2.16.1'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.2"
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.1"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"

View file

@ -1,5 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.util.AskpassAlert;
import io.xpipe.app.util.SecretManager;
import io.xpipe.app.util.SecretQueryState;
@ -34,9 +35,30 @@ public class AskpassExchangeImpl extends AskpassExchange {
if (p.getState() != SecretQueryState.NORMAL) {
throw new BeaconClientException(SecretQueryState.toErrorMessage(p.getState()));
}
focusTerminalIfNeeded(msg.getPid());
return Response.builder().value(secret.inPlace()).build();
}
private void focusTerminalIfNeeded(long pid) {
var found = TerminalView.get().findSession(pid);
if (found.isEmpty()) {
return;
}
var term = TerminalView.get().getTerminalInstances().stream()
.filter(instance ->
instance.getTerminalProcess().equals(found.get().getTerminal()))
.findFirst();
if (term.isEmpty()) {
return;
}
var control = term.get().controllable();
control.ifPresent(controllableTerminalSession -> {
controllableTerminalSession.focus();
});
}
@Override
public boolean requiresEnabledApi() {
return false;

View file

@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
@ -19,8 +19,8 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
if (!(e.getStore() instanceof FileSystemStore)) {
throw new BeaconClientException("Not a file system connection");
}
BrowserSessionModel.DEFAULT.openFileSystemSync(
e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null);
BrowserFullSessionModel.DEFAULT.openFileSystemSync(
e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null, true);
AppLayoutModel.get().selectBrowser();
return Response.builder().build();
}

View file

@ -15,9 +15,9 @@ public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
.getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection()));
if (e.getStore() instanceof FixedHierarchyStore) {
DataStorage.get().refreshChildren(e, null, true);
DataStorage.get().refreshChildren(e, true);
} else {
e.validateOrThrowAndClose(null);
e.validateOrThrow();
}
return Response.builder().build();
}

View file

@ -1,10 +1,10 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionTerminalExchange;
import io.xpipe.core.store.ShellStore;
import com.sun.net.httpserver.HttpExchange;
@ -18,9 +18,8 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
if (!(e.getStore() instanceof ShellStore shellStore)) {
throw new BeaconClientException("Not a shell connection");
}
try (var sc = shellStore.control().start()) {
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
}
var sc = shellStore.getOrStartSession();
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
return Response.builder().build();
}
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.launcher.LauncherInput;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.launcher.LauncherInput;
import io.xpipe.app.util.PlatformState;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.DaemonOpenExchange;

View file

@ -2,10 +2,10 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BeaconShellSession;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ShellStartExchange;
import io.xpipe.core.store.ShellStore;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
@ -25,7 +25,9 @@ public class ShellStartExchangeImpl extends ShellStartExchange {
var existing = AppBeaconServer.get().getCache().getShellSessions().stream()
.filter(beaconShellSession -> beaconShellSession.getEntry().equals(e))
.findFirst();
var control = (existing.isPresent() ? existing.get().getControl() : s.control());
var control = (existing.isPresent()
? existing.get().getControl()
: s.standaloneControl().start());
control.setNonInteractive();
control.start();

View file

@ -1,7 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.api.SshLaunchExchange;
import io.xpipe.core.process.ShellDialects;

View file

@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.TerminalLaunchExchange;

View file

@ -1,6 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalWaitExchange;
@ -10,7 +11,8 @@ import com.sun.net.httpserver.HttpExchange;
public class TerminalWaitExchangeImpl extends TerminalWaitExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
TerminalLauncherManager.waitExchange(msg.getRequest());
TerminalView.get().open(msg.getRequest(), msg.getPid());
TerminalLauncherManager.waitExchange(msg.getRequest(), msg.getPid());
return Response.builder().build();
}

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser.session;
package io.xpipe.app.browser;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
@ -13,13 +13,13 @@ import javafx.collections.ObservableList;
import lombok.Getter;
@Getter
public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();
protected final Property<T> selectedEntry = new SimpleObjectProperty<>();
protected final BooleanProperty busy = new SimpleBooleanProperty();
public void closeAsync(BrowserSessionTab<?> e) {
public void closeAsync(BrowserSessionTab e) {
ThreadHelper.runAsync(() -> {
closeSync(e);
});
@ -37,7 +37,7 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
}
}
public void closeSync(BrowserSessionTab<?> e) {
public void closeSync(BrowserSessionTab e) {
e.close();
synchronized (BrowserAbstractSessionModel.this) {
this.sessionEntries.remove(e);

View file

@ -1,25 +1,25 @@
package io.xpipe.app.browser.session;
package io.xpipe.app.browser;
import io.xpipe.app.browser.BrowserBookmarkComp;
import io.xpipe.app.browser.BrowserBookmarkHeaderComp;
import io.xpipe.app.browser.file.BrowserConnectionListComp;
import io.xpipe.app.browser.file.BrowserConnectionListFilterComp;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemComp;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.file.BrowserFileSystemTabComp;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener;
@ -37,12 +37,12 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class BrowserChooserComp extends DialogComp {
public class BrowserFileChooserSessionComp extends DialogComp {
private final Stage stage;
private final BrowserFileChooserModel model;
private final BrowserFileChooserSessionModel model;
public BrowserChooserComp(Stage stage, BrowserFileChooserModel model) {
public BrowserFileChooserSessionComp(Stage stage, BrowserFileChooserSessionModel model) {
this.stage = stage;
this.model = model;
}
@ -50,9 +50,9 @@ public class BrowserChooserComp extends DialogComp {
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file, boolean save) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserFileChooserModel(OpenFileSystemModel.SelectionMode.SINGLE_FILE);
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> {
var comp = new BrowserChooserComp(stage, model);
var comp = new BrowserFileChooserSessionComp(stage, model);
comp.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()))
.styleClass("browser")
@ -114,8 +114,8 @@ public class BrowserChooserComp extends DialogComp {
});
};
var bookmarkTopBar = new BrowserBookmarkHeaderComp();
var bookmarksList = new BrowserBookmarkComp(
var bookmarkTopBar = new BrowserConnectionListFilterComp();
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(model.getSelectedEntry(), v -> v.getEntry().get()),
applicable,
action,
@ -138,7 +138,7 @@ public class BrowserChooserComp extends DialogComp {
model.getSelectedEntry().subscribe(selected -> {
PlatformThread.runLaterIfNeeded(() -> {
if (selected != null) {
s.getChildren().setAll(new OpenFileSystemComp(selected, false).createRegion());
s.getChildren().setAll(new BrowserFileSystemTabComp(selected, false).createRegion());
} else {
s.getChildren().clear();
}
@ -148,7 +148,7 @@ public class BrowserChooserComp extends DialogComp {
});
var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer)).styleClass("left");
var splitPane = new SideSplitPaneComp(vertical, stack)
var splitPane = new LeftSplitPaneComp(vertical, stack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.styleClass("background")

View file

@ -1,10 +1,10 @@
package io.xpipe.app.browser.session;
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
@ -24,15 +24,15 @@ import java.util.List;
import java.util.function.Consumer;
@Getter
public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFileSystemModel> {
public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<BrowserFileSystemTabModel> {
private final OpenFileSystemModel.SelectionMode selectionMode;
private final BrowserFileSystemTabModel.SelectionMode selectionMode;
private final ObservableList<BrowserEntry> fileSelection = FXCollections.observableArrayList();
@Setter
private Consumer<List<FileReference>> onFinish;
public BrowserFileChooserModel(OpenFileSystemModel.SelectionMode selectionMode) {
public BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode selectionMode) {
this.selectionMode = selectionMode;
selectedEntry.addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
@ -48,7 +48,7 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFil
public void finishChooser() {
var chosen = new ArrayList<>(fileSelection);
synchronized (BrowserFileChooserModel.this) {
synchronized (BrowserFileChooserSessionModel.this) {
var open = selectedEntry.getValue();
if (open != null) {
ThreadHelper.runAsync(() -> {
@ -66,7 +66,7 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFil
}
public void finishWithoutChoice() {
synchronized (BrowserFileChooserModel.this) {
synchronized (BrowserFileChooserSessionModel.this) {
var open = selectedEntry.getValue();
if (open != null) {
ThreadHelper.runAsync(() -> {
@ -78,20 +78,20 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFil
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model;
BrowserFileSystemTabModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new OpenFileSystemModel(this, store, selectionMode);
model = new BrowserFileSystemTabModel(this, store, selectionMode);
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserFileChooserModel.this) {
synchronized (BrowserFileChooserSessionModel.this) {
selectedEntry.setValue(model);
sessionEntries.add(model);
}

View file

@ -1,44 +1,108 @@
package io.xpipe.app.browser.session;
package io.xpipe.app.browser;
import io.xpipe.app.browser.BrowserBookmarkComp;
import io.xpipe.app.browser.BrowserBookmarkHeaderComp;
import io.xpipe.app.browser.BrowserTransferComp;
import io.xpipe.app.browser.file.BrowserConnectionListComp;
import io.xpipe.app.browser.file.BrowserConnectionListFilterComp;
import io.xpipe.app.browser.file.BrowserTransferComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.AnchorComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.AnchorComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
public class BrowserSessionComp extends SimpleComp {
public class BrowserFullSessionComp extends SimpleComp {
private final BrowserSessionModel model;
private final BrowserFullSessionModel model;
public BrowserSessionComp(BrowserSessionModel model) {
public BrowserFullSessionComp(BrowserFullSessionModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var vertical = createLeftSide();
var leftSplit = new SimpleDoubleProperty();
var rightSplit = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, leftSplit, rightSplit);
tabs.apply(struc -> {
struc.get().setViewOrder(1);
struc.get().setPickOnBounds(false);
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setLeftAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
});
vertical.apply(struc -> {
struc.get()
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight()));
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 3.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");
var pinnedStack = createSplitStack(rightSplit, tabs);
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
leftSplit.set(d);
});
splitPane.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false);
});
splitPane.apply(struc -> {
struc.get().skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
struc.get().getChildrenUnmodifiable().forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
});
struc.get().lookupAll(".split-pane-divider").forEach(node -> node.setViewOrder(1));
});
}
});
});
splitPane.styleClass("browser");
return splitPane.createRegion();
}
private Comp<CompStructure<VBox>> createLeftSide() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
if (!storeEntryWrapper.getEntry().getValidity().isUsable()) {
return false;
@ -65,9 +129,13 @@ public class BrowserSessionComp extends SimpleComp {
});
};
var bookmarkTopBar = new BrowserBookmarkHeaderComp();
var bookmarksList = new BrowserBookmarkComp(
BindingsHelper.map(model.getSelectedEntry(), v -> v.getEntry().get()),
var bookmarkTopBar = new BrowserConnectionListFilterComp();
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(
model.getSelectedEntry(),
v -> v instanceof BrowserStoreSessionTab<?> st
? st.getEntry().get()
: null),
applicable,
action,
bookmarkTopBar.getCategory(),
@ -98,51 +166,49 @@ public class BrowserSessionComp extends SimpleComp {
localDownloadStage.maxHeight(200);
var vertical =
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
return vertical;
}
var split = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, split).apply(struc -> {
struc.get().setViewOrder(1);
struc.get().setPickOnBounds(false);
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setLeftAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");
var loadingStack = new AnchorComp(List.of(tabs, loadingIndicator));
var splitPane = new SideSplitPaneComp(vertical, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
split.set(d);
})
.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false);
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {
var cache = new HashMap<BrowserSessionTab, Region>();
var splitStack = new StackComp(List.of());
splitStack.apply(struc -> {
model.getEffectiveRightTab().subscribe((newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
var all = model.getAllTabs();
cache.keySet().removeIf(browserSessionTab -> !all.contains(browserSessionTab));
if (newValue == null) {
struc.get().getChildren().clear();
return;
}
var cached = cache.containsKey(newValue);
if (!cached) {
cache.put(newValue, newValue.comp().createRegion());
}
var r = cache.get(newValue);
struc.get().getChildren().clear();
struc.get().getChildren().add(r);
struc.get().setMinWidth(rightSplit.get());
struc.get().setMaxWidth(rightSplit.get());
struc.get().setPrefWidth(rightSplit.get());
});
});
splitPane.apply(struc -> {
struc.get().skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
struc.get().getChildrenUnmodifiable().forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
});
struc.get().lookupAll(".split-pane-divider").forEach(node -> node.setViewOrder(1));
});
}
rightSplit.addListener((observable, oldValue, newValue) -> {
struc.get().setMinWidth(newValue.doubleValue());
struc.get().setMaxWidth(newValue.doubleValue());
struc.get().setPrefWidth(newValue.doubleValue());
});
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
tabs.getHeaderHeight().subscribe(number -> {
AnchorPane.setTopAnchor(struc.get(), number.doubleValue());
});
});
var r = splitPane.createRegion();
r.getStyleClass().add("browser");
return r;
return splitStack;
}
}

View file

@ -0,0 +1,222 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.file.BrowserHistorySavedState;
import io.xpipe.app.browser.file.BrowserHistorySavedStateImpl;
import io.xpipe.app.browser.file.BrowserHistoryTabModel;
import io.xpipe.app.browser.file.BrowserTransferModel;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableMap;
import lombok.Getter;
import java.util.*;
@Getter
public class BrowserFullSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab> {
public static final BrowserFullSessionModel DEFAULT = new BrowserFullSessionModel();
static {
DEFAULT.getSessionEntries().add(new BrowserHistoryTabModel(DEFAULT));
}
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
private final Property<BrowserSessionTab> globalPinnedTab = new SimpleObjectProperty<>();
private final ObservableMap<BrowserSessionTab, BrowserSessionTab> splits = FXCollections.observableHashMap();
private final ObservableValue<BrowserSessionTab> effectiveRightTab = createEffectiveRightTab();
private ObservableValue<BrowserSessionTab> createEffectiveRightTab() {
return Bindings.createObjectBinding(
() -> {
var current = selectedEntry.getValue();
if (!current.isCloseable()) {
return null;
}
var split = splits.get(current);
if (split != null) {
return split;
}
var global = globalPinnedTab.getValue();
if (global == null) {
return null;
}
if (global == selectedEntry.getValue()) {
return null;
}
return global;
},
globalPinnedTab,
selectedEntry,
splits);
}
public BrowserFullSessionModel() {
sessionEntries.addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
var v = globalPinnedTab.getValue();
if (v != null && !c.getList().contains(v)) {
globalPinnedTab.setValue(null);
}
splits.keySet().removeIf(browserSessionTab -> !c.getList().contains(browserSessionTab));
});
}
public Set<BrowserSessionTab> getAllTabs() {
var set = new HashSet<BrowserSessionTab>();
set.addAll(sessionEntries);
set.addAll(splits.values());
if (globalPinnedTab.getValue() != null) {
set.add(globalPinnedTab.getValue());
}
return set;
}
public void splitTab(BrowserSessionTab tab, BrowserSessionTab split) {
if (splits.containsKey(tab)) {
return;
}
splits.put(tab, split);
ThreadHelper.runFailableAsync(() -> {
split.init();
});
}
public void unsplitTab(BrowserSessionTab tab) {
if (splits.values().remove(tab)) {
ThreadHelper.runFailableAsync(() -> {
tab.close();
});
}
}
public void pinTab(BrowserSessionTab tab) {
if (tab.equals(globalPinnedTab.getValue())) {
return;
}
globalPinnedTab.setValue(tab);
var nextIndex = getSessionEntries().indexOf(tab) + 1;
if (nextIndex < getSessionEntries().size()) {
getSelectedEntry().setValue(getSessionEntries().get(nextIndex));
}
}
public void unpinTab(BrowserSessionTab tab) {
ThreadHelper.runFailableAsync(() -> {
globalPinnedTab.setValue(null);
});
}
public void restoreState(BrowserHistorySavedState state) {
ThreadHelper.runAsync(() -> {
var l = new ArrayList<>(state.getEntries());
l.forEach(e -> {
restoreStateAsync(e, null);
// Don't try to run everything in parallel as that can be taxing
ThreadHelper.sleep(1000);
});
});
}
public void restoreStateAsync(BrowserHistorySavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(entry.ref(), model -> e.getPath(), busy);
});
}
public void reset() {
synchronized (BrowserFullSessionModel.this) {
var all = new ArrayList<>(sessionEntries);
for (var o : all) {
// Don't close busy connections gracefully
// as we otherwise might lock up
if (!o.canImmediatelyClose()) {
continue;
}
// Prevent blocking of shutdown
closeAsync(o);
}
BrowserHistorySavedStateImpl.get().save();
}
// Delete all files
localTransfersStage.clear(true);
}
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
openFileSystemSync(store, path, externalBusy, true);
});
}
public BrowserFileSystemTabModel openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
BooleanProperty externalBusy,
boolean select)
throws Exception {
BrowserFileSystemTabModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
model = new BrowserFileSystemTabModel(this, store, BrowserFileSystemTabModel.SelectionMode.ALL);
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserFullSessionModel.this) {
sessionEntries.add(model);
if (select) {
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
}
}
}
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {
model.initWithDefaultDirectory();
}
return model;
}
@Override
public void closeSync(BrowserSessionTab e) {
var split = splits.get(e);
if (split != null) {
split.close();
}
super.closeSync(e);
}
}

View file

@ -0,0 +1,41 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.storage.DataColor;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import lombok.Getter;
@Getter
public abstract class BrowserSessionTab {
protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final BrowserAbstractSessionModel<?> browserModel;
protected final String name;
protected final Property<BrowserSessionTab> splitTab = new SimpleObjectProperty<>();
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, String name) {
this.browserModel = browserModel;
this.name = name;
}
public abstract Comp<?> comp();
public abstract boolean canImmediatelyClose();
public abstract void init() throws Exception;
public abstract void close();
public abstract String getIcon();
public abstract DataColor getColor();
public boolean isCloseable() {
return true;
}
}

View file

@ -1,35 +1,34 @@
package io.xpipe.app.browser.session;
package io.xpipe.app.browser;
import io.xpipe.app.browser.BrowserWelcomeComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.skin.TabPaneSkin;
import javafx.scene.input.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.theme.Styles;
import lombok.Getter;
import java.util.*;
@ -39,28 +38,33 @@ import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
public class BrowserSessionTabsComp extends SimpleComp {
private final BrowserSessionModel model;
private final BrowserFullSessionModel model;
private final ObservableDoubleValue leftPadding;
private final DoubleProperty rightPadding;
public BrowserSessionTabsComp(BrowserSessionModel model, ObservableDoubleValue leftPadding) {
@Getter
private final DoubleProperty headerHeight;
public BrowserSessionTabsComp(
BrowserFullSessionModel model, ObservableDoubleValue leftPadding, DoubleProperty rightPadding) {
this.model = model;
this.leftPadding = leftPadding;
this.rightPadding = rightPadding;
this.headerHeight = new SimpleDoubleProperty();
}
public Region createSimple() {
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put(Comp.hspacer().styleClass("top-spacer"), new SimpleBooleanProperty(true));
map.put(Comp.of(() -> createTabPane()), Bindings.isNotEmpty(model.getSessionEntries()));
map.put(
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
Bindings.createBooleanBinding(
() -> {
return model.getSessionEntries().size() == 0;
},
model.getSessionEntries()));
var multi = new MultiContentComp(map);
multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER));
return multi.createRegion();
var tabs = createTabPane();
var topBackground = Comp.hspacer().styleClass("top-spacer").createRegion();
leftPadding.subscribe(number -> {
StackPane.setMargin(topBackground, new Insets(0, 0, 0, -number.doubleValue() - 6));
});
var stack = new StackPane(topBackground, tabs);
stack.setAlignment(Pos.TOP_CENTER);
topBackground.prefHeightProperty().bind(headerHeight);
topBackground.maxHeightProperty().bind(topBackground.prefHeightProperty());
topBackground.prefWidthProperty().bind(tabs.widthProperty());
return stack;
}
private TabPane createTabPane() {
@ -69,6 +73,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
tabs.setTabMinWidth(Region.USE_PREF_SIZE);
tabs.setTabMaxWidth(400);
tabs.setTabClosingPolicy(ALL_TABS);
tabs.setSkin(new TabPaneSkin(tabs));
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
@ -80,22 +85,31 @@ public class BrowserSessionTabsComp extends SimpleComp {
tabs.lookupAll(".tab-header-area").forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
var r = (Region) node;
r.prefHeightProperty().bind(r.maxHeightProperty());
r.setMinHeight(Region.USE_PREF_SIZE);
});
tabs.lookupAll(".headers-region").forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
var r = (Region) node;
r.prefHeightProperty().bind(r.maxHeightProperty());
r.setMinHeight(Region.USE_PREF_SIZE);
});
Region headerArea = (Region) tabs.lookup(".tab-header-area");
headerArea
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(0, 0, 0, -leftPadding.get() + 2), leftPadding));
() -> new Insets(2, 0, 4, -leftPadding.get() + 2), leftPadding));
headerHeight.bind(headerArea.heightProperty());
});
}
});
var map = new HashMap<BrowserSessionTab<?>, Tab>();
var map = new HashMap<BrowserSessionTab, Tab>();
// Restore state
model.getSessionEntries().forEach(v -> {
@ -156,7 +170,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
});
model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab<?>>) c -> {
model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
while (c.next()) {
for (var r : c.getRemoved()) {
PlatformThread.runLaterIfNeeded(() -> {
@ -245,9 +259,38 @@ public class BrowserSessionTabsComp extends SimpleComp {
return tabs;
}
private ContextMenu createContextMenu(TabPane tabs, Tab tab) {
private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) {
var cm = ContextMenuHelper.create();
if (tabModel.isCloseable()) {
var unpin = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("unpinTab"));
unpin.visibleProperty()
.bind(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return model.getGlobalPinnedTab().getValue() != null
&& model.getGlobalPinnedTab().getValue().equals(tabModel);
},
model.getGlobalPinnedTab())));
unpin.setOnAction(event -> {
model.unpinTab(tabModel);
event.consume();
});
cm.getItems().add(unpin);
var pin = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("pinTab"));
pin.visibleProperty()
.bind(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return model.getGlobalPinnedTab().getValue() == null;
},
model.getGlobalPinnedTab())));
pin.setOnAction(event -> {
model.pinTab(tabModel);
event.consume();
});
cm.getItems().add(pin);
}
var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab"));
select.acceleratorProperty()
.bind(Bindings.createObjectBinding(
@ -272,7 +315,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab"));
close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
if (tab.isClosable()) {
tabs.getTabs().remove(tab);
}
event.consume();
});
cm.getItems().add(close);
@ -280,7 +325,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs"));
closeOthers.setOnAction(event -> {
tabs.getTabs()
.removeAll(tabs.getTabs().stream().filter(t -> t != tab).toList());
.removeAll(tabs.getTabs().stream()
.filter(t -> t != tab && t.isClosable())
.toList());
event.consume();
});
cm.getItems().add(closeOthers);
@ -290,7 +337,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) < index)
.filter(t -> tabs.getTabs().indexOf(t) < index && t.isClosable())
.toList());
event.consume();
});
@ -301,7 +348,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) > index)
.filter(t -> tabs.getTabs().indexOf(t) > index && t.isClosable())
.toList());
event.consume();
});
@ -311,7 +358,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
closeAll.setAccelerator(
new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
closeAll.setOnAction(event -> {
tabs.getTabs().clear();
tabs.getTabs()
.removeAll(
tabs.getTabs().stream().filter(t -> t.isClosable()).toList());
event.consume();
});
cm.getItems().add(closeAll);
@ -319,36 +368,84 @@ public class BrowserSessionTabsComp extends SimpleComp {
return cm;
}
private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) {
private Tab createTab(TabPane tabs, BrowserSessionTab tabModel) {
var tab = new Tab();
tab.setContextMenu(createContextMenu(tabs, tab));
if (tabModel.isCloseable()) {
tab.setContextMenu(createContextMenu(tabs, tab, tabModel));
}
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get()
&& !AppPrefs.get().performanceMode().get()
? -1d
: 0,
PlatformThread.sync(model.getBusy()),
AppPrefs.get().performanceMode()));
tab.setClosable(tabModel.isCloseable());
// Prevent closing while busy
tab.setOnCloseRequest(event -> {
if (!tabModel.canImmediatelyClose()) {
event.consume();
}
});
var image = model.getEntry().get().getEffectiveIconFile();
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion();
if (tabModel.getIcon() != null) {
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> tabModel.getBusy().get()
&& !AppPrefs.get().performanceMode().get()
? -1d
: 0,
PlatformThread.sync(tabModel.getBusy()),
AppPrefs.get().performanceMode()));
tab.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
return model.getBusy().get() ? ring : logo;
},
PlatformThread.sync(model.getBusy())));
tab.setText(model.getName());
var image = tabModel.getIcon();
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion();
Comp<?> comp = model.comp();
tab.setContent(comp.createRegion());
tab.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
return tabModel.getBusy().get() ? ring : logo;
},
PlatformThread.sync(tabModel.getBusy())));
}
if (tabModel.getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
var global = PlatformThread.sync(sessionModel.getGlobalPinnedTab());
tab.textProperty()
.bind(Bindings.createStringBinding(
() -> {
return tabModel.getName()
+ (global.getValue() == tabModel ? " (" + AppI18n.get("pinned") + ")" : "");
},
global,
AppPrefs.get().language()));
} else {
tab.setText(tabModel.getName());
}
Comp<?> comp = tabModel.comp();
var compRegion = comp.createRegion();
var empty = new StackPane();
empty.setMinWidth(100);
empty.widthProperty().addListener((observable, oldValue, newValue) -> {
if (tabModel.isCloseable() && tabs.getSelectionModel().getSelectedItem() == tab) {
rightPadding.setValue(newValue.doubleValue());
}
});
var split = new SplitPane(compRegion);
if (tabModel.isCloseable()) {
split.getItems().add(empty);
}
model.getEffectiveRightTab().subscribe(browserSessionTab -> {
PlatformThread.runLaterIfNeeded(() -> {
if (browserSessionTab != null && split.getItems().size() > 1) {
split.getItems().set(1, empty);
} else if (browserSessionTab != null && split.getItems().size() == 1) {
split.getItems().add(empty);
} else if (browserSessionTab == null && split.getItems().size() > 1) {
split.getItems().remove(1);
}
});
});
tab.setContent(split);
var id = UUID.randomUUID().toString();
tab.setId(id);
@ -360,18 +457,19 @@ public class BrowserSessionTabsComp extends SimpleComp {
var w = l.maxWidthProperty();
l.minWidthProperty().bind(w);
l.prefWidthProperty().bind(w);
if (!tabModel.isCloseable()) {
l.pseudoClassStateChanged(PseudoClass.getPseudoClass("static"), true);
}
var close = (StackPane) tabs.lookup("#" + id + " .tab-close-button");
close.setPrefWidth(30);
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
c.getStyleClass().add("color-box");
var color =
DataStorage.get().getEffectiveColor(model.getEntry().get());
var color = tabModel.getColor();
if (color != null) {
c.getStyleClass().add(color.getId());
}
new TooltipAugment<>(new SimpleStringProperty(model.getTooltip()), null).augment(c);
c.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(

View file

@ -0,0 +1,38 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import lombok.Getter;
@Getter
public abstract class BrowserStoreSessionTab<T extends DataStore> extends BrowserSessionTab {
protected final DataStoreEntryRef<? extends T> entry;
public BrowserStoreSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
super(browserModel, DataStorage.get().getStoreEntryDisplayName(entry.get()));
this.entry = entry;
}
public abstract Comp<?> comp();
public abstract boolean canImmediatelyClose();
public abstract void init() throws Exception;
public abstract void close();
@Override
public String getIcon() {
return entry.get().getEffectiveIconFile();
}
@Override
public DataColor getColor() {
return DataStorage.get().getEffectiveColor(entry.get());
}
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.ModuleLayerLoader;
@ -18,25 +18,25 @@ public interface BrowserAction {
List<BrowserAction> ALL = new ArrayList<>();
static List<LeafAction> getFlattened(OpenFileSystemModel model, List<BrowserEntry> entries) {
static List<BrowserLeafAction> getFlattened(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return ALL.stream()
.map(browserAction -> getFlattened(browserAction, model, entries))
.flatMap(List::stream)
.toList();
}
static List<LeafAction> getFlattened(
BrowserAction browserAction, OpenFileSystemModel model, List<BrowserEntry> entries) {
return browserAction instanceof LeafAction
? List.of((LeafAction) browserAction)
: ((BranchAction) browserAction)
static List<BrowserLeafAction> getFlattened(
BrowserAction browserAction, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return browserAction instanceof BrowserLeafAction
? List.of((BrowserLeafAction) browserAction)
: ((BrowserBranchAction) browserAction)
.getBranchingActions(model, entries).stream()
.map(action -> getFlattened(action, model, entries))
.flatMap(List::stream)
.toList();
}
static LeafAction byId(String id, OpenFileSystemModel model, List<BrowserEntry> entries) {
static BrowserLeafAction byId(String id, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return getFlattened(model, entries).stream()
.filter(browserAction -> id.equals(browserAction.getId()))
.findAny()
@ -52,15 +52,15 @@ public interface BrowserAction {
: selected;
}
MenuItem toMenuItem(OpenFileSystemModel model, List<BrowserEntry> selected);
MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected);
default void init(OpenFileSystemModel model) throws Exception {}
default void init(BrowserFileSystemTabModel model) throws Exception {}
default String getProFeatureId() {
return null;
}
default Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
default Node getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return null;
}
@ -76,9 +76,9 @@ public interface BrowserAction {
return false;
}
ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries);
ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
default boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
@ -86,7 +86,7 @@ public interface BrowserAction {
return true;
}
default boolean isActive(OpenFileSystemModel model, List<BrowserEntry> entries) {
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}

View file

@ -1,22 +1,22 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import java.util.List;
public interface ApplicationPathAction extends BrowserAction {
public interface BrowserApplicationPathAction extends BrowserAction {
String getExecutable();
@Override
default void init(OpenFileSystemModel model) {
default void init(BrowserFileSystemTabModel model) {
// Cache result for later calls
model.getCache().isApplicationInPath(getExecutable());
}
@Override
default boolean isActive(OpenFileSystemModel model, List<BrowserEntry> entries) {
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getCache().isApplicationInPath(getExecutable());
}
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.Menu;
@ -11,9 +11,9 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface BranchAction extends BrowserAction {
public interface BrowserBranchAction extends BrowserAction {
default MenuItem toMenuItem(OpenFileSystemModel model, List<BrowserEntry> selected) {
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var m = new Menu(getName(model, selected).getValue() + " ...");
for (var sub : getBranchingActions(model, selected)) {
var subselected = resolveFilesIfNeeded(selected);
@ -37,5 +37,5 @@ public interface BranchAction extends BrowserAction {
return m;
}
List<? extends BrowserAction> getBranchingActions(OpenFileSystemModel model, List<BrowserEntry> entries);
List<? extends BrowserAction> getBranchingActions(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
}

View file

@ -1,9 +1,9 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.ThreadHelper;
@ -17,18 +17,13 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface LeafAction extends BrowserAction {
public interface BrowserLeafAction extends BrowserAction {
void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception;
void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) throws Exception;
default Button toButton(Region root, OpenFileSystemModel model, List<BrowserEntry> selected) {
default Button toButton(Region root, BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var b = new Button();
b.setOnAction(event -> {
// Only accept shortcut actions in the current tab
if (!model.equals(model.getBrowserModel().getSelectedEntry().getValue())) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
if (model.getFileSystem() == null) {
@ -71,13 +66,12 @@ public interface LeafAction extends BrowserAction {
return b;
}
default MenuItem toMenuItem(OpenFileSystemModel model, List<BrowserEntry> selected) {
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
var name = getName(model, selected);
var mi = new MenuItem();
mi.textProperty().bind(BindingsHelper.map(name, s -> {
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
return s + " (Pro)";
if (getProFeatureId() != null) {
return LicenseProvider.get().getFeature(getProFeatureId()).suffix(s);
}
return s;
}));

View file

@ -1,8 +1,7 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.store.FileNames;
import javafx.scene.Node;
@ -18,9 +17,9 @@ import java.util.ArrayList;
public class BrowserBreadcrumbBar extends SimpleComp {
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
public BrowserBreadcrumbBar(OpenFileSystemModel model) {
public BrowserBreadcrumbBar(BrowserFileSystemTabModel model) {
this.model = model;
}

View file

@ -1,8 +1,5 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
@ -55,7 +52,7 @@ public class BrowserClipboard {
var entries = new ArrayList<BrowserEntry>();
for (Path file : files) {
entries.add(LocalFileSystem.getLocalBrowserEntry(file));
entries.add(BrowserLocalFileSystem.getLocalBrowserEntry(file));
}
currentCopyClipboard.setValue(

View file

@ -1,11 +1,11 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.store.*;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@ -19,7 +19,7 @@ import java.util.HashSet;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
public final class BrowserBookmarkComp extends SimpleComp {
public final class BrowserConnectionListComp extends SimpleComp {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final ObservableValue<DataStoreEntry> selected;
@ -28,7 +28,7 @@ public final class BrowserBookmarkComp extends SimpleComp {
private final Property<StoreCategoryWrapper> category;
private final Property<String> filter;
public BrowserBookmarkComp(
public BrowserConnectionListComp(
ObservableValue<DataStoreEntry> selected,
Predicate<StoreEntryWrapper> applicable,
BiConsumer<StoreEntryWrapper, BooleanProperty> action,

View file

@ -1,11 +1,11 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.store.StoreCategoryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import javafx.beans.property.Property;
@ -19,7 +19,7 @@ import lombok.Getter;
import java.util.List;
@Getter
public final class BrowserBookmarkHeaderComp extends SimpleComp {
public final class BrowserConnectionListFilterComp extends SimpleComp {
private final Property<StoreCategoryWrapper> category =
new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());

View file

@ -1,7 +1,6 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.InputHelper;
@ -13,11 +12,11 @@ import java.util.List;
public final class BrowserContextMenu extends ContextMenu {
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
private final BrowserEntry source;
private final boolean quickAccess;
public BrowserContextMenu(OpenFileSystemModel model, BrowserEntry source, boolean quickAccess) {
public BrowserContextMenu(BrowserFileSystemTabModel model, BrowserEntry source, boolean quickAccess) {
this.model = model;
this.source = source;
this.quickAccess = quickAccess;

View file

@ -1,12 +1,10 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.*;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
@ -16,30 +14,23 @@ import io.xpipe.core.store.FileNames;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@ -81,7 +72,8 @@ public final class BrowserFileListComp extends SimpleComp {
: null));
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
filenameCol.setSortType(ASCENDING);
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing(), col.getTableView()));
filenameCol.setCellFactory(col ->
new BrowserFileListNameCell(fileList, typedSelection, fileList.getEditing(), col.getTableView()));
filenameCol.setReorderable(false);
filenameCol.setResizable(false);
@ -130,7 +122,7 @@ public final class BrowserFileListComp extends SimpleComp {
table.setAccessibleText("Directory contents");
table.setPlaceholder(new Region());
table.getStyleClass().add(Styles.STRIPED);
table.getColumns().setAll(filenameCol, sizeCol, modeCol, ownerCol, mtimeCol);
table.getColumns().setAll(filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);
table.getSortOrder().add(filenameCol);
table.setFocusTraversable(true);
table.setSortPolicy(param -> {
@ -138,6 +130,34 @@ public final class BrowserFileListComp extends SimpleComp {
return true;
});
table.setFixedCellSize(32.0);
prepareColumnVisibility(table, ownerCol, filenameCol);
prepareTableScrollFix(table);
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, filenameCol, mtimeCol, modeCol, ownerCol);
prepareTypedSelectionModel(table);
return table;
}
private static void prepareTableScrollFix(TableView<BrowserEntry> table) {
table.lookupAll(".scroll-bar").stream()
.filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass("horizontal")))
.findFirst()
.ifPresent(node -> {
Region region = (Region) node;
region.setMinHeight(0);
region.setPrefHeight(0);
region.setMaxHeight(0);
});
}
private void prepareColumnVisibility(
TableView<BrowserEntry> table,
TableColumn<BrowserEntry, String> ownerCol,
TableColumn<BrowserEntry, String> filenameCol) {
var os = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
@ -150,24 +170,6 @@ public final class BrowserFileListComp extends SimpleComp {
var width = getFilenameWidth(table);
filenameCol.setPrefWidth(width);
});
table.lookupAll(".scroll-bar").stream()
.filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass("horizontal")))
.findFirst()
.ifPresent(node -> {
Region region = (Region) node;
region.setMinHeight(0);
region.setPrefHeight(0);
region.setMaxHeight(0);
});
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, filenameCol, mtimeCol, modeCol, ownerCol);
prepareTypedSelectionModel(table);
return table;
}
private double getFilenameWidth(TableView<?> tableView) {
@ -274,8 +276,15 @@ public final class BrowserFileListComp extends SimpleComp {
}
table.getSelectionModel().setCellSelectionEnabled(false);
var updateFromModel = new BooleanScope(new SimpleBooleanProperty());
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
fileList.getSelection().setAll(c.getList());
if (updateFromModel.get()) {
return;
}
try (var ignored = updateFromModel) {
fileList.getSelection().setAll(c.getList());
}
});
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
@ -284,16 +293,27 @@ public final class BrowserFileListComp extends SimpleComp {
}
Platform.runLater(() -> {
if (c.getList().isEmpty()) {
var tableIndices = table.getSelectionModel().getSelectedItems().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
var indices = c.getList().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
if (Arrays.equals(indices, tableIndices)) {
return;
}
if (indices.length == 0) {
table.getSelectionModel().clearSelection();
return;
}
var indices = c.getList().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
table.getSelectionModel()
.selectIndices(table.getItems().indexOf(c.getList().getFirst()), indices);
if (indices.length == 1) {
table.getSelectionModel().clearAndSelect(indices[0]);
} else {
table.getSelectionModel().clearSelection();
table.getSelectionModel().selectIndices(indices[0], indices);
}
});
});
}
@ -313,8 +333,10 @@ public final class BrowserFileListComp extends SimpleComp {
.filter(browserAction -> browserAction.getShortcut().match(event))
.findAny();
action.ifPresent(browserAction -> {
// Prevent concurrent modification by creating copy on platform thread
var selectionCopy = new ArrayList<>(selected);
ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selected);
browserAction.execute(fileList.getFileSystemModel(), selectionCopy);
});
event.consume();
});
@ -610,157 +632,4 @@ public final class BrowserFileListComp extends SimpleComp {
}
}
}
private class FilenameCell extends TableCell<BrowserEntry, String> {
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
private final BooleanProperty updating = new SimpleBooleanProperty();
public FilenameCell(Property<BrowserEntry> editing, TableView<BrowserEntry> tableView) {
accessibleTextProperty()
.bind(Bindings.createStringBinding(
() -> {
return getItem() != null ? getItem() : null;
},
itemProperty()));
setAccessibleRole(AccessibleRole.TEXT);
var textField = new LazyTextFieldComp(text)
.minWidth(USE_PREF_SIZE)
.createStructure()
.get();
var quickAccess = new BrowserQuickAccessButtonComp(
() -> getTableRow().getItem(), fileList.getFileSystemModel())
.hide(Bindings.createBooleanBinding(
() -> {
if (getTableRow() == null) {
return true;
}
var item = getTableRow().getItem();
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
var isParentLink = item.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
return notDir || isParentLink;
},
itemProperty()))
.focusTraversable(false)
.createRegion();
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
PlatformThread.runLaterIfNeeded(() -> {
textField.setDisable(false);
textField.requestFocus();
});
}
});
ChangeListener<String> listener = (observable, oldValue, newValue) -> {
if (updating.get()) {
return;
}
getTableRow().requestFocus();
var it = getTableRow().getItem();
editing.setValue(null);
ThreadHelper.runAsync(() -> {
if (it == null) {
return;
}
var r = fileList.rename(it, newValue);
Platform.runLater(() -> {
updateItem(getItem(), isEmpty());
fileList.getSelection().setAll(r);
getTableView().scrollTo(r);
});
});
};
text.addListener(listener);
Node imageView = PrettyImageHelper.ofFixedSize(img, 24, 24).createRegion();
HBox graphic = new HBox(imageView, new Spacer(5), quickAccess, new Spacer(1), textField);
quickAccess.prefHeightProperty().bind(graphic.heightProperty());
graphic.setAlignment(Pos.CENTER_LEFT);
graphic.setPrefHeight(34);
HBox.setHgrow(textField, Priority.ALWAYS);
graphic.setAlignment(Pos.CENTER_LEFT);
setGraphic(graphic);
InputHelper.onExactKeyCode(tableView, KeyCode.RIGHT, false, event -> {
var selected = fileList.getSelection();
if (selected.size() == 1 && selected.getFirst() == getTableRow().getItem()) {
((ButtonBase) quickAccess).fire();
event.consume();
}
});
InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> {
var selection = typedSelection.get() + " ";
var found = fileList.getShown().getValue().stream()
.filter(browserEntry ->
browserEntry.getFileName().toLowerCase().startsWith(selection))
.findFirst();
// Ugly fix to prevent space from showing the menu when there is a file matching
// Due to the table view input map, these events always get sent and consumed, not allowing us to
// differentiate between these cases
if (found.isPresent()) {
return;
}
var selected = fileList.getSelection();
// Only show one menu across all selected entries
if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {
var cm = new BrowserContextMenu(
fileList.getFileSystemModel(), getTableRow().getItem(), false);
ContextMenuHelper.toggleShow(cm, this, Side.RIGHT);
event.consume();
}
});
}
@Override
protected void updateItem(String newName, boolean empty) {
if (updating.get()) {
super.updateItem(newName, empty);
return;
}
try (var ignored = new BooleanScope(updating).start()) {
super.updateItem(newName, empty);
if (empty || newName == null || getTableRow().getItem() == 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);
// Visibility seems to be bugged, so use opacity
setOpacity(0.0);
} else {
img.set(getTableRow().getItem().getIcon());
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
pseudoClassStateChanged(FOLDER, isDirectory);
var normalName = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.LINK
? getTableRow().getItem().getFileName() + " -> "
+ getTableRow()
.getItem()
.getRawFileEntry()
.resolved()
.getPath()
: getTableRow().getItem().getFileName();
var fileName = normalName;
var hidden =
getTableRow().getItem().getRawFileEntry().getInfo().explicitlyHidden()
|| fileName.startsWith(".");
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName);
// Visibility seems to be bugged, so use opacity
setOpacity(1.0);
}
}
}
}
}

View file

@ -1,8 +1,6 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserClipboard;
import io.xpipe.app.browser.BrowserSelectionListComp;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.core.store.FileKind;
import javafx.geometry.Point2D;
@ -219,7 +217,7 @@ public class BrowserFileListCompEntry {
return;
}
if (model.getFileSystemModel().getBrowserModel() instanceof BrowserSessionModel sessionModel) {
if (model.getFileSystemModel().getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
sessionModel.getDraggingFiles().setValue(true);
}
var selected = model.getSelection();
@ -229,7 +227,7 @@ public class BrowserFileListCompEntry {
selected,
event.isAltDown() ? BrowserFileTransferMode.MOVE : BrowserFileTransferMode.NORMAL));
Image image = BrowserSelectionListComp.snapshot(selected);
Image image = BrowserFileSelectionListComp.snapshot(selected);
db.setDragView(image, -20, 15);
event.setDragDetect(true);
@ -237,7 +235,7 @@ public class BrowserFileListCompEntry {
}
public void onDragDone(DragEvent event) {
if (model.getFileSystemModel().getBrowserModel() instanceof BrowserSessionModel sessionModel) {
if (model.getFileSystemModel().getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
sessionModel.getDraggingFiles().setValue(false);
event.consume();
}

View file

@ -1,10 +1,9 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.util.InputHelper;
import javafx.beans.property.Property;
@ -20,12 +19,12 @@ import javafx.scene.layout.HBox;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
public class BrowserFileListFilterComp extends Comp<BrowserFileListFilterComp.Structure> {
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
private final Property<String> filterString;
public BrowserFilterComp(OpenFileSystemModel model, Property<String> filterString) {
public BrowserFileListFilterComp(BrowserFileSystemTabModel model, Property<String> filterString) {
this.model = model;
this.filterString = filterString;
}
@ -38,6 +37,10 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
button.minWidthProperty().bind(button.heightProperty());
button.setFocusTraversable(true);
InputHelper.onExactKeyCode(text, KeyCode.ESCAPE, true, keyEvent -> {
if (!expanded.get()) {
return;
}
text.clear();
button.fire();
keyEvent.consume();

View file

@ -1,6 +1,5 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
@ -27,9 +26,9 @@ public final class BrowserFileListModel {
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
private final OpenFileSystemModel.SelectionMode selectionMode;
private final BrowserFileSystemTabModel.SelectionMode selectionMode;
private final OpenFileSystemModel fileSystemModel;
private final BrowserFileSystemTabModel fileSystemModel;
private final Property<Comparator<BrowserEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
private final Property<List<BrowserEntry>> all = new SimpleObjectProperty<>(new ArrayList<>());
@ -40,7 +39,8 @@ public final class BrowserFileListModel {
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();
public BrowserFileListModel(OpenFileSystemModel.SelectionMode selectionMode, OpenFileSystemModel fileSystemModel) {
public BrowserFileListModel(
BrowserFileSystemTabModel.SelectionMode selectionMode, BrowserFileSystemTabModel fileSystemModel) {
this.selectionMode = selectionMode;
this.fileSystemModel = fileSystemModel;

View file

@ -0,0 +1,202 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileKind;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableStringValue;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
class BrowserFileListNameCell extends TableCell<BrowserEntry, String> {
private final BrowserFileListModel fileList;
private final ObservableStringValue typedSelection;
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
private final BooleanProperty updating = new SimpleBooleanProperty();
public BrowserFileListNameCell(
BrowserFileListModel fileList,
ObservableStringValue typedSelection,
Property<BrowserEntry> editing,
TableView<BrowserEntry> tableView) {
this.fileList = fileList;
this.typedSelection = typedSelection;
accessibleTextProperty()
.bind(Bindings.createStringBinding(
() -> {
return getItem() != null ? getItem() : null;
},
itemProperty()));
setAccessibleRole(AccessibleRole.TEXT);
var textField = new LazyTextFieldComp(text)
.minWidth(USE_PREF_SIZE)
.createStructure()
.get();
var quickAccess = createQuickAccessButton();
setupShortcuts(tableView, (ButtonBase) quickAccess);
setupRename(fileList, textField, editing);
Node imageView = PrettyImageHelper.ofFixedSize(img, 24, 24).createRegion();
HBox graphic = new HBox(imageView, new Spacer(5), quickAccess, new Spacer(1), textField);
quickAccess.prefHeightProperty().bind(graphic.heightProperty());
graphic.setAlignment(Pos.CENTER_LEFT);
graphic.setPrefHeight(34);
HBox.setHgrow(textField, Priority.ALWAYS);
graphic.setAlignment(Pos.CENTER_LEFT);
setGraphic(graphic);
}
private Region createQuickAccessButton() {
var quickAccess = new BrowserQuickAccessButtonComp(() -> getTableRow().getItem(), fileList.getFileSystemModel())
.hide(Bindings.createBooleanBinding(
() -> {
if (getTableRow() == null) {
return true;
}
var item = getTableRow().getItem();
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
var isParentLink = item.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
return notDir || isParentLink;
},
itemProperty()))
.focusTraversable(false)
.createRegion();
return quickAccess;
}
private void setupShortcuts(TableView<BrowserEntry> tableView, ButtonBase quickAccess) {
InputHelper.onExactKeyCode(tableView, KeyCode.RIGHT, false, event -> {
var selected = fileList.getSelection();
if (selected.size() == 1 && selected.getFirst() == getTableRow().getItem()) {
quickAccess.fire();
event.consume();
}
});
InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> {
var selection = typedSelection.get() + " ";
var found = fileList.getShown().getValue().stream()
.filter(browserEntry ->
browserEntry.getFileName().toLowerCase().startsWith(selection))
.findFirst();
// Ugly fix to prevent space from showing the menu when there is a file matching
// Due to the table view input map, these events always get sent and consumed, not allowing us to
// differentiate between these cases
if (found.isPresent()) {
return;
}
var selected = fileList.getSelection();
// Only show one menu across all selected entries
if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {
var cm = new BrowserContextMenu(
fileList.getFileSystemModel(), getTableRow().getItem(), false);
ContextMenuHelper.toggleShow(cm, this, Side.RIGHT);
event.consume();
}
});
}
private void setupRename(BrowserFileListModel fileList, TextField textField, Property<BrowserEntry> editing) {
ChangeListener<String> listener = (observable, oldValue, newValue) -> {
if (updating.get()) {
return;
}
getTableRow().requestFocus();
var it = getTableRow().getItem();
editing.setValue(null);
ThreadHelper.runAsync(() -> {
if (it == null) {
return;
}
var r = fileList.rename(it, newValue);
Platform.runLater(() -> {
updateItem(getItem(), isEmpty());
fileList.getSelection().setAll(r);
getTableView().scrollTo(r);
});
});
};
text.addListener(listener);
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
PlatformThread.runLaterIfNeeded(() -> {
textField.setDisable(false);
textField.requestFocus();
});
}
});
}
@Override
protected void updateItem(String newName, boolean empty) {
if (updating.get()) {
super.updateItem(newName, empty);
return;
}
try (var ignored = new BooleanScope(updating).start()) {
super.updateItem(newName, empty);
if (empty || newName == null || getTableRow().getItem() == 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);
// Visibility seems to be bugged, so use opacity
setOpacity(0.0);
} else {
img.set(getTableRow().getItem().getIcon());
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
pseudoClassStateChanged(PseudoClass.getPseudoClass("folder"), isDirectory);
var normalName = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.LINK
? getTableRow().getItem().getFileName() + " -> "
+ getTableRow()
.getItem()
.getRawFileEntry()
.resolved()
.getPath()
: getTableRow().getItem().getFileName();
var fileName = normalName;
var hidden = getTableRow().getItem().getRawFileEntry().getInfo().explicitlyHidden()
|| fileName.startsWith(".");
getTableRow().pseudoClassStateChanged(PseudoClass.getPseudoClass("hidden"), hidden);
text.set(fileName);
// Visibility seems to be bugged, so use opacity
setOpacity(1.0);
}
}
}
}

View file

@ -1,6 +1,5 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
@ -20,7 +19,7 @@ import java.util.Objects;
public class BrowserFileOpener {
private static OutputStream openFileOutput(OpenFileSystemModel model, FileEntry file, long totalBytes)
private static OutputStream openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)
throws Exception {
var fileSystem = model.getFileSystem();
if (model.isClosed() || fileSystem.getShell().isEmpty()) {
@ -68,7 +67,7 @@ public class BrowserFileOpener {
return Objects.hash(entry.getPath(), entry.getFileSystem(), entry.getKind(), entry.getInfo());
}
public static void openWithAnyApplication(OpenFileSystemModel model, FileEntry entry) {
public static void openWithAnyApplication(BrowserFileSystemTabModel model, FileEntry entry) {
var file = entry.getPath();
var key = calculateKey(entry);
FileBridge.get()
@ -89,7 +88,7 @@ public class BrowserFileOpener {
s -> FileOpener.openWithAnyApplication(s));
}
public static void openInDefaultApplication(OpenFileSystemModel model, FileEntry entry) {
public static void openInDefaultApplication(BrowserFileSystemTabModel model, FileEntry entry) {
var file = entry.getPath();
var key = calculateKey(entry);
FileBridge.get()
@ -110,7 +109,7 @@ public class BrowserFileOpener {
s -> FileOpener.openInDefaultApplication(s));
}
public static void openInTextEditor(OpenFileSystemModel model, FileEntry entry) {
public static void openInTextEditor(BrowserFileSystemTabModel model, FileEntry entry) {
var editor = AppPrefs.get().externalEditor().getValue();
if (editor == null) {
return;

View file

@ -1,13 +1,12 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.BrowserIcons;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.VBoxViewComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.core.store.FileEntry;
import javafx.collections.ObservableList;
@ -25,7 +24,7 @@ import java.util.function.Function;
@EqualsAndHashCode(callSuper = true)
public class BrowserFileOverviewComp extends SimpleComp {
OpenFileSystemModel model;
BrowserFileSystemTabModel model;
ObservableList<FileEntry> list;
boolean grow;

View file

@ -1,14 +1,13 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
@ -31,17 +30,17 @@ import java.util.function.Function;
@Value
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class BrowserSelectionListComp extends SimpleComp {
public class BrowserFileSelectionListComp extends SimpleComp {
ObservableList<BrowserEntry> list;
Function<BrowserEntry, ObservableValue<String>> nameTransformation;
public BrowserSelectionListComp(ObservableList<BrowserEntry> list) {
public BrowserFileSelectionListComp(ObservableList<BrowserEntry> list) {
this(list, entry -> new SimpleStringProperty(entry.getFileName()));
}
public static Image snapshot(ObservableList<BrowserEntry> list) {
var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion();
var r = new BrowserFileSelectionListComp(list).styleClass("drag").createRegion();
var scene = new Scene(r);
AppWindowHelper.setupStylesheets(scene);
AppStyle.addStylesheets(scene);

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser.fs;
package io.xpipe.app.browser.file;
import io.xpipe.app.util.ShellControlCache;
import io.xpipe.core.process.CommandBuilder;
@ -12,14 +12,14 @@ import java.util.LinkedHashMap;
import java.util.Map;
@Getter
public class OpenFileSystemCache extends ShellControlCache {
public class BrowserFileSystemCache extends ShellControlCache {
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
private final String username;
private final Map<Integer, String> users = new LinkedHashMap<>();
private final Map<Integer, String> groups = new LinkedHashMap<>();
public OpenFileSystemCache(OpenFileSystemModel model) throws Exception {
public BrowserFileSystemCache(BrowserFileSystemTabModel model) throws Exception {
super(model.getFileSystem().getShell().orElseThrow());
this.model = model;

View file

@ -1,6 +1,5 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
@ -11,9 +10,9 @@ import io.xpipe.core.store.FileSystem;
import java.time.Instant;
import java.util.List;
public class FileSystemHelper {
public class BrowserFileSystemHelper {
public static String adjustPath(OpenFileSystemModel model, String path) {
public static String adjustPath(BrowserFileSystemTabModel model, String path) {
if (path == null) {
return null;
}
@ -46,7 +45,7 @@ public class FileSystemHelper {
return path;
}
public static String evaluatePath(OpenFileSystemModel model, String path) throws Exception {
public static String evaluatePath(BrowserFileSystemTabModel model, String path) throws Exception {
if (path == null) {
return null;
}
@ -67,7 +66,7 @@ public class FileSystemHelper {
}
}
public static String resolveDirectoryPath(OpenFileSystemModel model, String path, boolean allowRewrite)
public static String resolveDirectoryPath(BrowserFileSystemTabModel model, String path, boolean allowRewrite)
throws Exception {
if (path == null) {
return null;
@ -98,7 +97,7 @@ public class FileSystemHelper {
return FileNames.toDirectory(resolved);
}
public static void validateDirectoryPath(OpenFileSystemModel model, String path, boolean verifyExists)
public static void validateDirectoryPath(BrowserFileSystemTabModel model, String path, boolean verifyExists)
throws Exception {
if (path == null) {
return;

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser.fs;
package io.xpipe.app.browser.file;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
@ -9,7 +9,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class OpenFileSystemHistory {
public final class BrowserFileSystemHistory {
private final IntegerProperty cursor = new SimpleIntegerProperty(-1);
private final List<String> history = new ArrayList<>();

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser.fs;
package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppCache;
import io.xpipe.core.store.FileNames;
@ -31,35 +31,36 @@ import java.util.stream.Collectors;
@AllArgsConstructor
@Getter
@JsonSerialize(using = OpenFileSystemSavedState.Serializer.class)
@JsonDeserialize(using = OpenFileSystemSavedState.Deserializer.class)
public class OpenFileSystemSavedState {
@JsonSerialize(using = BrowserFileSystemSavedState.Serializer.class)
@JsonDeserialize(using = BrowserFileSystemSavedState.Deserializer.class)
public class BrowserFileSystemSavedState {
private static final Timer TIMEOUT_TIMER = new Timer(true);
private static final int STORED = 10;
private static final int STORED = 15;
@Setter
private OpenFileSystemModel model;
private BrowserFileSystemTabModel model;
private String lastDirectory;
@NonNull
private ObservableList<RecentEntry> recentDirectories;
public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
public BrowserFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
this.lastDirectory = lastDirectory;
this.recentDirectories = recentDirectories;
}
public OpenFileSystemSavedState() {
public BrowserFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
}
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState();
});
static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) {
var state = AppCache.getNonNull(
"fs-state-" + model.getEntry().get().getUuid(), BrowserFileSystemSavedState.class, () -> {
return new BrowserFileSystemSavedState();
});
state.setModel(model);
return state;
}
@ -121,14 +122,14 @@ public class OpenFileSystemSavedState {
}
}
public static class Serializer extends StdSerializer<OpenFileSystemSavedState> {
public static class Serializer extends StdSerializer<BrowserFileSystemSavedState> {
protected Serializer() {
super(OpenFileSystemSavedState.class);
super(BrowserFileSystemSavedState.class);
}
@Override
public void serialize(OpenFileSystemSavedState value, JsonGenerator gen, SerializerProvider provider)
public void serialize(BrowserFileSystemSavedState value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
var node = JsonNodeFactory.instance.objectNode();
node.set("recentDirectories", JacksonMapper.getDefault().valueToTree(value.getRecentDirectories()));
@ -136,10 +137,10 @@ public class OpenFileSystemSavedState {
}
}
public static class Deserializer extends StdDeserializer<OpenFileSystemSavedState> {
public static class Deserializer extends StdDeserializer<BrowserFileSystemSavedState> {
protected Deserializer() {
super(OpenFileSystemSavedState.class);
super(BrowserFileSystemSavedState.class);
}
private static <T> Predicate<T> distinctBy(Function<? super T, ?> f) {
@ -149,7 +150,7 @@ public class OpenFileSystemSavedState {
@Override
@SneakyThrows
public OpenFileSystemSavedState deserialize(JsonParser p, DeserializationContext ctxt) {
public BrowserFileSystemSavedState deserialize(JsonParser p, DeserializationContext ctxt) {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
JavaType javaType = JacksonMapper.getDefault()
.getTypeFactory()
@ -163,7 +164,7 @@ public class OpenFileSystemSavedState {
.map(recentEntry -> new RecentEntry(FileNames.toDirectory(recentEntry.directory), recentEntry.time))
.filter(distinctBy(recentEntry -> recentEntry.getDirectory()))
.collect(Collectors.toCollection(ArrayList::new));
return new OpenFileSystemSavedState(null, FXCollections.observableList(cleaned));
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
}
}

View file

@ -1,21 +1,15 @@
package io.xpipe.app.browser.fs;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFilterComp;
import io.xpipe.app.browser.BrowserNavBar;
import io.xpipe.app.browser.BrowserOverviewComp;
import io.xpipe.app.browser.BrowserStatusBarComp;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.util.InputHelper;
import javafx.geometry.Pos;
@ -37,12 +31,12 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class OpenFileSystemComp extends SimpleComp {
public class BrowserFileSystemTabComp extends SimpleComp {
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
private final boolean showStatusBar;
public OpenFileSystemComp(OpenFileSystemModel model, boolean showStatusBar) {
public BrowserFileSystemTabComp(BrowserFileSystemTabModel model, boolean showStatusBar) {
this.model = model;
this.showStatusBar = showStatusBar;
}
@ -82,12 +76,12 @@ public class OpenFileSystemComp extends SimpleComp {
menuButton.disableProperty().bind(model.getInOverview());
menuButton.setAccessibleText("Directory options");
var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
var filter = new BrowserFileListFilterComp(model, model.getFilter()).createStructure();
var topBar = new HBox();
topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top-bar");
var navBar = new BrowserNavBar(model).createStructure();
var navBar = new BrowserNavBarComp(model).createStructure();
filter.textField().prefHeightProperty().bind(navBar.get().heightProperty());
AppFont.medium(navBar.get());
topBar.getChildren()
@ -182,7 +176,7 @@ public class OpenFileSystemComp extends SimpleComp {
});
});
var home = new BrowserOverviewComp(model).styleClass("browser-content");
var home = new BrowserOverviewComp(model).styleClass("browser-overview");
var stack = new MultiContentComp(Map.of(
home,
model.getCurrentPath().isNull(),

View file

@ -1,34 +1,30 @@
package io.xpipe.app.browser.fs;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.BrowserAbstractSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserStoreSessionTab;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserFileListModel;
import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionTab;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.*;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ShellOpenFunction;
import io.xpipe.core.process.*;
import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.SneakyThrows;
@ -38,23 +34,25 @@ import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
@Getter
public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore> {
public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<FileSystemStore> {
private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final OpenFileSystemHistory history = new OpenFileSystemHistory();
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();
private FileSystem fileSystem;
private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache;
private BrowserFileSystemSavedState savedState;
private BrowserFileSystemCache cache;
public OpenFileSystemModel(
public BrowserFileSystemTabModel(
BrowserAbstractSessionModel<?> model,
DataStoreEntryRef<? extends FileSystemStore> entry,
SelectionMode selectionMode) {
@ -69,7 +67,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
@Override
public Comp<?> comp() {
return new OpenFileSystemComp(this, true);
return new BrowserFileSystemTabComp(this, true);
}
@Override
@ -99,12 +97,12 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
this.fileSystem = fs;
this.cache = new OpenFileSystemCache(this);
this.cache = new BrowserFileSystemCache(this);
for (BrowserAction b : BrowserAction.ALL) {
b.init(this);
}
});
this.savedState = OpenFileSystemSavedState.loadForStore(this);
this.savedState = BrowserFileSystemSavedState.loadForStore(this);
}
@Override
@ -119,8 +117,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
&& savedState != null
&& current != null) {
savedState.cd(current.getPath(), false);
BrowserSavedStateImpl.get()
.add(new BrowserSavedState.Entry(getEntry().get().getUuid(), current.getPath()));
BrowserHistorySavedStateImpl.get()
.add(new BrowserHistorySavedState.Entry(getEntry().get().getUuid(), current.getPath()));
}
try {
fileSystem.close();
@ -208,6 +206,37 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
cdSyncOrRetry(path, false).ifPresent(s -> cdSyncOrRetry(s, false));
}
private boolean shouldLaunchSplitTerminal() {
if (!AppPrefs.get().enableTerminalDocking().get()) {
return false;
}
if (OsType.getLocal() != OsType.WINDOWS) {
return false;
}
if (AppMainWindow.getInstance().getStage().getWidth() <= 1280) {
return false;
}
var term = AppPrefs.get().terminalType().getValue();
if (term == null || term.getOpenFormat() == TerminalOpenFormat.TABBED) {
return false;
}
if (!(browserModel instanceof BrowserFullSessionModel f)) {
return false;
}
// Check if the right side is already occupied
var existingSplit = f.getEffectiveRightTab().getValue();
if (existingSplit != null && !(existingSplit instanceof BrowserTerminalDockTabModel)) {
return false;
}
return true;
}
public Optional<String> cdSyncOrRetry(String path, boolean customInput) {
if (Objects.equals(path, currentPath.get())) {
return Optional.empty();
@ -226,7 +255,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
// Fix common issues with paths
var adjustedPath = FileSystemHelper.adjustPath(this, path);
var adjustedPath = BrowserFileSystemHelper.adjustPath(this, path);
if (!Objects.equals(path, adjustedPath)) {
return Optional.of(adjustedPath);
}
@ -234,7 +263,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
// Evaluate optional expressions
String evaluatedPath;
try {
evaluatedPath = FileSystemHelper.evaluatePath(this, adjustedPath);
evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
@ -252,21 +281,14 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath
.toLowerCase()
.startsWith(dialect.getExecutableName().toLowerCase()))) {
TerminalLauncher.open(
entry.getEntry(),
name,
directory,
fileSystem
.getShell()
.get()
.singularSubShell(
ShellOpenFunction.of(CommandBuilder.ofString(adjustedPath), false)));
var cc = fileSystem
.getShell()
.get()
.singularSubShell(ShellOpenFunction.of(CommandBuilder.ofString(adjustedPath), false));
openTerminalAsync(name, directory, cc, true);
} else {
TerminalLauncher.open(
entry.getEntry(),
name,
directory,
fileSystem.getShell().get().command(adjustedPath));
var cc = fileSystem.getShell().get().command(adjustedPath);
openTerminalAsync(name, directory, cc, true);
}
});
return Optional.ofNullable(currentPath.get());
@ -275,7 +297,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
// Evaluate optional links
String resolvedPath;
try {
resolvedPath = FileSystemHelper.resolveDirectoryPath(this, evaluatedPath, customInput);
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, evaluatedPath, customInput);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
@ -286,7 +308,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
try {
FileSystemHelper.validateDirectoryPath(this, resolvedPath, customInput);
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, customInput);
cdSyncWithoutCheck(path);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
@ -521,7 +543,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
history.updateCurrent(null);
}
public void openTerminalAsync(String directory) {
public void openTerminalAsync(
String name, String directory, ProcessControl processControl, boolean dockIfPossible) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
return;
@ -529,10 +552,16 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem.getShell().isPresent()) {
var connection = fileSystem.getShell().get();
var name = (directory != null ? directory + " - " : "")
+ entry.get().getName();
TerminalLauncher.open(entry.getEntry(), name, directory, connection);
var dock = shouldLaunchSplitTerminal() && dockIfPossible;
var uuid = UUID.randomUUID();
terminalRequests.add(uuid);
if (dock
&& browserModel instanceof BrowserFullSessionModel fullSessionModel
&& !(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) {
fullSessionModel.splitTab(
this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests));
}
TerminalLauncher.open(entry.getEntry(), name, directory, processControl, uuid, !dock);
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
startIfNeeded();

View file

@ -1,6 +1,5 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.*;
@ -50,7 +49,7 @@ public class BrowserFileTransferOperation {
}
try {
return LocalFileSystem.getLocalFileEntry(path);
return BrowserLocalFileSystem.getLocalFileEntry(path);
} catch (Exception e) {
throw new RuntimeException(e);
}

View file

@ -1,10 +1,10 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.scene.control.Label;

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import javafx.collections.ObservableList;
@ -9,7 +9,7 @@ import lombok.extern.jackson.Jacksonized;
import java.util.UUID;
public interface BrowserSavedState {
public interface BrowserHistorySavedState {
void add(Entry entry);

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppCache;
import io.xpipe.core.util.JacksonMapper;
@ -19,36 +19,36 @@ import lombok.Value;
import java.util.List;
@Value
@JsonDeserialize(using = BrowserSavedStateImpl.Deserializer.class)
public class BrowserSavedStateImpl implements BrowserSavedState {
@JsonDeserialize(using = BrowserHistorySavedStateImpl.Deserializer.class)
public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
@JsonSerialize(as = List.class)
ObservableList<Entry> lastSystems;
public BrowserSavedStateImpl(List<Entry> lastSystems) {
public BrowserHistorySavedStateImpl(List<Entry> lastSystems) {
this.lastSystems = FXCollections.observableArrayList(lastSystems);
}
private static BrowserSavedStateImpl INSTANCE;
private static BrowserHistorySavedStateImpl INSTANCE;
public static BrowserSavedState get() {
public static BrowserHistorySavedState get() {
if (INSTANCE == null) {
INSTANCE = load();
}
return INSTANCE;
}
private static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList());
private static BrowserHistorySavedStateImpl load() {
return AppCache.getNonNull("browser-state", BrowserHistorySavedStateImpl.class, () -> {
return new BrowserHistorySavedStateImpl(FXCollections.observableArrayList());
});
}
@Override
public synchronized void add(BrowserSavedState.Entry entry) {
public synchronized void add(BrowserHistorySavedState.Entry entry) {
lastSystems.removeIf(s -> s.getUuid().equals(entry.getUuid()));
lastSystems.addFirst(entry);
if (lastSystems.size() > 10) {
if (lastSystems.size() > 15) {
lastSystems.removeLast();
}
}
@ -63,15 +63,15 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
return lastSystems;
}
public static class Deserializer extends StdDeserializer<BrowserSavedStateImpl> {
public static class Deserializer extends StdDeserializer<BrowserHistorySavedStateImpl> {
protected Deserializer() {
super(BrowserSavedStateImpl.class);
super(BrowserHistorySavedStateImpl.class);
}
@Override
@SneakyThrows
public BrowserSavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) {
public BrowserHistorySavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
JavaType javaType =
JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, Entry.class);
@ -79,7 +79,7 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
if (ls == null) {
ls = List.of();
}
return new BrowserSavedStateImpl(ls);
return new BrowserHistorySavedStateImpl(ls);
}
}
}

View file

@ -1,20 +1,20 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.PrettySvgComp;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.PrettySvgComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@ -35,17 +35,17 @@ import atlantafx.base.theme.Styles;
import java.util.List;
public class BrowserWelcomeComp extends SimpleComp {
public class BrowserHistoryTabComp extends SimpleComp {
private final BrowserSessionModel model;
private final BrowserFullSessionModel model;
public BrowserWelcomeComp(BrowserSessionModel model) {
public BrowserHistoryTabComp(BrowserFullSessionModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var state = BrowserSavedStateImpl.get();
var state = BrowserHistorySavedStateImpl.get();
var welcome = new BrowserGreetingComp().createSimple();
@ -124,7 +124,7 @@ public class BrowserWelcomeComp extends SimpleComp {
var layout = new VBox();
layout.getStyleClass().add("welcome");
layout.setPadding(new Insets(60, 40, 40, 50));
layout.setPadding(new Insets(25, 40, 40, 40));
layout.setSpacing(18);
layout.getChildren().add(hbox);
layout.getChildren().add(Comp.separator().hide(empty).createRegion());
@ -140,13 +140,14 @@ public class BrowserWelcomeComp extends SimpleComp {
.hide(empty)
.accessibleTextKey("restoreAllSessions");
layout.getChildren().add(tile.createRegion());
AppFont.medium(layout);
return layout;
}
private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) {
private Comp<?> entryButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get().getEffectiveIconFile();
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
var view = PrettyImageHelper.ofFixedSize(graphic, 22, 16);
return new ButtonComp(
new SimpleStringProperty(DataStorage.get().getStoreEntryDisplayName(entry.get())),
view.createRegion(),
@ -158,7 +159,7 @@ public class BrowserWelcomeComp extends SimpleComp {
}
});
})
.minWidth(250)
.minWidth(300)
.accessibleText(DataStorage.get().getStoreEntryDisplayName(entry.get()))
.disable(disable)
.styleClass("entry-button")
@ -166,7 +167,7 @@ public class BrowserWelcomeComp extends SimpleComp {
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
}
private Comp<?> dirButton(BrowserSavedState.Entry e, BooleanProperty disable) {
private Comp<?> dirButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {
return new ButtonComp(new SimpleStringProperty(e.getPath()), null, () -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
@ -177,7 +178,7 @@ public class BrowserWelcomeComp extends SimpleComp {
.styleClass("directory-button")
.apply(struc -> struc.get().setMaxWidth(2000))
.styleClass(Styles.RIGHT_PILL)
.grow(true, false)
.hgrow()
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
}
}

View file

@ -0,0 +1,46 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserAbstractSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserSessionTab;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.storage.DataColor;
public final class BrowserHistoryTabModel extends BrowserSessionTab {
public BrowserHistoryTabModel(BrowserAbstractSessionModel<?> browserModel) {
super(browserModel, " " + AppI18n.get("history") + " ");
}
@Override
public Comp<?> comp() {
return new BrowserHistoryTabComp((BrowserFullSessionModel) browserModel);
}
@Override
public boolean canImmediatelyClose() {
return true;
}
@Override
public void init() throws Exception {}
@Override
public void close() {}
@Override
public String getIcon() {
return null;
}
@Override
public DataColor getColor() {
return null;
}
@Override
public boolean isCloseable() {
return false;
}
}

View file

@ -8,7 +8,7 @@ import io.xpipe.core.store.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
public class LocalFileSystem {
public class BrowserLocalFileSystem {
private static FileSystem localFileSystem;

View file

@ -1,15 +1,13 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.browser.icon.BrowserIconManager;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
@ -33,58 +31,16 @@ import javafx.scene.shape.Rectangle;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
@Override
public Structure createBase() {
var path = new SimpleStringProperty(model.getCurrentPath().get());
model.getCurrentPath().subscribe((newValue) -> {
path.set(newValue);
});
path.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
var changed = model.cdSyncOrRetry(newValue, true);
changed.ifPresent(s -> Platform.runLater(() -> path.set(s)));
});
});
});
var pathBar = new TextFieldComp(path, true)
.styleClass(Styles.CENTER_PILL)
.styleClass("path-text")
.apply(struc -> {
struc.get().focusedProperty().subscribe(val -> {
struc.get()
.pseudoClassStateChanged(
INVISIBLE,
!val && !model.getInOverview().get());
if (val) {
Platform.runLater(() -> {
struc.get().end();
});
}
});
model.getInOverview().subscribe(val -> {
// Pseudo classes do not apply if set instantly before shown
// If we start a new tab with a directory set, we have to set the pseudo class one pulse later
Platform.runLater(() -> {
struc.get()
.pseudoClassStateChanged(
INVISIBLE, !val && !struc.get().isFocused());
});
});
struc.get().setPromptText("Overview of " + model.getName());
})
.accessibleText("Current path");
var pathBar = createPathBar();
var graphic = Bindings.createStringBinding(
() -> {
return model.getCurrentDirectory() != null
? FileIconManager.getFileIcon(model.getCurrentDirectory())
? BrowserIconManager.getFileIcon(model.getCurrentDirectory())
: null;
},
model.getCurrentPath());
@ -154,6 +110,51 @@ public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
return new Structure(topBox, pathRegion, historyButton);
}
private Comp<CompStructure<TextField>> createPathBar() {
var path = new SimpleStringProperty(model.getCurrentPath().get());
model.getCurrentPath().subscribe((newValue) -> {
path.set(newValue);
});
path.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
var changed = model.cdSyncOrRetry(newValue, true);
changed.ifPresent(s -> Platform.runLater(() -> path.set(s)));
});
});
});
var pathBar =
new TextFieldComp(path, true).styleClass(Styles.CENTER_PILL).styleClass("path-text");
pathBar.apply(struc -> {
struc.get().focusedProperty().subscribe(val -> {
struc.get()
.pseudoClassStateChanged(
INVISIBLE,
!val && !model.getInOverview().get());
if (val) {
Platform.runLater(() -> {
struc.get().end();
});
}
});
model.getInOverview().subscribe(val -> {
// Pseudo classes do not apply if set instantly before shown
// If we start a new tab with a directory set, we have to set the pseudo class one pulse later
Platform.runLater(() -> {
struc.get()
.pseudoClassStateChanged(
INVISIBLE, !val && !struc.get().isFocused());
});
});
struc.get().setPromptText("Overview of " + model.getName());
})
.accessibleText("Current path");
return pathBar;
}
public record Structure(HBox box, TextField textField, Button historyButton) implements CompStructure<HBox> {
@Override
@ -164,9 +165,9 @@ public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
private static final PseudoClass INVISIBLE = PseudoClass.getPseudoClass("invisible");
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
public BrowserNavBar(OpenFileSystemModel model) {
public BrowserNavBarComp(BrowserFileSystemTabModel model) {
this.model = model;
}

View file

@ -1,13 +1,12 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.file.BrowserFileOverviewComp;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileEntry;
@ -24,9 +23,9 @@ import java.util.List;
public class BrowserOverviewComp extends SimpleComp {
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
public BrowserOverviewComp(OpenFileSystemModel model) {
public BrowserOverviewComp(BrowserFileSystemTabModel model) {
this.model = model;
}
@ -77,6 +76,8 @@ public class BrowserOverviewComp extends SimpleComp {
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview);
var vbox = new VerticalComp(List.of(recentPane, commonPane, rootsPane)).styleClass("overview");
return vbox.createRegion();
var r = vbox.createRegion();
AppFont.medium(r);
return r;
}
}

View file

@ -1,8 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.util.InputHelper;
import javafx.scene.layout.Region;
@ -12,9 +11,9 @@ import java.util.function.Supplier;
public class BrowserQuickAccessButtonComp extends SimpleComp {
private final Supplier<BrowserEntry> base;
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
public BrowserQuickAccessButtonComp(Supplier<BrowserEntry> base, OpenFileSystemModel model) {
public BrowserQuickAccessButtonComp(Supplier<BrowserEntry> base, BrowserFileSystemTabModel model) {
this.base = base;
this.model = model;
}

View file

@ -1,8 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.browser.icon.BrowserIconManager;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.util.BooleanAnimationTimer;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.ThreadHelper;
@ -32,13 +31,13 @@ import java.util.stream.Collectors;
public class BrowserQuickAccessContextMenu extends ContextMenu {
private final Supplier<BrowserEntry> base;
private final OpenFileSystemModel model;
private final BrowserFileSystemTabModel model;
private ContextMenu shownBrowserActionsMenu;
private boolean expandBrowserActionMenuKey;
private boolean keyBasedNavigation;
private boolean closeBrowserActionMenuKey;
public BrowserQuickAccessContextMenu(Supplier<BrowserEntry> base, OpenFileSystemModel model) {
public BrowserQuickAccessContextMenu(Supplier<BrowserEntry> base, BrowserFileSystemTabModel model) {
this.base = base;
this.model = model;
@ -142,7 +141,8 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
this.menu = new Menu(
// Use original name, not the link target
browserEntry.getRawFileEntry().getName(),
PrettyImageHelper.ofFixedSize(FileIconManager.getFileIcon(browserEntry.getRawFileEntry()), 24, 24)
PrettyImageHelper.ofFixedSize(
BrowserIconManager.getFileIcon(browserEntry.getRawFileEntry()), 24, 24)
.createRegion());
createMenu();
addInputListeners();

View file

@ -1,16 +1,13 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListCompEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat;
import javafx.beans.binding.Bindings;
@ -29,7 +26,7 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public class BrowserStatusBarComp extends SimpleComp {
OpenFileSystemModel model;
BrowserFileSystemTabModel model;
@Override
protected Region createSimple() {

View file

@ -0,0 +1,164 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserAbstractSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserSessionTab;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.terminal.TerminalDockComp;
import io.xpipe.app.terminal.TerminalDockModel;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.terminal.WindowsTerminalType;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.ObservableList;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
private final BrowserSessionTab origin;
private final ObservableList<UUID> terminalRequests;
private final TerminalDockModel dockModel = new TerminalDockModel();
private TerminalView.Listener listener;
private ObservableBooleanValue viewActive;
public BrowserTerminalDockTabModel(
BrowserAbstractSessionModel<?> browserModel,
BrowserSessionTab origin,
ObservableList<UUID> terminalRequests) {
super(browserModel, AppI18n.get("terminal"));
this.origin = origin;
this.terminalRequests = terminalRequests;
}
@Override
public Comp<?> comp() {
return new TerminalDockComp(dockModel);
}
@Override
public boolean canImmediatelyClose() {
return true;
}
@Override
public void init() throws Exception {
var hasOpened = new AtomicBoolean();
listener = new TerminalView.Listener() {
@Override
public void onSessionOpened(TerminalView.ShellSession session) {
if (!terminalRequests.contains(session.getRequest())) {
return;
}
hasOpened.set(true);
var sessions = TerminalView.get().getSessions();
var tv = sessions.stream()
.filter(s -> terminalRequests.contains(s.getRequest())
&& s.getTerminal().isRunning())
.map(s -> s.getTerminal().controllable())
.flatMap(Optional::stream)
.toList();
for (int i = 0; i < tv.size() - 1; i++) {
dockModel.closeTerminal(tv.get(i));
}
// Closing and opening windows at the same time might be problematic for some bad implementations
if (tv.size() > 1) {
ThreadHelper.sleep(250);
}
var toTrack = tv.getLast();
dockModel.trackTerminal(toTrack);
}
@Override
public void onSessionClosed(TerminalView.ShellSession session) {
if (!terminalRequests.contains(session.getRequest())) {
return;
}
// Ugly fix for Windows Terminal instances not closing properly if multiple windows exist
if (AppPrefs.get().terminalType().getValue() instanceof WindowsTerminalType) {
var sessions = TerminalView.get().getSessions();
var others = sessions.stream().filter(shellSession -> shellSession.getTerminal().equals(session.getTerminal())).count();
if (others == 0) {
session.getTerminal().controllable().ifPresent(controllableTerminalSession -> {
controllableTerminalSession.close();
});
}
}
}
@Override
public void onTerminalClosed(TerminalView.TerminalSession instance) {
refreshShowingState();
}
};
TerminalView.get().addListener(listener);
// If the terminal launch fails
ThreadHelper.runAsync(() -> {
ThreadHelper.sleep(5000);
if (!hasOpened.get()) {
refreshShowingState();
}
});
viewActive = Bindings.createBooleanBinding(
() -> {
return this.browserModel.getSelectedEntry().getValue() == origin
&& AppLayoutModel.get()
.getEntries()
.indexOf(AppLayoutModel.get()
.getSelected()
.getValue())
== 1;
},
this.browserModel.getSelectedEntry(),
AppLayoutModel.get().getSelected());
viewActive.subscribe(aBoolean -> {
Platform.runLater(() -> {
dockModel.toggleView(aBoolean);
});
});
}
private void refreshShowingState() {
var sessions = TerminalView.get().getSessions();
var remaining = sessions.stream()
.filter(s -> terminalRequests.contains(s.getRequest())
&& s.getTerminal().isRunning())
.toList();
if (remaining.isEmpty()) {
((BrowserFullSessionModel) browserModel).unsplitTab(BrowserTerminalDockTabModel.this);
}
}
@Override
public void close() {
if (listener != null) {
TerminalView.get().removeListener(listener);
}
dockModel.onClose();
}
@Override
public String getIcon() {
return null;
}
@Override
public DataColor getColor() {
return null;
}
}

View file

@ -1,13 +1,11 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@ -17,6 +15,7 @@ import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
@ -43,13 +42,16 @@ public class BrowserTransferComp extends SimpleComp {
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.apply(struc -> struc.get().setWrapText(true))
.visible(model.getEmpty());
var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var backgroundStack = new StackComp(List.of(background))
.grow(true, true)
.styleClass("color-box")
.styleClass("gray")
.styleClass("download-background");
var binding = new DerivedObservableList<>(model.getItems(), true)
.mapped(item -> item.getBrowserEntry())
.getList();
var list = new BrowserSelectionListComp(binding, entry -> {
var list = new BrowserFileSelectionListComp(binding, entry -> {
var sourceItem = model.getCurrentItems().stream()
.filter(item -> item.getBrowserEntry() == entry)
.findAny();
@ -102,84 +104,84 @@ public class BrowserTransferComp extends SimpleComp {
.padding(new Insets(10, 10, 5, 10))
.apply(struc -> struc.get().setMinHeight(200))
.apply(struc -> struc.get().setMaxHeight(200));
var stack = new StackComp(List.of(backgroundStack, listBox))
.apply(DragOverPseudoClassAugment.create())
.apply(struc -> {
struc.get().setOnDragOver(event -> {
// Accept drops from inside the app window
if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
});
struc.get().setOnDragDropped(event -> {
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
if (drag == null) {
return;
var stack = new StackComp(List.of(backgroundStack, listBox)).apply(struc -> {
struc.get().addEventFilter(DragEvent.DRAG_ENTERED, event -> {
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("drag-over"), true);
});
struc.get().addEventFilter(DragEvent.DRAG_EXITED, event -> struc.get()
.pseudoClassStateChanged(PseudoClass.getPseudoClass("drag-over"), false));
struc.get().setOnDragOver(event -> {
// Accept drops from inside the app window
if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
});
struc.get().setOnDragDropped(event -> {
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
if (drag == null) {
return;
}
if (!(model.getBrowserSessionModel().getSelectedEntry().getValue()
instanceof BrowserFileSystemTabModel fileSystemModel)) {
return;
}
var files = drag.getEntries();
model.drop(fileSystemModel, files);
event.setDropCompleted(true);
event.consume();
}
});
struc.get().setOnDragDetected(event -> {
var items = model.getCurrentItems();
var selected =
items.stream().map(item -> item.getBrowserEntry()).toList();
var files = items.stream()
.filter(item -> item.downloadFinished().get())
.map(item -> {
try {
var file = item.getLocalFile();
if (!Files.exists(file)) {
return Optional.<File>empty();
}
return Optional.of(file.toRealPath().toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.flatMap(Optional::stream)
.toList();
if (files.isEmpty()) {
return;
}
if (!(model.getBrowserSessionModel()
.getSelectedEntry()
.getValue()
instanceof OpenFileSystemModel fileSystemModel)) {
return;
}
var cc = new ClipboardContent();
cc.putFiles(files);
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
db.setContent(cc);
var files = drag.getEntries();
model.drop(fileSystemModel, files);
event.setDropCompleted(true);
event.consume();
}
});
struc.get().setOnDragDetected(event -> {
var items = model.getCurrentItems();
var selected = items.stream()
.map(item -> item.getBrowserEntry())
.toList();
var files = items.stream()
.filter(item -> item.downloadFinished().get())
.map(item -> {
try {
var file = item.getLocalFile();
if (!Files.exists(file)) {
return Optional.<File>empty();
}
Image image = BrowserFileSelectionListComp.snapshot(FXCollections.observableList(selected));
db.setDragView(image, -20, 15);
return Optional.of(file.toRealPath().toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.flatMap(Optional::stream)
.toList();
if (files.isEmpty()) {
return;
}
event.setDragDetect(true);
event.consume();
});
struc.get().setOnDragDone(event -> {
if (!event.isAccepted()) {
return;
}
var cc = new ClipboardContent();
cc.putFiles(files);
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
db.setContent(cc);
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
db.setDragView(image, -20, 15);
event.setDragDetect(true);
event.consume();
});
struc.get().setOnDragDone(event -> {
if (!event.isAccepted()) {
return;
}
// The files might not have been transferred yet
// We can't listen to this, so just don't delete them
model.clear(false);
event.consume();
});
});
// The files might not have been transferred yet
// We can't listen to this, so just don't delete them
model.clear(false);
event.consume();
});
});
stack.apply(struc -> {
model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> {

View file

@ -1,12 +1,8 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.util.ThreadHelper;
@ -23,6 +19,7 @@ import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
@ -34,11 +31,11 @@ public class BrowserTransferModel {
private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download");
BrowserSessionModel browserSessionModel;
BrowserFullSessionModel browserSessionModel;
ObservableList<Item> items = FXCollections.observableArrayList();
ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);
public BrowserTransferModel(BrowserSessionModel browserSessionModel) {
public BrowserTransferModel(BrowserFullSessionModel browserSessionModel) {
this.browserSessionModel = browserSessionModel;
var thread = ThreadHelper.createPlatformThread("file downloader", true, () -> {
while (true) {
@ -96,7 +93,7 @@ public class BrowserTransferModel {
}
}
public void drop(OpenFileSystemModel model, List<BrowserEntry> entries) {
public void drop(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
synchronized (items) {
entries.forEach(entry -> {
var name = entry.getFileName();
@ -130,7 +127,7 @@ public class BrowserTransferModel {
try {
var op = new BrowserFileTransferOperation(
LocalFileSystem.getLocalFileEntry(TEMP),
BrowserLocalFileSystem.getLocalFileEntry(TEMP),
List.of(item.getBrowserEntry().getRawFileEntry()),
BrowserFileTransferMode.COPY,
false,
@ -167,7 +164,7 @@ public class BrowserTransferModel {
}
var files = toMove.stream().map(item -> item.getLocalFile()).toList();
var downloads = DesktopHelper.getDownloadsDirectory();
var downloads = getDownloadsTargetDirectory();
Files.createDirectories(downloads);
for (Path file : files) {
if (!Files.exists(file)) {
@ -188,15 +185,33 @@ public class BrowserTransferModel {
DesktopHelper.browseFileInDirectory(downloads.resolve(files.getFirst().getFileName()));
}
private Path getDownloadsTargetDirectory() throws Exception {
var def = DesktopHelper.getDownloadsDirectory();
var custom = AppPrefs.get().downloadsDirectory().getValue();
if (custom == null || custom.isBlank()) {
return def;
}
try {
var path = Path.of(custom);
if (Files.isDirectory(path)) {
return path;
}
} catch (InvalidPathException ignored) {
}
return def;
}
@Value
public static class Item {
OpenFileSystemModel openFileSystemModel;
BrowserFileSystemTabModel openFileSystemModel;
String name;
BrowserEntry browserEntry;
Path localFile;
Property<BrowserTransferProgress> progress;
public Item(OpenFileSystemModel openFileSystemModel, String name, BrowserEntry browserEntry, Path localFile) {
public Item(
BrowserFileSystemTabModel openFileSystemModel, String name, BrowserEntry browserEntry, Path localFile) {
this.openFileSystemModel = openFileSystemModel;
this.name = name;
this.browserEntry = browserEntry;

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser;
package io.xpipe.app.browser.file;
import lombok.Value;

View file

@ -63,7 +63,7 @@ public abstract class BrowserIconDirectoryType {
var closedIcon = "browser/" + split[2].trim();
var lightClosedIcon = split.length > 4 ? "browser/" + split[4].trim() : closedIcon;
ALL.add(new Simple(id, new IconVariant(lightClosedIcon, closedIcon), filter));
ALL.add(new Simple(id, new BrowserIconVariant(lightClosedIcon, closedIcon), filter));
}
}
});
@ -84,10 +84,10 @@ public abstract class BrowserIconDirectoryType {
@Getter
private final String id;
private final IconVariant closed;
private final BrowserIconVariant closed;
private final Set<String> names;
public Simple(String id, IconVariant closed, Set<String> names) {
public Simple(String id, BrowserIconVariant closed, Set<String> names) {
this.id = id;
this.closed = closed;
this.names = names;

View file

@ -69,11 +69,11 @@ public abstract class BrowserIconFileType {
public static class Simple extends BrowserIconFileType {
private final String id;
private final IconVariant icon;
private final BrowserIconVariant icon;
private final Set<String> endings;
public Simple(String id, String lightIcon, String darkIcon, Set<String> endings) {
this.icon = new IconVariant(lightIcon, darkIcon);
this.icon = new BrowserIconVariant(lightIcon, darkIcon);
this.id = id;
this.endings = endings;
}

View file

@ -3,7 +3,7 @@ package io.xpipe.app.browser.icon;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
public class FileIconManager {
public class BrowserIconManager {
private static boolean loaded;

View file

@ -2,16 +2,16 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.prefs.AppPrefs;
public class IconVariant {
public class BrowserIconVariant {
private final String lightIcon;
private final String darkIcon;
public IconVariant(String icon) {
public BrowserIconVariant(String icon) {
this(icon, icon);
}
public IconVariant(String lightIcon, String darkIcon) {
public BrowserIconVariant(String lightIcon, String darkIcon) {
this.lightIcon = lightIcon;
this.darkIcon = darkIcon;
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.core.store.FileEntry;
public class BrowserIcons {
@ -19,6 +19,6 @@ public class BrowserIcons {
}
public static Comp<?> createIcon(FileEntry entry) {
return PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry), 24);
return PrettyImageHelper.ofFixedSizeSquare(BrowserIconManager.getFileIcon(entry), 24);
}
}

View file

@ -1,106 +0,0 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferModel;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import lombok.Getter;
import java.util.ArrayList;
@Getter
public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab<?>> {
public static final BrowserSessionModel DEFAULT = new BrowserSessionModel();
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
public void restoreState(BrowserSavedState state) {
ThreadHelper.runAsync(() -> {
var l = new ArrayList<>(state.getEntries());
l.forEach(e -> {
restoreStateAsync(e, null);
// Don't try to run everything in parallel as that can be taxing
ThreadHelper.sleep(1000);
});
});
}
public void restoreStateAsync(BrowserSavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(entry.ref(), model -> e.getPath(), busy);
});
}
public void reset() {
synchronized (BrowserSessionModel.this) {
for (var o : new ArrayList<>(sessionEntries)) {
// Don't close busy connections gracefully
// as we otherwise might lock up
if (!o.canImmediatelyClose()) {
continue;
}
// Prevent blocking of shutdown
closeAsync(o);
}
BrowserSavedStateImpl.get().save();
}
// Delete all files
localTransfersStage.clear(true);
}
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
openFileSystemSync(store, path, externalBusy);
});
}
public OpenFileSystemModel openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy)
throws Exception {
OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserSessionModel.this) {
sessionEntries.add(model);
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
}
}
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {
model.initWithDefaultDirectory();
}
return model;
}
}

View file

@ -1,36 +0,0 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import lombok.Getter;
@Getter
public abstract class BrowserSessionTab<T extends DataStore> {
protected final DataStoreEntryRef<? extends T> entry;
protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final BrowserAbstractSessionModel<?> browserModel;
protected final String name;
protected final String tooltip;
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
this.browserModel = browserModel;
this.entry = entry;
this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());
this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString();
}
public abstract Comp<?> comp();
public abstract boolean canImmediatelyClose();
public abstract void init() throws Exception;
public abstract void close();
}

View file

@ -1,11 +1,11 @@
package io.xpipe.app.fxcomps;
package io.xpipe.app.comp;
import io.xpipe.app.comp.augment.Augment;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
@ -97,8 +97,8 @@ public abstract class Comp<S extends CompStructure<?>> {
return apply(struc -> struc.get().setMinWidth(width));
}
public Comp<S> minHeight(double width) {
return apply(struc -> struc.get().setMinHeight(width));
public Comp<S> minHeight(double height) {
return apply(struc -> struc.get().setMinHeight(height));
}
public Comp<S> maxWidth(int width) {

View file

@ -1,4 +1,4 @@
package io.xpipe.app.fxcomps;
package io.xpipe.app.comp;
import javafx.scene.layout.Region;

View file

@ -1,59 +0,0 @@
package io.xpipe.app.comp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.issue.ErrorEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import java.nio.file.Path;
public class DeveloperTabComp extends SimpleComp {
@Override
protected Region createSimple() {
var button = new ButtonComp(AppI18n.observable("Throw exception"), null, () -> {
throw new IllegalStateException();
});
var button2 = new ButtonComp(AppI18n.observable("Throw exception with file"), null, () -> {
try {
throw new IllegalStateException();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex)
.attachment(Path.of("extensions.txt"))
.build()
.handle();
}
});
var button3 = new ButtonComp(AppI18n.observable("Exit"), null, () -> {
System.exit(0);
});
var button6 = new ButtonComp(AppI18n.observable("Restart"), null, () -> {
OperationMode.restart();
});
var button4 = new ButtonComp(AppI18n.observable("Throw terminal exception"), null, () -> {
try {
throw new IllegalStateException();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).terminal(true).build().handle();
}
});
var button5 = new ButtonComp(AppI18n.observable("Operation mode null"), null, OperationMode::close);
return new HBox(
button.createRegion(),
button2.createRegion(),
button3.createRegion(),
button4.createRegion(),
button5.createRegion(),
button6.createRegion());
}
}

View file

@ -1,4 +1,4 @@
package io.xpipe.app.fxcomps;
package io.xpipe.app.comp;
import javafx.scene.layout.Region;

View file

@ -1,4 +1,4 @@
package io.xpipe.app.fxcomps;
package io.xpipe.app.comp;
import javafx.scene.layout.Region;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.fxcomps.augment;
package io.xpipe.app.comp.augment;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import javafx.scene.Node;
import javafx.scene.layout.Region;

View file

@ -1,6 +1,6 @@
package io.xpipe.app.fxcomps.augment;
package io.xpipe.app.comp.augment;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.comp.CompStructure;
import javafx.event.ActionEvent;
import javafx.geometry.Side;

View file

@ -1,6 +1,6 @@
package io.xpipe.app.fxcomps.augment;
package io.xpipe.app.comp.augment;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.comp.CompStructure;
import javafx.beans.binding.Bindings;
import javafx.scene.Node;

View file

@ -1,8 +1,8 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import javafx.scene.layout.AnchorPane;

View file

@ -1,13 +1,11 @@
package io.xpipe.app.comp;
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.base.SideMenuBarComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@ -54,7 +52,7 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
DataStorage.get().saveAsync();
}
if (o != null && o.equals(model.getEntries().get(1))) {
if (o != null && o.equals(model.getEntries().get(0))) {
StoreViewState.get().updateDisplay();
}
});

View file

@ -1,62 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
public class BackgroundImageComp extends Comp<CompStructure<Pane>> {
private final Image image;
public BackgroundImageComp(Image image) {
this.image = image;
}
@Override
public CompStructure<Pane> createBase() {
ImageView v = new ImageView(image);
Pane pane = new Pane(v);
v.fitWidthProperty().bind(pane.widthProperty());
v.fitHeightProperty().bind(pane.heightProperty());
if (image == null) {
return new SimpleCompStructure<>(pane);
}
double imageAspect = image.getWidth() / image.getHeight();
ChangeListener<? super Number> cl = (c, o, n) -> {
double paneAspect = pane.getWidth() / pane.getHeight();
double relViewportWidth;
double relViewportHeight;
// Pane width too big for image
if (paneAspect > imageAspect) {
relViewportWidth = 1;
double newImageHeight = pane.getWidth() / imageAspect;
relViewportHeight = Math.min(1, pane.getHeight() / newImageHeight);
}
// Height too big
else {
relViewportHeight = 1;
double newImageWidth = pane.getHeight() * imageAspect;
relViewportWidth = Math.min(1, pane.getWidth() / newImageWidth);
}
v.setViewport(new Rectangle2D(
((1 - relViewportWidth) / 2.0) * image.getWidth(),
((1 - relViewportHeight) / 2.0) * image.getHeight(),
image.getWidth() * relViewportWidth,
image.getHeight() * relViewportHeight));
};
pane.widthProperty().addListener(cl);
pane.heightProperty().addListener(cl);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -1,66 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.CompStructure;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import lombok.Builder;
import lombok.Value;
public class BigIconButton extends ButtonComp {
public BigIconButton(ObservableValue<String> name, Node graphic, Runnable listener) {
super(name, graphic, listener);
}
@Override
public Structure createBase() {
var vbox = new VBox();
vbox.getStyleClass().add("vbox");
vbox.setAlignment(Pos.CENTER);
var icon = new StackPane(getGraphic());
icon.setAlignment(Pos.CENTER);
icon.getStyleClass().add("icon");
vbox.getChildren().add(icon);
var label = new Label();
label.textProperty().bind(getName());
label.getStyleClass().add("name");
vbox.getChildren().add(label);
var b = new Button(null);
b.accessibleTextProperty().bind(getName());
b.setGraphic(vbox);
b.setOnAction(e -> getListener().run());
b.getStyleClass().add("big-icon-button-comp");
return Structure.builder()
.stack(vbox)
.graphic(getGraphic())
.graphicPane(icon)
.text(label)
.button(b)
.build();
}
@Value
@Builder
public static class Structure implements CompStructure<Button> {
Button button;
VBox stack;
Node graphic;
StackPane graphicPane;
Label text;
@Override
public Button get() {
return button;
}
}
}

View file

@ -1,9 +1,9 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

View file

@ -1,10 +1,10 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.Translatable;
import javafx.beans.property.Property;

View file

@ -1,9 +1,9 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;

View file

@ -1,9 +1,9 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.collections.FXCollections;

View file

@ -1,12 +1,11 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.browser.session.BrowserChooserComp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.browser.BrowserFileChooserSessionComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.ContextualFileReference;
@ -67,7 +66,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
public CompStructure<HBox> createBase() {
var path = previousFileReferences.isEmpty() ? createTextField() : createComboBox();
var fileBrowseButton = new ButtonComp(null, new FontIcon("mdi2f-folder-open-outline"), () -> {
BrowserChooserComp.openSingleFile(
BrowserFileChooserSessionComp.openSingleFile(
() -> fileSystem.getValue(),
fileStore -> {
if (fileStore == null) {

View file

@ -1,9 +1,9 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;

View file

@ -1,10 +1,10 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;

View file

@ -1,9 +1,9 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;

View file

@ -1,8 +1,8 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.FailableConsumer;

View file

@ -1,11 +1,11 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
@ -58,6 +58,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
filter.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) {
filter.clear();
filter.getScene().getRoot().requestFocus();
event.consume();
}

View file

@ -1,8 +1,8 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;

View file

@ -1,8 +1,8 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;

View file

@ -1,8 +1,8 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;

View file

@ -1,10 +1,10 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;

View file

@ -1,9 +1,9 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;

View file

@ -1,9 +1,9 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;

View file

@ -1,9 +1,7 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.TextAreaComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.util.FileOpener;
import javafx.application.Platform;
@ -68,7 +66,14 @@ public class IntegratedTextAreaComp extends Comp<IntegratedTextAreaComp.Structur
return new TextAreaStructure(c, textArea.getTextArea());
}
},
paths -> value.setValue(Files.readString(paths.getFirst())));
paths -> {
var first = paths.getFirst();
if (Files.size(first) > 1_000_000) {
return;
}
value.setValue(Files.readString(first));
});
var struc = fileDrop.createStructure();
return new Structure(struc.get(), struc.getCompStructure().getTextArea());
}

View file

@ -1,9 +1,9 @@
package io.xpipe.app.fxcomps.impl;
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;

View file

@ -1,9 +1,9 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.Region;
@ -11,14 +11,14 @@ import lombok.Value;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
public class LeftSplitPaneComp extends Comp<LeftSplitPaneComp.Structure> {
private final Comp<?> left;
private final Comp<?> center;
private Double initialWidth;
private Consumer<Double> onDividerChange;
public SideSplitPaneComp(Comp<?> left, Comp<?> center) {
public LeftSplitPaneComp(Comp<?> left, Comp<?> center) {
this.left = left;
this.center = center;
}
@ -58,12 +58,12 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
return new Structure(sidebar, c, r, r.getDividers().getFirst());
}
public SideSplitPaneComp withInitialWidth(double val) {
public LeftSplitPaneComp withInitialWidth(double val) {
this.initialWidth = val;
return this;
}
public SideSplitPaneComp withOnDividerChange(Consumer<Double> onDividerChange) {
public LeftSplitPaneComp withOnDividerChange(Consumer<Double> onDividerChange) {
this.onDividerChange = onDividerChange;
return this;
}

View file

@ -1,10 +1,10 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;

Some files were not shown because too many files have changed in this diff Show more