Rework dialogs

This commit is contained in:
crschnick 2024-09-21 18:01:23 +00:00
parent ba9918e166
commit 5b25f34988
8 changed files with 269 additions and 268 deletions

View file

@ -9,11 +9,8 @@ import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
@ -23,14 +20,12 @@ import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
@ -40,7 +35,7 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class BrowserChooserComp extends SimpleComp {
public class BrowserChooserComp extends DialogComp {
private final BrowserFileChooserModel model;
@ -52,24 +47,16 @@ public class BrowserChooserComp extends SimpleComp {
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file, boolean save) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserFileChooserModel(OpenFileSystemModel.SelectionMode.SINGLE_FILE);
var comp = new BrowserChooserComp(model)
.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(
AppI18n.get(save ? "saveFileTitle" : "openFileTitle"),
stage -> {
return comp;
},
false,
null);
DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> {
var comp = new BrowserChooserComp(model);
comp.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()))
.styleClass("browser")
.styleClass("chooser");
return comp;
});
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close();
});
window.show();
window.setOnHidden(event -> {
model.finishWithoutChoice();
event.consume();
});
ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), null, null);
@ -77,8 +64,32 @@ public class BrowserChooserComp extends SimpleComp {
});
}
@Override
protected Region createSimple() {
protected String finishKey() {
return "select";
}
@Override
protected Comp<?> pane(Comp<?> content) {
return content;
}
@Override
protected void finish() {
model.finishChooser();
}
@Override
protected void discard() {
model.finishWithoutChoice();
}
@Override
public Comp<?> content() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore)
&& storeEntryWrapper.getEntry().getValidity().isUsable();
@ -144,60 +155,33 @@ public class BrowserChooserComp extends SimpleComp {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
return splitPane;
}
var dialogPane = new DialogComp() {
@Override
protected String finishKey() {
return "select";
}
@Override
protected Comp<?> pane(Comp<?> content) {
return content;
}
@Override
protected void finish() {
model.finishChooser();
}
@Override
public Comp<?> content() {
return splitPane;
}
@Override
public Comp<?> bottom() {
return Comp.of(() -> {
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren()
.setAll(c.getList().stream()
.map(s -> {
var field = new TextField(
s.getRawFileEntry().getPath());
field.setEditable(false);
field.getStyleClass().add("chooser-selection");
HBox.setHgrow(field, Priority.ALWAYS);
return field;
})
.toList());
});
});
var bottomBar = new HBox(selected);
HBox.setHgrow(selected, Priority.ALWAYS);
bottomBar.setAlignment(Pos.CENTER);
return bottomBar;
@Override
public Comp<?> bottom() {
return Comp.of(() -> {
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren()
.setAll(c.getList().stream()
.map(s -> {
var field = new TextField(
s.getRawFileEntry().getPath());
field.setEditable(false);
field.getStyleClass().add("chooser-selection");
HBox.setHgrow(field, Priority.ALWAYS);
return field;
})
.toList());
});
}
};
var r = dialogPane.createRegion();
r.getStyleClass().add("browser");
r.getStyleClass().add("chooser");
return r;
});
var bottomBar = new HBox(selected);
HBox.setHgrow(selected, Priority.ALWAYS);
bottomBar.setAlignment(Pos.CENTER);
return bottomBar;
});
}
}

View file

