SSH config fixes

This commit is contained in:
crschnick 2024-06-07 08:38:07 +00:00
parent bc9b962be9
commit 880b17c7c1
61 changed files with 650 additions and 788 deletions

View file

@ -108,7 +108,6 @@ run {
}
workingDir = rootDir
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
}
task runAttachedDebugger(type: JavaExec) {
@ -122,7 +121,7 @@ task runAttachedDebugger(type: JavaExec) {
"-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost".toString(),
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0"
)
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
jvmArgs += '-XX:+EnableDynamicAgentLoading'
systemProperties run.systemProperties
}

View file

@ -6,16 +6,18 @@ import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.SneakyThrows;
import java.util.List;
@ -68,8 +70,9 @@ public class BrowserOverviewComp extends SimpleComp {
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true).mapped(
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())).getList();
var recent = ListBindingsHelper.mappedContentBinding(
model.getSavedState().getRecentDirectories(),
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview);

View file

@ -8,7 +8,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
@ -47,7 +47,7 @@ public class BrowserTransferComp extends SimpleComp {
var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var binding = new DerivedObservableList<>(syncItems, true).mapped(item -> item.getBrowserEntry()).getList();
var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getBrowserEntry());
var list = new BrowserSelectionListComp(
binding,
entry -> Bindings.createStringBinding(

View file

@ -1,7 +1,5 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
@ -15,9 +13,10 @@ import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.PrettySvgComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -31,6 +30,9 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.util.List;
public class BrowserWelcomeComp extends SimpleComp {
@ -65,7 +67,7 @@ public class BrowserWelcomeComp extends SimpleComp {
return new VBox(hbox);
}
var list = new DerivedObservableList<>(state.getEntries(), true).filtered(e -> {
var list = ListBindingsHelper.filteredContentBinding(state.getEntries(), e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (entry.isEmpty()) {
return false;
@ -76,7 +78,7 @@ public class BrowserWelcomeComp extends SimpleComp {
}
return true;
}).getList();
});
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
var headerBinding = BindingsHelper.flatMap(empty, b -> {

View file

@ -24,10 +24,6 @@ public class BrowserEntry {
}
private static BrowserIconFileType fileType(FileSystem.FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
if (rawFileEntry.getKind() == FileKind.DIRECTORY) {
return null;
}
@ -42,10 +38,6 @@ public class BrowserEntry {
}
private static BrowserIconDirectoryType directoryType(FileSystem.FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
if (rawFileEntry.getKind() != FileKind.DIRECTORY) {
return null;
}

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser.session;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileReference;
@ -10,10 +10,12 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
@ -38,8 +40,7 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFil
return;
}
var l = new DerivedObservableList<>(fileSelection, true);
l.bindContent(newValue.getFileList().getSelection());
ListBindingsHelper.bindContent(fileSelection, newValue.getFileList().getSelection());
});
}

View file

@ -4,9 +4,8 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.control.Button;
@ -39,13 +38,10 @@ public class DropdownComp extends Comp<CompStructure<Button>> {
}))
.createRegion();
List<? extends ObservableValue<Boolean>> l = cm.getItems().stream()
.map(menuItem -> menuItem.getGraphic().visibleProperty())
.toList();
button.visibleProperty()
.bind(Bindings.createBooleanBinding(() -> {
return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue());
}, l.toArray(ObservableValue[]::new)));
.bind(ListBindingsHelper.anyMatch(cm.getItems().stream()
.map(menuItem -> menuItem.getGraphic().visibleProperty())
.toList()));
var graphic = new FontIcon("mdi2c-chevron-double-down");
button.fontProperty().subscribe(c -> {

View file

@ -3,8 +3,9 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
@ -88,8 +89,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
}
if (!listView.getChildren().equals(newShown)) {
var d = new DerivedObservableList<>(listView.getChildren(), true);
d.setContent(newShown);
ListBindingsHelper.setContent(listView.getChildren(), newShown);
}
};

View file

@ -52,6 +52,7 @@ public class StoreToggleComp extends SimpleComp {
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
v -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
section.getWrapper().refreshChildren();
});
}

View file

@ -60,7 +60,7 @@ public class StoreCategoryWrapper {
}
public StoreCategoryWrapper getParent() {
return StoreViewState.get().getCategories().getList().stream()
return StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny()
@ -122,7 +122,7 @@ public class StoreCategoryWrapper {
sortMode.setValue(category.getSortMode());
share.setValue(category.isShare());
containedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream()
containedEntries.setAll(StoreViewState.get().getAllEntries().stream()
.filter(entry -> {
return entry.getEntry().getCategoryUuid().equals(category.getUuid())
|| (AppPrefs.get()
@ -132,7 +132,7 @@ public class StoreCategoryWrapper {
.anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)));
})
.toList());
children.setAll(StoreViewState.get().getCategories().getList().stream()
children.setAll(StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper -> getCategory()
.getUuid()
.equals(storeCategoryWrapper.getCategory().getParentCategory()))

View file

@ -286,6 +286,10 @@ public class StoreCreationComp extends DialogComp {
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
skippable.set(false);
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
skippable.set(false);
} else {
skippable.set(true);
}

View file

@ -372,14 +372,6 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(new SeparatorMenuItem());
}
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
notes.setOnAction(event -> {
wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
event.consume();
});
notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null));
contextMenu.getItems().add(notes);
if (AppPrefs.get().developerMode().getValue()) {
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
browse.setOnAction(
@ -387,6 +379,26 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(browse);
}
if (wrapper.getEntry().getProvider() != null) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem();
m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null || storeCategoryWrapper.equals(wrapper.getCategory().getValue())) {
m.setDisable(true);
}
move.getItems().add(m);
});
contextMenu.getItems().add(move);
}
if (DataStorage.get().isRootEntry(wrapper.getEntry())) {
var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill"));
var none = new MenuItem("None");
@ -406,72 +418,13 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(color);
}
if (wrapper.getEntry().getProvider() != null) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
.getList()
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem();
m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null) {
m.setDisable(true);
}
move.getItems().add(m);
});
contextMenu.getItems().add(move);
}
var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks"));
var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal"));
noOrder.setOnAction(event -> {
DataStorage.get().orderBefore(wrapper.getEntry(), null);
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
notes.setOnAction(event -> {
wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
event.consume();
});
if (wrapper.getEntry().getOrderBefore() == null) {
noOrder.setDisable(true);
}
order.getItems().add(noOrder);
order.getItems().add(new SeparatorMenuItem());
var stick = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending"));
stick.setOnAction(event -> {
DataStorage.get().orderBefore(wrapper.getEntry(), wrapper.getEntry());
event.consume();
});
if (wrapper.getEntry().getUuid().equals(wrapper.getEntry().getOrderBefore())) {
stick.setDisable(true);
}
order.getItems().add(stick);
order.getItems().add(new SeparatorMenuItem());
var desc = new MenuItem(AppI18n.get("orderAheadOf"), new FontIcon("mdi2o-order-bool-descending-variant"));
desc.setDisable(true);
order.getItems().add(desc);
var section = StoreViewState.get().getParentSectionForWrapper(wrapper);
if (section.isPresent()) {
section.get().getAllChildren().getList().forEach(other -> {
var ow = other.getWrapper();
var op = ow.getEntry().getProvider();
MenuItem m = new MenuItem(ow.getName().getValue(),
op != null ? PrettyImageHelper.ofFixedSizeSquare(op.getDisplayIconFileName(ow.getEntry().getStore()),
16).createRegion() : null);
if (other.getWrapper().equals(wrapper) || ow.getEntry().getUuid().equals(wrapper.getEntry().getOrderBefore())) {
m.setDisable(true);
}
m.setOnAction(event -> {
wrapper.orderBefore(ow);
event.consume();
});
order.getItems().add(m);
});
}
contextMenu.getItems().add(order);
contextMenu.getItems().add(new SeparatorMenuItem());
notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null));
contextMenu.getItems().add(notes);
var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.disableProperty()

View file

