Compare commits

..

No commits in common. "master" and "15.2" have entirely different histories.
master ... 15.2

159 changed files with 1313 additions and 2739 deletions

View file

@ -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.
More information about the security approach of the XPipe application can be found on the documentation website at https://docs.xpipe.io/reference/security.
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.
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.

View file

@ -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.1")
api("com.github.weisj:jsvg:1.7.0")
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'

View file

@ -1,7 +1,6 @@
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;
@ -10,11 +9,7 @@ public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
OperationMode.switchUp(OperationMode.GUI);
var w = AppMainWindow.getInstance();
if (w != null) {
w.focus();
}
OperationMode.switchUp(OperationMode.map(msg.getMode()));
return Response.builder().build();
}

View file

@ -2,14 +2,13 @@ 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, BeaconServerException {
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
var r = TerminalLauncherManager.launchExchange(msg.getRequest());
return Response.builder().targetFile(r).build();
}

View file

@ -24,9 +24,6 @@ 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);
});
}

View file

@ -73,7 +73,6 @@ 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 -> {
@ -148,8 +147,8 @@ public class BrowserFullSessionComp extends SimpleComp {
var rec = new Rectangle();
rec.widthProperty().bind(struc.get().widthProperty());
rec.heightProperty().bind(struc.get().heightProperty());
rec.setArcHeight(11);
rec.setArcWidth(11);
rec.setArcHeight(7);
rec.setArcWidth(7);
struc.get().getChildren().getFirst().setClip(rec);
})
.vgrow();
@ -174,7 +173,6 @@ 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(() -> {

View file

@ -156,8 +156,8 @@ public final class BrowserFileListComp extends SimpleComp {
var os = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
.map(shellControl -> shellControl.getOsType())
.orElse(null);
.orElseThrow()
.getOsType();
table.widthProperty().subscribe((newValue) -> {
if (os != OsType.WINDOWS && os != OsType.MACOS) {
ownerCol.setVisible(newValue.doubleValue() > 1000);

View file

@ -53,7 +53,7 @@ public class BrowserFileSystemSavedState {
public BrowserFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<>(STORED)));
recentDirectories = 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.synchronizedObservableList(FXCollections.observableList(cleaned)));
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
}
}

View file

@ -207,7 +207,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
home,
model.getCurrentPath().isNull(),
fileList,
model.getCurrentPath().isNull().not()), false);
model.getCurrentPath().isNull().not()));
var r = stack.styleClass("browser-content-container").createRegion();
r.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {

View file

@ -1,7 +1,6 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.*;
import javafx.beans.property.BooleanProperty;
@ -12,10 +11,7 @@ import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.regex.Pattern;
@ -417,60 +413,23 @@ public class BrowserFileTransferOperation {
// Initialize progress immediately prior to reading anything
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
var killStreams = new AtomicBoolean(false);
var exception = new AtomicReference<Exception>();
var thread = ThreadHelper.createPlatformThread("transfer", true, () -> {
try {
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
if (cancelled()) {
killStreams.set(true);
break;
}
if (!checkTransferValidity()) {
killStreams.set(true);
break;
}
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
}
} catch (Exception ex) {
exception.set(ex);
}
});
thread.start();
while (true) {
var alive = thread.isAlive();
var cancelled = cancelled();
if (cancelled) {
// Assume that the transfer has stalled if it doesn't finish until then
thread.join(1000);
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
if (cancelled()) {
killStreams();
break;
}
if (alive) {
Thread.sleep(100);
continue;
}
if (killStreams.get()) {
if (!checkTransferValidity()) {
killStreams();
}
var ex = exception.get();
if (ex != null) {
throw ex;
} else {
break;
}
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
}
}

View file

@ -26,7 +26,7 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
ObservableList<Entry> lastSystems;
public BrowserHistorySavedStateImpl(List<Entry> lastSystems) {
this.lastSystems = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(lastSystems));
this.lastSystems = FXCollections.observableArrayList(lastSystems);
}
private static BrowserHistorySavedStateImpl INSTANCE;

View file

@ -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, false);
var stack = new MultiContentComp(map);
return stack.createRegion();
}
@ -165,7 +165,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
.accessibleText(e.getPath())
.disable(disable)
.styleClass("directory-button")
.apply(struc -> struc.get().setMaxWidth(20000))
.apply(struc -> struc.get().setMaxWidth(2000))
.styleClass(Styles.RIGHT_PILL)
.hgrow()
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));

View file

@ -38,7 +38,7 @@ public class BrowserOverviewComp extends SimpleComp {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
var commonPlatform = FXCollections.<FileEntry>observableArrayList();
ThreadHelper.runFailableAsync(() -> {
var common = sc.getOsType().determineInterestingPaths(sc).stream()
.filter(s -> !s.isBlank())

View file

@ -4,11 +4,8 @@ import io.xpipe.app.browser.BrowserAbstractSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserSessionTab;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.AppMainWindowContentComp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.terminal.TerminalDockComp;
@ -21,7 +18,6 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.Optional;
@ -138,13 +134,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
dockModel.toggleView(aBoolean);
});
});
AppDialog.getModalOverlay().addListener((ListChangeListener<? super ModalOverlay>) c -> {
if (c.getList().size() > 0) {
dockModel.toggleView(false);
} else {
dockModel.toggleView(viewActive.get());
}
});
}
private void refreshShowingState() {

View file

@ -65,14 +65,13 @@ public class BrowserTransferComp extends SimpleComp {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
var hideProgress = sourceItem
.get()
.downloadFinished()
.get();
var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0;
var progressSuffix = hideProgress
var progressSuffix = p == null
|| sourceItem
.get()
.downloadFinished()
.get()
? ""
: " " + share + "%";
: " " + (p.getTransferred() * 100 / p.getTotal()) + "%";
return entry.getFileName() + progressSuffix;
},
sourceItem.get().getProgress());
@ -82,14 +81,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(Bindings.or(model.getEmpty(), model.getTransferring()));
.hide(model.getEmpty());
var clearButton = new IconButtonComp("mdi2c-close", () -> {
ThreadHelper.runAsync(() -> {
model.clear(true);
});
})
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
.hide(model.getEmpty())
.tooltipKey("clearTransferDescription");
var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> {
@ -97,7 +96,7 @@ public class BrowserTransferComp extends SimpleComp {
model.transferToDownloads();
});
})
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
.hide(model.getEmpty())
.tooltipKey("downloadStageDescription");
var bottom = new HorizontalComp(

View file

@ -8,9 +8,7 @@ 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;
@ -36,7 +34,6 @@ 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;
@ -50,9 +47,8 @@ public class BrowserTransferModel {
}
if (toDownload.isPresent()) {
downloadSingle(toDownload.get());
} else {
ThreadHelper.sleep(20);
}
ThreadHelper.sleep(20);
}
});
thread.start();
@ -130,7 +126,6 @@ public class BrowserTransferModel {
}
try {
transferring.setValue(true);
var op = new BrowserFileTransferOperation(
BrowserLocalFileSystem.getLocalFileEntry(TEMP),
List.of(item.getBrowserEntry().getRawFileEntry()),
@ -155,8 +150,6 @@ public class BrowserTransferModel {
synchronized (items) {
items.remove(item);
}
} finally {
transferring.setValue(false);
}
}

View file

@ -154,15 +154,7 @@ public abstract class Comp<S extends CompStructure<?>> {
}
public Comp<S> disable(ObservableValue<Boolean> o) {
return apply(struc -> {
var region = struc.get();
BindingsHelper.preserve(region, o);
o.subscribe(n -> {
PlatformThread.runLaterIfNeeded(() -> {
region.setDisable(n);
});
});
});
return apply(struc -> struc.get().disableProperty().bind(o));
}
public Comp<S> padding(Insets insets) {

View file

@ -22,6 +22,7 @@ public class AnchorComp extends Comp<CompStructure<AnchorPane>> {
for (var c : comps) {
pane.getChildren().add(c.createRegion());
}
pane.setPickOnBounds(false);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -39,7 +39,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
return model.getSelected().getValue().equals(entry);
},
model.getSelected())));
var multi = new MultiContentComp(map, true);
var multi = new MultiContentComp(map);
multi.styleClass("background");
var pane = new BorderPane();

View file

@ -6,7 +6,6 @@ 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;
@ -83,7 +82,6 @@ public class AppMainWindowContentComp extends SimpleComp {
loaded.subscribe(struc -> {
if (struc != null) {
TrackEvent.info("Window content node set");
PlatformThread.runNestedLoopIteration();
anim.stop();
struc.prepareAddition();
@ -92,7 +90,6 @@ public class AppMainWindowContentComp extends SimpleComp {
pane.getStyleClass().remove("background");
pane.getChildren().remove(vbox);
struc.show();
TrackEvent.info("Window content node shown");
}
});
@ -110,6 +107,14 @@ 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);

View file

@ -42,7 +42,7 @@ public class ComboTextFieldComp extends Comp<CompStructure<ComboBox<String>>> {
});
});
text.setEditable(true);
text.setMaxWidth(20000);
text.setMaxWidth(2000);
text.setValue(value.getValue() != null ? value.getValue() : null);
text.valueProperty().addListener((c, o, n) -> {
value.setValue(n != null && n.length() > 0 ? n : null);

View file

@ -41,7 +41,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
});
var filter = new CustomTextField();
filter.setMinHeight(0);
filter.setMaxHeight(20000);
filter.setMaxHeight(2000);
filter.getStyleClass().add("filter-comp");
filter.promptTextProperty().bind(AppI18n.observable("searchFilter"));
filter.rightProperty()
@ -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) && !(val == null && "".equals(filter.getText()))) {
if (!Objects.equals(filter.getText(), val)) {
filter.setText(val);
}
});

