From 9321af9998b3976ff4430dc980f5d025f0876a8a Mon Sep 17 00:00:00 2001 From: crschnick Date: Tue, 27 Jun 2023 00:04:02 +0000 Subject: [PATCH] Rework wsl and k8s system --- .../app/comp/base/LoadingOverlayComp.java | 12 +- .../xpipe/app/comp/base/NamedToggleComp.java | 35 ++ .../store/DsStoreProviderChoiceComp.java | 2 +- .../source/SourceEntryContextMenu.java | 2 - .../storage/store/DenseStoreEntryComp.java | 109 +++++++ .../storage/store/StandardStoreEntryComp.java | 304 ++++++++++++++++++ .../comp/storage/store/StoreEntryComp.java | 174 ++++------ .../storage/store/StoreEntryListComp.java | 2 +- ...ection.java => StoreEntrySectionComp.java} | 11 +- .../comp/storage/store/StoreEntryWrapper.java | 8 +- .../app/comp/storage/store/StoreSection.java | 14 +- .../exchange/cli/ListStoresExchangeImpl.java | 2 +- .../cli/StoreProviderListExchangeImpl.java | 2 +- .../io/xpipe/app/ext/DataStoreProvider.java | 33 +- .../app/fxcomps/impl/IconButtonComp.java | 10 +- .../io/xpipe/app/storage/DataStorage.java | 68 ++-- .../io/xpipe/app/storage/DataStoreEntry.java | 13 +- app/src/main/java/module-info.java | 1 + .../app/resources/style/store-entry-comp.css | 8 + .../io/xpipe/ext/base/FileStoreProvider.java | 2 +- .../xpipe/ext/base/InMemoryStoreProvider.java | 2 +- .../ext/base/InternalStreamProvider.java | 2 +- .../ext/base/SinkDrainStoreProvider.java | 2 +- .../ext/base/actions/RefreshStoreAction.java | 64 ++++ ...ction.java => RefreshDirectoryAction.java} | 2 +- ext/base/src/main/java/module-info.java | 2 +- 26 files changed, 711 insertions(+), 175 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/comp/base/NamedToggleComp.java create mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java create mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java rename app/src/main/java/io/xpipe/app/comp/storage/store/{StoreEntrySection.java => StoreEntrySectionComp.java} (85%) create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/actions/RefreshStoreAction.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/{RefreshAction.java => RefreshDirectoryAction.java} (95%) diff --git a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java index 2c270f136..700f99060 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java @@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.scene.layout.StackPane; @@ -27,8 +28,6 @@ public class LoadingOverlayComp extends Comp> { var loading = new RingProgressIndicator(0, false); loading.setProgress(-1); - loading.setPrefWidth(50); - loading.setPrefHeight(50); var loadingBg = new StackPane(loading); loadingBg.getStyleClass().add("loading-comp"); @@ -69,7 +68,14 @@ public class LoadingOverlayComp extends Comp> { }; PlatformThread.sync(showLoading).addListener(listener); - var stack = new StackPane(compStruc.get(), loadingBg); + var r = compStruc.get(); + var stack = new StackPane(r, loadingBg); + + loading.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> { + return Math.min(r.getHeight() - 20, 50); + }, r.heightProperty())); + loading.prefHeightProperty().bind(loading.prefWidthProperty()); + return new SimpleCompStructure<>(stack); } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/NamedToggleComp.java b/app/src/main/java/io/xpipe/app/comp/base/NamedToggleComp.java new file mode 100644 index 000000000..de9c08c58 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/NamedToggleComp.java @@ -0,0 +1,35 @@ +package io.xpipe.app.comp.base; + +import atlantafx.base.controls.ToggleSwitch; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.util.PlatformThread; +import javafx.beans.property.BooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.layout.Region; + +public class NamedToggleComp extends SimpleComp { + + private final BooleanProperty selected; + private final ObservableValue name; + + public NamedToggleComp(BooleanProperty selected, ObservableValue name) { + this.selected = selected; + this.name = name; + } + + @Override + protected Region createSimple() { + var s = new ToggleSwitch(); + s.setSelected(selected.getValue()); + s.selectedProperty().addListener((observable, oldValue, newValue) -> { + selected.set(newValue); + }); + selected.addListener((observable, oldValue, newValue) -> { + PlatformThread.runLaterIfNeeded(() -> { + s.setSelected(newValue); + }); + }); + s.textProperty().bind(PlatformThread.sync(name)); + return s; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/source/store/DsStoreProviderChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/source/store/DsStoreProviderChoiceComp.java index fcb7bf9ae..63d57487e 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/store/DsStoreProviderChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/source/store/DsStoreProviderChoiceComp.java @@ -50,7 +50,7 @@ public class DsStoreProviderChoiceComp extends Comp var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true); comboBox.setAccessibleNames(dataStoreProvider -> dataStoreProvider.getDisplayName()); getProviders().stream() - .filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.shouldShow()) + .filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.canManuallyCreate()) .forEach(comboBox::add); ComboBox cb = comboBox.build(); cb.getStyleClass().add("data-source-type"); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java b/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java index 82b6345f6..8e0a322de 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java @@ -7,7 +7,6 @@ import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataSourceEntry; -import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.DesktopHelper; import javafx.beans.binding.Bindings; import javafx.scene.control.ContextMenu; @@ -63,7 +62,6 @@ public class SourceEntryContextMenu> extends ContextM var validate = new MenuItem(AppI18n.get("refresh"), new FontIcon("mdal-360")); validate.setOnAction(event -> { - DataStorage.get().refreshAsync(entry.getEntry(), true); }); cm.getItems().add(validate); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java new file mode 100644 index 000000000..b07852171 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java @@ -0,0 +1,109 @@ +package io.xpipe.app.comp.storage.store; + +import atlantafx.base.controls.Spacer; +import com.jfoenix.controls.JFXButton; +import io.xpipe.app.comp.base.LoadingOverlayComp; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.augment.ContextMenuAugment; +import io.xpipe.app.fxcomps.augment.GrowAugment; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.util.ThreadHelper; +import javafx.beans.binding.Bindings; +import javafx.geometry.HPos; +import javafx.scene.control.Label; +import javafx.scene.layout.*; +import lombok.SneakyThrows; + +public class DenseStoreEntryComp extends StoreEntryComp { + + private final boolean showIcon; + private final Comp content; + + public DenseStoreEntryComp(StoreEntryWrapper entry, boolean showIcon, Comp content) { + super(entry); + this.showIcon = showIcon; + this.content = content; + } + + protected Region createContent() { + var name = createName().createRegion(); + + var size = createInformation(); + + var date = new Label(); + date.textProperty().bind(AppI18n.readableDuration("usedDate", PlatformThread.sync(entry.lastAccessProperty()))); + AppFont.small(date); + date.getStyleClass().add("date"); + + var grid = new GridPane(); + + if (showIcon) { + var storeIcon = createIcon(30, 25); + grid.getColumnConstraints().add(new ColumnConstraints(45)); + grid.add(storeIcon, 0, 0); + GridPane.setHalignment(storeIcon, HPos.CENTER); + } else { + grid.add(new Region(), 0, 0); + grid.getColumnConstraints().add(new ColumnConstraints(5)); + } + + var fill = new ColumnConstraints(); + fill.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().addAll(new ColumnConstraints(450), fill); + + grid.add(name, 1, 0); + + var c = content != null ? content.createRegion() : new Region(); + grid.add(c, 2, 0); + GridPane.setHalignment(c, HPos.CENTER); + + grid.add(createButtonBar().createRegion(), 3, 0, 1, 1); + GrowAugment.create(true, false).augment(grid); + + AppFont.small(size); + AppFont.small(date); + + grid.getStyleClass().add("store-entry-grid"); + + applyState(grid); + + var button = new JFXButton(); + button.setGraphic(grid); + GrowAugment.create(true, false).augment(new SimpleCompStructure<>(grid)); + button.getStyleClass().add("store-entry-comp"); + button.getStyleClass().add("condensed-store-entry-comp"); + button.setMaxWidth(2000); + button.setFocusTraversable(true); + button.accessibleTextProperty() + .bind(Bindings.createStringBinding( + () -> { + return entry.getName(); + }, + entry.nameProperty())); + button.accessibleHelpProperty().bind(entry.getInformation()); + button.setOnAction(event -> { + event.consume(); + ThreadHelper.runFailableAsync(() -> { + entry.refreshIfNeeded(); + entry.executeDefaultAction(); + }); + }); + HBox.setHgrow(button, Priority.ALWAYS); + + new ContextMenuAugment<>(() -> DenseStoreEntryComp.this.createContextMenu()) + .augment(new SimpleCompStructure<>(button)); + + return new HBox(button, new Spacer(25)); + } + + @SneakyThrows + @Override + protected Region createSimple() { + var loading = new LoadingOverlayComp(Comp.of(() -> createContent()), entry.getLoading()); + var region = loading.createRegion(); + return region; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java new file mode 100644 index 000000000..be8b28b4c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java @@ -0,0 +1,304 @@ +package io.xpipe.app.comp.storage.store; + +import com.jfoenix.controls.JFXButton; +import io.xpipe.app.comp.base.LoadingOverlayComp; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.augment.ContextMenuAugment; +import io.xpipe.app.fxcomps.augment.GrowAugment; +import io.xpipe.app.fxcomps.impl.*; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.util.DesktopHelper; +import io.xpipe.app.util.ThreadHelper; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; +import javafx.css.PseudoClass; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Region; +import lombok.SneakyThrows; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.ArrayList; + +public class StandardStoreEntryComp extends SimpleComp { + + public static Comp customSection(StoreEntryWrapper e) { + var prov = e.getEntry().getProvider(); + if (prov != null) { + return prov.customDisplay(e); + } else { + return new StandardStoreEntryComp(e); + } + } + + private static final double NAME_WIDTH = 0.30; + private static final double STORE_TYPE_WIDTH = 0.08; + private static final double DETAILS_WIDTH = 0.52; + private static final double BUTTONS_WIDTH = 0.1; + private static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed"); + private static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete"); + private final StoreEntryWrapper entry; + + public StandardStoreEntryComp(StoreEntryWrapper entry) { + this.entry = entry; + } + + private Label createInformation() { + var information = new Label(); + information.textProperty().bind(PlatformThread.sync(entry.getInformation())); + information.getStyleClass().add("information"); + AppFont.header(information); + return information; + } + + private Label createSummary() { + var summary = new Label(); + summary.textProperty().bind(PlatformThread.sync(entry.getSummary())); + summary.getStyleClass().add("summary"); + AppFont.small(summary); + return summary; + } + + private void applyState(Node node) { + SimpleChangeListener.apply(PlatformThread.sync(entry.getState()), val -> { + switch (val) { + case LOAD_FAILED -> { + node.pseudoClassStateChanged(FAILED, true); + node.pseudoClassStateChanged(INCOMPLETE, false); + } + case INCOMPLETE -> { + node.pseudoClassStateChanged(FAILED, false); + node.pseudoClassStateChanged(INCOMPLETE, true); + } + default -> { + node.pseudoClassStateChanged(FAILED, false); + node.pseudoClassStateChanged(INCOMPLETE, false); + } + } + }); + } + + private Comp createName() { + var name = new LabelComp(entry.nameProperty()) + .apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS)) + .apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0))); + name.apply(s -> AppFont.header(s.get())); + return name; + } + + private Node createIcon() { + var img = entry.isDisabled() + ? "disabled_icon.png" + : entry.getEntry() + .getProvider() + .getDisplayIconFileName(entry.getEntry().getStore()); + var imageComp = new PrettyImageComp(new SimpleStringProperty(img), 55, 45); + var storeIcon = imageComp.createRegion(); + storeIcon.getStyleClass().add("icon"); + if (entry.getState().getValue().isUsable()) { + new FancyTooltipAugment<>(new SimpleStringProperty( + entry.getEntry().getProvider().getDisplayName())) + .augment(storeIcon); + } + return storeIcon; + } + + protected Region createContent() { + var name = createName().createRegion(); + + var size = createInformation(); + + var date = new Label(); + date.textProperty().bind(AppI18n.readableDuration("usedDate", PlatformThread.sync(entry.lastAccessProperty()))); + AppFont.small(date); + date.getStyleClass().add("date"); + + var grid = new GridPane(); + + var storeIcon = createIcon(); + + grid.getColumnConstraints() + .addAll( + createShareConstraint(grid, STORE_TYPE_WIDTH), createShareConstraint(grid, NAME_WIDTH), + createShareConstraint(grid, DETAILS_WIDTH), createShareConstraint(grid, BUTTONS_WIDTH)); + grid.add(storeIcon, 0, 0, 1, 2); + grid.add(name, 1, 0); + grid.add(date, 1, 1); + grid.add(createSummary(), 2, 1); + grid.add(createInformation(), 2, 0); + grid.add(createButtonBar().createRegion(), 3, 0, 1, 2); + grid.setVgap(5); + GridPane.setHalignment(storeIcon, HPos.CENTER); + + AppFont.small(size); + AppFont.small(date); + + grid.getStyleClass().add("store-entry-grid"); + + applyState(grid); + + var button = new JFXButton(); + button.setGraphic(grid); + GrowAugment.create(true, false).augment(new SimpleCompStructure<>(grid)); + button.getStyleClass().add("store-entry-comp"); + button.setMaxWidth(2000); + button.setFocusTraversable(true); + button.accessibleTextProperty() + .bind(Bindings.createStringBinding( + () -> { + return entry.getName(); + }, + entry.nameProperty())); + button.accessibleHelpProperty().bind(entry.getInformation()); + button.setOnAction(event -> { + event.consume(); + ThreadHelper.runFailableAsync(() -> { + entry.refreshIfNeeded(); + entry.executeDefaultAction(); + }); + }); + + new ContextMenuAugment<>(() -> StandardStoreEntryComp.this.createContextMenu()) + .augment(new SimpleCompStructure<>(button)); + + return button; + } + + protected Comp createButtonBar() { + var list = new ArrayList>(); + for (var p : entry.getActionProviders().entrySet()) { + var actionProvider = p.getKey().getDataStoreCallSite(); + if (!actionProvider.isMajor() + || p.getKey().equals(entry.getDefaultActionProvider().getValue())) { + continue; + } + + var button = new IconButtonComp( + actionProvider.getIcon(entry.getEntry().getStore().asNeeded()), () -> { + ThreadHelper.runFailableAsync(() -> { + var action = actionProvider.createAction( + entry.getEntry().getStore().asNeeded()); + action.execute(); + }); + }); + button.apply(new FancyTooltipAugment<>( + actionProvider.getName(entry.getEntry().getStore().asNeeded()))); + if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { + button.hide(Bindings.not(p.getValue())); + } else if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_SHOW) { + button.disable(Bindings.not(p.getValue())); + } + list.add(button); + } + + var settingsButton = createSettingsButton(); + list.add(settingsButton); + return new HorizontalComp(list) + .apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT)) + .apply(struc -> { + for (Node child : struc.get().getChildren()) { + ((Region) child) + .prefWidthProperty() + .bind((struc.get().heightProperty().divide(1.7))); + ((Region) child).prefHeightProperty().bind((struc.get().heightProperty())); + } + }); + } + + protected Comp createSettingsButton() { + var settingsButton = new IconButtonComp("mdomz-settings"); + settingsButton.styleClass("settings"); + settingsButton.accessibleText("Settings"); + settingsButton.apply(new ContextMenuAugment<>( + event -> event.getButton() == MouseButton.PRIMARY, () -> StandardStoreEntryComp.this.createContextMenu())); + settingsButton.apply(GrowAugment.create(false, true)); + settingsButton.apply(s -> { + s.get().prefWidthProperty().bind(Bindings.divide(s.get().heightProperty(), 1.35)); + }); + settingsButton.apply(new FancyTooltipAugment<>("more")); + return settingsButton; + } + + protected ContextMenu createContextMenu() { + var contextMenu = new ContextMenu(); + AppFont.normal(contextMenu.getStyleableNode()); + + for (var p : entry.getActionProviders().entrySet()) { + var actionProvider = p.getKey().getDataStoreCallSite(); + if (actionProvider.isMajor()) { + continue; + } + + var name = actionProvider.getName(entry.getEntry().getStore().asNeeded()); + var icon = actionProvider.getIcon(entry.getEntry().getStore().asNeeded()); + var item = new MenuItem(null, new FontIcon(icon)); + item.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + var action = actionProvider.createAction( + entry.getEntry().getStore().asNeeded()); + action.execute(); + }); + }); + item.textProperty().bind(name); + if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { + item.visibleProperty().bind(p.getValue()); + } else if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_SHOW) { + item.disableProperty().bind(Bindings.not(p.getValue())); + } + contextMenu.getItems().add(item); + } + + if (entry.getActionProviders().size() > 0) { + contextMenu.getItems().add(new SeparatorMenuItem()); + } + + if (AppPrefs.get().developerMode().getValue()) { + var browse = new MenuItem(AppI18n.get("browse"), new FontIcon("mdi2f-folder-open-outline")); + browse.setOnAction( + event -> DesktopHelper.browsePath(entry.getEntry().getDirectory())); + contextMenu.getItems().add(browse); + } + + var refresh = new MenuItem(AppI18n.get("refresh"), new FontIcon("mdal-360")); + refresh.disableProperty().bind(entry.getRefreshable().not()); + refresh.setOnAction(event -> { + DataStorage.get().refreshAsync(entry.getEntry(), true); + }); + contextMenu.getItems().add(refresh); + + var del = new MenuItem(AppI18n.get("delete"), new FontIcon("mdal-delete_outline")); + del.disableProperty().bind(entry.getDeletable().not()); + del.setOnAction(event -> entry.delete()); + contextMenu.getItems().add(del); + + return contextMenu; + } + + protected ColumnConstraints createShareConstraint(Region r, double share) { + var cc = new ColumnConstraints(); + cc.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> r.getWidth() * share, r.widthProperty())); + return cc; + } + + @SneakyThrows + @Override + protected Region createSimple() { + var loading = new LoadingOverlayComp(Comp.of(() -> createContent()), entry.getLoading()); + var region = loading.createRegion(); + return region; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java index fb561117c..8cffc71bb 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java @@ -1,54 +1,59 @@ package io.xpipe.app.comp.storage.store; -import com.jfoenix.controls.JFXButton; -import io.xpipe.app.comp.base.LoadingOverlayComp; +import atlantafx.base.theme.Styles; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; -import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.*; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.DesktopHelper; import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.store.FixedHierarchyStore; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.css.PseudoClass; -import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.input.MouseButton; import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.GridPane; import javafx.scene.layout.Region; -import lombok.SneakyThrows; import org.kordamp.ikonli.javafx.FontIcon; import java.util.ArrayList; -public class StoreEntryComp extends SimpleComp { +public abstract class StoreEntryComp extends SimpleComp { - private static final double NAME_WIDTH = 0.30; - private static final double STORE_TYPE_WIDTH = 0.08; - private static final double DETAILS_WIDTH = 0.52; - private static final double BUTTONS_WIDTH = 0.1; - private static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed"); - private static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete"); - private final StoreEntryWrapper entry; + public static Comp customSection(StoreEntryWrapper e) { + var prov = e.getEntry().getProvider(); + if (prov != null) { + return prov.customDisplay(e); + } else { + return new StandardStoreEntryComp(e); + } + } + + public static final double NAME_WIDTH = 0.30; + public static final double STORE_TYPE_WIDTH = 0.08; + public static final double DETAILS_WIDTH = 0.52; + public static final double BUTTONS_WIDTH = 0.1; + public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed"); + public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete"); + protected final StoreEntryWrapper entry; public StoreEntryComp(StoreEntryWrapper entry) { this.entry = entry; } - private Label createInformation() { + protected Label createInformation() { var information = new Label(); information.textProperty().bind(PlatformThread.sync(entry.getInformation())); information.getStyleClass().add("information"); @@ -56,7 +61,7 @@ public class StoreEntryComp extends SimpleComp { return information; } - private Label createSummary() { + protected Label createSummary() { var summary = new Label(); summary.textProperty().bind(PlatformThread.sync(entry.getSummary())); summary.getStyleClass().add("summary"); @@ -64,7 +69,7 @@ public class StoreEntryComp extends SimpleComp { return summary; } - private void applyState(Node node) { + protected void applyState(Node node) { SimpleChangeListener.apply(PlatformThread.sync(entry.getState()), val -> { switch (val) { case LOAD_FAILED -> { @@ -83,21 +88,41 @@ public class StoreEntryComp extends SimpleComp { }); } - private Comp createName() { - var name = new LabelComp(entry.nameProperty()) - .apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS)) + protected Comp createName() { + var filtered = BindingsHelper.filteredContentBinding( + StoreViewState.get().getAllEntries(), + other -> other.getEntry().getState().isUsable() + && entry.getEntry() + .getStore() + .equals(other.getEntry() + .getProvider() + .getLogicalParent(other.getEntry().getStore()))); + LabelComp name = new LabelComp(Bindings.createStringBinding( + () -> { + return entry.getName() + + (entry.getInformation().get() != null + ? " [" + entry.getInformation().get() + "]" + : "") + + (filtered.size() > 0 && entry.getEntry().getStore() instanceof FixedHierarchyStore + ? " (" + filtered.size() + ")" + : ""); + }, + entry.nameProperty(), + entry.getInformation(), + filtered)); + name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS)) .apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0))); name.apply(s -> AppFont.header(s.get())); return name; } - private Node createIcon() { + protected Node createIcon(int w, int h) { var img = entry.isDisabled() ? "disabled_icon.png" : entry.getEntry() .getProvider() .getDisplayIconFileName(entry.getEntry().getStore()); - var imageComp = new PrettyImageComp(new SimpleStringProperty(img), 55, 45); + var imageComp = new PrettyImageComp(new SimpleStringProperty(img), w, h); var storeIcon = imageComp.createRegion(); storeIcon.getStyleClass().add("icon"); if (entry.getState().getValue().isUsable()) { @@ -108,68 +133,7 @@ public class StoreEntryComp extends SimpleComp { return storeIcon; } - protected Region createContent() { - var name = createName().createRegion(); - - var size = createInformation(); - - var date = new Label(); - date.textProperty().bind(AppI18n.readableDuration("usedDate", PlatformThread.sync(entry.lastAccessProperty()))); - AppFont.small(date); - date.getStyleClass().add("date"); - - var grid = new GridPane(); - - var storeIcon = createIcon(); - - grid.getColumnConstraints() - .addAll( - createShareConstraint(grid, STORE_TYPE_WIDTH), createShareConstraint(grid, NAME_WIDTH), - createShareConstraint(grid, DETAILS_WIDTH), createShareConstraint(grid, BUTTONS_WIDTH)); - grid.add(storeIcon, 0, 0, 1, 2); - grid.add(name, 1, 0); - grid.add(date, 1, 1); - grid.add(createSummary(), 2, 1); - grid.add(createInformation(), 2, 0); - grid.add(createButtonBar().createRegion(), 3, 0, 1, 2); - grid.setVgap(5); - GridPane.setHalignment(storeIcon, HPos.CENTER); - - AppFont.small(size); - AppFont.small(date); - - grid.getStyleClass().add("store-entry-grid"); - - applyState(grid); - - var button = new JFXButton(); - button.setGraphic(grid); - GrowAugment.create(true, false).augment(new SimpleCompStructure<>(grid)); - button.getStyleClass().add("store-entry-comp"); - button.setMaxWidth(2000); - button.setFocusTraversable(true); - button.accessibleTextProperty() - .bind(Bindings.createStringBinding( - () -> { - return entry.getName(); - }, - entry.nameProperty())); - button.accessibleHelpProperty().bind(entry.getInformation()); - button.setOnAction(event -> { - event.consume(); - ThreadHelper.runFailableAsync(() -> { - entry.refreshIfNeeded(); - entry.executeDefaultAction(); - }); - }); - - new ContextMenuAugment<>(() -> StoreEntryComp.this.createContextMenu()) - .augment(new SimpleCompStructure<>(button)); - - return button; - } - - private Comp createButtonBar() { + protected Comp createButtonBar() { var list = new ArrayList>(); for (var p : entry.getActionProviders().entrySet()) { var actionProvider = p.getKey().getDataStoreCallSite(); @@ -198,33 +162,35 @@ public class StoreEntryComp extends SimpleComp { var settingsButton = createSettingsButton(); list.add(settingsButton); + if (list.size() > 1) { + list.get(0).styleClass(Styles.LEFT_PILL); + for (int i = 1; i < list.size() - 1; i++) { + list.get(i).styleClass(Styles.CENTER_PILL); + } + list.get(list.size() - 1).styleClass(Styles.RIGHT_PILL); + } + list.forEach(comp -> { + comp.apply(struc -> struc.get().getStyleClass().remove(Styles.FLAT)); + }); return new HorizontalComp(list) - .apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT)) .apply(struc -> { - for (Node child : struc.get().getChildren()) { - ((Region) child) - .prefWidthProperty() - .bind((struc.get().heightProperty().divide(1.7))); - ((Region) child).prefHeightProperty().bind((struc.get().heightProperty())); - } - }); + struc.get().setAlignment(Pos.CENTER_RIGHT); + struc.get().setPadding(new Insets(5)); + }) + .styleClass("button-bar"); } - private Comp createSettingsButton() { + protected Comp createSettingsButton() { var settingsButton = new IconButtonComp("mdomz-settings"); settingsButton.styleClass("settings"); settingsButton.accessibleText("Settings"); settingsButton.apply(new ContextMenuAugment<>( event -> event.getButton() == MouseButton.PRIMARY, () -> StoreEntryComp.this.createContextMenu())); - settingsButton.apply(GrowAugment.create(false, true)); - settingsButton.apply(s -> { - s.get().prefWidthProperty().bind(Bindings.divide(s.get().heightProperty(), 1.35)); - }); settingsButton.apply(new FancyTooltipAugment<>("more")); return settingsButton; } - private ContextMenu createContextMenu() { + protected ContextMenu createContextMenu() { var contextMenu = new ContextMenu(); AppFont.normal(contextMenu.getStyleableNode()); @@ -279,17 +245,9 @@ public class StoreEntryComp extends SimpleComp { return contextMenu; } - private ColumnConstraints createShareConstraint(Region r, double share) { + protected ColumnConstraints createShareConstraint(Region r, double share) { var cc = new ColumnConstraints(); cc.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> r.getWidth() * share, r.widthProperty())); return cc; } - - @SneakyThrows - @Override - protected Region createSimple() { - var loading = new LoadingOverlayComp(Comp.of(() -> createContent()), entry.getLoading()); - var region = loading.createRegion(); - return region; - } } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java index 62b063a17..c55464bb5 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java @@ -23,7 +23,7 @@ public class StoreEntryListComp extends SimpleComp { .getFilterString() .map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s)))); var content = new ListBoxViewComp<>(filtered, topLevel.getChildren(), (StoreSection e) -> { - return new StoreEntrySection(e); + return StoreSection.customSection(e); }); return content.styleClass("store-list-comp").styleClass(Styles.STRIPED); } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySection.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySectionComp.java similarity index 85% rename from app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySection.java rename to app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySectionComp.java index df0e8b169..9fc3dfebe 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySection.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySectionComp.java @@ -14,17 +14,17 @@ import javafx.scene.paint.Color; import java.util.List; -public class StoreEntrySection extends Comp> { +public class StoreEntrySectionComp extends Comp> { private final StoreSection section; - public StoreEntrySection(StoreSection section) { + public StoreEntrySectionComp(StoreSection section) { this.section = section; } @Override public CompStructure createBase() { - var root = new StoreEntryComp(section.getWrapper()).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); + var root = StandardStoreEntryComp.customSection(section.getWrapper()).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); var button = new IconButtonComp( Bindings.createStringBinding( () -> section.getWrapper().getExpanded().get() @@ -51,7 +51,7 @@ public class StoreEntrySection extends Comp> { .getFilterString() .map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s)))); var content = new ListBoxViewComp<>(shown, all, (StoreSection e) -> { - return new StoreEntrySection(e).apply(GrowAugment.create(true, false)); + return StoreSection.customSection(e).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)))); @@ -62,7 +62,8 @@ public class StoreEntrySection extends Comp> { return padding; }); return new VerticalComp(List.of( - new HorizontalComp(topEntryList), + new HorizontalComp(topEntryList) + .apply(struc -> struc.get().setFillHeight(true)), new HorizontalComp(List.of(spacer, content)) .apply(struc -> struc.get().setFillHeight(true)) .hide(BindingsHelper.persist(Bindings.or( diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java index b238f9656..bf80465c3 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java @@ -8,6 +8,7 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.FixedHierarchyStore; import javafx.beans.property.*; import lombok.Getter; @@ -94,10 +95,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { disabled.setValue(entry.isDisabled()); state.setValue(entry.getState()); expanded.setValue(entry.isExpanded()); - information.setValue( - entry.getInformation() != null - ? entry.getInformation() - : entry.isDisabled() ? null : entry.getProvider().getDisplayName()); + information.setValue(entry.getInformation()); loading.setValue(entry.getState() == DataStoreEntry.State.VALIDATING); if (entry.getState().isUsable()) { @@ -178,6 +176,8 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { if (found != null) { entry.updateLastUsed(); found.createAction(entry.getStore().asNeeded()).execute(); + } else if (getEntry().getStore() instanceof FixedHierarchyStore) { + DataStorage.get().refreshChildren(entry); } } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java index 8cd4d7755..d7edfc283 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java @@ -1,6 +1,7 @@ package io.xpipe.app.comp.storage.store; 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; @@ -14,6 +15,15 @@ import java.util.Comparator; @Value public class StoreSection implements StorageFilter.Filterable { + public static Comp customSection(StoreSection e) { + var prov = e.getWrapper().getEntry().getProvider(); + if (prov != null) { + return prov.customContainer(e); + } else { + return new StoreEntrySectionComp(e); + } + } + StoreEntryWrapper wrapper; ObservableList children; @@ -36,7 +46,7 @@ public class StoreSection implements StorageFilter.Filterable { var parent = section.getWrapper() .getEntry() .getProvider() - .getParent(section.getWrapper().getEntry().getStore()); + .getLogicalParent(section.getWrapper().getEntry().getStore()); return parent == null || (DataStorage.get().getStoreEntryIfPresent(parent).isEmpty()); }); @@ -56,7 +66,7 @@ public class StoreSection implements StorageFilter.Filterable { .getStore() .equals(other.getEntry() .getProvider() - .getParent(other.getEntry().getStore()))); + .getLogicalParent(other.getEntry().getStore()))); var children = BindingsHelper.mappedContentBinding(filtered, entry1 -> create(entry1)); var ordered = BindingsHelper.orderedContentBinding(children, COMPARATOR); return new StoreSection(e, ordered); diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/ListStoresExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/ListStoresExchangeImpl.java index a672460e3..312a77e42 100644 --- a/app/src/main/java/io/xpipe/app/exchange/cli/ListStoresExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/cli/ListStoresExchangeImpl.java @@ -15,7 +15,7 @@ public class ListStoresExchangeImpl extends ListStoresExchange public Response handleRequest(BeaconHandler handler, Request msg) { DataStorage s = DataStorage.get(); var e = s.getStoreEntries().stream() - .filter(entry -> !entry.isDisabled() && entry.getProvider().shouldShow()) + .filter(entry -> !entry.isDisabled() && entry.getProvider().canManuallyCreate()) .sorted(Comparator.comparing(dataStoreEntry -> dataStoreEntry.getLastUsed())) .map(col -> StoreListEntry.builder() .name(col.getName()) diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java index 81809a6f9..ee1db68c3 100644 --- a/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java @@ -24,7 +24,7 @@ public class StoreProviderListExchangeImpl extends StoreProviderListExchange .map(p -> ProviderEntry.builder() .id(p.getId()) .description(p.getDisplayDescription()) - .hidden(!p.shouldShow()) + .hidden(!p.canManuallyCreate()) .build()) .toList())); diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index 810436455..a002cbdb7 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -1,13 +1,14 @@ package io.xpipe.app.ext; import io.xpipe.app.comp.base.MarkdownComp; +import io.xpipe.app.comp.storage.store.StandardStoreEntryComp; +import io.xpipe.app.comp.storage.store.StoreEntrySectionComp; +import io.xpipe.app.comp.storage.store.StoreEntryWrapper; +import io.xpipe.app.comp.storage.store.StoreSection; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.core.dialog.Dialog; -import io.xpipe.core.store.DataStore; -import io.xpipe.core.store.FileSystem; -import io.xpipe.core.store.ShellStore; -import io.xpipe.core.store.StreamDataStore; +import io.xpipe.core.store.*; import io.xpipe.core.util.JacksonizedValue; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; @@ -31,6 +32,14 @@ public interface DataStoreProvider { } } + default Comp customDisplay(StoreEntryWrapper w) { + return new StandardStoreEntryComp(w); + } + + default Comp customContainer(StoreSection section) { + return new StoreEntrySectionComp(section); + } + default Comp createInsightsComp(ObservableValue store) { var content = Bindings.createStringBinding( () -> { @@ -77,10 +86,14 @@ public interface DataStoreProvider { return DisplayCategory.OTHER; } - default DataStore getParent(DataStore store) { + default DataStore getLogicalParent(DataStore store) { return null; } + default DataStore getDisplayParent(DataStore store) { + return getLogicalParent(store); + } + default GuiDialog guiDialog(Property store) { return null; } @@ -129,7 +142,13 @@ public interface DataStoreProvider { return null; } - DataStore defaultStore(); + default boolean requiresFrequentRefresh() { + return getStoreClasses().stream().anyMatch(aClass -> FixedHierarchyStore.class.isAssignableFrom(aClass)); + } + + default DataStore defaultStore() { + return null; + } List getPossibleNames(); @@ -139,7 +158,7 @@ public interface DataStoreProvider { List> getStoreClasses(); - default boolean shouldShow() { + default boolean canManuallyCreate() { return true; } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java index 9c11f1f23..78bcf5fa9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java @@ -1,6 +1,6 @@ package io.xpipe.app.fxcomps.impl; -import com.jfoenix.controls.JFXButton; +import atlantafx.base.theme.Styles; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; @@ -9,9 +9,10 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.css.Size; import javafx.css.SizeUnits; +import javafx.scene.control.Button; import org.kordamp.ikonli.javafx.FontIcon; -public class IconButtonComp extends Comp> { +public class IconButtonComp extends Comp> { private final ObservableValue icon; private final Runnable listener; @@ -30,8 +31,9 @@ public class IconButtonComp extends Comp> { } @Override - public CompStructure createBase() { - var button = new JFXButton(); + public CompStructure