Improve hierarchical store display [release]

This commit is contained in:
crschnick 2023-03-13 15:07:08 +00:00
parent add30cb413
commit 849e8f0ef4
8 changed files with 185 additions and 112 deletions

View file

@ -1,8 +0,0 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.browser;
import io.xpipe.app.storage.DataStoreEntry;
record Bookmark(DataStoreEntry entry) {
}

View file

@ -1,16 +1,14 @@
package io.xpipe.app.browser;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.comp.storage.store.StoreEntryFlatMiniSection;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.Map;
final class BookmarkList extends SimpleComp {
@ -22,19 +20,23 @@ final class BookmarkList extends SimpleComp {
@Override
protected Region createSimple() {
var list = DataStorage.get().getStoreEntries().stream().filter(entry -> entry.getStore() instanceof ShellStore).map(entry -> new Bookmark(entry)).toList();
return new ListBoxViewComp<>(FXCollections.observableList(list), FXCollections.observableList(list), bookmark -> {
var imgView =
new PrettyImageComp(new SimpleStringProperty(bookmark.entry().getProvider().getDisplayIconFileName()), 16, 16).createRegion();
var button = new JFXButton(bookmark.entry().getName(), imgView);
button.setOnAction(event -> {
event.consume();
var map = StoreEntryFlatMiniSection.createMap();
var list = new VBox();
for (Map.Entry<StoreEntryWrapper, Region> e : map.entrySet()) {
if (!(e.getKey().getEntry().getStore() instanceof ShellStore)) {
continue;
}
var fileSystem = ((ShellStore) bookmark.entry().getStore());
var button = new JFXButton(null, e.getValue());
button.setOnAction(event -> {
var fileSystem = ((ShellStore) e.getKey().getEntry().getStore());
model.openFileSystem(fileSystem);
event.consume();
});
button.setAlignment(Pos.CENTER_LEFT);
return Comp.of(() -> button).grow(true, false);
}).createRegion();
button.prefWidthProperty().bind(list.widthProperty());
list.getChildren().add(button);
}
list.setFillWidth(true);
return list;
}
}

View file

@ -0,0 +1,48 @@
package io.xpipe.app.comp.storage.store;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Orientation;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
import java.util.LinkedHashMap;
import java.util.Map;
@Value
@EqualsAndHashCode(callSuper = true)
public class StoreEntryFlatMiniSection extends SimpleComp {
public static Map<StoreEntryWrapper, Region> createMap() {
var map = new LinkedHashMap<StoreEntryWrapper, Region>();
var topLevel = StoreViewSection.createTopLevels();
var depth = 0;
for (StoreViewSection v : topLevel) {
add(depth, v, map);
}
return map;
}
private static void add(int depth, StoreViewSection section, Map<StoreEntryWrapper, Region> map) {
map.put(section.getEntry(), new StoreEntryFlatMiniSection(depth, section.getEntry()).createRegion());
for (StoreViewSection child : section.getChildren()) {
add(depth + 1, child, map);
}
}
int depth;
StoreEntryWrapper wrapper;
@Override
protected Region createSimple() {
var label = new Label(wrapper.getName(), new PrettyImageComp(new SimpleStringProperty(wrapper.getEntry().getProvider().getDisplayIconFileName()), 20, 20).createRegion());
var spacer = new Spacer(depth * 10, Orientation.HORIZONTAL);
var box = new HBox(spacer, label);
return box;
}
}

View file

@ -15,14 +15,14 @@ import java.util.LinkedHashMap;
public class StoreEntryListComp extends SimpleComp {
private Comp<?> createList() {
var topLevel = StoreEntrySection.createTopLevels();
var topLevel = StoreViewSection.createTopLevels();
var filtered = BindingsHelper.filteredContentBinding(
topLevel,
StoreViewState.get()
.getFilterString()
.map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s))));
var content = new ListBoxViewComp<>(filtered, topLevel, (StoreEntrySection e) -> {
return e.comp(true);
var content = new ListBoxViewComp<>(filtered, topLevel, (StoreViewSection e) -> {
return new StoreEntrySection(e, true);
});
return content.styleClass("store-list-comp").styleClass(Styles.STRIPED);
}

View file

@ -1,85 +1,32 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.kordamp.ikonli.javafx.FontIcon;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
public class StoreEntrySection implements StorageFilter.Filterable {
public class StoreEntrySection extends Comp<CompStructure<VBox>> {
private static final Comparator<StoreEntrySection> COMPARATOR = Comparator.<StoreEntrySection, Instant>comparing(
o -> o.entry.getEntry().getState().equals(DataStoreEntry.State.COMPLETE_AND_VALID)
? o.entry.getEntry().getLastAccess()
: Instant.EPOCH).reversed()
.thenComparing(
storeEntrySection -> storeEntrySection.entry.getEntry().getName());
private final StoreViewSection section;
private final boolean top;
public StoreEntrySection(StoreEntryWrapper entry, ObservableList<StoreEntrySection> children) {
this.entry = entry;
this.children = children;
public StoreEntrySection(StoreViewSection section, boolean top) {
this.section = section;
this.top = top;
}
public static ObservableList<StoreEntrySection> createTopLevels() {
var filtered =
BindingsHelper.filteredContentBinding(StoreViewState.get().getAllEntries(), storeEntryWrapper -> {
if (!storeEntryWrapper.getEntry().getState().isUsable()) {
return true;
}
var parent = storeEntryWrapper
.getEntry()
.getProvider()
.getParent(storeEntryWrapper.getEntry().getStore());
return parent == null
|| (DataStorage.get().getStoreEntryIfPresent(parent).isEmpty());
});
var topLevel = BindingsHelper.mappedContentBinding(filtered, storeEntryWrapper -> create(storeEntryWrapper));
var ordered = BindingsHelper.orderedContentBinding(
topLevel,
COMPARATOR);
return ordered;
}
public static StoreEntrySection create(StoreEntryWrapper e) {
if (!e.getEntry().getState().isUsable()) {
return new StoreEntrySection(e, FXCollections.observableArrayList());
}
var filtered = BindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
other -> other.getEntry().getState().isUsable()
&& e.getEntry()
.getStore()
.equals(other.getEntry()
.getProvider()
.getParent(other.getEntry().getStore())));
var children = BindingsHelper.mappedContentBinding(filtered, entry1 -> create(entry1));
var ordered = BindingsHelper.orderedContentBinding(
children,
COMPARATOR);
return new StoreEntrySection(e, ordered);
}
private final StoreEntryWrapper entry;
private final ObservableList<StoreEntrySection> children;
public Comp<?> comp(boolean top) {
var root = new StoreEntryComp(entry).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
@Override
public CompStructure<VBox> createBase() {
var root = new StoreEntryComp(section.getEntry()).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
var icon = Comp.of(() -> {
var padding = new FontIcon("mdal-arrow_forward_ios");
padding.setIconSize(14);
@ -90,15 +37,15 @@ public class StoreEntrySection implements StorageFilter.Filterable {
});
List<Comp<?>> topEntryList = top ? List.of(root) : List.of(icon, root);
var all = children;
var all = section.getChildren();
var shown = BindingsHelper.filteredContentBinding(
all,
StoreViewState.get()
.getFilterString()
.map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s))));
var content = new ListBoxViewComp<>(shown, all, (StoreEntrySection e) -> {
return e.comp(false).apply(GrowAugment.create(true, false));
})
var content = new ListBoxViewComp<>(shown, all, (StoreViewSection e) -> {
return new StoreEntrySection(e, false).apply(GrowAugment.create(true, false));
})
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS))
.apply(struc -> struc.get().backgroundProperty().set(Background.fill(Color.color(0, 0, 0, 0.01))));
var spacer = Comp.of(() -> {
@ -111,12 +58,6 @@ public class StoreEntrySection implements StorageFilter.Filterable {
new HorizontalComp(topEntryList),
new HorizontalComp(List.of(spacer, content))
.apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.size(children).isEqualTo(0)))));
}
@Override
public boolean shouldShow(String filter) {
return entry.shouldShow(filter)
|| children.stream().anyMatch(storeEntrySection -> storeEntrySection.shouldShow(filter));
.hide(BindingsHelper.persist(Bindings.size(section.getChildren()).isEqualTo(0))))).createStructure();
}
}