@ -20,22 +20,30 @@ import javafx.stage.Stage;
import atlantafx.base.theme.Styles;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
public abstract class DialogComp extends Comp<CompStructure<Region>> {
public static void showWindow(String titleKey, Function<Stage, DialogComp> f) {
var loading = new SimpleBooleanProperty();
var dialog = new AtomicReference<DialogComp>();
Platform.runLater(() -> {
var stage = AppWindowHelper.sideWindow(
AppI18n.get(titleKey),
window -> {
var c = f.apply(window);
dialog.set(c);
loading.bind(c.busy());
return c;
},
false,
loading);
stage.setOnCloseRequest(event -> {
if (dialog.get() != null) {
dialog.get().discard();
}
});
stage.show();
});
}
@ -97,6 +105,8 @@ public abstract class DialogComp extends Comp<CompStructure<Region>> {
protected abstract void finish();
protected abstract void discard();
public abstract Comp<?> content();
protected Comp<?> pane(Comp<?> content) {

View file

@ -323,9 +323,6 @@ public class StoreCreationComp extends DialogComp {
try (var ignored = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
var context = entry.getValue().validateAndKeepOpenOrThrowAndClose(null);
if (context == null) {
entry.getValue().validateRefreshChildrenOrThrow();
}
commit(context, true);
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
@ -354,6 +351,9 @@ public class StoreCreationComp extends DialogComp {
});
}
@Override
protected void discard() {}
@Override
public Comp<?> content() {
return Comp.of(this::createLayout);

View file

@ -63,6 +63,9 @@ public class StoreIconChoiceDialogComp extends SimpleComp {
dialogStage.close();
}
@Override
protected void discard() {}
@Override
public Comp<?> content() {
return new StoreIconChoiceComp(selected, SystemIcons.getSystemIcons(), 5, filterText, () -> {

View file

@ -96,6 +96,9 @@ public class StoreNotesComp extends Comp<StoreNotesComp.Structure> {
ref.get().hide();
}
@Override
protected void discard() {}
@Override
protected String finishKey() {
return "apply";
@ -130,8 +133,7 @@ public class StoreNotesComp extends Comp<StoreNotesComp.Structure> {
popover.setTitle(wrapper.getName().getValue());
popover.showingProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
n.setValue(
new StoreNotes(n.getValue().getCommited(), n.getValue().getCommited()));
n.setValue(new StoreNotes(n.getValue().getCommited(), n.getValue().getCommited()));
DataStorage.get().saveAsync();
ref.set(null);
}

View file

@ -115,13 +115,14 @@ public class AppLayoutModel {
null,
() -> Hyperlinks.open(
"http://localhost:" + AppBeaconServer.get().getPort()),
null),
new Entry(
AppI18n.observable("webtop"),
"mdi2d-desktop-mac",
null,
() -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
null)));
null)
// new Entry(
// AppI18n.observable("webtop"),
// "mdi2d-desktop-mac",
// null,
// () -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
// null)
));
return l;
}

View file

@ -1,16 +1,9 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp;
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 io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.process.ShellTtyState;
@ -18,21 +11,9 @@ import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.ShellValidationContext;
import io.xpipe.core.store.ValidationContext;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import static javafx.scene.layout.Priority.ALWAYS;
public class ScanAlert {
@ -90,168 +71,8 @@ public class ScanAlert {
ShellValidationContext shellValidationContext) {
DialogComp.showWindow(
"scanAlertTitle",
stage -> new Dialog(
stage -> new ScanDialog(
stage, initialStore != null ? initialStore.ref() : null, applicable, shellValidationContext));
}
private static class Dialog extends DialogComp {
private final DataStoreEntryRef<ShellStore> initialStore;
private final BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable;
private final Stage window;
private final ObjectProperty<DataStoreEntryRef<ShellStore>> entry;
private final ListProperty<ScanProvider.ScanOperation> selected =
new SimpleListProperty<>(FXCollections.observableArrayList());
private final BooleanProperty busy = new SimpleBooleanProperty();
private ShellValidationContext shellValidationContext;
private Dialog(
Stage window,
DataStoreEntryRef<ShellStore> entry,
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable,
ShellValidationContext shellValidationContext) {
this.window = window;
this.initialStore = entry;
this.entry = new SimpleObjectProperty<>(entry);
this.applicable = applicable;
this.shellValidationContext = shellValidationContext;
}
@Override
protected ObservableValue<Boolean> busy() {
return busy;
}
@Override
protected void finish() {
ThreadHelper.runFailableAsync(() -> {
try {
if (entry.get() == null) {
return;
}
Platform.runLater(() -> {
window.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
shellValidationContext.get().start();
try {
a.getScanner().run();
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
});
} finally {
shellValidationContext.close();
shellValidationContext = null;
}
});
}
@Override
protected Comp<?> pane(Comp<?> content) {
return content;
}
@Override
public Comp<?> content() {
StackPane stackPane = new StackPane();
stackPane.getStyleClass().add("scan-list");
var b = new OptionsBuilder()
.name("scanAlertChoiceHeader")
.description("scanAlertChoiceHeaderDescription")
.addComp(new DataStoreChoiceComp<>(
DataStoreChoiceComp.Mode.OTHER,
null,
entry,
ShellStore.class,
store1 -> true,
StoreViewState.get().getAllConnectionsCategory())
.disable(new SimpleBooleanProperty(initialStore != null)))
.name("scanAlertHeader")
.description("scanAlertHeaderDescription")
.addComp(Comp.of(() -> stackPane).vgrow())
.buildComp()
.prefWidth(500)
.prefHeight(680)
.apply(struc -> {
VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS);
})
.padding(new Insets(5, 20, 20, 20));
entry.subscribe(newValue -> {
selected.clear();
stackPane.getChildren().clear();
if (newValue == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (shellValidationContext != null) {
shellValidationContext.close();
shellValidationContext = null;
}
shellValidationContext = new ShellValidationContext(newValue.getStore()
.control()
.withoutLicenseCheck()
.start());
var a = applicable.apply(entry.get().get(), shellValidationContext.get());
Platform.runLater(() -> {
if (a == null) {
window.close();
return;
}
selected.setAll(a.stream()
.filter(scanOperation ->
scanOperation.isDefaultSelected() && !scanOperation.isDisabled())
.toList());
Function<ScanProvider.ScanOperation, String> nameFunc = (ScanProvider.ScanOperation 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<>(
a,
nameFunc,
selected,
scanOperation -> scanOperation.isDisabled(),
a.size() > 3)
.createRegion();
stackPane.getChildren().add(r);
});
});
});
});
return b;
}
}
}

View file

@ -0,0 +1,180 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp;
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 io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.ShellValidationContext;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import static javafx.scene.layout.Priority.ALWAYS;
class ScanDialog extends DialogComp {
private final DataStoreEntryRef<ShellStore> initialStore;
private final BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable;
private final Stage window;
private final ObjectProperty<DataStoreEntryRef<ShellStore>> entry;
private final ListProperty<ScanProvider.ScanOperation> selected = new SimpleListProperty<>(FXCollections.observableArrayList());
private final BooleanProperty busy = new SimpleBooleanProperty();
private ShellValidationContext shellValidationContext;
ScanDialog(
Stage window, DataStoreEntryRef<ShellStore> entry, BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable,
ShellValidationContext shellValidationContext
) {
this.window = window;
this.initialStore = entry;
this.entry = new SimpleObjectProperty<>(entry);
this.applicable = applicable;
this.shellValidationContext = shellValidationContext;
}
@Override
protected ObservableValue<Boolean> busy() {
return busy;
}
@Override
protected void finish() {
ThreadHelper.runFailableAsync(() -> {
try {
if (entry.get() == null) {
return;
}
Platform.runLater(() -> {
window.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
shellValidationContext.get().start();
try {
a.getScanner().run();
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
});
} finally {
shellValidationContext.close();
shellValidationContext = null;
}
});
}
@Override
protected void discard() {
ThreadHelper.runAsync(() -> {
shellValidationContext.close();
shellValidationContext = null;
});
}
@Override
protected Comp<?> pane(Comp<?> content) {
return content;
}
@Override
public Comp<?> content() {
StackPane stackPane = new StackPane();
stackPane.getStyleClass().add("scan-list");
var b = new OptionsBuilder().name("scanAlertChoiceHeader")
.description("scanAlertChoiceHeaderDescription")
.addComp(new DataStoreChoiceComp<>(DataStoreChoiceComp.Mode.OTHER, null, entry, ShellStore.class, store1 -> true,
StoreViewState.get().getAllConnectionsCategory()).disable(new SimpleBooleanProperty(initialStore != null)))
.name("scanAlertHeader")
.description("scanAlertHeaderDescription")
.addComp(Comp.of(() -> stackPane).vgrow())
.buildComp()
.prefWidth(500)
.prefHeight(680)
.apply(struc -> {
VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS);
})
.padding(new Insets(5, 20, 20, 20));
entry.subscribe(newValue -> {
onUpdate(newValue, stackPane);
});
return b;
}
private void onUpdate(DataStoreEntryRef<ShellStore> newValue, StackPane stackPane) {
selected.clear();
stackPane.getChildren().clear();
if (newValue == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (shellValidationContext != null) {
shellValidationContext.close();
shellValidationContext = null;
}
shellValidationContext = new ShellValidationContext(newValue.getStore().control().withoutLicenseCheck().start());
var a = applicable.apply(entry.get().get(), shellValidationContext.get());
Platform.runLater(() -> {
if (a == null) {
window.close();
return;
}
selected.setAll(
a.stream().filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled()).toList());
Function<ScanProvider.ScanOperation, String> nameFunc = (ScanProvider.ScanOperation 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<>(a, nameFunc, selected, scanOperation -> scanOperation.isDisabled(),
a.size() > 3).createRegion();
stackPane.getChildren().add(r);
});
});
});
}
}