Merge branch 'batch' into 16-release

This commit is contained in:
crschnick
2025-03-05 19:12:22 +00:00
parent d2ae1c64f5
commit 155eed8564
36 changed files with 1229 additions and 269 deletions

View File

@@ -101,7 +101,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
.grow(true, false)
.accessibleTextKey("restoreAllSessions");
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.separator(), tile));
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.hseparator(), tile));
layout.styleClass("welcome");
layout.spacing(14);
layout.maxWidth(1000);

View File

@@ -58,10 +58,14 @@ public abstract class Comp<S extends CompStructure<?>> {
};
}
public static Comp<CompStructure<Separator>> separator() {
public static Comp<CompStructure<Separator>> hseparator() {
return of(() -> new Separator(Orientation.HORIZONTAL));
}
public static Comp<CompStructure<Separator>> vseparator() {
return of(() -> new Separator(Orientation.VERTICAL));
}
@SuppressWarnings("unchecked")
public static <IR extends Region, SIN extends CompStructure<IR>, OR extends Region> Comp<CompStructure<OR>> derive(
Comp<SIN> comp, Function<IR, OR> r) {

View File

@@ -88,7 +88,7 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
if (!hide.get()) {
var cm = contextMenu.get();
if (cm != null) {
cm.show(r, Side.BOTTOM, 0, 0);
cm.show(r, Side.TOP, 0, 0);
currentContextMenu.set(cm);
}
}

View File

@@ -4,17 +4,26 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.List;
public class HorizontalComp extends Comp<CompStructure<HBox>> {
private final List<Comp<?>> entries;
private final ObservableList<Comp<?>> entries;
public HorizontalComp(List<Comp<?>> comps) {
entries = List.copyOf(comps);
entries = FXCollections.observableArrayList(List.copyOf(comps));
}
public HorizontalComp(ObservableList<Comp<?>> entries) {
this.entries = PlatformThread.sync(entries);
}
public Comp<CompStructure<HBox>> spacing(double spacing) {
@@ -23,8 +32,11 @@ public class HorizontalComp extends Comp<CompStructure<HBox>> {
@Override
public CompStructure<HBox> createBase() {
HBox b = new HBox();
var b = new HBox();
b.getStyleClass().add("horizontal-comp");
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
b.getChildren().setAll(c.getList().stream().map(Comp::createRegion).toList());
});
for (var entry : entries) {
b.getChildren().add(entry.createRegion());
}

View File

@@ -0,0 +1,40 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.HBox;
import java.util.List;
public class ToolbarComp extends Comp<CompStructure<ToolBar>> {
private final ObservableList<Comp<?>> entries;
public ToolbarComp(List<Comp<?>> comps) {
entries = FXCollections.observableArrayList(List.copyOf(comps));
}
public ToolbarComp(ObservableList<Comp<?>> entries) {
this.entries = PlatformThread.sync(entries);
}
@Override
public CompStructure<ToolBar> createBase() {
var b = new ToolBar();
b.getStyleClass().add("horizontal-comp");
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
b.getItems().setAll(c.getList().stream().map(Comp::createRegion).toList());
});
for (var entry : entries) {
b.getItems().add(entry.createRegion());
}
return new SimpleCompStructure<>(b);
}
}

View File

@@ -8,6 +8,7 @@ import javafx.beans.binding.Bindings;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
@@ -71,9 +72,20 @@ public class DenseStoreEntryComp extends StoreEntryComp {
var notes = new StoreNotesComp(getWrapper()).createRegion();
var userIcon = createUserIcon().createRegion();
var selection = createBatchSelection().createRegion();
grid.add(selection, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(25));
StoreViewState.get().getBatchMode().subscribe(batch -> {
if (batch) {
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
} else {
grid.getColumnConstraints().set(0, new ColumnConstraints(-8));
}
});
var storeIcon = createIcon(28, 24);
GridPane.setHalignment(storeIcon, HPos.CENTER);
grid.add(storeIcon, 0, 0);
grid.add(storeIcon, 1, 0);
grid.getColumnConstraints().add(new ColumnConstraints(34));
var customSize = content != null ? 100 : 0;

View File

@@ -38,15 +38,26 @@ public class StandardStoreEntryComp extends StoreEntryComp {
grid.setHgap(6);
grid.setVgap(OsType.getLocal() == OsType.MACOS ? 2 : 0);
var selection = createBatchSelection();
grid.add(selection.createRegion(), 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(25));
StoreViewState.get().getBatchMode().subscribe(batch -> {
if (batch) {
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
} else {
grid.getColumnConstraints().set(0, new ColumnConstraints(-6));
}
});
var storeIcon = createIcon(46, 40);
grid.add(storeIcon, 0, 0, 1, 2);
grid.add(storeIcon, 1, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(52));
var active = new StoreActiveComp(getWrapper()).createRegion();
var nameBox = new HBox(name, userIcon, notes);
nameBox.setSpacing(6);
nameBox.setAlignment(Pos.CENTER_LEFT);
grid.add(nameBox, 1, 0);
grid.add(nameBox, 2, 0);
GridPane.setVgrow(nameBox, Priority.ALWAYS);
getWrapper().getSessionActive().subscribe(aBoolean -> {
if (!aBoolean) {
@@ -59,7 +70,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
var summaryBox = new HBox(createSummary());
summaryBox.setAlignment(Pos.TOP_LEFT);
GridPane.setVgrow(summaryBox, Priority.ALWAYS);
grid.add(summaryBox, 1, 1);
grid.add(summaryBox, 2, 1);
var nameCC = new ColumnConstraints();
nameCC.setMinWidth(100);
@@ -67,7 +78,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
nameCC.setPrefWidth(100);
grid.getColumnConstraints().addAll(nameCC);
grid.add(createInformation(), 2, 0, 1, 2);
grid.add(createInformation(), 3, 0, 1, 2);
var info = new ColumnConstraints();
info.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);
info.setHalignment(HPos.LEFT);
@@ -84,7 +95,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
controls.setAlignment(Pos.CENTER_RIGHT);
controls.setSpacing(10);
controls.setPadding(new Insets(0, 0, 0, 10));
grid.add(controls, 3, 0, 1, 2);
grid.add(controls, 4, 0, 1, 2);
grid.getColumnConstraints().add(custom);
grid.getStyleClass().add("store-entry-grid");

View File

@@ -0,0 +1,66 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
public class StoreEntryBatchSelectComp extends SimpleComp {
private final StoreSection section;
public StoreEntryBatchSelectComp(StoreSection section) {this.section = section;}
@Override
protected Region createSimple() {
var cb = new CheckBox();
cb.setAllowIndeterminate(true);
cb.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
StoreViewState.get().selectBatchMode(section);
} else {
StoreViewState.get().unselectBatchMode(section);
}
});
StoreViewState.get().getBatchModeSelection().getList().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
Platform.runLater(() -> {
update(cb);
});
});
section.getShownChildren().getList().addListener((ListChangeListener<? super StoreSection>) c -> {
if (cb.isSelected()) {
StoreViewState.get().selectBatchMode(section);
} else {
StoreViewState.get().unselectBatchMode(section);
}
});
cb.getStyleClass().add("batch-mode-selector");
cb.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
cb.setSelected(!cb.isSelected());
event.consume();
}
});
return cb;
}
private void update(CheckBox checkBox) {
var isSelected = StoreViewState.get().isSectionSelected(section);
checkBox.setSelected(isSelected);
if (section.getShownChildren().getList().size() == 0) {
checkBox.setIndeterminate(false);
return;
}
var count = section.getShownChildren().getList().stream().filter(c -> StoreViewState.get().getBatchModeSelection().getList().contains(c.getWrapper())).count();
checkBox.setIndeterminate(count > 0 && count != section.getShownChildren().getList().size());
return;
}
}

