mirror of
https://github.com/xpipe-io/xpipe.git
synced 2025-04-18 18:23:38 +00:00
Compare commits
124 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a49d1baf87 | ||
![]() |
fe8d8fe383 | ||
![]() |
ab71d178f3 | ||
![]() |
9318a24120 | ||
![]() |
a69ecdc94c | ||
![]() |
937d59a27a | ||
![]() |
f648d63f4b | ||
![]() |
881452ccd0 | ||
![]() |
c7bde460af | ||
![]() |
12fe24a695 | ||
![]() |
3c13cf5e2a | ||
![]() |
bc0e14a332 | ||
![]() |
10dab33ed4 | ||
![]() |
c67411815e | ||
![]() |
74bedf630c | ||
![]() |
93413c3da1 | ||
![]() |
b39c74b4ff | ||
![]() |
474d201708 | ||
![]() |
5c6acc50e4 | ||
![]() |
0c0d91b67d | ||
![]() |
894b93e3c2 | ||
![]() |
8c516c043c | ||
![]() |
9dc368ae7b | ||
![]() |
f5543beb71 | ||
![]() |
8c51eac399 | ||
![]() |
d4a713f29b | ||
![]() |
c738331acd | ||
![]() |
25fc3ca0c1 | ||
![]() |
e1ebd24c04 | ||
![]() |
96fb6b579b | ||
![]() |
a01756562d | ||
![]() |
ad0e628b60 | ||
![]() |
a7f33dd0d2 | ||
![]() |
5b6dadedfe | ||
![]() |
45ef2eda8c | ||
![]() |
b4d1a9e68b | ||
![]() |
086237f965 | ||
![]() |
0417ce635e | ||
![]() |
fa02ee1bc2 | ||
![]() |
7e2663c6ea | ||
![]() |
39e6e66b16 | ||
![]() |
7b9afaef08 | ||
![]() |
a130bcfa91 | ||
![]() |
6084fb8d4d | ||
![]() |
72a5c67aab | ||
![]() |
e0bdf3f52d | ||
![]() |
e4b01ccf0b | ||
![]() |
516cfced4a | ||
![]() |
40e2015780 | ||
![]() |
fe632cd474 | ||
![]() |
8ee9c25f5f | ||
![]() |
e1d0642557 | ||
![]() |
6f29cfd637 | ||
![]() |
4f15a39280 | ||
![]() |
10c8ba62e7 | ||
![]() |
28dd386752 | ||
![]() |
ca1559937b | ||
![]() |
62ef05f707 | ||
![]() |
e78a791c06 | ||
![]() |
72f2decd75 | ||
![]() |
d93eacdb9b | ||
![]() |
795a3dde7c | ||
![]() |
4ecc55443a | ||
![]() |
51c8fff9bd | ||
![]() |
25e2ddd2a3 | ||
![]() |
5473474334 | ||
![]() |
c94e270eee | ||
![]() |
8850a78a36 | ||
![]() |
bffe75a540 | ||
![]() |
484028f22f | ||
![]() |
b2ac3c1fba | ||
![]() |
12c414eecc | ||
![]() |
c42b6d8439 | ||
![]() |
05d93c68ee | ||
![]() |
ec3b95c11b | ||
![]() |
e69da5d5b6 | ||
![]() |
c265b6b87d | ||
![]() |
9844cadbdd | ||
![]() |
1c42605650 | ||
![]() |
5b454cd8cd | ||
![]() |
d7cb5967c6 | ||
![]() |
a98425f100 | ||
![]() |
793ca373aa | ||
![]() |
b0a7f9d17e | ||
![]() |
55e7c65462 | ||
![]() |
c57000acee | ||
![]() |
ff5941249d | ||
![]() |
48450490c3 | ||
![]() |
6cc46ed08f | ||
![]() |
03d222e5f5 | ||
![]() |
b2efb8ddfa | ||
![]() |
f5f14a441f | ||
![]() |
3b84e5e480 | ||
![]() |
0af114ca8b | ||
![]() |
ca5a3a63c6 | ||
![]() |
09a0323069 | ||
![]() |
69524fb140 | ||
![]() |
8dd23fde64 | ||
![]() |
9ce9c31674 | ||
![]() |
907b6b8033 | ||
![]() |
35eb46df84 | ||
![]() |
287e0031a0 | ||
![]() |
1c933cd34f | ||
![]() |
5f0732f5ae | ||
![]() |
2643a47116 | ||
![]() |
50565d28ed | ||
![]() |
d66571b799 | ||
![]() |
b77921549b | ||
![]() |
39ee41e7b4 | ||
![]() |
ee418ed0ff | ||
![]() |
a898341011 | ||
![]() |
e4d29c441f | ||
![]() |
afaf206a42 | ||
![]() |
572a0f0341 | ||
![]() |
ab5fa40828 | ||
![]() |
78f6278e2b | ||
![]() |
98a7a9ae0e | ||
![]() |
04f17c6978 | ||
![]() |
9870a42744 | ||
![]() |
64069c7085 | ||
![]() |
95e5816e1b | ||
![]() |
da30fbd302 | ||
![]() |
97356b45d5 | ||
![]() |
df8f3d1f8a |
143 changed files with 2209 additions and 1099 deletions
|
@ -2,6 +2,6 @@
|
|||
|
||||
Due to its nature, XPipe has to handle a lot of sensitive information. Therefore, the security, integrity, and privacy of your data has topmost priority.
|
||||
|
||||
General information about the security approach of the XPipe application can be found on the website at https://xpipe.io/features#security. If you're interested in security implementation details, you can find them at https://docs.xpipe.io/security.
|
||||
More information about the security approach of the XPipe application can be found on the documentation website at https://docs.xpipe.io/reference/security.
|
||||
|
||||
You can report security vulnerabilities in this GitHub repository in a confidential manner. We will get back to you as soon as possible if you do.
|
||||
|
|
|
@ -48,7 +48,7 @@ dependencies {
|
|||
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
|
||||
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
|
||||
|
||||
api("com.github.weisj:jsvg:1.7.0")
|
||||
api("com.github.weisj:jsvg:1.7.1")
|
||||
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
|
||||
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.80'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.beacon.api.DaemonFocusExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
@ -9,7 +10,11 @@ public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
|
|||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) {
|
||||
OperationMode.switchUp(OperationMode.map(msg.getMode()));
|
||||
OperationMode.switchUp(OperationMode.GUI);
|
||||
var w = AppMainWindow.getInstance();
|
||||
if (w != null) {
|
||||
w.focus();
|
||||
}
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,14 @@ package io.xpipe.app.beacon.impl;
|
|||
|
||||
import io.xpipe.app.terminal.TerminalLauncherManager;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.BeaconServerException;
|
||||
import io.xpipe.beacon.api.TerminalLaunchExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange {
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
|
||||
var r = TerminalLauncherManager.launchExchange(msg.getRequest());
|
||||
return Response.builder().targetFile(r).build();
|
||||
}
|
||||
|
|
|
@ -24,6 +24,9 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
|
|||
|
||||
public void closeAsync(BrowserSessionTab e) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
// This is a bit ugly
|
||||
// If we die on tab init, wait a bit with closing to avoid removal while it is still being inited/added
|
||||
ThreadHelper.sleep(100);
|
||||
closeSync(e);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ public class BrowserFullSessionComp extends SimpleComp {
|
|||
var pinnedStack = createSplitStack(rightSplit, tabs);
|
||||
|
||||
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
|
||||
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
|
||||
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
|
||||
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
|
||||
.withOnDividerChange(d -> {
|
||||
|
@ -173,6 +174,7 @@ public class BrowserFullSessionComp extends SimpleComp {
|
|||
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {
|
||||
var cache = new HashMap<BrowserSessionTab, Region>();
|
||||
var splitStack = new StackComp(List.of());
|
||||
splitStack.apply(struc -> struc.get().setPickOnBounds(false));
|
||||
splitStack.apply(struc -> {
|
||||
model.getEffectiveRightTab().subscribe((newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
|
|
@ -156,8 +156,8 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
var os = fileList.getFileSystemModel()
|
||||
.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.getOsType();
|
||||
.map(shellControl -> shellControl.getOsType())
|
||||
.orElse(null);
|
||||
table.widthProperty().subscribe((newValue) -> {
|
||||
if (os != OsType.WINDOWS && os != OsType.MACOS) {
|
||||
ownerCol.setVisible(newValue.doubleValue() > 1000);
|
||||
|
|
|
@ -53,7 +53,7 @@ public class BrowserFileSystemSavedState {
|
|||
|
||||
public BrowserFileSystemSavedState() {
|
||||
lastDirectory = null;
|
||||
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
|
||||
recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<>(STORED)));
|
||||
}
|
||||
|
||||
static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) {
|
||||
|
@ -164,7 +164,7 @@ public class BrowserFileSystemSavedState {
|
|||
.map(recentEntry -> new RecentEntry(FileNames.toDirectory(recentEntry.directory), recentEntry.time))
|
||||
.filter(distinctBy(recentEntry -> recentEntry.getDirectory()))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
|
||||
return new BrowserFileSystemSavedState(null, FXCollections.synchronizedObservableList(FXCollections.observableList(cleaned)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
|||
home,
|
||||
model.getCurrentPath().isNull(),
|
||||
fileList,
|
||||
model.getCurrentPath().isNull().not()));
|
||||
model.getCurrentPath().isNull().not()), false);
|
||||
var r = stack.styleClass("browser-content-container").createRegion();
|
||||
r.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
|
|
|
@ -26,7 +26,7 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
|
|||
ObservableList<Entry> lastSystems;
|
||||
|
||||
public BrowserHistorySavedStateImpl(List<Entry> lastSystems) {
|
||||
this.lastSystems = FXCollections.observableArrayList(lastSystems);
|
||||
this.lastSystems = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(lastSystems));
|
||||
}
|
||||
|
||||
private static BrowserHistorySavedStateImpl INSTANCE;
|
||||
|
|
|
@ -60,7 +60,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
|||
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
|
||||
map.put(emptyDisplay, empty);
|
||||
map.put(contentDisplay, empty.not());
|
||||
var stack = new MultiContentComp(map);
|
||||
var stack = new MultiContentComp(map, false);
|
||||
return stack.createRegion();
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ public class BrowserOverviewComp extends SimpleComp {
|
|||
|
||||
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
|
||||
|
||||
var commonPlatform = FXCollections.<FileEntry>observableArrayList();
|
||||
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var common = sc.getOsType().determineInterestingPaths(sc).stream()
|
||||
.filter(s -> !s.isBlank())
|
||||
|
|
|
@ -65,13 +65,14 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
return Bindings.createStringBinding(
|
||||
() -> {
|
||||
var p = sourceItem.get().getProgress().getValue();
|
||||
var progressSuffix = p == null
|
||||
|| sourceItem
|
||||
.get()
|
||||
.downloadFinished()
|
||||
.get()
|
||||
var hideProgress = sourceItem
|
||||
.get()
|
||||
.downloadFinished()
|
||||
.get();
|
||||
var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0;
|
||||
var progressSuffix = hideProgress
|
||||
? ""
|
||||
: " " + (p.getTransferred() * 100 / p.getTotal()) + "%";
|
||||
: " " + share + "%";
|
||||
return entry.getFileName() + progressSuffix;
|
||||
},
|
||||
sourceItem.get().getProgress());
|
||||
|
@ -81,14 +82,14 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
|
||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
|
||||
.apply(struc -> struc.get().setWrapText(true))
|
||||
.hide(model.getEmpty());
|
||||
.hide(Bindings.or(model.getEmpty(), model.getTransferring()));
|
||||
|
||||
var clearButton = new IconButtonComp("mdi2c-close", () -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.clear(true);
|
||||
});
|
||||
})
|
||||
.hide(model.getEmpty())
|
||||
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
|
||||
.tooltipKey("clearTransferDescription");
|
||||
|
||||
var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> {
|
||||
|
@ -96,7 +97,7 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
model.transferToDownloads();
|
||||
});
|
||||
})
|
||||
.hide(model.getEmpty())
|
||||
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
|
||||
.tooltipKey("downloadStageDescription");
|
||||
|
||||
var bottom = new HorizontalComp(
|
||||
|
|
|
@ -8,7 +8,9 @@ import io.xpipe.app.util.ShellTemp;
|
|||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.collections.FXCollections;
|
||||
|
@ -34,6 +36,7 @@ public class BrowserTransferModel {
|
|||
BrowserFullSessionModel browserSessionModel;
|
||||
ObservableList<Item> items = FXCollections.observableArrayList();
|
||||
ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);
|
||||
BooleanProperty transferring = new SimpleBooleanProperty();
|
||||
|
||||
public BrowserTransferModel(BrowserFullSessionModel browserSessionModel) {
|
||||
this.browserSessionModel = browserSessionModel;
|
||||
|
@ -47,8 +50,9 @@ public class BrowserTransferModel {
|
|||
}
|
||||
if (toDownload.isPresent()) {
|
||||
downloadSingle(toDownload.get());
|
||||
} else {
|
||||
ThreadHelper.sleep(20);
|
||||
}
|
||||
ThreadHelper.sleep(20);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
|
@ -126,6 +130,7 @@ public class BrowserTransferModel {
|
|||
}
|
||||
|
||||
try {
|
||||
transferring.setValue(true);
|
||||
var op = new BrowserFileTransferOperation(
|
||||
BrowserLocalFileSystem.getLocalFileEntry(TEMP),
|
||||
List.of(item.getBrowserEntry().getRawFileEntry()),
|
||||
|
@ -150,6 +155,8 @@ public class BrowserTransferModel {
|
|||
synchronized (items) {
|
||||
items.remove(item);
|
||||
}
|
||||
} finally {
|
||||
transferring.setValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -154,7 +154,15 @@ public abstract class Comp<S extends CompStructure<?>> {
|
|||
}
|
||||
|
||||
public Comp<S> disable(ObservableValue<Boolean> o) {
|
||||
return apply(struc -> struc.get().disableProperty().bind(o));
|
||||
return apply(struc -> {
|
||||
var region = struc.get();
|
||||
BindingsHelper.preserve(region, o);
|
||||
o.subscribe(n -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
region.setDisable(n);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public Comp<S> padding(Insets insets) {
|
||||
|
|
|
@ -22,7 +22,6 @@ public class AnchorComp extends Comp<CompStructure<AnchorPane>> {
|
|||
for (var c : comps) {
|
||||
pane.getChildren().add(c.createRegion());
|
||||
}
|
||||
pane.setPickOnBounds(false);
|
||||
return new SimpleCompStructure<>(pane);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
|
|||
return model.getSelected().getValue().equals(entry);
|
||||
},
|
||||
model.getSelected())));
|
||||
var multi = new MultiContentComp(map);
|
||||
var multi = new MultiContentComp(map, true);
|
||||
multi.styleClass("background");
|
||||
|
||||
var pane = new BorderPane();
|
||||
|
|
|
@ -6,6 +6,7 @@ import io.xpipe.app.core.AppFontSizes;
|
|||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.resources.AppImages;
|
||||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
@ -82,6 +83,7 @@ public class AppMainWindowContentComp extends SimpleComp {
|
|||
|
||||
loaded.subscribe(struc -> {
|
||||
if (struc != null) {
|
||||
TrackEvent.info("Window content node set");
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
anim.stop();
|
||||
struc.prepareAddition();
|
||||
|
@ -90,6 +92,7 @@ public class AppMainWindowContentComp extends SimpleComp {
|
|||
pane.getStyleClass().remove("background");
|
||||
pane.getChildren().remove(vbox);
|
||||
struc.show();
|
||||
TrackEvent.info("Window content node shown");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -107,14 +110,6 @@ public class AppMainWindowContentComp extends SimpleComp {
|
|||
}
|
||||
});
|
||||
|
||||
loaded.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
Platform.runLater(() -> {
|
||||
stage.requestFocus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return pane;
|
||||
});
|
||||
var modal = new ModalOverlayStackComp(bg, overlay);
|
||||
|
|
|
@ -67,7 +67,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
|
|||
filterText.subscribe(val -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
clear.setVisible(val != null);
|
||||
if (!Objects.equals(filter.getText(), val)) {
|
||||
if (!Objects.equals(filter.getText(), val) && !(val == null && "".equals(filter.getText()))) {
|
||||
filter.setText(val);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GrowPaneComp extends Comp<CompStructure<Pane>> {
|
||||
|
||||
private final List<Comp<?>> comps;
|
||||
|
||||
public GrowPaneComp(List<Comp<?>> comps) {
|
||||
this.comps = List.copyOf(comps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<Pane> createBase() {
|
||||
var pane = new BorderPane();
|
||||
for (var c : comps) {
|
||||
pane.setCenter(c.createRegion());
|
||||
}
|
||||
pane.setPickOnBounds(false);
|
||||
return new SimpleCompStructure<>(pane);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import io.xpipe.app.comp.CompStructure;
|
|||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.control.TextField;
|
||||
|
@ -13,6 +14,8 @@ import javafx.scene.input.KeyEvent;
|
|||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
||||
|
||||
|
@ -34,29 +37,38 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
|||
|
||||
@Override
|
||||
public CompStructure<TextField> createBase() {
|
||||
var text = new TextField(value.getValue() != null ? value.getValue().toString() : null);
|
||||
var field = new TextField(value.getValue() != null ? value.getValue().toString() : null);
|
||||
|
||||
value.addListener((ChangeListener<Number>) (observableValue, oldValue, newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
if (newValue == null) {
|
||||
text.setText("");
|
||||
} else {
|
||||
if (newValue.intValue() < minValue) {
|
||||
value.setValue(minValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.intValue() > maxValue) {
|
||||
value.setValue(maxValue);
|
||||
return;
|
||||
}
|
||||
|
||||
text.setText(newValue.toString());
|
||||
// Check if control value is the same. Then don't set it as that might cause bugs
|
||||
if ((newValue == null && field.getText().isEmpty())
|
||||
|| Objects.equals(field.getText(), newValue != null ? newValue.toString() : null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue == null) {
|
||||
Platform.runLater(() -> {
|
||||
field.setText(null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.intValue() < minValue) {
|
||||
value.setValue(minValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.intValue() > maxValue) {
|
||||
value.setValue(maxValue);
|
||||
return;
|
||||
}
|
||||
|
||||
field.setText(newValue.toString());
|
||||
});
|
||||
});
|
||||
|
||||
text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
|
||||
field.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
|
||||
if (minValue < 0) {
|
||||
if (!"-0123456789".contains(keyEvent.getCharacter())) {
|
||||
keyEvent.consume();
|
||||
|
@ -68,7 +80,7 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
|||
}
|
||||
});
|
||||
|
||||
text.textProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
field.textProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
if (newValue == null
|
||||
|| newValue.isEmpty()
|
||||
|| (minValue < 0 && "-".equals(newValue))
|
||||
|
@ -79,12 +91,12 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
|||
|
||||
int intValue = Integer.parseInt(newValue);
|
||||
if (minValue > intValue || intValue > maxValue) {
|
||||
text.textProperty().setValue(oldValue);
|
||||
field.textProperty().setValue(oldValue);
|
||||
}
|
||||
|
||||
value.setValue(Integer.parseInt(text.textProperty().get()));
|
||||
value.setValue(Integer.parseInt(field.textProperty().get()));
|
||||
});
|
||||
|
||||
return new SimpleCompStructure<>(text);
|
||||
return new SimpleCompStructure<>(field);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,17 @@ import io.xpipe.app.browser.BrowserFullSessionModel;
|
|||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
|
@ -22,6 +27,7 @@ import javafx.scene.layout.VBox;
|
|||
import lombok.Setter;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
|
@ -37,7 +43,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
private final boolean scrollBar;
|
||||
|
||||
@Setter
|
||||
private int platformPauseInterval = -1;
|
||||
private boolean visibilityControl = false;
|
||||
|
||||
public ListBoxViewComp(
|
||||
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
|
||||
|
@ -56,16 +62,24 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
vbox.setFocusTraversable(false);
|
||||
var scroll = new ScrollPane(vbox);
|
||||
|
||||
refresh(scroll, vbox, shown, all, cache, false, false);
|
||||
refresh(scroll, vbox, shown, all, cache, false);
|
||||
|
||||
shown.addListener((ListChangeListener<? super T>) (c) -> {
|
||||
refresh(scroll, vbox, c.getList(), all, cache, true, true);
|
||||
var hadScene = new AtomicBoolean(false);
|
||||
scroll.sceneProperty().subscribe(scene -> {
|
||||
if (scene != null) {
|
||||
hadScene.set(true);
|
||||
refresh(scroll, vbox, shown, all, cache, true);
|
||||
}
|
||||
});
|
||||
|
||||
all.addListener((ListChangeListener<? super T>) c -> {
|
||||
synchronized (cache) {
|
||||
cache.keySet().retainAll(c.getList());
|
||||
}
|
||||
shown.addListener((ListChangeListener<? super T>) (c) -> {
|
||||
Platform.runLater(() -> {
|
||||
if (scroll.getScene() == null && hadScene.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
refresh(scroll, vbox, c.getList(), all, cache, true);
|
||||
});
|
||||
});
|
||||
|
||||
if (scrollBar) {
|
||||
|
@ -92,50 +106,84 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
scroll.setFitToWidth(true);
|
||||
scroll.getStyleClass().add("list-box-view-comp");
|
||||
|
||||
registerVisibilityListeners(scroll, vbox);
|
||||
|
||||
return new SimpleCompStructure<>(scroll);
|
||||
}
|
||||
|
||||
private void registerVisibilityListeners(ScrollPane scroll, VBox vbox) {
|
||||
if (!visibilityControl) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dirty = new SimpleBooleanProperty();
|
||||
var animationTimer = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
if (!dirty.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateVisibilities(scroll, vbox);
|
||||
dirty.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
scroll.vvalueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
dirty.set(true);
|
||||
});
|
||||
scroll.heightProperty().addListener((observable, oldValue, newValue) -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
dirty.set(true);
|
||||
});
|
||||
vbox.heightProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
dirty.set(true);
|
||||
});
|
||||
|
||||
// We can't directly listen to any parent element changing visibility, so this is a compromise
|
||||
if (AppLayoutModel.get() != null) {
|
||||
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
dirty.set(true);
|
||||
});
|
||||
}
|
||||
BrowserFullSessionModel.DEFAULT.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
dirty.set(true);
|
||||
});
|
||||
if (StoreViewState.get() != null) {
|
||||
StoreViewState.get().getSortMode().addListener((observable, oldValue, newValue) -> {
|
||||
// This is very ugly, but it just takes multiple iterations for the order to apply
|
||||
Platform.runLater(() -> {
|
||||
Platform.runLater(() -> {
|
||||
Platform.runLater(() -> {
|
||||
dirty.set(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
vbox.sceneProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Node c = vbox;
|
||||
while ((c = c.getParent()) != null) {
|
||||
c.boundsInParentProperty().addListener((observable1, oldValue1, newValue1) -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
dirty.set(true);
|
||||
|
||||
if (newValue != null) {
|
||||
animationTimer.start();
|
||||
} else {
|
||||
animationTimer.stop();
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
|
||||
Node c = vbox;
|
||||
do {
|
||||
c.boundsInParentProperty().addListener((change, oldBounds,newBounds) -> {
|
||||
dirty.set(true);
|
||||
});
|
||||
// Don't listen to root node changes, that seemingly can cause exceptions
|
||||
} while ((c = c.getParent()) != null && c.getParent() != null);
|
||||
|
||||
if (newValue != null) {
|
||||
newValue.heightProperty().addListener((observable1, oldValue1, newValue1) -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
dirty.set(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new SimpleCompStructure<>(scroll);
|
||||
}
|
||||
|
||||
private boolean isVisible(ScrollPane pane, VBox box, Node node) {
|
||||
|
@ -178,9 +226,20 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
}
|
||||
|
||||
private void updateVisibilities(ScrollPane scroll, VBox vbox) {
|
||||
if (!visibilityControl) {
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (Node child : vbox.getChildren()) {
|
||||
var v = isVisible(scroll, vbox, child);
|
||||
child.setVisible(v);
|
||||
if (v) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 10) {
|
||||
// System.out.println("Visible: " + count);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,44 +249,42 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
List<? extends T> shown,
|
||||
List<? extends T> all,
|
||||
Map<T, Region> cache,
|
||||
boolean asynchronous,
|
||||
boolean refreshVisibilities) {
|
||||
Runnable update = () -> {
|
||||
synchronized (cache) {
|
||||
var set = new HashSet<T>();
|
||||
// These lists might diverge on updates
|
||||
set.addAll(shown);
|
||||
set.addAll(all);
|
||||
// These lists might diverge on updates, so add both
|
||||
synchronized (shown) {
|
||||
set.addAll(shown);
|
||||
}
|
||||
synchronized (all) {
|
||||
set.addAll(all);
|
||||
}
|
||||
// Clear cache of unused values
|
||||
cache.keySet().removeIf(t -> !set.contains(t));
|
||||
}
|
||||
|
||||
final long[] lastPause = {System.currentTimeMillis()};
|
||||
// Create copy to reduce chances of concurrent modification
|
||||
var shownCopy = new ArrayList<>(shown);
|
||||
var newShown = shownCopy.stream()
|
||||
.map(v -> {
|
||||
var elapsed = System.currentTimeMillis() - lastPause[0];
|
||||
if (platformPauseInterval != -1 && elapsed > platformPauseInterval) {
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
lastPause[0] = System.currentTimeMillis();
|
||||
// Use copy to prevent concurrent modifications and to not synchronize to long
|
||||
List<T> shownCopy;
|
||||
synchronized (shown) {
|
||||
shownCopy = new ArrayList<>(shown);
|
||||
}
|
||||
List<Region> newShown = shownCopy.stream().map(v -> {
|
||||
if (!cache.containsKey(v)) {
|
||||
var comp = compFunction.apply(v);
|
||||
if (comp != null) {
|
||||
var r = comp.createRegion();
|
||||
if (visibilityControl) {
|
||||
r.setVisible(false);
|
||||
}
|
||||
cache.put(v, r);
|
||||
} else {
|
||||
cache.put(v, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.containsKey(v)) {
|
||||
var comp = compFunction.apply(v);
|
||||
if (comp != null) {
|
||||
var r = comp.createRegion();
|
||||
r.setVisible(false);
|
||||
cache.put(v, r);
|
||||
} else {
|
||||
cache.put(v, null);
|
||||
}
|
||||
}
|
||||
|
||||
return cache.get(v);
|
||||
})
|
||||
.filter(region -> region != null)
|
||||
.toList();
|
||||
return cache.get(v);
|
||||
}).filter(region -> region != null).toList();
|
||||
|
||||
if (listView.getChildren().equals(newShown)) {
|
||||
return;
|
||||
|
@ -247,11 +304,6 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
updateVisibilities(scroll, listView);
|
||||
}
|
||||
};
|
||||
|
||||
if (asynchronous) {
|
||||
Platform.runLater(update);
|
||||
} else {
|
||||
PlatformThread.runLaterIfNeeded(update);
|
||||
}
|
||||
update.run();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.comp.base;
|
|||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
@ -15,9 +16,11 @@ import java.util.Map;
|
|||
|
||||
public class MultiContentComp extends SimpleComp {
|
||||
|
||||
private final boolean log;
|
||||
private final Map<Comp<?>, ObservableValue<Boolean>> content;
|
||||
|
||||
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) {
|
||||
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content, boolean log) {
|
||||
this.log = log;
|
||||
this.content = FXCollections.observableMap(content);
|
||||
}
|
||||
|
||||
|
@ -34,7 +37,14 @@ public class MultiContentComp extends SimpleComp {
|
|||
});
|
||||
|
||||
for (Map.Entry<Comp<?>, 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().createRegion();
|
||||
if (log) {
|
||||
TrackEvent.trace("Created content tab region for element " + name);
|
||||
}
|
||||
e.getValue().subscribe(val -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
r.setManaged(val);
|
||||
|
@ -42,6 +52,9 @@ public class MultiContentComp extends SimpleComp {
|
|||
});
|
||||
});
|
||||
m.put(e.getKey(), r);
|
||||
if (log) {
|
||||
TrackEvent.trace("Added content tab region for element " + name);
|
||||
}
|
||||
}
|
||||
|
||||
return stack;
|
||||
|
|
|
@ -10,6 +10,8 @@ import javafx.beans.property.Property;
|
|||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
|
@ -68,23 +70,32 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
|
||||
@Override
|
||||
public Structure createBase() {
|
||||
var text = new PasswordField();
|
||||
text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
|
||||
text.textProperty().addListener((c, o, n) -> {
|
||||
var field = new PasswordField();
|
||||
field.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.isControlDown() && e.getCode() == KeyCode.BACK_SPACE) {
|
||||
var sel = field.getSelection();
|
||||
if (sel.getEnd() > 0) {
|
||||
field.setText(field.getText().substring(sel.getEnd()));
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
field.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
|
||||
field.textProperty().addListener((c, o, n) -> {
|
||||
value.setValue(n != null && n.length() > 0 ? encrypt(n.toCharArray()) : null);
|
||||
});
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
// Check if control value is the same. Then don't set it as that might cause bugs
|
||||
if ((n == null && text.getText().isEmpty())
|
||||
|| Objects.equals(text.getText(), n != null ? n.getSecretValue() : null)) {
|
||||
if ((n == null && field.getText().isEmpty())
|
||||
|| Objects.equals(field.getText(), n != null ? n.getSecretValue() : null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
text.setText(n != null ? n.getSecretValue() : null);
|
||||
field.setText(n != null ? n.getSecretValue() : null);
|
||||
});
|
||||
});
|
||||
HBox.setHgrow(text, Priority.ALWAYS);
|
||||
HBox.setHgrow(field, Priority.ALWAYS);
|
||||
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
|
||||
ClipboardHelper.copyPassword(value.getValue());
|
||||
|
@ -93,7 +104,7 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
.tooltipKey("copyPassword")
|
||||
.createRegion();
|
||||
|
||||
var ig = new InputGroup(text);
|
||||
var ig = new InputGroup(field);
|
||||
ig.setFillHeight(true);
|
||||
ig.getStyleClass().add("secret-field-comp");
|
||||
if (allowCopy) {
|
||||
|
@ -103,10 +114,10 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
|
||||
ig.focusedProperty().addListener((c, o, n) -> {
|
||||
if (n) {
|
||||
text.requestFocus();
|
||||
field.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
return new Structure(ig, text);
|
||||
return new Structure(ig, field);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
var shortcut = e.combination();
|
||||
b.apply(new TooltipAugment<>(e.name(), shortcut));
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.xl(struc.get());
|
||||
AppFontSizes.lg(struc.get());
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
@ -123,7 +125,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
.tooltipKey("updateAvailableTooltip")
|
||||
.accessibleTextKey("updateAvailableTooltip");
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.xl(struc.get());
|
||||
AppFontSizes.lg(struc.get());
|
||||
});
|
||||
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
|
|
|
@ -24,7 +24,6 @@ public class StackComp extends Comp<CompStructure<StackPane>> {
|
|||
pane.getChildren().add(c.createRegion());
|
||||
}
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
pane.setPickOnBounds(false);
|
||||
return new SimpleCompStructure<>(pane);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.comp.Comp;
|
|||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import io.xpipe.core.process.OsType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
|
@ -57,6 +58,11 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return OsType.getLocal() == OsType.WINDOWS ? 38 : 37;
|
||||
}
|
||||
|
||||
protected Region createContent() {
|
||||
var grid = new GridPane();
|
||||
grid.setHgap(8);
|
||||
|
|
|
@ -82,7 +82,8 @@ public class OsLogoComp extends SimpleComp {
|
|||
}
|
||||
|
||||
return ICONS.entrySet().stream()
|
||||
.filter(e -> name.toLowerCase().contains(e.getKey()))
|
||||
.filter(e -> name.toLowerCase().contains(e.getKey()) ||
|
||||
name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
|
||||
.findAny()
|
||||
.map(e -> e.getValue())
|
||||
.orElse("os/linux.svg");
|
||||
|
|
|
@ -21,6 +21,11 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return 57;
|
||||
}
|
||||
|
||||
private Label createSummary() {
|
||||
var summary = new Label();
|
||||
summary.textProperty().bind(getWrapper().getShownSummary());
|
||||
|
|
|
@ -170,6 +170,7 @@ public class StoreCategoryComp extends SimpleComp {
|
|||
new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false);
|
||||
children.styleClass("children");
|
||||
children.minHeight(0);
|
||||
children.setVisibilityControl(true);
|
||||
|
||||
var hide = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
|
|
|
@ -24,6 +24,7 @@ import javafx.beans.property.SimpleStringProperty;
|
|||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
@ -224,6 +225,14 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
|||
}
|
||||
event.consume();
|
||||
});
|
||||
struc.get().setOnMouseClicked(event -> {
|
||||
if (event.getButton() != MouseButton.SECONDARY) {
|
||||
return;
|
||||
}
|
||||
|
||||
selected.setValue(mode == Mode.PROXY ? DataStorage.get().local().ref() : null);
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
.styleClass("choice-comp");
|
||||
|
||||
|
|
|
@ -183,6 +183,10 @@ public class StoreCreationComp extends DialogComp {
|
|||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e) {
|
||||
showEdit(e, dataStoreEntry -> {});
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
|
||||
show(
|
||||
e.getName(),
|
||||
e.getProvider(),
|
||||
|
@ -204,6 +208,7 @@ public class StoreCreationComp extends DialogComp {
|
|||
}
|
||||
}
|
||||
}
|
||||
consumer.accept(e);
|
||||
});
|
||||
},
|
||||
true,
|
||||
|
|
|
@ -68,10 +68,10 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
}
|
||||
}
|
||||
|
||||
public static StoreEntryComp customSection(StoreSection e, boolean topLevel) {
|
||||
public static StoreEntryComp customSection(StoreSection e) {
|
||||
var prov = e.getWrapper().getEntry().getProvider();
|
||||
if (prov != null) {
|
||||
return prov.customEntryComp(e, topLevel);
|
||||
return prov.customEntryComp(e, e.getDepth() == 1);
|
||||
} else {
|
||||
var forceCondensed = AppPrefs.get() != null
|
||||
&& AppPrefs.get().condenseConnectionDisplay().get();
|
||||
|
@ -81,6 +81,8 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
public abstract boolean isFullSize();
|
||||
|
||||
public abstract int getHeight();
|
||||
|
||||
@Override
|
||||
protected final Region createSimple() {
|
||||
var r = createContent();
|
||||
|
|
|
@ -27,11 +27,11 @@ public class StoreEntryListComp extends SimpleComp {
|
|||
.getAllChildren()
|
||||
.getList(),
|
||||
(StoreSection e) -> {
|
||||
var custom = StoreSection.customSection(e, true).hgrow();
|
||||
var custom = StoreSection.customSection(e).hgrow();
|
||||
return custom;
|
||||
},
|
||||
true);
|
||||
content.setPlatformPauseInterval(50);
|
||||
content.setVisibilityControl(true);
|
||||
content.apply(struc -> {
|
||||
// Reset scroll
|
||||
StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> {
|
||||
|
@ -142,6 +142,6 @@ public class StoreEntryListComp extends SimpleComp {
|
|||
map.put(new StoreScriptsIntroComp(scriptsIntroShowing), showScriptsIntro);
|
||||
map.put(new StoreIdentitiesIntroComp(), showIdentitiesIntro);
|
||||
|
||||
return new MultiContentComp(map).createRegion();
|
||||
return new MultiContentComp(map, false).createRegion();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ public class StoreEntryWrapper {
|
|||
private final Property<String> name;
|
||||
private final DataStoreEntry entry;
|
||||
private final Property<Instant> lastAccess;
|
||||
private final Property<Instant> lastAccessApplied = new SimpleObjectProperty<>();
|
||||
private final BooleanProperty disabled = new SimpleBooleanProperty();
|
||||
private final BooleanProperty busy = new SimpleBooleanProperty();
|
||||
private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();
|
||||
|
@ -104,10 +103,6 @@ public class StoreEntryWrapper {
|
|||
setupListeners();
|
||||
}
|
||||
|
||||
public void applyLastAccess() {
|
||||
this.lastAccessApplied.setValue(lastAccess.getValue());
|
||||
}
|
||||
|
||||
public void moveTo(DataStoreCategory category) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
DataStorage.get().moveEntryToCategory(entry, category);
|
||||
|
@ -130,8 +125,7 @@ public class StoreEntryWrapper {
|
|||
|
||||
public void delete() {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
DataStorage.get().deleteChildren(this.entry);
|
||||
DataStorage.get().deleteStoreEntry(this.entry);
|
||||
DataStorage.get().deleteWithChildren(this.entry);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import io.xpipe.app.util.BooleanScope;
|
|||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
|
@ -107,18 +108,29 @@ public class StoreIconChoiceComp extends SimpleComp {
|
|||
}
|
||||
|
||||
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
|
||||
var stream = icons.stream()
|
||||
var available = icons.stream()
|
||||
.filter(systemIcon -> AppImages.hasNormalImage("icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
|
||||
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()));
|
||||
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
|
||||
.toList();
|
||||
table.getPlaceholder().setVisible(available.size() == 0);
|
||||
var filtered = available;
|
||||
if (filterString != null && !filterString.isBlank() && filterString.length() >= 2) {
|
||||
stream = stream.filter(icon -> containsString(icon.getId(), filterString));
|
||||
filtered = available.stream().filter(icon -> containsString(icon.getId(), filterString)).toList();
|
||||
}
|
||||
var displayedIcons = stream.toList();
|
||||
var data = partitionList(displayedIcons, columns);
|
||||
var data = partitionList(filtered, columns);
|
||||
table.getItems().setAll(data);
|
||||
|
||||
var selectMatch = filtered.size() == 1 || filtered.stream().anyMatch(systemIcon -> systemIcon.getId().equals(filterString));
|
||||
// Table updates seem to not always be instant, sometimes the column is not there yet
|
||||
if (selectMatch && table.getColumns().size() > 0) {
|
||||
table.getSelectionModel().select(0, table.getColumns().getFirst());
|
||||
selected.setValue(filtered.getFirst());
|
||||
} else {
|
||||
selected.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> Collection<List<T>> partitionList(List<T> list, int size) {
|
||||
private <T> List<List<T>> partitionList(List<T> list, int size) {
|
||||
List<List<T>> partitions = new ArrayList<>();
|
||||
if (list.size() == 0) {
|
||||
return partitions;
|
||||
|
|
|
@ -53,12 +53,12 @@ public class StoreSection {
|
|||
}
|
||||
}
|
||||
|
||||
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
|
||||
public static Comp<?> customSection(StoreSection e) {
|
||||
var prov = e.getWrapper().getEntry().getProvider();
|
||||
if (prov != null) {
|
||||
return prov.customSectionComp(e, topLevel);
|
||||
return prov.customSectionComp(e);
|
||||
} else {
|
||||
return new StoreSectionComp(e, topLevel);
|
||||
return new StoreSectionComp(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ public class StoreSection {
|
|||
|
||||
var current = mappedSortMode.getValue();
|
||||
if (current != null) {
|
||||
return current.comparator().compare(current.representative(o1), current.representative(o2));
|
||||
return current.comparator().compare(o1, o2);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.HorizontalComp;
|
||||
import io.xpipe.app.comp.base.IconButtonComp;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class StoreSectionBaseComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
private static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
|
||||
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
|
||||
private static final PseudoClass TOP = PseudoClass.getPseudoClass("top");
|
||||
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
|
||||
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
|
||||
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
|
||||
|
||||
protected final StoreSection section;
|
||||
|
||||
public StoreSectionBaseComp(StoreSection section) {
|
||||
this.section = section;
|
||||
}
|
||||
|
||||
protected ObservableBooleanValue effectiveExpanded(ObservableBooleanValue expanded) {
|
||||
return section.getWrapper() != null ? Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return expanded.get()
|
||||
&& section.getShownChildren().getList().size() > 0;
|
||||
},
|
||||
expanded,
|
||||
section.getShownChildren().getList()) : new SimpleBooleanProperty(true);
|
||||
}
|
||||
|
||||
protected void addPseudoClassListeners(VBox vbox, ObservableBooleanValue expanded) {
|
||||
var observable = effectiveExpanded(expanded);
|
||||
BindingsHelper.preserve(this, observable);
|
||||
observable.subscribe(val -> {
|
||||
vbox.pseudoClassStateChanged(EXPANDED, val);
|
||||
});
|
||||
|
||||
vbox.pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||
vbox.pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
|
||||
vbox.pseudoClassStateChanged(ROOT, section.getDepth() == 0);
|
||||
vbox.pseudoClassStateChanged(SUB, section.getDepth() > 1);
|
||||
vbox.pseudoClassStateChanged(TOP, section.getDepth() == 1);
|
||||
|
||||
if (section.getWrapper() != null) {
|
||||
if (section.getDepth() == 1) {
|
||||
section.getWrapper().getColor().subscribe(val -> {
|
||||
var newList = new ArrayList<>(vbox.getStyleClass());
|
||||
newList.removeIf(s -> Arrays.stream(DataColor.values()).anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));
|
||||
newList.remove("gray");
|
||||
newList.add("color-box");
|
||||
if (val != null) {
|
||||
newList.add(val.getId());
|
||||
} else {
|
||||
newList.add("gray");
|
||||
}
|
||||
vbox.getStyleClass().setAll(newList);
|
||||
});
|
||||
}
|
||||
|
||||
section.getWrapper().getPerUser().subscribe(val -> {
|
||||
vbox.pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void addVisibilityListeners(VBox root, HBox hbox) {
|
||||
var children = new ArrayList<>(hbox.getChildren());
|
||||
hbox.getChildren().clear();
|
||||
root.visibleProperty().subscribe((newValue) -> {
|
||||
if (newValue) {
|
||||
hbox.getChildren().addAll(children);
|
||||
} else {
|
||||
hbox.getChildren().removeAll(children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected ListBoxViewComp<StoreSection> createChildrenList(Function<StoreSection, Comp<?>> function, ObservableBooleanValue hide) {
|
||||
var content = new ListBoxViewComp<>(
|
||||
section.getShownChildren().getList(),
|
||||
section.getAllChildren().getList(),
|
||||
(StoreSection e) -> {
|
||||
return function.apply(e).grow(true, false);
|
||||
},
|
||||
section.getWrapper() == null);
|
||||
content.setVisibilityControl(true);
|
||||
content.minHeight(0);
|
||||
content.hgrow();
|
||||
content.styleClass("children-content");
|
||||
content.hide(hide);
|
||||
return content;
|
||||
}
|
||||
|
||||
protected Comp<CompStructure<Button>> createExpandButton(Runnable action, int width, ObservableBooleanValue expanded) {
|
||||
var icon = Bindings.createObjectBinding(() -> new LabelGraphic.IconGraphic(
|
||||
expanded.get() && section.getShownChildren().getList().size() > 0 ?
|
||||
"mdal-keyboard_arrow_down" :
|
||||
"mdal-keyboard_arrow_right"), expanded, section.getShownChildren().getList());
|
||||
var expandButton = new IconButtonComp(icon,
|
||||
action);
|
||||
expandButton
|
||||
.minWidth(width)
|
||||
.prefWidth(width)
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Expand " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
|
||||
.styleClass("expand-button")
|
||||
.maxHeight(100);
|
||||
return expandButton;
|
||||
}
|
||||
|
||||
protected Comp<CompStructure<Button>> createQuickAccessButton(int width, Consumer<StoreSection> r) {
|
||||
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, r)
|
||||
.styleClass("quick-access-button")
|
||||
.minWidth(width)
|
||||
.prefWidth(width)
|
||||
.maxHeight(100)
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Access " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(quickAccessDisabled);
|
||||
return quickAccessButton;
|
||||
}
|
||||
}
|
|
@ -12,11 +12,14 @@ import io.xpipe.app.util.LabelGraphic;
|
|||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -24,103 +27,24 @@ import java.util.Arrays;
|
|||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
||||
public class StoreSectionComp extends StoreSectionBaseComp {
|
||||
|
||||
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
|
||||
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
|
||||
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
|
||||
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
|
||||
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
|
||||
private final StoreSection section;
|
||||
private final boolean topLevel;
|
||||
|
||||
public StoreSectionComp(StoreSection section, boolean topLevel) {
|
||||
this.section = section;
|
||||
this.topLevel = topLevel;
|
||||
}
|
||||
|
||||
private Comp<CompStructure<Button>> createQuickAccessButton() {
|
||||
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
Consumer<StoreSection> quickAccessAction = w -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
w.getWrapper().executeDefaultAction();
|
||||
});
|
||||
};
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
|
||||
.vgrow()
|
||||
.styleClass("quick-access-button")
|
||||
.apply(struc -> struc.get().setMinWidth(30))
|
||||
.apply(struc -> struc.get().setPrefWidth(30))
|
||||
.maxHeight(100)
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Access " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(quickAccessDisabled)
|
||||
.focusTraversableForAccessibility()
|
||||
.tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
|
||||
return quickAccessButton;
|
||||
}
|
||||
|
||||
private Comp<CompStructure<Button>> createExpandButton() {
|
||||
var expandButton = new IconButtonComp(
|
||||
Bindings.createObjectBinding(
|
||||
() -> new LabelGraphic.IconGraphic(
|
||||
section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren()
|
||||
.getList()
|
||||
.size()
|
||||
> 0
|
||||
? "mdal-keyboard_arrow_down"
|
||||
: "mdal-keyboard_arrow_right"),
|
||||
section.getWrapper().getExpanded(),
|
||||
section.getShownChildren().getList()),
|
||||
() -> {
|
||||
section.getWrapper().toggleExpanded();
|
||||
});
|
||||
expandButton
|
||||
.apply(struc -> struc.get().setMinWidth(30))
|
||||
.apply(struc -> struc.get().setPrefWidth(30))
|
||||
.focusTraversableForAccessibility()
|
||||
.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE))
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Expand " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
|
||||
.styleClass("expand-button")
|
||||
.maxHeight(100)
|
||||
.vgrow();
|
||||
return expandButton;
|
||||
public StoreSectionComp(StoreSection section) {
|
||||
super(section);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<VBox> createBase() {
|
||||
var entryButton = StoreEntryComp.customSection(section, topLevel);
|
||||
var quickAccessButton = createQuickAccessButton();
|
||||
var expandButton = createExpandButton();
|
||||
var buttonList = new ArrayList<Comp<?>>();
|
||||
if (entryButton.isFullSize()) {
|
||||
buttonList.add(quickAccessButton);
|
||||
}
|
||||
buttonList.add(expandButton);
|
||||
var buttons = new VerticalComp(buttonList);
|
||||
var topEntryList = new HorizontalComp(List.of(buttons, entryButton.hgrow()));
|
||||
topEntryList.apply(struc -> {
|
||||
var mainButton = struc.get().getChildren().get(1);
|
||||
mainButton.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
var entryButton = StoreEntryComp.customSection(section);
|
||||
entryButton.hgrow();
|
||||
entryButton.apply(struc -> {
|
||||
struc.get().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode() == KeyCode.SPACE) {
|
||||
section.getWrapper().toggleExpanded();
|
||||
event.consume();
|
||||
}
|
||||
if (event.getCode() == KeyCode.RIGHT) {
|
||||
var ref = (VBox) struc.get().getChildren().getFirst();
|
||||
var ref = (VBox) ((HBox) struc.get().getParent()).getChildren().getFirst();
|
||||
if (entryButton.isFullSize()) {
|
||||
var btn = (Button) ref.getChildren().getFirst();
|
||||
btn.fire();
|
||||
|
@ -130,72 +54,45 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
|||
});
|
||||
});
|
||||
|
||||
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
||||
// section is actually expanded
|
||||
var listSections = section.getShownChildren()
|
||||
.filtered(
|
||||
storeSection -> section.getAllChildren().getList().size() <= 20
|
||||
|| section.getWrapper().getExpanded().get(),
|
||||
section.getWrapper().getExpanded(),
|
||||
section.getAllChildren().getList());
|
||||
var content = new ListBoxViewComp<>(
|
||||
listSections.getList(),
|
||||
section.getAllChildren().getList(),
|
||||
(StoreSection e) -> {
|
||||
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
|
||||
},
|
||||
false);
|
||||
content.minHeight(0).hgrow();
|
||||
var quickAccessButton = createQuickAccessButton(30, c -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
c.getWrapper().executeDefaultAction();
|
||||
});
|
||||
});
|
||||
quickAccessButton.vgrow();
|
||||
quickAccessButton.focusTraversableForAccessibility();
|
||||
quickAccessButton.tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
|
||||
|
||||
var expandButton = createExpandButton(() -> section.getWrapper().toggleExpanded(), 30, section.getWrapper().getExpanded());
|
||||
expandButton.vgrow();
|
||||
expandButton.focusTraversableForAccessibility();
|
||||
expandButton.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE));
|
||||
var buttonList = new ArrayList<Comp<?>>();
|
||||
if (entryButton.isFullSize()) {
|
||||
buttonList.add(quickAccessButton);
|
||||
}
|
||||
buttonList.add(expandButton);
|
||||
var buttons = new VerticalComp(buttonList);
|
||||
var topEntryList = new HorizontalComp(List.of(buttons, entryButton));
|
||||
topEntryList.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
|
||||
topEntryList.minHeight(entryButton.getHeight());
|
||||
topEntryList.maxHeight(entryButton.getHeight());
|
||||
topEntryList.prefHeight(entryButton.getHeight());
|
||||
|
||||
var effectiveExpanded = effectiveExpanded(section.getWrapper().getExpanded());
|
||||
var content = createChildrenList(c -> StoreSection.customSection(c), Bindings.not(effectiveExpanded));
|
||||
|
||||
var expanded = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren().getList().size() > 0;
|
||||
},
|
||||
section.getWrapper().getExpanded(),
|
||||
section.getShownChildren().getList());
|
||||
var full = new VerticalComp(List.of(
|
||||
topEntryList,
|
||||
Comp.separator().hide(expanded.not()),
|
||||
content.styleClass("children-content")
|
||||
.hide(Bindings.or(
|
||||
Bindings.not(section.getWrapper().getExpanded()),
|
||||
Bindings.size(section.getShownChildren().getList())
|
||||
.isEqualTo(0)))));
|
||||
return full.styleClass("store-entry-section-comp")
|
||||
.apply(struc -> {
|
||||
Comp.separator().hide(Bindings.not(effectiveExpanded)),
|
||||
content));
|
||||
full.styleClass("store-entry-section-comp");
|
||||
full.apply(struc -> {
|
||||
struc.get().setFillWidth(true);
|
||||
expanded.subscribe(val -> {
|
||||
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
||||
});
|
||||
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||
struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
|
||||
struc.get().pseudoClassStateChanged(ROOT, topLevel);
|
||||
struc.get().pseudoClassStateChanged(SUB, !topLevel);
|
||||
|
||||
section.getWrapper().getColor().subscribe(val -> {
|
||||
if (!topLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newList = new ArrayList<>(struc.get().getStyleClass());
|
||||
newList.removeIf(s -> Arrays.stream(DataColor.values())
|
||||
.anyMatch(
|
||||
dataStoreColor -> dataStoreColor.getId().equals(s)));
|
||||
newList.remove("gray");
|
||||
newList.add("color-box");
|
||||
if (val != null) {
|
||||
newList.add(val.getId());
|
||||
} else {
|
||||
newList.add("gray");
|
||||
}
|
||||
struc.get().getStyleClass().setAll(newList);
|
||||
});
|
||||
|
||||
section.getWrapper().getPerUser().subscribe(val -> {
|
||||
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val);
|
||||
});
|
||||
})
|
||||
.createStructure();
|
||||
var hbox = ((HBox) struc.get().getChildren().getFirst());
|
||||
addPseudoClassListeners(struc.get(), section.getWrapper().getExpanded());
|
||||
addVisibilityListeners(struc.get(), hbox);
|
||||
});
|
||||
return full.createStructure();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,23 +12,18 @@ import javafx.beans.property.SimpleBooleanProperty;
|
|||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
||||
public class StoreSectionMiniComp extends StoreSectionBaseComp {
|
||||
|
||||
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
|
||||
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
|
||||
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
|
||||
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
|
||||
private static final PseudoClass TOP = PseudoClass.getPseudoClass("top");
|
||||
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
|
||||
|
||||
private final StoreSection section;
|
||||
private final BooleanProperty expanded;
|
||||
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment;
|
||||
private final Consumer<StoreSection> action;
|
||||
|
||||
|
@ -36,142 +31,61 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
|||
StoreSection section,
|
||||
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment,
|
||||
Consumer<StoreSection> action) {
|
||||
this.section = section;
|
||||
super(section);
|
||||
this.augment = augment;
|
||||
this.action = action;
|
||||
this.expanded = new SimpleBooleanProperty(section.getWrapper() == null || section.getWrapper().getExpanded().getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<VBox> createBase() {
|
||||
var list = new ArrayList<Comp<?>>();
|
||||
BooleanProperty expanded;
|
||||
if (section.getWrapper() != null) {
|
||||
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {})
|
||||
.apply(struc -> {
|
||||
struc.get()
|
||||
.setGraphic(PrettyImageHelper.ofFixedSize(
|
||||
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {
|
||||
action.accept(section);
|
||||
});
|
||||
root.hgrow();
|
||||
root.maxWidth(2000);
|
||||
root.styleClass("item");
|
||||
root.apply(struc -> {
|
||||
struc.get().setAlignment(Pos.CENTER_LEFT);
|
||||
struc.get().setGraphic(PrettyImageHelper.ofFixedSize(
|
||||
section.getWrapper().getIconFile(), 16, 16)
|
||||
.createRegion());
|
||||
})
|
||||
.apply(struc -> {
|
||||
struc.get().setAlignment(Pos.CENTER_LEFT);
|
||||
})
|
||||
.apply(struc -> {
|
||||
struc.get().setOnAction(event -> {
|
||||
action.accept(section);
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
.grow(true, false)
|
||||
.apply(struc -> struc.get().setMnemonicParsing(false))
|
||||
.styleClass("item");
|
||||
struc.get().setMnemonicParsing(false);
|
||||
});
|
||||
augment.accept(section, root);
|
||||
|
||||
expanded =
|
||||
new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren().getList().size() > 0);
|
||||
var button = new IconButtonComp(
|
||||
Bindings.createObjectBinding(
|
||||
() -> new LabelGraphic.IconGraphic(
|
||||
expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"),
|
||||
expanded),
|
||||
() -> {
|
||||
expanded.set(!expanded.get());
|
||||
})
|
||||
.apply(struc -> struc.get().setMinWidth(20))
|
||||
.apply(struc -> struc.get().setPrefWidth(20))
|
||||
.focusTraversable()
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Expand "
|
||||
+ section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
|
||||
.grow(false, true)
|
||||
.styleClass("expand-button");
|
||||
var expandButton = createExpandButton(() -> expanded.set(!expanded.get()), 20, expanded);
|
||||
expandButton.focusTraversable();
|
||||
|
||||
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
Consumer<StoreSection> quickAccessAction = action;
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
|
||||
.vgrow()
|
||||
.styleClass("quick-access-button")
|
||||
.maxHeight(100)
|
||||
.disable(quickAccessDisabled);
|
||||
var quickAccessButton = createQuickAccessButton(20, action);
|
||||
|
||||
var buttonList = new ArrayList<Comp<?>>();
|
||||
buttonList.add(button);
|
||||
buttonList.add(expandButton);
|
||||
buttonList.add(root);
|
||||
if (section.getDepth() == 1) {
|
||||
buttonList.add(quickAccessButton);
|
||||
}
|
||||
list.add(new HorizontalComp(buttonList).apply(struc -> struc.get().setFillHeight(true)));
|
||||
} else {
|
||||
expanded = new SimpleBooleanProperty(true);
|
||||
var h = new HorizontalComp(buttonList);
|
||||
h.apply(struc -> struc.get().setFillHeight(true));
|
||||
h.prefHeight(28);
|
||||
list.add(h);
|
||||
}
|
||||
|
||||
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
||||
// section is actually expanded
|
||||
var listSections = section.getWrapper() != null
|
||||
? section.getShownChildren()
|
||||
.filtered(
|
||||
storeSection ->
|
||||
section.getAllChildren().getList().size() <= 20 || expanded.get(),
|
||||
expanded,
|
||||
section.getAllChildren().getList())
|
||||
: section.getShownChildren();
|
||||
var content = new ListBoxViewComp<>(
|
||||
listSections.getList(),
|
||||
section.getAllChildren().getList(),
|
||||
(StoreSection e) -> {
|
||||
return new StoreSectionMiniComp(e, this.augment, this.action);
|
||||
},
|
||||
section.getWrapper() == null)
|
||||
.minHeight(0)
|
||||
.hgrow();
|
||||
var content = createChildrenList(c -> new StoreSectionMiniComp(c, this.augment, this.action), Bindings.not(expanded));
|
||||
list.add(content);
|
||||
|
||||
list.add(content.styleClass("children-content")
|
||||
.hide(Bindings.or(
|
||||
Bindings.not(expanded),
|
||||
Bindings.size(section.getAllChildren().getList()).isEqualTo(0))));
|
||||
|
||||
var vert = new VerticalComp(list);
|
||||
return vert.styleClass("store-section-mini-comp")
|
||||
.apply(struc -> {
|
||||
var full = new VerticalComp(list);
|
||||
full.styleClass("store-section-mini-comp");
|
||||
full.apply(struc -> {
|
||||
struc.get().setFillWidth(true);
|
||||
expanded.subscribe(val -> {
|
||||
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
||||
});
|
||||
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||
struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
|
||||
struc.get().pseudoClassStateChanged(ROOT, section.getDepth() == 0);
|
||||
struc.get().pseudoClassStateChanged(TOP, section.getDepth() == 1);
|
||||
struc.get().pseudoClassStateChanged(SUB, section.getDepth() > 1);
|
||||
})
|
||||
.apply(struc -> {
|
||||
addPseudoClassListeners(struc.get(), expanded);
|
||||
if (section.getWrapper() != null) {
|
||||
section.getWrapper().getColor().subscribe(val -> {
|
||||
if (section.getDepth() != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataColor.values())
|
||||
.anyMatch(dataStoreColor ->
|
||||
dataStoreColor.getId().equals(s)));
|
||||
struc.get().getStyleClass().remove("gray");
|
||||
struc.get().getStyleClass().add("color-box");
|
||||
if (val != null) {
|
||||
struc.get().getStyleClass().add(val.getId());
|
||||
} else {
|
||||
struc.get().getStyleClass().add("gray");
|
||||
}
|
||||
});
|
||||
var hbox = ((HBox) struc.get().getChildren().getFirst());
|
||||
addVisibilityListeners(struc.get(), hbox);
|
||||
}
|
||||
})
|
||||
.createStructure();
|
||||
});
|
||||
return full.createStructure();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface StoreSortMode {
|
||||
|
||||
StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() {
|
||||
@Override
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
|
@ -27,11 +20,6 @@ public interface StoreSortMode {
|
|||
}
|
||||
};
|
||||
StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() {
|
||||
@Override
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "alphabetical-asc";
|
||||
|
@ -44,10 +32,10 @@ public interface StoreSortMode {
|
|||
.reversed();
|
||||
}
|
||||
};
|
||||
StoreSortMode DATE_DESC = new StoreSortMode() {
|
||||
StoreSortMode DATE_DESC = new StoreSortMode.DateSortMode() {
|
||||
|
||||
private Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccessApplied().getValue();
|
||||
protected Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccess().getValue();
|
||||
if (la == null) {
|
||||
return Instant.MAX;
|
||||
}
|
||||
|
@ -56,35 +44,19 @@ public interface StoreSortMode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return Stream.concat(
|
||||
s.getShownChildren().getList().stream()
|
||||
.filter(section -> section.getWrapper()
|
||||
.getEntry()
|
||||
.getValidity()
|
||||
.isUsable())
|
||||
.map(this::representative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
protected int compare(Instant s1, Instant s2) {
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "date-desc";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<StoreSection> comparator() {
|
||||
return Comparator.comparing(e -> {
|
||||
return date(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
StoreSortMode DATE_ASC = new StoreSortMode() {
|
||||
StoreSortMode DATE_ASC = new StoreSortMode.DateSortMode() {
|
||||
|
||||
private Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccessApplied().getValue();
|
||||
protected Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccess().getValue();
|
||||
if (la == null) {
|
||||
return Instant.MIN;
|
||||
}
|
||||
|
@ -93,32 +65,16 @@ public interface StoreSortMode {
|
|||
}
|
||||
|
||||
@Override
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return Stream.concat(
|
||||
s.getShownChildren().getList().stream()
|
||||
.filter(section -> section.getWrapper()
|
||||
.getEntry()
|
||||
.getValidity()
|
||||
.isUsable())
|
||||
.map(this::representative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
protected int compare(Instant s1, Instant s2) {
|
||||
return s2.compareTo(s1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "date-asc";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<StoreSection> comparator() {
|
||||
return Comparator.<StoreSection, Instant>comparing(e -> {
|
||||
return date(e);
|
||||
})
|
||||
.reversed();
|
||||
}
|
||||
};
|
||||
|
||||
List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);
|
||||
|
||||
static Optional<StoreSortMode> fromId(String id) {
|
||||
|
@ -131,9 +87,54 @@ public interface StoreSortMode {
|
|||
return DATE_ASC;
|
||||
}
|
||||
|
||||
StoreSection representative(StoreSection s);
|
||||
|
||||
String getId();
|
||||
|
||||
Comparator<StoreSection> comparator();
|
||||
|
||||
abstract class DateSortMode implements StoreSortMode {
|
||||
|
||||
private int entriesListOberservableIndex = -1;
|
||||
private final Map<StoreSection, StoreSection> cachedRepresentatives = new IdentityHashMap<>();
|
||||
|
||||
private StoreSection computeRepresentative(StoreSection s) {
|
||||
return Stream.concat(
|
||||
s.getShownChildren().getList().stream()
|
||||
.filter(section -> section.getWrapper()
|
||||
.getEntry()
|
||||
.getValidity()
|
||||
.isUsable())
|
||||
.map(this::getRepresentative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
private StoreSection getRepresentative(StoreSection s) {
|
||||
if (StoreViewState.get().getEntriesListUpdateObservable().get() != entriesListOberservableIndex) {
|
||||
cachedRepresentatives.clear();
|
||||
entriesListOberservableIndex = StoreViewState.get().getEntriesListUpdateObservable().get();
|
||||
}
|
||||
|
||||
if (cachedRepresentatives.containsKey(s)) {
|
||||
return cachedRepresentatives.get(s);
|
||||
}
|
||||
|
||||
var r = computeRepresentative(s);
|
||||
cachedRepresentatives.put(s, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
protected abstract Instant date(StoreSection s);
|
||||
|
||||
protected abstract int compare(Instant s1, Instant s2);
|
||||
|
||||
@Override
|
||||
public Comparator<StoreSection> comparator() {
|
||||
return (o1, o2) -> {
|
||||
var r1 = getRepresentative(o1);
|
||||
var r2 = getRepresentative(o2);
|
||||
return DateSortMode.this.compare(date(r1), date(r2));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,6 @@ public class StoreViewState {
|
|||
.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()
|
||||
.map(StoreEntryWrapper::new)
|
||||
.toList()));
|
||||
allEntries.getList().forEach(e -> e.applyLastAccess());
|
||||
categories
|
||||
.getList()
|
||||
.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()
|
||||
|
@ -153,7 +152,6 @@ public class StoreViewState {
|
|||
}
|
||||
|
||||
public void updateDisplay() {
|
||||
allEntries.getList().forEach(e -> e.applyLastAccess());
|
||||
toggleStoreListUpdate();
|
||||
}
|
||||
|
||||
|
@ -192,7 +190,6 @@ public class StoreViewState {
|
|||
var l = Arrays.stream(entry)
|
||||
.map(StoreEntryWrapper::new)
|
||||
.peek(storeEntryWrapper -> storeEntryWrapper.update())
|
||||
.peek(wrapper -> wrapper.applyLastAccess())
|
||||
.toList();
|
||||
|
||||
// Don't update anything if we have already reset
|
||||
|
|
|
@ -20,10 +20,6 @@ public class AppFont {
|
|||
// Load ikonli fonts
|
||||
TrackEvent.info("Loading ikonli fonts ...");
|
||||
new FontIcon("mdi2s-stop");
|
||||
new FontIcon("mdi2m-magnify");
|
||||
new FontIcon("mdi2d-database-plus");
|
||||
new FontIcon("mdi2p-professional-hexagon");
|
||||
new FontIcon("mdi2c-chevron-double-right");
|
||||
|
||||
TrackEvent.info("Loading bundled fonts ...");
|
||||
AppResources.with(
|
||||
|
|
|
@ -56,10 +56,7 @@ public class AppInstance {
|
|||
try {
|
||||
var inputs = AppProperties.get().getArguments().getOpenArgs();
|
||||
// Assume that we want to open the GUI if we launched again
|
||||
client.get()
|
||||
.performRequest(DaemonFocusExchange.Request.builder()
|
||||
.mode(XPipeDaemonMode.GUI)
|
||||
.build());
|
||||
client.get().performRequest(DaemonFocusExchange.Request.builder().build());
|
||||
if (!inputs.isEmpty()) {
|
||||
client.get()
|
||||
.performRequest(DaemonOpenExchange.Request.builder()
|
||||
|
|
|
@ -210,19 +210,26 @@ public class AppTheme {
|
|||
}
|
||||
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
if (AppMainWindow.getInstance() == null) {
|
||||
var window = AppMainWindow.getInstance();
|
||||
if (window == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var window = AppMainWindow.getInstance().getStage();
|
||||
var scene = window.getScene();
|
||||
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 = scene.snapshot(null);
|
||||
ImageView imageView = new ImageView(snapshot);
|
||||
root.getChildren().add(imageView);
|
||||
|
||||
newTheme.apply();
|
||||
TrackEvent.debug("Set theme " + newTheme.getId() + " for scene");
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// Animate!
|
||||
|
@ -348,7 +355,7 @@ public class AppTheme {
|
|||
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
|
||||
() -> ColorHelper.withOpacity(
|
||||
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),
|
||||
115);
|
||||
91);
|
||||
|
||||
// Adjust this to create your own theme
|
||||
public static final Theme CUSTOM = new DerivedTheme(
|
||||
|
|
|
@ -91,6 +91,7 @@ public class BaseMode extends OperationMode {
|
|||
AppPrefs.setLocalDefaultsIfNeeded();
|
||||
PlatformInit.init(true);
|
||||
AppMainWindow.addUpdateTitleListener();
|
||||
TrackEvent.info("Shell initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
shellLoaded.await();
|
||||
|
@ -103,15 +104,16 @@ public class BaseMode extends OperationMode {
|
|||
DataStorage.init();
|
||||
storageLoaded.countDown();
|
||||
StoreViewState.init();
|
||||
AppMainWindow.loadingText("loadingUserInterface");
|
||||
AppLayoutModel.init();
|
||||
PlatformInit.init(true);
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
AppGreetingsDialog.showIfNeeded();
|
||||
AppMainWindow.loadingText("initializingApp");
|
||||
});
|
||||
imagesLoaded.await();
|
||||
browserLoaded.await();
|
||||
iconsLoaded.await();
|
||||
TrackEvent.info("Waiting for startup dialogs to close");
|
||||
AppDialog.waitForAllDialogsClose();
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
try {
|
||||
|
@ -121,6 +123,7 @@ public class BaseMode extends OperationMode {
|
|||
}
|
||||
});
|
||||
UpdateChangelogAlert.showIfNeeded();
|
||||
TrackEvent.info("Connection storage initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
AppFileWatcher.init();
|
||||
|
@ -128,6 +131,7 @@ public class BaseMode extends OperationMode {
|
|||
BlobManager.init();
|
||||
TerminalView.init();
|
||||
TerminalLauncherManager.init();
|
||||
TrackEvent.info("File/Watcher initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
PlatformInit.init(true);
|
||||
|
@ -136,13 +140,16 @@ public class BaseMode extends OperationMode {
|
|||
storageLoaded.await();
|
||||
SystemIconManager.init();
|
||||
iconsLoaded.countDown();
|
||||
TrackEvent.info("Platform initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
BrowserIconManager.loadIfNecessary();
|
||||
shellLoaded.await();
|
||||
BrowserLocalFileSystem.init();
|
||||
storageLoaded.await();
|
||||
BrowserFullSessionModel.init();
|
||||
browserLoaded.countDown();
|
||||
TrackEvent.info("Browser initialization thread completed");
|
||||
});
|
||||
ActionProvider.initProviders();
|
||||
DataStoreProviders.init();
|
||||
|
|
|
@ -180,6 +180,8 @@ public abstract class OperationMode {
|
|||
|
||||
var startupMode = getStartupMode();
|
||||
switchToSyncOrThrow(map(startupMode));
|
||||
// If it doesn't find time, the JVM will not gc the startup workload
|
||||
System.gc();
|
||||
inStartup = false;
|
||||
AppOpenArguments.init();
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ public class AppDialog {
|
|||
var transition = new PauseTransition(Duration.millis(200));
|
||||
transition.setOnFinished(e -> {
|
||||
if (wait) {
|
||||
Platform.exitNestedEventLoop(key, null);
|
||||
PlatformThread.exitNestedEventLoop(key);
|
||||
}
|
||||
});
|
||||
transition.play();
|
||||
|
@ -95,7 +95,7 @@ public class AppDialog {
|
|||
}
|
||||
});
|
||||
if (wait) {
|
||||
Platform.enterNestedEventLoop(key);
|
||||
PlatformThread.enterNestedEventLoop(key);
|
||||
waitForDialogClose(o);
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ public class AppDialog {
|
|||
public static Comp<?> dialogText(String s) {
|
||||
return Comp.of(() -> {
|
||||
var text = new Text(s);
|
||||
text.getStyleClass().add("dialog-text");
|
||||
text.setWrappingWidth(450);
|
||||
var sp = new StackPane(text);
|
||||
return sp;
|
||||
|
@ -118,6 +119,7 @@ public class AppDialog {
|
|||
public static Comp<?> dialogText(ObservableValue<String> s) {
|
||||
return Comp.of(() -> {
|
||||
var text = new Text();
|
||||
text.getStyleClass().add("dialog-text");
|
||||
text.textProperty().bind(s);
|
||||
text.setWrappingWidth(450);
|
||||
var sp = new StackPane(text);
|
||||
|
|
|
@ -138,8 +138,10 @@ public class AppMainWindow {
|
|||
}
|
||||
|
||||
public static synchronized void initContent() {
|
||||
TrackEvent.info("Window content node creation started");
|
||||
var content = new AppLayoutComp();
|
||||
var s = content.createStructure();
|
||||
TrackEvent.info("Window content node structure created");
|
||||
loadedContent.setValue(s);
|
||||
}
|
||||
|
||||
|
@ -150,6 +152,13 @@ public class AppMainWindow {
|
|||
}
|
||||
}
|
||||
|
||||
public void focus() {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
stage.setIconified(false);
|
||||
stage.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
private static String createTitle() {
|
||||
var t = LicenseProvider.get().licenseTitle();
|
||||
var base =
|
||||
|
|
|
@ -124,10 +124,13 @@ public class ModifiedStage extends Stage {
|
|||
var transition = new PauseTransition(Duration.millis(300));
|
||||
transition.setOnFinished(e -> {
|
||||
applyModes(stage);
|
||||
stage.setWidth(stage.getWidth() - 1);
|
||||
Platform.runLater(() -> {
|
||||
stage.setWidth(stage.getWidth() + 1);
|
||||
});
|
||||
// We only need to update the frame by resizing on Windows
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
stage.setWidth(stage.getWidth() - 1);
|
||||
Platform.runLater(() -> {
|
||||
stage.setWidth(stage.getWidth() + 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
transition.play();
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.ext;
|
|||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.util.FailableConsumer;
|
||||
|
@ -21,6 +22,7 @@ public interface ActionProvider {
|
|||
List<ActionProvider> ALL_STANDALONE = new ArrayList<>();
|
||||
|
||||
static void initProviders() {
|
||||
TrackEvent.trace("Starting action provider initialization");
|
||||
for (ActionProvider actionProvider : ALL) {
|
||||
try {
|
||||
actionProvider.init();
|
||||
|
@ -28,6 +30,7 @@ public interface ActionProvider {
|
|||
ErrorEvent.fromThrowable(t).handle();
|
||||
}
|
||||
}
|
||||
TrackEvent.trace("Finished action provider initialization");
|
||||
}
|
||||
|
||||
default void init() throws Exception {}
|
||||
|
|
|
@ -93,8 +93,8 @@ public interface DataStoreProvider {
|
|||
return StoreEntryComp.create(s, null, preferLarge);
|
||||
}
|
||||
|
||||
default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) {
|
||||
return new StoreSectionComp(section, topLevel);
|
||||
default StoreSectionComp customSectionComp(StoreSection section) {
|
||||
return new StoreSectionComp(section);
|
||||
}
|
||||
|
||||
default boolean shouldShowScan() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.github.weisj.jsvg.SVGDocument;
|
|||
import com.github.weisj.jsvg.SVGRenderingHints;
|
||||
import com.github.weisj.jsvg.attributes.ViewBox;
|
||||
import com.github.weisj.jsvg.parser.SVGLoader;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.awt.*;
|
||||
|
@ -65,6 +66,8 @@ public class SystemIconCache {
|
|||
|
||||
var scheme = rasterizeSizes(icon.getFile(), target, icon.getName(), dark);
|
||||
if (scheme == ImageColorScheme.TRANSPARENT) {
|
||||
var message = "Failed to rasterize icon icon " + icon.getFile().getFileName().toString() + ": Rasterized image is transparent";
|
||||
ErrorEvent.fromMessage(message).omit().expected().handle();
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -104,6 +107,7 @@ public class SystemIconCache {
|
|||
}
|
||||
|
||||
private static ImageColorScheme rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
|
||||
TrackEvent.trace("Rasterizing image " + path.getFileName().toString());
|
||||
try {
|
||||
ImageColorScheme c = null;
|
||||
for (var size : sizes) {
|
||||
|
@ -121,11 +125,8 @@ public class SystemIconCache {
|
|||
}
|
||||
return c;
|
||||
} catch (Exception ex) {
|
||||
if (ex instanceof IOException) {
|
||||
throw ex;
|
||||
}
|
||||
|
||||
ErrorEvent.fromThrowable(ex).omit().expected().handle();
|
||||
var message = "Failed to rasterize icon icon " + path.getFileName().toString() + ": " + ex.getMessage();
|
||||
ErrorEvent.fromThrowable(ex).description(message).omit().expected().handle();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -195,6 +196,7 @@ public class SystemIconCache {
|
|||
}
|
||||
|
||||
private static ImageColorScheme determineColorScheme(BufferedImage image) {
|
||||
var transparent = true;
|
||||
var counter = 0;
|
||||
var mean = 0.0;
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
|
@ -205,6 +207,10 @@ public class SystemIconCache {
|
|||
int green = (clr & 0x0000ff00) >> 8;
|
||||
int blue = clr & 0x000000ff;
|
||||
|
||||
if (alpha > 0) {
|
||||
transparent = false;
|
||||
}
|
||||
|
||||
if (alpha < 200) {
|
||||
continue;
|
||||
}
|
||||
|
@ -214,7 +220,7 @@ public class SystemIconCache {
|
|||
}
|
||||
}
|
||||
|
||||
if (counter == 0) {
|
||||
if (transparent) {
|
||||
return ImageColorScheme.TRANSPARENT;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package io.xpipe.app.icon;
|
||||
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
|
@ -90,7 +92,14 @@ public interface SystemIconSource {
|
|||
@Override
|
||||
public void refresh() throws Exception {
|
||||
try (var sc =
|
||||
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
|
||||
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
|
||||
var present = sc.view().findProgram("git").isPresent();
|
||||
if (!present) {
|
||||
var msg = "Git command-line tools are not available in the PATH but are required to use icons from a git repository. For more details, see https://git-scm.com/downloads.";
|
||||
ErrorEvent.fromMessage(msg).expected().handle();
|
||||
return;
|
||||
}
|
||||
|
||||
var dir = SystemIconManager.getPoolPath().resolve(id);
|
||||
if (!Files.exists(dir)) {
|
||||
sc.command(CommandBuilder.of()
|
||||
|
|
|
@ -39,6 +39,14 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
return hasEmail || hasText;
|
||||
}
|
||||
|
||||
private static boolean doesExceedCommentSize(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return text.length() > 5000;
|
||||
}
|
||||
|
||||
private static Throwable adjustCopy(Throwable throwable, boolean clear) {
|
||||
if (throwable == null) {
|
||||
return null;
|
||||
|
@ -139,6 +147,16 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
atts.forEach(attachment -> s.addAttachment(attachment));
|
||||
}
|
||||
|
||||
if (doesExceedCommentSize(ee.getUserReport())) {
|
||||
try {
|
||||
var report = Files.createTempFile("report", ".txt");
|
||||
Files.writeString(report, ee.getUserReport());
|
||||
s.addAttachment(new Attachment(report.toString()));
|
||||
} catch (Exception ex) {
|
||||
AppLogs.get().logException("Unable to create report file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
s.setTag(
|
||||
"hasLicense",
|
||||
String.valueOf(
|
||||
|
@ -176,7 +194,7 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
AppPrefs.get() != null
|
||||
? String.valueOf(AppPrefs.get().useLocalFallbackShell().get())
|
||||
: "unknown");
|
||||
s.setTag("initial", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + "" : null);
|
||||
s.setTag("initial", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + "" : "false");
|
||||
|
||||
var exMessage = ee.getThrowable() != null ? ee.getThrowable().getMessage() : null;
|
||||
if (ee.getDescription() != null
|
||||
|
@ -231,7 +249,11 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
if (hasEmail) {
|
||||
fb.setEmail(email);
|
||||
}
|
||||
fb.setComments(text);
|
||||
if (doesExceedCommentSize(text)) {
|
||||
fb.setComments("<Attachment>");
|
||||
} else {
|
||||
fb.setComments(text);
|
||||
}
|
||||
Sentry.captureUserFeedback(fb);
|
||||
}
|
||||
Sentry.flush(3000);
|
||||
|
|
|
@ -117,6 +117,8 @@ public class AppPrefs {
|
|||
mapLocal(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class, false);
|
||||
final StringProperty customEditorCommand =
|
||||
mapLocal(new SimpleStringProperty(""), "customEditorCommand", String.class, false);
|
||||
final BooleanProperty customEditorCommandInTerminal =
|
||||
mapLocal(new SimpleBooleanProperty(false), "customEditorCommandInTerminal", Boolean.class, false);
|
||||
final BooleanProperty automaticallyCheckForUpdates =
|
||||
mapLocal(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class, false);
|
||||
final BooleanProperty encryptAllVaultData =
|
||||
|
@ -405,6 +407,10 @@ public class AppPrefs {
|
|||
return customEditorCommand;
|
||||
}
|
||||
|
||||
public ObservableBooleanValue customEditorCommandInTerminal() {
|
||||
return customEditorCommandInTerminal;
|
||||
}
|
||||
|
||||
public final ReadOnlyIntegerProperty editorReloadTimeout() {
|
||||
return editorReloadTimeout;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@ public class AppPrefsComp extends SimpleComp {
|
|||
split.setFillHeight(true);
|
||||
split.getStyleClass().add("prefs");
|
||||
var stack = new StackPane(split);
|
||||
stack.setPickOnBounds(false);
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,19 +14,22 @@ public class ConnectionsCategory extends AppPrefsCategory {
|
|||
@Override
|
||||
protected Comp<?> create() {
|
||||
var prefs = AppPrefs.get();
|
||||
var options = new OptionsBuilder()
|
||||
.addTitle("connections")
|
||||
.sub(new OptionsBuilder()
|
||||
.pref(prefs.condenseConnectionDisplay)
|
||||
.addToggle(prefs.condenseConnectionDisplay)
|
||||
.pref(prefs.showChildCategoriesInParentCategory)
|
||||
.addToggle(prefs.showChildCategoriesInParentCategory)
|
||||
.pref(prefs.openConnectionSearchWindowOnConnectionCreation)
|
||||
.addToggle(prefs.openConnectionSearchWindowOnConnectionCreation)
|
||||
.pref(prefs.requireDoubleClickForConnections)
|
||||
.addToggle(prefs.requireDoubleClickForConnections))
|
||||
var connectionsBuilder = new OptionsBuilder().pref(prefs.condenseConnectionDisplay).addToggle(prefs.condenseConnectionDisplay).pref(
|
||||
prefs.showChildCategoriesInParentCategory).addToggle(prefs.showChildCategoriesInParentCategory).pref(
|
||||
prefs.openConnectionSearchWindowOnConnectionCreation).addToggle(prefs.openConnectionSearchWindowOnConnectionCreation).pref(
|
||||
prefs.requireDoubleClickForConnections).addToggle(prefs.requireDoubleClickForConnections);
|
||||
var localShellBuilder = new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell);
|
||||
// Change order to prioritize fallback shell on macOS
|
||||
var options = OsType.getLocal() == OsType.MACOS ? new OptionsBuilder()
|
||||
.addTitle("localShell")
|
||||
.sub(new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell));
|
||||
.sub(localShellBuilder)
|
||||
.addTitle("connections")
|
||||
.sub(connectionsBuilder) :
|
||||
new OptionsBuilder()
|
||||
.addTitle("connections")
|
||||
.sub(connectionsBuilder)
|
||||
.addTitle("localShell")
|
||||
.sub(localShellBuilder);
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
options.addTitle("sshConfiguration")
|
||||
.sub(new OptionsBuilder()
|
||||
|
|
|
@ -47,9 +47,13 @@ public class EditorCategory extends AppPrefsCategory {
|
|||
prefs.externalEditor, PrefsChoiceValue.getSupported(ExternalEditorType.class), false))
|
||||
.nameAndDescription("customEditorCommand")
|
||||
.addComp(new TextFieldComp(prefs.customEditorCommand, true)
|
||||
.apply(struc -> struc.get().setPromptText("myeditor $FILE"))
|
||||
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM)))
|
||||
.addComp(terminalTest))
|
||||
.apply(struc -> struc.get().setPromptText("myeditor $FILE")))
|
||||
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))
|
||||
.addComp(terminalTest)
|
||||
.nameAndDescription("customEditorCommandInTerminal")
|
||||
.addToggle(prefs.customEditorCommandInTerminal)
|
||||
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))
|
||||
)
|
||||
.buildComp();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.prefs;
|
|||
|
||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.WindowsRegistry;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
|
@ -171,10 +172,13 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
throw ErrorEvent.expected(new IllegalStateException("No custom editor command specified"));
|
||||
}
|
||||
|
||||
var format =
|
||||
customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
|
||||
ExternalApplicationHelper.startAsync(CommandBuilder.of()
|
||||
.add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString())));
|
||||
var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
|
||||
var command = CommandBuilder.of().add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString()));
|
||||
if (AppPrefs.get().customEditorCommandInTerminal().get()) {
|
||||
TerminalLauncher.openDirect(file.toString(), sc -> command.buildFull(sc), AppPrefs.get().terminalType.get());
|
||||
} else {
|
||||
ExternalApplicationHelper.startAsync(command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -108,7 +108,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
ThreadHelper.runFailableAsync(() -> {
|
||||
// Startup is slow
|
||||
ThreadHelper.sleep(10000);
|
||||
Files.delete(config);
|
||||
FileUtils.deleteQuietly(config.toFile());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -125,8 +125,12 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
|
||||
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
|
||||
launch(configuration.getTitle(), CommandBuilder.of().addFile(file.toString()).add("/cert-ignore").add("/p:'" + escapedPw + "'"));
|
||||
var b = CommandBuilder.of().addFile(file.toString()).add("/cert-ignore");
|
||||
if (configuration.getPassword() != null) {
|
||||
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
|
||||
b.add("/p:'" + escapedPw + "'");
|
||||
}
|
||||
launch(configuration.getTitle(), b);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -15,6 +15,7 @@ import javafx.beans.property.SimpleStringProperty;
|
|||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -111,8 +112,13 @@ public class IconsCategory extends AppPrefsCategory {
|
|||
return;
|
||||
}
|
||||
|
||||
var path = Path.of(dir.get());
|
||||
if (Files.isRegularFile(path)) {
|
||||
throw new IllegalArgumentException("A custom icon directory requires to be a directory of .svg files, not a single file");
|
||||
}
|
||||
|
||||
var source = SystemIconSource.Directory.builder()
|
||||
.path(Path.of(dir.get()))
|
||||
.path(path)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
if (!sources.contains(source)) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.xpipe.app.comp.base.IntegratedTextAreaComp;
|
|||
import io.xpipe.app.comp.base.LabelComp;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
|
@ -115,6 +116,7 @@ public class PasswordManagerCategory extends AppPrefsCategory {
|
|||
.minHeight(120);
|
||||
var templates = Comp.of(() -> {
|
||||
var cb = new MenuButton();
|
||||
AppFontSizes.base(cb);
|
||||
cb.textProperty().bind(BindingsHelper.flatMap(prefs.passwordManager, externalPasswordManager -> {
|
||||
return externalPasswordManager != null
|
||||
? AppI18n.observable(externalPasswordManager.getId())
|
||||
|
@ -145,6 +147,7 @@ public class PasswordManagerCategory extends AppPrefsCategory {
|
|||
new TextFieldComp(testPasswordManagerValue)
|
||||
.apply(struc -> struc.get().setPromptText("Enter password key"))
|
||||
.styleClass(Styles.LEFT_PILL)
|
||||
.prefWidth(400)
|
||||
.apply(struc -> struc.get().setOnKeyPressed(event -> {
|
||||
if (event.getCode() == KeyCode.ENTER) {
|
||||
test.run();
|
||||
|
@ -153,14 +156,17 @@ public class PasswordManagerCategory extends AppPrefsCategory {
|
|||
})),
|
||||
new ButtonComp(null, new FontIcon("mdi2p-play"), test).styleClass(Styles.RIGHT_PILL)));
|
||||
testInput.apply(struc -> {
|
||||
struc.get().setFillHeight(true);
|
||||
var first = ((Region) struc.get().getChildren().get(0));
|
||||
var second = ((Region) struc.get().getChildren().get(1));
|
||||
second.minHeightProperty().bind(first.heightProperty());
|
||||
second.maxHeightProperty().bind(first.heightProperty());
|
||||
second.prefHeightProperty().bind(first.heightProperty());
|
||||
});
|
||||
|
||||
var testPasswordManager = new HorizontalComp(List.of(
|
||||
testInput, Comp.hspacer(25), new LabelComp(testPasswordManagerResult).apply(struc -> struc.get()
|
||||
.setOpacity(0.5))))
|
||||
.setOpacity(0.8))))
|
||||
.padding(new Insets(10, 0, 0, 0))
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
|
||||
.apply(struc -> struc.get().setFillHeight(true));
|
||||
|
|
|
@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.*;
|
|||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.storage.DataStorageSyncHandler;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
|
@ -27,14 +28,6 @@ public class SyncCategory extends AppPrefsCategory {
|
|||
return "vaultSync";
|
||||
}
|
||||
|
||||
private static void showHelpAlert() {
|
||||
var md = AppI18n.get().getMarkdownDocumentation("vault");
|
||||
var markdown = new MarkdownComp(md, s -> s, true).prefWidth(600);
|
||||
var modal = ModalOverlay.of(markdown);
|
||||
modal.addButton(ModalButton.ok());
|
||||
AppDialog.show(modal);
|
||||
}
|
||||
|
||||
public Comp<?> create() {
|
||||
var prefs = AppPrefs.get();
|
||||
AtomicReference<Region> button = new AtomicReference<>();
|
||||
|
@ -61,7 +54,7 @@ public class SyncCategory extends AppPrefsCategory {
|
|||
|
||||
var remoteRepo = new TextFieldComp(prefs.storageGitRemote).hgrow();
|
||||
var helpButton = new ButtonComp(AppI18n.observable("help"), new FontIcon("mdi2h-help-circle-outline"), () -> {
|
||||
showHelpAlert();
|
||||
Hyperlinks.open(Hyperlinks.DOCS_SYNC);
|
||||
});
|
||||
var remoteRow = new HorizontalComp(List.of(remoteRepo, helpButton)).spacing(10);
|
||||
remoteRow.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
|
||||
|
|
|
@ -15,6 +15,7 @@ import io.xpipe.app.terminal.ExternalTerminalType;
|
|||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
import io.xpipe.app.util.*;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
|
@ -58,6 +59,10 @@ public class TerminalCategory extends AppPrefsCategory {
|
|||
var feature = LicenseProvider.get().getFeature("logging");
|
||||
if (newValue && !feature.isSupported()) {
|
||||
try {
|
||||
// Disable it again so people don't forget that they left it on
|
||||
Platform.runLater(() -> {
|
||||
prefs.enableTerminalLogging.set(false);
|
||||
});
|
||||
feature.throwIfUnsupported();
|
||||
} catch (LicenseRequiredException ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
|
|
|
@ -600,25 +600,6 @@ public abstract class DataStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public void deleteChildren(DataStoreEntry e) {
|
||||
var c = getDeepStoreChildren(e);
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
c.forEach(entry -> entry.finalizeEntry());
|
||||
this.storeEntriesSet.removeAll(c);
|
||||
synchronized (identityStoreEntryMapCache) {
|
||||
identityStoreEntryMapCache.remove(e.getStore());
|
||||
}
|
||||
synchronized (storeEntryMapCache) {
|
||||
storeEntryMapCache.remove(e.getStore());
|
||||
}
|
||||
this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new)));
|
||||
refreshEntries();
|
||||
saveAsync();
|
||||
}
|
||||
|
||||
public void deleteWithChildren(DataStoreEntry... entries) {
|
||||
List<DataStoreEntry> toDelete = Arrays.stream(entries)
|
||||
.flatMap(entry -> {
|
||||
|
|
|
@ -34,7 +34,10 @@ public class DataStoreEntryRef<T extends DataStore> {
|
|||
}
|
||||
|
||||
public void checkComplete() throws Throwable {
|
||||
getStore().checkComplete();
|
||||
var store = getStore();
|
||||
if (store != null) {
|
||||
getStore().checkComplete();
|
||||
}
|
||||
}
|
||||
|
||||
public DataStoreEntry get() {
|
||||
|
@ -42,7 +45,7 @@ public class DataStoreEntryRef<T extends DataStore> {
|
|||
}
|
||||
|
||||
public T getStore() {
|
||||
return entry.getStore().asNeeded();
|
||||
return entry.getStore() != null ? entry.getStore().asNeeded() : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
|
@ -59,14 +59,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
// };
|
||||
|
||||
static ExternalTerminalType determineFallbackTerminalToOpen(ExternalTerminalType type) {
|
||||
if (type == XSHELL || type == MOBAXTERM || type == SECURECRT) {
|
||||
return ProcessControlProvider.get().getEffectiveLocalDialect() == ShellDialects.CMD ? CMD : POWERSHELL;
|
||||
}
|
||||
|
||||
if (type != TERMIUS && type instanceof WaveTerminalType) {
|
||||
if (type != XSHELL && type != MOBAXTERM && type != SECURECRT && type != TERMIUS && !(type instanceof WaveTerminalType)) {
|
||||
return type;
|
||||
}
|
||||
|
||||
// Fallback to an available default
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {
|
||||
// This should not be termius or wave as all others take precedence
|
||||
|
@ -643,7 +640,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
};
|
||||
ExternalTerminalType WARP = new WarpTerminalType();
|
||||
ExternalTerminalType CUSTOM = new CustomTerminalType();
|
||||
List<ExternalTerminalType> WINDOWS_TERMINALS = List.of(
|
||||
WindowsTerminalType.WINDOWS_TERMINAL_CANARY,
|
||||
|
@ -651,6 +647,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
WindowsTerminalType.WINDOWS_TERMINAL,
|
||||
AlacrittyTerminalType.ALACRITTY_WINDOWS,
|
||||
WezTerminalType.WEZTERM_WINDOWS,
|
||||
WarpTerminalType.WINDOWS,
|
||||
CMD,
|
||||
PWSH,
|
||||
POWERSHELL,
|
||||
|
@ -682,10 +679,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
DEEPIN_TERMINAL,
|
||||
FOOT,
|
||||
Q_TERMINAL,
|
||||
WarpTerminalType.LINUX,
|
||||
TERMIUS,
|
||||
WaveTerminalType.WAVE_LINUX);
|
||||
List<ExternalTerminalType> MACOS_TERMINALS = List.of(
|
||||
WARP,
|
||||
WarpTerminalType.MACOS,
|
||||
ITERM2,
|
||||
KittyTerminalType.KITTY_MACOS,
|
||||
TabbyTerminalType.TABBY_MAC_OS,
|
||||
|
|
|
@ -2,8 +2,10 @@ package io.xpipe.app.terminal;
|
|||
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.ScriptHelper;
|
||||
import io.xpipe.app.util.SecretManager;
|
||||
import io.xpipe.app.util.SecretQueryProgress;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
|
@ -77,7 +79,7 @@ public class TerminalLauncherManager {
|
|||
req = entries.get(request);
|
||||
}
|
||||
if (req == null) {
|
||||
throw new BeaconClientException("Unknown launch request " + request);
|
||||
return;
|
||||
}
|
||||
var byPid = ProcessHandle.of(pid);
|
||||
if (byPid.isEmpty()) {
|
||||
|
@ -90,33 +92,44 @@ public class TerminalLauncherManager {
|
|||
req.setPid(shell.pid());
|
||||
}
|
||||
|
||||
public static Path waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
|
||||
public static void waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
|
||||
TerminalLaunchRequest req;
|
||||
synchronized (entries) {
|
||||
req = entries.get(request);
|
||||
}
|
||||
if (req == null) {
|
||||
throw new BeaconClientException("Unknown launch request " + request);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.isSetupCompleted()) {
|
||||
submitAsync(req.getRequest(), req.getProcessControl(), req.getConfig(), req.getWorkingDirectory());
|
||||
}
|
||||
try {
|
||||
return req.waitForCompletion();
|
||||
req.waitForCompletion();
|
||||
} finally {
|
||||
req.setSetupCompleted(true);
|
||||
}
|
||||
}
|
||||
|
||||
public static Path launchExchange(UUID request) throws BeaconClientException {
|
||||
public static Path launchExchange(UUID request) throws BeaconClientException, BeaconServerException {
|
||||
synchronized (entries) {
|
||||
var e = entries.values().stream()
|
||||
.filter(entry -> entry.getRequest().equals(request))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (e == null) {
|
||||
throw new BeaconClientException("Unknown launch request " + request);
|
||||
// It seems like that some terminals might enter a restart loop to try to start an older process again
|
||||
// This would spam XPipe continuously with launch requests if we returned an error here
|
||||
// Therefore, we just return a new local shell session
|
||||
TrackEvent.withTrace("Unknown launch request").tag("request", request.toString()).handle();
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var defaultShell = ProcessControlProvider.get().getEffectiveLocalDialect();
|
||||
var shellExec = defaultShell.getExecutableName();
|
||||
var script = ScriptHelper.createExecScript(sc, shellExec);
|
||||
return Path.of(script.toString());
|
||||
} catch (Exception ex) {
|
||||
throw new BeaconServerException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(e.getResult() instanceof TerminalLaunchResult.ResultSuccess)) {
|
||||
|
|
|
@ -1,57 +1,140 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.prefs.ExternalApplicationHelper;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.WindowsRegistry;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
import io.xpipe.core.process.TerminalInitFunction;
|
||||
|
||||
public class WarpTerminalType extends ExternalTerminalType.MacOsType {
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public WarpTerminalType() {
|
||||
super("app.warp", "Warp");
|
||||
public interface WarpTerminalType extends ExternalTerminalType, TrackableTerminalType {
|
||||
|
||||
static WarpTerminalType WINDOWS = new Windows();
|
||||
static WarpTerminalType LINUX = new Linux();
|
||||
static WarpTerminalType MACOS = new MacOs();
|
||||
|
||||
class Windows implements WarpTerminalType {
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
if (!configuration.isPreferTabs()) {
|
||||
DesktopHelper.openUrl("warp://action/new_window?path=" + configuration.getScriptFile());
|
||||
} else {
|
||||
DesktopHelper.openUrl("warp://action/new_tab?path=" + configuration.getScriptFile());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return WindowsRegistry.local().keyExists(WindowsRegistry.HKEY_CURRENT_USER, "Software\\Classes\\warp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "app.warp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
// Warp always opens the new separate window, so we don't want to use it in the file browser for docking
|
||||
// Just say that we don't support new windows, that way it doesn't dock
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Linux implements WarpTerminalType {
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
if (!configuration.isPreferTabs()) {
|
||||
DesktopHelper.openUrl("warp://action/new_window?path=" + configuration.getScriptFile());
|
||||
} else {
|
||||
DesktopHelper.openUrl("warp://action/new_tab?path=" + configuration.getScriptFile());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return Files.exists(Path.of("/opt/warpdotdev"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "app.warp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
// Warp always opens the new separate window, so we don't want to use it in the file browser for docking
|
||||
// Just say that we don't support new windows, that way it doesn't dock
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
}
|
||||
|
||||
class MacOs extends MacOsType implements WarpTerminalType {
|
||||
|
||||
public MacOs() {
|
||||
super("app.warp", "Warp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addQuoted("Warp.app")
|
||||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
default String getWebsite() {
|
||||
return "https://www.warp.dev/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
default boolean isRecommended() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useColoredTitle() {
|
||||
default boolean useColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldClear() {
|
||||
default boolean shouldClear() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addQuoted("Warp.app")
|
||||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalInitFunction additionalInitCommands() {
|
||||
default TerminalInitFunction additionalInitCommands() {
|
||||
return TerminalInitFunction.of(sc -> {
|
||||
if (sc.getShellDialect() == ShellDialects.ZSH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"zsh\"}}\\x9c'";
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.comp.base.ModalButton;
|
|||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppDistributionType;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
|
@ -182,7 +183,7 @@ public abstract class UpdateHandler {
|
|||
prepareUpdateImpl();
|
||||
|
||||
// Show available update in PTB more aggressively
|
||||
if (AppProperties.get().isStaging() && preparedUpdate.getValue() != null) {
|
||||
if (AppProperties.get().isStaging() && preparedUpdate.getValue() != null && !OperationMode.isInStartup()) {
|
||||
UpdateAvailableDialog.showIfNeeded();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.util;
|
|||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.core.process.CountDown;
|
||||
import io.xpipe.core.process.ElevationHandler;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.util.SecretReference;
|
||||
|
||||
|
@ -21,7 +20,7 @@ public class BaseElevationHandler implements ElevationHandler {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean handleRequest(ShellControl parent, UUID requestId, CountDown countDown, boolean confirmIfNeeded) {
|
||||
public boolean handleRequest(UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive) {
|
||||
var ref = getSecretRef();
|
||||
if (ref == null) {
|
||||
return false;
|
||||
|
@ -35,7 +34,7 @@ public class BaseElevationHandler implements ElevationHandler {
|
|||
List.of(),
|
||||
List.of(),
|
||||
countDown,
|
||||
parent.isInteractive());
|
||||
interactive);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import lombok.Value;
|
|||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
@ -15,12 +16,14 @@ import java.util.function.Function;
|
|||
@SuppressWarnings("InfiniteLoopStatement")
|
||||
public class BindingsHelper {
|
||||
|
||||
private static final Set<ReferenceEntry> REFERENCES = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
private static final Set<ReferenceEntry> REFERENCES = new HashSet<>();
|
||||
|
||||
static {
|
||||
ThreadHelper.createPlatformThread("referenceGC", true, () -> {
|
||||
while (true) {
|
||||
REFERENCES.removeIf(ReferenceEntry::canGc);
|
||||
synchronized (REFERENCES) {
|
||||
REFERENCES.removeIf(ReferenceEntry::canGc);
|
||||
}
|
||||
ThreadHelper.sleep(1000);
|
||||
|
||||
// Use for testing
|
||||
|
@ -31,7 +34,9 @@ public class BindingsHelper {
|
|||
}
|
||||
|
||||
public static void preserve(Object source, Object target) {
|
||||
REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));
|
||||
synchronized (REFERENCES) {
|
||||
REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));
|
||||
}
|
||||
}
|
||||
|
||||
public static <T, U> ObservableValue<U> map(
|
||||
|
|
|
@ -85,9 +85,19 @@ public class DerivedObservableList<T> {
|
|||
target.setAll(newList);
|
||||
}
|
||||
|
||||
private int indexOfFromStart(List<? extends T> list, T value, int start) {
|
||||
for (int i = start; i < list.size(); i++) {
|
||||
if (Objects.equals(list.get(i), value)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void setContentUnique(List<? extends T> newList) {
|
||||
var listSet = new HashSet<>(list);
|
||||
var newSet = new HashSet<>(newList);
|
||||
|
||||
// Addition
|
||||
if (newSet.containsAll(list)) {
|
||||
var l = new ArrayList<>(newList);
|
||||
|
@ -100,7 +110,7 @@ public class DerivedObservableList<T> {
|
|||
|
||||
var start = 0;
|
||||
for (int end = 0; end <= list.size(); end++) {
|
||||
var index = end < list.size() ? newList.indexOf(list.get(end)) : newList.size();
|
||||
var index = end < list.size() ? indexOfFromStart(newList, list.get(end), end) : newList.size();
|
||||
for (; start < index; start++) {
|
||||
list.add(start, newList.get(start));
|
||||
}
|
||||
|
@ -133,7 +143,8 @@ public class DerivedObservableList<T> {
|
|||
var cache = new HashMap<T, V>();
|
||||
var l1 = this.<V>createNewDerived();
|
||||
Runnable runnable = () -> {
|
||||
cache.keySet().removeIf(t -> !getList().contains(t));
|
||||
var listSet = new HashSet<>(list);
|
||||
cache.keySet().removeIf(t -> !listSet.contains(t));
|
||||
l1.setContent(list.stream()
|
||||
.map(v -> {
|
||||
if (!cache.containsKey(v)) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package io.xpipe.app.util;
|
||||
|
||||
import io.xpipe.app.core.AppDistributionType;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
@ -8,11 +10,48 @@ import io.xpipe.core.store.FileKind;
|
|||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class DesktopHelper {
|
||||
|
||||
private static final String[] browsers = {
|
||||
"xdg-open", "google-chrome", "firefox", "opera", "konqueror", "mozilla", "gnome-open", "open"
|
||||
};
|
||||
|
||||
public static void openUrl(String uri) {
|
||||
try {
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
var pb = new ProcessBuilder("rundll32", "url.dll,FileProtocolHandler", uri);
|
||||
pb.directory(new File(System.getProperty("user.home")));
|
||||
pb.redirectErrorStream(true);
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
|
||||
pb.start();
|
||||
} else if (OsType.getLocal() == OsType.LINUX) {
|
||||
String browser = null;
|
||||
for (String b : browsers) {
|
||||
if (browser == null
|
||||
&& Runtime.getRuntime()
|
||||
.exec(new String[] {"which", b})
|
||||
.getInputStream()
|
||||
.read()
|
||||
!= -1) {
|
||||
Runtime.getRuntime().exec(new String[] {browser = b, uri});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var pb = new ProcessBuilder("open", uri);
|
||||
pb.directory(new File(System.getProperty("user.home")));
|
||||
pb.redirectErrorStream(true);
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
|
||||
pb.start();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
}
|
||||
}
|
||||
|
||||
public static Path getDesktopDirectory() throws Exception {
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
return Path.of(LocalShell.getLocalPowershell()
|
||||
|
@ -90,47 +129,46 @@ public class DesktopHelper {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Files.exists(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
try {
|
||||
Desktop.getDesktop().open(file.toFile());
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).expected().handle();
|
||||
var xdg = OsType.getLocal() == OsType.LINUX;
|
||||
if (Desktop.getDesktop().isSupported(Desktop.Action.OPEN) && AppDistributionType.get() != AppDistributionType.WEBTOP) {
|
||||
try {
|
||||
Desktop.getDesktop().open(file.toFile());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).expected().omitted(xdg).handle();
|
||||
}
|
||||
}
|
||||
|
||||
if (xdg) {
|
||||
LocalExec.readStdoutIfPossible("xdg-open", file.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void browseFileInDirectory(Path file) {
|
||||
if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
|
||||
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
|
||||
ErrorEvent.fromMessage("Desktop integration unable to open file " + file)
|
||||
.expected()
|
||||
.handle();
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
try {
|
||||
Desktop.getDesktop().open(file.getParent().toFile());
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).expected().handle();
|
||||
}
|
||||
});
|
||||
browsePathLocal(file.getParent());
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
try {
|
||||
Desktop.getDesktop().browseFileDirectory(file.toFile());
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).expected().handle();
|
||||
var xdg = OsType.getLocal() == OsType.LINUX;
|
||||
if (AppDistributionType.get() != AppDistributionType.WEBTOP) {
|
||||
try {
|
||||
Desktop.getDesktop().browseFileDirectory(file.toFile());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).expected().omitted(xdg).handle();
|
||||
}
|
||||
}
|
||||
|
||||
if (xdg) {
|
||||
LocalExec.readStdoutIfPossible("xdg-open", file.getParent().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@ import javax.crypto.SecretKey;
|
|||
public class EncryptionToken {
|
||||
|
||||
private static EncryptionToken vaultToken;
|
||||
private static EncryptionToken userToken;
|
||||
|
||||
public static void invalidateUserToken() {
|
||||
userToken = null;
|
||||
}
|
||||
|
||||
private static EncryptionToken createUserToken() {
|
||||
var userHandler = DataStorageUserHandler.getInstance();
|
||||
|
@ -39,12 +44,15 @@ public class EncryptionToken {
|
|||
}
|
||||
|
||||
public static EncryptionToken ofUser() {
|
||||
var userHandler = DataStorageUserHandler.getInstance();
|
||||
if (userHandler.getActiveUser() == null) {
|
||||
throw new IllegalStateException("No active user available");
|
||||
}
|
||||
if (userToken == null) {
|
||||
var userHandler = DataStorageUserHandler.getInstance();
|
||||
if (userHandler.getActiveUser() == null) {
|
||||
throw new IllegalStateException("No active user available");
|
||||
}
|
||||
|
||||
return createUserToken();
|
||||
userToken = createUserToken();
|
||||
}
|
||||
return userToken;
|
||||
}
|
||||
|
||||
public static EncryptionToken ofVaultKey() {
|
||||
|
@ -59,6 +67,12 @@ public class EncryptionToken {
|
|||
@JsonIgnore
|
||||
private Boolean isVault;
|
||||
|
||||
@JsonIgnore
|
||||
private Boolean isUser;
|
||||
|
||||
@JsonIgnore
|
||||
private EncryptionToken usedUserToken;
|
||||
|
||||
public boolean canDecrypt() {
|
||||
return isVault() || isUser();
|
||||
}
|
||||
|
@ -79,7 +93,13 @@ public class EncryptionToken {
|
|||
return false;
|
||||
}
|
||||
|
||||
return userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()));
|
||||
if (userToken == EncryptionToken.ofUser() && isUser != null) {
|
||||
return isUser;
|
||||
}
|
||||
|
||||
usedUserToken = ofUser();
|
||||
isUser = userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()));
|
||||
return isUser;
|
||||
}
|
||||
|
||||
public boolean isVault() {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package io.xpipe.app.util;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Hyperlinks {
|
||||
|
||||
|
@ -13,6 +16,7 @@ public class Hyperlinks {
|
|||
public static final String DOCS_EULA = "https://docs.xpipe.io/legal/end-user-license-agreement";
|
||||
public static final String DOCS_SECURITY = "https://docs.xpipe.io/reference/security";
|
||||
public static final String DOCS_WEBTOP_UPDATE = "https://docs.xpipe.io/guide/webtop#updating";
|
||||
public static final String DOCS_SYNC = "https://docs.xpipe.io/guide/sync";
|
||||
|
||||
public static final String GITHUB = "https://github.com/xpipe-io/xpipe";
|
||||
public static final String GITHUB_PTB = "https://github.com/xpipe-io/xpipe-ptb";
|
||||
|
@ -24,38 +28,7 @@ public class Hyperlinks {
|
|||
public static final String SLACK =
|
||||
"https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg";
|
||||
|
||||
static final String[] browsers = {
|
||||
"xdg-open", "google-chrome", "firefox", "opera", "konqueror", "mozilla", "gnome-open", "open"
|
||||
};
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void open(String uri) {
|
||||
String osName = System.getProperty("os.name");
|
||||
try {
|
||||
if (osName.startsWith("Mac OS")) {
|
||||
Runtime.getRuntime().exec("open " + uri);
|
||||
} else if (osName.startsWith("Windows")) {
|
||||
Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + uri);
|
||||
} else { // assume Unix or Linux
|
||||
String browser = null;
|
||||
for (String b : browsers) {
|
||||
if (browser == null
|
||||
&& Runtime.getRuntime()
|
||||
.exec(new String[] {"which", b})
|
||||
.getInputStream()
|
||||
.read()
|
||||
!= -1) {
|
||||
Runtime.getRuntime().exec(new String[] {browser = b, uri});
|
||||
}
|
||||
}
|
||||
if (browser == null) {
|
||||
throw new Exception("No web browser or URL opener found to open " + uri);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// should not happen
|
||||
// dump stack for debug purpose
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
}
|
||||
DesktopHelper.openUrl(uri);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.app.util;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.ExternalApplicationType;
|
||||
import io.xpipe.app.prefs.ExternalEditorType;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
@ -15,7 +14,7 @@ public class LocalShellCache extends ShellControlCache {
|
|||
super(shellControl);
|
||||
}
|
||||
|
||||
public Optional<Path> getVsCodePath() {
|
||||
public Optional<Path> getVsCodeCliPath() {
|
||||
if (!has("codePath")) {
|
||||
try {
|
||||
var app =
|
||||
|
@ -25,8 +24,8 @@ public class LocalShellCache extends ShellControlCache {
|
|||
.map(s -> Path.of(s));
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
yield new ExternalApplicationType.MacApplication(
|
||||
"app.vscode", "Visual Studio Code") {}.findApp();
|
||||
yield CommandSupport.findProgram(getShellControl(), "code")
|
||||
.map(s -> Path.of(s));
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
yield ExternalEditorType.VSCODE_WINDOWS.findExecutable();
|
||||
|
|
|
@ -274,6 +274,29 @@ public class PlatformThread {
|
|||
return true;
|
||||
}
|
||||
|
||||
public static void enterNestedEventLoop(Object key) {
|
||||
if (!Platform.canStartNestedEventLoop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Platform.enterNestedEventLoop(key);
|
||||
} catch (IllegalStateException ex) {
|
||||
// We might be in an animation or layout call
|
||||
ErrorEvent.fromThrowable(ex).omit().expected().handle();
|
||||
}
|
||||
}
|
||||
|
||||
public static void exitNestedEventLoop(Object key) {
|
||||
try {
|
||||
Platform.exitNestedEventLoop(key, null);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// The event loop might have died somehow
|
||||
// Or we passed an invalid key
|
||||
ErrorEvent.fromThrowable(ex).omit().expected().handle();
|
||||
}
|
||||
}
|
||||
|
||||
public static void runNestedLoopIteration() {
|
||||
if (!Platform.canStartNestedEventLoop()) {
|
||||
return;
|
||||
|
@ -281,9 +304,9 @@ public class PlatformThread {
|
|||
|
||||
var key = new Object();
|
||||
Platform.runLater(() -> {
|
||||
Platform.exitNestedEventLoop(key, null);
|
||||
exitNestedEventLoop(key);
|
||||
});
|
||||
Platform.enterNestedEventLoop(key);
|
||||
enterNestedEventLoop(key);
|
||||
}
|
||||
|
||||
public static void runLaterIfNeeded(Runnable r) {
|
||||
|
|
|
@ -23,7 +23,7 @@ public enum SecretQueryState {
|
|||
yield "Session is not interactive but required user input for authentication";
|
||||
}
|
||||
case FIXED_SECRET_WRONG -> {
|
||||
yield "Authentication failed: Provided authentication secret is wrong";
|
||||
yield "Authentication failed: Provided authentication secret was not accepted by the server, probably because it is incorrect";
|
||||
}
|
||||
case RETRIEVAL_FAILURE -> {
|
||||
yield "Failed to retrieve secret for authentication";
|
||||
|
|
|
@ -204,7 +204,7 @@ public class SshLocalBridge {
|
|||
var exec = CommandSupport.findProgram(sc, "sshd");
|
||||
if (exec.isEmpty()) {
|
||||
throw ErrorEvent.expected(new IllegalStateException(
|
||||
"No sshd executable found in PATH. The SSH terminal bridge requires a local ssh server"));
|
||||
"No sshd executable found in PATH. The SSH terminal bridge for SSH clients requires a local ssh server to be installed"));
|
||||
}
|
||||
return exec.get();
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ public class Validators {
|
|||
|
||||
public static <T extends DataStore> void isType(DataStoreEntryRef<? extends T> ref, Class<T> c)
|
||||
throws ValidationException {
|
||||
if (ref == null || !c.isAssignableFrom(ref.getStore().getClass())) {
|
||||
if (ref == null || ref.getStore() == null || !c.isAssignableFrom(ref.getStore().getClass())) {
|
||||
throw new ValidationException("Value must be an instance of " + c.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,3 +38,7 @@
|
|||
-fx-max-height: 2.8em;
|
||||
-fx-padding: 6 6 6 4;
|
||||
}
|
||||
|
||||
.bookmarks-header .ikonli-font-icon {
|
||||
-fx-icon-color: -color-fg-default;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,18 @@
|
|||
-fx-focus-color: transparent;
|
||||
}
|
||||
|
||||
.combo-box-base:hover .arrow-button:hover .arrow {
|
||||
-fx-background-color: -color-accent-fg;
|
||||
}
|
||||
|
||||
.identity-select-comp .clear-button:hover .ikonli-font-icon {
|
||||
-fx-icon-color: -color-accent-fg;
|
||||
}
|
||||
|
||||
.identity-select-comp .clear-button {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-cell:hover, .combo-box-popup .list-cell:focused {
|
||||
-fx-background-color: -color-context-menu;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,10 @@
|
|||
-fx-border-color: -color-border-default;
|
||||
}
|
||||
|
||||
.root:dark:seamless-frame.primer .layout > .background, .root:dark:seamless-frame.mocha .layout > .background {
|
||||
-fx-border-color: #444;
|
||||
}
|
||||
|
||||
.root:macos:seamless-frame .layout > .background {
|
||||
-fx-background-insets: 0;
|
||||
-fx-border-insets: 0;
|
||||
|
|
|
@ -32,3 +32,7 @@
|
|||
.options-comp .titled-pane > .title {
|
||||
-fx-padding: 8 20 8 10;
|
||||
}
|
||||
|
||||
.options-comp .titled-pane > .title .text {
|
||||
-fx-font-size: 0.95em;
|
||||
}
|
|
@ -24,15 +24,11 @@
|
|||
-fx-text-fill: -color-fg-muted;
|
||||
}
|
||||
|
||||
.store-entry-grid:failed .jfx-text-field {
|
||||
-fx-text-fill: #ee4829;
|
||||
}
|
||||
|
||||
.root:dark .store-entry-grid:incomplete .name {
|
||||
.root:dark .store-entry-grid:incomplete .name, .root:dark .store-entry-grid:failed .name {
|
||||
-fx-text-fill: #aa473c;
|
||||
}
|
||||
|
||||
.root:light .store-entry-grid:incomplete .name {
|
||||
.root:light .store-entry-grid:incomplete .name, .root:light .store-entry-grid:failed .name {
|
||||
-fx-text-fill: #88352b;
|
||||
}
|
||||
|
||||
|
@ -137,7 +133,7 @@
|
|||
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2, 0.5, 0, 1);
|
||||
}
|
||||
|
||||
.store-entry-section-comp:root {
|
||||
.store-entry-section-comp:top {
|
||||
-fx-border-radius: 4px;
|
||||
-fx-background-radius: 4px;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.titled-pane {
|
||||
.third-party-dependency-list-comp .titled-pane {
|
||||
-fx-background-radius: 5;
|
||||
}
|
||||
|
||||
.titled-pane .content {
|
||||
.third-party-dependency-list-comp .titled-pane .content {
|
||||
-fx-padding: 4;
|
||||
}
|
||||
|
|
|
@ -84,30 +84,6 @@
|
|||
-color-danger-emphasis: rgb(255, 69, 58);
|
||||
-color-danger-muted: rgba(255, 69, 58, 0.4);
|
||||
-color-danger-subtle: rgba(255, 69, 58, 0.15);
|
||||
-color-chart-1: #f3622d;
|
||||
-color-chart-2: #fba71b;
|
||||
-color-chart-3: #57b757;
|
||||
-color-chart-4: #41a9c9;
|
||||
-color-chart-5: #4258c9;
|
||||
-color-chart-6: #9a42c8;
|
||||
-color-chart-7: #c84164;
|
||||
-color-chart-8: #888888;
|
||||
-color-chart-1-alpha70: rgba(243, 98, 45, 0.7);
|
||||
-color-chart-2-alpha70: rgba(251, 167, 27, 0.7);
|
||||
-color-chart-3-alpha70: rgba(87, 183, 87, 0.7);
|
||||
-color-chart-4-alpha70: rgba(65, 169, 201, 0.7);
|
||||
-color-chart-5-alpha70: rgba(66, 88, 201, 0.7);
|
||||
-color-chart-6-alpha70: rgba(154, 66, 200, 0.7);
|
||||
-color-chart-7-alpha70: rgba(200, 65, 100, 0.7);
|
||||
-color-chart-8-alpha70: rgba(136, 136, 136, 0.7);
|
||||
-color-chart-1-alpha20: rgba(243, 98, 45, 0.2);
|
||||
-color-chart-2-alpha20: rgba(251, 167, 27, 0.2);
|
||||
-color-chart-3-alpha20: rgba(87, 183, 87, 0.2);
|
||||
-color-chart-4-alpha20: rgba(65, 169, 201, 0.2);
|
||||
-color-chart-5-alpha20: rgba(66, 88, 201, 0.2);
|
||||
-color-chart-6-alpha20: rgba(154, 66, 200, 0.2);
|
||||
-color-chart-7-alpha20: rgba(200, 65, 100, 0.2);
|
||||
-color-chart-8-alpha20: rgba(136, 136, 136, 0.2);
|
||||
-fx-background-color: -color-bg-default;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,10 +18,7 @@ public class DaemonFocusExchange extends BeaconInterface<DaemonFocusExchange.Req
|
|||
@Jacksonized
|
||||
@Builder
|
||||
@Value
|
||||
public static class Request {
|
||||
@NonNull
|
||||
XPipeDaemonMode mode;
|
||||
}
|
||||
public static class Request {}
|
||||
|
||||
@Jacksonized
|
||||
@Builder
|
||||
|
|
|
@ -124,7 +124,7 @@ project.ext {
|
|||
website = 'https://xpipe.io'
|
||||
sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe'
|
||||
authors = 'Christopher Schnick'
|
||||
javafxVersion = '24-ea+15'
|
||||
javafxVersion = '24-ea+5'
|
||||
platformName = getPlatformName()
|
||||
jvmRunArgs = [
|
||||
"--add-opens", "java.base/java.lang=io.xpipe.app",
|
||||
|
@ -145,7 +145,7 @@ project.ext {
|
|||
]
|
||||
|
||||
// GC config
|
||||
jvmRunArgs += ['-Xms200m', '-Xmx4G', '-XX:MinHeapFreeRatio=20', '-XX:MaxHeapFreeRatio=30', '-XX:GCTimeRatio=39'];
|
||||
jvmRunArgs += ['-XX:+UseG1GC', '-Xms300m', '-Xmx4G', '-XX:GCTimeRatio=9', '-XX:+UseStringDeduplication'];
|
||||
|
||||
if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) {
|
||||
jvmRunArgs += ["-Dapple.awt.application.appearance=system"]
|
||||
|
|
|
@ -11,9 +11,9 @@ public interface ElevationHandler {
|
|||
|
||||
@Override
|
||||
public boolean handleRequest(
|
||||
ShellControl parent, UUID requestId, CountDown countDown, boolean confirmIfNeeded) {
|
||||
var r = ElevationHandler.this.handleRequest(parent, requestId, countDown, confirmIfNeeded);
|
||||
return r || other.handleRequest(parent, requestId, countDown, confirmIfNeeded);
|
||||
UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive) {
|
||||
var r = ElevationHandler.this.handleRequest(requestId, countDown, confirmIfNeeded, interactive);
|
||||
return r || other.handleRequest(requestId, countDown, confirmIfNeeded, interactive);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -24,7 +24,7 @@ public interface ElevationHandler {
|
|||
};
|
||||
}
|
||||
|
||||
boolean handleRequest(ShellControl parent, UUID requestId, CountDown countDown, boolean confirmIfNeeded);
|
||||
boolean handleRequest(UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive);
|
||||
|
||||
SecretReference getSecretRef();
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ import java.util.function.Function;
|
|||
|
||||
public interface ShellControl extends ProcessControl {
|
||||
|
||||
boolean isInitializing();
|
||||
|
||||
void setDumbOpen(ShellOpenFunction openFunction);
|
||||
|
||||
void setTerminalOpen(ShellOpenFunction openFunction);
|
||||
|
|
|
@ -215,6 +215,11 @@ public class WrapperShellControl implements ShellControl {
|
|||
return parent.getShellDialect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitializing() {
|
||||
return parent.isInitializing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDumbOpen(ShellOpenFunction openFunction) {
|
||||
parent.setDumbOpen(openFunction);
|
||||
|
|
|
@ -22,12 +22,19 @@ public class InPlaceSecretValue extends AesSecretValue {
|
|||
|
||||
private static final int AES_KEY_BIT = 128;
|
||||
private static final int SALT_BIT = 16;
|
||||
private static final int ITERATION_COUNT = 2048;
|
||||
private static final SecretKeyFactory SECRET_FACTORY;
|
||||
private static final SecretKey SECRET_KEY;
|
||||
|
||||
static {
|
||||
try {
|
||||
SECRET_FACTORY = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
|
||||
var salt = new byte[SALT_BIT];
|
||||
new Random(AES_KEY_BIT).nextBytes(salt);
|
||||
KeySpec spec = new PBEKeySpec(new char[] {'X', 'P', 'E' << 1}, salt, ITERATION_COUNT, AES_KEY_BIT);
|
||||
SECRET_KEY = new SecretKeySpec(SECRET_FACTORY.generateSecret(spec).getEncoded(), "AES");
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
@ -52,10 +59,6 @@ public class InPlaceSecretValue extends AesSecretValue {
|
|||
return new InPlaceSecretValue(b);
|
||||
}
|
||||
|
||||
protected int getIterationCount() {
|
||||
return 2048;
|
||||
}
|
||||
|
||||
protected byte[] getNonce(int numBytes) {
|
||||
byte[] nonce = new byte[numBytes];
|
||||
new Random(1 - 28 + 213213).nextBytes(nonce);
|
||||
|
@ -63,11 +66,8 @@ public class InPlaceSecretValue extends AesSecretValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey getSecretKey() throws InvalidKeySpecException {
|
||||
var salt = new byte[SALT_BIT];
|
||||
new Random(AES_KEY_BIT).nextBytes(salt);
|
||||
KeySpec spec = new PBEKeySpec(new char[] {'X', 'P', 'E' << 1}, salt, getIterationCount(), AES_KEY_BIT);
|
||||
return new SecretKeySpec(SECRET_FACTORY.generateSecret(spec).getEncoded(), "AES");
|
||||
protected SecretKey getSecretKey() {
|
||||
return SECRET_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -57,10 +57,17 @@ public class XPipeInstallation {
|
|||
|
||||
@SneakyThrows
|
||||
public static Path getCurrentInstallationBasePath() {
|
||||
var command = ProcessHandle.current().info().command();
|
||||
// We should always have a command associated with the current process, otherwise something went seriously wrong
|
||||
if (command.isEmpty()) {
|
||||
var javaHome = System.getProperty("java.home");
|
||||
var javaExec = toRealPathIfPossible(Path.of(javaHome, "bin", "java"));
|
||||
var path = getLocalInstallationBasePathForJavaExecutable(javaExec);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Resolve any possible links to a real path
|
||||
Path path = toRealPathIfPossible(
|
||||
Path.of(ProcessHandle.current().info().command().orElseThrow()));
|
||||
Path path = toRealPathIfPossible(Path.of(command.get()));
|
||||
// Check if the process was started using a relative path, and adapt it if necessary
|
||||
if (!path.isAbsolute()) {
|
||||
path = toRealPathIfPossible(Path.of(System.getProperty("user.dir")).resolve(path));
|
||||
|
|
66
dist/changelogs/15.4.md
vendored
Normal file
66
dist/changelogs/15.4.md
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.
|
||||
|
||||
## Tailscale SSH support
|
||||
|
||||
You can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.
|
||||
|
||||
## Custom icons
|
||||
|
||||
You can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.
|
||||
|
||||
Your existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.
|
||||
|
||||
## Package manager repositories
|
||||
|
||||
There is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater.
|
||||
|
||||
## New docs
|
||||
|
||||
There is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.
|
||||
|
||||
## Other
|
||||
|
||||
- Rework application styling
|
||||
- Improve performance when having many connections and categories
|
||||
- Add new action to run scripts in the file browser and show their output without having to open a terminal
|
||||
- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore
|
||||
- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY
|
||||
- The custom service command opener will now use \$PORT instead of \$ADDRESS to allow for the use of commands that have a separate port argument
|
||||
- Add support for Gnome Console and Ptyxis Terminal
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fix user interface not being responsive for a few seconds after launch
|
||||
- Fix VSCode open actions not showing if code executable was not in PATH
|
||||
- Fix startup failure on Windows systems when vcredist140.dll was missing
|
||||
- Fix various issues with shells to Android systems
|
||||
- Fix issues on Linux systems where language en_US.UTF-8 was not available
|
||||
- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus
|
||||
- Fix permission denied errors on terminal launch when file system had noexec flag set
|
||||
- Fix git synced vault keys not working on other systems
|
||||
- Fix double sudo prompt when elevating to root in file browser
|
||||
- Fix file browser shift selection not marking selected files
|
||||
- Fix file browser yellow keyboard focus indicator showing after typing path
|
||||
- Fix ssh service tunnel sometimes failing with a timeout on close
|
||||
- Fix modal dialogs flickering a bit
|
||||
- Fix some icons resetting on updates
|
||||
- Fix desktop shortcuts not launching actions properly
|
||||
- Fix teleport integration failing for newer teleport versions
|
||||
- Fix MobaXterm integration not working correctly
|
||||
- Fix git sync SSH key password always prompting, even if it is specified in place
|
||||
- Fix creation dialog for scripts and identities still referring to the name as connection name
|
||||
- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost
|
||||
- Fix Windows Terminal start up failing if it was the first time that it was launched on the system
|
||||
- Fix some translations not updating when changing display language
|
||||
- Fix custom service open command not working properly with PowerShell
|
||||
- Fix shortcut actions not running when daemon had to be started first
|
||||
- Fix browser directory list entering endless loop if directory contained symlink to itself
|
||||
- Fix certain actions like RDP failing when XPipe was launched from pwsh
|
||||
- Fix terminal selection defaulting to Wave if no other terminal is found
|
||||
- Fix vault version incompatibility notice only offering to disable the git sync
|
||||
- Fix several cases where adding/deleting vault users would corrupt vault data
|
||||
- Fix vault user encryption settings not updating properly
|
||||
- Fix shell init scripts being run multiple times when background shell session was active
|
||||
- Fix restart button not working for custom workspace locations
|
||||
- Fix git readme list updating each time when using a different vault user
|
||||
- Fix installation type detection being wrong when using installer and portable installation side by side
|
13
dist/changelogs/15.4_incremental.md
vendored
Normal file
13
dist/changelogs/15.4_incremental.md
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
## Performance
|
||||
|
||||
This release mainly focuses on performance improvements across the board.
|
||||
It especially tackles bad application performance when a lot of connections are present.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fix potential issues with terminals spamming restart requests, slowing down xpipe to a halt
|
||||
- Fix XPipe freezing when many too tunnels were set to start automatically on XPipe launch
|
||||
- Fix custom SSH connections failing when connection name contained some special characters
|
||||
- Fix some connections not being able to be added again after being deleted when searching for connections
|
||||
- Fix some .svg icons missing from the icon list
|
||||
- Fix some small styling issues
|
66
dist/changelogs/15.5.md
vendored
Normal file
66
dist/changelogs/15.5.md
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.
|
||||
|
||||
## Tailscale SSH support
|
||||
|
||||
You can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.
|
||||
|
||||
## Custom icons
|
||||
|
||||
You can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.
|
||||
|
||||
Your existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.
|
||||
|
||||
## Package manager repositories
|
||||
|
||||
There is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater.
|
||||
|
||||
## New docs
|
||||
|
||||
There is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.
|
||||
|
||||
## Other
|
||||
|
||||
- Rework application styling
|
||||
- Improve performance when having many connections and categories
|
||||
- Add new action to run scripts in the file browser and show their output without having to open a terminal
|
||||
- The Homelab/Pro preview for new features is now handled automatically, you don't have to enable it anymore
|
||||
- You can now import saved PuTTY sessions on a system when searching for available connections. This also works for KiTTY
|
||||
- The custom service command opener will now use \$PORT instead of \$ADDRESS to allow for the use of commands that have a separate port argument
|
||||
- Add support for Gnome Console and Ptyxis Terminal
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fix user interface not being responsive for a few seconds after launch
|
||||
- Fix VSCode open actions not showing if code executable was not in PATH
|
||||
- Fix startup failure on Windows systems when vcredist140.dll was missing
|
||||
- Fix various issues with shells to Android systems
|
||||
- Fix issues on Linux systems where language en_US.UTF-8 was not available
|
||||
- Fix docked file browser terminal staying ahead of other windows even if XPipe loses focus
|
||||
- Fix permission denied errors on terminal launch when file system had noexec flag set
|
||||
- Fix git synced vault keys not working on other systems
|
||||
- Fix double sudo prompt when elevating to root in file browser
|
||||
- Fix file browser shift selection not marking selected files
|
||||
- Fix file browser yellow keyboard focus indicator showing after typing path
|
||||
- Fix ssh service tunnel sometimes failing with a timeout on close
|
||||
- Fix modal dialogs flickering a bit
|
||||
- Fix some icons resetting on updates
|
||||
- Fix desktop shortcuts not launching actions properly
|
||||
- Fix teleport integration failing for newer teleport versions
|
||||
- Fix MobaXterm integration not working correctly
|
||||
- Fix git sync SSH key password always prompting, even if it is specified in place
|
||||
- Fix creation dialog for scripts and identities still referring to the name as connection name
|
||||
- Fix password prompts for tunneled VM SSH connections showing wrong hostname as localhost
|
||||
- Fix Windows Terminal start up failing if it was the first time that it was launched on the system
|
||||
- Fix some translations not updating when changing display language
|
||||
- Fix custom service open command not working properly with PowerShell
|
||||
- Fix shortcut actions not running when daemon had to be started first
|
||||
- Fix browser directory list entering endless loop if directory contained symlink to itself
|
||||
- Fix certain actions like RDP failing when XPipe was launched from pwsh
|
||||
- Fix terminal selection defaulting to Wave if no other terminal is found
|
||||
- Fix vault version incompatibility notice only offering to disable the git sync
|
||||
- Fix several cases where adding/deleting vault users would corrupt vault data
|
||||
- Fix vault user encryption settings not updating properly
|
||||
- Fix shell init scripts being run multiple times when background shell session was active
|
||||
- Fix restart button not working for custom workspace locations
|
||||
- Fix git readme list updating each time when using a different vault user
|
||||
- Fix installation type detection being wrong when using installer and portable installation side by side
|
9
dist/changelogs/15.5_incremental.md
vendored
Normal file
9
dist/changelogs/15.5_incremental.md
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
- Fix connection hierarchy backgrounds not updating probably when expanding them
|
||||
- Fix some issues on macOS when zsh failed to start
|
||||
- Fix XPipe not falling back to sh in some cases on macOS when zsh failed to start
|
||||
- Fix vscode remote open functionality using wrong app path on macOS
|
||||
- Fix zsh module zsh/stat breaking some file browser functionality when enabled
|
||||
- Fix tailscale refresh operations failing with an out-of-bounds error in some cases
|
||||
- Fix some OS logos not showing correctly
|
||||
- Fix NullPointer when launching FreeRDP
|
||||
- Fix outdated manpages docs link
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue