Rework connection notes

This commit is contained in:
crschnick
2026-02-26 07:03:45 +00:00
parent 37f367ba2a
commit 8d8bbdfca1
7 changed files with 59 additions and 229 deletions

View File

@@ -4,6 +4,7 @@ import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.core.AppExtensionManager;
import io.xpipe.app.core.AppNames;
import io.xpipe.app.ext.*;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.process.ScriptHelper;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.process.TerminalInitScriptConfig;
@@ -82,6 +83,10 @@ public final class McpTools {
@NonNull
String path;
String information;
String notes;
}
public static McpServerFeatures.SyncToolSpecification listSystems() throws IOException {
@@ -104,9 +109,14 @@ public final class McpTools {
continue;
}
var section = StoreViewState.get().getSectionForWrapper(StoreViewState.get().getEntryWrapper(e));
var info = section.isPresent() ? e.getProvider().informationString(section.get()).getValue() : null;
var r = ConnectionResource.builder()
.name(e.getName())
.path(DataStorage.get().getStorePath(e).toString())
.information(info)
.notes(e.getNotes())
.build();
list.add(r);
}

View File

@@ -1,88 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.core.AppI18n;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import atlantafx.base.theme.Styles;
import org.int4.fx.builders.common.AbstractRegionBuilder;
import java.util.List;
public abstract class DialogComp extends RegionBuilder<Region> {
protected Region createNavigation() {
HBox buttons = new HBox();
buttons.setFillHeight(true);
var customButton = bottom();
if (customButton != null) {
var c = customButton.build();
buttons.getChildren().add(c);
HBox.setHgrow(c, Priority.ALWAYS);
}
var spacer = new Region();
HBox.setHgrow(spacer, Priority.SOMETIMES);
buttons.getChildren().add(spacer);
buttons.getStyleClass().add("buttons");
buttons.setSpacing(5);
buttons.setAlignment(Pos.CENTER_RIGHT);
buttons.getChildren()
.addAll(customButtons().stream()
.map(buttonComp -> buttonComp.build())
.toList());
var nextButton = finishButton();
buttons.getChildren().add(nextButton.build());
return buttons;
}
protected AbstractRegionBuilder<?, ?> finishButton() {
return new ButtonComp(AppI18n.observable(finishKey()), this::finish)
.style(Styles.ACCENT)
.style("next");
}
protected String finishKey() {
return "finishStep";
}
protected List<AbstractRegionBuilder<?, ?>> customButtons() {
return List.of();
}
@Override
public Region createSimple() {
var sp = pane(content()).style("dialog-content").build();
VBox vbox = new VBox();
vbox.getChildren().addAll(sp, createNavigation());
vbox.getStyleClass().add("dialog-comp");
vbox.setFillWidth(true);
VBox.setVgrow(sp, Priority.ALWAYS);
return vbox;
}
protected abstract void finish();
public abstract AbstractRegionBuilder<?, ?> content();
protected AbstractRegionBuilder<?, ?> pane(AbstractRegionBuilder<?, ?> content) {
var entry = content;
return RegionBuilder.of(() -> {
var entryR = entry.build();
var sp = new ScrollPane(entryR);
sp.setFitToWidth(true);
entryR.minHeightProperty().bind(sp.heightProperty());
return sp;
});
}
public AbstractRegionBuilder<?, ?> bottom() {
return null;
}
}

View File

@@ -459,10 +459,10 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2c-comment-text-outline"));
notes.setOnAction(event -> {
getWrapper().getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
StoreNotesComp.showDialog(getWrapper(), getDefaultNotes());
event.consume();
});
notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s.getCommited() == null));
notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s == null));
items.add(items.size(), notes);
var freeze = new MenuItem();

View File