View File

@@ -86,6 +86,7 @@ public abstract class StoreEntryComp extends SimpleComp {
var r = createContent();
var buttonBar = r.lookup(".button-bar");
var iconChooser = r.lookup(".icon");
var batchMode = r.lookup(".batch-mode-selector");
var button = new Button();
button.setGraphic(r);
@@ -103,6 +104,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|| NodeHelper.isParent(batchMode, event.getTarget())
|| NodeHelper.isParent(buttonBar, event.getTarget());
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
@@ -116,6 +118,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|| NodeHelper.isParent(batchMode, event.getTarget())
|| NodeHelper.isParent(buttonBar, event.getTarget());
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
@@ -274,6 +277,12 @@ public abstract class StoreEntryComp extends SimpleComp {
return settingsButton;
}
protected Comp<?> createBatchSelection() {
var c = new StoreEntryBatchSelectComp(section);
c.hide(StoreViewState.get().getBatchMode().not());
return c;
}
protected ContextMenu createContextMenu() {
var contextMenu = ContextMenuHelper.create();

View File

@@ -2,17 +2,22 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppLayoutModel;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.LinkedHashMap;
import java.util.List;
public class StoreEntryListComp extends SimpleComp {
@@ -48,7 +53,15 @@ public class StoreEntryListComp extends SimpleComp {
struc.get().setVvalue(0);
});
});
return content.styleClass("store-list-comp");
content.styleClass("store-list-comp");
content.vgrow();
var statusBar = new StoreEntryListStatusBarComp();
statusBar.apply(struc -> {
VBox.setMargin(struc.get(), new Insets(3, 6, 4, 2));
});
statusBar.hide(StoreViewState.get().getBatchMode().not());
return new VerticalComp(List.of(content, statusBar));
}
@Override

View File

@@ -1,5 +1,6 @@
package io.xpipe.app.comp.store;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.CountComp;
@@ -91,24 +92,30 @@ public class StoreEntryListOverviewComp extends SimpleComp {
StoreViewState.get().getFilterString().setValue(newValue);
});
});
var filter = new FilterComp(StoreViewState.get().getFilterString());
var f = filter.createRegion();
var button = createAddButton();
var hbox = new HBox(button, f);
f.minHeightProperty().bind(button.heightProperty());
f.prefHeightProperty().bind(button.heightProperty());
f.maxHeightProperty().bind(button.heightProperty());
var filter = new FilterComp(StoreViewState.get().getFilterString()).createRegion();
var add = createAddButton();
var batchMode = createBatchModeButton().createRegion();
var hbox = new HBox(add, filter, batchMode);
filter.minHeightProperty().bind(add.heightProperty());
filter.prefHeightProperty().bind(add.heightProperty());
filter.maxHeightProperty().bind(add.heightProperty());
batchMode.minHeightProperty().bind(add.heightProperty());
batchMode.prefHeightProperty().bind(add.heightProperty());
batchMode.maxHeightProperty().bind(add.heightProperty());
batchMode.minWidthProperty().bind(add.heightProperty());
batchMode.prefWidthProperty().bind(add.heightProperty());
batchMode.maxWidthProperty().bind(add.heightProperty());
hbox.setSpacing(8);
hbox.setAlignment(Pos.CENTER);
HBox.setHgrow(f, Priority.ALWAYS);
HBox.setHgrow(filter, Priority.ALWAYS);
f.getStyleClass().add("filter-bar");
filter.getStyleClass().add("filter-bar");
return hbox;
}
private Region createAddButton() {
var menu = new MenuButton(null, new FontIcon("mdi2p-plus-thick"));
menu.textProperty().bind(AppI18n.observable("addConnections"));
menu.textProperty().bind(AppI18n.observable("new"));
menu.setAlignment(Pos.CENTER);
menu.setTextAlignment(TextAlignment.CENTER);
StoreCreationMenu.addButtons(menu);
@@ -124,6 +131,29 @@ public class StoreEntryListOverviewComp extends SimpleComp {
return menu;
}
private Comp<?> createBatchModeButton() {
var batchMode = StoreViewState.get().getBatchMode();
var b = new IconButtonComp("mdi2l-layers", () -> {
batchMode.setValue(!batchMode.getValue());
});
b.apply(struc -> {
struc
.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
if (batchMode.getValue()) {
return 1.0;
}
return 0.4;
},
batchMode));
struc.get().getStyleClass().remove(Styles.FLAT);
});
return b;
}
private Comp<?> createAlphabeticalSortButton() {
var sortMode = StoreViewState.get().getSortMode();
var icon = Bindings.createObjectBinding(

View File

@@ -0,0 +1,176 @@
package io.xpipe.app.comp.store;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import java.util.ArrayList;
import java.util.List;
public class StoreEntryListStatusBarComp extends SimpleComp {
@Override
protected Region createSimple() {
var checkbox = new StoreEntryBatchSelectComp(StoreViewState.get().getCurrentTopLevelSection());
var l = new LabelComp(Bindings.createStringBinding(() -> {
return AppI18n.get("connectionsSelected", StoreViewState.get().getEffectiveBatchModeSelection().getList().size());
}, StoreViewState.get().getEffectiveBatchModeSelection().getList(), AppI18n.activeLanguage()));
l.minWidth(Region.USE_PREF_SIZE);
l.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
});
var actions = new ToolbarComp(createActions());
var close = new IconButtonComp("mdi2c-close", () -> {
StoreViewState.get().getBatchMode().setValue(false);
});
close.apply(struc -> {
struc.get().getStyleClass().remove(Styles.FLAT);
struc.get().minWidthProperty().bind(struc.get().heightProperty());
struc.get().prefWidthProperty().bind(struc.get().heightProperty());
struc.get().maxWidthProperty().bind(struc.get().heightProperty());
});
var bar = new HorizontalComp(List.of(checkbox, Comp.hspacer(12), l, Comp.hspacer(20), actions, Comp.hspacer(), Comp.hspacer(20), close));
bar.apply(struc -> {
struc.get().setFillHeight(true);
struc.get().setAlignment(Pos.CENTER_LEFT);
});
bar.minHeight(40);
bar.prefHeight(40);
bar.styleClass("bar");
bar.styleClass("store-entry-list-status-bar");
return bar.createRegion();
}
private ObservableList<Comp<?>> createActions() {
var l = new DerivedObservableList<ActionProvider>(FXCollections.observableArrayList(), true);
StoreViewState.get().getEffectiveBatchModeSelection().getList().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
l.setContent(getCompatibleActionProviders());
});
return l.<Comp<?>>mapped(actionProvider -> {
return buildButton(actionProvider);
}).getList();
}
private List<ActionProvider> getCompatibleActionProviders() {
var l = StoreViewState.get().getEffectiveBatchModeSelection().getList();
if (l.isEmpty()) {
return List.of();
}
var all = new ArrayList<>(ActionProvider.ALL);
for (StoreEntryWrapper w : l) {
var actions = ActionProvider.ALL.stream().filter(actionProvider -> {
var s = actionProvider.getBatchDataStoreCallSite();
if (s == null) {
return false;
}
if (!s.getApplicableClass().isAssignableFrom(w.getStore().getValue().getClass())) {
return false;
}
if (!s.isApplicable(w.getEntry().ref())) {
return false;
}
return true;
}).toList();
all.removeIf(actionProvider -> !actions.contains(actionProvider));
}
return all;
}
@SuppressWarnings("unchecked")
private <T extends DataStore> Comp<?> buildButton(ActionProvider p) {
ActionProvider.BatchDataStoreCallSite<T> s = (ActionProvider.BatchDataStoreCallSite<T>) p.getBatchDataStoreCallSite();
if (s == null) {
return Comp.empty();
}
List<DataStoreEntryRef<T>> childrenRefs = StoreViewState.get().getEffectiveBatchModeSelection().getList().stream().map(
storeEntryWrapper -> storeEntryWrapper.getEntry().<T>ref()).toList();
var batchActions = s.getChildren(childrenRefs);
var button = new ButtonComp(s.getName(), new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(s.getIcon())), () -> {
if (batchActions.size() > 0) {
return;
}
runActions(s);
});
if (batchActions.size() > 0) {
button.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
var cm = ContextMenuHelper.create();
s.getChildren(childrenRefs)
.forEach(childProvider -> {
var menu = buildMenuItemForAction(childrenRefs, childProvider);
cm.getItems().add(menu);
});
return cm;
}));
}
return button;
}
@SuppressWarnings("unchecked")
private <T extends DataStore> MenuItem buildMenuItemForAction(List<DataStoreEntryRef<T>> batch, ActionProvider a) {
ActionProvider.BatchDataStoreCallSite<T> s = (ActionProvider.BatchDataStoreCallSite<T>) a.getBatchDataStoreCallSite();
var name = s.getName();
var icon = s.getIcon();
var children = s.getChildren(batch);
if (children.size() > 0) {
var menu = new Menu();
menu.textProperty().bind(name);
menu.setGraphic(new LabelGraphic.IconGraphic(icon).createGraphicNode());
var items = children.stream()
.filter(actionProvider -> actionProvider.getBatchDataStoreCallSite() != null)
.map(c -> buildMenuItemForAction(batch, c))
.toList();
menu.getItems().addAll(items);
return menu;
} else {
var item = new MenuItem();
item.textProperty().bind(name);
item.setGraphic(new LabelGraphic.IconGraphic(icon).createGraphicNode());
item.setOnAction(event -> {
runActions(s);
event.consume();
if (event.getTarget() instanceof Menu m) {
m.getParentPopup().hide();
}
});
return item;
}
}
@SuppressWarnings("unchecked")
private <T extends DataStore> void runActions(ActionProvider.BatchDataStoreCallSite<?> s) {
ThreadHelper.runFailableAsync(() -> {
var l = new ArrayList<>( StoreViewState.get().getEffectiveBatchModeSelection().getList());
var mapped = l.stream().map(w -> w.getEntry().<T>ref()).toList();
var action = ((ActionProvider.BatchDataStoreCallSite<T>) s).createAction(mapped);
if (action != null) {
action.execute();
}
});
}
}

View File

@@ -21,7 +21,7 @@ public class StoreLayoutComp extends SimpleComp {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
})
.createStructure();
struc.getLeft().setMinWidth(260);
struc.getLeft().setMinWidth(270);
struc.getLeft().setMaxWidth(500);
struc.get().getStyleClass().add("store-layout");
InputHelper.onKeyCombination(

View File

@@ -156,7 +156,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
section.getShownChildren().getList());
var full = new VerticalComp(List.of(
topEntryList,
Comp.separator().hide(expanded.not()),
Comp.hseparator().hide(expanded.not()),
content.styleClass("children-content")
.hide(Bindings.or(
Bindings.not(section.getWrapper().getExpanded()),

View File

@@ -1,6 +1,7 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.ext.DataStoreUsageCategory;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@@ -14,6 +15,8 @@ import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.util.*;
@@ -42,6 +45,23 @@ public class StoreViewState {
@Getter
private final Property<StoreSortMode> sortMode = new SimpleObjectProperty<>();
@Getter
private final BooleanProperty batchMode = new SimpleBooleanProperty(true);
@Getter
private final DerivedObservableList<StoreEntryWrapper> batchModeSelection = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
@Getter
private final DerivedObservableList<StoreEntryWrapper> effectiveBatchModeSelection = batchModeSelection.filtered(storeEntryWrapper -> {
if (!storeEntryWrapper.getValidity().getValue().isUsable()) {
return false;
}
if (storeEntryWrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {
return false;
}
return true;
});
@Getter
private StoreSection currentTopLevelSection;
@@ -60,6 +80,7 @@ public class StoreViewState {
INSTANCE.initSections();
INSTANCE.updateContent();
INSTANCE.initFilterListener();
INSTANCE.initBatchListener();
}
public static void reset() {
@@ -80,6 +101,36 @@ public class StoreViewState {
return INSTANCE;
}
public void selectBatchMode(StoreSection section) {
var wrapper = section.getWrapper();
if (wrapper != null && !batchModeSelection.getList().contains(wrapper)) {
batchModeSelection.getList().add(wrapper);
}
if (wrapper == null || (wrapper.getValidity().getValue().isUsable() && wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
section.getShownChildren().getList().forEach(c -> selectBatchMode(c));
}
}
public void unselectBatchMode(StoreSection section) {
var wrapper = section.getWrapper();
if (wrapper != null) {
batchModeSelection.getList().remove(wrapper);
}
if (wrapper == null || (wrapper.getValidity().getValue().isUsable() && wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
section.getShownChildren().getList().forEach(c -> unselectBatchMode(c));
}
}
public boolean isSectionSelected(StoreSection section) {
if (section.getWrapper() == null) {
var batchSet = new HashSet<>(batchModeSelection.getList());
var childSet = section.getShownChildren().getList().stream().map(s -> s.getWrapper()).toList();
return batchSet.containsAll(childSet);
}
return getBatchModeSelection().getList().contains(section.getWrapper());
}
private void updateContent() {
categories.getList().forEach(c -> c.update());
allEntries.getList().forEach(e -> e.update());
@@ -115,6 +166,14 @@ public class StoreViewState {
});
}
private void initBatchListener() {
allEntries.getList().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
batchModeSelection.getList().removeIf(storeEntryWrapper -> {
return allEntries.getList().contains(storeEntryWrapper);
});
});
}
private void initContent() {
allEntries
.getList()

View File

@@ -46,7 +46,7 @@ public class AppLayoutModel {
}
public static void init() {
var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(260, 300));
var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(270, 300));
INSTANCE = new AppLayoutModel(state);
}

View File

@@ -52,6 +52,10 @@ public interface ActionProvider {
return null;
}
default BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return null;
}
default DefaultDataStoreCallSite<?> getDefaultDataStoreCallSite() {
return null;
}
@@ -191,6 +195,41 @@ public interface ActionProvider {
}
}
interface BatchDataStoreCallSite<T extends DataStore> {
ObservableValue<String> getName();
String getIcon();
Class<?> getApplicableClass();
default boolean isApplicable(DataStoreEntryRef<T> o) {
return true;
}
default Action createAction(List<DataStoreEntryRef<T>> stores) {
var individual = stores.stream().map(ref -> {
return createAction(ref);
}).filter(action -> action != null).toList();
return new Action() {
@Override
public void execute() throws Exception {
for (Action action : individual) {
action.execute();
}
}
};
}
default Action createAction(DataStoreEntryRef<T> store) {
return null;
}
default List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<T>> batch) {
return List.of();
}
}
class Loader implements ModuleLayerLoader {
@Override

View File

@@ -80,7 +80,7 @@ public class AboutCategory extends AppPrefsCategory {
protected Comp<?> create() {
var props = createProperties().padding(new Insets(0, 0, 0, 5));
var update = new UpdateCheckComp().grow(true, false);
return new VerticalComp(List.of(props, Comp.separator(), update, Comp.separator(), createLinks()))
return new VerticalComp(List.of(props, Comp.hseparator(), update, Comp.hseparator(), createLinks()))
.apply(s -> s.get().setFillWidth(true))
.apply(struc -> struc.get().setSpacing(15))
.styleClass("information")

View File

@@ -131,9 +131,9 @@ public class IconsCategory extends AppPrefsCategory {
var vbox = new VerticalComp(List.of(
Comp.vspacer(10),
box,
Comp.separator(),
Comp.hseparator(),
refreshButton,
Comp.separator(),
Comp.hseparator(),
addDirectoryButton,
addGitButton));
vbox.spacing(10);

View File

@@ -322,7 +322,7 @@ public class OptionsBuilder {
}
public OptionsBuilder separator() {
return addComp(Comp.separator());
return addComp(Comp.hseparator());
}
public OptionsBuilder name(String nameKey) {

View File

@@ -2,76 +2,44 @@ package io.xpipe.app.util;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.process.ShellTtyState;
import io.xpipe.core.process.SystemState;
import javafx.collections.ObservableList;
import java.util.List;
public class ScanDialog {
public static void showAsync(DataStoreEntry entry) {
ThreadHelper.runAsync(() -> {
var showForCon = entry == null
|| (entry.getStore() instanceof ShellStore
&& (!(entry.getStorePersistentState() instanceof SystemState systemState)
|| systemState.getTtyState() == null
|| systemState.getTtyState() == ShellTtyState.NONE));
if (showForCon) {
showForShellStore(entry);
}
});
}
public static void showForShellStore(DataStoreEntry initial) {
var action = new ScanDialogAction() {
@Override
public boolean scan(
ObservableList<ScanProvider.ScanOpportunity> all,
ObservableList<ScanProvider.ScanOpportunity> selected,
DataStoreEntry entry,
ShellControl sc) {
if (!sc.canHaveSubshells()) {
return false;
}
if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
return false;
}
if (sc.getTtyState() != ShellTtyState.NONE) {
return false;
}
var providers = ScanProvider.getAll();
for (ScanProvider scanProvider : providers) {
try {
// Previous scan operation could have exited the shell
sc.start();
ScanProvider.ScanOpportunity operation = scanProvider.create(entry, sc);
if (operation != null) {
if (!operation.isDisabled() && operation.isDefaultSelected()) {
selected.add(operation);
}
all.add(operation);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
return true;
}
};
show(initial, action);
var showForCon = entry == null
|| (entry.getStore() instanceof ShellStore
&& (!(entry.getStorePersistentState() instanceof SystemState systemState)
|| systemState.getTtyState() == null
|| systemState.getTtyState() == ShellTtyState.NONE));
if (showForCon) {
show(entry, ScanDialogAction.shellScanAction());
}
}
private static void show(DataStoreEntry initialStore, ScanDialogAction action) {
var comp = new ScanDialogComp(initialStore != null ? initialStore.ref() : null, action);
var comp = new ScanSingleDialogComp(initialStore != null ? initialStore.ref() : null, action);
var modal = ModalOverlay.of("scanAlertTitle", comp);
var button = new ModalButton(
"ok",
() -> {
comp.finish();
},
false,
true);
button.augment(r -> r.disableProperty().bind(PlatformThread.sync(comp.getBusy())));
modal.addButton(button);
modal.show();
}
public static void showMulti(List<DataStoreEntryRef<ShellStore>> entries, ScanDialogAction action) {
var comp = new ScanMultiDialogComp(entries, action);
var modal = ModalOverlay.of("scanAlertTitle", comp);
var button = new ModalButton(
"ok",

View File

@@ -1,13 +1,59 @@
package io.xpipe.app.util;
import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellTtyState;
import javafx.collections.ObservableList;
public interface ScanDialogAction {
static ScanDialogAction shellScanAction() {
var action = new ScanDialogAction() {
@Override
public boolean scan(
ObservableList<ScanProvider.ScanOpportunity> all,
ObservableList<ScanProvider.ScanOpportunity> selected,
DataStoreEntry entry,
ShellControl sc) {
if (!sc.canHaveSubshells()) {
return false;
}
if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
return false;
}
if (sc.getTtyState() != ShellTtyState.NONE) {
return false;
}
var providers = ScanProvider.getAll();
for (ScanProvider scanProvider : providers) {
try {
// Previous scan operation could have exited the shell
sc.start();
ScanProvider.ScanOpportunity operation = scanProvider.create(entry, sc);
if (operation != null) {
if (!operation.isDisabled() && operation.isDefaultSelected()) {
selected.add(operation);
}
all.add(operation);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
return true;
}
};
return action;
}
boolean scan(
ObservableList<ScanProvider.ScanOpportunity> all,
ObservableList<ScanProvider.ScanOpportunity> selected,

View File

@@ -0,0 +1,139 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.store.StoreChoiceComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import lombok.Getter;
import java.util.ArrayList;
import java.util.function.Function;
import static javafx.scene.layout.Priority.ALWAYS;
public class ScanDialogBase {
private final boolean expand;
private final Runnable closeAction;
private final ScanDialogAction action;
private final ObservableList<DataStoreEntryRef<ShellStore>> entries;
private final ObservableList<ScanProvider.ScanOpportunity> available =
FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ListProperty<ScanProvider.ScanOpportunity> selected =
new SimpleListProperty<>(FXCollections.synchronizedObservableList(FXCollections.observableArrayList()));
@Getter
private final BooleanProperty busy = new SimpleBooleanProperty();
public ScanDialogBase(boolean expand, Runnable closeAction, ScanDialogAction action, ObservableList<DataStoreEntryRef<ShellStore>> entries) {
this.expand = expand;
this.closeAction = closeAction;
this.action = action;
this.entries = entries;
}
public void finish() throws Exception {
if (entries.isEmpty()) {
return;
}
BooleanScope.executeExclusive(busy, () -> {
for (var entry : entries) {
if (expand) {
entry.get().setExpanded(true);
}
var copy = new ArrayList<>(selected);
for (var a : copy) {
// If the user decided to remove the selected entry
// while the scan is running, just return instantly
if (!DataStorage.get().getStoreEntriesSet().contains(entry.get())) {
return;
}
// Previous scan operation could have exited the shell
var sc = entry.getStore().getOrStartSession();
try {
a.getProvider().scan(entry.get(), sc);
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
}
});
}
private void onUpdate() {
available.clear();
selected.clear();
if (entries.isEmpty()) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
for (var entry : entries) {
boolean r;
try {
var sc = entry.getStore().getOrStartSession();
r = action.scan(available, selected, entry.get(), sc);
} catch (Throwable t) {
closeAction.run();
throw t;
}
if (!r) {
closeAction.run();
}
}
});
});
}
public Comp<?> createContent() {
StackPane stackPane = new StackPane();
stackPane.getStyleClass().add("scan-list");
VBox.setVgrow(stackPane, ALWAYS);
Function<ScanProvider.ScanOpportunity, String> nameFunc = (ScanProvider.ScanOpportunity s) -> {
var n = AppI18n.get(s.getNameKey());
if (s.getLicensedFeatureId() == null) {
return n;
}
var suffix = LicenseProvider.get().getFeature(s.getLicensedFeatureId());
return n + suffix.getDescriptionSuffix().map(d -> " (" + d + ")").orElse("");
};
var r = new ListSelectorComp<>(
available,
nameFunc,
selected,
scanOperation -> scanOperation.isDisabled(),
() -> available.size() > 3)
.createRegion();
stackPane.getChildren().add(r);
onUpdate();
entries.addListener((ListChangeListener<? super DataStoreEntryRef<ShellStore>>) c -> onUpdate());
var comp = LoadingOverlayComp.noProgress(Comp.of(() -> stackPane), busy)
.vgrow();
return comp;
}
}

View File

@@ -1,170 +0,0 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.ModalOverlayContentComp;
import io.xpipe.app.comp.store.StoreChoiceComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import lombok.Getter;
import java.util.ArrayList;
import java.util.function.Function;
import static javafx.scene.layout.Priority.ALWAYS;
class ScanDialogComp extends ModalOverlayContentComp {
private final DataStoreEntryRef<ShellStore> initialStore;
private final ScanDialogAction action;
private final ObjectProperty<DataStoreEntryRef<ShellStore>> entry;
private final ObservableList<ScanProvider.ScanOpportunity> available =
FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ListProperty<ScanProvider.ScanOpportunity> selected =
new SimpleListProperty<>(FXCollections.synchronizedObservableList(FXCollections.observableArrayList()));
@Getter
private final BooleanProperty busy = new SimpleBooleanProperty();
ScanDialogComp(DataStoreEntryRef<ShellStore> entry, ScanDialogAction action) {
this.initialStore = entry;
this.entry = new SimpleObjectProperty<>(entry);
this.action = action;
}
protected void finish() {
ThreadHelper.runFailableAsync(() -> {
if (entry.get() == null) {
return;
}
Platform.runLater(() -> {
var modal = getModalOverlay();
if (modal != null) {
modal.close();
}
});
BooleanScope.executeExclusive(busy, () -> {
entry.get().get().setExpanded(true);
var copy = new ArrayList<>(selected);
for (var a : copy) {
// If the user decided to remove the selected entry
// while the scan is running, just return instantly
if (!DataStorage.get()
.getStoreEntriesSet()
.contains(entry.get().get())) {
return;
}
// Previous scan operation could have exited the shell
var sc = entry.get().getStore().getOrStartSession();
try {
a.getProvider().scan(entry.get().get(), sc);
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
});
});
}
private void onUpdate(DataStoreEntryRef<ShellStore> newValue) {
available.clear();
selected.clear();
if (newValue == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
boolean r;
try {
var sc = entry.get().getStore().getOrStartSession();
r = action.scan(available, selected, newValue.get(), sc);
} catch (Throwable t) {
var modal = getModalOverlay();
if (initialStore != null && modal != null) {
modal.close();
}
throw t;
}
if (!r) {
var modal = getModalOverlay();
if (initialStore != null && modal != null) {
modal.close();
}
}
});
});
}
@Override
protected Region createSimple() {
StackPane stackPane = new StackPane();
stackPane.getStyleClass().add("scan-list");
var b = new OptionsBuilder()
.name("scanAlertChoiceHeader")
.description("scanAlertChoiceHeaderDescription")
.addComp(new StoreChoiceComp<>(
StoreChoiceComp.Mode.OTHER,
null,
entry,
ShellStore.class,
store1 -> true,
StoreViewState.get().getAllConnectionsCategory())
.disable(busy.or(new SimpleBooleanProperty(initialStore != null))))
.name("scanAlertHeader")
.description("scanAlertHeaderDescription")
.addComp(LoadingOverlayComp.noProgress(Comp.of(() -> stackPane), busy)
.vgrow())
.buildComp()
.prefWidth(500)
.prefHeight(680)
.apply(struc -> {
VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS);
});
Function<ScanProvider.ScanOpportunity, String> nameFunc = (ScanProvider.ScanOpportunity s) -> {
var n = AppI18n.get(s.getNameKey());
if (s.getLicensedFeatureId() == null) {
return n;
}
var suffix = LicenseProvider.get().getFeature(s.getLicensedFeatureId());
return n + suffix.getDescriptionSuffix().map(d -> " (" + d + ")").orElse("");
};
var r = new ListSelectorComp<>(
available,
nameFunc,
selected,
scanOperation -> scanOperation.isDisabled(),
() -> available.size() > 3)
.createRegion();
stackPane.getChildren().add(r);
entry.subscribe(newValue -> {
onUpdate(newValue);
});
return b.createRegion();
}
}

View File

@@ -0,0 +1,61 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.base.ModalOverlayContentComp;
import io.xpipe.app.comp.store.StoreChoiceComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import java.util.List;
import static javafx.scene.layout.Priority.ALWAYS;
class ScanMultiDialogComp extends ModalOverlayContentComp {
private final ScanDialogBase base;
ScanMultiDialogComp(List<DataStoreEntryRef<ShellStore>> entries, ScanDialogAction action) {
ObservableList<DataStoreEntryRef<ShellStore>> list = FXCollections.observableArrayList(entries);
this.base = new ScanDialogBase(true, () -> {
var modal = getModalOverlay();
if (modal != null) {
modal.close();
}
}, action, list);
}
void finish() {
ThreadHelper.runFailableAsync(() -> {
base.finish();
});
}
BooleanProperty getBusy() {
return base.getBusy();
}
@Override
protected Region createSimple() {
var list = base.createContent();
var b = new OptionsBuilder()
.name("scanAlertHeader")
.description("scanAlertHeaderDescription")
.addComp(list.vgrow())
.buildComp()
.prefWidth(500)
.prefHeight(680)
.apply(struc -> {
VBox.setVgrow(struc.get().getChildren().getFirst(), ALWAYS);
});;
return b.createRegion();
}
}

View File

@@ -0,0 +1,80 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.base.ModalOverlayContentComp;
import io.xpipe.app.comp.store.StoreChoiceComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import static javafx.scene.layout.Priority.ALWAYS;
class ScanSingleDialogComp extends ModalOverlayContentComp {
private final DataStoreEntryRef<ShellStore> initialStore;
private final ObjectProperty<DataStoreEntryRef<ShellStore>> entry;
private final ScanDialogBase base;
@SuppressWarnings("unchecked")
ScanSingleDialogComp(DataStoreEntryRef<ShellStore> entry, ScanDialogAction action) {
this.initialStore = entry;
this.entry = new SimpleObjectProperty<>(entry);
ObservableList<DataStoreEntryRef<ShellStore>> list = FXCollections.observableArrayList();
this.entry.subscribe(v -> {
if (v != null) {
list.setAll(v);
} else {
list.clear();
}
});
this.base = new ScanDialogBase(true, () -> {
var modal = getModalOverlay();
if (initialStore != null && modal != null) {
modal.close();
}
}, action, list);
}
void finish() {
ThreadHelper.runFailableAsync(() -> {
base.finish();
});
}
BooleanProperty getBusy() {
return base.getBusy();
}
@Override
protected Region createSimple() {
var list = base.createContent();
var b = new OptionsBuilder()
.name("scanAlertChoiceHeader")
.description("scanAlertChoiceHeaderDescription")
.addComp(new StoreChoiceComp<>(
StoreChoiceComp.Mode.OTHER,
null,
entry,
ShellStore.class,
store1 -> true,
StoreViewState.get().getAllConnectionsCategory())
.disable(base.getBusy().or(new SimpleBooleanProperty(initialStore != null))))
.name("scanAlertHeader")
.description("scanAlertHeaderDescription")
.addComp(list.vgrow())
.buildComp()
.prefWidth(500)
.prefHeight(680)
.apply(struc -> {
VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS);
});
return b.createRegion();
}
}

View File

@@ -55,7 +55,6 @@ open module io.xpipe.app {
requires io.xpipe.modulefs;
requires io.xpipe.core;
requires static lombok;
requires java.desktop;
requires org.apache.commons.io;
requires org.apache.commons.lang3;
requires javafx.base;
@@ -91,6 +90,7 @@ open module io.xpipe.app {
requires jdk.jdwp.agent;
requires java.net.http;
requires org.bouncycastle.provider;
requires org.jetbrains.annotations;
uses TerminalLauncher;
uses io.xpipe.app.ext.ActionProvider;

View File

@@ -1,3 +1,13 @@
.store-entry-list-status-bar {
-fx-padding: 0 8 0 8;
-fx-border-radius: 0 0 4 4;
-fx-background-radius: 0 0 4 4;
}
.store-entry-list-status-bar .button {
-fx-padding: 4 8;
}
.store-list-comp.scroll-pane * {
-fx-icon-color: -color-fg-default;
}

View File

@@ -10,6 +10,7 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.core.process.ShellTtyState;
import io.xpipe.core.process.SystemState;
import io.xpipe.core.store.DataStore;
import io.xpipe.ext.base.script.ScriptHierarchy;
import javafx.beans.property.SimpleStringProperty;
@@ -71,6 +72,40 @@ public class RunScriptActionMenu implements ActionProvider {
}
};
}
@Override
public BatchDataStoreCallSite<ShellStore> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<ShellStore>() {
@Override
public ObservableValue<String> getName() {
var t = AppPrefs.get().terminalType().getValue();
return AppI18n.observable(
"executeInTerminal",
t != null ? t.toTranslatedString().getValue() : "?");
}
@Override
public String getIcon() {
return "mdi2d-desktop-mac";
}
@Override
public Class<?> getApplicableClass() {
return ShellStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<ShellStore> store) {
return new Action(store);
}
@Override
public List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {
return List.of();
}
};
}
}
@Value
@@ -116,6 +151,37 @@ public class RunScriptActionMenu implements ActionProvider {
}
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<ShellStore>() {
@Override
public Class<ShellStore> getApplicableClass() {
return ShellStore.class;
}
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("executeInBackground");
}
@Override
public String getIcon() {
return "mdi2f-flip-to-back";
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<ShellStore> store) {
return new Action(store);
}
@Override
public List<ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {
return List.of();
}
};
}
}
@Value
@@ -189,6 +255,44 @@ public class RunScriptActionMenu implements ActionProvider {
}
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<ShellStore>() {
@Override
public ObservableValue<String> getName() {
return new SimpleStringProperty(hierarchy.getBase().get().getName());
}
@Override
public String getIcon() {
return "mdi2p-play-box-multiple-outline";
}
@Override
public Class<?> getApplicableClass() {
return ShellStore.class;
}
@Override
public Action createAction(DataStoreEntryRef<ShellStore> store) {
return null;
}
@Override
public List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {
if (hierarchy.isLeaf()) {
return List.of(
new TerminalRunActionProvider(hierarchy), new BackgroundRunActionProvider(hierarchy));
}
return hierarchy.getChildren().stream()
.map(c -> new ScriptActionProvider(c))
.toList();
}
};
}
}
private static class NoScriptsActionProvider implements ActionProvider {
@@ -225,6 +329,36 @@ public class RunScriptActionMenu implements ActionProvider {
}
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<ShellStore>() {
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("noScriptsAvailable");
}
@Override
public String getIcon() {
return "mdi2i-image-filter-none";
}
@Override
public Class<?> getApplicableClass() {
return ShellStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<ShellStore> store) {
return null;
}
@Override
public List<ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {
return List.of();
}
};
}
}
private static class NoStateActionProvider implements ActionProvider {
@@ -342,4 +476,49 @@ public class RunScriptActionMenu implements ActionProvider {
}
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<ShellStore>() {
@Override
public Class<ShellStore> getApplicableClass() {
return ShellStore.class;
}
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("runScript");
}
@Override
public String getIcon() {
return "mdi2p-play-box-multiple-outline";
}
@Override
public Action createAction(DataStoreEntryRef<ShellStore> store) {
return null;
}
@Override
public List<ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {
var hierarchy = ScriptHierarchy.buildEnabledHierarchy(ref -> {
if (!ref.getStore().isRunnableScript()) {
return false;
}
return true;
});
var list = hierarchy.getChildren().stream()
.<ActionProvider>map(c -> new ScriptActionProvider(c))
.toList();
if (list.isEmpty()) {
return List.of(new NoScriptsActionProvider());
} else {
return list;
}
}
};
}
}

