From be684d7b728d6e71cd1fc9ca161e5af1c73cecee Mon Sep 17 00:00:00 2001 From: crschnick Date: Thu, 22 Aug 2024 17:34:36 +0000 Subject: [PATCH] Rework categories --- .../session/BrowserSessionTabsComp.java | 4 +- .../app/comp/base/LazyTextFieldComp.java | 21 +-- .../app/comp/store/StoreCategoryWrapper.java | 4 + .../xpipe/app/comp/store/StoreEntryComp.java | 4 +- .../app/comp/store/StoreEntryWrapper.java | 4 +- .../app/comp/store/StoreSectionComp.java | 4 +- .../app/comp/store/StoreSectionMiniComp.java | 4 +- .../app/core/window/AppWindowHelper.java | 8 +- .../app/fxcomps/impl/StoreCategoryComp.java | 127 +++++++++++++----- .../{DataStoreColor.java => DataColor.java} | 25 +++- .../io/xpipe/app/storage/DataStorage.java | 17 +++ .../xpipe/app/storage/DataStoreCategory.java | 20 ++- .../io/xpipe/app/storage/DataStoreEntry.java | 23 +--- .../app/storage/ImpersistentStorage.java | 1 + .../io/xpipe/app/storage/StandardStorage.java | 2 +- .../io/xpipe/app/storage/StorageElement.java | 16 ++- .../app/terminal/ExternalTerminalType.java | 4 +- .../io/xpipe/app/util/TerminalLauncher.java | 2 +- .../io/xpipe/app/resources/style/category.css | 47 ++++--- 19 files changed, 228 insertions(+), 109 deletions(-) rename app/src/main/java/io/xpipe/app/storage/{DataStoreColor.java => DataColor.java} (60%) diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java index 891810db0..0de950840 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java @@ -364,9 +364,7 @@ public class BrowserSessionTabsComp extends SimpleComp { StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container"); c.getStyleClass().add("color-box"); - var color = DataStorage.get() - .getRootForEntry(model.getEntry().get()) - .getColor(); + var color = DataStorage.get().getEffectiveColor(model.getEntry().get()); if (color != null) { c.getStyleClass().add(color.getId()); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java index 5a4c98676..a434a857e 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java @@ -2,6 +2,7 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; import javafx.beans.property.Property; @@ -15,7 +16,7 @@ import lombok.Value; import java.util.Objects; -public class LazyTextFieldComp extends Comp { +public class LazyTextFieldComp extends Comp> { private final Property currentValue; private final Property appliedValue; @@ -26,8 +27,7 @@ public class LazyTextFieldComp extends Comp { } @Override - public LazyTextFieldComp.Structure createBase() { - var sp = new StackPane(); + public CompStructure createBase() { var r = new TextField(); r.setOnKeyPressed(ke -> { @@ -48,23 +48,14 @@ public class LazyTextFieldComp extends Comp { } }); - sp.focusedProperty().addListener((c, o, n) -> { - if (n) { - r.setDisable(false); - r.requestFocus(); - } - }); - // Handles external updates PlatformThread.sync(appliedValue).addListener((observable, oldValue, n) -> { currentValue.setValue(n); }); - r.setPrefWidth(0); - sp.getChildren().add(r); - sp.prefWidthProperty().bind(r.prefWidthProperty()); - sp.prefHeightProperty().bind(r.prefHeightProperty()); + r.setMinWidth(0); r.setDisable(true); + r.prefWidthProperty().bind(r.minWidthProperty()); currentValue.subscribe(n -> { PlatformThread.runLaterIfNeeded(() -> { @@ -86,7 +77,7 @@ public class LazyTextFieldComp extends Comp { } }); r.getStyleClass().add("lazy-text-field-comp"); - return new Structure(sp, r); + return new SimpleCompStructure<>(r); } @Value diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java index 2eac24a21..33441988a 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java @@ -6,6 +6,7 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; +import io.xpipe.app.storage.DataColor; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -29,6 +30,7 @@ public class StoreCategoryWrapper { private final ObservableList children; private final ObservableList containedEntries; private final BooleanProperty expanded = new SimpleBooleanProperty(); + private final Property color = new SimpleObjectProperty<>(); public StoreCategoryWrapper(DataStoreCategory category) { var d = 0; @@ -51,6 +53,7 @@ public class StoreCategoryWrapper { this.share = new SimpleObjectProperty<>(category.isShare()); this.children = FXCollections.observableArrayList(); this.containedEntries = FXCollections.observableArrayList(); + this.color.setValue(category.getColor()); setupListeners(); } @@ -130,6 +133,7 @@ public class StoreCategoryWrapper { sortMode.setValue(category.getSortMode()); share.setValue(category.isShare()); expanded.setValue(category.isExpanded()); + color.setValue(category.getColor()); containedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream() .filter(entry -> { 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 2c88d7c9a..99ce1ce4c 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 @@ -17,7 +17,7 @@ import io.xpipe.app.fxcomps.util.DerivedObservableList; 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.storage.DataColor; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.*; @@ -345,7 +345,7 @@ public abstract class StoreEntryComp extends SimpleComp { event.consume(); }); color.getItems().add(none); - Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> { + Arrays.stream(DataColor.values()).forEach(dataStoreColor -> { MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId())); m.setOnAction(event -> { getWrapper().getEntry().setColor(dataStoreColor); 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 90d0e58a6..5ccca858e 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 @@ -6,7 +6,7 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; -import io.xpipe.app.storage.DataStoreColor; +import io.xpipe.app.storage.DataColor; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.ThreadHelper; @@ -36,7 +36,7 @@ public class StoreEntryWrapper { private final BooleanProperty expanded = new SimpleBooleanProperty(); private final Property persistentState = new SimpleObjectProperty<>(); private final Property> cache = new SimpleObjectProperty<>(Map.of()); - private final Property color = new SimpleObjectProperty<>(); + private final Property color = new SimpleObjectProperty<>(); private final Property category = new SimpleObjectProperty<>(); private final Property summary = new SimpleObjectProperty<>(); private final Property notes; diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java index d3a9f526b..154e72013 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java @@ -7,7 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.storage.DataStoreColor; +import io.xpipe.app.storage.DataColor; import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; @@ -175,7 +175,7 @@ public class StoreSectionComp extends Comp> { } var newList = new ArrayList<>(struc.get().getStyleClass()); - newList.removeIf(s -> Arrays.stream(DataStoreColor.values()) + newList.removeIf(s -> Arrays.stream(DataColor.values()) .anyMatch( dataStoreColor -> dataStoreColor.getId().equals(s))); newList.remove("gray"); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java index cceeddb74..9e35bf600 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java @@ -8,7 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.storage.DataStoreColor; +import io.xpipe.app.storage.DataColor; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; @@ -168,7 +168,7 @@ public class StoreSectionMiniComp extends Comp> { return; } - struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataStoreColor.values()) + struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataColor.values()) .anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s))); struc.get().getStyleClass().remove("gray"); diff --git a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java index 320a05624..ae0af853a 100644 --- a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java +++ b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java @@ -214,8 +214,9 @@ public class AppWindowHelper { var r = scene.getRoot(); if (r != null) { var acc = Platform.isAccessibilityActive(); - r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), kb && !acc); - r.pseudoClassStateChanged(PseudoClass.getPseudoClass("normal-navigation"), !kb && !acc); + // This property is broken on some systems + r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), kb); + r.pseudoClassStateChanged(PseudoClass.getPseudoClass("normal-navigation"), !kb); r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), acc); } }); @@ -223,7 +224,8 @@ public class AppWindowHelper { Platform.accessibilityActiveProperty().addListener((observable, oldValue, newValue) -> { var r = scene.getRoot(); if (r != null) { - r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), false); + // This property is broken on some systems + r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), true); r.pseudoClassStateChanged(PseudoClass.getPseudoClass("normal-navigation"), false); r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), true); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java index 40f8c359d..c2ebf7a26 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java @@ -12,25 +12,30 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.storage.DataColor; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.util.ContextMenuHelper; - +import io.xpipe.app.util.DataStoreFormatter; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.ContextMenu; +import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.layout.Region; - import lombok.EqualsAndHashCode; import lombok.Value; import org.kordamp.ikonli.javafx.FontIcon; +import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Locale; @@ -45,43 +50,60 @@ public class StoreCategoryComp extends SimpleComp { @Override protected Region createSimple() { - var i = Bindings.createStringBinding( + var name = new LazyTextFieldComp(category.nameProperty()) + .styleClass("name") + .createRegion(); + var showing = new SimpleBooleanProperty(); + + var expandIcon = Bindings.createStringBinding( () -> { + var exp = category.getExpanded().get() && category.getChildren().size() > 0; + return exp ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"; + }, + category.getExpanded(), category.getChildren()); + var expandButton = new IconButtonComp(expandIcon, () -> { + category.toggleExpanded(); + }) + .apply(struc -> AppFont.medium(struc.get())) + .apply(struc -> { + struc.get().setAlignment(Pos.CENTER); + struc.get().setPadding(new Insets(-2, 0, 0, 0)); + struc.get().setFocusTraversable(false); + }) + .styleClass("expand-button") + .tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE)); + + var hover = new SimpleBooleanProperty(); + var statusIcon = Bindings.createStringBinding( + () -> { + if (hover.get()) { + return "mdomz-settings"; + } + if (!DataStorage.get().supportsSharing() || !category.getCategory().canShare()) { - var exp = category.getExpanded().get() && category.getChildren().size() > 0; - return exp ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"; + return "mdi2a-account-lock"; } return category.getShare().getValue() ? "mdi2g-git" : "mdi2a-account-cancel"; }, - category.getShare(), category.getExpanded(), category.getChildren()); - var icon = new IconButtonComp(i, () -> { - category.toggleExpanded(); - }) + category.getShare(), hover); + var statusButton = new IconButtonComp(statusIcon) .apply(struc -> AppFont.small(struc.get())) .apply(struc -> { struc.get().setAlignment(Pos.CENTER); - struc.get().setPadding(new Insets(0, 0, 6, 0)); + struc.get().setPadding(new Insets(0, 0, 7, 0)); struc.get().setFocusTraversable(false); - }); - var name = new LazyTextFieldComp(category.nameProperty()) - .apply(struc -> { - struc.get().prefWidthProperty().unbind(); - struc.get().setPrefWidth(150); - struc.getTextField().minWidthProperty().bind(struc.get().widthProperty()); + hover.bind(struc.get().hoverProperty()); }) - .styleClass("name") - .createRegion(); - var showing = new SimpleBooleanProperty(); - var settings = new IconButtonComp("mdomz-settings") - .styleClass("settings") .apply(new ContextMenuAugment<>( mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, null, () -> { - var cm = createContextMenu(name); - showing.bind(cm.showingProperty()); - return cm; - })); + var cm = createContextMenu(name); + showing.bind(cm.showingProperty()); + return cm; + })) + .styleClass("status-button"); + var shownList = new DerivedObservableList<>(category.getContainedEntries(), true) .filtered( storeEntryWrapper -> { @@ -91,18 +113,21 @@ public class StoreCategoryComp extends SimpleComp { StoreViewState.get().getFilterString()) .getList(); var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")"); - var hover = new SimpleBooleanProperty(); + + var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSharing())).or(showing); var focus = new SimpleBooleanProperty(); var h = new HorizontalComp(List.of( - icon, - Comp.hspacer(4), - Comp.of(() -> name), - Comp.hspacer(), - count.hide(hover.or(showing).or(focus)), - settings.hide(hover.not().and(showing.not()).and(focus.not())))); + expandButton, + Comp.hspacer(1), + Comp.of(() -> name).hgrow(), + Comp.hspacer(2), + count, + Comp.hspacer(7), + statusButton.hide(showStatus.not()))); h.padding(new Insets(0, 10, 0, (category.getDepth() * 10))); var categoryButton = new ButtonComp(null, h.createRegion(), category::select) + .focusTraversable() .styleClass("category-button") .apply(struc -> hover.bind(struc.get().hoverProperty())) .apply(struc -> focus.bind(struc.get().focusedProperty())) @@ -112,12 +137,21 @@ public class StoreCategoryComp extends SimpleComp { mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, keyEvent -> keyEvent.getCode() == KeyCode.SPACE, () -> createContextMenu(name))); + categoryButton.apply(struc -> { + struc.get().addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.SPACE) { + category.toggleExpanded(); + event.consume(); + } + }); + }); var l = category.getChildren() .sorted(Comparator.comparing(storeCategoryWrapper -> storeCategoryWrapper.nameProperty().getValue().toLowerCase(Locale.ROOT))); var children = new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false); + children.styleClass("children"); var hide = Bindings.createBooleanBinding(() -> { return !category.getExpanded().get() || category.getChildren().isEmpty(); @@ -128,6 +162,10 @@ public class StoreCategoryComp extends SimpleComp { StoreViewState.get().getActiveCategory().subscribe(val -> { struc.get().pseudoClassStateChanged(SELECTED, val.equals(category)); }); + + category.getColor().subscribe((c) -> { + DataColor.applyStyleClasses(c, struc.get()); + }); }); return v.createRegion(); @@ -135,6 +173,7 @@ public class StoreCategoryComp extends SimpleComp { private ContextMenu createContextMenu(Region text) { var contextMenu = ContextMenuHelper.create(); + AppFont.normal(contextMenu.getStyleableNode()); var newCategory = new MenuItem(AppI18n.get("newCategory"), new FontIcon("mdi2p-plus-thick")); newCategory.setOnAction(event -> { @@ -144,6 +183,25 @@ public class StoreCategoryComp extends SimpleComp { }); contextMenu.getItems().add(newCategory); + contextMenu.getItems().add(new SeparatorMenuItem()); + + var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill")); + var none = new MenuItem("None"); + none.setOnAction(event -> { + category.getCategory().setColor(null); + event.consume(); + }); + color.getItems().add(none); + Arrays.stream(DataColor.values()).forEach(dataStoreColor -> { + MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId())); + m.setOnAction(event -> { + category.getCategory().setColor(dataStoreColor); + event.consume(); + }); + color.getItems().add(m); + }); + contextMenu.getItems().add(color); + if (DataStorage.get().supportsSharing() && category.getCategory().canShare()) { var share = new MenuItem(); share.textProperty() @@ -162,7 +220,7 @@ public class StoreCategoryComp extends SimpleComp { if (category.getShare().getValue()) { return new FontIcon("mdi2b-block-helper"); } else { - return new FontIcon("mdi2s-share"); + return new FontIcon("mdi2g-git"); } }, category.getShare())); @@ -174,10 +232,13 @@ public class StoreCategoryComp extends SimpleComp { var rename = new MenuItem(AppI18n.get("rename"), new FontIcon("mdal-edit")); rename.setOnAction(event -> { + text.setDisable(false); text.requestFocus(); }); contextMenu.getItems().add(rename); + contextMenu.getItems().add(new SeparatorMenuItem()); + var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); del.setOnAction(event -> { category.delete(); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java b/app/src/main/java/io/xpipe/app/storage/DataColor.java similarity index 60% rename from app/src/main/java/io/xpipe/app/storage/DataStoreColor.java rename to app/src/main/java/io/xpipe/app/storage/DataColor.java index c3250d7f8..e629d09e6 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java +++ b/app/src/main/java/io/xpipe/app/storage/DataColor.java @@ -1,12 +1,15 @@ package io.xpipe.app.storage; -import javafx.scene.paint.Color; - import com.fasterxml.jackson.annotation.JsonProperty; +import javafx.scene.Node; +import javafx.scene.paint.Color; import lombok.Getter; +import java.util.ArrayList; +import java.util.Arrays; + @Getter -public enum DataStoreColor { +public enum DataColor { @JsonProperty("red") RED("red", "\uD83D\uDD34", Color.DARKRED), @@ -23,7 +26,7 @@ public enum DataStoreColor { private final String emoji; private final Color terminalColor; - DataStoreColor(String id, String emoji, Color terminalColor) { + DataColor(String id, String emoji, Color terminalColor) { this.id = id; this.emoji = emoji; this.terminalColor = terminalColor; @@ -38,4 +41,18 @@ public enum DataStoreColor { var value = terminalColor; return "#" + (format(value.getRed()) + format(value.getGreen()) + format(value.getBlue())).toUpperCase(); } + + public static void applyStyleClasses(DataColor color, Node node) { + var newList = new ArrayList<>(node.getStyleClass()); + newList.removeIf(s -> Arrays.stream(DataColor.values()) + .anyMatch( + dataStoreColor -> dataStoreColor.getId().equals(s))); + newList.remove("gray"); + if (color != null) { + newList.add(color.getId()); + } else { + newList.add("gray"); + } + node.getStyleClass().setAll(newList); + } } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 3a2827e3b..e16403f4c 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -172,6 +172,7 @@ public abstract class DataStorage { "Default", Instant.now(), Instant.now(), + null, true, ALL_CONNECTIONS_CATEGORY_UUID, StoreSortMode.getDefault(), @@ -693,6 +694,22 @@ public abstract class DataStorage { return false; } + public DataColor getEffectiveColor(DataStoreEntry entry) { + var root = getRootForEntry(entry); + if (root.getColor() != null) { + return root.getColor(); + } + + var cats = getCategoryParentHierarchy(getStoreCategoryIfPresent(entry.getCategoryUuid()).orElseThrow()); + for (DataStoreCategory cat : cats.reversed()) { + if (cat.getColor() != null) { + return cat.getColor(); + } + } + + return null; + } + public DataStoreEntry getRootForEntry(DataStoreEntry entry) { if (entry == null) { return null; diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java b/app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java index 82c8ed230..0a85c5040 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java @@ -1,5 +1,6 @@ package io.xpipe.app.storage; +import com.fasterxml.jackson.core.JsonProcessingException; import io.xpipe.app.comp.store.StoreSortMode; import io.xpipe.core.util.JacksonMapper; @@ -38,12 +39,13 @@ public class DataStoreCategory extends StorageElement { String name, Instant lastUsed, Instant lastModified, + DataColor color, boolean dirty, UUID parentCategory, StoreSortMode sortMode, boolean share, boolean expanded) { - super(directory, uuid, name, lastUsed, lastModified, expanded, dirty); + super(directory, uuid, name, lastUsed, lastModified, color, expanded, dirty); this.parentCategory = parentCategory; this.sortMode = sortMode; this.share = share; @@ -56,6 +58,7 @@ public class DataStoreCategory extends StorageElement { name, Instant.now(), Instant.now(), + null, true, parentCategory, StoreSortMode.getDefault(), @@ -70,6 +73,7 @@ public class DataStoreCategory extends StorageElement { name, Instant.now(), Instant.now(), + null, true, parentCategory, StoreSortMode.getDefault(), @@ -95,8 +99,17 @@ public class DataStoreCategory extends StorageElement { .filter(jsonNode -> !jsonNode.isNull()) .map(jsonNode -> UUID.fromString(jsonNode.textValue())) .orElse(null); - + var color = Optional.ofNullable(json.get("color")) + .map(node -> { + try { + return mapper.treeToValue(node, DataColor.class); + } catch (JsonProcessingException e) { + return null; + } + }) + .orElse(null); var name = json.required("name").textValue(); + var sortMode = Optional.ofNullable(stateJson.get("sortMode")) .map(JsonNode::asText) .flatMap(string -> StoreSortMode.fromId(string)) @@ -116,7 +129,7 @@ public class DataStoreCategory extends StorageElement { .orElse(true); return Optional.of( - new DataStoreCategory(dir, uuid, name, lastUsed, lastModified, false, parentUuid, sortMode, share, expanded)); + new DataStoreCategory(dir, uuid, name, lastUsed, lastModified, color, false, parentUuid, sortMode, share, expanded)); } public void setSortMode(StoreSortMode sortMode) { @@ -180,6 +193,7 @@ public class DataStoreCategory extends StorageElement { obj.put("uuid", uuid.toString()); obj.put("name", name); obj.put("share", share); + obj.set("color", mapper.valueToTree(color)); stateObj.put("lastUsed", lastUsed.toString()); stateObj.put("lastModified", lastModified.toString()); stateObj.put("sortMode", sortMode.getId()); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index 9bbb81579..e6cebf963 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -59,9 +59,6 @@ public class DataStoreEntry extends StorageElement { @NonFinal JsonNode storePersistentStateNode; - @NonFinal - DataStoreColor color; - @NonFinal @Setter Set childrenCache = null; @@ -86,16 +83,15 @@ public class DataStoreEntry extends StorageElement { Configuration configuration, JsonNode storePersistentState, boolean expanded, - DataStoreColor color, + DataColor color, String notes, Order explicitOrder) { - super(directory, uuid, name, lastUsed, lastModified, expanded, dirty); + super(directory, uuid, name, lastUsed, lastModified, color, expanded, dirty); this.categoryUuid = categoryUuid; this.store = store; this.storeNode = storeNode; this.validity = validity; this.configuration = configuration; - this.color = color; this.explicitOrder = explicitOrder; this.provider = store != null ? DataStoreProviders.byStore(store) : null; this.storePersistentStateNode = storePersistentState; @@ -111,7 +107,7 @@ public class DataStoreEntry extends StorageElement { Instant lastModified, DataStore store, Order explicitOrder) { - super(directory, uuid, name, lastUsed, lastModified, false,false); + super(directory, uuid, name, lastUsed, lastModified, null, false,false); this.categoryUuid = categoryUuid; this.store = store; this.explicitOrder = explicitOrder; @@ -119,7 +115,6 @@ public class DataStoreEntry extends StorageElement { this.validity = Validity.INCOMPLETE; this.configuration = Configuration.defaultConfiguration(); this.expanded = false; - this.color = null; this.provider = null; this.storePersistentStateNode = null; } @@ -225,7 +220,7 @@ public class DataStoreEntry extends StorageElement { var color = Optional.ofNullable(stateJson.get("color")) .map(node -> { try { - return mapper.treeToValue(node, DataStoreColor.class); + return mapper.treeToValue(node, DataColor.class); } catch (JsonProcessingException e) { return null; } @@ -372,9 +367,9 @@ public class DataStoreEntry extends StorageElement { obj.put("uuid", uuid.toString()); obj.put("name", name); obj.put("categoryUuid", categoryUuid.toString()); + obj.set("color", mapper.valueToTree(color)); stateObj.put("lastUsed", lastUsed.toString()); stateObj.put("lastModified", lastModified.toString()); - stateObj.set("color", mapper.valueToTree(color)); stateObj.set("persistentState", storePersistentStateNode); obj.set("configuration", mapper.valueToTree(configuration)); stateObj.put("expanded", expanded); @@ -405,14 +400,6 @@ public class DataStoreEntry extends StorageElement { } } - public void setColor(DataStoreColor newColor) { - var changed = !Objects.equals(color, newColor); - this.color = newColor; - if (changed) { - notifyUpdate(false, true); - } - } - public boolean isDisabled() { return validity == Validity.LOAD_FAILED; } diff --git a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java index 66703e001..247fa8b3c 100644 --- a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java @@ -30,6 +30,7 @@ public class ImpersistentStorage extends DataStorage { "Default", Instant.now(), Instant.now(), + null, true, ALL_CONNECTIONS_CATEGORY_UUID, StoreSortMode.getDefault(), diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java index 9191c2f91..b4f85473e 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -223,7 +223,7 @@ public class StandardStorage extends DataStorage { var local = DataStorage.get().getStoreEntry(LOCAL_ID); if (storeEntriesSet.stream().noneMatch(entry -> entry.getColor() != null)) { - local.setColor(DataStoreColor.BLUE); + local.setColor(DataColor.BLUE); } // Reload stores, this time with all entry refs present diff --git a/app/src/main/java/io/xpipe/app/storage/StorageElement.java b/app/src/main/java/io/xpipe/app/storage/StorageElement.java index 10bba2a66..641341b3a 100644 --- a/app/src/main/java/io/xpipe/app/storage/StorageElement.java +++ b/app/src/main/java/io/xpipe/app/storage/StorageElement.java @@ -13,6 +13,7 @@ import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; public abstract class StorageElement { @@ -42,13 +43,18 @@ public abstract class StorageElement { @Getter protected boolean expanded; + protected @NonFinal + @Getter DataColor color; + + public StorageElement( - Path directory, UUID uuid, String name, Instant lastUsed, Instant lastModified, boolean expanded, boolean dirty) { + Path directory, UUID uuid, String name, Instant lastUsed, Instant lastModified, DataColor color, boolean expanded, boolean dirty) { this.directory = directory; this.uuid = uuid; this.name = name; this.lastUsed = lastUsed; this.lastModified = lastModified; + this.color = color; this.expanded = expanded; this.dirty = dirty; } @@ -83,6 +89,14 @@ public abstract class StorageElement { FileUtils.deleteDirectory(directory.toFile()); } + public void setColor(DataColor newColor) { + var changed = !Objects.equals(color, newColor); + this.color = newColor; + if (changed) { + notifyUpdate(false, true); + } + } + public abstract void writeDataToDisk() throws Exception; public synchronized Instant getLastAccess() { diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index 2c12f062a..eab218332 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -7,7 +7,7 @@ import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.ExternalApplicationType; -import io.xpipe.app.storage.DataStoreColor; +import io.xpipe.app.storage.DataColor; import io.xpipe.app.util.*; import io.xpipe.core.process.*; import io.xpipe.core.store.FilePath; @@ -1103,7 +1103,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Value class LaunchConfiguration { - DataStoreColor color; + DataColor color; String coloredTitle; String cleanTitle; diff --git a/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java index 502d7359e..4cc84e72f 100644 --- a/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java +++ b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java @@ -49,7 +49,7 @@ public class TerminalLauncher { throw ErrorEvent.expected(new IllegalStateException(AppI18n.get("noTerminalSet"))); } - var color = entry != null ? DataStorage.get().getRootForEntry(entry).getColor() : null; + var color = entry != null ? DataStorage.get().getEffectiveColor(entry) : null; var prefix = entry != null && color != null && type.supportsColoredTitle() ? color.getEmoji() + " " : ""; var cleanTitle = (title != null ? title : entry != null ? entry.getName() : "?"); var adjustedTitle = prefix + cleanTitle; diff --git a/app/src/main/resources/io/xpipe/app/resources/style/category.css b/app/src/main/resources/io/xpipe/app/resources/style/category.css index cbf999d84..57881105b 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/category.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/category.css @@ -1,7 +1,8 @@ .category-button { - -fx-opacity: 0.8; - -fx-border-width: 0; -fx-background-color: transparent; + -fx-background-radius: 4px; + -fx-border-radius: 4px; + -fx-border-width: 1px; -fx-padding: 0 0 0 2; -fx-background-insets: 0; } @@ -10,31 +11,43 @@ -fx-background-color: transparent; } -.category-button .settings { - -fx-opacity: 1.0; -} - .category-button:hover, .root:key-navigation .category-button:focused { -fx-background-color: -color-bg-default; } .category:selected .category-button { - -fx-opacity: 1.0; - -fx-background-radius: 4px; - -fx-border-radius: 4px; - -fx-border-width: 1px; -fx-border-color: -color-border-default; -fx-background-color: -color-bg-default; } -.category .separator { - -fx-padding: 0 5 0 5; +.root:light .category.yellow > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: #888800; } - -.category .separator .line { - -fx-pref-height: 1; - -fx-background-color: -color-fg-default; - -fx-opacity: 0.5; +.root:light .category.green > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: #0d770d; } +.root:light .category.blue > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: #1c62be; +} + +.root:light .category.red > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: #a40000; +} + +.root:dark .category.yellow > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: yellow; +} + +.root:dark .category.green > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: green; +} + +.root:dark .category.blue > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: #397fd5; +} + +.root:dark .category.red > .category-button .expand-button .ikonli-font-icon { + -fx-icon-color: red; +}