@@ -58,7 +58,7 @@ public class StoreEntryWrapper {
private final Property<DataStoreColor> color = new SimpleObjectProperty<>();
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<String> summary = new SimpleObjectProperty<>();
private final Property<StoreNotes> notes;
private final ObjectProperty<String> notes;
private final Property<String> customIcon = new SimpleObjectProperty<>();
private final Property<String> iconFile = new SimpleObjectProperty<>();
private final BooleanProperty sessionActive = new SimpleBooleanProperty();
@@ -117,7 +117,7 @@ public class StoreEntryWrapper {
shownSummary,
AppI18n.activeLanguage());
this.shownInformation = new SimpleObjectProperty<>();
this.notes = new SimpleObjectProperty<>(new StoreNotes(entry.getNotes(), entry.getNotes()));
this.notes = new SimpleObjectProperty<>(entry.getNotes());
setupListeners();
}
@@ -158,12 +158,6 @@ public class StoreEntryWrapper {
entry.addListener(() -> PlatformThread.runLaterIfNeeded(() -> {
update();
}));
notes.addListener((observable, oldValue, newValue) -> {
if (newValue.isCommited()) {
entry.setNotes(newValue.getCurrent());
}
});
}
public void stopSession() {
@@ -211,7 +205,7 @@ public class StoreEntryWrapper {
}
orderIndex.setValue(entry.getOrderIndex());
color.setValue(entry.getColor());
notes.setValue(new StoreNotes(entry.getNotes(), entry.getNotes()));
notes.setValue(entry.getNotes());
customIcon.setValue(entry.getIcon());
readOnly.setValue(entry.isFreeze());
iconFile.setValue(entry.getEffectiveIconFile());

View File

@@ -1,30 +1,43 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.comp.*;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.MarkdownEditorComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.FileOpener;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.paint.Color;
import javafx.scene.input.MouseButton;
import atlantafx.base.controls.Popover;
import org.int4.fx.builders.common.AbstractRegionBuilder;
import java.util.UUID;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public class StoreNotesComp extends RegionBuilder<Button> {
public class StoreNotesComp extends RegionStructureBuilder<Button, StoreNotesComp.Structure> {
public static void showDialog(StoreEntryWrapper wrapper, String initial) {
var prop = new SimpleStringProperty(initial);
var md = new MarkdownEditorComp(prop, "notes-" + wrapper.getName().getValue())
.prefWidth(600)
.prefHeight(600);
var modal = ModalOverlay.of(new ReadOnlyStringWrapper(wrapper.getName().getValue()), md, null);
if (wrapper.getNotes().getValue() != null) {
modal.addButton(new ModalButton("delete", () -> {
wrapper.getEntry().setNotes(null);
DataStorage.get().saveAsync();
}, true, false));
}
modal.addButton(new ModalButton("cancel", () -> {}, true, false));
modal.addButton(new ModalButton("apply", () -> {
wrapper.getEntry().setNotes(prop.getValue());
DataStorage.get().saveAsync();
}, true, true));
modal.show();
}
private final StoreEntryWrapper wrapper;
@@ -33,133 +46,30 @@ public class StoreNotesComp extends RegionStructureBuilder<Button, StoreNotesCom
}
@Override
public Structure createBase() {
protected Button createSimple() {
var n = wrapper.getNotes();
var button = new IconButtonComp("mdi2n-note-text-outline")
.apply(struc -> AppFontSizes.xs(struc))
.describe(d ->
d.nameKey("notes").focusTraversal(RegionDescriptor.FocusTraversal.ENABLED_FOR_ACCESSIBILITY))
.style("notes-button")
.hide(BindingsHelper.map(n, s -> s.getCommited() == null && s.getCurrent() == null))
.hide(n.isNull())
.build();
button.setOpacity(0.85);
button.prefWidthProperty().bind(button.heightProperty());
var prop = new SimpleStringProperty(n.getValue().getCurrent());
var popover = new AtomicReference<Popover>();
button.setOnAction(e -> {
if (n.getValue().getCurrent() == null) {
return;
}
if (popover.get() != null && popover.get().isShowing()) {
e.consume();
return;
}
popover.set(createPopover(popover, prop));
popover.get().show(button);
showDialog(wrapper, wrapper.getNotes().getValue());
e.consume();
});
prop.addListener((observable, oldValue, newValue) -> {
n.setValue(new StoreNotes(n.getValue().getCommited(), newValue));
});
n.addListener((observable, oldValue, s) -> {
prop.set(s.getCurrent());
// Check for scene existence. If we exited the platform immediately after adding notes, this might throw an
// exception
if (s.getCurrent() != null
&& oldValue.getCommited() == null
&& oldValue.isCommited()
&& button.getScene() != null) {
Platform.runLater(() -> {
popover.set(createPopover(popover, prop));
popover.get().show(button);
});
var editKey = UUID.randomUUID().toString();
button.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY && e.isShiftDown()) {
FileOpener.openString("notes.md", editKey, wrapper.getNotes().getValue(), s -> wrapper.getEntry().setNotes(s));
e.consume();
}
});
return new Structure(popover.get(), button);
}
private Popover createPopover(AtomicReference<Popover> ref, Property<String> prop) {
var n = wrapper.getNotes();
var md = new MarkdownEditorComp(prop, "notes-" + wrapper.getName().getValue())
.prefWidth(600)
.prefHeight(600)
.buildStructure();
var dialog = new DialogComp() {
@Override
protected String finishKey() {
return "apply";
}
@Override
protected List<AbstractRegionBuilder<?, ?>> customButtons() {
return List.of(new ButtonComp(AppI18n.observable("cancel"), () -> {
ref.get().hide();
}));
}
@Override
protected void finish() {
n.setValue(
new StoreNotes(n.getValue().getCurrent(), n.getValue().getCurrent()));
ref.get().hide();
}
@Override
public BaseRegionBuilder<?, ?> content() {
return RegionBuilder.of(() -> md.get());
}
@Override
public BaseRegionBuilder<?, ?> bottom() {
return new ButtonComp(AppI18n.observable("delete"), () -> {
n.setValue(new StoreNotes(null, null));
})
.hide(BindingsHelper.map(n, v -> v.getCommited() == null));
}
}.build();
var popover = new Popover(dialog);
popover.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());
popover.getScene().setFill(Color.TRANSPARENT);
popover.setCloseButtonEnabled(true);
popover.setHeaderAlwaysVisible(true);
popover.setDetachable(true);
popover.setTitle(wrapper.getName().getValue());
popover.showingProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
n.setValue(
new StoreNotes(n.getValue().getCommited(), n.getValue().getCommited()));
DataStorage.get().saveAsync();
ref.set(null);
}
});
AppFontSizes.xs(popover.getContentNode());
md.getEditButton().addEventFilter(ActionEvent.ANY, event -> {
if (!popover.isDetached()) {
popover.setDetached(true);
event.consume();
Platform.runLater(() -> {
Platform.runLater(() -> {
md.getEditButton().fire();
});
});
}
});
return popover;
}
public record Structure(Popover popover, Button button) implements RegionStructure<Button> {
@Override
public Button get() {
return button;
}
return button;
}
}

View File

@@ -27,6 +27,14 @@
"path": {
"type": "string",
"description": "The full path of the system"
},
"information": {
"type": "string",
"description": "Summary of the known system information"
},
"notes": {
"type": "string",
"description": "User-supplied notes for the individual system"
}
},
"required": [

View File

@@ -26,10 +26,6 @@
-fx-border-color: -color-neutral-emphasis;
}
.edit-button.icon-button-comp:hover, .root:key-navigation .edit-button.icon-button-comp:focused {
-fx-background-color: -color-accent-muted;
}
.scan-list {
-fx-background-radius: 4px;
-fx-border-width: 1;