View file

@ -0,0 +1,29 @@
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);
}
}

View file

@ -37,7 +37,7 @@ public class IntComboFieldComp extends Comp<CompStructure<ComboBox<String>>> {
text.setValue(value.getValue() != null ? value.getValue().toString() : null);
text.setItems(FXCollections.observableList(
predefined.stream().map(integer -> "" + integer).toList()));
text.setMaxWidth(20000);
text.setMaxWidth(2000);
text.getStyleClass().add("int-combo-field-comp");
text.setSkin(new ComboBoxListViewSkin<>(text));
text.setVisibleRowCount(Math.min(10, predefined.size()));

View file

@ -5,7 +5,6 @@ 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;
@ -14,8 +13,6 @@ 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>> {
@ -37,38 +34,29 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
@Override
public CompStructure<TextField> createBase() {
var field = new TextField(value.getValue() != null ? value.getValue().toString() : null);
var text = new TextField(value.getValue() != null ? value.getValue().toString() : null);
value.addListener((ChangeListener<Number>) (observableValue, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
// 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;
}
text.setText("");
} else {
if (newValue.intValue() < minValue) {
value.setValue(minValue);
return;
}
if (newValue.intValue() < minValue) {
value.setValue(minValue);
return;
}
if (newValue.intValue() > maxValue) {
value.setValue(maxValue);
return;
}
if (newValue.intValue() > maxValue) {
value.setValue(maxValue);
return;
text.setText(newValue.toString());
}
field.setText(newValue.toString());
});
});
field.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
if (minValue < 0) {
if (!"-0123456789".contains(keyEvent.getCharacter())) {
keyEvent.consume();
@ -80,7 +68,7 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
}
});
field.textProperty().addListener((observableValue, oldValue, newValue) -> {
text.textProperty().addListener((observableValue, oldValue, newValue) -> {
if (newValue == null
|| newValue.isEmpty()
|| (minValue < 0 && "-".equals(newValue))
@ -91,12 +79,12 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
int intValue = Integer.parseInt(newValue);
if (minValue > intValue || intValue > maxValue) {
field.textProperty().setValue(oldValue);
text.textProperty().setValue(oldValue);
}
value.setValue(Integer.parseInt(field.textProperty().get()));
value.setValue(Integer.parseInt(text.textProperty().get()));
});
return new SimpleCompStructure<>(field);
return new SimpleCompStructure<>(text);
}
}

View file

@ -4,17 +4,12 @@ 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;
@ -27,7 +22,6 @@ 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>> {
@ -43,7 +37,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
private final boolean scrollBar;
@Setter
private boolean visibilityControl = false;
private int platformPauseInterval = -1;
public ListBoxViewComp(
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
@ -62,24 +56,16 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
vbox.setFocusTraversable(false);
var scroll = new ScrollPane(vbox);
refresh(scroll, vbox, shown, all, cache, false);
var hadScene = new AtomicBoolean(false);
scroll.sceneProperty().subscribe(scene -> {
if (scene != null) {
hadScene.set(true);
refresh(scroll, vbox, shown, all, cache, true);
}
});
refresh(scroll, vbox, shown, all, cache, false, false);
shown.addListener((ListChangeListener<? super T>) (c) -> {
Platform.runLater(() -> {
if (scroll.getScene() == null && hadScene.get()) {
return;
}
refresh(scroll, vbox, c.getList(), all, cache, true, true);
});
refresh(scroll, vbox, c.getList(), all, cache, true);
});
all.addListener((ListChangeListener<? super T>) c -> {
synchronized (cache) {
cache.keySet().retainAll(c.getList());
}
});
if (scrollBar) {
@ -106,84 +92,50 @@ 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) -> {
dirty.set(true);
updateVisibilities(scroll, vbox);
});
scroll.heightProperty().addListener((observable, oldValue, newValue) -> {
dirty.set(true);
updateVisibilities(scroll, vbox);
});
vbox.heightProperty().addListener((observable, oldValue, newValue) -> {
dirty.set(true);
Platform.runLater(() -> {
updateVisibilities(scroll, vbox);
});
});
// 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) -> {
dirty.set(true);
PlatformThread.runLaterIfNeeded(() -> {
updateVisibilities(scroll, vbox);
});
});
}
BrowserFullSessionModel.DEFAULT.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
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);
});
});
});
PlatformThread.runLaterIfNeeded(() -> {
updateVisibilities(scroll, vbox);
});
}
});
vbox.sceneProperty().addListener((observable, oldValue, newValue) -> {
dirty.set(true);
if (newValue != null) {
animationTimer.start();
} else {
animationTimer.stop();
}
Node c = vbox;
do {
c.boundsInParentProperty().addListener((change, oldBounds,newBounds) -> {
dirty.set(true);
while ((c = c.getParent()) != null) {
c.boundsInParentProperty().addListener((observable1, oldValue1, newValue1) -> {
updateVisibilities(scroll, vbox);
});
// Don't listen to root node changes, that seemingly can cause exceptions
} while ((c = c.getParent()) != null && c.getParent() != null);
}
Platform.runLater(() -> {
updateVisibilities(scroll, vbox);
});
if (newValue != null) {
newValue.heightProperty().addListener((observable1, oldValue1, newValue1) -> {
dirty.set(true);
updateVisibilities(scroll, vbox);
});
}
});
return new SimpleCompStructure<>(scroll);
}
private boolean isVisible(ScrollPane pane, VBox box, Node node) {
@ -226,20 +178,9 @@ 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);
}
}
@ -249,42 +190,44 @@ 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, so add both
synchronized (shown) {
set.addAll(shown);
}
synchronized (all) {
set.addAll(all);
}
// These lists might diverge on updates
set.addAll(shown);
set.addAll(all);
// Clear cache of unused values
cache.keySet().removeIf(t -> !set.contains(t));
}
// 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);
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();
}
cache.put(v, r);
} else {
cache.put(v, null);
}
}
return cache.get(v);
}).filter(region -> region != null).toList();
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();
if (listView.getChildren().equals(newShown)) {
return;
@ -304,6 +247,11 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
updateVisibilities(scroll, listView);
}
};
update.run();
if (asynchronous) {
Platform.runLater(update);
} else {
PlatformThread.runLaterIfNeeded(update);
}
}
}

View file

@ -2,7 +2,6 @@ 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;
@ -16,11 +15,9 @@ 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, boolean log) {
this.log = log;
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) {
this.content = FXCollections.observableMap(content);
}
@ -37,14 +34,7 @@ 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);
@ -52,9 +42,6 @@ public class MultiContentComp extends SimpleComp {
});
});
m.put(e.getKey(), r);
if (log) {
TrackEvent.trace("Added content tab region for element " + name);
}
}
return stack;

View file

@ -10,8 +10,6 @@ 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;
@ -70,32 +68,23 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
@Override
public Structure createBase() {
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) -> {
var text = new PasswordField();
text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
text.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 && field.getText().isEmpty())
|| Objects.equals(field.getText(), n != null ? n.getSecretValue() : null)) {
if ((n == null && text.getText().isEmpty())
|| Objects.equals(text.getText(), n != null ? n.getSecretValue() : null)) {
return;
}
field.setText(n != null ? n.getSecretValue() : null);
text.setText(n != null ? n.getSecretValue() : null);
});
});
HBox.setHgrow(field, Priority.ALWAYS);
HBox.setHgrow(text, Priority.ALWAYS);
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
ClipboardHelper.copyPassword(value.getValue());
@ -104,7 +93,7 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
.tooltipKey("copyPassword")
.createRegion();
var ig = new InputGroup(field);
var ig = new InputGroup(text);
ig.setFillHeight(true);
ig.getStyleClass().add("secret-field-comp");
if (allowCopy) {
@ -114,10 +103,10 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
ig.focusedProperty().addListener((c, o, n) -> {
if (n) {
field.requestFocus();
text.requestFocus();
}
});
return new Structure(ig, field);
return new Structure(ig, text);
}
}

View file