@ -18,8 +18,8 @@ public class StoreEntryListComp extends SimpleComp {
private Comp<?> createList() {
var content = new ListBoxViewComp<>(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList(),
StoreViewState.get().getCurrentTopLevelSection().getAllChildren().getList(),
StoreViewState.get().getCurrentTopLevelSection().getShownChildren(),
StoreViewState.get().getCurrentTopLevelSection().getAllChildren(),
(StoreSection e) -> {
var custom = StoreSection.customSection(e, true).hgrow();
return new HorizontalComp(List.of(Comp.hspacer(8), custom, Comp.hspacer(10)))
@ -35,7 +35,7 @@ public class StoreEntryListComp extends SimpleComp {
var showIntro = Bindings.createBooleanBinding(
() -> {
var all = StoreViewState.get().getAllConnectionsCategory();
var connections = StoreViewState.get().getAllEntries().getList().stream()
var connections = StoreViewState.get().getAllEntries().stream()
.filter(wrapper -> all.contains(wrapper))
.toList();
return initialCount == connections.size()
@ -45,21 +45,21 @@ public class StoreEntryListComp extends SimpleComp {
.getRoot()
.equals(StoreViewState.get().getAllConnectionsCategory());
},
StoreViewState.get().getAllEntries().getList(),
StoreViewState.get().getAllEntries(),
StoreViewState.get().getActiveCategory());
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put(
createList(),
Bindings.not(Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList())));
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
map.put(new StoreIntroComp(), showIntro);
map.put(
new StoreNotFoundComp(),
Bindings.and(
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries().getList())),
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList())));
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
return new MultiContentComp(map).createRegion();
}
}

View file

