diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownEditorComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownEditorComp.java new file mode 100644 index 000000000..1b0fbee66 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownEditorComp.java @@ -0,0 +1,80 @@ +package io.xpipe.app.comp.base; + +import atlantafx.base.theme.Styles; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.impl.IconButtonComp; +import io.xpipe.app.util.FileOpener; +import javafx.application.Platform; +import javafx.beans.property.Property; +import javafx.scene.control.Button; +import javafx.scene.control.TextArea; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import lombok.Builder; +import lombok.Value; + +public class MarkdownEditorComp extends Comp { + + private final Property value; + private final String identifier; + + public MarkdownEditorComp( + Property value, String identifier) { + this.value = value; + this.identifier = identifier; + } + + private Button createOpenButton() { + return new IconButtonComp( + "mdal-edit", + () -> FileOpener.openString( + identifier + ".md", + this, + value.getValue(), + (s) -> { + Platform.runLater(() -> value.setValue(s)); + })) + .styleClass("edit-button") + .apply(struc -> struc.get().getStyleClass().remove(Styles.FLAT)) + .createStructure() + .get(); + } + + @Override + public Structure createBase() { + var markdown = new MarkdownComp(value, s -> s).createRegion(); + var editButton = createOpenButton(); + var pane = new AnchorPane(markdown, editButton); + pane.setPickOnBounds(false); + AnchorPane.setTopAnchor(editButton, 10.0); + AnchorPane.setRightAnchor(editButton, 10.0); + return new Structure(pane, markdown, editButton); + } + + @Value + @Builder + public static class TextAreaStructure implements CompStructure { + StackPane pane; + TextArea textArea; + + @Override + public StackPane get() { + return pane; + } + } + + @Value + @Builder + public static class Structure implements CompStructure { + AnchorPane pane; + Region markdown; + Button editButton; + + @Override + public AnchorPane get() { + return pane; + } + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java index ce4395aa5..ed5fc1198 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java @@ -72,6 +72,7 @@ public class DenseStoreEntryComp extends StoreEntryComp { return grid.getWidth() / 2.5; }, grid.widthProperty())); + var notes = new StoreNotesComp(wrapper).createRegion(); if (showIcon) { var storeIcon = createIcon(30, 24); @@ -92,7 +93,8 @@ public class DenseStoreEntryComp extends StoreEntryComp { nameCC.setMinWidth(100); nameCC.setHgrow(Priority.ALWAYS); grid.getColumnConstraints().addAll(nameCC); - var nameBox = new HBox(name); + var nameBox = new HBox(name, notes); + nameBox.setSpacing(8); nameBox.setAlignment(Pos.CENTER_LEFT); grid.addRow(0, nameBox); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java index 8d0f58e4d..5efbd8d8c 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java @@ -20,6 +20,7 @@ public class StandardStoreEntryComp extends StoreEntryComp { protected Region createContent() { var name = createName().createRegion(); + var notes = new StoreNotesComp(wrapper).createRegion(); var grid = new GridPane(); grid.setHgap(7); @@ -29,7 +30,10 @@ public class StandardStoreEntryComp extends StoreEntryComp { grid.add(storeIcon, 0, 0, 1, 2); grid.getColumnConstraints().add(new ColumnConstraints(66)); - grid.add(name, 1, 0); + var nameAndNotes = new HBox(name, notes); + nameAndNotes.setSpacing(8); + nameAndNotes.setAlignment(Pos.CENTER_LEFT); + grid.add(nameAndNotes, 1, 0); grid.add(createSummary(), 1, 1); var nameCC = new ColumnConstraints(); nameCC.setMinWidth(100); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java index a26a69560..9859cd033 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java @@ -1,10 +1,8 @@ package io.xpipe.app.comp.store; +import atlantafx.base.theme.Styles; import io.xpipe.app.comp.base.LoadingOverlayComp; -import io.xpipe.app.core.App; -import io.xpipe.app.core.AppActionLinkDetector; -import io.xpipe.app.core.AppFont; -import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.*; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; @@ -12,13 +10,13 @@ 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.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.*; - import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableDoubleValue; @@ -29,13 +27,11 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; - -import atlantafx.base.theme.Styles; import org.kordamp.ikonli.javafx.FontIcon; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; @@ -422,6 +418,14 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(color); } + var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text")); + notes.setOnAction(event -> { + wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes())); + event.consume(); + }); + notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null)); + contextMenu.getItems().add(notes); + var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); del.disableProperty() .bind(Bindings.createBooleanBinding( @@ -439,9 +443,15 @@ public abstract class StoreEntryComp extends SimpleComp { 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; + + private static String DEFAULT_NOTES = null; + + private static String getDefaultNotes() { + if (DEFAULT_NOTES == null) { + AppResources.with(AppResources.XPIPE_MODULE, "misc/notes_default.md", f -> { + DEFAULT_NOTES = Files.readString(f); + }); + } + return DEFAULT_NOTES; } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java index 254d41a69..b9dab8f46 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java @@ -39,6 +39,7 @@ public class StoreEntryWrapper { private final Property color = new SimpleObjectProperty<>(); private final Property category = new SimpleObjectProperty<>(); private final Property summary = new SimpleObjectProperty<>(); + private final Property notes; public StoreEntryWrapper(DataStoreEntry entry) { this.entry = entry; @@ -60,6 +61,7 @@ public class StoreEntryWrapper { actionProviders.put(dataStoreActionProvider, new SimpleBooleanProperty(true)); }); this.defaultActionProvider = new SimpleObjectProperty<>(); + this.notes = new SimpleObjectProperty<>(new StoreNotes(entry.getNotes(), entry.getNotes())); setupListeners(); } @@ -96,6 +98,12 @@ public class StoreEntryWrapper { entry.addListener(() -> PlatformThread.runLaterIfNeeded(() -> { update(); })); + + notes.addListener((observable, oldValue, newValue) -> { + if (newValue.isCommited()) { + entry.setNotes(newValue.getCurrent()); + } + }); } public void update() { @@ -120,6 +128,7 @@ public class StoreEntryWrapper { cache.setValue(new HashMap<>(entry.getStoreCache())); } color.setValue(entry.getColor()); + notes.setValue(new StoreNotes(entry.getNotes(), entry.getNotes())); busy.setValue(entry.isInRefresh()); deletable.setValue(entry.getConfiguration().isDeletable() diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreNotes.java b/app/src/main/java/io/xpipe/app/comp/store/StoreNotes.java new file mode 100644 index 000000000..343cfb7de --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreNotes.java @@ -0,0 +1,16 @@ +package io.xpipe.app.comp.store; + +import lombok.Value; + +import java.util.Objects; + +@Value +public class StoreNotes { + + String commited; + String current; + + public boolean isCommited() { + return Objects.equals(commited, current); + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java new file mode 100644 index 000000000..a3f9ee3b0 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java @@ -0,0 +1,146 @@ +package io.xpipe.app.comp.store; + +import atlantafx.base.controls.Popover; +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.comp.base.DialogComp; +import io.xpipe.app.comp.base.MarkdownEditorComp; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.impl.IconButtonComp; +import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.storage.DataStorage; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.event.ActionEvent; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.layout.Region; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class StoreNotesComp extends Comp { + + private final StoreEntryWrapper wrapper; + + public StoreNotesComp(StoreEntryWrapper wrapper) {this.wrapper = wrapper;} + + @Override + public Structure createBase() { + var n = wrapper.getNotes(); + var button = new IconButtonComp("mdi2n-note-text") + .apply(struc -> AppFont.small(struc.get())) + .focusTraversableForAccessibility() + .tooltipKey("notes") + .styleClass("notes-button") + .grow(false, true) + .hide(BindingsHelper.map(n, s -> s.getCommited() == null && s.getCurrent() == null)) + .padding(new Insets(5)) + .createStructure().get(); + button.prefWidthProperty().bind(button.heightProperty()); + + var prop = new SimpleStringProperty(n.getValue().getCurrent()); + var md = new MarkdownEditorComp(prop,"notes-" + wrapper.getName().getValue()).createStructure(); + + var popover = new AtomicReference(); + var dialog = new DialogComp() { + + @Override + protected void finish() { + n.setValue(new StoreNotes(n.getValue().getCurrent(), n.getValue().getCurrent())); + popover.get().hide(); + } + + @Override + protected String finishKey() { + return "apply"; + } + + @Override + public Comp bottom() { + return new ButtonComp(AppI18n.observable("delete"), () -> { + n.setValue(new StoreNotes(null, null)); + }).hide(BindingsHelper.map(n, v -> v.getCommited() == null)); + } + + @Override + protected List> customButtons() { + return List.of(new ButtonComp(AppI18n.observable("cancel"), () -> { + popover.get().hide(); + })); + } + + @Override + public Comp content() { + return Comp.of(() -> md.get()); + } + }.createRegion(); + + popover.set(createPopover(dialog)); + button.setOnAction(e -> { + if (n.getValue().getCurrent() == null) { + return; + } + + if (popover.get().isShowing()) { + e.consume(); + return; + } + + popover.get().show(button); + e.consume(); + }); + md.getEditButton().addEventFilter(ActionEvent.ANY, event -> { + if (!popover.get().isDetached()) { + popover.get().setDetached(true); + event.consume(); + Platform.runLater(() -> { + Platform.runLater(() -> { + md.getEditButton().fire(); + }); + }); + } + }); + popover.get().showingProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue) { + n.setValue(new StoreNotes(n.getValue().getCommited(), n.getValue().getCommited())); + DataStorage.get().saveAsync(); + } + }); + prop.addListener((observable, oldValue, newValue) -> { + n.setValue(new StoreNotes(n.getValue().getCommited(), newValue)); + }); + n.addListener((observable, oldValue, s) -> { + prop.set(s.getCurrent()); + if (s.getCurrent() != null && oldValue.getCommited() == null && oldValue.isCommited()) { + Platform.runLater(() -> { + popover.get().show(button); + }); + } + }); + return new Structure(popover.get(), button); + } + + private Popover createPopover(Region content) { + var popover = new Popover(content); + popover.setCloseButtonEnabled(true); + popover.setHeaderAlwaysVisible(true); + popover.setDetachable(true); + popover.setTitle(wrapper.getName().getValue()); + popover.setMaxWidth(400); + popover.setHeight(600); + AppFont.small(popover.getContentNode()); + return popover; + } + + + public record Structure(Popover popover, Button button) implements CompStructure