@ -76,9 +76,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var shortcut = e.combination();
b.apply(new TooltipAugment<>(e.name(), shortcut));
b.apply(struc -> {
AppFontSizes.lg(struc.get());
struc.get().setAlignment(Pos.CENTER);
AppFontSizes.xl(struc.get());
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
@ -125,7 +123,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
.tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> {
AppFontSizes.lg(struc.get());
AppFontSizes.xl(struc.get());
});
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {

View file

@ -24,6 +24,7 @@ public class StackComp extends Comp<CompStructure<StackPane>> {
pane.getChildren().add(c.createRegion());
}
pane.setAlignment(Pos.CENTER);
pane.setPickOnBounds(false);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -4,7 +4,6 @@ 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;
@ -58,11 +57,6 @@ 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);

View file

@ -82,8 +82,7 @@ public class OsLogoComp extends SimpleComp {
}
return ICONS.entrySet().stream()
.filter(e -> name.toLowerCase().contains(e.getKey()) ||
name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
.filter(e -> name.toLowerCase().contains(e.getKey()))
.findAny()
.map(e -> e.getValue())
.orElse("os/linux.svg");

View file

@ -21,11 +21,6 @@ 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());

View file

@ -170,7 +170,6 @@ 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(
() -> {

View file

@ -24,7 +24,6 @@ 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;
@ -200,7 +199,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
selected),
() -> {});
button.apply(struc -> {
struc.get().setMaxWidth(20000);
struc.get().setMaxWidth(2000);
struc.get().setAlignment(Pos.CENTER_LEFT);
Comp<?> graphic = PrettyImageHelper.ofFixedSize(
Bindings.createStringBinding(
@ -225,14 +224,6 @@ 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");
@ -250,7 +241,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
StackPane.setMargin(icon, new Insets(10));
pane.setPickOnBounds(false);
StackPane.setAlignment(icon, Pos.CENTER_RIGHT);
pane.setMaxWidth(20000);
pane.setMaxWidth(2000);
r.prefWidthProperty().bind(pane.widthProperty());
r.maxWidthProperty().bind(pane.widthProperty());
return pane;

View file

@ -183,10 +183,6 @@ 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(),
@ -208,7 +204,6 @@ public class StoreCreationComp extends DialogComp {
}
}
}
consumer.accept(e);
});
},
true,

View file

