Squash merge branch 22-release into master

This commit is contained in:
crschnick
2026-03-26 11:35:04 +00:00
parent 0194ed313d
commit 609bdc478d
353 changed files with 9170 additions and 2290 deletions
+4 -7
View File
@@ -48,13 +48,10 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api ('io.modelcontextprotocol.sdk:mcp-core:0.17.2') {
api ('io.modelcontextprotocol.sdk:mcp-core:1.0.0') {
exclude group: "com.ethlo.time", module: "itu"
}
api ('io.modelcontextprotocol.sdk:mcp-json:0.17.2') {
exclude group: "com.ethlo.time", module: "itu"
}
api ('io.modelcontextprotocol.sdk:mcp-json-jackson2:0.17.2') {
api ('io.modelcontextprotocol.sdk:mcp-json-jackson2:1.0.0') {
exclude group: "com.ethlo.time", module: "itu"
exclude group: "com.fasterxml.jackson.dataformat", module: "jackson-dataformat-yaml"
}
@@ -73,9 +70,9 @@ dependencies {
api 'org.apache.commons:commons-lang3:3.20.0'
api 'io.sentry:sentry:8.20.0'
api 'commons-io:commons-io:2.21.0'
api "com.fasterxml.jackson.core:jackson-databind:2.21.0"
api "com.fasterxml.jackson.core:jackson-databind:2.21.1"
api "com.fasterxml.jackson.core:jackson-annotations:2.21"
api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.0"
api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.1"
api "org.kordamp.ikonli:ikonli-material2-pack:12.4.0"
api "org.kordamp.ikonli:ikonli-materialdesign2-pack:12.4.0"
api 'org.kordamp.ikonli:ikonli-bootstrapicons-pack:12.4.0'
@@ -84,7 +84,8 @@ public class ActionShortcutComp extends SimpleRegionBuilder {
ThreadHelper.runFailableAsync(() -> {
var file = DesktopShortcuts.createOpen(
name.getValue(),
"open \"" + url.getValue() + "\" -d \"" + AppProperties.get().getDataDir() + "\"",
"open \"" + url.getValue() + "\" -d \""
+ AppProperties.get().getDataDir() + "\"",
null);
DesktopHelper.browseFileInDirectory(file);
});
@@ -1,9 +1,27 @@
package io.xpipe.app.action;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public interface LauncherUrlProvider extends ActionProvider {
static List<LauncherUrlProvider> getAll() {
return ActionProvider.ALL.stream()
.map(actionProvider -> actionProvider instanceof LauncherUrlProvider lup ? lup : null)
.filter(Objects::nonNull)
.toList();
}
static Optional<LauncherUrlProvider> find(String url) {
return ActionProvider.ALL.stream()
.filter(actionProvider -> actionProvider instanceof LauncherUrlProvider lup
&& url.toLowerCase().startsWith(lup.getScheme().toLowerCase() + ":"))
.findFirst()
.map(lup -> (LauncherUrlProvider) lup);
}
String getScheme();
AbstractAction createAction(URI uri) throws Exception;
@@ -0,0 +1,44 @@
package io.xpipe.app.action;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.hub.action.impl.OpenHubMenuLeafProvider;
import io.xpipe.app.storage.DataStoreEntry;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public interface QuickConnectProvider extends ActionProvider {
static List<QuickConnectProvider> getAll() {
return ActionProvider.ALL.stream()
.map(actionProvider -> actionProvider instanceof QuickConnectProvider qcp ? qcp : null)
.filter(Objects::nonNull)
.toList();
}
static Optional<QuickConnectProvider> find(String input) {
return ActionProvider.ALL.stream()
.filter(actionProvider -> actionProvider instanceof QuickConnectProvider qcp
&& (input.length() <= qcp.getName().length()
&& input.toLowerCase()
.startsWith(qcp.getName().toLowerCase())
|| input.toLowerCase().startsWith(qcp.getName().toLowerCase())))
.findFirst()
.map(qcp -> (QuickConnectProvider) qcp);
}
String getName();
Optional<DataStoreEntry> findExisting(DataStore store);
DataStore createStore(String arguments, DataStore existing);
String getPlaceholder();
boolean skipDialogIfPossible();
default void open(DataStoreEntry e) throws Exception {
OpenHubMenuLeafProvider.Action.builder().ref(e.ref()).build().executeSync();
}
}
@@ -4,6 +4,7 @@ import io.xpipe.app.beacon.mcp.AppMcpServer;
import io.xpipe.app.core.AppLocalTemp;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.beacon.BeaconConfig;
import io.xpipe.beacon.BeaconInterface;
@@ -170,13 +171,16 @@ public class AppBeaconServer {
}
private boolean handleCorsHeaders(HttpExchange exchange) throws IOException {
exchange.getResponseHeaders()
.add("Origin", "http://localhost:" + AppBeaconServer.get().getPort());
exchange.getResponseHeaders().add("Vary", "Origin");
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
exchange.getResponseHeaders().add("Access-Control-Allow-Credentials", "true");
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "*");
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "*");
if (AppPrefs.get().enableHttpApi().get()) {
exchange.getResponseHeaders()
.add("Origin", "http://localhost:" + AppBeaconServer.get().getPort());
exchange.getResponseHeaders().add("Vary", "Origin");
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
exchange.getResponseHeaders().add("Access-Control-Allow-Credentials", "true");
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "*");
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "*");
}
if (exchange.getRequestMethod().equals("OPTIONS")) {
exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1);
return true;
@@ -51,7 +51,7 @@ public class AskpassExchangeImpl extends AskpassExchange {
prompt = prompt.replace("[sudo: authenticate]", "[sudo]");
if (msg.getRequest() == null) {
var r = AskpassAlert.queryRaw(prompt, null, true);
var r = AskpassAlert.queryRaw(prompt, null, false);
return Response.builder()
.value(r.getState() == SecretQueryState.NORMAL ? r.getSecret() : InPlaceSecretValue.of(""))
.build();
@@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.HttpHeaders;
@@ -83,7 +83,10 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
public List<String> protocolVersions() {
return List.of(
ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
ProtocolVersions.MCP_2024_11_05,
ProtocolVersions.MCP_2025_03_26,
ProtocolVersions.MCP_2025_06_18,
ProtocolVersions.MCP_2025_11_25);
}
@Override
@@ -137,7 +137,8 @@ public interface McpToolHandler
return e.ref();
}
public DataStoreEntryRef<ShellStore> getShellStoreRef(String name, boolean mutation) throws BeaconClientException {
public DataStoreEntryRef<ShellStore> getShellStoreRef(String name, boolean mutation)
throws BeaconClientException {
var ref = getDataStoreRef(name);
var isShell = ref.getStore() instanceof ShellStore;
if (!isShell) {
@@ -145,10 +146,12 @@ public interface McpToolHandler
+ DataStorage.get().getStorePath(ref.get()).toString() + " is not a shell connection");
}
var disableMutation = DataStorage.get().getEffectiveCategoryConfig(ref.get()).getDontAllowScripts();
var disableMutation =
DataStorage.get().getEffectiveCategoryConfig(ref.get()).getDontAllowScripts();
if (mutation && disableMutation != null && disableMutation) {
throw new BeaconClientException("Modifications to connection "
+ DataStorage.get().getStorePath(ref.get()).toString() + " is disabled by the category setting");
+ DataStorage.get().getStorePath(ref.get()).toString()
+ " is disabled by the category setting");
}
return ref.asNeeded();
@@ -18,7 +18,7 @@ import io.xpipe.core.FilePath;
import io.xpipe.core.JacksonMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.Builder;
@@ -109,8 +109,13 @@ public final class McpTools {
continue;
}
var section = StoreViewState.get().getSectionForWrapper(StoreViewState.get().getEntryWrapper(e));
var info = section.isPresent() ? e.getProvider().informationString(section.get()).getValue() : null;
var section = StoreViewState.get()
.getSectionForWrapper(StoreViewState.get().getEntryWrapper(e));
var info = section.isPresent()
? e.getProvider()
.informationString(section.get())
.getValue()
: null;
var r = ConnectionResource.builder()
.name(e.getName())
@@ -11,6 +11,7 @@ import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.FileSystemStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.StoreEntryWrapper;
import io.xpipe.app.hub.comp.StoreFilter;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.InputHelper;
@@ -123,7 +124,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
var category = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue());
var filter = new SimpleStringProperty();
var filter = new SimpleObjectProperty<StoreFilter>();
var filterTrigger = new ObservableSubscriber();
var bookmarkTopBar = new BrowserConnectionListFilterComp(filterTrigger, category, filter);
var bookmarksList = new BrowserConnectionListComp(
@@ -141,6 +142,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
rec.heightProperty().bind(struc.heightProperty());
rec.setArcHeight(7);
rec.setArcWidth(7);
rec.setSmooth(false);
struc.getChildren().getFirst().setClip(rec);
})
.vgrow();
@@ -168,7 +170,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
var splitPane = new LeftSplitPaneComp(vertical, stack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.applyStructure(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMinWidth(250);
struc.getLeft().setMaxWidth(500);
});
splitPane.disable(model.getBusy());
@@ -11,6 +11,7 @@ import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.StoreEntryWrapper;
import io.xpipe.app.hub.comp.StoreFilter;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.InputHelper;
@@ -23,7 +24,6 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
@@ -90,7 +90,7 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder {
leftSplit.set(d);
});
splitPane.applyStructure(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMinWidth(250);
struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false);
});
@@ -149,7 +149,7 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder {
var category = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue());
var filter = new SimpleStringProperty();
var filter = new SimpleObjectProperty<StoreFilter>();
var bookmarkTopBar = new BrowserConnectionListFilterComp(filterTrigger, category, filter);
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(
@@ -165,6 +165,7 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder {
bookmarksContainer
.apply(struc -> {
var rec = new Rectangle();
rec.setSmooth(false);
rec.widthProperty().bind(struc.widthProperty());
rec.heightProperty().bind(struc.heightProperty());
rec.setArcHeight(11);
@@ -225,6 +226,7 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder {
});
var clip = new Rectangle();
clip.setSmooth(false);
clip.widthProperty().bind(struc.widthProperty());
clip.heightProperty().bind(struc.heightProperty());
struc.setClip(clip);
@@ -25,14 +25,14 @@ public final class BrowserConnectionListComp extends SimpleRegionBuilder {
private final Predicate<StoreEntryWrapper> applicable;
private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;
private final Property<StoreCategoryWrapper> category;
private final Property<String> filter;
private final Property<StoreFilter> filter;
public BrowserConnectionListComp(
ObservableValue<DataStoreEntry> selected,
Predicate<StoreEntryWrapper> applicable,
BiConsumer<StoreEntryWrapper, BooleanProperty> action,
Property<StoreCategoryWrapper> category,
Property<String> filter) {
Property<StoreFilter> filter) {
this.selected = selected;
this.applicable = applicable;
this.action = action;
@@ -4,8 +4,9 @@ import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.hub.comp.DataStoreCategoryChoiceComp;
import io.xpipe.app.hub.comp.StoreCategoryChoiceComp;
import io.xpipe.app.hub.comp.StoreCategoryWrapper;
import io.xpipe.app.hub.comp.StoreFilter;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.util.ObservableSubscriber;
@@ -24,21 +25,21 @@ public final class BrowserConnectionListFilterComp extends SimpleRegionBuilder {
private final ObservableSubscriber filterTrigger;
private final Property<StoreCategoryWrapper> category;
private final Property<String> filter;
private final Property<StoreFilter> filter;
@Override
protected Region createSimple() {
var category = new DataStoreCategoryChoiceComp(
var category = new StoreCategoryChoiceComp(
StoreViewState.get().getAllConnectionsCategory(),
StoreViewState.get().getActiveCategory(),
this.category,
true,
ignored -> true)
ignored -> true)
.style(Styles.LEFT_PILL)
.apply(struc -> {
AppFontSizes.base(struc);
});
var filter = new FilterComp(this.filter)
var filter = FilterComp.ofStoreFilter(this.filter)
.style(Styles.RIGHT_PILL)
.minWidth(0)
.hgrow()
@@ -47,7 +47,7 @@ public interface BrowserFileInput {
return false;
}
if (info != null) {
if (info != null && info.getPermissions() != null) {
var otherWrite = info.getPermissions().charAt(6) == 'r';
if (otherWrite) {
return false;
@@ -241,7 +241,7 @@ public class BrowserFileSystemTabComp extends SimpleRegionBuilder {
});
var home = new BrowserOverviewComp(model).style("browser-overview");
var stack = new MultiContentComp(false, Map.of(home, showOverview, fileList, showOverview.not()), false);
var stack = new MultiContentComp(false, Map.of(home, showOverview, fileList, showOverview.not()));
var r = stack.style("browser-content-container").build();
r.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
@@ -425,7 +425,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
} else {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
openTerminalSync(name, directory, fileSystem.getShell().get().command(adjustedPath), true);
openTerminalSync(
name, directory, fileSystem.getShell().get().command(adjustedPath), true);
});
});
}
@@ -590,7 +591,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
// If we docked once, we don't want to break it by opening new tabs in maybe still docked tabs
var preferTabs = !wasTerminalDocked && !dock;
var preferTabs = !TerminalDockHubManager.get().getDockModel().isRunning() && !wasTerminalDocked && !dock;
wasTerminalDocked = wasTerminalDocked || dock;
TerminalLaunch.builder()
@@ -63,7 +63,7 @@ public class BrowserHistoryTabComp extends SimpleRegionBuilder {
var map = new LinkedHashMap<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>>();
map.put(emptyDisplay, empty);
map.put(contentDisplay, empty.not());
var stack = new MultiContentComp(false, map, false);
var stack = new MultiContentComp(false, map);
return stack.build();
}
@@ -113,6 +113,7 @@ public class BrowserNavBarComp extends RegionStructureBuilder<HBox, BrowserNavBa
// Prevent overflow
var clip = new Rectangle();
clip.setSmooth(false);
clip.widthProperty().bind(stack.widthProperty());
clip.heightProperty().bind(stack.heightProperty());
stack.setClip(clip);
@@ -84,6 +84,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
}
public void showMenu(Node anchor) {
keyBasedNavigation = true;
getItems().clear();
ThreadHelper.runFailableAsync(() -> {
var entry = base.get();
@@ -264,7 +265,9 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
}
});
new BooleanAnimationTimer(hover, 100, () -> {
expandDirectoryMenu(empty);
if (!keyBasedNavigation) {
expandDirectoryMenu(empty);
}
})
.start();
}
@@ -289,7 +292,12 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
}
});
contextMenu.addEventFilter(MouseEvent.ANY, event -> {
keyBasedNavigation = false;
var mouseEvent = event.getEventType() == MouseEvent.MOUSE_PRESSED
|| event.getEventType() == MouseEvent.MOUSE_MOVED;
keyBasedNavigation = keyBasedNavigation && !mouseEvent;
if (keyBasedNavigation) {
event.consume();
}
});
}
});
@@ -51,29 +51,32 @@ public class BrowserTransferComp extends SimpleRegionBuilder {
.mapped(item -> item.getBrowserEntry())
.getList();
var list = new BrowserFileSelectionListComp(binding, entry -> {
var sourceItem = model.getCurrentItems().stream()
.filter(item -> item.getBrowserEntry() == entry)
.findAny();
if (sourceItem.isEmpty()) {
return new SimpleStringProperty("?");
}
synchronized (sourceItem.get().getProgress()) {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
if (p == null || p.getTotal() == 0) {
return entry.getFileName();
}
var sourceItem = model.getCurrentItems().stream()
.filter(item -> item.getBrowserEntry() == entry)
.findAny();
if (sourceItem.isEmpty()) {
return new SimpleStringProperty("?");
}
synchronized (sourceItem.get().getProgress()) {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
if (p == null || p.getTotal() == 0) {
return entry.getFileName();
}
var hideProgress =
sourceItem.get().getDownloadFinished().get();
var share = p.getTransferred() * 100 / p.getTotal();
var progressSuffix = hideProgress ? "" : " " + share + "%";
return entry.getFileName() + progressSuffix;
},
sourceItem.get().getProgress());
}
}).vgrow();
var hideProgress = sourceItem
.get()
.getDownloadFinished()
.get();
var share = p.getTransferred() * 100 / p.getTotal();
var progressSuffix = hideProgress ? "" : " " + share + "%";
return entry.getFileName() + progressSuffix;
},
sourceItem.get().getProgress());
}
})
.vgrow();
var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
.apply(struc -> struc.setGraphic(new FontIcon("mdi2h-hand-back-left-outline")))
.apply(struc -> struc.setWrapText(true))
@@ -11,6 +11,7 @@ import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.core.OsType;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
@@ -34,10 +35,11 @@ public class GradleRunMenuProvider implements BrowserMenuLeafProvider {
}
OsType.Any osType = model.getFileSystem().getShell().orElseThrow().getOsType();
var ext = switch (osType) {
case OsType.Windows ignored -> "gradlew.bat";
default -> "gradlew";
};
var ext =
switch (osType) {
case OsType.Windows ignored -> "gradlew.bat";
default -> "gradlew";
};
if (!entries.getFirst().getFileName().equalsIgnoreCase(ext)) {
return false;
@@ -79,15 +81,16 @@ public class GradleRunMenuProvider implements BrowserMenuLeafProvider {
}
var parent = entries.getFirst().getRawFileEntry().getPath().getParent();
var command = model.getFileSystem().getShell().orElseThrow().command(CommandBuilder.of()
.add("sh")
.addFile(entries.getFirst().getRawFileEntry().getPath())
.add(fixedTasks)
);
var command = model.getFileSystem()
.getShell()
.orElseThrow()
.command(CommandBuilder.of()
.add("sh")
.addFile(entries.getFirst().getRawFileEntry().getPath())
.add(fixedTasks));
model.openTerminalAsync(fixedTasks, parent, command, true);
});
modal.show();
}
}
@@ -34,17 +34,12 @@ public class UnzipActionProvider implements BrowserActionProvider {
public void executeImpl() throws Exception {
var sc = model.getFileSystem().getShell().orElseThrow();
if (sc.getOsType() == OsType.WINDOWS) {
if (ShellDialects.isPowershell(sc)) {
sc.enforceDialect(ShellDialects.POWERSHELL, p -> {
for (BrowserEntry entry : getEntries()) {
runPowershellCommand(sc, model, entry);
runPowershellCommand(p, model, entry);
}
} else {
try (var sub = sc.subShell(ShellDialects.POWERSHELL)) {
for (BrowserEntry entry : getEntries()) {
runPowershellCommand(sub, model, entry);
}
}
}
return null;
});
} else {
for (BrowserEntry entry : getEntries()) {
var command = CommandBuilder.of()
@@ -53,13 +53,10 @@ public class ZipActionProvider implements BrowserActionProvider {
}
}
if (ShellDialects.isPowershell(sc)) {
sc.command(command).withWorkingDirectory(base).execute();
} else {
try (var sub = sc.subShell(ShellDialects.POWERSHELL)) {
sub.command(command).withWorkingDirectory(base).execute();
}
}
sc.enforceDialect(ShellDialects.POWERSHELL, p -> {
p.command(command).withWorkingDirectory(base).execute();
return null;
});
} else {
var command = CommandBuilder.of().add("zip", "-q", "-y", "-r", "-");
for (BrowserEntry entry : getEntries()) {
@@ -4,27 +4,22 @@ import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.comp.RegionStructure;
import io.xpipe.app.comp.RegionStructureBuilder;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppRestart;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.terminal.TerminalDockHubManager;
import io.xpipe.app.util.GlobalTimer;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.ButtonBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.bouncycastle.math.raw.Mod;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -47,7 +42,7 @@ public class AppLayoutComp extends RegionStructureBuilder<BorderPane, AppLayoutC
model.getSelected()),
(v1, v2) -> v2,
LinkedHashMap::new));
var multi = new MultiContentComp(true, map, true);
var multi = new MultiContentComp(true, map);
multi.style("background");
var pane = new BorderPane();
@@ -56,34 +51,6 @@ public class AppLayoutComp extends RegionStructureBuilder<BorderPane, AppLayoutC
pane.setCenter(multiR);
var sidebarR = sidebar.build();
pane.setRight(sidebarR);
model.getSelected().addListener((c, o, n) -> {
if (o != null && o.equals(model.getEntries().get(2))) {
var prefs = AppPrefs.get();
if (prefs != null) {
prefs.save();
}
var storage = DataStorage.get();
if (storage != null) {
storage.saveAsync();
}
if (AppPrefs.get() != null && AppPrefs.get().getRequiresRestart().get()) {
GlobalTimer.delay(() -> {
var modal = ModalOverlay.of("prefsRestartTitle", AppDialog.dialogTextKey("prefsRestartContent"));
modal.addButton(ModalButton.cancel());
modal.addButton(new ModalButton("restart", () -> AppRestart.restart(), true, true));
modal.show();
}, Duration.ofSeconds(1));
}
}
if (o != null && o.equals(model.getEntries().get(0))) {
var svs = StoreViewState.get();
if (svs != null) {
svs.triggerStoreListUpdate();
}
}
});
pane.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
sidebarR.getChildrenUnmodifiable().forEach(node -> {
var shortcut = (KeyCodeCombination) node.getProperties().get("shortcut");
@@ -92,6 +59,13 @@ public class AppLayoutComp extends RegionStructureBuilder<BorderPane, AppLayoutC
event.consume();
}
});
if (new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN).match(event)) {
if (TerminalDockHubManager.get().getEnabled().get()) {
TerminalDockHubManager.get().toggleDock();
event.consume();
}
}
});
pane.getStyleClass().add("layout");
return new Structure(pane, multiR, sidebarR, new ArrayList<>(multiR.getChildren()));
@@ -27,7 +27,6 @@ import javafx.scene.control.ListCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import lombok.NonNull;
import lombok.Setter;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
@@ -4,10 +4,12 @@ import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.RegionDescriptor;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppOpenArguments;
import io.xpipe.app.hub.comp.StoreFilter;
import io.xpipe.app.platform.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Cursor;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
@@ -15,6 +17,8 @@ import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import atlantafx.base.controls.CustomTextField;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Rectangle;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@@ -22,6 +26,14 @@ import java.util.Objects;
public class FilterComp extends RegionBuilder<CustomTextField> {
public static FilterComp ofStoreFilter(Property<StoreFilter> filter) {
var prop = new SimpleStringProperty();
prop.subscribe(s -> {
filter.setValue(StoreFilter.of(s));
});
return new FilterComp(prop);
}
private final Property<String> filterText;
public FilterComp(Property<String> filterText) {
@@ -83,6 +95,15 @@ public class FilterComp extends RegionBuilder<CustomTextField> {
filterText.setValue(n != null && n.length() > 0 ? n : null);
});
// Fix caret not being visible on right side when overflowing
filter.setSkin(filter.createDefaultSkin());
Pane pane = (Pane) filter.getChildrenUnmodifiable().getFirst();
var rec = new Rectangle();
rec.widthProperty().bind(pane.widthProperty().add(2));
rec.heightProperty().bind(pane.heightProperty());
rec.setSmooth(false);
filter.getChildrenUnmodifiable().getFirst().setClip(rec);
return filter;
}
}
@@ -91,8 +91,11 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder<AnchorPane, I
var val = value.getValue() != null ? value.getValue() : "";
var valCount = (int) val.lines().count() + (val.endsWith("\n") ? 1 : 0);
var promptVal = struc.getTextArea().getPromptText() != null ? struc.getTextArea().getPromptText() : "";
var promptValCount = (int) promptVal.lines().count() + (promptVal.endsWith("\n") ? 1 : 0);
var promptVal = struc.getTextArea().getPromptText() != null
? struc.getTextArea().getPromptText()
: "";
var promptValCount =
(int) promptVal.lines().count() + (promptVal.endsWith("\n") ? 1 : 0);
var count = Math.max(valCount, promptValCount);
// Somehow the handling of trailing newlines is weird
@@ -107,8 +110,8 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder<AnchorPane, I
var copyButton = createOpenButton();
var pane = new AnchorPane(textAreaStruc.get(), copyButton);
pane.setPickOnBounds(false);
AnchorPane.setTopAnchor(copyButton, 7.0);
AnchorPane.setRightAnchor(copyButton, 7.0);
AnchorPane.setTopAnchor(copyButton, 4.0);
AnchorPane.setRightAnchor(copyButton, 4.0);
AnchorPane.setLeftAnchor(textAreaStruc.get(), 0.0);
AnchorPane.setRightAnchor(textAreaStruc.get(), 0.0);
pane.maxHeightProperty().bind(textAreaStruc.get().heightProperty());
@@ -62,13 +62,13 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
vbox.setFocusTraversable(false);
var scroll = new ScrollPane(vbox);
refresh(scroll, vbox, shown, all, cache, false);
refresh(vbox, shown, all, cache);
var hadScene = new AtomicBoolean(false);
scroll.sceneProperty().subscribe(scene -> {
if (scene != null) {
hadScene.set(true);
refresh(scroll, vbox, shown, all, cache, true);
refresh(vbox, shown, all, cache);
}
});
@@ -78,7 +78,7 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
return;
}
refresh(scroll, vbox, c.getList(), all, cache, true);
refresh(vbox, c.getList(), all, cache);
});
});
@@ -118,12 +118,21 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
var dirty = new SimpleBooleanProperty();
var animationTimer = new AnimationTimer() {
private long delayThresholdCrossed;
@Override
public void handle(long now) {
if (!dirty.get()) {
return;
}
var ms = now / 1_000_000;
if (ms < delayThresholdCrossed + (super.hashCode() % 100)) {
return;
}
delayThresholdCrossed = ms;
updateVisibilities(scroll, vbox);
dirty.set(false);
}
@@ -146,6 +155,19 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
vbox.heightProperty().addListener((observable, oldValue, newValue) -> {
dirty.set(true);
});
vbox.getChildren().addListener((ListChangeListener<? super Node>) (change) -> {
dirty.set(true);
});
shown.addListener((ListChangeListener<? super T>) (change) -> {
Platform.runLater(() -> {
dirty.set(true);
});
});
scroll.sceneProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
dirty.set(true);
}
});
// We can't directly listen to any parent element changing visibility, so this is a compromise
if (AppLayoutModel.get() != null) {
@@ -296,13 +318,7 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
// }
}
private void refresh(
ScrollPane scroll,
VBox listView,
List<? extends T> shown,
List<? extends T> all,
Map<T, Region> cache,
boolean refreshVisibilities) {
private void refresh(VBox vbox, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache) {
Runnable update = () -> {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not in FxApplication thread");
@@ -344,7 +360,7 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
.filter(region -> region != null)
.toList();
if (listView.getChildren().equals(newShown)) {
if (vbox.getChildren().equals(newShown)) {
return;
}
@@ -356,11 +372,8 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
r.pseudoClassStateChanged(LAST, i == newShown.size() - 1);
}
var d = DerivedObservableList.wrap(listView.getChildren(), true);
var d = DerivedObservableList.wrap(vbox.getChildren(), true);
d.setContent(newShown);
if (refreshVisibilities) {
updateVisibilities(scroll, listView);
}
};
update.run();
}
@@ -2,7 +2,6 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.platform.PlatformThread;
import javafx.application.Platform;
@@ -18,13 +17,10 @@ import java.util.Map;
public class MultiContentComp extends SimpleRegionBuilder {
private final boolean requestFocus;
private final boolean log;
private final Map<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> content;
public MultiContentComp(
boolean requestFocus, Map<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> content, boolean log) {
public MultiContentComp(boolean requestFocus, Map<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> content) {
this.requestFocus = requestFocus;
this.log = log;
this.content = FXCollections.observableMap(content);
}
@@ -53,14 +49,7 @@ public class MultiContentComp extends SimpleRegionBuilder {
});
for (Map.Entry<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>> e : content.entrySet()) {
var name = e.getKey().getClass().getSimpleName();
if (log) {
TrackEvent.trace("Creating content tab region for element " + name);
}
var r = e.getKey().build();
if (log) {
TrackEvent.trace("Created content tab region for element " + name);
}
e.getValue().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
r.setManaged(val);
@@ -73,33 +62,8 @@ public class MultiContentComp extends SimpleRegionBuilder {
});
});
m.put(e.getKey(), r);
if (log) {
TrackEvent.trace("Added content tab region for element " + name);
}
}
return stack;
}
// Lazy impl
// @Override
// protected Region createSimple() {
// var stack = new StackPane();
// for (Map.Entry<BaseRegionBuilder<?,?>, ObservableValue<Boolean>> e : content.entrySet()) {
// var r = e.getKey().build();
// e.getValue().subscribe(val -> {
// PlatformThread.runLaterIfNeeded(() -> {
// r.setManaged(val);
// r.setVisible(val);
// if (val && !stack.getChildren().contains(r)) {
// stack.getChildren().add(r);
// } else {
// stack.getChildren().remove(r);
// }
// });
// });
// }
//
// return stack;
// }
}
@@ -121,7 +121,7 @@ public class SecretFieldComp extends RegionStructureBuilder<InputGroup, SecretFi
HBox.setHgrow(field, Priority.ALWAYS);
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
ClipboardHelper.copyPassword(value.getValue());
ClipboardHelper.copyPassword(value.getValue(), true);
})
.describe(d -> d.nameKey("copy"));
@@ -7,9 +7,12 @@ import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.AppDistributionType;
import io.xpipe.app.update.UpdateAvailableDialog;
import io.xpipe.app.update.UpdateHandler;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -66,18 +69,25 @@ public class SideMenuBarComp extends RegionBuilder<VBox> {
}
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded(false));
var b = new IconButtonComp("mdi2u-update", () -> {
var r = UpdateAvailableDialog.showIfNeeded(false);
if (!r) {
AppPrefs.get().selectCategory("about");
ThreadHelper.runFailableAsync(() -> {
UpdateHandler uh = AppDistributionType.get().getUpdateHandler();
uh.prepareUpdate();
});
}
});
b.describe(d -> d.nameKey("updateAvailableTooltip"));
var stack = createStyle(null, b);
var h = AppDistributionType.get().getUpdateHandler();
stack.hide(Bindings.createBooleanBinding(
() -> {
return AppDistributionType.get()
.getUpdateHandler()
.getPreparedUpdate()
.getValue()
== null;
return h.getPreparedUpdate().getValue() == null;
},
AppDistributionType.get().getUpdateHandler().getPreparedUpdate()));
h.getPreparedUpdate(),
h.getBusy()));
vbox.getChildren().add(stack.build());
}
@@ -6,7 +6,10 @@ import io.xpipe.app.platform.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Rectangle;
import java.util.Objects;
@@ -70,6 +73,15 @@ public class TextFieldComp extends RegionBuilder<TextField> {
}
});
// Fix caret not being visible on right side when overflowing
text.setSkin(new TextFieldSkin(text));
Pane pane = (Pane) text.getChildrenUnmodifiable().getFirst();
var rec = new Rectangle();
rec.widthProperty().bind(pane.widthProperty().add(2));
rec.heightProperty().bind(pane.heightProperty());
rec.setSmooth(false);
text.getChildrenUnmodifiable().getFirst().setClip(rec);
return text;
}
}
@@ -6,6 +6,7 @@ import io.xpipe.app.comp.base.ScrollComp;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.EditorCategory;
import io.xpipe.app.prefs.PasswordManagerCategory;
import io.xpipe.app.prefs.PersonalizationCategory;
import io.xpipe.app.prefs.TerminalCategory;
import io.xpipe.app.util.DocumentationLink;
@@ -24,6 +25,7 @@ public class AppConfigurationDialog {
.sub(PersonalizationCategory.themeChoice())
.sub(TerminalCategory.terminalChoice(false))
.sub(EditorCategory.editorChoice())
.sub(PasswordManagerCategory.passwordManagerChoice())
.buildComp();
options.style("initial-setup");
options.style("prefs-container");
@@ -88,7 +88,9 @@ public class AppFontSizes {
AppPrefs.get().useSystemFont().addListener((ignored, ignored2, newValue) -> {
var refNode = ref.get();
if (refNode != null) {
var effective = AppPrefs.get().theme().getValue() != null ? AppPrefs.get().theme().getValue().getFontSizes().get() : getDefault();
var effective = AppPrefs.get().theme().getValue() != null
? AppPrefs.get().theme().getValue().getFontSizes().get()
: getDefault();
setFont(refNode, function.apply(effective));
}
});
@@ -170,6 +170,10 @@ public class AppImages {
return;
}
if (images.containsKey(key)) {
return;
}
images.put(key, loadImage(p));
}
@@ -3,10 +3,16 @@ package io.xpipe.app.core;
import io.xpipe.app.browser.BrowserFullSessionComp;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.hub.comp.StoreLayoutComp;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.AppPrefsComp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.terminal.TerminalDockHubManager;
import io.xpipe.app.update.AppDistributionType;
import io.xpipe.app.util.*;
@@ -60,6 +66,46 @@ public class AppLayoutModel {
public static void init() {
var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(270, 300));
INSTANCE = new AppLayoutModel(state);
INSTANCE.addListeners();
}
private void addListeners() {
getSelected().addListener((c, o, n) -> {
if (o != null && o.equals(getEntries().get(2))) {
var prefs = AppPrefs.get();
if (prefs != null) {
prefs.save();
}
var storage = DataStorage.get();
if (storage != null) {
ThreadHelper.runAsync(() -> {
storage.refreshEntries();
storage.saveAsync();
});
}
if (AppPrefs.get() != null
&& AppPrefs.get().getRequiresRestart().get()) {
GlobalTimer.delay(
() -> {
var modal = ModalOverlay.of(
"prefsRestartTitle", AppDialog.dialogTextKey("prefsRestartContent"));
modal.addButton(ModalButton.cancel());
modal.addButton(new ModalButton("restart", () -> AppRestart.restart(), true, true));
modal.show();
},
Duration.ofSeconds(1));
AppPrefs.get().getRequiresRestart().set(false);
}
}
if (o != null && o.equals(getEntries().get(0))) {
var svs = StoreViewState.get();
if (svs != null) {
svs.triggerStoreListUpdate();
}
}
});
}
public static void reset() {
@@ -1,7 +1,6 @@
package io.xpipe.app.core;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.action.LauncherUrlProvider;
import io.xpipe.app.browser.action.impl.OpenDirectoryActionProvider;
import io.xpipe.app.core.mode.AppOperationMode;
@@ -81,15 +80,11 @@ public class AppOpenArguments {
var uri = URI.create(input);
var scheme = uri.getScheme();
if (scheme != null) {
var action = uri.getScheme();
var found = ActionProvider.ALL.stream()
.filter(actionProvider -> actionProvider instanceof LauncherUrlProvider lcs
&& lcs.getScheme().equalsIgnoreCase(action))
.findFirst();
var found = LauncherUrlProvider.find(input);
if (found.isPresent()) {
AbstractAction a;
try {
a = ((LauncherUrlProvider) found.get()).createAction(uri);
a = found.get().createAction(uri);
} catch (Exception e) {
ErrorEventFactory.fromThrowable(e).omit().expected().handle();
return List.of();
@@ -10,6 +10,8 @@ import lombok.Value;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.*;
@Value
@@ -235,4 +237,19 @@ public class AppProperties {
public Optional<AppVersion> getCanonicalVersion() {
return Optional.ofNullable(canonicalVersion);
}
public Instant getFirstStartupDate() {
var dir = defaultDataDir;
if (!Files.exists(dir)) {
return Instant.now();
}
try {
var attr = Files.readAttributes(dir, BasicFileAttributes.class);
return attr.creationTime().toInstant();
} catch (Exception e) {
ErrorEventFactory.fromThrowable(e).expected().omit().handle();
return Instant.now();
}
}
}
@@ -215,8 +215,10 @@ public abstract class AppSystemInfo {
var r = Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_Downloads);
// Replace 8.3 filename
return (downloads = Path.of(r).toRealPath());
} catch (Throwable e) {
ErrorEventFactory.fromThrowable(e).handle();
} catch (Throwable e) {
if (!(e instanceof Win32Exception)) {
ErrorEventFactory.fromThrowable(e).handle();
}
var fallback = getUserHome().resolve("Downloads");
return (downloads = fallback);
}
@@ -232,7 +234,9 @@ public abstract class AppSystemInfo {
// Replace 8.3 filename
return (documents = Path.of(r).toRealPath());
} catch (Throwable e) {
ErrorEventFactory.fromThrowable(e).handle();
if (!(e instanceof Win32Exception)) {
ErrorEventFactory.fromThrowable(e).handle();
}
var fallback = getUserHome().resolve("Documents");
return (documents = fallback);
}
@@ -249,7 +253,9 @@ public abstract class AppSystemInfo {
// Replace 8.3 filename
return (desktop = Path.of(r).toRealPath());
} catch (Throwable e) {
ErrorEventFactory.fromThrowable(e).handle();
if (!(e instanceof Win32Exception)) {
ErrorEventFactory.fromThrowable(e).handle();
}
var fallback = getUserHome().resolve("Desktop");
return (desktop = fallback);
}
@@ -260,6 +266,7 @@ public abstract class AppSystemInfo {
private Path downloads;
private Path desktop;
private Path config;
private Boolean vm;
public boolean isDebianBased() {
@@ -356,6 +363,31 @@ public abstract class AppSystemInfo {
return (desktop = fallback);
}
public Path getConfigDir() {
if (config != null) {
return config;
}
if (System.getenv("XDG_CONFIG_HOME") != null) {
return (config = Path.of(System.getenv("XDG_CONFIG_HOME")));
} else {
return (config = AppSystemInfo.ofLinux().getUserHome().resolve(".config"));
}
}
public Path getRuntimeDir() {
if (config != null) {
return config;
}
if (System.getenv("XDG_RUNTIME_DIR") != null) {
return (config = Path.of(System.getenv("XDG_RUNTIME_DIR")));
} else {
// Bad fallback, but we don't want to run any commands to retrieve uids here
return (config = Path.of("/run/user/1000"));
}
}
@Override
public Path getTemp() {
return Path.of(System.getProperty("java.io.tmpdir"));
@@ -9,10 +9,6 @@ import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.OsType;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.ColorScheme;
import javafx.application.Platform;
@@ -21,12 +17,8 @@ import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import atlantafx.base.theme.*;
import lombok.AllArgsConstructor;
@@ -98,11 +90,13 @@ public class AppTheme {
r.pseudoClassStateChanged(PseudoClass.getPseudoClass(OsType.ofLocal().getId()), true);
Theme.ALL.forEach(theme -> {
r.pseudoClassStateChanged(
PseudoClass.getPseudoClass(theme.getCssId()),
theme.getCssId().equals(t.getCssId()));
});
if (t != null) {
Theme.ALL.forEach(theme -> {
r.pseudoClassStateChanged(
PseudoClass.getPseudoClass(theme.getCssId()),
theme.getCssId().equals(t.getCssId()));
});
}
if (t != null) {
r.pseudoClassStateChanged(LIGHT, !t.isDark());
@@ -251,41 +245,8 @@ public class AppTheme {
TrackEvent.debug("Setting theme " + newTheme.getId() + " for scene");
// Don't animate transition in performance mode
if (AppPrefs.get() == null || AppPrefs.get().performanceMode().get()) {
newTheme.apply();
return;
}
var stage = window.getStage();
var scene = stage.getScene();
Pane root = (Pane) scene.getRoot();
Image snapshot = null;
try {
scene.snapshot(null);
} catch (Exception ex) {
// This can fail if there is no window / screen I guess?
ErrorEventFactory.fromThrowable(ex).expected().omit().handle();
return;
}
ImageView imageView = new ImageView(snapshot);
root.getChildren().add(imageView);
// Don't animate anything for performance reasons
newTheme.apply();
Platform.runLater(() -> {
// Animate!
var transition = new Timeline(
new KeyFrame(
Duration.millis(0),
new KeyValue(imageView.opacityProperty(), 1, Interpolator.EASE_OUT)),
new KeyFrame(
Duration.millis(600),
new KeyValue(imageView.opacityProperty(), 0, Interpolator.EASE_OUT)));
transition.setOnFinished(e -> {
root.getChildren().remove(imageView);
});
transition.play();
});
});
}
@@ -4,11 +4,11 @@ import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.User32Ex;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.*;
import com.sun.jna.win32.StdCallLibrary;
import io.xpipe.app.util.User32Ex;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@@ -92,5 +92,4 @@ public class AppWindowsLock {
return User32.INSTANCE.CallWindowProc(oldWindowProc, hwnd, uMsg, wParam, lParam);
}
}
}
@@ -16,6 +16,8 @@ import io.xpipe.app.core.window.AppWindowTitle;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.StartOnInitStore;
import io.xpipe.app.hub.comp.StoreFilterState;
import io.xpipe.app.hub.comp.StoreQuickConnect;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.issue.TrackEvent;
@@ -129,7 +131,9 @@ public class AppBaseMode extends AppOperationMode {
storageLoaded.countDown();
AppMcpServer.init();
iconsInit.await();
StoreFilterState.init();
StoreViewState.init();
StoreQuickConnect.init();
AppMainWindow.loadingText("loadingSettings");
TrackEvent.info("Connection storage initialization thread completed");
},
@@ -209,6 +213,7 @@ public class AppBaseMode extends AppOperationMode {
AppBeaconServer.reset();
KeePassXcPasswordManager.reset();
StoreViewState.reset();
StoreFilterState.reset();
AppLayoutModel.reset();
AppTheme.reset();
PlatformState.teardown();
@@ -141,6 +141,13 @@ public class AppDialog {
.prefWidth(450);
}
public static void information(String translationKey) {
var content = dialogTextKey(translationKey + "Content");
var modal = ModalOverlay.of(translationKey + "Title", content);
modal.addButton(ModalButton.ok());
show(modal);
}
public static boolean confirm(String translationKey) {
var confirmed = new AtomicBoolean(false);
var content = dialogTextKey(translationKey + "Content");
@@ -52,7 +52,6 @@ public class AppMainWindow {
private final BooleanProperty windowActive = new SimpleBooleanProperty(false);
private volatile Instant lastUpdate;
private boolean shown = false;
private AppMainWindow(Stage stage) {
this.stage = stage;
@@ -189,8 +188,16 @@ public class AppMainWindow {
AppWindowsShutdown.registerHook(ctrl.getWindowHandle());
}
}
}
shown = true;
public void hide() {
PlatformThread.runLaterIfNeeded(() -> {
if (!stage.isShowing()) {
return;
}
stage.hide();
});
}
public void focus() {
@@ -300,12 +307,6 @@ public class AppMainWindow {
// Close other windows
Stage.getWindows().stream().filter(w -> !w.equals(stage)).toList().forEach(w -> w.fireEvent(e));
// Iconifying stages on Windows will break if the window is closed
// Work around this issue it by re-showing it immediately before hiding it again
if (OsType.ofLocal() == OsType.WINDOWS) {
stage.setIconified(false);
}
// Close self
stage.close();
AppOperationMode.onWindowClose();
@@ -83,7 +83,7 @@ public class AppSideWindow {
public static Alert createEmptyAlert() {
Alert alert = new Alert(Alert.AlertType.NONE);
if (AppMainWindow.get() != null) {
if (AppMainWindow.get() != null && AppMainWindow.get().getStage().isShowing() && !AppMainWindow.get().getStage().isIconified()) {
alert.initOwner(AppMainWindow.get().getStage());
}
alert.getDialogPane().getScene().setFill(Color.TRANSPARENT);
@@ -24,7 +24,7 @@ public class AppWindowBounds {
var o = oldValue.doubleValue();
if (stage.isShowing() && areNumbersValid(o, n)) {
// Ignore rounding events
if (Math.abs(n - o) < 0.5) {
if (Math.abs(n - o) <= 0.5) {
return;
}
@@ -40,7 +40,7 @@ public class AppWindowBounds {
var o = oldValue.doubleValue();
if (stage.isShowing() && areNumbersValid(o, n)) {
// Ignore rounding events
if (Math.abs(n - o) < 0.5) {
if (Math.abs(n - o) <= 0.5) {
return;
}
@@ -59,6 +59,12 @@ public class AppWindowStyle {
return false;
});
scene.focusOwnerProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null || !newValue.isFocusTraversable()) {
keyInput.set(false);
}
});
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var c = event.getCode();
var list = List.of(KeyCode.SPACE, KeyCode.ENTER, KeyCode.SHIFT, KeyCode.TAB);
@@ -1,21 +1,20 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
@@ -32,23 +31,24 @@ import java.util.List;
@Value
@Jacksonized
@Builder
public class CustomAgentStrategy implements SshIdentityStrategy {
public class CustomAgentStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<CustomAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {
var forward =
new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
var socketBinding = Bindings.createObjectBinding(() -> {
var agent = AppPrefs.get().sshAgentSocket().getValue();
if (agent == null) {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
return agent != null ? agent.toString() : AppI18n.get("agentSocketNotConfigured");
}, AppPrefs.get().defaultSshAgentSocket(), AppPrefs.get().sshAgentSocket());
var socketBinding = Bindings.createObjectBinding(
() -> {
var agent = AppPrefs.get().sshAgentSocket().getValue();
if (agent == null) {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
return agent != null ? agent.toString() : AppI18n.get("agentSocketNotConfigured");
},
AppPrefs.get().defaultSshAgentSocket(),
AppPrefs.get().sshAgentSocket());
var socketProp = new SimpleStringProperty();
socketProp.bind(socketBinding);
var socketDisplay = new HorizontalComp(List.of(
@@ -66,34 +66,31 @@ public class CustomAgentStrategy implements SshIdentityStrategy {
.addComp(socketDisplay)
.check(val -> Validator.create(
val,
AppI18n.observable("agentSocketNotConfigured"), Bindings.createObjectBinding(() -> {
var agent = AppPrefs.get().sshAgentSocket().getValue();
if (agent == null) {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
return agent;
}, AppPrefs.get().sshAgentSocket(), AppPrefs.get().defaultSshAgentSocket()),
AppI18n.observable("agentSocketNotConfigured"),
Bindings.createObjectBinding(
() -> {
var agent = AppPrefs.get().sshAgentSocket().getValue();
if (agent == null) {
agent = AppPrefs.get()
.defaultSshAgentSocket()
.getValue();
}
return agent;
},
AppPrefs.get().sshAgentSocket(),
AppPrefs.get().defaultSshAgentSocket()),
i -> {
return i != null;
}))
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
.bind(
() -> {
return new CustomAgentStrategy(forward.get(), publicKey.get());
return new CustomAgentStrategy(publicKey.get());
},
p);
}
boolean forwardAgent;
String publicKey;
@Override
@@ -103,15 +100,12 @@ public class CustomAgentStrategy implements SshIdentityStrategy {
if (agent == null) {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
SshIdentityStateManager.prepareLocalCustomAgent(
parent, agent);
SshIdentityStateManager.prepareLocalCustomAgent(parent, agent);
}
}
@Override
public void buildCommand(CommandBuilder builder) {}
private String getIdentityAgent(ShellControl sc) throws Exception {
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
if (!sc.isLocal() || sc.getOsType() == OsType.WINDOWS) {
return null;
}
@@ -122,27 +116,33 @@ public class CustomAgentStrategy implements SshIdentityStrategy {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
if (agent != null) {
return agent.resolveTildeHome(sc.view().userHome()).toString();
return agent.resolveTildeHome(sc.view().userHome());
}
}
return null;
}
@Override
public void buildCommand(CommandBuilder builder) {}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
var l = new ArrayList<>(List.of(
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"),
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
new KeyValue("PKCS11Provider", "none")));
var agent = getIdentityAgent(sc);
var agent = determinetAgentSocketLocation(sc);
if (agent != null) {
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
}
return l;
}
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.ext.ValidationException;
@@ -116,8 +116,7 @@ public class CustomPkcs11LibraryStrategy implements SshIdentityStrategy {
new KeyValue("IdentityAgent", "none"));
}
@Override
public String getPublicKey() {
public PublicKeyStrategy getPublicKeyStrategy() {
return null;
}
}
@@ -1,17 +1,16 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -27,20 +26,15 @@ import java.util.List;
@Jacksonized
@Builder
@JsonTypeName("gpgAgent")
public class GpgAgentStrategy implements SshIdentityStrategy {
public class GpgAgentStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(Property<GpgAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {
var forward =
new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
return new OptionsBuilder()
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
@@ -48,7 +42,7 @@ public class GpgAgentStrategy implements SshIdentityStrategy {
publicKey)
.bind(
() -> {
return new GpgAgentStrategy(forward.get(), publicKey.get());
return new GpgAgentStrategy(publicKey.get());
},
p);
}
@@ -60,18 +54,6 @@ public class GpgAgentStrategy implements SshIdentityStrategy {
return supported;
}
try {
var found = LocalShell.getShell()
.view()
.findProgram("gpg-connect-agent")
.isPresent();
if (!found) {
return (supported = false);
}
} catch (Exception ex) {
return (supported = false);
}
if (OsType.ofLocal() == OsType.WINDOWS) {
var file = AppSystemInfo.ofWindows().getRoamingAppData().resolve("gnupg", "gpg-agent.conf");
return (supported = Files.exists(file));
@@ -81,7 +63,6 @@ public class GpgAgentStrategy implements SshIdentityStrategy {
}
}
boolean forwardAgent;
String publicKey;
@Override
@@ -93,9 +74,7 @@ public class GpgAgentStrategy implements SshIdentityStrategy {
}
@Override
public void buildCommand(CommandBuilder builder) {}
private String getIdentityAgent(ShellControl sc) throws Exception {
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
if (sc.getOsType() == OsType.WINDOWS) {
return null;
}
@@ -105,23 +84,29 @@ public class GpgAgentStrategy implements SshIdentityStrategy {
return null;
}
return r;
return FilePath.of(r);
}
@Override
public void buildCommand(CommandBuilder builder) {}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
var l = new ArrayList<>(List.of(
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"),
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
new KeyValue("PKCS11Provider", "none")));
var agent = getIdentityAgent(sc);
var agent = determinetAgentSocketLocation(sc);
if (agent != null) {
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
}
return l;
}
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
@@ -184,4 +184,8 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
+ Math.abs(Objects.hash(this, AppSystemInfo.ofCurrent().getUser())) + ".key");
return temp;
}
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
@@ -268,4 +268,8 @@ public class KeyFileStrategy implements SshIdentityStrategy {
}
return s;
}
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
@@ -30,7 +30,7 @@ public class NoIdentityStrategy implements SshIdentityStrategy {
}
@Override
public String getPublicKey() {
public PublicKeyStrategy getPublicKeyStrategy() {
return null;
}
@@ -1,16 +1,15 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -25,38 +24,27 @@ import java.util.List;
@Value
@Jacksonized
@Builder
public class OpenSshAgentStrategy implements SshIdentityStrategy {
public class OpenSshAgentStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<OpenSshAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {
var socket = AppPrefs.get().defaultSshAgentSocket().getValue();
var forward =
new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
return new OptionsBuilder()
.nameAndDescription("agentSocket")
.addStaticString(socket != null ? socket : AppI18n.get("agentSocketNotFound"))
.hide(OsType.ofLocal() == OsType.WINDOWS)
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
.bind(
() -> {
return new OpenSshAgentStrategy(forward.get(), publicKey.get());
return new OpenSshAgentStrategy(publicKey.get());
},
p);
}
boolean forwardAgent;
String publicKey;
@Override
@@ -68,9 +56,7 @@ public class OpenSshAgentStrategy implements SshIdentityStrategy {
}
@Override
public void buildCommand(CommandBuilder builder) {}
private String getIdentityAgent(ShellControl sc) throws Exception {
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
if (sc.getOsType() == OsType.WINDOWS) {
return null;
}
@@ -78,27 +64,33 @@ public class OpenSshAgentStrategy implements SshIdentityStrategy {
if (AppPrefs.get() != null) {
var socket = AppPrefs.get().defaultSshAgentSocket().getValue();
if (socket != null) {
return socket.resolveTildeHome(sc.view().userHome()).toString();
return socket.resolveTildeHome(sc.view().userHome());
}
}
return null;
}
@Override
public void buildCommand(CommandBuilder builder) {}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
var l = new ArrayList<>(List.of(
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"),
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
new KeyValue("PKCS11Provider", "none")));
var agent = getIdentityAgent(sc);
var agent = determinetAgentSocketLocation(sc);
if (agent != null) {
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
}
return l;
}
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -1,13 +1,12 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -21,43 +20,38 @@ import java.util.List;
@Value
@Jacksonized
@Builder
public class OtherExternalAgentStrategy implements SshIdentityStrategy {
public class OtherExternalAgentStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<OtherExternalAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {
var forward =
new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
return new OptionsBuilder()
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
.nameAndDescription("publicKey")
.bind(
() -> {
return new OtherExternalAgentStrategy(forward.get(), publicKey.get());
return new OtherExternalAgentStrategy(publicKey.get());
},
p);
}
boolean forwardAgent;
String publicKey;
@Override
public void prepareParent(ShellControl parent) throws Exception {
if (parent.isLocal()) {
SshIdentityStateManager.prepareLocalExternalAgent();
SshIdentityStateManager.prepareLocalExternalAgent(null);
}
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl parent) {
return null;
}
@Override
public void buildCommand(CommandBuilder builder) {}
@@ -66,8 +60,11 @@ public class OtherExternalAgentStrategy implements SshIdentityStrategy {
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
return List.of(
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"),
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
new KeyValue("PKCS11Provider", "none"));
}
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -1,18 +1,17 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.LocalExec;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -30,28 +29,18 @@ import java.util.List;
@Value
@Jacksonized
@Builder
public class PageantStrategy implements SshIdentityStrategy {
public class PageantStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(Property<PageantStrategy> p, SshIdentityStrategyChoiceConfig config) {
var forward =
new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
return new OptionsBuilder()
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
.bind(
() -> {
return new PageantStrategy(forward.get(), publicKey.get());
return new PageantStrategy(publicKey.get());
},
p);
}
@@ -67,7 +56,7 @@ public class PageantStrategy implements SshIdentityStrategy {
return true;
} else {
try {
var found = LocalShell.getShell().view().findProgram("pageant").isPresent();
var found = LocalExec.readStdoutIfPossible("which", "pageant").isPresent();
return (supported = found);
} catch (Exception ex) {
return (supported = false);
@@ -75,7 +64,6 @@ public class PageantStrategy implements SshIdentityStrategy {
}
}
boolean forwardAgent;
String publicKey;
@Override
@@ -92,30 +80,33 @@ public class PageantStrategy implements SshIdentityStrategy {
throw ErrorEventFactory.expected(new IllegalStateException(
"Pageant is not running as the primary agent via the $SSH_AUTH_SOCK variable."));
}
} else if (parent.isLocal()) {
// Check if it exists
getPageantWindowsPipe();
}
}
@Override
public void buildCommand(CommandBuilder builder) {}
private String getIdentityAgent(ShellControl sc) {
public FilePath determinetAgentSocketLocation(ShellControl sc) {
if (sc.isLocal() && sc.getOsType() == OsType.WINDOWS) {
return getPageantWindowsPipe();
return FilePath.of(getPageantWindowsPipe());
}
return null;
}
@Override
public void buildCommand(CommandBuilder builder) {}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
var l = new ArrayList<>(List.of(
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"),
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
new KeyValue("PKCS11Provider", "none")));
var agent = getIdentityAgent(sc);
var agent = determinetAgentSocketLocation(sc);
if (agent != null) {
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
}
@@ -125,7 +116,8 @@ public class PageantStrategy implements SshIdentityStrategy {
private String getPageantWindowsPipe() {
Memory p = new Memory(WinBase.WIN32_FIND_DATA.sizeOf());
var r = Kernel32.INSTANCE.FindFirstFile("\\\\.\\pipe\\*pageant." + AppSystemInfo.ofCurrent().getUser() + "*", p);
var r = Kernel32.INSTANCE.FindFirstFile(
"\\\\.\\pipe\\*pageant." + AppSystemInfo.ofCurrent().getUser() + "*", p);
if (r == WinBase.INVALID_HANDLE_VALUE) {
throw ErrorEventFactory.expected(new IllegalStateException("Pageant is not running"));
}
@@ -136,4 +128,8 @@ public class PageantStrategy implements SshIdentityStrategy {
var file = "\\\\.\\pipe\\" + fd.getFileName();
return file;
}
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -0,0 +1,167 @@
package io.xpipe.app.cred;
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.core.AppI18n;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.pwman.PasswordManagerKeyConfiguration;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@JsonTypeName("passwordManagerAgent")
@Value
@Jacksonized
@Builder
public class PasswordManagerAgentStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<PasswordManagerAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {
var identifier =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getIdentifier() : null);
var pwmanError = Bindings.createObjectBinding(
() -> {
var pwman = AppPrefs.get().passwordManager().getValue();
if (pwman == null) {
return AppI18n.get("passwordManagerEmpty");
}
if (!pwman.supportsKeyConfiguration()) {
return AppI18n.get("passwordManagerNoAgentSupport");
}
if (!pwman.getKeyConfiguration().useAgent()) {
return AppI18n.get("passwordManagerNoAgentConfigured");
}
return null;
},
AppPrefs.get().passwordManager(),
AppI18n.activeLanguage());
var pwmanErrorProp = new SimpleStringProperty();
pwmanErrorProp.bind(pwmanError);
var pwmanDisplay = new HorizontalComp(List.of(
new LabelComp(pwmanErrorProp)
.maxWidth(10000)
.apply(label -> label.setAlignment(Pos.CENTER_LEFT))
.hgrow(),
new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
AppPrefs.get().selectCategory("passwordManager");
})
.padding(new Insets(7))))
.spacing(9);
return new OptionsBuilder()
.nameAndDescription("passwordManagerSshKeyConfig")
.addComp(pwmanDisplay)
.hide(pwmanErrorProp.isNull())
.nameAndDescription(useKeyName() ? "agentKeyName" : "publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, identifier, useKeyName()), identifier)
.disable(pwmanErrorProp.isNotNull())
.nonNull()
.hide(!config.isAllowAgentForward())
.bind(
() -> {
return new PasswordManagerAgentStrategy(identifier.get());
},
p);
}
String identifier;
private static PasswordManagerKeyConfiguration getConfig() {
var pwman = AppPrefs.get().passwordManager().getValue();
return pwman != null
&& pwman.getKeyConfiguration() != null
&& pwman.getKeyConfiguration().useAgent()
? pwman.getKeyConfiguration()
: null;
}
private static boolean useKeyName() {
var config = getConfig();
return config != null && config.supportsAgentKeyNames();
}
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(identifier);
var config = getConfig();
if (config == null) {
throw new ValidationException(AppI18n.get("passwordManagerSshKeysNotSupported"));
}
}
@Override
public void prepareParent(ShellControl parent) throws Exception {
var config = getConfig();
if (config != null) {
var strat = config.getSshIdentityStrategy(null, false);
strat.prepareParent(parent);
}
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl parent) {
var config = getConfig();
return config != null ? FilePath.of(config.getDefaultSocketLocation()) : null;
}
@Override
public void buildCommand(CommandBuilder builder) {
var config = getConfig();
if (config != null) {
var strat = config.getSshIdentityStrategy(null, false);
strat.buildCommand(builder);
}
}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var config = getConfig();
if (config != null) {
var strat = config.getSshIdentityStrategy(getPublicKeyStrategy().retrievePublicKey(), false);
return strat.configOptions(sc);
} else {
return List.of();
}
}
@Override
public PublicKeyStrategy getPublicKeyStrategy() {
if (identifier == null) {
return null;
}
if (!useKeyName()) {
return PublicKeyStrategy.Fixed.of(identifier);
}
return new PublicKeyStrategy.Dynamic(() -> {
return SshAgentKeyList.findAgentIdentity(DataStorage.get().local().ref(), this, identifier)
.toString();
});
}
}
@@ -0,0 +1,83 @@
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.App;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@JsonTypeName("passwordManagerInPlaceKey")
@Value
@Jacksonized
@Builder
public class PasswordManagerInPlaceKeyStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<PasswordManagerInPlaceKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
var options = new OptionsBuilder();
var prefs = AppPrefs.get();
var keyProperty = options.map(p, PasswordManagerInPlaceKeyStrategy::getKey);
var field = new TextFieldComp(keyProperty).apply(struc -> struc.promptTextProperty()
.bind(Bindings.createStringBinding(
() -> {
return prefs.passwordManager().getValue() != null
? prefs.passwordManager().getValue().getKeyPlaceholder()
: "?";
},
prefs.passwordManager())));
var button = new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
AppPrefs.get().selectCategory("passwordManager");
App.getApp().getStage().requestFocus();
});
var content = new InputGroupComp(List.of(field, button));
content.setMainReference(field);
return options.nameAndDescription("passwordManagerInPlaceKeyKey")
.addComp(content, keyProperty)
.nonNull()
.bind(
() -> {
return PasswordManagerInPlaceKeyStrategy.builder()
.key(keyProperty.get())
.build();
},
p);
}
String key;
@Override
public void prepareParent(ShellControl parent) {}
@Override
public void buildCommand(CommandBuilder builder) {}
@Override
public List<KeyValue> configOptions(ShellControl sc) {
return List.of();
}
@Override
public PublicKeyStrategy getPublicKeyStrategy() {
return null;
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl parent) {
return null;
}
}
@@ -0,0 +1,71 @@
package io.xpipe.app.cred;
import io.xpipe.core.FailableSupplier;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Optional;
public interface PublicKeyStrategy {
String retrievePublicKey() throws Exception;
@EqualsAndHashCode
@ToString
final class Fixed implements PublicKeyStrategy {
public static Fixed of(String publicKey) {
return publicKey != null ? new Fixed(publicKey) : null;
}
private final String publicKey;
public Fixed(String publicKey) {
this.publicKey = publicKey;
}
public String get() {
return publicKey;
}
private Optional<String> getFixedPublicKey() {
return Optional.ofNullable(publicKey);
}
@Override
public String retrievePublicKey() {
return getFixedPublicKey().orElseThrow();
}
}
final class Dynamic implements PublicKeyStrategy {
private final FailableSupplier<String> publicKey;
public Dynamic(FailableSupplier<String> publicKey) {
this.publicKey = publicKey;
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof Dynamic;
}
@Override
public String toString() {
return "<dynamic>";
}
@Override
public String retrievePublicKey() throws Exception {
var r = publicKey.get();
return r;
}
}
}
@@ -0,0 +1,101 @@
package io.xpipe.app.cred;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.storage.DataStoreEntryRef;
import lombok.Value;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SshAgentKeyList {
@Value
public static class Entry {
String type;
String publicKey;
String name;
@Override
public String toString() {
return type + " " + publicKey + (name != null ? " " + name : "");
}
}
public static Entry findAgentIdentity(
DataStoreEntryRef<ShellStore> ref, SshIdentityAgentStrategy strategy, String identifier) throws Exception {
var all = listAgentIdentities(ref, strategy);
var list = all.stream()
.filter(entry -> {
return (entry.getName() != null && entry.getName().equalsIgnoreCase(identifier))
|| entry.getPublicKey().equals(identifier)
|| (entry.getType() + " " + entry.getPublicKey()).equals(identifier);
})
.toList();
if (list.isEmpty()) {
var noNames = all.stream().allMatch(entry -> entry.getName() == null);
var isPublicKey = identifier.contains(" ");
if (!isPublicKey) {
try {
Base64.getDecoder().decode(identifier);
isPublicKey = true;
} catch (IllegalArgumentException ignored) {
}
}
if (noNames) {
if (!isPublicKey) {
throw ErrorEventFactory.expected(new IllegalArgumentException("Found no agent identity for name "
+ identifier + " as the agent does not support names. Use a public key instead"));
}
}
throw ErrorEventFactory.expected(new IllegalArgumentException(
"Found no agent identity for " + (isPublicKey ? "public key " : "name ") + identifier));
}
if (list.size() > 1) {
throw ErrorEventFactory.expected(new IllegalArgumentException("Ambiguous agent identities: "
+ list.stream()
.map(entry -> entry.getName() != null ? entry.getName() : entry.toString())
.collect(Collectors.joining(", "))));
}
return list.getFirst();
}
public static List<Entry> listAgentIdentities(DataStoreEntryRef<ShellStore> ref, SshIdentityAgentStrategy strategy)
throws Exception {
var session = ref.getStore().getOrStartSession();
strategy.prepareParent(session);
var socket = strategy.determinetAgentSocketLocation(session);
var out = session.command(CommandBuilder.of()
.add("ssh-add", "-L")
.fixedEnvironment("SSH_AUTH_SOCK", socket != null ? socket.toString() : null))
.readStdoutOrThrow();
var pattern = Pattern.compile("([^ ]+) ([^ ]+)\\s*(?: (.+))?");
var lines = out.lines().toList();
var list = new ArrayList<Entry>();
for (String line : lines) {
var matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
var type = matcher.group(1);
var publicKey = matcher.group(2);
var name = matcher.groupCount() > 2 ? matcher.group(3) : null;
list.add(new Entry(type, publicKey, name));
}
return list;
}
}
@@ -0,0 +1,108 @@
package io.xpipe.app.cred;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
import java.util.List;
public class SshAgentKeyListComp extends SimpleRegionBuilder {
private final ObservableValue<DataStoreEntryRef<ShellStore>> ref;
private final ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy;
private final StringProperty value;
private final boolean useKeyNames;
public SshAgentKeyListComp(
ObservableValue<DataStoreEntryRef<ShellStore>> ref,
ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy,
StringProperty value,
boolean useKeyNames) {
this.ref = ref;
this.sshIdentityStrategy = sshIdentityStrategy;
this.value = value;
this.useKeyNames = useKeyNames;
}
@Override
protected Region createSimple() {
var field = new TextFieldComp(value);
field.apply(struc -> struc.setPromptText(
useKeyNames ? "<name>" : "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== <key comment>"));
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2m-magnify-scan"), null);
button.apply(struc -> {
struc.setOnAction(event -> {
DataStoreEntryRef<ShellStore> refToUse = ref != null && ref.getValue() != null
? ref.getValue()
: DataStorage.get().local().ref();
ThreadHelper.runFailableAsync(() -> {
var list = SshAgentKeyList.listAgentIdentities(refToUse, sshIdentityStrategy.getValue());
Platform.runLater(() -> {
var popover = new Popover();
popover.setArrowLocation(Popover.ArrowLocation.TOP_CENTER);
if (list.size() > 0) {
var content = new VBox();
content.setPadding(new Insets(10));
content.setFillWidth(true);
var header = new Label(AppI18n.get("sshAgentHasKeys"));
header.setPadding(new Insets(0, 0, 8, 8));
content.getChildren().add(header);
for (SshAgentKeyList.Entry entry : list) {
var buttonName = entry.getType() + " "
+ (entry.getName() != null ? entry.getName() : entry.getPublicKey());
var entryButton = new Button(buttonName);
entryButton.setMaxWidth(400);
entryButton.getStyleClass().add(Styles.FLAT);
entryButton.setOnAction(e -> {
value.setValue(
useKeyNames && entry.getName() != null
? entry.getName()
: entry.getType() + " " + entry.getPublicKey());
popover.hide();
e.consume();
});
entryButton.setMinWidth(400);
entryButton.setAlignment(Pos.CENTER_LEFT);
entryButton.setMnemonicParsing(false);
content.getChildren().add(entryButton);
}
popover.setContentNode(content);
} else {
var content = new Label(AppI18n.get("sshAgentNoKeys"));
content.setPadding(new Insets(10));
popover.setContentNode(content);
}
var target = struc.getParent().getChildrenUnmodifiable().getFirst();
popover.show(target);
});
});
event.consume();
});
});
var inputGroup = new InputGroupComp(List.of(field, button));
inputGroup.setMainReference(field);
return inputGroup.build();
}
}
@@ -0,0 +1,84 @@
package io.xpipe.app.cred;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
public class SshAgentTestComp extends SimpleRegionBuilder {
private final Runnable beforeTest;
private final ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy;
public SshAgentTestComp(
Runnable beforeTest, ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy) {
this.beforeTest = beforeTest;
this.sshIdentityStrategy = sshIdentityStrategy;
}
@Override
protected Region createSimple() {
var button = new ButtonComp(AppI18n.observable("test"), new LabelGraphic.IconGraphic("mdi2p-play"), null);
button.padding(new Insets(6, 9, 6, 9));
button.apply(struc -> {
struc.setOnAction(event -> {
DataStoreEntryRef<ShellStore> refToUse =
DataStorage.get().local().ref();
ThreadHelper.runFailableAsync(() -> {
beforeTest.run();
var list = SshAgentKeyList.listAgentIdentities(refToUse, sshIdentityStrategy.getValue());
Platform.runLater(() -> {
var popover = new Popover();
popover.setArrowLocation(Popover.ArrowLocation.LEFT_CENTER);
if (list.size() > 0) {
var content = new VBox();
content.setPadding(new Insets(10));
content.setFillWidth(true);
var header = new Label(AppI18n.get("sshAgentHasKeys"));
header.setPadding(new Insets(0, 0, 8, 8));
content.getChildren().add(header);
for (SshAgentKeyList.Entry entry : list) {
var buttonName = entry.getType() + " "
+ (entry.getName() != null ? entry.getName() : entry.getPublicKey());
var entryButton = new Button(buttonName);
entryButton.setMaxWidth(400);
entryButton.getStyleClass().add(Styles.FLAT);
entryButton.setMinWidth(400);
entryButton.setAlignment(Pos.CENTER_LEFT);
entryButton.setMnemonicParsing(false);
content.getChildren().add(entryButton);
}
popover.setContentNode(content);
} else {
var content = new Label(AppI18n.get("sshAgentNoKeys"));
content.setPadding(new Insets(10));
popover.setContentNode(content);
}
var target = struc;
popover.show(target);
});
});
event.consume();
});
});
return button.build();
}
}
@@ -0,0 +1,11 @@
package io.xpipe.app.cred;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
public interface SshIdentityAgentStrategy extends SshIdentityStrategy {
void prepareParent(ShellControl parent) throws Exception;
FilePath determinetAgentSocketLocation(ShellControl parent) throws Exception;
}
@@ -1,9 +1,10 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.issue.ErrorAction;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.*;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
@@ -45,7 +46,7 @@ public class SshIdentityStateManager {
if (external && !gpgRunning && !opensshRunning) {
throw ErrorEventFactory.expected(new IllegalStateException(
"An external password manager agent is running, but XPipe requested to use another SSH agent. You have to disable the "
"An external password manager agent is running, but XPipe requested to use another SSH agent. You have to disable the other running "
+ "password manager agent first."));
}
@@ -112,15 +113,16 @@ public class SshIdentityStateManager {
var r = c.readStdoutAndStderr();
if (c.getExitCode() != 0) {
var posixMessage = sc.getOsType() != OsType.WINDOWS
? authSock != null
? " and the socket " + authSock
: " and the SSH agent socket in the settings menu"
? authSock != null ? " at the socket location " + authSock : ""
: "";
var ex = new IllegalStateException("Unable to list agent identities via command ssh-add -l:\n" + r[0]
+ "\n"
+ r[1]
+ "\nPlease check your SSH agent configuration%s.".formatted(posixMessage));
var joined = (!r[0].isEmpty() ? r[0] + "\n" : "") + r[1];
var ex = new IllegalStateException("Unable to list agent identities via command ssh-add -l:\n" + joined
+ "\n\n" + "Please check whether the agent is running correctly%s.".formatted(posixMessage));
var eventBuilder = ErrorEventFactory.fromThrowable(ex).expected();
if (joined.toLowerCase().contains("signing failed: agent refused operation")
|| joined.toLowerCase().contains("error connecting to agent: connecting refused")) {
eventBuilder.documentationLink(DocumentationLink.SSH_AGENT_REFUSAL);
}
ErrorEventFactory.preconfigure(eventBuilder);
throw ex;
}
@@ -136,7 +138,7 @@ public class SshIdentityStateManager {
}
}
public static synchronized void prepareLocalExternalAgent() throws Exception {
public static synchronized void prepareLocalExternalAgent(FilePath socket) throws Exception {
if (runningAgent == RunningAgent.EXTERNAL_AGENT) {
return;
}
@@ -149,12 +151,12 @@ public class SshIdentityStateManager {
if (!pipeExists) {
// No agent is running
throw ErrorEventFactory.expected(new IllegalStateException(
"An external password manager agent is set for this connection, but no external SSH agent is running. Make sure that the "
+ "agent is started in your password manager"));
"An external agent is configured, but no external SSH agent is running. Make sure that the external "
+ "agent is started"));
}
}
checkLocalAgentIdentities(null);
checkLocalAgentIdentities(socket != null ? socket.toString() : null);
runningAgent = RunningAgent.EXTERNAL_AGENT;
}
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.issue.ErrorEventFactory;
@@ -11,7 +11,6 @@ import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.ArrayList;
@@ -19,26 +18,15 @@ import java.util.List;
import java.util.Optional;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = NoIdentityStrategy.class),
@JsonSubTypes.Type(value = KeyFileStrategy.class),
@JsonSubTypes.Type(value = InPlaceKeyStrategy.class),
@JsonSubTypes.Type(value = OpenSshAgentStrategy.class),
@JsonSubTypes.Type(value = PageantStrategy.class),
@JsonSubTypes.Type(value = CustomAgentStrategy.class),
@JsonSubTypes.Type(value = GpgAgentStrategy.class),
@JsonSubTypes.Type(value = YubikeyPivStrategy.class),
@JsonSubTypes.Type(value = CustomPkcs11LibraryStrategy.class),
@JsonSubTypes.Type(value = OtherExternalAgentStrategy.class)
})
public interface SshIdentityStrategy {
static List<Class<?>> getSubclasses() {
static List<Class<?>> getAvailable() {
var l = new ArrayList<Class<?>>();
l.add(NoIdentityStrategy.class);
l.add(InPlaceKeyStrategy.class);
l.add(KeyFileStrategy.class);
l.add(OpenSshAgentStrategy.class);
l.add(PasswordManagerAgentStrategy.class);
if (OsType.ofLocal() != OsType.WINDOWS) {
l.add(CustomAgentStrategy.class);
}
@@ -55,6 +43,24 @@ public interface SshIdentityStrategy {
return l;
}
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
l.add(NoIdentityStrategy.class);
l.add(InPlaceKeyStrategy.class);
l.add(KeyFileStrategy.class);
l.add(OpenSshAgentStrategy.class);
l.add(PasswordManagerAgentStrategy.class);
l.add(PasswordManagerInPlaceKeyStrategy.class);
l.add(CustomAgentStrategy.class);
l.add(GpgAgentStrategy.class);
l.add(PageantStrategy.class);
l.add(YubikeyPivStrategy.class);
l.add(CustomPkcs11LibraryStrategy.class);
l.add(OtherExternalAgentStrategy.class);
return l;
}
static Optional<FilePath> getPublicKeyPath(ShellControl sc, String publicKey) throws Exception {
if (publicKey == null || publicKey.isBlank()) {
return Optional.empty();
@@ -97,5 +103,5 @@ public interface SshIdentityStrategy {
return new SecretNoneStrategy();
}
String getPublicKey();
PublicKeyStrategy getPublicKeyStrategy();
}
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity;
package io.xpipe.app.cred;
import io.xpipe.core.FailableSupplier;
@@ -1,4 +1,4 @@
package io.xpipe.ext.base.identity.ssh;
package io.xpipe.app.cred;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.CommandBuilder;
@@ -81,7 +81,7 @@ public class YubikeyPivStrategy implements SshIdentityStrategy {
}
@Override
public String getPublicKey() {
public PublicKeyStrategy getPublicKeyStrategy() {
return null;
}
}
@@ -1,10 +1,16 @@
package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntryRef;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.List;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface DataStore {
List<DataStoreEntryRef<?>> getDependencies();
default boolean isComplete() {
try {
checkComplete();
@@ -0,0 +1,33 @@
package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class DataStoreDependencies {
@SuppressWarnings({"unchecked", "rawtypes"})
public static List<DataStoreEntryRef<?>> of(Object... dependencies) {
var l = new ArrayList<DataStoreEntryRef<?>>();
for (Object dependency : dependencies) {
if (dependency instanceof DataStoreEntryRef<?> r) {
l.add(r);
} else if (dependency instanceof List li) {
l.addAll(li.stream().filter(o -> o != null).toList());
}
}
return l;
}
public static List<DataStoreEntryRef<?>> of(DataStoreEntryRef<?>... dependencies) {
return Arrays.stream(dependencies).filter(Objects::nonNull).toList();
}
public static <T extends DataStore> List<DataStoreEntryRef<?>> of(List<DataStoreEntryRef<T>> refs) {
return refs.stream().filter(Objects::nonNull).collect(Collectors.toList());
}
}
@@ -27,6 +27,10 @@ import java.util.UUID;
public interface DataStoreProvider {
default boolean allowCreation() {
return true;
}
default boolean showIncompleteInfo() {
return false;
}
@@ -0,0 +1,10 @@
package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.List;
public interface DependentDataStore extends DataStore {
List<DataStoreEntryRef<?>> getDependencies();
}
@@ -2,10 +2,17 @@ package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.List;
public interface GroupStore<T extends DataStore> extends DataStore {
DataStoreEntryRef<? extends T> getParent();
@Override
default List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(getParent());
}
@Override
default void checkComplete() throws Throwable {
var p = getParent();
@@ -7,6 +7,8 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import java.util.List;
@JsonTypeName("local")
@Value
public class LocalStore implements NetworkTunnelStore, ShellStore, StatefulDataStore<ShellStoreState> {
@@ -39,4 +41,9 @@ public class LocalStore implements NetworkTunnelStore, ShellStore, StatefulDataS
public NetworkTunnelSession createTunnelSession(int localPort, int remotePort, String address) {
return null;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
}
@@ -0,0 +1,34 @@
package io.xpipe.app.ext;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@Getter
@EqualsAndHashCode(callSuper = true)
@SuperBuilder(toBuilder = true)
@Jacksonized
public class NetworkContainerStoreState extends ContainerStoreState {
String ipv4;
String ipv6;
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
var n = (NetworkContainerStoreState) newer;
var b = toBuilder();
mergeBuilder(n, b);
return b.build();
}
protected void mergeBuilder(
NetworkContainerStoreState css, NetworkContainerStoreState.NetworkContainerStoreStateBuilder<?, ?> b) {
super.mergeBuilder(css, b);
b.ipv4 = css.ipv4;
b.ipv6 = css.ipv6;
}
}
@@ -10,7 +10,6 @@ import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.secret.SecretRetrievalStrategy;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.vnc.VncBaseStore;
import io.xpipe.core.FilePath;
import io.xpipe.core.SecretValue;
import javafx.beans.property.Property;
@@ -79,6 +78,4 @@ public abstract class ProcessControlProvider {
public abstract void cloneRepository(String url, Path target) throws Exception;
public abstract void pullRepository(Path target) throws Exception;
public abstract void checkSshAgent(ShellControl sc, FilePath agent) throws Exception;
}
@@ -14,18 +14,7 @@ public abstract class InitHubLeafProvider<T extends DataStore, O> implements Hub
public void init() {
ThreadHelper.runFailableAsync(() -> {
available = check();
// Update entries to potentially show item
if (available != null) {
StoreViewState.get().getAllEntries().getList().stream()
.filter(w -> w.getValidity().getValue().isUsable())
.forEach(w -> {
if (getApplicableClass()
.isAssignableFrom(w.getStore().getValue().getClass())) {
w.update();
}
});
}
StoreViewState.get().updateWrappers();
});
}
@@ -38,6 +38,10 @@ public class ScanHubBatchProvider implements BatchHubProvider<ShellStore> {
@Override
public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {
if (!o.get().getProvider().shouldShowScan()) {
return false;
}
var state = o.get().getStorePersistentState();
if (state instanceof SystemState systemState) {
return (systemState.getShellDialect() == null
@@ -27,6 +27,11 @@ public class ScanHubLeafProvider implements HubLeafProvider<ShellStore> {
return true;
}
@Override
public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {
return o.get().getProvider().shouldShowScan();
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {
return AppI18n.observable("scanConnections");
@@ -1,6 +1,7 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.platform.PlatformThread;
import javafx.beans.property.Property;
@@ -15,7 +16,7 @@ import lombok.Value;
import java.util.function.Predicate;
public class DataStoreCategoryChoiceComp extends SimpleRegionBuilder {
public class StoreCategoryChoiceComp extends SimpleRegionBuilder {
private final StoreCategoryWrapper root;
private final Property<StoreCategoryWrapper> external;
@@ -23,12 +24,12 @@ public class DataStoreCategoryChoiceComp extends SimpleRegionBuilder {
private final boolean applyExternalInitially;
private final Predicate<StoreCategoryWrapper> filter;
public DataStoreCategoryChoiceComp(
public StoreCategoryChoiceComp(
StoreCategoryWrapper root,
Property<StoreCategoryWrapper> external,
Property<StoreCategoryWrapper> value,
boolean applyExternalInitially, Predicate<StoreCategoryWrapper> filter
) {
boolean applyExternalInitially,
Predicate<StoreCategoryWrapper> filter) {
this.root = root;
this.external = external;
this.value = value;
@@ -57,7 +58,10 @@ public class DataStoreCategoryChoiceComp extends SimpleRegionBuilder {
if (!applyExternalInitially) {
value.setValue(last);
}
var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root).filtered(filter).getList());
var box = new ComboBox<>(StoreViewState.get()
.getSortedCategories(root, true)
.filtered(filter)
.getList());
box.setValue(value.getValue());
box.valueProperty().addListener((observable, oldValue, newValue) -> {
value.setValue(newValue);
@@ -83,9 +87,12 @@ public class DataStoreCategoryChoiceComp extends SimpleRegionBuilder {
super.updateItem(w, empty);
textProperty().unbind();
if (w != null) {
setGraphic(PrettyImageHelper.ofFixedSizeSquare(w.getIconFile().getValue(), 16)
.build());
textProperty().bind(w.getShownName());
setPadding(new Insets(6, 6, 6, 8 + (indent ? w.getDepth() * 8 : 0)));
} else {
setGraphic(null);
setText("None");
}
}
@@ -22,10 +22,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
@@ -37,6 +34,7 @@ import lombok.EqualsAndHashCode;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
@@ -46,6 +44,7 @@ import java.util.Locale;
public class StoreCategoryComp extends SimpleRegionBuilder {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
StoreCategoryWrapper category;
@@ -136,20 +135,25 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
var count = new CountComp(
category.getShownContainedEntriesCount(),
category.getAllContainedEntriesCount(),
string -> "(" + string + ")");
string -> "[" + string + "]");
count.style("count");
count.hide(Bindings.equal(0, category.getShownContainedEntriesCount()));
count.minWidth(Region.USE_PREF_SIZE);
var iconButton = new StoreCategoryIconComp(category, 16);
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSync()))
.or(showing)
.or(focus);
var h = new HorizontalComp(List.of(
expandButton,
RegionBuilder.hspacer(category.getCategory().getParentCategory() == null ? 3 : 0),
RegionBuilder.hspacer(2),
iconButton,
RegionBuilder.hspacer(4),
RegionBuilder.of(() -> name).hgrow(),
RegionBuilder.hspacer(2),
count,
RegionBuilder.hspacer(7),
RegionBuilder.hspacer(9),
statusButton.hide(showStatus.not())));
h.padding(new Insets(0, 10, 0, (category.getDepth() * 10)));
@@ -193,6 +197,7 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
var v = new VerticalComp(List.of(categoryButton, children.hide(hide)));
v.style("category");
v.apply(struc -> {
struc.pseudoClassStateChanged(ROOT, category.getCategory().getParentCategory() == null);
StoreViewState.get().getActiveCategory().subscribe(val -> {
struc.pseudoClassStateChanged(SELECTED, val.equals(category));
});
@@ -226,6 +231,7 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
newCategory.setOnAction(event -> {
StoreViewState.get().createNewCategory(category);
});
newCategory.setDisable(!DataStorage.get().canCreateStoreCategoryWithin(category.getCategory()));
contextMenu.getItems().add(newCategory);
contextMenu.getItems().add(new SeparatorMenuItem());
@@ -243,18 +249,48 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
});
contextMenu.getItems().add(rename);
var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill"));
var none = new MenuItem();
none.textProperty().bind(AppI18n.observable("none"));
none.setOnAction(event -> {
category.getCategory().setConfig(category.getCategory().getConfig().withColor(null));
event.consume();
});
none.setGraphic(DataStoreColor.createDisplayGraphic(null));
color.getItems().add(none);
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
MenuItem m = new MenuItem();
m.textProperty().bind(AppI18n.observable(dataStoreColor.getId()));
m.setOnAction(event -> {
category.getCategory()
.setConfig(category.getCategory().getConfig().withColor(dataStoreColor));
event.consume();
});
m.setGraphic(DataStoreColor.createDisplayGraphic(dataStoreColor));
color.getItems().add(m);
});
contextMenu.getItems().add(color);
contextMenu.getItems().add(new SeparatorMenuItem());
if (category.canMove()) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get()
.getSortedCategories(getCategory().getRoot())
.getSortedCategories(getCategory().getRoot(), true)
.getList()
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem();
m.textProperty()
.setValue(" ".repeat(storeCategoryWrapper.getDepth())
+ storeCategoryWrapper.getName().getValue());
var m = new CustomMenuItem();
var l = new Label();
l.setGraphic(PrettyImageHelper.ofFixedSizeSquare(
storeCategoryWrapper.getIconFile().getValue(), 16)
.padding(new Insets(0, 0, 1, 0))
.build());
l.setText(storeCategoryWrapper.getName().getValue());
l.setPadding(new Insets(0, 1, 1, storeCategoryWrapper.getDepth() * 10));
m.setContent(l);
l.prefWidthProperty().bind(contextMenu.widthProperty().subtract(40));
m.setOnAction(event -> {
category.moveToParent(storeCategoryWrapper.getCategory());
event.consume();
@@ -1,28 +1,26 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.ChoiceComp;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.comp.base.ToggleGroupComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategoryConfig;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ListCell;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import java.util.LinkedHashMap;
import java.util.function.Supplier;
@AllArgsConstructor
public class StoreCategoryConfigComp extends SimpleRegionBuilder {
@@ -43,16 +41,25 @@ public class StoreCategoryConfigComp extends SimpleRegionBuilder {
modal.show();
}
private RegionBuilder<?> createToggle(Property<Boolean> prop, Boolean inherited) {
var map = new LinkedHashMap<Boolean, ObservableValue<String>>();
map.put(Boolean.FALSE, AppI18n.observable("no"));
map.put(
null,
AppI18n.observable("inherit", inherited != null && inherited ? AppI18n.get("yes") : AppI18n.get("no")));
map.put(Boolean.TRUE, AppI18n.observable("yes"));
var comp = new ToggleGroupComp<>(prop, new SimpleObjectProperty<>(map));
return comp;
}
@Override
protected Region createSimple() {
var colors = new LinkedHashMap<DataStoreColor, ObservableValue<String>>();
colors.put(null, AppI18n.observable("none"));
for (DataStoreColor value : DataStoreColor.values()) {
colors.put(value, AppI18n.observable(value.getId()));
}
var parents = DataStorage.get().getCategoryParentHierarchy(wrapper.getCategory());
var parentConfig = parents.size() > 1
? DataStorage.get().getEffectiveCategoryConfig(parents.get(parents.size() - 2))
: DataStoreCategoryConfig.empty();
var c = config.getValue();
var color = new SimpleObjectProperty<>(c.getColor());
var scripts = new SimpleObjectProperty<>(c.getDontAllowScripts());
var confirm = new SimpleObjectProperty<>(c.getConfirmAllModifications());
var sync = new SimpleObjectProperty<>(c.getSync());
@@ -66,52 +73,28 @@ public class StoreCategoryConfigComp extends SimpleRegionBuilder {
: null);
var connectionsCategory = wrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory());
var colorChoice = new ChoiceComp<>(color, colors, false);
colorChoice.apply(struc -> {
Supplier<ListCell<DataStoreColor>> cell = () -> new ListCell<>() {
@Override
protected void updateItem(DataStoreColor color, boolean empty) {
super.updateItem(color, empty);
if (color == null) {
setText(AppI18n.get("none"));
setGraphic(DataStoreColor.createDisplayGraphic(null));
return;
}
setText(AppI18n.get(color.getId()));
setGraphic(DataStoreColor.createDisplayGraphic(color));
}
};
struc.setButtonCell(cell.get());
struc.setCellFactory(ignored -> {
return cell.get();
});
});
var options = new OptionsBuilder();
var specialCategorySync = !wrapper.getCategory().canShare();
var syncDisable = !DataStorage.get().supportsSync()
|| ((sync.getValue() == null || !sync.getValue())
&& !wrapper.getCategory().canShare());
var syncHide = !DataStorage.get().supportsSync();
options.name(
specialCategorySync
? AppI18n.observable(
"categorySyncSpecial", wrapper.getName().getValue())
: AppI18n.observable("categorySync"))
.description("categorySyncDescription")
.addYesNoToggle(sync)
.addComp(createToggle(sync, parentConfig.getSync()), sync)
.disable(syncDisable)
.hide(syncHide)
.nameAndDescription("categoryDontAllowScripts")
.addYesNoToggle(scripts)
.addComp(createToggle(scripts, parentConfig.getDontAllowScripts()), scripts)
.hide(!connectionsCategory)
.nameAndDescription("categoryConfirmAllModifications")
.addYesNoToggle(confirm)
.addComp(createToggle(confirm, parentConfig.getConfirmAllModifications()), confirm)
.hide(!connectionsCategory)
.nameAndDescription("categoryFreeze")
.addYesNoToggle(freeze)
.addComp(createToggle(freeze, parentConfig.getFreezeConfigurations()), freeze)
.hide(!connectionsCategory)
.nameAndDescription("categoryDefaultIdentity")
.addComp(
@@ -123,12 +106,10 @@ public class StoreCategoryConfigComp extends SimpleRegionBuilder {
StoreViewState.get().getAllIdentitiesCategory()),
ref)
.hide(!connectionsCategory)
.nameAndDescription("categoryColor")
.addComp(colorChoice, color)
.bind(
() -> {
return new DataStoreCategoryConfig(
color.get(),
c.getColor(),
scripts.get(),
confirm.get(),
sync.get(),
@@ -0,0 +1,58 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.PrettyImageHelper;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.AllArgsConstructor;
import org.kordamp.ikonli.javafx.FontIcon;
@AllArgsConstructor
public class StoreCategoryIconComp extends SimpleRegionBuilder {
private final StoreCategoryWrapper wrapper;
private final int size;
@Override
protected Region createSimple() {
var imageComp = PrettyImageHelper.ofFixedSize(wrapper.getIconFile(), size, size);
var storeIcon = imageComp.build();
storeIcon.setPadding(new Insets(0, 0, 1, 0));
var dots = new FontIcon("mdi2d-dots-horizontal");
dots.setIconSize((int) (size * 1.1));
var stack = new StackPane(storeIcon, dots);
stack.setMinHeight(size);
stack.setMinWidth(size);
stack.setMaxHeight(size);
stack.setMaxWidth(size);
stack.getStyleClass().add("icon");
stack.setAlignment(Pos.CENTER);
dots.visibleProperty().bind(stack.hoverProperty());
storeIcon
.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
return stack.isHover() ? 0.5 : 1.0;
},
stack.hoverProperty()));
stack.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
StoreIconChoiceDialog.show(wrapper.getCategory());
event.consume();
}
});
return stack;
}
}
@@ -36,7 +36,7 @@ public class StoreCategoryWrapper {
private final IntegerProperty allContainedEntriesCount = new SimpleIntegerProperty();
private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<DataStoreColor> color = new SimpleObjectProperty<>();
private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();
private final Property<String> iconFile = new SimpleObjectProperty<>();
private final Trigger<Void> renameTrigger = Trigger.of();
private StoreCategoryWrapper cachedParent;
@@ -100,6 +100,16 @@ public class StoreCategoryWrapper {
return cachedParent;
}
public boolean isHierarchyExpanded() {
StoreCategoryWrapper current = this;
while ((current = current.getParent()) != null) {
if (!current.getExpanded().get()) {
return false;
}
}
return true;
}
public void select() {
PlatformThread.runLaterIfNeeded(() -> {
StoreViewState.get().getActiveCategory().setValue(this);
@@ -152,12 +162,17 @@ public class StoreCategoryWrapper {
this.expanded.set(!expanded.getValue());
}
public void update() {
public synchronized void update() {
// We are probably in shutdown then
if (StoreViewState.get() == null) {
return;
}
// We received a delayed update after removal
if (!DataStorage.get().getStoreCategories().contains(category)) {
return;
}
// Avoid reupdating name when changed from the name property!
var catName = translatedName(category.getName());
if (!catName.equals(name.getValue())) {
@@ -169,6 +184,7 @@ public class StoreCategoryWrapper {
DataStorage.get().getEffectiveCategoryConfig(category).getSync()));
expanded.setValue(category.isExpanded());
color.setValue(DataStorage.get().getEffectiveCategoryConfig(category).getColor());
iconFile.setValue(category.getEffectiveIconFile());
var allEntries = new ArrayList<>(StoreViewState.get().getAllEntries().getList());
directContainedEntries.setContent(allEntries.stream()
@@ -191,15 +207,9 @@ public class StoreCategoryWrapper {
.sum();
allContainedEntriesCount.setValue(direct + sub);
var performanceCount =
AppPrefs.get().showChildCategoriesInParentCategory().get() ? allContainedEntriesCount.get() : direct;
if (performanceCount > 500) {
largeCategoryOptimizations.setValue(true);
}
var directFiltered = directContainedEntries.getList().stream()
.filter(storeEntryWrapper -> {
var filter = StoreViewState.get().getFilterString().getValue();
var filter = StoreFilterState.get().getEffectiveFilter().getValue();
if (filter != null) {
var matches = storeEntryWrapper.matchesFilter(filter);
return matches;
@@ -217,6 +227,8 @@ public class StoreCategoryWrapper {
Optional.ofNullable(getParent()).ifPresent(storeCategoryWrapper -> {
storeCategoryWrapper.update();
});
StoreViewState.get().refreshActiveCategory();
}
private String translatedName(String original) {
@@ -18,7 +18,6 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
@@ -82,7 +81,7 @@ public class StoreChoicePopover<T extends DataStore> {
: (rootCategory != null
? (rootCategory.getRoot().equals(cur.getRoot()) ? cur : rootCategory)
: cur));
var filterText = new SimpleStringProperty();
var storeFilter = new SimpleObjectProperty<StoreFilter>();
popover = new Popover();
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
var e = storeEntryWrapper.getEntry();
@@ -120,7 +119,7 @@ public class StoreChoicePopover<T extends DataStore> {
StoreViewState.get().getAllEntries(),
Set.of(),
applicable,
filterText,
storeFilter,
selectedCategory,
StoreViewState.get().getEntriesListVisibilityObservable(),
StoreViewState.get().getEntriesListUpdateObservable(),
@@ -138,14 +137,16 @@ public class StoreChoicePopover<T extends DataStore> {
},
initialExpanded);
var category = new DataStoreCategoryChoiceComp(
var category = new StoreCategoryChoiceComp(
rootCategory != null ? rootCategory.getRoot() : null,
StoreViewState.get().getActiveCategory(),
selectedCategory,
explicitCategory == null,
ignored -> true)
ignored -> true)
.style(Styles.LEFT_PILL);
var filter = new FilterComp(filterText).style(Styles.CENTER_PILL).hgrow();
var filter = FilterComp.ofStoreFilter(storeFilter)
.style(Styles.CENTER_PILL)
.hgrow();
var addButton = RegionBuilder.of(() -> {
var m = MenuHelper.createMenuButton();
@@ -204,7 +205,7 @@ public class StoreChoicePopover<T extends DataStore> {
content.setFillWidth(true);
content.getStyleClass().add("choice-comp-content");
content.setPrefWidth(480);
content.setMaxHeight(550);
content.setMaxHeight(430);
popover.setContentNode(content);
popover.setCloseButtonEnabled(true);
@@ -44,7 +44,6 @@ public class StoreComboChoiceComp<T extends DataStore> extends SimpleRegionBuild
private final Property<ComboValue<T>> selected;
private final Function<T, String> stringConverter;
private final StoreChoicePopover<T> popover;
private final boolean requireComplete;
public StoreComboChoiceComp(
Function<T, String> stringConverter,
@@ -56,7 +55,6 @@ public class StoreComboChoiceComp<T extends DataStore> extends SimpleRegionBuild
boolean requireComplete) {
this.stringConverter = stringConverter;
this.selected = selected;
this.requireComplete = requireComplete;
var popoverProp = new SimpleObjectProperty<>(
selected.getValue() != null ? selected.getValue().getRef() : null);
@@ -88,7 +88,6 @@ public class StoreCreationComp extends ModalOverlayContentComp {
});
}
var propOptions = createStoreProperties();
model.getInitialStore().setValue(model.getStore().getValue());
var valSp = new GraphicDecorationStackPane();
@@ -103,7 +102,11 @@ public class StoreCreationComp extends ModalOverlayContentComp {
}
full.sub(d.getOptions());
full.sub(propOptions);
if (!model.isQuickConnect()) {
var propOptions = createStoreProperties();
full.sub(propOptions);
}
var comp = full.buildComp();
var region = comp.style("store-creator-options").build();
@@ -26,17 +26,24 @@ import java.util.function.Predicate;
public class StoreCreationDialog {
public static void showEdit(DataStoreEntry e) {
showEdit(e, dataStoreEntry -> {});
public static StoreCreationModel showEdit(DataStoreEntry e) {
return showEdit(e, dataStoreEntry -> {});
}
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> c) {
showEdit(e, e.getStore(), c);
public static StoreCreationModel showEdit(DataStoreEntry e, Consumer<DataStoreEntry> c) {
return showEdit(e, e.getStore(), true, c);
}
public static void showEdit(DataStoreEntry e, DataStore base, Consumer<DataStoreEntry> c) {
public static StoreCreationModel showEdit(
DataStoreEntry e, DataStore base, boolean addToStorage, Consumer<DataStoreEntry> c) {
StoreCreationConsumer consumer = (newE, validated) -> {
ThreadHelper.runAsync(() -> {
if (!addToStorage) {
DataStorage.get().updateEntry(e, newE);
c.accept(e);
return;
}
if (!DataStorage.get().getStoreEntries().contains(e)
|| DataStorage.get().getEffectiveReadOnlyState(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE);
@@ -48,14 +55,13 @@ public class StoreCreationDialog {
var madeValid = !e.getValidity().isUsable()
&& newE.getValidity().isUsable();
DataStorage.get().updateEntry(e, newE);
if (madeValid) {
if (validated
&& e.getProvider().shouldShowScan()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanDialog.showSingleAsync(e);
}
if (madeValid
&& validated
&& e.getProvider().shouldShowScan()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanDialog.showSingleAsync(e);
}
}
}
@@ -72,11 +78,11 @@ public class StoreCreationDialog {
c.accept(e);
});
};
show(e.getName(), DataStoreProviders.byStore(base), base, v -> true, consumer, true, e);
return show(e.getName(), DataStoreProviders.byStore(base), base, v -> true, consumer, true, e);
}
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
showCreation(
public static StoreCreationModel showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
return showCreation(
null,
selected != null ? selected.defaultStore(DataStorage.get().getSelectedCategory()) : null,
category,
@@ -84,7 +90,7 @@ public class StoreCreationDialog {
true);
}
public static void showCreation(
public static StoreCreationModel showCreation(
String name,
DataStore base,
DataStoreCreationCategory category,
@@ -118,18 +124,20 @@ public class StoreCreationDialog {
ErrorEventFactory.fromThrowable(ex).handle();
}
};
show(
return show(
name,
prov,
base,
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
dataStoreProvider -> (category != null
&& dataStoreProvider.allowCreation()
&& category.equals(dataStoreProvider.getCreationCategory()))
|| dataStoreProvider.equals(prov),
consumer,
false,
null);
}
private static void show(
private static StoreCreationModel show(
String initialName,
DataStoreProvider provider,
DataStore s,
@@ -140,7 +148,7 @@ public class StoreCreationDialog {
var ex = StoreCreationQueueEntry.findExisting(existingEntry);
if (ex.isPresent()) {
ex.get().execute();
return;
return null;
}
var prop = new SimpleObjectProperty<>(provider);
@@ -148,14 +156,14 @@ public class StoreCreationDialog {
var model = new StoreCreationModel(prop, store, filter, initialName, existingEntry, staticDisplay, con);
var modal = createModalOverlay(model);
modal.show();
return model;
}
private static ModalOverlay createModalOverlay(StoreCreationModel model) {
var comp = new StoreCreationComp(model);
comp.prefWidth(650);
var nameKey = model.storeTypeNameKey() + "Add";
var nameKey = model.isQuickConnect() ? "quickConnect" : model.storeTypeNameKey() + "Add";
var modal = ModalOverlay.of(nameKey, comp);
var queueEntry = StoreCreationQueueEntry.of(model, modal);
comp.apply(struc -> {
struc.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
@@ -167,16 +175,21 @@ public class StoreCreationDialog {
}
});
});
modal.hideable(queueEntry);
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
if (model.getFinished().get() || !modal.isShowing()) {
return;
}
modal.hide();
AppLayoutModel.get().getQueueEntries().add(queueEntry);
});
modal.setRequireCloseButtonForClose(true);
if (!model.isQuickConnect()) {
var queueEntry = StoreCreationQueueEntry.of(model, modal);
modal.hideable(queueEntry);
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
if (model.getFinished().get() || !modal.isShowing()) {
return;
}
modal.hide();
AppLayoutModel.get().getQueueEntries().add(queueEntry);
});
modal.setRequireCloseButtonForClose(true);
}
var loadingLabel = new LabelComp(Bindings.createStringBinding(
() -> {
return model.getBusy().get() ? AppI18n.get("testingConnection") : null;
@@ -205,21 +218,23 @@ public class StoreCreationDialog {
.augment(button -> {
button.visibleProperty().bind(Bindings.not(model.canConnect()));
}));
modal.addButton(new ModalButton(
"skip",
() -> {
model.commit(false);
modal.close();
},
false,
false))
.augment(button -> {
button.visibleProperty().bind(model.getSkippable());
button.disableProperty().bind(model.getBusy());
});
if (!model.isQuickConnect()) {
modal.addButton(new ModalButton(
"skip",
() -> {
model.commit(false);
modal.close();
},
false,
false))
.augment(button -> {
button.visibleProperty().bind(model.getSkippable());
button.disableProperty().bind(model.getBusy());
});
}
modal.addButton(new ModalButton(
"finish",
model.isQuickConnect() ? "connect" : "finish",
() -> {
model.finish();
},
@@ -239,7 +254,9 @@ public class StoreCreationDialog {
button.textProperty()
.bind(Bindings.createStringBinding(
() -> {
return !model.getBusy().get() ? AppI18n.get("finish") : null;
return !model.getBusy().get()
? AppI18n.get(model.isQuickConnect() ? "connect" : "finish")
: null;
},
PlatformThread.sync(model.getBusy()),
AppI18n.activeLanguage()));
@@ -54,6 +54,10 @@ public class StoreCreationMenu {
menu.getItems().add(categoryMenu("addDesktop", "mdi2c-camera-plus", DataStoreCreationCategory.DESKTOP, null));
menu.getItems().add(cloudMenu());
menu.getItems().add(new SeparatorMenuItem());
menu.getItems()
.add(categoryMenu(
"addIdentity",
@@ -61,8 +65,6 @@ public class StoreCreationMenu {
DataStoreCreationCategory.IDENTITY,
"localIdentity"));
menu.getItems().add(cloudMenu());
menu.getItems().add(new SeparatorMenuItem());
menu.getItems()
@@ -72,13 +74,6 @@ public class StoreCreationMenu {
.add(categoryMenu(
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, "sshLocalTunnel"));
menu.getItems()
.add(categoryMenu(
"addFileSystem",
"mdi2f-folder-plus-outline",
DataStoreCreationCategory.FILE_SYSTEM,
"genericS3Bucket"));
menu.getItems().add(new SeparatorMenuItem());
menu.getItems()
@@ -104,6 +99,13 @@ public class StoreCreationMenu {
});
actionMenu.getItems().addFirst(item);
menu.getItems()
.add(categoryMenu(
"addFileSystem",
"mdi2f-folder-plus-outline",
DataStoreCreationCategory.FILE_SYSTEM,
"genericS3Bucket"));
menu.getItems().add(categoryMenu("addSerial", "mdi2s-serial-port", DataStoreCreationCategory.SERIAL, "serial"));
menu.getItems().add(new SeparatorMenuItem());
@@ -153,7 +155,7 @@ public class StoreCreationMenu {
});
int lastOrder = providers.getFirst().getOrderPriority();
for (io.xpipe.app.ext.DataStoreProvider dataStoreProvider : providers) {
for (var dataStoreProvider : providers) {
if (dataStoreProvider.getOrderPriority() != lastOrder) {
menu.getItems().add(new SeparatorMenuItem());
lastOrder = dataStoreProvider.getOrderPriority();
@@ -167,6 +169,7 @@ public class StoreCreationMenu {
StoreCreationDialog.showCreation(dataStoreProvider, category);
event.consume();
});
item.setDisable(!dataStoreProvider.allowCreation());
menu.getItems().add(item);
}
return menu;
@@ -95,7 +95,9 @@ public class StoreCreationModel {
DataStorage.get().getSelectedCategory().getUuid(),
name.getValue(),
store.getValue());
var entryRef = existingEntry != null ? existingEntry : DataStorage.get().getDefaultDisplayParent(initial).orElse(initial);
var entryRef = existingEntry != null
? existingEntry
: DataStorage.get().getDefaultDisplayParent(initial).orElse(initial);
var targetCategory = getTargetCategory(entryRef);
return DataStoreEntry.createNew(
UUID.randomUUID(), targetCategory.getUuid(), name.getValue(), store.getValue());
@@ -167,6 +169,10 @@ public class StoreCreationModel {
store));
}
boolean isQuickConnect() {
return existingEntry != null && existingEntry.getUuid().equals(StoreQuickConnect.STORE_ID);
}
void connect() {
var temp = entry.getValue() != null ? entry.getValue() : DataStoreEntry.createTempWrapper(store.getValue());
var action = OpenHubMenuLeafProvider.Action.builder().ref(temp.ref()).build();
@@ -211,7 +217,7 @@ public class StoreCreationModel {
}
// We didn't change anything
if (store.getValue().isComplete() && !wasChanged()) {
if (store.getValue().isComplete() && !wasChanged() && !isQuickConnect()) {
commit(false);
return;
}
@@ -307,7 +313,7 @@ public class StoreCreationModel {
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
? "connection"
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
? (p.getId().equals("scriptGroup") ? "scriptGroup" : "script")
? "script"
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_IDENTITIES_CATEGORY_UUID)
? "identity"
: "macro";
@@ -66,7 +66,8 @@ public class StoreEntryBatchSelectComp extends SimpleRegionBuilder {
}
private void externalUpdate(CheckBox checkBox) {
if (section.getWrapper() != null && section.getWrapper().getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
if (section.getWrapper() != null
&& section.getWrapper().getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
checkBox.setSelected(false);
checkBox.setIndeterminate(false);
checkBox.setDisable(true);
@@ -304,7 +304,7 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
}
protected BaseRegionBuilder<?, ?> createIcon(int w, int h, Consumer<Node> fontSize) {
var icon = new StoreIconComp(getWrapper(), w, h);
var icon = new StoreEntryIconComp(getWrapper(), w, h);
icon.apply(struc -> {
struc.opacityProperty()
.bind(Bindings.createDoubleBinding(
@@ -644,13 +644,25 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
var move = new Menu(AppI18n.get("category"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get()
.getSortedCategories(
getWrapper().getCategory().getValue().getRoot())
getWrapper().getCategory().getValue().getRoot(), true)
.getList()
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem();
m.textProperty()
.setValue(" ".repeat(storeCategoryWrapper.getDepth())
+ storeCategoryWrapper.getName().getValue());
var m = new CustomMenuItem();
var l = new Label();
l.setGraphic(PrettyImageHelper.ofFixedSizeSquare(
storeCategoryWrapper
.getIconFile()
.getValue(),
16)
.padding(new Insets(0, 0, 1, 0))
.build());
l.setText(storeCategoryWrapper.getName().getValue());
l.setPadding(new Insets(0, 1, 1, storeCategoryWrapper.getDepth() * 10));
m.setContent(l);
l.prefWidthProperty()
.bind(contextMenu.widthProperty().subtract(40));
m.setOnAction(event -> {
getWrapper().moveTo(storeCategoryWrapper.getCategory());
event.consume();
@@ -0,0 +1,23 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.util.ObservableSubscriber;
import javafx.scene.layout.Region;
public class StoreEntryFilterCompBar extends SimpleRegionBuilder {
private final ObservableSubscriber filterTrigger;
public StoreEntryFilterCompBar(ObservableSubscriber filterTrigger) {
this.filterTrigger = filterTrigger;
}
@Override
public Region createSimple() {
var bar = new StoreFilterFieldComp(filterTrigger);
bar.style("bar");
bar.style("store-filter-bar");
return bar.build();
}
}
@@ -16,7 +16,7 @@ import lombok.AllArgsConstructor;
import org.kordamp.ikonli.javafx.FontIcon;
@AllArgsConstructor
public class StoreIconComp extends SimpleRegionBuilder {
public class StoreEntryIconComp extends SimpleRegionBuilder {
private final StoreEntryWrapper wrapper;
private final int w;
@@ -52,7 +52,7 @@ public class StoreEntryListComp extends SimpleRegionBuilder {
});
// Reset scroll
StoreViewState.get().getFilterString().addListener((observable, oldValue, newValue) -> {
StoreFilterState.get().getEffectiveFilter().addListener((observable, oldValue, newValue) -> {
struc.setVvalue(0);
});
@@ -181,6 +181,6 @@ public class StoreEntryListComp extends SimpleRegionBuilder {
map.put(new StoreScriptSourcesIntroComp(), showScriptSourcesIntro);
map.put(new StoreIdentitiesIntroComp(), showIdentitiesIntro);
return new MultiContentComp(false, map, false).build();
return new MultiContentComp(false, map).build();
}
}
@@ -4,7 +4,6 @@ import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.CountComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
@@ -22,7 +21,6 @@ import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
@@ -34,13 +32,9 @@ import java.util.function.Function;
public class StoreEntryListOverviewComp extends SimpleRegionBuilder {
private final ObservableSubscriber filterTrigger;
private final ObservableSubscriber quickConnectTrigger = new ObservableSubscriber();
public StoreEntryListOverviewComp(ObservableSubscriber filterTrigger) {
this.filterTrigger = filterTrigger;
}
private Region createGroupListHeader() {
private Region createHeaderBar() {
var label = new Label();
var name = BindingsHelper.flatMap(
StoreViewState.get().getActiveCategory(),
@@ -77,11 +71,11 @@ public class StoreEntryListOverviewComp extends SimpleRegionBuilder {
label,
c,
RegionBuilder.hspacer().build(),
createIndexSortButton().build(),
sep,
createDateSortButton().build(),
RegionBuilder.hspacer(2).build(),
createAlphabeticalSortButton().build());
createAlphabeticalSortButton().build(),
sep,
createIndexSortButton().build());
if (OsType.ofLocal() == OsType.MACOS) {
AppFontSizes.xxxl(label);
AppFontSizes.xxxl(c);
@@ -94,28 +88,20 @@ public class StoreEntryListOverviewComp extends SimpleRegionBuilder {
return topBar;
}
private Region createGroupListFilter() {
var filter = new FilterComp(StoreViewState.get().getFilterString()).build();
filterTrigger.subscribe(() -> {
filter.requestFocus();
});
private Region createAddBar() {
var add = createAddButton();
var batchMode = createBatchModeButton().build();
var hbox = new HBox(add, filter, batchMode);
filter.minHeightProperty().bind(add.heightProperty());
filter.prefHeightProperty().bind(add.heightProperty());
filter.maxHeightProperty().bind(add.heightProperty());
var hbox = new HBox(add, batchMode);
batchMode.minHeightProperty().bind(add.heightProperty());
batchMode.prefHeightProperty().bind(add.heightProperty());
batchMode.maxHeightProperty().bind(add.heightProperty());
batchMode.minWidthProperty().bind(add.heightProperty());
batchMode.prefWidthProperty().bind(add.heightProperty());
batchMode.maxWidthProperty().bind(add.heightProperty());
hbox.setSpacing(8);
hbox.setAlignment(Pos.CENTER);
HBox.setHgrow(filter, Priority.ALWAYS);
filter.getStyleClass().add("filter-bar");
hbox.setSpacing(8);
hbox.setAlignment(Pos.CENTER_LEFT);
return hbox;
}
@@ -267,7 +253,8 @@ public class StoreEntryListOverviewComp extends SimpleRegionBuilder {
@Override
public Region createSimple() {
var bar = new VBox(createGroupListHeader(), createGroupListFilter());
var addBar = createAddBar();
var bar = new VBox(createHeaderBar(), addBar);
bar.setFillWidth(true);
bar.getStyleClass().add("bar");
bar.getStyleClass().add("store-header-bar");
@@ -59,7 +59,6 @@ public class StoreEntryWrapper {
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<String> summary = new SimpleObjectProperty<>();
private final ObjectProperty<String> notes;
private final Property<String> customIcon = new SimpleObjectProperty<>();
private final Property<String> iconFile = new SimpleObjectProperty<>();
private final BooleanProperty sessionActive = new SimpleBooleanProperty();
private final Property<DataStore> store = new SimpleObjectProperty<>();
@@ -69,7 +68,6 @@ public class StoreEntryWrapper {
private final ObservableValue<String> shownSummary;
private final ObservableValue<String> shownDescription;
private final Property<String> shownInformation;
private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();
private final BooleanProperty readOnly = new SimpleBooleanProperty();
private final BooleanProperty renaming = new SimpleBooleanProperty();
private final BooleanProperty pinToTop = new SimpleBooleanProperty();
@@ -206,7 +204,6 @@ public class StoreEntryWrapper {
orderIndex.setValue(entry.getOrderIndex());
color.setValue(entry.getColor());
notes.setValue(entry.getNotes());
customIcon.setValue(entry.getIcon());
readOnly.setValue(entry.isFreeze());
iconFile.setValue(entry.getEffectiveIconFile());
busy.setValue(entry.getBusyCounter().get() != 0);
@@ -220,8 +217,6 @@ public class StoreEntryWrapper {
storeCategoryWrapper.getCategory().getUuid().equals(entry.getCategoryUuid()))
.findFirst()
.orElse(StoreViewState.get().getAllConnectionsCategory()));
largeCategoryOptimizations.setValue(
category.getValue().getLargeCategoryOptimizations().getValue());
perUser.setValue(
!category.getValue().getRoot().equals(StoreViewState.get().getAllIdentitiesCategory())
&& entry.isPerUserStore());
@@ -540,36 +535,22 @@ public class StoreEntryWrapper {
}
}
public boolean matchesFilter(String filter) {
if (filter == null || name.getValue().toLowerCase().contains(filter.toLowerCase())) {
public boolean matchesFilter(StoreFilter filter) {
if (filter == null) {
return true;
}
if (getEntry().getUuid().toString().equalsIgnoreCase(filter)) {
return true;
var l = new ArrayList<String>();
l.add(name.getValue());
l.add(getEntry().getUuid().toString());
if (entry.getValidity().isUsable()) {
l.addAll(entry.getProvider().getSearchableTerms(entry.getStore()));
l.add(AppI18n.get(entry.getProvider().getId() + ".displayName"));
}
if (entry.getValidity().isUsable()
&& entry.getProvider().getSearchableTerms(entry.getStore()).stream()
.anyMatch(s -> s.toLowerCase().contains(filter.toLowerCase()))) {
return true;
}
var is = information.getValue();
if (is != null && is.toLowerCase().contains(filter.toLowerCase())) {
return true;
}
var ss = summary.getValue();
if (ss != null && ss.toLowerCase().contains(filter.toLowerCase())) {
return true;
}
if (tags.stream().anyMatch(s -> s.toLowerCase().contains(filter.toLowerCase()))) {
return true;
}
return false;
l.add(information.getValue());
l.add(summary.getValue());
l.addAll(tags);
return filter.matches(l);
}
public Property<String> nameProperty() {
@@ -0,0 +1,53 @@
package io.xpipe.app.hub.comp;
import lombok.Value;
import java.util.Arrays;
import java.util.List;
@Value
public class StoreFilter {
public static StoreFilter of(String s) {
if (s == null || s.isEmpty()) {
return null;
}
var split = s.split(",");
var l = Arrays.stream(split)
.map(sub -> sub.strip())
.filter(sub -> !sub.isEmpty())
.toList();
return new StoreFilter(l);
}
List<String> parts;
public boolean matches(List<String> input) {
if (input == null) {
return false;
}
for (String part : parts) {
var partl = part.toLowerCase();
var found = false;
for (String s : input) {
if (s == null || s.isEmpty()) {
continue;
}
var sl = s.toLowerCase();
if (sl.contains(partl)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,174 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.RegionDescriptor;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ObservableSubscriber;
import javafx.beans.binding.Bindings;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
import javafx.scene.shape.Rectangle;
import java.util.List;
import java.util.Objects;
public class StoreFilterFieldComp extends SimpleRegionBuilder {
private final ObservableSubscriber filterTrigger;
public StoreFilterFieldComp(ObservableSubscriber filterTrigger) {
this.filterTrigger = filterTrigger;
}
@Override
public Region createSimple() {
var state = StoreFilterState.get();
var popover = new Popover();
popover.setContentNode(new StoreFilterStateComp().build());
popover.setPrefWidth(600);
popover.setArrowLocation(Popover.ArrowLocation.TOP_CENTER);
popover.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());
var field = new CustomTextField();
filterTrigger.subscribe(() -> {
field.requestFocus();
});
field.focusedProperty().subscribe(focus -> {
if (focus) {
popover.hide();
} else {
state.onFocusLost();
}
});
var clearButton = new IconButtonComp("mdi2c-close", () -> {
field.clear();
})
.build();
clearButton.setCursor(Cursor.DEFAULT);
AppFontSizes.sm(clearButton);
var searchButton = new IconButtonComp("mdi2m-magnify", () -> {
if (state.onApply()) {
field.clear();
}
})
.build();
searchButton.setCursor(Cursor.DEFAULT);
AppFontSizes.sm(searchButton);
var launchButton = new IconButtonComp("mdi2p-play", () -> {
if (state.onApply()) {
field.clear();
}
})
.build();
launchButton.setCursor(Cursor.DEFAULT);
AppFontSizes.sm(launchButton);
field.setMinHeight(0);
field.setMaxHeight(20000);
field.getStyleClass().add("store-filter-comp");
field.promptTextProperty().bind(AppI18n.observable("storeFilterPrompt"));
field.rightProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (!field.isFocused()) {
return state.getEffectiveFilter().getValue() != null ? clearButton : null;
}
if (state.getIsQuickConnectString().get()
|| state.getIsUrlString().get()) {
return launchButton;
}
if (state.getIsSearchString().get()) {
return searchButton;
}
return null;
},
field.focusedProperty(),
state.getRawText()));
RegionDescriptor.builder().nameKey("search").showTooltips(false).build().apply(field);
field.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER) {
if (state.onApply()) {
field.clear();
}
field.getParent().getParent().requestFocus();
event.consume();
} else if (event.getCode() == KeyCode.ESCAPE) {
field.clear();
field.getParent().getParent().requestFocus();
event.consume();
}
});
state.getRawText().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
var wasFocused = field.isFocused();
if (!wasFocused) {
field.requestFocus();
}
if (!Objects.equals(field.getText(), val) && !(val == null && "".equals(field.getText()))) {
field.setText(val);
}
if (!wasFocused) {
field.end();
}
});
});
field.textProperty().addListener((observable, oldValue, n) -> {
state.getRawText().setValue(n != null && n.length() > 0 ? n : null);
});
// Fix caret not being visible on right side when overflowing
field.setSkin(field.createDefaultSkin());
Pane pane = (Pane) field.getChildrenUnmodifiable().getFirst();
var rec = new Rectangle();
rec.widthProperty().bind(pane.widthProperty().add(2));
rec.heightProperty().bind(pane.heightProperty());
rec.setSmooth(false);
field.getChildrenUnmodifiable().getFirst().setClip(rec);
var menuButton = new IconButtonComp("mdi2a-animation-play", () -> {
Bounds bounds = field.localToScreen(field.getBoundsInLocal());
popover.show(field, bounds.getMinX() + (field.getWidth() / 1.5), bounds.getMaxY() - 4.0);
});
menuButton.describe(d -> d.nameKey("quickConnect"));
menuButton.style("quick-connect-button");
menuButton.apply(struc -> {
struc.getStyleClass().remove(Styles.FLAT);
});
menuButton.prefWidth(30);
var fieldComp = RegionBuilder.of(() -> field);
var inputGroup = new InputGroupComp(List.of(fieldComp, menuButton));
inputGroup.setMainReference(fieldComp);
inputGroup.prefHeight(47);
inputGroup.minHeight(47);
return inputGroup.build();
}
}

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