mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-05-29 07:20:35 +00:00
Squash merge branch 22-release into master
This commit is contained in:
+4
-7
@@ -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;
|
||||
|
||||
+4
-1
@@ -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);
|
||||
|
||||
+39
-39
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
-30
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
+15
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
+16
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
+22
-26
@@ -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;
|
||||
}
|
||||
+15
-13
@@ -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;
|
||||
}
|
||||
+22
-16
@@ -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
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.core.FailableSupplier;
|
||||
|
||||
+2
-2
@@ -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");
|
||||
|
||||
+12
-5
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user