@ -68,10 +68,10 @@ public abstract class StoreEntryComp extends SimpleComp {
}
}
public static StoreEntryComp customSection(StoreSection e) {
public static StoreEntryComp customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customEntryComp(e, e.getDepth() == 1);
return prov.customEntryComp(e, topLevel);
} else {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
@ -81,8 +81,6 @@ public abstract class StoreEntryComp extends SimpleComp {
public abstract boolean isFullSize();
public abstract int getHeight();
@Override
protected final Region createSimple() {
var r = createContent();
@ -359,7 +357,9 @@ public abstract class StoreEntryComp extends SimpleComp {
getWrapper().moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null) {
if (storeCategoryWrapper.getParent() == null
|| storeCategoryWrapper.equals(
getWrapper().getCategory().getValue())) {
m.setDisable(true);
}

View file

@ -27,11 +27,11 @@ public class StoreEntryListComp extends SimpleComp {
.getAllChildren()
.getList(),
(StoreSection e) -> {
var custom = StoreSection.customSection(e).hgrow();
var custom = StoreSection.customSection(e, true).hgrow();
return custom;
},
true);
content.setVisibilityControl(true);
content.setPlatformPauseInterval(50);
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, false).createRegion();
return new MultiContentComp(map).createRegion();
}
}

View file

@ -32,6 +32,23 @@ import java.util.function.Function;
public class StoreEntryListOverviewComp extends SimpleComp {
private final Property<StoreSortMode> sortMode;
public StoreEntryListOverviewComp() {
this.sortMode = new SimpleObjectProperty<>();
StoreViewState.get().getActiveCategory().subscribe(val -> {
sortMode.setValue(val.getSortMode().getValue());
});
sortMode.addListener((observable, oldValue, newValue) -> {
var cat = StoreViewState.get().getActiveCategory().getValue();
if (cat == null) {
return;
}
cat.getSortMode().setValue(newValue);
});
}
private Region createGroupListHeader() {
var label = new Label();
var name = BindingsHelper.flatMap(
@ -125,7 +142,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
private Comp<?> createAlphabeticalSortButton() {
var sortMode = StoreViewState.get().getSortMode();
var icon = Bindings.createObjectBinding(
() -> {
if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_ASC) {
@ -166,7 +182,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
private Comp<?> createDateSortButton() {
var sortMode = StoreViewState.get().getSortMode();
var icon = Bindings.createObjectBinding(
() -> {
if (sortMode.getValue() == StoreSortMode.DATE_ASC) {

View file

@ -32,6 +32,7 @@ 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<>();
@ -103,6 +104,10 @@ public class StoreEntryWrapper {
setupListeners();
}
public void applyLastAccess() {
this.lastAccessApplied.setValue(lastAccess.getValue());
}
public void moveTo(DataStoreCategory category) {
ThreadHelper.runAsync(() -> {
DataStorage.get().moveEntryToCategory(entry, category);
@ -125,7 +130,8 @@ public class StoreEntryWrapper {
public void delete() {
ThreadHelper.runAsync(() -> {
DataStorage.get().deleteWithChildren(this.entry);
DataStorage.get().deleteChildren(this.entry);
DataStorage.get().deleteStoreEntry(this.entry);
});
}

View file

@ -6,12 +6,10 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.icon.SystemIcon;
import io.xpipe.app.icon.SystemIconCache;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.resources.AppImages;
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.*;
@ -108,29 +106,19 @@ public class StoreIconChoiceComp extends SimpleComp {
}
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
var available = icons.stream()
.filter(systemIcon -> AppImages.hasNormalImage("icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
.toList();
table.getPlaceholder().setVisible(available.size() == 0);
var filtered = available;
if (filterString != null && !filterString.isBlank() && filterString.length() >= 2) {
filtered = available.stream().filter(icon -> containsString(icon.getId(), filterString)).toList();
}
var data = partitionList(filtered, columns);
table.getItems().setAll(data);
var displayedIcons = filterString == null || filterString.isBlank() || filterString.length() < 2
? icons.stream()
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
.toList()
: icons.stream()
.filter(icon -> containsString(icon.getId(), filterString))
.toList();
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);
}
var data = partitionList(displayedIcons, columns);
table.getItems().setAll(data);
}
private <T> List<List<T>> partitionList(List<T> list, int size) {
private <T> Collection<List<T>> partitionList(List<T> list, int size) {
List<List<T>> partitions = new ArrayList<>();
if (list.size() == 0) {
return partitions;

View file

@ -53,12 +53,12 @@ public class StoreSection {
}
}
public static Comp<?> customSection(StoreSection e) {
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customSectionComp(e);
return prov.customSectionComp(e, topLevel);
} else {
return new StoreSectionComp(e);
return new StoreSectionComp(e, topLevel);
}
}
@ -96,7 +96,7 @@ public class StoreSection {
var current = mappedSortMode.getValue();
if (current != null) {
return current.comparator().compare(o1, o2);
return current.comparator().compare(current.representative(o1), current.representative(o2));
} else {
return 0;
}

View file

@ -1,165 +0,0 @@
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;
}
}

View file

@ -12,14 +12,11 @@ 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;
@ -27,24 +24,103 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class StoreSectionComp extends StoreSectionBaseComp {
public class StoreSectionComp extends Comp<CompStructure<VBox>> {
public StoreSectionComp(StoreSection section) {
super(section);
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;
}
@Override
public CompStructure<VBox> createBase() {
var entryButton = StoreEntryComp.customSection(section);
entryButton.hgrow();
entryButton.apply(struc -> {
struc.get().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
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 -> {
if (event.getCode() == KeyCode.SPACE) {
section.getWrapper().toggleExpanded();
event.consume();
}
if (event.getCode() == KeyCode.RIGHT) {
var ref = (VBox) ((HBox) struc.get().getParent()).getChildren().getFirst();
var ref = (VBox) struc.get().getChildren().getFirst();
if (entryButton.isFullSize()) {
var btn = (Button) ref.getChildren().getFirst();
btn.fire();
@ -54,45 +130,72 @@ public class StoreSectionComp extends StoreSectionBaseComp {
});
});
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));
// 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 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(Bindings.not(effectiveExpanded)),
content));
full.styleClass("store-entry-section-comp");
full.apply(struc -> {
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 -> {
struc.get().setFillWidth(true);
var hbox = ((HBox) struc.get().getChildren().getFirst());
addPseudoClassListeners(struc.get(), section.getWrapper().getExpanded());
addVisibilityListeners(struc.get(), hbox);
});
return full.createStructure();
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();
}
}

View file

@ -12,18 +12,23 @@ 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 StoreSectionBaseComp {
public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
private final BooleanProperty expanded;
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 BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment;
private final Consumer<StoreSection> action;
@ -31,61 +36,142 @@ public class StoreSectionMiniComp extends StoreSectionBaseComp {
StoreSection section,
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment,
Consumer<StoreSection> action) {
super(section);
this.section = 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(), () -> {
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(
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {})
.apply(struc -> {
struc.get()
.setGraphic(PrettyImageHelper.ofFixedSize(
section.getWrapper().getIconFile(), 16, 16)
.createRegion());
struc.get().setMnemonicParsing(false);
});
})
.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");
augment.accept(section, root);
var expandButton = createExpandButton(() -> expanded.set(!expanded.get()), 20, expanded);
expandButton.focusTraversable();
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 quickAccessButton = createQuickAccessButton(20, action);
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 buttonList = new ArrayList<Comp<?>>();
buttonList.add(expandButton);
buttonList.add(button);
buttonList.add(root);
if (section.getDepth() == 1) {
buttonList.add(quickAccessButton);
}
var h = new HorizontalComp(buttonList);
h.apply(struc -> struc.get().setFillHeight(true));
h.prefHeight(28);
list.add(h);
list.add(new HorizontalComp(buttonList).apply(struc -> struc.get().setFillHeight(true)));
} else {
expanded = new SimpleBooleanProperty(true);
}
var content = createChildrenList(c -> new StoreSectionMiniComp(c, this.augment, this.action), Bindings.not(expanded));
list.add(content);
// 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 full = new VerticalComp(list);
full.styleClass("store-section-mini-comp");
full.apply(struc -> {
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 -> {
struc.get().setFillWidth(true);
addPseudoClassListeners(struc.get(), expanded);
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 -> {
if (section.getWrapper() != null) {
var hbox = ((HBox) struc.get().getChildren().getFirst());
addVisibilityListeners(struc.get(), hbox);
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");
}
});
}
});
return full.createStructure();
})
.createStructure();
}
}

View file

@ -1,12 +1,19 @@
package io.xpipe.app.comp.store;
import java.time.Instant;
import java.util.*;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
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() {
@ -20,6 +27,11 @@ public interface StoreSortMode {
}
};
StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override
public String getId() {
return "alphabetical-asc";
@ -32,10 +44,10 @@ public interface StoreSortMode {
.reversed();
}
};
StoreSortMode DATE_DESC = new StoreSortMode.DateSortMode() {
StoreSortMode DATE_DESC = new StoreSortMode() {
protected Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccess().getValue();
private Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccessApplied().getValue();
if (la == null) {
return Instant.MAX;
}
@ -44,19 +56,35 @@ public interface StoreSortMode {
}
@Override
protected int compare(Instant s1, Instant s2) {
return s1.compareTo(s2);
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();
}
@Override
public String getId() {
return "date-desc";
}
};
StoreSortMode DATE_ASC = new StoreSortMode.DateSortMode() {
protected Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccess().getValue();
@Override
public Comparator<StoreSection> comparator() {
return Comparator.comparing(e -> {
return date(e);
});
}
};
StoreSortMode DATE_ASC = new StoreSortMode() {
private Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccessApplied().getValue();
if (la == null) {
return Instant.MIN;
}
@ -65,16 +93,32 @@ public interface StoreSortMode {
}
@Override
protected int compare(Instant s1, Instant s2) {
return s2.compareTo(s1);
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();
}
@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) {
@ -87,54 +131,9 @@ 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));
};
}
}
}

View file

@ -39,9 +39,6 @@ public class StoreViewState {
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
@Getter
private final Property<StoreSortMode> sortMode = new SimpleObjectProperty<>();
@Getter
private StoreSection currentTopLevelSection;
@ -121,23 +118,15 @@ 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()
.map(StoreCategoryWrapper::new)
.toList()));
sortMode.addListener((observable, oldValue, newValue) -> {
var cat = getActiveCategory().getValue();
if (cat == null) {
return;
}
cat.getSortMode().setValue(newValue);
});
activeCategory.addListener((observable, oldValue, newValue) -> {
DataStorage.get().setSelectedCategory(newValue.getCategory());
sortMode.setValue(newValue.getSortMode().getValue());
});
var selected = AppCache.getNonNull("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);
activeCategory.setValue(categories.getList().stream()
@ -152,6 +141,7 @@ public class StoreViewState {
}
public void updateDisplay() {
allEntries.getList().forEach(e -> e.applyLastAccess());
toggleStoreListUpdate();
}
@ -190,6 +180,7 @@ 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

View file

@ -20,6 +20,10 @@ 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(

View file

@ -56,7 +56,10 @@ 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().build());
client.get()
.performRequest(DaemonFocusExchange.Request.builder()
.mode(XPipeDaemonMode.GUI)
.build());
if (!inputs.isEmpty()) {
client.get()
.performRequest(DaemonOpenExchange.Request.builder()

View file

@ -19,7 +19,6 @@ import javafx.application.ColorScheme;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@ -84,40 +83,33 @@ public class AppTheme {
public static void init() {
if (init) {
TrackEvent.trace("Theme init requested again");
return;
}
if (AppPrefs.get() == null) {
TrackEvent.trace("Theme init prior to prefs init, setting theme to default");
Theme.getDefaultLightTheme().apply();
return;
}
try {
var lastSystemDark = AppCache.getBoolean("lastDarkTheme", false);
var nowDark = isDarkMode();
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
AppCache.update("lastDarkTheme", nowDark);
if (AppPrefs.get().theme().getValue() == null || lastSystemDark != nowDark) {
TrackEvent.trace("Updating theme to system theme");
setDefault();
}
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
TrackEvent.withTrace("Platform preference changed").tag("change", change.toString()).handle();
});
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
if (change.getKey().equals("GTK.theme_name")) {
Platform.runLater(() -> {
updateThemeToThemeName(change.getValueRemoved(), change.getValueAdded());
});
}
});
Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {
Platform.runLater(() -> {
updateThemeToColorScheme(t1);
if (t1 == ColorScheme.DARK
&& !AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (t1 != ColorScheme.DARK
&& AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
});
});
} catch (IllegalStateException ex) {
@ -138,52 +130,12 @@ public class AppTheme {
init = true;
}
private static void updateThemeToThemeName(Object oldName, Object newName) {
if (OsType.getLocal() == OsType.LINUX && newName != null) {
var toDark = (oldName == null || !oldName.toString().contains("-dark")) &&
newName.toString().contains("-dark");
var toLight = (oldName == null || oldName.toString().contains("-dark")) &&
!newName.toString().contains("-dark");
if (toDark) {
updateThemeToColorScheme(ColorScheme.DARK);
} else if (toLight) {
updateThemeToColorScheme(ColorScheme.LIGHT);
}
}
}
private static boolean isDarkMode() {
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
if (nowDark) {
return true;
}
var gtkTheme = Platform.getPreferences().get("GTK.theme_name");
return gtkTheme != null && gtkTheme.toString().contains("-dark");
}
private static void updateThemeToColorScheme(ColorScheme colorScheme) {
if (colorScheme == null) {
return;
}
if (colorScheme == ColorScheme.DARK
&& !AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (colorScheme != ColorScheme.DARK
&& AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
}
public static void reset() {
if (!init) {
return;
}
var nowDark = isDarkMode();
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
AppCache.update("lastDarkTheme", nowDark);
}
@ -210,26 +162,19 @@ public class AppTheme {
}
PlatformThread.runLaterIfNeeded(() -> {
var window = AppMainWindow.getInstance();
if (window == null) {
if (AppMainWindow.getInstance() == null) {
return;
}
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();
var window = AppMainWindow.getInstance().getStage();
var scene = window.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!
@ -355,7 +300,7 @@ public class AppTheme {
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),
91);
115);
// Adjust this to create your own theme
public static final Theme CUSTOM = new DerivedTheme(

View file

@ -0,0 +1,36 @@
package io.xpipe.app.core.check;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import java.util.concurrent.TimeUnit;
public class AppBundledToolsCheck {
private static boolean getResult() {
var fc = new ProcessBuilder("where", "ssh")
.redirectErrorStream(true)
.redirectOutput(ProcessBuilder.Redirect.DISCARD);
try {
var proc = fc.start();
proc.waitFor(2, TimeUnit.SECONDS);
return proc.exitValue() == 0;
} catch (Exception e) {
return false;
}
}
public static void check() {
if (AppPrefs.get().useBundledTools().get()) {
return;
}
if (!OsType.getLocal().equals(OsType.WINDOWS)) {
return;
}
if (!getResult()) {
AppPrefs.get().useBundledTools.set(true);
}
}
}

View file

@ -56,6 +56,9 @@ public class BaseMode extends OperationMode {
TrackEvent.info("Initializing base mode components ...");
AppMainWindow.loadingText("initializingApp");
LicenseProvider.get().init();
// We no longer need this
// AppCertutilCheck.check();
AppBundledToolsCheck.check();
AppHomebrewCoreutilsCheck.check();
AppAvCheck.check();
AppJavaOptionsCheck.check();
@ -91,7 +94,6 @@ public class BaseMode extends OperationMode {
AppPrefs.setLocalDefaultsIfNeeded();
PlatformInit.init(true);
AppMainWindow.addUpdateTitleListener();
TrackEvent.info("Shell initialization thread completed");
},
() -> {
shellLoaded.await();
@ -104,16 +106,15 @@ 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 {
@ -123,7 +124,6 @@ public class BaseMode extends OperationMode {
}
});
UpdateChangelogAlert.showIfNeeded();
TrackEvent.info("Connection storage initialization thread completed");
},
() -> {
AppFileWatcher.init();
@ -131,7 +131,6 @@ public class BaseMode extends OperationMode {
BlobManager.init();
TerminalView.init();
TerminalLauncherManager.init();
TrackEvent.info("File/Watcher initialization thread completed");
},
() -> {
PlatformInit.init(true);
@ -140,16 +139,13 @@ 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();

View file

@ -180,8 +180,6 @@ 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();
}

View file

@ -84,7 +84,7 @@ public class AppDialog {
var transition = new PauseTransition(Duration.millis(200));
transition.setOnFinished(e -> {
if (wait) {
PlatformThread.exitNestedEventLoop(key);
Platform.exitNestedEventLoop(key, null);
}
});
transition.play();
@ -95,7 +95,7 @@ public class AppDialog {
}
});
if (wait) {
PlatformThread.enterNestedEventLoop(key);
Platform.enterNestedEventLoop(key);
waitForDialogClose(o);
}
}
@ -108,7 +108,6 @@ 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;
@ -119,7 +118,6 @@ 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);

View file

@ -138,10 +138,8 @@ 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);
}
@ -152,13 +150,6 @@ public class AppMainWindow {
}
}
public void focus() {
PlatformThread.runLaterIfNeeded(() -> {
stage.setIconified(false);
stage.requestFocus();
});
}
private static String createTitle() {
var t = LicenseProvider.get().licenseTitle();
var base =

View file

@ -124,13 +124,10 @@ public class ModifiedStage extends Stage {
var transition = new PauseTransition(Duration.millis(300));
transition.setOnFinished(e -> {
applyModes(stage);
// 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);
});
}
stage.setWidth(stage.getWidth() - 1);
Platform.runLater(() -> {
stage.setWidth(stage.getWidth() + 1);
});
});
transition.play();
});

View file

@ -2,7 +2,6 @@ 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;
@ -22,7 +21,6 @@ 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();
@ -30,7 +28,6 @@ public interface ActionProvider {
ErrorEvent.fromThrowable(t).handle();
}
}
TrackEvent.trace("Finished action provider initialization");
}
default void init() throws Exception {}

View file

@ -93,8 +93,8 @@ public interface DataStoreProvider {
return StoreEntryComp.create(s, null, preferLarge);
}
default StoreSectionComp customSectionComp(StoreSection section) {
return new StoreSectionComp(section);
default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) {
return new StoreSectionComp(section, topLevel);
}
default boolean shouldShowScan() {

View file

@ -7,7 +7,6 @@ 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.*;
@ -22,14 +21,6 @@ import javax.imageio.ImageIO;
public class SystemIconCache {
private static enum ImageColorScheme {
TRANSPARENT,
MIXED,
LIGHT,
DARK
}
private static final Path DIRECTORY =
AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("raster");
private static final int[] sizes = new int[] {16, 24, 40, 80};
@ -59,31 +50,11 @@ public class SystemIconCache {
Files.createDirectories(target);
for (var icon : e.getValue().getIcons()) {
var dark = icon.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK;
if (refreshChecksum(icon.getFile(), target, icon.getName(), dark)) {
if (refreshChecksum(icon.getFile(), target, icon.getName(), icon.isDark())) {
continue;
}
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;
}
if (scheme != ImageColorScheme.DARK || icon.getColorSchemeData() != SystemIconSourceFile.ColorSchemeData.DEFAULT) {
continue;
}
var hasExplicitDark = e.getValue().getIcons().stream().anyMatch(
systemIconSourceFile -> systemIconSourceFile.getSource().equals(icon.getSource()) &&
systemIconSourceFile.getName().equals(icon.getName()) &&
systemIconSourceFile.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK);
if (hasExplicitDark) {
continue;
}
rasterizeSizesInverted(icon.getFile(), target, icon.getName(), true);
rasterizeSizes(icon.getFile(), target, icon.getName(), icon.isDark());
}
}
} catch (Exception e) {
@ -106,60 +77,28 @@ public class SystemIconCache {
}
}
private static ImageColorScheme rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
TrackEvent.trace("Rasterizing image " + path.getFileName().toString());
private static boolean rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
try {
ImageColorScheme c = null;
for (var size : sizes) {
var image = rasterize(path, size);
if (image == null) {
continue;
}
if (c == null) {
c = determineColorScheme(image);
if (c == ImageColorScheme.TRANSPARENT) {
return ImageColorScheme.TRANSPARENT;
}
}
write(dir, name, dark, size, image);
rasterize(path, dir, name, dark, size);
}
return c;
} catch (Exception ex) {
var message = "Failed to rasterize icon icon " + path.getFileName().toString() + ": " + ex.getMessage();
ErrorEvent.fromThrowable(ex).description(message).omit().expected().handle();
return null;
}
}
private static ImageColorScheme rasterizeSizesInverted(Path path, Path dir, String name, boolean dark) throws IOException {
try {
ImageColorScheme c = null;
for (var size : sizes) {
var image = rasterize(path, size);
if (image == null) {
continue;
}
var invert = invert(image);
write(dir, name, dark, size, invert);
}
return c;
return true;
} catch (Exception ex) {
if (ex instanceof IOException) {
throw ex;
}
ErrorEvent.fromThrowable(ex).omit().expected().handle();
return null;
return false;
}
}
private static BufferedImage rasterize(Path path, int px) throws IOException {
private static void rasterize(Path path, Path dir, String name, boolean dark, int px) throws IOException {
SVGLoader loader = new SVGLoader();
URL svgUrl = path.toUri().toURL();
SVGDocument svgDocument = loader.load(svgUrl);
if (svgDocument == null) {
return null;
return;
}
BufferedImage image = new BufferedImage(px, px, BufferedImage.TYPE_INT_ARGB);
@ -170,67 +109,8 @@ public class SystemIconCache {
g.setRenderingHint(SVGRenderingHints.KEY_SOFT_CLIPPING, SVGRenderingHints.VALUE_SOFT_CLIPPING_ON);
svgDocument.render((Component) null, g, new ViewBox(0, 0, px, px));
g.dispose();
return image;
}
private static BufferedImage write(Path dir, String name, boolean dark, int px, BufferedImage image) throws IOException {
var out = dir.resolve(name + "-" + px + (dark ? "-dark" : "") + ".png");
ImageIO.write(image, "png", out.toFile());
return image;
}
private static BufferedImage invert(BufferedImage image) {
var buffer = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
buffer.setRGB(x, y, new Color(255- red, 255- green, 255- blue, alpha).getRGB());
}
}
return buffer;
}
private static ImageColorScheme determineColorScheme(BufferedImage image) {
var transparent = true;
var counter = 0;
var mean = 0.0;
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
if (alpha > 0) {
transparent = false;
}
if (alpha < 200) {
continue;
}
mean += red + green + blue;
counter++;
}
}
if (transparent) {
return ImageColorScheme.TRANSPARENT;
}
mean /= counter * 3;
if (mean < 50) {
return ImageColorScheme.DARK;
} else if (mean > 205) {
return ImageColorScheme.LIGHT;
} else {
return ImageColorScheme.MIXED;
}
}
}