@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
@ -55,24 +56,25 @@ public class StoreEntryListStatusComp extends SimpleComp {
label.textProperty().bind(name);
label.getStyleClass().add("name");
var all = StoreViewState.get().getAllEntries().filtered(
var all = ListBindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
storeEntryWrapper -> {
var rootCategory = storeEntryWrapper.getCategory().getValue().getRoot();
var inRootCategory = StoreViewState.get().getActiveCategory().getValue().getRoot().equals(rootCategory);
// Sadly the all binding does not update when the individual visibility of entries changes
// But it is good enough.
var showProvider = storeEntryWrapper.getEntry().getProvider() == null ||
storeEntryWrapper.getEntry().getProvider().shouldShow(storeEntryWrapper);
return inRootCategory && showProvider;
var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
return StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(storeRoot);
},
StoreViewState.get().getActiveCategory());
var shownList = all.filtered(
var shownList = ListBindingsHelper.filteredContentBinding(
all,
storeEntryWrapper -> {
return storeEntryWrapper.shouldShow(
StoreViewState.get().getFilterString().getValue());
},
StoreViewState.get().getFilterString());
var count = new CountComp<>(shownList.getList(), all.getList());
var count = new CountComp<>(shownList, all);
var c = count.createRegion();
var topBar = new HBox(

View file

@ -9,13 +9,17 @@ import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.Observable;
import javafx.beans.property.*;
import lombok.Getter;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Getter
public class StoreEntryWrapper {
@ -61,22 +65,12 @@ public class StoreEntryWrapper {
setupListeners();
}
public List<Observable> getUpdateObservables() {
return List.of(category);
}
public void moveTo(DataStoreCategory category) {
ThreadHelper.runAsync(() -> {
DataStorage.get().updateCategory(entry, category);
});
}
public void orderBefore(StoreEntryWrapper other) {
ThreadHelper.runAsync(() -> {
DataStorage.get().orderBefore(getEntry(),other.getEntry());
});
}
public boolean isInStorage() {
return DataStorage.get().getStoreEntries().contains(entry);
}

View file

@ -26,7 +26,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
}
private ContextMenu createMenu() {
if (section.getShownChildren().getList().isEmpty()) {
if (section.getShownChildren().isEmpty()) {
return null;
}
@ -42,7 +42,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
var w = section.getWrapper();
var graphic =
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
if (c.getList().isEmpty()) {
if (c.isEmpty()) {
var item = ContextMenuHelper.item(
PrettyImageHelper.ofFixedSizeSquare(graphic, 16),
w.getName().getValue());
@ -55,7 +55,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
}
var items = new ArrayList<MenuItem>();
for (StoreSection sub : c.getList()) {
for (StoreSection sub : c) {
if (!sub.getWrapper().getValidity().getValue().isUsable()) {
continue;
}

View file

@ -2,37 +2,37 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Value;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
@Value
public class StoreSection {
StoreEntryWrapper wrapper;
DerivedObservableList<StoreSection> allChildren;
DerivedObservableList<StoreSection> shownChildren;
ObservableList<StoreSection> allChildren;
ObservableList<StoreSection> shownChildren;
int depth;
ObservableBooleanValue showDetails;
public StoreSection(
StoreEntryWrapper wrapper,
DerivedObservableList<StoreSection> allChildren,
DerivedObservableList<StoreSection> shownChildren,
ObservableList<StoreSection> allChildren,
ObservableList<StoreSection> shownChildren,
int depth) {
this.wrapper = wrapper;
this.allChildren = allChildren;
@ -41,10 +41,10 @@ public class StoreSection {
if (wrapper != null) {
this.showDetails = Bindings.createBooleanBinding(
() -> {
return wrapper.getExpanded().get() || allChildren.getList().isEmpty();
return wrapper.getExpanded().get() || allChildren.isEmpty();
},
wrapper.getExpanded(),
allChildren.getList());
allChildren);
} else {
this.showDetails = new SimpleBooleanProperty(true);
}
@ -59,77 +59,51 @@ public class StoreSection {
}
}
private static DerivedObservableList<StoreSection> sorted(
DerivedObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
private static ObservableList<StoreSection> sorted(
ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
if (category == null) {
return list;
}
var explicitOrderComp = Comparator.<StoreSection>comparingInt(new ToIntFunction<>() {
@Override
public int applyAsInt(StoreSection value) {
var explicit = value.getWrapper().getEntry().getOrderBefore();
if (explicit == null) {
return 1;
}
if (explicit.equals(value.getWrapper().getEntry().getUuid())) {
return Integer.MIN_VALUE;
}
return -count(value.getWrapper(), new HashSet<>());
}
private int count(StoreEntryWrapper wrapper, Set<StoreEntryWrapper> seen) {
if (seen.contains(wrapper)) {
// Loop!
return 0;
}
seen.add(wrapper);
var found = list.getList().stream().filter(section -> wrapper.getEntry().getOrderBefore().equals(section.getWrapper().getEntry().getUuid())).findFirst();
if (found.isPresent()) {
return count(found.get().getWrapper(), seen);
} else {
return seen.size();
}
}
});
var usableComp = Comparator.<StoreSection>comparingInt(
var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
var comp = explicitOrderComp.thenComparing(usableComp);
var mappedSortMode = BindingsHelper.flatMap(
category,
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
return list.sorted((o1, o2) -> {
return ListBindingsHelper.orderedContentBinding(
list,
(o1, o2) -> {
var current = mappedSortMode.getValue();
if (current != null) {
return comp.thenComparing(current.comparator())
return c.thenComparing(current.comparator())
.compare(current.representative(o1), current.representative(o2));
} else {
return comp.compare(o1, o2);
return c.compare(o1, o2);
}
},
mappedSortMode,
StoreViewState.get().getEntriesOrderChangeObservable());
mappedSortMode);
}
public static StoreSection createTopLevel(
DerivedObservableList<StoreEntryWrapper> all,
ObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) {
var topLevel = all.filtered(section -> {
var topLevel = ListBindingsHelper.filteredContentBinding(
all,
section -> {
return DataStorage.get().isRootEntry(section.getEntry());
},
category,
StoreViewState.get().getEntriesListChangeObservable());
var cached = topLevel.mapped(
category);
var cached = ListBindingsHelper.cachedMappedContentBinding(
topLevel,
topLevel,
storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category);
var shown = ordered.filtered(
var shown = ListBindingsHelper.filteredContentBinding(
ordered,
section -> {
var showFilter = filterString == null || section.matchesFilter(filterString.get());
var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter);
var sameCategory = category == null
|| category.getValue() == null
@ -144,17 +118,15 @@ public class StoreSection {
private static StoreSection create(
StoreEntryWrapper e,
int depth,
DerivedObservableList<StoreEntryWrapper> all,
ObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) {
if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return new StoreSection(e, new DerivedObservableList<>(
FXCollections.observableArrayList(), true), new DerivedObservableList<>(
FXCollections.observableArrayList(), true), depth);
return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth);
}
var allChildren = all.filtered(other -> {
var allChildren = ListBindingsHelper.filteredContentBinding(all, other -> {
// Legacy implementation that does not use children caches. Use for testing
// if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry())
@ -162,35 +134,29 @@ public class StoreSection {
// .orElse(false);
// This check is fast as the children are cached in the storage
var isChildren = DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
var showProvider = other.getEntry().getProvider() == null ||
other.getEntry().getProvider().shouldShow(other);
return isChildren && showProvider;
}, e.getPersistentState(), e.getCache(), StoreViewState.get().getEntriesListChangeObservable());
var cached = allChildren.mapped(
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
});
var cached = ListBindingsHelper.cachedMappedContentBinding(
allChildren,
allChildren,
entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category);
var filtered = ordered.filtered(
var filtered = ListBindingsHelper.filteredContentBinding(
ordered,
section -> {
var showFilter = filterString == null || section.matchesFilter(filterString.get());
var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter);
// Prevent updates for children on category switching by checking depth
var showCategory = category == null
var sameCategory = category == null
|| category.getValue() == null
|| showInCategory(category.getValue(), section.getWrapper())
|| depth > 0;
|| showInCategory(category.getValue(), section.getWrapper());
// If this entry is already shown as root due to a different category than parent, don't show it
// again here
var notRoot =
!DataStorage.get().isRootEntry(section.getWrapper().getEntry());
var showProvider = section.getWrapper().getEntry().getProvider() == null ||
section.getWrapper().getEntry().getProvider().shouldShow(section.getWrapper());
return showFilter && matchesSelector && showCategory && notRoot && showProvider;
return showFilter && matchesSelector && sameCategory && notRoot;
},
category,
filterString,
e.getPersistentState(),
e.getCache());
filterString);
return new StoreSection(e, cached, filtered, depth);
}
@ -213,13 +179,13 @@ public class StoreSection {
return false;
}
public boolean matchesFilter(String filter) {
public boolean shouldShow(String filter) {
return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter));
}
public boolean anyMatches(Predicate<StoreEntryWrapper> c) {
return c == null
|| c.test(wrapper)
|| allChildren.getList().stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c));
|| allChildren.stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c));
}
}

View file

@ -7,8 +7,10 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.css.PseudoClass;
import javafx.scene.control.Button;
@ -40,9 +42,9 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
private Comp<CompStructure<Button>> createQuickAccessButton() {
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().getList().isEmpty();
return section.getShownChildren().isEmpty();
},
section.getShownChildren().getList());
section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> {
w.executeDefaultAction();
@ -69,11 +71,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
var expandButton = new IconButtonComp(
Bindings.createStringBinding(
() -> section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0
&& section.getShownChildren().size() > 0
? "mdal-keyboard_arrow_down"
: "mdal-keyboard_arrow_right",
section.getWrapper().getExpanded(),
section.getShownChildren().getList()),
section.getShownChildren()),
() -> {
section.getWrapper().toggleExpanded();
});
@ -87,7 +89,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
return "Expand " + section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
.disable(Bindings.size(section.getShownChildren()).isEqualTo(0))
.styleClass("expand-button")
.maxHeight(100)
.vgrow();
@ -126,12 +128,13 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded
var listSections = section.getShownChildren().filtered(
storeSection -> section.getAllChildren().getList().size() <= 20
var listSections = ListBindingsHelper.filteredContentBinding(
section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20
|| section.getWrapper().getExpanded().get(),
section.getWrapper().getExpanded(),
section.getAllChildren().getList());
var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> {
section.getAllChildren());
var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> {
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
})
.minHeight(0)
@ -140,10 +143,10 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
var expanded = Bindings.createBooleanBinding(
() -> {
return section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0;
&& section.getShownChildren().size() > 0;
},
section.getWrapper().getExpanded(),
section.getShownChildren().getList());
section.getShownChildren());
var full = new VerticalComp(List.of(
topEntryList,
Comp.separator().hide(expanded.not()),
@ -152,7 +155,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setFillHeight(true))
.hide(Bindings.or(
Bindings.not(section.getWrapper().getExpanded()),
Bindings.size(section.getShownChildren().getList()).isEqualTo(0)))));
Bindings.size(section.getShownChildren()).isEqualTo(0)))));
return full.styleClass("store-entry-section-comp")
.apply(struc -> {
struc.get().setFillWidth(true);

View file

@ -8,7 +8,9 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStoreColor;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -82,7 +84,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
expanded =
new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0);
&& section.getShownChildren().size() > 0);
var button = new IconButtonComp(
Bindings.createStringBinding(
() -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right",
@ -99,15 +101,15 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
+ section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
.disable(Bindings.size(section.getShownChildren()).isEqualTo(0))
.grow(false, true)
.styleClass("expand-button");
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().getList().isEmpty();
return section.getShownChildren().isEmpty();
},
section.getShownChildren().getList());
section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = action;
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
.vgrow()
@ -129,12 +131,13 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded
var listSections = section.getWrapper() != null
? section.getShownChildren().filtered(
storeSection -> section.getAllChildren().getList().size() <= 20 || expanded.get(),
? ListBindingsHelper.filteredContentBinding(
section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
expanded,
section.getAllChildren().getList())
section.getAllChildren())
: section.getShownChildren();
var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> {
var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> {
return new StoreSectionMiniComp(e, this.augment, this.action, this.condensedStyle);
})
.minHeight(0)
@ -145,7 +148,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setFillHeight(true))
.hide(Bindings.or(
Bindings.not(expanded),
Bindings.size(section.getAllChildren().getList()).isEqualTo(0))));
Bindings.size(section.getAllChildren()).isEqualTo(0))));
var vert = new VerticalComp(list);
if (condensedStyle) {

View file

@ -48,7 +48,7 @@ public interface StoreSortMode {
@Override
public StoreSection representative(StoreSection s) {
return Stream.concat(
s.getShownChildren().getList().stream()
s.getShownChildren().stream()
.filter(section -> section.getWrapper()
.getEntry()
.getValidity()
@ -76,7 +76,7 @@ public interface StoreSortMode {
@Override
public StoreSection representative(StoreSection s) {
return Stream.concat(
s.getShownChildren().getList().stream()
s.getShownChildren().stream()
.filter(section -> section.getWrapper()
.getEntry()
.getValidity()

View file

@ -1,16 +1,22 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.StorageListener;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.util.*;
@ -23,18 +29,12 @@ public class StoreViewState {
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final DerivedObservableList<StoreEntryWrapper> allEntries =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
private final ObservableList<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final DerivedObservableList<StoreCategoryWrapper> categories =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
@Getter
private final IntegerProperty entriesOrderChangeObservable = new SimpleIntegerProperty();
@Getter
private final IntegerProperty entriesListChangeObservable = new SimpleIntegerProperty();
private final ObservableList<StoreCategoryWrapper> categories =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
@ -76,8 +76,8 @@ public class StoreViewState {
}
private void updateContent() {
categories.getList().forEach(c -> c.update());
allEntries.getList().forEach(e -> e.update());
categories.forEach(c -> c.update());
allEntries.forEach(e -> e.update());
}
private void initSections() {
@ -86,19 +86,16 @@ public class StoreViewState {
StoreSection.createTopLevel(allEntries, storeEntryWrapper -> true, filter, activeCategory);
} catch (Exception exception) {
currentTopLevelSection =
new StoreSection(null,
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
0);
new StoreSection(null, FXCollections.emptyObservableList(), FXCollections.emptyObservableList(), 0);
ErrorEvent.fromThrowable(exception).handle();
}
}
private void initContent() {
allEntries.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()
allEntries.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()
.map(StoreEntryWrapper::new)
.toList()));
categories.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()
categories.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()
.map(StoreCategoryWrapper::new)
.toList()));
@ -106,11 +103,11 @@ public class StoreViewState {
DataStorage.get().setSelectedCategory(newValue.getCategory());
});
var selected = AppCache.get("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);
activeCategory.setValue(categories.getList().stream()
activeCategory.setValue(categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(selected))
.findFirst()
.orElse(categories.getList().stream()
.orElse(categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID))
.findFirst()
@ -122,9 +119,9 @@ public class StoreViewState {
AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
synchronized (this) {
var l = new ArrayList<>(allEntries.getList());
allEntries.getList().clear();
allEntries.getList().setAll(l);
var l = new ArrayList<>(allEntries);
allEntries.clear();
allEntries.setAll(l);
}
});
});
@ -132,21 +129,6 @@ public class StoreViewState {
// Watch out for synchronizing all calls to the entries and categories list!
DataStorage.get().addListener(new StorageListener() {
@Override
public void onStoreOrderUpdate() {
Platform.runLater(() -> {
entriesOrderChangeObservable.set(entriesOrderChangeObservable.get() + 1);
});
}
@Override
public void onStoreListUpdate() {
Platform.runLater(() -> {
entriesListChangeObservable.set(entriesListChangeObservable.get() + 1);
});
}
@Override
public void onStoreAdd(DataStoreEntry... entry) {
var l = Arrays.stream(entry)
@ -160,11 +142,11 @@ public class StoreViewState {
}
synchronized (this) {
allEntries.getList().addAll(l);
allEntries.addAll(l);
}
synchronized (this) {
categories.getList().stream()
.filter(storeCategoryWrapper -> allEntries.getList().stream()
categories.stream()
.filter(storeCategoryWrapper -> allEntries.stream()
.anyMatch(storeEntryWrapper -> storeEntryWrapper
.getEntry()
.getCategoryUuid()
@ -181,14 +163,14 @@ public class StoreViewState {
var a = Arrays.stream(entry).collect(Collectors.toSet());
List<StoreEntryWrapper> l;
synchronized (this) {
l = allEntries.getList().stream()
l = allEntries.stream()
.filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry()))
.toList();
}
List<StoreCategoryWrapper> cats;
synchronized (this) {
cats = categories.getList().stream()
.filter(storeCategoryWrapper -> allEntries.getList().stream()
cats = categories.stream()
.filter(storeCategoryWrapper -> allEntries.stream()
.anyMatch(storeEntryWrapper -> storeEntryWrapper
.getEntry()
.getCategoryUuid()
@ -204,7 +186,7 @@ public class StoreViewState {
}
synchronized (this) {
allEntries.getList().removeAll(l);
allEntries.removeAll(l);
}
cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
});
@ -221,7 +203,7 @@ public class StoreViewState {
}
synchronized (this) {
categories.getList().add(l);
categories.add(l);
}
l.update();
});
@ -231,7 +213,7 @@ public class StoreViewState {
public void onCategoryRemove(DataStoreCategory category) {
Optional<StoreCategoryWrapper> found;
synchronized (this) {
found = categories.getList().stream()
found = categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().equals(category))
.findFirst();
@ -247,7 +229,7 @@ public class StoreViewState {
}
synchronized (this) {
categories.getList().remove(found.get());
categories.remove(found.get());
}
var p = found.get().getParent();
if (p != null) {
@ -258,34 +240,15 @@ public class StoreViewState {
});
}
public Optional<StoreSection> getParentSectionForWrapper(StoreEntryWrapper wrapper) {
StoreSection current = getCurrentTopLevelSection();
while (true) {
var child = current.getAllChildren().getList().stream().filter(section -> section.getWrapper().equals(wrapper)).findFirst();
if (child.isPresent()) {
return Optional.of(current);
}
var traverse = current.getAllChildren().getList().stream().filter(section -> section.anyMatches(w -> w.equals(wrapper))).findFirst();
if (traverse.isPresent()) {
current = traverse.get();
} else {
return Optional.empty();
}
}
}
public DerivedObservableList<StoreCategoryWrapper> getSortedCategories(StoreCategoryWrapper root) {
public ObservableList<StoreCategoryWrapper> getSortedCategories(StoreCategoryWrapper root) {
Comparator<StoreCategoryWrapper> comparator = new Comparator<>() {
@Override
public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) {
var o1Root = o1.getRoot();
var o2Root = o2.getRoot();
if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
return -1;
}
if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
return 1;
}
@ -302,6 +265,22 @@ public class StoreViewState {
return 1;
}
if (o1.getDepth() > o2.getDepth()) {
if (o1.getParent() == o2) {
return 1;
}
return compare(o1.getParent(), o2);
}
if (o1.getDepth() < o2.getDepth()) {
if (o2.getParent() == o1) {
return -1;
}
return compare(o1, o2.getParent());
}
var parent = compare(o1.getParent(), o2.getParent());
if (parent != 0) {
return parent;
@ -312,11 +291,13 @@ public class StoreViewState {
.compareToIgnoreCase(o2.nameProperty().getValue());
}
};
return categories.filtered(cat -> root == null || cat.getRoot().equals(root)).sorted(comparator);
return ListBindingsHelper.filteredContentBinding(
categories, cat -> root == null || cat.getRoot().equals(root))
.sorted(comparator);
}
public StoreCategoryWrapper getAllConnectionsCategory() {
return categories.getList().stream()
return categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID))
.findFirst()
@ -324,7 +305,7 @@ public class StoreViewState {
}
public StoreCategoryWrapper getAllScriptsCategory() {
return categories.getList().stream()
return categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID))
.findFirst()
@ -332,14 +313,14 @@ public class StoreViewState {
}
public StoreEntryWrapper getEntryWrapper(DataStoreEntry entry) {
return allEntries.getList().stream()
return allEntries.stream()
.filter(storeCategoryWrapper -> storeCategoryWrapper.getEntry().equals(entry))
.findFirst()
.orElseThrow();
}
public StoreCategoryWrapper getCategoryWrapper(DataStoreCategory entry) {
return categories.getList().stream()
return categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().equals(entry))
.findFirst()

View file

@ -28,10 +28,6 @@ import java.util.List;
public interface DataStoreProvider {
default boolean shouldShow(StoreEntryWrapper w) {
return true;
}
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
return new SimpleBooleanProperty(false);
}

View file

@ -4,14 +4,17 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.Translatable;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.scene.control.ComboBox;
import javafx.util.StringConverter;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@ -76,7 +79,7 @@ public class ChoiceComp<T> extends Comp<CompStructure<ComboBox<T>>> {
list.add(null);
}
cb.getItems().setAll(list);
ListBindingsHelper.setContent(cb.getItems(), list);
});
cb.valueProperty().addListener((observable, oldValue, newValue) -> {

View file

@ -11,10 +11,11 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.ContextMenuHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
@ -25,6 +26,7 @@ import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
@ -77,12 +79,13 @@ public class StoreCategoryComp extends SimpleComp {
showing.bind(cm.showingProperty());
return cm;
}));
var shownList = new DerivedObservableList<>(category.getContainedEntries(), true).filtered(
var shownList = ListBindingsHelper.filteredContentBinding(
category.getContainedEntries(),
storeEntryWrapper -> {
return storeEntryWrapper.shouldShow(
StoreViewState.get().getFilterString().getValue());
},
StoreViewState.get().getFilterString()).getList();
StoreViewState.get().getFilterString());
var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")");
var hover = new SimpleBooleanProperty();
var focus = new SimpleBooleanProperty();

View file

@ -1,228 +0,0 @@
package io.xpipe.app.fxcomps.util;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
@Getter
public class DerivedObservableList<T> {
private final ObservableList<T> list;
private final boolean unique;
public DerivedObservableList(ObservableList<T> list, boolean unique) {
this.list = list;
this.unique = unique;
}
private <V> DerivedObservableList<V> createNewDerived() {
var l = FXCollections.<V>observableArrayList();
BindingsHelper.preserve(l, list);
return new DerivedObservableList<>(l, unique);
}
public void setContent(List<? extends T> newList) {
if (list.equals(newList)) {
return;
}
if (list.size() == 0) {
list.addAll(newList);
return;
}
if (newList.size() == 0) {
list.clear();
return;
}
if (unique) {
setContentUnique(newList);
} else {
setContentNonUnique(newList);
}
}
public void setContentNonUnique(List<? extends T> newList) {
var target = list;
var targetSet = new HashSet<>(target);
var newSet = new HashSet<>(newList);
// Only add missing element
if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) {
var l = new HashSet<>(newSet);
l.removeAll(targetSet);
if (l.size() > 0) {
var found = l.iterator().next();
var index = newList.indexOf(found);
target.add(index, found);
return;
}
}
// Only remove not needed element
if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {
var l = new HashSet<>(targetSet);
l.removeAll(newSet);
if (l.size() > 0) {
target.remove(l.iterator().next());
return;
}
}
// Other cases are more difficult
target.setAll(newList);
}
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);
l.removeIf(t -> !listSet.contains(t));
// Reordering occurred
if (!l.equals(list)) {
list.setAll(newList);
return;
}
var start = 0;
for (int end = 0; end <= list.size(); end++) {
var index = end < list.size() ? newList.indexOf(list.get(end)) : newList.size();
for (; start < index; start++) {
list.add(start, newList.get(start));
}
start = index + 1;
}
return;
}
// Removal
if (listSet.containsAll(newList)) {
var l = new ArrayList<>(list);
l.removeIf(t -> !newSet.contains(t));
// Reordering occurred
if (!l.equals(newList)) {
list.setAll(newList);
return;
}
var toRemove = new ArrayList<>(list);
toRemove.removeIf(t -> newSet.contains(t));
list.removeAll(toRemove);
return;
}
// Other cases are more difficult
list.setAll(newList);
}
public <V> DerivedObservableList<V> mapped(Function<T, V> map) {
var l1 = this.<V>createNewDerived();
Runnable runnable = () -> {
l1.setContent(list.stream().map(map).toList());
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
return l1;
}
public void bindContent(ObservableList<T> other) {
setContent(other);
other.addListener((ListChangeListener<? super T>) c -> {
setContent(other);
});
}
public DerivedObservableList<T> filtered(Predicate<T> predicate) {
return filtered(new SimpleObjectProperty<>(predicate));
}
public DerivedObservableList<T> filtered(Predicate<T> predicate, Observable... observables) {
return filtered(
Bindings.createObjectBinding(
() -> {
return new Predicate<>() {
@Override
public boolean test(T v) {
return predicate.test(v);
}
};
},
Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new)));
}
public DerivedObservableList<T> filtered(ObservableValue<Predicate<T>> predicate) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(
predicate.getValue() != null
? list.stream().filter(predicate.getValue()).toList()
: list);
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
predicate.addListener(observable -> {
runnable.run();
});
return d;
}
public DerivedObservableList<T> sorted(Comparator<T> comp, Observable... observables) {
return sorted(Bindings.createObjectBinding(
() -> {
return new Comparator<>() {
@Override
public int compare(T o1, T o2) {
return comp.compare(o1, o2);
}
};
},
observables));
}
public DerivedObservableList<T> sorted(ObservableValue<Comparator<T>> comp) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(list.stream().sorted(comp.getValue()).toList());
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
comp.addListener(observable -> {
d.list.sort(comp.getValue());
});
return d;
}
public DerivedObservableList<T> blockUpdatesIf(ObservableBooleanValue block) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(list);
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
block.addListener(observable -> {
runnable.run();
});
return d;
}
}

View file

@ -0,0 +1,190 @@
package io.xpipe.app.fxcomps.util;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
public class ListBindingsHelper {
public static <T> void bindContent(ObservableList<T> l1, ObservableList<? extends T> l2) {
setContent(l1, l2);
l2.addListener((ListChangeListener<? super T>) c -> {
setContent(l1, l2);
});
}
public static <T, U> ObservableValue<Boolean> anyMatch(List<? extends ObservableValue<Boolean>> l) {
return Bindings.createBooleanBinding(
() -> {
return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue());
},
l.toArray(ObservableValue[]::new));
}
public static <T, V> ObservableList<T> mappedContentBinding(ObservableList<V> l2, Function<V, T> map) {
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().map(map).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <T, V> ObservableList<T> cachedMappedContentBinding(
ObservableList<V> all, ObservableList<V> shown, Function<V, T> map) {
var cache = new HashMap<V, T>();
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
cache.keySet().removeIf(t -> !all.contains(t));
setContent(
l1,
shown.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, map.apply(v));
}
return cache.get(v);
})
.toList());
};
runnable.run();
shown.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
BindingsHelper.preserve(l1, all);
BindingsHelper.preserve(l1, shown);
return l1;
}
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, Comparator<V> comp, Observable... observables) {
return orderedContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Comparator<>() {
@Override
public int compare(V o1, V o2) {
return comp.compare(o1, o2);
}
};
},
observables));
}
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, ObservableValue<Comparator<V>> comp) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().sorted(comp.getValue()).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
comp.addListener((observable, oldValue, newValue) -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <V> ObservableList<V> filteredContentBinding(ObservableList<V> l2, Predicate<V> predicate) {
return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, Predicate<V> predicate, Observable... observables) {
return filteredContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Predicate<>() {
@Override
public boolean test(V v) {
return predicate.test(v);
}
};
},
Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new)));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, ObservableValue<Predicate<V>> predicate) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(
l1,
predicate.getValue() != null
? l2.stream().filter(predicate.getValue()).toList()
: l2);
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
predicate.addListener((c, o, n) -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <T> void setContent(ObservableList<T> target, List<? extends T> newList) {
if (target.equals(newList)) {
return;
}
if (target.size() == 0) {
target.setAll(newList);
return;
}
if (newList.size() == 0) {
target.clear();
return;
}
var targetSet = new HashSet<>(target);
var newSet = new HashSet<>(newList);
// Only add missing element
if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) {
var l = new HashSet<>(newSet);
l.removeAll(targetSet);
if (l.size() > 0) {
var found = l.iterator().next();
var index = newList.indexOf(found);
target.add(index, found);
return;
}
}
// Only remove not needed element
if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {
var l = new HashSet<>(targetSet);
l.removeAll(newSet);
if (l.size() > 0) {
target.remove(l.iterator().next());
return;
}
}
// Other cases are more difficult
target.setAll(newList);
}
}