View file

@ -0,0 +1,73 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.storage.StorageFilter;
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;
import java.time.Instant;
import java.util.Comparator;
@Value
public class StoreViewSection implements StorageFilter.Filterable {
StoreEntryWrapper entry;
ObservableList<StoreViewSection> children;
private static final Comparator<StoreViewSection> COMPARATOR = Comparator.<StoreViewSection, Instant>comparing(
o -> o.entry.getEntry().getState().equals(DataStoreEntry.State.COMPLETE_AND_VALID)
? o.entry.getEntry().getLastAccess()
: Instant.EPOCH).reversed()
.thenComparing(
storeEntrySection -> storeEntrySection.entry.getEntry().getName());
public static ObservableList<StoreViewSection> createTopLevels() {
var filtered =
BindingsHelper.filteredContentBinding(StoreViewState.get().getAllEntries(), storeEntryWrapper -> {
if (!storeEntryWrapper.getEntry().getState().isUsable()) {
return true;
}
var parent = storeEntryWrapper
.getEntry()
.getProvider()
.getParent(storeEntryWrapper.getEntry().getStore());
return parent == null
|| (DataStorage.get().getStoreEntryIfPresent(parent).isEmpty());
});
var topLevel = BindingsHelper.mappedContentBinding(filtered, storeEntryWrapper -> create(storeEntryWrapper));
var ordered = BindingsHelper.orderedContentBinding(
topLevel,
COMPARATOR);
return ordered;
}
public static StoreViewSection create(StoreEntryWrapper e) {
if (!e.getEntry().getState().isUsable()) {
return new StoreViewSection(e, FXCollections.observableArrayList());
}
var filtered = BindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
other -> other.getEntry().getState().isUsable()
&& e.getEntry()
.getStore()
.equals(other.getEntry()
.getProvider()
.getParent(other.getEntry().getStore())));
var children = BindingsHelper.mappedContentBinding(filtered, entry1 -> create(entry1));
var ordered = BindingsHelper.orderedContentBinding(
children,
COMPARATOR);
return new StoreViewSection(e, ordered);
}
@Override
public boolean shouldShow(String filter) {
return entry.shouldShow(filter)
|| children.stream().anyMatch(storeEntrySection -> storeEntrySection.shouldShow(filter));
}
}