View File

@@ -6,6 +6,7 @@ import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ScanDialog;
import io.xpipe.app.util.ScanDialogAction;
import io.xpipe.core.process.ShellTtyState;
import io.xpipe.core.process.SystemState;
@@ -13,6 +14,8 @@ import javafx.beans.value.ObservableValue;
import lombok.Value;
import java.util.List;
public class ScanStoreAction implements ActionProvider {
@Override
@@ -61,6 +64,34 @@ public class ScanStoreAction implements ActionProvider {
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<ShellStore>() {
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("addConnections");
}
@Override
public String getIcon() {
return "mdi2l-layers-plus";
}
@Override
public Class<?> getApplicableClass() {
return ShellStore.class;
}
@Override
public ActionProvider.Action createAction(List<DataStoreEntryRef<ShellStore>> stores) {
return () -> {
ScanDialog.showMulti(stores, ScanDialogAction.shellScanAction());
};
}
};
}
@Value
static class Action implements ActionProvider.Action {
@@ -69,7 +100,7 @@ public class ScanStoreAction implements ActionProvider {
@Override
public void execute() {
if (entry == null || entry.getStore() instanceof ShellStore) {
ScanDialog.showForShellStore(entry);
ScanDialog.showAsync(entry);
}
}
}

View File

@@ -47,6 +47,37 @@ public class ServiceRefreshAction implements ActionProvider {
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<FixedServiceCreatorStore>() {
@Override
public boolean isApplicable(DataStoreEntryRef<FixedServiceCreatorStore> o) {
return o.getStore().allowManualServicesRefresh();
}
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("refreshServices");
}
@Override
public String getIcon() {
return "mdi2w-web";
}
@Override
public Class<?> getApplicableClass() {
return FixedServiceCreatorStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<FixedServiceCreatorStore> store) {
return new Action(store);
}
};
}
@Value
static class Action implements ActionProvider.Action {

View File

@@ -41,6 +41,32 @@ public class StorePauseAction implements ActionProvider {
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<PauseableStore>() {
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("pause");
}
@Override
public String getIcon() {
return "mdi2p-pause";
}
@Override
public Class<?> getApplicableClass() {
return PauseableStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<PauseableStore> store) {
return new Action(store);
}
};
}
@Value
static class Action implements ActionProvider.Action {

View File

@@ -47,6 +47,37 @@ public class StoreRestartAction implements ActionProvider {
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<DataStore>() {
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("restart");
}
@Override
public String getIcon() {
return "mdi2r-restart";
}
@Override
public Class<?> getApplicableClass() {
return DataStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<DataStore> store) {
return new Action(store);
}
@Override
public boolean isApplicable(DataStoreEntryRef<DataStore> o) {
return o.getStore() instanceof StartableStore && o.getStore() instanceof StoppableStore;
}
};
}
@Value
static class Action implements ActionProvider.Action {

View File

@@ -41,6 +41,32 @@ public class StoreStartAction implements ActionProvider {
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<StartableStore>() {
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("start");
}
@Override
public String getIcon() {
return "mdi2p-play";
}
@Override
public Class<?> getApplicableClass() {
return StartableStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<StartableStore> store) {
return new Action(store);
}
};
}
@Value
static class Action implements ActionProvider.Action {

View File

@@ -41,6 +41,32 @@ public class StoreStopAction implements ActionProvider {
};
}
@Override
public BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<StoppableStore>() {
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("stop");
}
@Override
public String getIcon() {
return "mdi2s-stop";
}
@Override
public Class<?> getApplicableClass() {
return StoppableStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<StoppableStore> store) {
return new Action(store);
}
};
}
@Value
static class Action implements ActionProvider.Action {

View File

@@ -33,7 +33,7 @@ addCommand=Command ...
addAutomatically=Search Automatically ...
addOther=Add Other ...
addConnection=Add Connection
addConnections=New
new=New
selectType=Select Type
selectTypeDescription=Select connection type
#context: computer shell program
@@ -672,7 +672,6 @@ openFileWith=Open with ...
openWithDefaultApplication=Open with default application
rename=Rename
run=Run
new=New
openInTerminal=Open in terminal
file=File
directory=Directory
@@ -858,9 +857,11 @@ sshConfig=SSH config files
autostart=Automatically connect on XPipe startup
acceptHostKey=Accept host key
modifyHostKeyPermissions=Modify host key permissions
attachContainer=Attach to container
#force
attachContainer=Attach
openInVsCode=Open in VSCode
containerLogs=Show container logs
#force
containerLogs=Show logs
openSftpClient=Open in external SFTP client
openTermius=Open in Termius
showInternalInstances=Show internal instances
@@ -945,10 +946,10 @@ k8sCmd.displayName=kubectl client
k8sCmd.displayDescription=Access Kubernetes clusters via kubectl
k8sClusters=Kubernetes clusters
shells=Available shells
startContainer=Start container
stopContainer=Stop container
inspectContainer=Inspect container
inspectContext=Inspect context
#force
inspectContainer=Inspect
#force
inspectContext=Inspect
k8sClusterNameDescription=The name of the context the cluster is in.
#context: kubernetes
pod=Pod
@@ -960,7 +961,8 @@ k8sClusterNamespaceDescription=The custom namespace or the default one if empty
k8sConfigLocation=Config file
k8sConfigLocationDescription=The custom kubeconfig file or the default one if left empty
#context: kubernetes
inspectPod=Inspect pod
#force
inspectPod=Inspect
showAllContainers=Show non-running containers
showAllPods=Show non-running pods
k8sPodHostDescription=The host on which the pod is located
@@ -1348,3 +1350,6 @@ noScriptStateAvailable=Refresh to determine script compatibility ...
documentationDescription=Check out the documentation
disableApiHttpsTlsCheck=Disable API HTTPS request certificate verification
disableApiHttpsTlsCheckDescription=If your organization is decrypting your HTTPS traffic in firewalls using SSL interception, any update checks or license checks will fail due to the certificates not matching up. You can fix this by enabling this option and disabling TLS certificate validation.
connectionsSelected=$NUMBER$ connections selected
#force
addConnections=Add connections