View file

@ -80,18 +80,7 @@ public class SystemIconManager {
});
}
private static void reloadImages() {
AppImages.remove(s -> s.startsWith("icons/"));
try {
for (var source : getEffectiveSources()) {
AppImages.loadRasterImages(SystemIconCache.getDirectory(source), "icons/" + source.getId());
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
private static void clearInvalidImages() {
public static void reloadImages() {
AppImages.remove(s -> s.startsWith("icons/"));
try {
for (var source : getEffectiveSources()) {

View file

@ -1,13 +1,11 @@
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;
@ -92,14 +90,7 @@ public interface SystemIconSource {
@Override
public void refresh() throws Exception {
try (var sc =
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;
}
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
var dir = SystemIconManager.getPoolPath().resolve(id);
if (!Files.exists(dir)) {
sc.command(CommandBuilder.of()

View file

@ -31,53 +31,25 @@ public class SystemIconSourceData {
}
var files = Files.walk(dir).toList();
List<Path> flatFiles = files.stream()
.filter(path -> Files.isRegularFile(path))
.filter(path -> path.toString().endsWith(".svg"))
.map(path -> {
var name = FilenameUtils.getBaseName(path.getFileName().toString());
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
var cleanedPath = path.getParent().resolve(cleanedName + ".svg");
return cleanedPath;
}).toList();
for (var file : flatFiles) {
var name = FilenameUtils.getBaseName(file.getFileName().toString());
var displayName = name.toLowerCase(Locale.ROOT);
var baseFile = file.getParent().resolve(name + ".svg");
var hasBaseVariant = Files.exists(baseFile);
var darkModeFile = file.getParent().resolve(name + "-light.svg");
var hasDarkModeVariant = Files.exists(darkModeFile);
var lightModeFile = file.getParent().resolve(name + "-dark.svg");
var hasLightModeVariant = Files.exists(lightModeFile);
if (hasBaseVariant && hasDarkModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
continue;
}
if (hasBaseVariant && hasLightModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DARK));
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
continue;
}
if (!hasBaseVariant) {
if (hasLightModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
if (hasDarkModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
}
} else {
if (hasDarkModeVariant) {
sourceFiles.add(
new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
}
for (var file : files) {
if (file.getFileName().toString().endsWith(".svg")) {
var name = FilenameUtils.getBaseName(file.getFileName().toString());
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
var hasLightVariant = Files.exists(file.getParent().resolve(cleanedName + "-light.svg"));
var hasDarkVariant = Files.exists(file.getParent().resolve(cleanedName + "-dark.svg"));
if (hasLightVariant && !hasDarkVariant && name.endsWith("-light")) {
var s = new SystemIconSourceFile(source, cleanedName.toLowerCase(Locale.ROOT), file, true);
sourceFiles.add(s);
continue;
}
continue;
}
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
if (hasLightVariant && hasDarkVariant && (name.endsWith("-dark") || name.endsWith("-light"))) {
continue;
}
var s = new SystemIconSourceFile(source, cleanedName.toLowerCase(Locale.ROOT), file, false);
sourceFiles.add(s);
}
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();

View file

@ -7,13 +7,8 @@ import java.nio.file.Path;
@Value
public class SystemIconSourceFile {
public static enum ColorSchemeData {
DARK,
DEFAULT;
}
SystemIconSource source;
String name;
Path file;
ColorSchemeData colorSchemeData;
boolean dark;
}

View file

@ -39,14 +39,6 @@ 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;
@ -147,16 +139,6 @@ 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(
@ -194,7 +176,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() + "" : "false");
s.setTag("initial", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + "" : null);
var exMessage = ee.getThrowable() != null ? ee.getThrowable().getMessage() : null;
if (ee.getDescription() != null
@ -249,11 +231,7 @@ public class SentryErrorHandler implements ErrorHandler {
if (hasEmail) {
fb.setEmail(email);
}
if (doesExceedCommentSize(text)) {
fb.setComments("<Attachment>");
} else {
fb.setComments(text);
}
fb.setComments(text);
Sentry.captureUserFeedback(fb);
}
Sentry.flush(3000);

View file

@ -32,8 +32,8 @@ public class AboutCategory extends AppPrefsCategory {
.grow(true, false),
null)
.addComp(
new TileButtonComp("documentation", "documentationDescription", "mdi2b-book-open-variant", e -> {
Hyperlinks.open(Hyperlinks.DOCS);
new TileButtonComp("slack", "slackDescription", "mdi2s-slack", e -> {
Hyperlinks.open(Hyperlinks.SLACK);
e.consume();
})
.grow(true, false),
@ -45,6 +45,13 @@ public class AboutCategory extends AppPrefsCategory {
})
.grow(true, false),
null)
.addComp(
new TileButtonComp("securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> {
Hyperlinks.open(Hyperlinks.DOCS_SECURITY);
e.consume();
})
.grow(true, false),
null)
.addComp(
new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
Hyperlinks.open(Hyperlinks.DOCS_PRIVACY);
@ -59,8 +66,7 @@ public class AboutCategory extends AppPrefsCategory {
.styleClass("open-source-notices");
var modal = ModalOverlay.of("openSourceNotices", comp);
modal.show();
})
.grow(true, false))
}))
.addComp(
new TileButtonComp("eula", "eulaDescription", "mdi2c-card-text-outline", e -> {
Hyperlinks.open(Hyperlinks.DOCS_EULA);

View file

@ -9,7 +9,6 @@ import io.xpipe.app.icon.SystemIconSource;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.util.ModuleHelper;
@ -58,6 +57,8 @@ public class AppPrefs {
mapVaultShared(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class, false);
public final BooleanProperty performanceMode =
mapLocal(new SimpleBooleanProperty(), "performanceMode", Boolean.class, false);
public final BooleanProperty useBundledTools =
mapLocal(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class, true);
public final ObjectProperty<AppTheme.Theme> theme =
mapLocal(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class, false);
final BooleanProperty useSystemFont =
@ -117,8 +118,6 @@ 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 =
@ -273,7 +272,7 @@ public class AppPrefs {
INSTANCE = new AppPrefs();
PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler));
INSTANCE.loadLocal();
INSTANCE.adjustLocalValues();
INSTANCE.fixInvalidLocalValues();
INSTANCE.vaultStorageHandler = new AppPrefsStorageHandler(
INSTANCE.storageDirectory().getValue().resolve("preferences.json"));
}
@ -327,6 +326,10 @@ public class AppPrefs {
return performanceMode;
}
public ObservableBooleanValue useBundledTools() {
return useBundledTools;
}
public ObservableValue<Boolean> useSystemFont() {
return useSystemFont;
}
@ -407,10 +410,6 @@ public class AppPrefs {
return customEditorCommand;
}
public ObservableBooleanValue customEditorCommandInTerminal() {
return customEditorCommandInTerminal;
}
public final ReadOnlyIntegerProperty editorReloadTimeout() {
return editorReloadTimeout;
}
@ -532,7 +531,7 @@ public class AppPrefs {
}
}
private void adjustLocalValues() {
private void fixInvalidLocalValues() {
// You can set the directory to empty in the settings
if (storageDirectory.get() == null || storageDirectory.get().toString().isBlank()) {
storageDirectory.setValue(DEFAULT_STORAGE_DIR);
@ -544,11 +543,6 @@ public class AppPrefs {
ErrorEvent.fromThrowable(e).expected().build().handle();
storageDirectory.setValue(DEFAULT_STORAGE_DIR);
}
if (AppProperties.get().isInitialLaunch()) {
var f = PlatformState.determineDefaultScalingFactor();
uiScale.setValue(f.isPresent() ? f.getAsInt() : null);
}
}
private void loadSharedRemote() {

View file

@ -52,6 +52,7 @@ public class AppPrefsComp extends SimpleComp {
split.setFillHeight(true);
split.getStyleClass().add("prefs");
var stack = new StackPane(split);
stack.setPickOnBounds(false);
return stack;
}
}

View file

@ -14,25 +14,24 @@ public class ConnectionsCategory extends AppPrefsCategory {
@Override
protected Comp<?> create() {
var prefs = AppPrefs.get();
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(localShellBuilder)
var options = new OptionsBuilder()
.addTitle("connections")
.sub(connectionsBuilder) :
new OptionsBuilder()
.addTitle("connections")
.sub(connectionsBuilder)
.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))
.addTitle("localShell")
.sub(localShellBuilder);
.sub(new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell));
if (OsType.getLocal() == OsType.WINDOWS) {
options.addTitle("sshConfiguration")
.sub(new OptionsBuilder()
.pref(prefs.useBundledTools)
.addToggle(prefs.useBundledTools)
.addComp(prefs.getCustomComp("x11WslInstance")));
}
return options.buildComp();

View file

@ -47,13 +47,9 @@ 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)
.nameAndDescription("customEditorCommandInTerminal")
.addToggle(prefs.customEditorCommandInTerminal)
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))
)
.apply(struc -> struc.get().setPromptText("myeditor $FILE"))
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM)))
.addComp(terminalTest))
.buildComp();
}
}

View file

@ -2,7 +2,6 @@ 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;
@ -172,13 +171,10 @@ 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";
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);
}
var format =
customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString())));
}
@Override

