Store improvements

This commit is contained in:
crschnick 2023-07-01 00:25:38 +00:00
parent 94f8a41dfb
commit 397fc785d6
12 changed files with 174 additions and 146 deletions

View file

@ -170,10 +170,14 @@ final class BrowserBookmarkList extends SimpleComp {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
// Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
setGraphic(null);
// Use opacity instead of visibility as visibility is kinda bugged with web views
setOpacity(0.0);
setFocusTraversable(false);
setAccessibleText(null);
} else {
@ -190,7 +194,7 @@ final class BrowserBookmarkList extends SimpleComp {
img.set(item.getEntry()
.getProvider()
.getDisplayIconFileName(item.getEntry().getStore()));
setGraphic(imageView);
setOpacity(1.0);
setFocusTraversable(true);
setAccessibleText(
item.getName() + " " + item.getEntry().getProvider().getDisplayName());

View file

@ -39,11 +39,11 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
VBox listView = new VBox();
listView.setFocusTraversable(false);
refresh(listView, shown, cache, false);
refresh(listView, shown, all, cache, false);
listView.requestLayout();
shown.addListener((ListChangeListener<? super T>) (c) -> {
refresh(listView, c.getList(), cache, true);
refresh(listView, c.getList(), all, cache, true);
});
all.addListener((ListChangeListener<? super T>) c -> {
@ -57,9 +57,12 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
return new SimpleCompStructure<>(scroll);
}
private void refresh(VBox listView, List<? extends T> c, Map<T, Region> cache, boolean asynchronous) {
private void refresh(VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
Runnable update = () -> {
var newShown = c.stream()
// Clear cache of unused values
cache.keySet().removeIf(t -> !all.contains(t));
var newShown = shown.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, compFunction.apply(v).createRegion());

View file

@ -1,7 +1,7 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.store.GuiDsStoreCreator;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.comp.store.GuiDsStoreCreator;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
@ -181,7 +181,8 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
public void executeDefaultAction() throws Exception {
var found = getDefaultActionProvider().getValue();
if (found != null) {
if (entry.getState().equals(DataStoreEntry.State.COMPLETE_BUT_INVALID) || entry.getState().equals(DataStoreEntry.State.COMPLETE_NOT_VALIDATED)) {
if (entry.getState().equals(DataStoreEntry.State.COMPLETE_BUT_INVALID)
|| entry.getState().equals(DataStoreEntry.State.COMPLETE_NOT_VALIDATED)) {
executeRefreshAction();
}

View file

@ -4,7 +4,6 @@ import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Value;
@ -28,7 +27,7 @@ public class StoreSection implements StorageFilter.Filterable {
ObservableList<StoreSection> children;
private static final Comparator<StoreSection> COMPARATOR = Comparator.<StoreSection, Instant>comparing(
o -> o.wrapper.getEntry().getState().equals(DataStoreEntry.State.COMPLETE_AND_VALID)
o -> o.wrapper.getEntry().getState().isUsable()
? o.wrapper.getEntry().getLastModified()
: Instant.EPOCH)
.reversed()
@ -39,16 +38,9 @@ public class StoreSection implements StorageFilter.Filterable {
var topLevel = BindingsHelper.mappedContentBinding(
StoreViewState.get().getAllEntries(), storeEntryWrapper -> create(storeEntryWrapper));
var filtered = BindingsHelper.filteredContentBinding(topLevel, section -> {
if (!section.getWrapper().getEntry().getState().isUsable()) {
return true;
}
var parent = section.getWrapper()
.getEntry()
.getProvider()
.getDisplayParent(section.getWrapper().getEntry().getStore());
return parent == null
|| (DataStorage.get().getStoreEntryIfPresent(parent).isEmpty());
return DataStorage.get()
.getParent(section.getWrapper().getEntry(), true)
.isEmpty();
});
var ordered = BindingsHelper.orderedContentBinding(filtered, COMPARATOR);
return new StoreSection(null, ordered);
@ -59,14 +51,13 @@ public class StoreSection implements StorageFilter.Filterable {
return new StoreSection(e, FXCollections.observableArrayList());
}
var filtered = BindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
other -> other.getEntry().getState().isUsable()
&& e.getEntry()
.getStore()
.equals(other.getEntry()
.getProvider()
.getDisplayParent(other.getEntry().getStore())));
var filtered =
BindingsHelper.filteredContentBinding(StoreViewState.get().getAllEntries(), other -> {
return DataStorage.get()
.getParent(other.getEntry(), true)
.map(found -> found.equals(e.getEntry()))
.orElse(false);
});
var children = BindingsHelper.mappedContentBinding(filtered, entry1 -> create(entry1));
var ordered = BindingsHelper.orderedContentBinding(children, COMPARATOR);
return new StoreSection(e, ordered);

View file

@ -134,6 +134,10 @@ public interface DataStoreProvider {
String queryInformationString(DataStore store, int length) throws Exception;
default String queryInvalidInformationString(DataStore store, int length) throws Exception {
return null;
}
String toSummaryString(DataStore store, int length);
default String i18n(String key) {

View file

@ -6,15 +6,12 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebView;
public class PrettyImageComp extends SimpleComp {
@ -31,6 +28,8 @@ public class PrettyImageComp extends SimpleComp {
@Override
protected Region createSimple() {
var aspectRatioProperty = new SimpleDoubleProperty(1);
var imageAspectRatioProperty = new SimpleDoubleProperty(1);
var svgAspectRatioProperty = new SimpleDoubleProperty(1);
var widthProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < aspectRatioProperty.doubleValue();
@ -51,89 +50,84 @@ public class PrettyImageComp extends SimpleComp {
}
},
aspectRatioProperty);
var image = new SimpleStringProperty();
var currentNode = new SimpleObjectProperty<Node>();
var stack = new StackPane();
{
var svgImageContent = new SimpleStringProperty();
var storeIcon = SvgView.create(svgImageContent);
SimpleChangeListener.apply(image, newValue -> {
if (AppImages.hasSvgImage(newValue)) {
svgImageContent.set(AppImages.svgImage(newValue));
}
});
var ar = Bindings.createDoubleBinding(
() -> {
return storeIcon.getWidth().getValue().doubleValue()
/ storeIcon.getHeight().getValue().doubleValue();
},
storeIcon.getWidth(),
storeIcon.getHeight());
svgAspectRatioProperty.bind(ar);
var node = storeIcon.createWebview();
node.prefWidthProperty().bind(widthProperty);
node.maxWidthProperty().bind(widthProperty);
node.minWidthProperty().bind(widthProperty);
node.prefHeightProperty().bind(heightProperty);
node.maxHeightProperty().bind(heightProperty);
node.minHeightProperty().bind(heightProperty);
stack.getChildren().add(node);
}
{
var storeIcon = new ImageView();
storeIcon.setFocusTraversable(false);
storeIcon
.imageProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (!AppImages.hasNormalImage(image.getValue())) {
return null;
}
return AppImages.image(image.getValue());
},
image));
var ar = Bindings.createDoubleBinding(
() -> {
if (storeIcon.getImage() == null) {
return 1.0;
}
return storeIcon.getImage().getWidth()
/ storeIcon.getImage().getHeight();
},
storeIcon.imageProperty());
imageAspectRatioProperty.bind(ar);
storeIcon.fitWidthProperty().bind(widthProperty);
storeIcon.fitHeightProperty().bind(heightProperty);
storeIcon.setSmooth(true);
stack.getChildren().add(storeIcon);
}
SimpleChangeListener.apply(PlatformThread.sync(value), val -> {
image.set(val);
var requiresChange = val == null
|| (val.endsWith(".svg") && !(currentNode.get() instanceof WebView)
|| !(currentNode.get() instanceof ImageView));
if (!requiresChange) {
return;
}
aspectRatioProperty.unbind();
if (val == null) {
currentNode.set(new Region());
stack.getChildren().get(0).setOpacity(0.0);
stack.getChildren().get(1).setOpacity(0.0);
} else if (val.endsWith(".svg")) {
var storeIcon = SvgView.create(Bindings.createStringBinding(
() -> {
if (!AppImages.hasSvgImage(image.getValue())) {
return null;
}
return AppImages.svgImage(image.getValue());
},
image));
var ar = Bindings.createDoubleBinding(
() -> {
return storeIcon.getWidth().getValue().doubleValue()
/ storeIcon.getHeight().getValue().doubleValue();
},
storeIcon.getWidth(),
storeIcon.getHeight());
aspectRatioProperty.bind(ar);
var node = storeIcon.createWebview();
node.prefWidthProperty().bind(widthProperty);
node.maxWidthProperty().bind(widthProperty);
node.minWidthProperty().bind(widthProperty);
node.prefHeightProperty().bind(heightProperty);
node.maxHeightProperty().bind(heightProperty);
node.minHeightProperty().bind(heightProperty);
currentNode.set(node);
aspectRatioProperty.bind(svgAspectRatioProperty);
stack.getChildren().get(0).setOpacity(1.0);
stack.getChildren().get(1).setOpacity(0.0);
} else {
var storeIcon = new ImageView();
storeIcon.setFocusTraversable(false);
storeIcon
.imageProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (!AppImages.hasNormalImage(image.getValue())) {
return null;
}
return AppImages.image(image.getValue());
},
image));
var ar = Bindings.createDoubleBinding(
() -> {
if (storeIcon.getImage() == null) {
return 1.0;
}
return storeIcon.getImage().getWidth()
/ storeIcon.getImage().getHeight();
},
storeIcon.imageProperty());
aspectRatioProperty.bind(ar);
storeIcon.fitWidthProperty().bind(widthProperty);
storeIcon.fitHeightProperty().bind(heightProperty);
storeIcon.setSmooth(true);
currentNode.set(storeIcon);
aspectRatioProperty.bind(imageAspectRatioProperty);
stack.getChildren().get(0).setOpacity(0.0);
stack.getChildren().get(1).setOpacity(1.0);
}
});
var stack = new StackPane();
SimpleChangeListener.apply(currentNode, val -> {
if (val == null) {
stack.getChildren().clear();
return;
}
stack.getChildren().setAll(val);
});
stack.setFocusTraversable(false);
stack.setPrefWidth(width);
stack.setMinWidth(width);

View file

@ -68,24 +68,14 @@ public abstract class DataStorage {
}
public synchronized boolean refreshChildren(DataStoreEntry e) {
var children = getLogicalStoreChildren(e, false);
return refreshChildren(e, children);
}
public synchronized boolean refreshChildren(DataStoreEntry e, List<DataStoreEntry> oldChildren) {
if (!(e.getStore() instanceof FixedHierarchyStore)) {
return false;
}
try {
deleteChildren(e, true);
var newChildren = ((FixedHierarchyStore) e.getStore()).listChildren();
oldChildren.stream().filter(entry -> !newChildren.containsValue(entry.getStore())).forEach(entry -> {
deleteChildren(entry, true);
deleteStoreEntry(entry);
});
newChildren.entrySet().stream().filter(entry -> oldChildren.stream().noneMatch(old -> old.getStore().equals(entry.getValue()))).forEach(entry -> {
addStoreEntryIfNotPresent(entry.getKey(), entry.getValue());
});
newChildren.forEach((key, value) -> addStoreEntryIfNotPresent(key, value));
return newChildren.size() > 0;
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
@ -95,29 +85,44 @@ public abstract class DataStorage {
public synchronized void deleteChildren(DataStoreEntry e, boolean deep) {
// Reverse to delete deepest children first
var ordered = getLogicalStoreChildren(e, deep);
var ordered = getStoreChildren(e, false, deep);
Collections.reverse(ordered);
ordered.forEach(entry -> {
deleteStoreEntry(entry);
synchronized (this) {
this.storeEntries.remove(entry);
}
this.listeners.forEach(l -> l.onStoreRemove(entry));
});
save();
}
public synchronized List<DataStoreEntry> getLogicalStoreChildren(DataStoreEntry entry, boolean deep) {
var children = new ArrayList<>(getStoreEntries().stream()
.filter(other -> {
if (!other.getState().isUsable()) {
return false;
}
public synchronized Optional<DataStoreEntry> getParent(DataStoreEntry entry, boolean display) {
if (!entry.getState().isUsable()) {
return Optional.empty();
}
var parent = other.getProvider().getLogicalParent(other.getStore());
return Objects.equals(entry.getStore(), parent);
})
.toList());
var provider = entry.getProvider();
var parent = display ? provider.getDisplayParent(entry.getStore()) : provider.getLogicalParent(entry.getStore());
return parent != null ? getStoreEntryIfPresent(parent) : Optional.empty();
}
public synchronized List<DataStoreEntry> getStoreChildren(DataStoreEntry entry, boolean display, boolean deep) {
var children = new ArrayList<>(getStoreEntries().stream()
.filter(other -> {
if (!other.getState().isUsable()) {
return false;
}
var provider = other.getProvider();
var parent = display ? provider.getDisplayParent(other.getStore()) : provider.getLogicalParent(other.getStore());
return parent != null && entry.getStore().getClass().equals(parent.getClass()) && entry.getStore().equals(parent);
})
.toList());
if (deep) {
for (DataStoreEntry dataStoreEntry : new ArrayList<>(children)) {
children.addAll(getLogicalStoreChildren(dataStoreEntry, true));
children.addAll(getStoreChildren(dataStoreEntry, display, true));
}
}
@ -174,7 +179,7 @@ public abstract class DataStorage {
public synchronized DataStoreEntry getStoreEntry(@NonNull DataStore store) {
var entry = storeEntries.stream()
.filter(n -> store.equals(n.getStore()))
.filter(n -> Objects.equals(store.getClass(), n.getStore().getClass()) && store.equals(n.getStore()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Store not found"));
return entry;
@ -195,13 +200,11 @@ public abstract class DataStorage {
public void setAndRefreshAsync(DataStoreEntry entry, DataStore s) {
ThreadHelper.runAsync(() -> {
var old = entry.getStore();
var children = getLogicalStoreChildren(entry, false);
deleteChildren(entry, true);
try {
entry.setStoreInternal(s, false);
entry.refresh(true);
// Update old children
children.forEach(entry1 -> propagateUpdate(entry1));
DataStorage.get().refreshChildren(entry, children);
DataStorage.get().refreshChildren(entry);
} catch (Exception e) {
entry.setStoreInternal(old, false);
entry.simpleRefresh();
@ -222,7 +225,7 @@ public abstract class DataStorage {
}
void propagateUpdate(DataStoreEntry origin) {
getLogicalStoreChildren(origin, false).forEach(entry -> {
getStoreChildren(origin, false, false).forEach(entry -> {
entry.simpleRefresh();
propagateUpdate(entry);
});

View file

@ -46,6 +46,9 @@ public class DataStoreEntry extends StorageElement {
@NonFinal
boolean expanded;
@NonFinal
DataStoreProvider provider;
private DataStoreEntry(
Path directory,
UUID uuid,
@ -65,6 +68,7 @@ public class DataStoreEntry extends StorageElement {
this.state = state;
this.configuration = configuration;
this.expanded = expanded;
this.provider = store != null ? DataStoreProviders.byStoreClass(store.getClass()).orElse(null) : null;
}
@SneakyThrows
@ -207,6 +211,7 @@ public class DataStoreEntry extends StorageElement {
lastModified = Instant.now();
information = e.information;
dirty = true;
provider = e.provider;
simpleRefresh();
}
@ -230,6 +235,7 @@ public class DataStoreEntry extends StorageElement {
store = null;
state = State.LOAD_FAILED;
information = null;
provider = null;
dirty = dirty || oldStore != null;
listeners.forEach(l -> l.onUpdate());
} else {
@ -241,6 +247,7 @@ public class DataStoreEntry extends StorageElement {
dirty = dirty || !nodesEqual;
store = newStore;
provider = DataStoreProviders.byStoreClass(newStore.getClass()).orElse(null);
var complete = newStore.isComplete();
try {
@ -261,13 +268,18 @@ public class DataStoreEntry extends StorageElement {
? State.COMPLETE_NOT_VALIDATED
: state;
state = stateToUse;
information = state == State.COMPLETE_AND_VALID
? information
: state == State.COMPLETE_BUT_INVALID
? getProvider().queryInvalidInformationString(getStore(), 50)
: null;
} else {
var stateToUse = state == State.LOAD_FAILED ? State.COMPLETE_BUT_INVALID : State.INCOMPLETE;
state = stateToUse;
}
} catch (Exception e) {
state = store.isComplete() ? State.COMPLETE_BUT_INVALID : State.INCOMPLETE;
information = null;
state = State.COMPLETE_BUT_INVALID;
information = getProvider().queryInvalidInformationString(getStore(), 50);
throw e;
} finally {
propagateUpdate();
@ -318,11 +330,7 @@ public class DataStoreEntry extends StorageElement {
}
public DataStoreProvider getProvider() {
if (store == null) {
return null;
}
return DataStoreProviders.byStoreClass(store.getClass()).orElse(null);
return provider;
}
@Getter

View file

@ -121,8 +121,23 @@ public class FileBridge {
return Optional.empty();
}
public void openReadOnlyString(String input, Consumer<String> fileConsumer) {
if (input == null) {
input = "";
}
var id = UUID.randomUUID();
String s = input;
openIO(
id.toString(),
id,
() -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)),
null,
fileConsumer);
}
public void openString(
String keyName, Object key, String input, Consumer<String> output, Consumer<String> consumer) {
String keyName, Object key, String input, Consumer<String> output, Consumer<String> fileConsumer) {
if (input == null) {
input = "";
}
@ -139,7 +154,7 @@ public class FileBridge {
output.accept(new String(toByteArray(), StandardCharsets.UTF_8));
}
},
consumer);
fileConsumer);
}
public void openIO(

View file

@ -83,6 +83,10 @@ public class FileOpener {
}
}
public static void openReadOnlyString(String input) {
FileBridge.get().openReadOnlyString(input, s -> openInTextEditor(s));
}
public static void openString(String keyName, Object key, String input, Consumer<String> output) {
FileBridge.get().openString(keyName, key, input, output, file -> openInTextEditor(file));
}

View file

@ -5,6 +5,7 @@ import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedHierarchyStore;
import javafx.beans.value.ObservableValue;
import lombok.Value;
@ -47,8 +48,8 @@ public class DeleteStoreChildrenAction implements ActionProvider {
@Override
public boolean isApplicable(DataStore o) {
return DataStorage.get()
.getLogicalStoreChildren(DataStorage.get().getStoreEntry(o), true)
return !(o instanceof FixedHierarchyStore) && DataStorage.get()
.getStoreChildren(DataStorage.get().getStoreEntry(o), true, true)
.size()
> 1;
}

View file

@ -24,7 +24,7 @@ tilda=Tilda
copyShareLink=Copy share link
selectStore=Select Store
saveSource=Save for later
deleteChildren=Delete children
deleteChildren=Remove children
selectSource=Select Source
commandLineRead=Update
commandLineWrite=Write