View file

@ -1,5 +1,7 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.comp.storage.store.StoreEntryFlatMiniSection;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.SimpleComp;
@ -15,6 +17,7 @@ import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
@ -71,17 +74,31 @@ public class ShellStoreChoiceComp<T extends ShellStore> extends SimpleComp {
@Override
@SuppressWarnings("unchecked")
protected Region createSimple() {
var comboBox =
new CustomComboBoxBuilder<T>(selected, this::createGraphic, new Label(AppI18n.get("none")), n -> true);
var map = StoreEntryFlatMiniSection.createMap();
var comboBox = new CustomComboBoxBuilder<T>(
selected,
t -> map.entrySet().stream()
.filter(e -> t.equals(e.getKey().getEntry().getStore()))
.findFirst()
.orElseThrow()
.getValue(),
new Label(AppI18n.get("none")),
n -> true);
comboBox.setUnknownNode(t -> createGraphic(t));
var available = DataStorage.get().getUsableStores().stream()
.filter(s -> s != self)
.filter(s -> storeClass.isAssignableFrom(s.getClass()) && applicableCheck.test((T) s))
.map(s -> (ShellStore) s)
.toList();
for (Map.Entry<StoreEntryWrapper, Region> e : map.entrySet()) {
if (e.getKey().getEntry().getStore() == self) {
continue;
}
var s = e.getKey().getEntry().getStore();
if (!storeClass.isAssignableFrom(s.getClass()) || !applicableCheck.test((T) s)) {
continue;
}
comboBox.add((T) e.getKey().getEntry().getStore());
}
available.forEach(s -> comboBox.add((T) s));
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("choice-comp");
cb.setMaxWidth(2000);

View file

@ -1 +1 @@
0.5.11
0.5.12