View file

@ -108,7 +108,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
ThreadHelper.runFailableAsync(() -> {
// Startup is slow
ThreadHelper.sleep(10000);
FileUtils.deleteQuietly(config.toFile());
Files.delete(config);
});
}
@ -125,12 +125,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
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);
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
launch(configuration.getTitle(), CommandBuilder.of().addFile(file.toString()).add("/cert-ignore").add("/p:'" + escapedPw + "'"));
}
@Override

View file

@ -15,7 +15,6 @@ 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;
@ -112,13 +111,8 @@ 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)
.path(Path.of(dir.get()))
.id(UUID.randomUUID().toString())
.build();
if (!sources.contains(source)) {

View file

@ -7,7 +7,6 @@ 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;
@ -116,7 +115,6 @@ 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())
@ -147,7 +145,6 @@ 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();
@ -156,17 +153,14 @@ 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.8))))
.setOpacity(0.5))))
.padding(new Insets(10, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.apply(struc -> struc.get().setFillHeight(true));

View file

@ -5,7 +5,6 @@ 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;
@ -28,6 +27,14 @@ 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<>();
@ -54,7 +61,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"), () -> {
Hyperlinks.open(Hyperlinks.DOCS_SYNC);
showHelpAlert();
});
var remoteRow = new HorizontalComp(List.of(remoteRepo, helpButton)).spacing(10);
remoteRow.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));

View file

@ -15,7 +15,6 @@ 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;
@ -59,10 +58,6 @@ 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();

View file