View file

@ -45,16 +45,17 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
@Override
public boolean isAvailable() {
try (ShellControl pc = LocalShell.getShell().start()) {
return pc.command(String.format(
var out = pc.command(String.format(
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications",
applicationName))
.executeAndCheck();
.readStdoutIfPossible();
return out.isPresent() && !out.get().isBlank();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
}
@Override
public boolean isSelectable() {
return OsType.getLocal().equals(OsType.MACOS);

View file

@ -194,10 +194,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
@Override
public void launch(Path file) throws Exception {
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.add("open", "-a")
.addQuoted(applicationName)
.addFile(file.toString()));
try (var sc = LocalShell.getShell().start()) {
sc.executeSimpleCommand(CommandBuilder.of()
.add("open", "-a").addQuoted(applicationName).addFile(file.toString()));
}
}
}

View file

@ -328,16 +328,16 @@ public abstract class DataStorage {
return;
}
entry.setCategoryUuid(newCategory.getUuid());
var children = getDeepStoreChildren(entry);
children.forEach(child -> child.setCategoryUuid(newCategory.getUuid()));
listeners.forEach(storageListener -> storageListener.onStoreListUpdate());
saveAsync();
}
var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove));
public void orderBefore(DataStoreEntry entry, DataStoreEntry reference) {
entry.setOrderBefore(reference != null ? reference.getUuid() : null);
listeners.forEach(storageListener -> storageListener.onStoreOrderUpdate());
entry.setCategoryUuid(newCategory.getUuid());
children.forEach(child -> child.setCategoryUuid(newCategory.getUuid()));
var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
saveAsync();
}
public boolean refreshChildren(DataStoreEntry e) {
@ -439,8 +439,8 @@ public abstract class DataStorage {
pair.getKey().setStoreInternal(merged, false);
}
var s = pair.getKey().getStorePersistentState();
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());
var mergedState = pair.getKey().getStorePersistentState().deepCopy();
mergedState.merge(pair.getValue().get().getStorePersistentState());
pair.getKey().setStorePersistentState(mergedState);
}
}
@ -788,7 +788,9 @@ public abstract class DataStorage {
public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) {
return storeEntriesSet.stream()
.filter(n -> n.getStore() == store || (!identityOnly && (n.getStore() != null
.filter(n -> n.getStore() == store
|| (!identityOnly
&& (n.getStore() != null
&& Objects.equals(
store.getClass(), n.getStore().getClass())
&& store.equals(n.getStore()))))

View file

@ -72,9 +72,6 @@ public class DataStoreEntry extends StorageElement {
@NonFinal
String notes;
@NonFinal
UUID orderBefore;
private DataStoreEntry(
Path directory,
UUID uuid,
@ -89,8 +86,7 @@ public class DataStoreEntry extends StorageElement {
JsonNode storePersistentState,
boolean expanded,
DataStoreColor color,
String notes, UUID orderBefore
) {
String notes) {
super(directory, uuid, name, lastUsed, lastModified, dirty);
this.categoryUuid = categoryUuid;
this.store = DataStorageParser.storeFromNode(storeNode);
@ -99,7 +95,6 @@ public class DataStoreEntry extends StorageElement {
this.configuration = configuration;
this.expanded = expanded;
this.color = color;
this.orderBefore = orderBefore;
this.provider = store != null
? DataStoreProviders.byStoreClass(store.getClass()).orElse(null)
: null;
@ -114,12 +109,10 @@ public class DataStoreEntry extends StorageElement {
String name,
Instant lastUsed,
Instant lastModified,
DataStore store, UUID orderBefore
) {
DataStore store) {
super(directory, uuid, name, lastUsed, lastModified, false);
this.categoryUuid = categoryUuid;
this.store = store;
this.orderBefore = orderBefore;
this.storeNode = null;
this.validity = Validity.INCOMPLETE;
this.configuration = Configuration.defaultConfiguration();
@ -137,8 +130,7 @@ public class DataStoreEntry extends StorageElement {
UUID.randomUUID().toString(),
Instant.now(),
Instant.now(),
store,
null);
store);
}
public static DataStoreEntry createNew(@NonNull String name, @NonNull DataStore store) {
@ -167,7 +159,6 @@ public class DataStoreEntry extends StorageElement {
null,
false,
null,
null,
null);
return entry;
}
@ -185,8 +176,7 @@ public class DataStoreEntry extends StorageElement {
JsonNode storePersistentState,
boolean expanded,
DataStoreColor color,
String notes,
UUID orderBeforeEntry) {
String notes) {
return new DataStoreEntry(
directory,
uuid,
@ -201,8 +191,7 @@ public class DataStoreEntry extends StorageElement {
storePersistentState,
expanded,
color,
notes,
orderBeforeEntry);
notes);
}
public static Optional<DataStoreEntry> fromDirectory(Path dir) throws Exception {
@ -237,15 +226,6 @@ public class DataStoreEntry extends StorageElement {
.map(jsonNode -> jsonNode.textValue())
.map(Instant::parse)
.orElse(Instant.EPOCH);
var order = Optional.ofNullable(stateJson.get("orderBefore"))
.map(node -> {
try {
return mapper.treeToValue(node, UUID.class);
} catch (JsonProcessingException e) {
return null;
}
})
.orElse(null);
var configuration = Optional.ofNullable(json.get("configuration"))
.map(node -> {
try {
@ -295,19 +275,10 @@ public class DataStoreEntry extends StorageElement {
persistentState,
expanded,
color,
notes,
order
notes
));
}
public void setOrderBefore(UUID uuid) {
var changed = !Objects.equals(orderBefore, uuid);
this.orderBefore = uuid;
if (changed) {
notifyUpdate(false, true);
}
}
@Override
public int hashCode() {
return getUuid().hashCode();
@ -359,7 +330,7 @@ public class DataStoreEntry extends StorageElement {
storePersistentStateNode = JacksonMapper.getDefault().valueToTree(storePersistentState);
}
}
return (T) storePersistentState;
return (T) sds.getStateClass().cast(storePersistentState);
}
public void setStorePersistentState(DataStoreState value) {
@ -408,7 +379,6 @@ public class DataStoreEntry extends StorageElement {
stateObj.set("persistentState", storePersistentStateNode);
obj.set("configuration", mapper.valueToTree(configuration));
stateObj.put("expanded", expanded);
stateObj.put("orderBefore", orderBefore != null ? orderBefore.toString() : null);
var entryString = mapper.writeValueAsString(obj);
var stateString = mapper.writeValueAsString(stateObj);

View file

@ -2,10 +2,6 @@ package io.xpipe.app.storage;
public interface StorageListener {
void onStoreOrderUpdate();
void onStoreListUpdate();
void onStoreAdd(DataStoreEntry... entry);
void onStoreRemove(DataStoreEntry... entry);

View file

@ -514,17 +514,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
try (ShellControl pc = LocalShell.getShell()) {
var suffix = "\"" + configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"") + "\"";
pc.osascriptCommand(String.format(
"""
activate application "Terminal"
delay 1
tell app "Terminal" to do script %s
""",
suffix))
.execute();
}
LocalShell.getShell()
.executeSimpleCommand(CommandBuilder.of()
.add("open", "-a")
.addQuoted("Terminal.app")
.addFile(configuration.getScriptFile()));
}
};
ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") {
@ -550,26 +544,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
try (ShellControl pc = LocalShell.getShell()) {
pc.osascriptCommand(String.format(
"""
if application "iTerm" is not running then
launch application "iTerm"
delay 1
tell application "iTerm"
tell current tab of current window
close
end tell
end tell
end if
tell application "iTerm"
activate
create window with default profile command "%s"
end tell
""",
configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"")))
.execute();
}
LocalShell.getShell()
.executeSimpleCommand(CommandBuilder.of()
.add("open", "-a")
.addQuoted("iTerm.app")
.addFile(configuration.getScriptFile()));
}
};
ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") {

View file

@ -1,9 +1,14 @@
package io.xpipe.app.terminal;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.ExternalApplicationHelper;
import io.xpipe.app.prefs.ExternalApplicationType;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import java.nio.file.Path;
import java.util.Optional;
@ -26,7 +31,7 @@ public interface WezTerminalType extends ExternalTerminalType {
@Override
default boolean isRecommended() {
return false;
return OsType.getLocal() != OsType.WINDOWS;
}
@Override
@ -51,25 +56,62 @@ public interface WezTerminalType extends ExternalTerminalType {
@Override
protected Optional<Path> determineInstallation() {
Optional<String> launcherDir;
launcherDir = WindowsRegistry.local().readValue(
WindowsRegistry.HKEY_LOCAL_MACHINE,
"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1",
"InstallLocation")
.map(p -> p + "\\wezterm-gui.exe");
return launcherDir.map(Path::of);
try {
var foundKey = WindowsRegistry.local().findKeyForEqualValueMatchRecursive(WindowsRegistry.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "http://wezfurlong.org/wezterm");
if (foundKey.isPresent()) {
var installKey = WindowsRegistry.local().readValue(
foundKey.get().getHkey(),
foundKey.get().getKey(),
"InstallLocation");
if (installKey.isPresent()) {
return installKey.map(p -> p + "\\wezterm-gui.exe").map(Path::of);
}
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
}
try (ShellControl pc = LocalShell.getShell()) {
if (pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui"))) {
return Optional.of(Path.of("wezterm-gui"));
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
return Optional.empty();
}
}
class Linux extends SimplePathType implements WezTerminalType {
class Linux extends ExternalApplicationType implements WezTerminalType {
public Linux() {
super("app.wezterm", "wezterm-gui", true);
super("app.wezterm");
}
public boolean isAvailable() {
try (ShellControl pc = LocalShell.getShell()) {
return pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm")) &&
pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui"));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
return false;
}
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
return CommandBuilder.of().add("start").addFile(configuration.getScriptFile());
public void launch(LaunchConfiguration configuration) throws Exception {
var spawn = LocalShell.getShell().command(CommandBuilder.of().addFile("wezterm")
.add("cli", "spawn")
.addFile(configuration.getScriptFile()))
.executeAndCheck();
if (!spawn) {
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.addFile("wezterm-gui")
.add("start")
.addFile(configuration.getScriptFile()));
}
}
}
@ -81,20 +123,27 @@ public interface WezTerminalType extends ExternalTerminalType {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var path = LocalShell.getShell()
.command(String.format(
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
applicationName))
.readStdoutOrThrow();
var c = CommandBuilder.of()
.addFile(Path.of(path)
.resolve("Contents")
.resolve("MacOS")
.resolve("wezterm-gui")
.toString())
.add("start")
.add(configuration.getDialectLaunchCommand());
ExternalApplicationHelper.startAsync(c);
try (var sc = LocalShell.getShell()) {
var path = sc.command(
String.format("mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
applicationName)).readStdoutOrThrow();
var spawn = sc.command(CommandBuilder.of().addFile(Path.of(path)
.resolve("Contents")
.resolve("MacOS")
.resolve("wezterm").toString())
.add("cli", "spawn", "--pane-id", "0")
.addFile(configuration.getScriptFile()))
.executeAndCheck();
if (!spawn) {
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.addFile(Path.of(path)
.resolve("Contents")
.resolve("MacOS")
.resolve("wezterm-gui").toString())
.add("start")
.addFile(configuration.getScriptFile()));
}
}
}
}
}

View file

@ -40,7 +40,7 @@ public class DataStoreCategoryChoiceComp extends SimpleComp {
value.setValue(newValue);
}
});
var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root).getList());
var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root));
box.setValue(value.getValue());
box.valueProperty().addListener((observable, oldValue, newValue) -> {
value.setValue(newValue);

View file

@ -28,10 +28,9 @@ public class FileOpener {
try {
editor.launch(Path.of(localFile).toRealPath());
} catch (Exception e) {
ErrorEvent.fromThrowable(e)
.description("Unable to launch editor "
ErrorEvent.fromThrowable("Unable to launch editor "
+ editor.toTranslatedString().getValue()
+ ".\nMaybe try to use a different editor in the settings.")
+ ".\nMaybe try to use a different editor in the settings.", e)
.expected()
.handle();
}
@ -52,8 +51,7 @@ public class FileOpener {
}
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e)
.description("Unable to open file " + localFile)
ErrorEvent.fromThrowable("Unable to open file " + localFile, e)
.handle();
}
}
@ -68,8 +66,7 @@ public class FileOpener {
pc.executeSimpleCommand("open \"" + localFile + "\"");
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e)
.description("Unable to open file " + localFile)
ErrorEvent.fromThrowable("Unable to open file " + localFile, e)
.handle();
}
}