@ -451,7 +451,6 @@ public abstract class DataStorage {
newChildren = l.stream()
.filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null)
.toList();
e.getProvider().onChildrenRefresh(e);
} else {
newChildren = null;
}
@ -519,35 +518,14 @@ public abstract class DataStorage {
.toList());
toUpdate.removeIf(pair -> {
// Children classes might not be the same, the same goes for state classes
// This can happen when there are multiple child classes and the ids got switched around
var storeClassMatch = pair.getKey()
.getStore()
.getClass()
.equals(pair.getValue().get().getStore().getClass());
if (!storeClassMatch) {
if (pair.getKey().getStorePersistentState() != null
&& pair.getValue().get().getStorePersistentState() != null) {
return pair.getKey()
.getStorePersistentState()
.equals(pair.getValue().get().getStorePersistentState());
} else {
return true;
}
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
.merge(pair.getValue().getStore().asNeeded());
var mergedStoreChanged = pair.getKey().getStore() != merged;
if (pair.getKey().getStorePersistentState() == null || pair.getValue().get().getStorePersistentState() == null) {
return !mergedStoreChanged;
}
var stateClassMatch = pair.getKey()
.getStorePersistentState()
.getClass()
.equals(pair.getValue().get().getStorePersistentState().getClass());
if (!stateClassMatch) {
return true;
}
var stateChange = !pair.getKey()
.getStorePersistentState()
.equals(pair.getValue().get().getStorePersistentState());
return !mergedStoreChanged && !stateChange;
});
if (toRemove.isEmpty() && toAdd.isEmpty() && toUpdate.isEmpty()) {
@ -569,18 +547,31 @@ public abstract class DataStorage {
}
addStoreEntriesIfNotPresent(toAdd.stream().map(DataStoreEntryRef::get).toArray(DataStoreEntry[]::new));
toUpdate.forEach(pair -> {
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
.merge(pair.getValue().getStore().asNeeded());
if (merged != pair.getKey().getStore()) {
pair.getKey().setStoreInternal(merged, false);
}
// Update state by merging
if (pair.getKey().getStorePersistentState() != null
&& pair.getValue().get().getStorePersistentState() != null) {
var classMatch = pair.getKey()
.getStorePersistentState()
.getClass()
.equals(pair.getValue().get().getStorePersistentState().getClass());
// Children classes might not be the same, the same goes for state classes
// This can happen when there are multiple child classes and the ids got switched around
if (classMatch) {
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
.merge(pair.getValue().getStore().asNeeded());
if (merged != pair.getKey().getStore()) {
pair.getKey().setStoreInternal(merged, false);
}
var s = pair.getKey().getStorePersistentState();
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());
pair.getKey().setStorePersistentState(mergedState);
var s = pair.getKey().getStorePersistentState();
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());
pair.getKey().setStorePersistentState(mergedState);
}
}
});
refreshEntries();
saveAsync();
e.getProvider().onChildrenRefresh(e);
toAdd.forEach(
dataStoreEntryRef -> dataStoreEntryRef.get().getProvider().onParentRefresh(dataStoreEntryRef.get()));
toUpdate.forEach(dataStoreEntryRef ->
@ -600,6 +591,25 @@ 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 -> {

View file

@ -34,10 +34,7 @@ public class DataStoreEntryRef<T extends DataStore> {
}
public void checkComplete() throws Throwable {
var store = getStore();
if (store != null) {
getStore().checkComplete();
}
getStore().checkComplete();
}
public DataStoreEntry get() {
@ -45,7 +42,7 @@ public class DataStoreEntryRef<T extends DataStore> {
}
public T getStore() {
return entry.getStore() != null ? entry.getStore().asNeeded() : null;
return entry.getStore().asNeeded();
}
@SuppressWarnings("unchecked")

View file

@ -59,11 +59,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
// };
static ExternalTerminalType determineFallbackTerminalToOpen(ExternalTerminalType type) {
if (type != XSHELL && type != MOBAXTERM && type != SECURECRT && type != TERMIUS && !(type instanceof WaveTerminalType)) {
if (type == XSHELL || type == MOBAXTERM || type == SECURECRT) {
return ProcessControlProvider.get().getEffectiveLocalDialect() == ShellDialects.CMD ? CMD : POWERSHELL;
}
if (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
@ -640,6 +643,7 @@ 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,
@ -647,7 +651,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
WindowsTerminalType.WINDOWS_TERMINAL,
AlacrittyTerminalType.ALACRITTY_WINDOWS,
WezTerminalType.WEZTERM_WINDOWS,
WarpTerminalType.WINDOWS,
CMD,
PWSH,
POWERSHELL,
@ -679,11 +682,10 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
DEEPIN_TERMINAL,
FOOT,
Q_TERMINAL,
WarpTerminalType.LINUX,
TERMIUS,
WaveTerminalType.WAVE_LINUX);
List<ExternalTerminalType> MACOS_TERMINALS = List.of(
WarpTerminalType.MACOS,
WARP,
ITERM2,
KittyTerminalType.KITTY_MACOS,
TabbyTerminalType.TABBY_MAC_OS,

View file

@ -2,10 +2,8 @@ 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;
@ -79,7 +77,7 @@ public class TerminalLauncherManager {
req = entries.get(request);
}
if (req == null) {
return;
throw new BeaconClientException("Unknown launch request " + request);
}
var byPid = ProcessHandle.of(pid);
if (byPid.isEmpty()) {
@ -92,44 +90,33 @@ public class TerminalLauncherManager {
req.setPid(shell.pid());
}
public static void waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
public static Path waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
TerminalLaunchRequest req;
synchronized (entries) {
req = entries.get(request);
}
if (req == null) {
return;
throw new BeaconClientException("Unknown launch request " + request);
}
if (req.isSetupCompleted()) {
submitAsync(req.getRequest(), req.getProcessControl(), req.getConfig(), req.getWorkingDirectory());
}
try {
req.waitForCompletion();
return req.waitForCompletion();
} finally {
req.setSetupCompleted(true);
}
}
public static Path launchExchange(UUID request) throws BeaconClientException, BeaconServerException {
public static Path launchExchange(UUID request) throws BeaconClientException {
synchronized (entries) {
var e = entries.values().stream()
.filter(entry -> entry.getRequest().equals(request))
.findFirst()
.orElse(null);
if (e == null) {
// 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);
}
throw new BeaconClientException("Unknown launch request " + request);
}
if (!(e.getResult() instanceof TerminalLaunchResult.ResultSuccess)) {

View file

@ -1,140 +1,57 @@
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;
import java.nio.file.Files;
import java.nio.file.Path;
public class WarpTerminalType extends ExternalTerminalType.MacOsType {
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;
}
public WarpTerminalType() {
super("app.warp", "Warp");
}
@Override
default String getWebsite() {
public TerminalOpenFormat getOpenFormat() {
return TerminalOpenFormat.TABBED;
}
@Override
public int getProcessHierarchyOffset() {
return 2;
}
@Override
public String getWebsite() {
return "https://www.warp.dev/";
}
@Override
default boolean isRecommended() {
public boolean isRecommended() {
return true;
}
@Override
default boolean useColoredTitle() {
public boolean useColoredTitle() {
return true;
}
@Override
default boolean shouldClear() {
public boolean shouldClear() {
return false;
}
@Override
default TerminalInitFunction additionalInitCommands() {
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() {
return TerminalInitFunction.of(sc -> {
if (sc.getShellDialect() == ShellDialects.ZSH) {
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"zsh\"}}\\x9c'";

View file

@ -4,7 +4,6 @@ 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;
@ -183,7 +182,7 @@ public abstract class UpdateHandler {
prepareUpdateImpl();
// Show available update in PTB more aggressively
if (AppProperties.get().isStaging() && preparedUpdate.getValue() != null && !OperationMode.isInStartup()) {
if (AppProperties.get().isStaging() && preparedUpdate.getValue() != null) {
UpdateAvailableDialog.showIfNeeded();
}
}

View file

@ -3,6 +3,7 @@ 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;
@ -20,7 +21,7 @@ public class BaseElevationHandler implements ElevationHandler {
}
@Override
public boolean handleRequest(UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive) {
public boolean handleRequest(ShellControl parent, UUID requestId, CountDown countDown, boolean confirmIfNeeded) {
var ref = getSecretRef();
if (ref == null) {
return false;
@ -34,7 +35,7 @@ public class BaseElevationHandler implements ElevationHandler {
List.of(),
List.of(),
countDown,
interactive);
parent.isInteractive());
return true;
}

View file

@ -8,7 +8,6 @@ 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;
@ -16,14 +15,12 @@ import java.util.function.Function;
@SuppressWarnings("InfiniteLoopStatement")
public class BindingsHelper {
private static final Set<ReferenceEntry> REFERENCES = new HashSet<>();
private static final Set<ReferenceEntry> REFERENCES = Collections.newSetFromMap(new ConcurrentHashMap<>());
static {
ThreadHelper.createPlatformThread("referenceGC", true, () -> {
while (true) {
synchronized (REFERENCES) {
REFERENCES.removeIf(ReferenceEntry::canGc);
}
REFERENCES.removeIf(ReferenceEntry::canGc);
ThreadHelper.sleep(1000);
// Use for testing
@ -34,9 +31,7 @@ public class BindingsHelper {
}
public static void preserve(Object source, Object target) {
synchronized (REFERENCES) {
REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));
}
REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));
}
public static <T, U> ObservableValue<U> map(

View file

@ -85,19 +85,9 @@ 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);
@ -110,7 +100,7 @@ public class DerivedObservableList<T> {
var start = 0;
for (int end = 0; end <= list.size(); end++) {
var index = end < list.size() ? indexOfFromStart(newList, list.get(end), end) : newList.size();
var index = end < list.size() ? newList.indexOf(list.get(end)) : newList.size();
for (; start < index; start++) {
list.add(start, newList.get(start));
}
@ -143,8 +133,7 @@ public class DerivedObservableList<T> {
var cache = new HashMap<T, V>();
var l1 = this.<V>createNewDerived();
Runnable runnable = () -> {
var listSet = new HashSet<>(list);
cache.keySet().removeIf(t -> !listSet.contains(t));
cache.keySet().removeIf(t -> !getList().contains(t));
l1.setContent(list.stream()
.map(v -> {
if (!cache.containsKey(v)) {

View file

@ -1,8 +1,6 @@
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;
@ -10,48 +8,11 @@ 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()
@ -129,46 +90,47 @@ public class DesktopHelper {
return;
}
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
return;
}
if (!Files.exists(file)) {
return;
}
ThreadHelper.runAsync(() -> {
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());
try {
Desktop.getDesktop().open(file.toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().handle();
}
});
}
public static void browseFileInDirectory(Path file) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
browsePathLocal(file.getParent());
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();
}
});
return;
}
ThreadHelper.runAsync(() -> {
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());
try {
Desktop.getDesktop().browseFileDirectory(file.toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().handle();
}
});
}

View file

@ -18,11 +18,6 @@ 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();
@ -44,15 +39,12 @@ public class EncryptionToken {
}
public static EncryptionToken ofUser() {
if (userToken == null) {
var userHandler = DataStorageUserHandler.getInstance();
if (userHandler.getActiveUser() == null) {
throw new IllegalStateException("No active user available");
}
userToken = createUserToken();
var userHandler = DataStorageUserHandler.getInstance();
if (userHandler.getActiveUser() == null) {
throw new IllegalStateException("No active user available");
}
return userToken;
return createUserToken();
}
public static EncryptionToken ofVaultKey() {
@ -67,12 +59,6 @@ public class EncryptionToken {
@JsonIgnore
private Boolean isVault;
@JsonIgnore
private Boolean isUser;
@JsonIgnore
private EncryptionToken usedUserToken;
public boolean canDecrypt() {
return isVault() || isUser();
}
@ -93,13 +79,7 @@ public class EncryptionToken {
return false;
}
if (userToken == EncryptionToken.ofUser() && isUser != null) {
return isUser;
}
usedUserToken = ofUser();
isUser = userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()));
return isUser;
return userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()));
}
public boolean isVault() {

View file

@ -1,9 +1,6 @@
package io.xpipe.app.util;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import java.io.File;
public class Hyperlinks {
@ -16,7 +13,6 @@ 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";
@ -28,7 +24,38 @@ 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) {
DesktopHelper.openUrl(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();
}
}
}

View file

@ -20,7 +20,7 @@ public class LocalExec {
if (process.exitValue() != 0) {
return Optional.empty();
} else {
var s = new String(out, StandardCharsets.UTF_8).trim();
var s = new String(out, StandardCharsets.UTF_8);
TrackEvent.withTrace("Local command finished")
.tag("command", String.join(" ", command))
.tag("stdout", s)

View file

@ -1,6 +1,7 @@
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;
@ -14,7 +15,7 @@ public class LocalShellCache extends ShellControlCache {
super(shellControl);
}
public Optional<Path> getVsCodeCliPath() {
public Optional<Path> getVsCodePath() {
if (!has("codePath")) {
try {
var app =
@ -24,8 +25,8 @@ public class LocalShellCache extends ShellControlCache {
.map(s -> Path.of(s));
}
case OsType.MacOs macOs -> {
yield CommandSupport.findProgram(getShellControl(), "code")
.map(s -> Path.of(s));
yield new ExternalApplicationType.MacApplication(
"app.vscode", "Visual Studio Code") {}.findApp();
}
case OsType.Windows windows -> {
yield ExternalEditorType.VSCODE_WINDOWS.findExecutable();

View file

@ -4,7 +4,6 @@ import io.xpipe.app.core.check.AppSystemFontCheck;
import io.xpipe.app.core.window.ModifiedStage;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import javafx.application.Platform;
@ -15,7 +14,6 @@ import lombok.Setter;
import org.apache.commons.lang3.SystemUtils;
import java.awt.*;
import java.util.OptionalInt;
import java.util.concurrent.CountDownLatch;
public enum PlatformState {
@ -168,40 +166,4 @@ public enum PlatformState {
return;
}
}
public static OptionalInt determineDefaultScalingFactor() {
if (OsType.getLocal() != OsType.LINUX) {
return OptionalInt.empty();
}
var factor = LocalExec.readStdoutIfPossible("gsettings", "get", "org.gnome.desktop.interface", "scaling-factor");
if (factor.isEmpty()) {
return OptionalInt.empty();
}
var readCustom = factor.get().equals("uint32 1") || factor.get().equals("uint32 2");
if (!readCustom) {
return OptionalInt.empty();
}
var textFactor = LocalExec.readStdoutIfPossible("gsettings", "get", "org.gnome.desktop.interface", "text-scaling-factor");
if (textFactor.isEmpty()) {
return OptionalInt.empty();
}
var s = textFactor.get();
if (s.equals("1.0")) {
return OptionalInt.empty();
} else if (s.equals("2.0")) {
return OptionalInt.of(200);
} else if (s.equals("1.25")) {
return OptionalInt.of(125);
} else if (s.equals("1.5")) {
return OptionalInt.of(150);
} else if (s.equals("1.75")) {
return OptionalInt.of(175);
} else {
return OptionalInt.empty();
}
}
}

View file

@ -274,29 +274,6 @@ 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;
@ -304,9 +281,9 @@ public class PlatformThread {
var key = new Object();
Platform.runLater(() -> {
exitNestedEventLoop(key);
Platform.exitNestedEventLoop(key, null);
});
enterNestedEventLoop(key);
Platform.enterNestedEventLoop(key);
}
public static void runLaterIfNeeded(Runnable r) {

View file

@ -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 was not accepted by the server, probably because it is incorrect";
yield "Authentication failed: Provided authentication secret is wrong";
}
case RETRIEVAL_FAILURE -> {
yield "Failed to retrieve secret for authentication";

View file

@ -201,12 +201,19 @@ public class SshLocalBridge {
}
private static String getSshd(ShellControl sc) throws Exception {
var exec = CommandSupport.findProgram(sc, "sshd");
if (exec.isEmpty()) {
throw ErrorEvent.expected(new IllegalStateException(
"No sshd executable found in PATH. The SSH terminal bridge for SSH clients requires a local ssh server to be installed"));
if (OsType.getLocal() == OsType.WINDOWS) {
return XPipeInstallation.getLocalBundledToolsDirectory()
.resolve("openssh")
.resolve("sshd")
.toString();
} else {
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"));
}
return exec.get();
}
return exec.get();
}
public static void reset() {

View file

@ -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 || ref.getStore() == null || !c.isAssignableFrom(ref.getStore().getClass())) {
if (ref == null || !c.isAssignableFrom(ref.getStore().getClass())) {
throw new ValidationException("Value must be an instance of " + c.getSimpleName());
}
}

View file

@ -26,7 +26,7 @@
}
.bookmarks-container {
-fx-background-radius: 4 2 2 4;
-fx-background-radius: 4 0 0 0;
-fx-background-insets: 0 7 4 4, 1 8 5 5;
-fx-padding: 1 0 5 5;
-fx-background-color: -color-border-default, -color-bg-default;
@ -38,7 +38,3 @@
-fx-max-height: 2.8em;
-fx-padding: 6 6 6 4;
}
.bookmarks-header .ikonli-font-icon {
-fx-icon-color: -color-fg-default;
}

View file

@ -2,18 +2,6 @@
-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;
}

View file

@ -62,10 +62,6 @@
-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;

View file

@ -32,7 +32,3 @@
.options-comp .titled-pane > .title {
-fx-padding: 8 20 8 10;
}
.options-comp .titled-pane > .title .text {
-fx-font-size: 0.95em;
}

View file

@ -24,11 +24,15 @@
-fx-text-fill: -color-fg-muted;
}
.root:dark .store-entry-grid:incomplete .name, .root:dark .store-entry-grid:failed .name {
.store-entry-grid:failed .jfx-text-field {
-fx-text-fill: #ee4829;
}
.root:dark .store-entry-grid:incomplete .name {
-fx-text-fill: #aa473c;
}
.root:light .store-entry-grid:incomplete .name, .root:light .store-entry-grid:failed .name {
.root:light .store-entry-grid:incomplete .name {
-fx-text-fill: #88352b;
}
@ -133,7 +137,7 @@
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2, 0.5, 0, 1);
}
.store-entry-section-comp:top {
.store-entry-section-comp:root {
-fx-border-radius: 4px;
-fx-background-radius: 4px;
}

View file

@ -2,10 +2,10 @@
-fx-background-color: transparent;
}
.third-party-dependency-list-comp .titled-pane {
.titled-pane {
-fx-background-radius: 5;
}
.third-party-dependency-list-comp .titled-pane .content {
.titled-pane .content {
-fx-padding: 4;
}

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