View file

@ -88,7 +88,6 @@ project.ext {
arch = getArchName()
privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : []
isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE'))
isPreRelease = System.getenv('PRERELEASE') != null && Boolean.parseBoolean(System.getenv('PRERELEASE'))
isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE'))
rawVersion = file('version').text.trim()
versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT')
@ -106,7 +105,7 @@ project.ext {
website = 'https://xpipe.io'
sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe'
authors = 'Christopher Schnick'
javafxVersion = '22.0.1'
javafxVersion = '23-ea+18'
platformName = getPlatformName()
languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"]
jvmRunArgs = [
@ -159,6 +158,11 @@ if (isFullRelease && rawVersion.contains("-")) {
throw new IllegalArgumentException("Releases must have canonical versions")
}
if (isStage && !rawVersion.contains("-")) {
throw new IllegalArgumentException("Stage releases must have release numbers")
}
def replaceVariablesInFileAsString(String f, Map<String, String> replacements) {
def fileName = file(f).getName()
def text = file(f).text

View file

@ -58,13 +58,12 @@ public interface ShellControl extends ProcessControl {
default <T extends ShellStoreState> ShellControl withShellStateInit(StatefulDataStore<T> store) {
return onInit(shellControl -> {
var s = store.getState().toBuilder()
.osType(shellControl.getOsType())
.shellDialect(shellControl.getOriginalShellDialect())
.running(true)
.osName(shellControl.getOsName())
.build();
store.setState(s.asNeeded());
var s = store.getState();
s.setOsType(shellControl.getOsType());
s.setShellDialect(shellControl.getOriginalShellDialect());
s.setRunning(true);
s.setOsName(shellControl.getOsName());
store.setState(s);
});
}
@ -75,8 +74,9 @@ public interface ShellControl extends ProcessControl {
return;
}
var s = store.getState().toBuilder().running(false).build();
store.setState(s.asNeeded());
var s = store.getState();
s.setRunning(false);
store.setState(s);
});
}

View file

@ -1,24 +0,0 @@
package io.xpipe.core.process;
import io.xpipe.core.store.DataStoreState;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@Value
@EqualsAndHashCode(callSuper=true)
@SuperBuilder(toBuilder = true)
@Jacksonized
public class ShellNameStoreState extends ShellStoreState {
String shellName;
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
var n = (ShellNameStoreState) newer;
var b = toBuilder();
mergeBuilder(n,b);
return b.shellName(useNewer(shellName, n.shellName)).build();
}
}

View file

@ -1,18 +1,19 @@
package io.xpipe.core.process;
import io.xpipe.core.store.DataStoreState;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@FieldDefaults(level = AccessLevel.PRIVATE)
@Setter
@Getter
@EqualsAndHashCode(callSuper=true)
@SuperBuilder(toBuilder = true)
@Jacksonized
@SuperBuilder
public class ShellStoreState extends DataStoreState implements OsNameState {
OsType.Any osType;
@ -25,17 +26,11 @@ public class ShellStoreState extends DataStoreState implements OsNameState {
}
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
public void merge(DataStoreState newer) {
var shellStoreState = (ShellStoreState) newer;
var b = toBuilder();
mergeBuilder(shellStoreState, b);
return b.build();
}
protected void mergeBuilder(ShellStoreState shellStoreState, ShellStoreStateBuilder<?,?> b) {
b.osType(useNewer(osType, shellStoreState.getOsType()))
.osName(useNewer(osName, shellStoreState.getOsName()))
.shellDialect(useNewer(shellDialect, shellStoreState.getShellDialect()))
.running(useNewer(running, shellStoreState.getRunning()));
osType = useNewer(osType, shellStoreState.getOsType());
osName = useNewer(osName, shellStoreState.getOsName());
shellDialect = useNewer(shellDialect, shellStoreState.getShellDialect());
running = useNewer(running, shellStoreState.getRunning());
}
}

View file

@ -1,22 +1,49 @@
package io.xpipe.core.store;
import io.xpipe.core.util.JacksonMapper;
import lombok.SneakyThrows;
import lombok.experimental.SuperBuilder;
@SuperBuilder(toBuilder = true)
@SuperBuilder
public abstract class DataStoreState {
public DataStoreState() {}
@SuppressWarnings("unchecked")
public <DS extends DataStoreState> DS asNeeded() {
return (DS) this;
}
protected static <T> T useNewer(T older, T newer) {
return newer != null ? newer : older;
}
public DataStoreState mergeCopy(DataStoreState newer) {
return this;
public abstract void merge(DataStoreState newer);
@SneakyThrows
public DataStoreState deepCopy() {
return JacksonMapper.getDefault().treeToValue(JacksonMapper.getDefault().valueToTree(this), getClass());
}
@Override
public final int hashCode() {
var tree = JacksonMapper.getDefault().valueToTree(this);
return tree.hashCode();
}
@Override
public final boolean equals(Object o) {
if (this == o) {
return true;
}
if (o != null && getClass() != o.getClass()) {
return false;
}
var tree = JacksonMapper.getDefault().valueToTree(this);
var otherTree = JacksonMapper.getDefault().valueToTree(o);
return tree.equals(otherTree);
}
@SneakyThrows
public String toString() {
var tree = JacksonMapper.getDefault().valueToTree(this);
return tree.toPrettyString();
}
}

View file

@ -1,9 +1,11 @@
package io.xpipe.core.store;
import io.xpipe.core.util.DataStateProvider;
import lombok.SneakyThrows;
import java.util.Arrays;
import java.util.function.Supplier;
public interface StatefulDataStore<T extends DataStoreState> extends DataStore {
@ -17,14 +19,20 @@ public interface StatefulDataStore<T extends DataStoreState> extends DataStore {
return getStateClass().cast(m.invoke(b));
}
@SuppressWarnings("unchecked")
default T getState() {
return DataStateProvider.get().getState(this, this::createDefaultState);
return (T)
DataStateProvider.get().getState(this, this::createDefaultState).deepCopy();
}
default void setState(T val) {
DataStateProvider.get().setState(this, val);
}
default T getState(Supplier<T> def) {
return DataStateProvider.get().getState(this, def);
}
@SneakyThrows
@SuppressWarnings("unchecked")
default Class<T> getStateClass() {

View file

@ -8,6 +8,8 @@ The file transfer mechanism when editing files had some flaws, which under rare
The entire transfer implementation has been rewritten to iron out these issues and increase reliability. Other file browser actions have also been made more reliable.
There seems to be another separate issue with a PowerShell bug when connecting to a Windows system, causing file uploads to be slow. For now, xpipe can fall back to pwsh if it is installed to work around this issue.
## Git vault improvements
The conflict resolution has been improved
@ -15,11 +17,27 @@ The conflict resolution has been improved
- In case of a merge conflict, overwriting local changes will now preserve all connections that are not added to the git vault, including local connections
- You now have the option to force push changes when a conflict occurs while XPipe is saving while running, not requiring a restart anymore
## Terminal improvements
The terminal integration got reworked for some terminals:
- iTerm can now launch tabs instead of individual windows. There were also a few issues fixed that prevented it from launching sometimes
- WezTerm now supports tabs on Linux and macOS. The Windows installation detection has been improved to detect all installed versions
- Terminal.app will now launch faster
## Other
- You can now add simple RDP connections without a file
- Fix VMware Player/Workstation and MSYS2 not being detected on Windows. Now simply searching for connections should add them automatically if they are installed
- The file browser sidebar now only contains connections that can be opened in it, reducing the amount of connection shown
- Clarify error message for RealVNC servers, highlighting that RealVNC uses a proprietary protocol spec that can't be supported by third-party VNC clients like xpipe
- Fix Linux builds containing unnecessary debug symbols
- Fix AUR package also installing a debug package
- Fix application restart not working properly on macOS
- Fix possibility of selecting own children connections as hosts, causing a stack overflow. Please don't try to create cycles in your connection graphs
- Fix vault secrets not correctly updating unless restarted when changing vault passphrase
- Fix connection launcher desktop shortcuts and URLs not properly executing if xpipe is not running
- Fix move to ... menu sometimes not ordering categories correctly
- Fix SSH command failing on macOS with homebrew openssh package installed
- Fix SSH connections not opening the correct shell environment on Windows when username contained spaces due to an OpenSSH bug
- Fix SSH connections not opening the correct shell environment on Windows systems when username contained spaces due to an OpenSSH bug
- Fix newly added connections not having the correct order
- Fix error messages of external editor programs not being shown when they failed to start

View file

@ -58,7 +58,7 @@ jlink {
]
if (org.gradle.internal.os.OperatingSystem.current().isLinux()) {
options += ['--strip-native-debug-symbols']
options.addAll('--strip-native-debug-symbols', 'exclude-debuginfo-files')
}
if (useBundledJavaFx) {

View file

@ -31,13 +31,15 @@ public class ScriptGroupStoreProvider implements DataStoreProvider {
var def = StoreToggleComp.<ScriptGroupStore>simpleToggle(
"base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> {
var state = s.getState().toBuilder().isDefault(aBoolean).build();
var state = s.getState();
state.setDefault(aBoolean);
s.setState(state);
});
var bring = StoreToggleComp.<ScriptGroupStore>simpleToggle(
"base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> {
var state = s.getState().toBuilder().bringToShell(aBoolean).build();
var state = s.getState();
state.setBringToShell(aBoolean);
s.setState(state);
});

View file

@ -6,14 +6,15 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.util.Validators;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellInitCommand;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreState;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.StatefulDataStore;
import io.xpipe.core.util.JacksonizedValue;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@ -221,12 +222,20 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
public abstract List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts();
@Value
@EqualsAndHashCode(callSuper=true)
@SuperBuilder(toBuilder = true)
@FieldDefaults(level = AccessLevel.PRIVATE)
@Setter
@Getter
@SuperBuilder
@Jacksonized
public static class State extends DataStoreState {
boolean isDefault;
boolean bringToShell;
@Override
public void merge(DataStoreState newer) {
var s = (State) newer;
isDefault = s.isDefault;
bringToShell = s.bringToShell;
}
}
}

View file

@ -56,13 +56,15 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
var def = StoreToggleComp.<SimpleScriptStore>simpleToggle(
"base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> {
var state = s.getState().toBuilder().isDefault(aBoolean).build();
var state = s.getState();
state.setDefault(aBoolean);
s.setState(state);
});
var bring = StoreToggleComp.<SimpleScriptStore>simpleToggle(
"base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> {
var state = s.getState().toBuilder().bringToShell(aBoolean).build();
var state = s.getState();
state.setBringToShell(aBoolean);
s.setState(state);
});

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-rc-1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View file

@ -457,6 +457,3 @@ history=Browsing-historik
skipAll=Spring alle over
notes=Bemærkninger
addNotes=Tilføj noter
order=Bestille ...
stickToTop=Hold dig på toppen
orderAheadOf=Bestil på forhånd ...

View file

@ -451,6 +451,3 @@ history=Browsing-Verlauf
skipAll=Alles überspringen
notes=Anmerkungen
addNotes=Notizen hinzufügen
order=Bestellen ...
stickToTop=Oben bleiben
orderAheadOf=Vorbestellen ...

View file

@ -454,7 +454,3 @@ history=Browsing history
skipAll=Skip all
notes=Notes
addNotes=Add notes
#context: verb
order=Order ...
stickToTop=Keep on top
orderAheadOf=Order ahead of ...

View file

@ -438,6 +438,3 @@ history=Historial de navegación
skipAll=Saltar todo
notes=Notas
addNotes=Añadir notas
order=Ordenar ...
stickToTop=Mantener arriba
orderAheadOf=Haz tu pedido antes de ...

View file

@ -438,6 +438,3 @@ history=Historique de navigation
skipAll=Sauter tout
notes=Notes
addNotes=Ajouter des notes
order=Commander...
stickToTop=Garde le dessus
orderAheadOf=Commande en avance...

View file

@ -438,6 +438,3 @@ history=Cronologia di navigazione
skipAll=Salta tutto
notes=Note
addNotes=Aggiungi note
order=Ordinare ...
stickToTop=Continua a essere in cima
orderAheadOf=Ordina prima di ...

View file

@ -438,6 +438,3 @@ history=閲覧履歴
skipAll=すべてスキップする
notes=備考
addNotes=メモを追加する
order=注文する
stickToTop=トップをキープする
orderAheadOf=先に注文する

View file

@ -438,6 +438,3 @@ history=Browsegeschiedenis
skipAll=Alles overslaan
notes=Opmerkingen
addNotes=Opmerkingen toevoegen
order=Bestellen ...
stickToTop=Bovenaan houden
orderAheadOf=Vooruitbestellen ...

View file

@ -438,6 +438,3 @@ history=Histórico de navegação
skipAll=Salta tudo
notes=Nota
addNotes=Adiciona notas
order=Encomenda ...
stickToTop=Mantém-te no topo
orderAheadOf=Encomenda antes de ...

View file

@ -438,6 +438,3 @@ history=История просмотров
skipAll=Пропустить все
notes=Заметки
addNotes=Добавляй заметки
order=Заказать ...
stickToTop=Держись на высоте
orderAheadOf=Заказать заранее ...

View file

@ -439,6 +439,3 @@ history=Tarama geçmişi
skipAll=Tümünü atla
notes=Notlar
addNotes=Notlar ekleyin
order=Sipariş ...
stickToTop=Zirvede kal
orderAheadOf=Önceden sipariş verin ...

View file

@ -438,6 +438,3 @@ history=浏览历史
skipAll=全部跳过
notes=说明
addNotes=添加注释
order=订购 ...
stickToTop=保持在顶部
orderAheadOf=提前订购...

View file

@ -1 +1 @@
9.4-3
9.4