Rework categories

This commit is contained in:
crschnick 2024-08-22 17:34:36 +00:00
parent a65a0bd1b0
commit be684d7b72
19 changed files with 228 additions and 109 deletions

View file

@ -364,9 +364,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container"); StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
c.getStyleClass().add("color-box"); c.getStyleClass().add("color-box");
var color = DataStorage.get() var color = DataStorage.get().getEffectiveColor(model.getEntry().get());
.getRootForEntry(model.getEntry().get())
.getColor();
if (color != null) { if (color != null) {
c.getStyleClass().add(color.getId()); c.getStyleClass().add(color.getId());
} }

View file

@ -2,6 +2,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.property.Property; import javafx.beans.property.Property;
@ -15,7 +16,7 @@ import lombok.Value;
import java.util.Objects; import java.util.Objects;
public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> { public class LazyTextFieldComp extends Comp<CompStructure<TextField>> {
private final Property<String> currentValue; private final Property<String> currentValue;
private final Property<String> appliedValue; private final Property<String> appliedValue;
@ -26,8 +27,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
} }
@Override @Override
public LazyTextFieldComp.Structure createBase() { public CompStructure<TextField> createBase() {
var sp = new StackPane();
var r = new TextField(); var r = new TextField();
r.setOnKeyPressed(ke -> { r.setOnKeyPressed(ke -> {
@ -48,23 +48,14 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
} }
}); });
sp.focusedProperty().addListener((c, o, n) -> {
if (n) {
r.setDisable(false);
r.requestFocus();
}
});
// Handles external updates // Handles external updates
PlatformThread.sync(appliedValue).addListener((observable, oldValue, n) -> { PlatformThread.sync(appliedValue).addListener((observable, oldValue, n) -> {
currentValue.setValue(n); currentValue.setValue(n);
}); });
r.setPrefWidth(0); r.setMinWidth(0);
sp.getChildren().add(r);
sp.prefWidthProperty().bind(r.prefWidthProperty());
sp.prefHeightProperty().bind(r.prefHeightProperty());
r.setDisable(true); r.setDisable(true);
r.prefWidthProperty().bind(r.minWidthProperty());
currentValue.subscribe(n -> { currentValue.subscribe(n -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
@ -86,7 +77,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
} }
}); });
r.getStyleClass().add("lazy-text-field-comp"); r.getStyleClass().add("lazy-text-field-comp");
return new Structure(sp, r); return new SimpleCompStructure<>(r);
} }
@Value @Value

View file

@ -6,6 +6,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataColor;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -29,6 +30,7 @@ public class StoreCategoryWrapper {
private final ObservableList<StoreCategoryWrapper> children; private final ObservableList<StoreCategoryWrapper> children;
private final ObservableList<StoreEntryWrapper> containedEntries; private final ObservableList<StoreEntryWrapper> containedEntries;
private final BooleanProperty expanded = new SimpleBooleanProperty(); private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<DataColor> color = new SimpleObjectProperty<>();
public StoreCategoryWrapper(DataStoreCategory category) { public StoreCategoryWrapper(DataStoreCategory category) {
var d = 0; var d = 0;
@ -51,6 +53,7 @@ public class StoreCategoryWrapper {
this.share = new SimpleObjectProperty<>(category.isShare()); this.share = new SimpleObjectProperty<>(category.isShare());
this.children = FXCollections.observableArrayList(); this.children = FXCollections.observableArrayList();
this.containedEntries = FXCollections.observableArrayList(); this.containedEntries = FXCollections.observableArrayList();
this.color.setValue(category.getColor());
setupListeners(); setupListeners();
} }
@ -130,6 +133,7 @@ public class StoreCategoryWrapper {
sortMode.setValue(category.getSortMode()); sortMode.setValue(category.getSortMode());
share.setValue(category.isShare()); share.setValue(category.isShare());
expanded.setValue(category.isExpanded()); expanded.setValue(category.isExpanded());
color.setValue(category.getColor());
containedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream() containedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream()
.filter(entry -> { .filter(entry -> {

View file

@ -17,7 +17,7 @@ import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; 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.storage.DataStoreEntry;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
@ -345,7 +345,7 @@ public abstract class StoreEntryComp extends SimpleComp {
event.consume(); event.consume();
}); });
color.getItems().add(none); color.getItems().add(none);
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> { Arrays.stream(DataColor.values()).forEach(dataStoreColor -> {
MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId())); MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId()));
m.setOnAction(event -> { m.setOnAction(event -> {
getWrapper().getEntry().setColor(dataStoreColor); getWrapper().getEntry().setColor(dataStoreColor);

View file

@ -6,7 +6,7 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; 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.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
@ -36,7 +36,7 @@ public class StoreEntryWrapper {
private final BooleanProperty expanded = new SimpleBooleanProperty(); private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<Object> persistentState = new SimpleObjectProperty<>(); private final Property<Object> persistentState = new SimpleObjectProperty<>();
private final Property<Map<String, Object>> cache = new SimpleObjectProperty<>(Map.of()); private final Property<Map<String, Object>> cache = new SimpleObjectProperty<>(Map.of());
private final Property<DataStoreColor> color = new SimpleObjectProperty<>(); private final Property<DataColor> color = new SimpleObjectProperty<>();
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>(); private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<String> summary = new SimpleObjectProperty<>(); private final Property<String> summary = new SimpleObjectProperty<>();
private final Property<StoreNotes> notes; private final Property<StoreNotes> notes;

View file

@ -7,7 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; 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 io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -175,7 +175,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
} }
var newList = new ArrayList<>(struc.get().getStyleClass()); var newList = new ArrayList<>(struc.get().getStyleClass());
newList.removeIf(s -> Arrays.stream(DataStoreColor.values()) newList.removeIf(s -> Arrays.stream(DataColor.values())
.anyMatch( .anyMatch(
dataStoreColor -> dataStoreColor.getId().equals(s))); dataStoreColor -> dataStoreColor.getId().equals(s)));
newList.remove("gray"); newList.remove("gray");

View file

@ -8,7 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp; 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.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@ -168,7 +168,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
return; return;
} }
struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataStoreColor.values()) struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataColor.values())
.anyMatch(dataStoreColor -> .anyMatch(dataStoreColor ->
dataStoreColor.getId().equals(s))); dataStoreColor.getId().equals(s)));
struc.get().getStyleClass().remove("gray"); struc.get().getStyleClass().remove("gray");

View file

@ -214,8 +214,9 @@ public class AppWindowHelper {
var r = scene.getRoot(); var r = scene.getRoot();
if (r != null) { if (r != null) {
var acc = Platform.isAccessibilityActive(); var acc = Platform.isAccessibilityActive();
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), kb && !acc); // This property is broken on some systems
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("normal-navigation"), !kb && !acc); r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), kb);
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("normal-navigation"), !kb);
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), acc); r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), acc);
} }
}); });
@ -223,7 +224,8 @@ public class AppWindowHelper {
Platform.accessibilityActiveProperty().addListener((observable, oldValue, newValue) -> { Platform.accessibilityActiveProperty().addListener((observable, oldValue, newValue) -> {
var r = scene.getRoot(); var r = scene.getRoot();
if (r != null) { 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("normal-navigation"), false);
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), true); r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), true);
} }

View file

@ -12,25 +12,30 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.ContextMenuHelper; import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.DataStoreFormatter;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -45,43 +50,60 @@ public class StoreCategoryComp extends SimpleComp {
@Override @Override
protected Region createSimple() { 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() if (!DataStorage.get().supportsSharing()
|| !category.getCategory().canShare()) { || !category.getCategory().canShare()) {
var exp = category.getExpanded().get() && category.getChildren().size() > 0; return "mdi2a-account-lock";
return exp ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right";
} }
return category.getShare().getValue() ? "mdi2g-git" : "mdi2a-account-cancel"; return category.getShare().getValue() ? "mdi2g-git" : "mdi2a-account-cancel";
}, },
category.getShare(), category.getExpanded(), category.getChildren()); category.getShare(), hover);
var icon = new IconButtonComp(i, () -> { var statusButton = new IconButtonComp(statusIcon)
category.toggleExpanded();
})
.apply(struc -> AppFont.small(struc.get())) .apply(struc -> AppFont.small(struc.get()))
.apply(struc -> { .apply(struc -> {
struc.get().setAlignment(Pos.CENTER); 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); struc.get().setFocusTraversable(false);
}); hover.bind(struc.get().hoverProperty());
var name = new LazyTextFieldComp(category.nameProperty())
.apply(struc -> {
struc.get().prefWidthProperty().unbind();
struc.get().setPrefWidth(150);
struc.getTextField().minWidthProperty().bind(struc.get().widthProperty());
}) })
.styleClass("name")
.createRegion();
var showing = new SimpleBooleanProperty();
var settings = new IconButtonComp("mdomz-settings")
.styleClass("settings")
.apply(new ContextMenuAugment<>( .apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, null, () -> { mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, null, () -> {
var cm = createContextMenu(name); var cm = createContextMenu(name);
showing.bind(cm.showingProperty()); showing.bind(cm.showingProperty());
return cm; return cm;
})); }))
.styleClass("status-button");
var shownList = new DerivedObservableList<>(category.getContainedEntries(), true) var shownList = new DerivedObservableList<>(category.getContainedEntries(), true)
.filtered( .filtered(
storeEntryWrapper -> { storeEntryWrapper -> {
@ -91,18 +113,21 @@ public class StoreCategoryComp extends SimpleComp {
StoreViewState.get().getFilterString()) StoreViewState.get().getFilterString())
.getList(); .getList();
var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")"); 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 focus = new SimpleBooleanProperty();
var h = new HorizontalComp(List.of( var h = new HorizontalComp(List.of(
icon, expandButton,
Comp.hspacer(4), Comp.hspacer(1),
Comp.of(() -> name), Comp.of(() -> name).hgrow(),
Comp.hspacer(), Comp.hspacer(2),
count.hide(hover.or(showing).or(focus)), count,
settings.hide(hover.not().and(showing.not()).and(focus.not())))); Comp.hspacer(7),
statusButton.hide(showStatus.not())));
h.padding(new Insets(0, 10, 0, (category.getDepth() * 10))); h.padding(new Insets(0, 10, 0, (category.getDepth() * 10)));
var categoryButton = new ButtonComp(null, h.createRegion(), category::select) var categoryButton = new ButtonComp(null, h.createRegion(), category::select)
.focusTraversable()
.styleClass("category-button") .styleClass("category-button")
.apply(struc -> hover.bind(struc.get().hoverProperty())) .apply(struc -> hover.bind(struc.get().hoverProperty()))
.apply(struc -> focus.bind(struc.get().focusedProperty())) .apply(struc -> focus.bind(struc.get().focusedProperty()))
@ -112,12 +137,21 @@ public class StoreCategoryComp extends SimpleComp {
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
keyEvent -> keyEvent.getCode() == KeyCode.SPACE, keyEvent -> keyEvent.getCode() == KeyCode.SPACE,
() -> createContextMenu(name))); () -> 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() var l = category.getChildren()
.sorted(Comparator.comparing(storeCategoryWrapper -> .sorted(Comparator.comparing(storeCategoryWrapper ->
storeCategoryWrapper.nameProperty().getValue().toLowerCase(Locale.ROOT))); storeCategoryWrapper.nameProperty().getValue().toLowerCase(Locale.ROOT)));
var children = var children =
new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false); new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false);
children.styleClass("children");
var hide = Bindings.createBooleanBinding(() -> { var hide = Bindings.createBooleanBinding(() -> {
return !category.getExpanded().get() || category.getChildren().isEmpty(); return !category.getExpanded().get() || category.getChildren().isEmpty();
@ -128,6 +162,10 @@ public class StoreCategoryComp extends SimpleComp {
StoreViewState.get().getActiveCategory().subscribe(val -> { StoreViewState.get().getActiveCategory().subscribe(val -> {
struc.get().pseudoClassStateChanged(SELECTED, val.equals(category)); struc.get().pseudoClassStateChanged(SELECTED, val.equals(category));
}); });
category.getColor().subscribe((c) -> {
DataColor.applyStyleClasses(c, struc.get());
});
}); });
return v.createRegion(); return v.createRegion();
@ -135,6 +173,7 @@ public class StoreCategoryComp extends SimpleComp {
private ContextMenu createContextMenu(Region text) { private ContextMenu createContextMenu(Region text) {
var contextMenu = ContextMenuHelper.create(); var contextMenu = ContextMenuHelper.create();
AppFont.normal(contextMenu.getStyleableNode());
var newCategory = new MenuItem(AppI18n.get("newCategory"), new FontIcon("mdi2p-plus-thick")); var newCategory = new MenuItem(AppI18n.get("newCategory"), new FontIcon("mdi2p-plus-thick"));
newCategory.setOnAction(event -> { newCategory.setOnAction(event -> {
@ -144,6 +183,25 @@ public class StoreCategoryComp extends SimpleComp {
}); });
contextMenu.getItems().add(newCategory); 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()) { if (DataStorage.get().supportsSharing() && category.getCategory().canShare()) {
var share = new MenuItem(); var share = new MenuItem();
share.textProperty() share.textProperty()
@ -162,7 +220,7 @@ public class StoreCategoryComp extends SimpleComp {
if (category.getShare().getValue()) { if (category.getShare().getValue()) {
return new FontIcon("mdi2b-block-helper"); return new FontIcon("mdi2b-block-helper");
} else { } else {
return new FontIcon("mdi2s-share"); return new FontIcon("mdi2g-git");
} }
}, },
category.getShare())); category.getShare()));
@ -174,10 +232,13 @@ public class StoreCategoryComp extends SimpleComp {
var rename = new MenuItem(AppI18n.get("rename"), new FontIcon("mdal-edit")); var rename = new MenuItem(AppI18n.get("rename"), new FontIcon("mdal-edit"));
rename.setOnAction(event -> { rename.setOnAction(event -> {
text.setDisable(false);
text.requestFocus(); text.requestFocus();
}); });
contextMenu.getItems().add(rename); contextMenu.getItems().add(rename);
contextMenu.getItems().add(new SeparatorMenuItem());
var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.setOnAction(event -> { del.setOnAction(event -> {
category.delete(); category.delete();

View file

@ -1,12 +1,15 @@
package io.xpipe.app.storage; package io.xpipe.app.storage;
import javafx.scene.paint.Color;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import lombok.Getter; import lombok.Getter;
import java.util.ArrayList;
import java.util.Arrays;
@Getter @Getter
public enum DataStoreColor { public enum DataColor {
@JsonProperty("red") @JsonProperty("red")
RED("red", "\uD83D\uDD34", Color.DARKRED), RED("red", "\uD83D\uDD34", Color.DARKRED),
@ -23,7 +26,7 @@ public enum DataStoreColor {
private final String emoji; private final String emoji;
private final Color terminalColor; private final Color terminalColor;
DataStoreColor(String id, String emoji, Color terminalColor) { DataColor(String id, String emoji, Color terminalColor) {
this.id = id; this.id = id;
this.emoji = emoji; this.emoji = emoji;
this.terminalColor = terminalColor; this.terminalColor = terminalColor;
@ -38,4 +41,18 @@ public enum DataStoreColor {
var value = terminalColor; var value = terminalColor;
return "#" + (format(value.getRed()) + format(value.getGreen()) + format(value.getBlue())).toUpperCase(); 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);
}
} }

View file

@ -172,6 +172,7 @@ public abstract class DataStorage {
"Default", "Default",
Instant.now(), Instant.now(),
Instant.now(), Instant.now(),
null,
true, true,
ALL_CONNECTIONS_CATEGORY_UUID, ALL_CONNECTIONS_CATEGORY_UUID,
StoreSortMode.getDefault(), StoreSortMode.getDefault(),
@ -693,6 +694,22 @@ public abstract class DataStorage {
return false; 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) { public DataStoreEntry getRootForEntry(DataStoreEntry entry) {
if (entry == null) { if (entry == null) {
return null; return null;

View file

@ -1,5 +1,6 @@
package io.xpipe.app.storage; package io.xpipe.app.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.xpipe.app.comp.store.StoreSortMode; import io.xpipe.app.comp.store.StoreSortMode;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
@ -38,12 +39,13 @@ public class DataStoreCategory extends StorageElement {
String name, String name,
Instant lastUsed, Instant lastUsed,
Instant lastModified, Instant lastModified,
DataColor color,
boolean dirty, boolean dirty,
UUID parentCategory, UUID parentCategory,
StoreSortMode sortMode, StoreSortMode sortMode,
boolean share, boolean share,
boolean expanded) { boolean expanded) {
super(directory, uuid, name, lastUsed, lastModified, expanded, dirty); super(directory, uuid, name, lastUsed, lastModified, color, expanded, dirty);
this.parentCategory = parentCategory; this.parentCategory = parentCategory;
this.sortMode = sortMode; this.sortMode = sortMode;
this.share = share; this.share = share;
@ -56,6 +58,7 @@ public class DataStoreCategory extends StorageElement {
name, name,
Instant.now(), Instant.now(),
Instant.now(), Instant.now(),
null,
true, true,
parentCategory, parentCategory,
StoreSortMode.getDefault(), StoreSortMode.getDefault(),
@ -70,6 +73,7 @@ public class DataStoreCategory extends StorageElement {
name, name,
Instant.now(), Instant.now(),
Instant.now(), Instant.now(),
null,
true, true,
parentCategory, parentCategory,
StoreSortMode.getDefault(), StoreSortMode.getDefault(),
@ -95,8 +99,17 @@ public class DataStoreCategory extends StorageElement {
.filter(jsonNode -> !jsonNode.isNull()) .filter(jsonNode -> !jsonNode.isNull())
.map(jsonNode -> UUID.fromString(jsonNode.textValue())) .map(jsonNode -> UUID.fromString(jsonNode.textValue()))
.orElse(null); .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 name = json.required("name").textValue();
var sortMode = Optional.ofNullable(stateJson.get("sortMode")) var sortMode = Optional.ofNullable(stateJson.get("sortMode"))
.map(JsonNode::asText) .map(JsonNode::asText)
.flatMap(string -> StoreSortMode.fromId(string)) .flatMap(string -> StoreSortMode.fromId(string))
@ -116,7 +129,7 @@ public class DataStoreCategory extends StorageElement {
.orElse(true); .orElse(true);
return Optional.of( 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) { public void setSortMode(StoreSortMode sortMode) {
@ -180,6 +193,7 @@ public class DataStoreCategory extends StorageElement {
obj.put("uuid", uuid.toString()); obj.put("uuid", uuid.toString());
obj.put("name", name); obj.put("name", name);
obj.put("share", share); obj.put("share", share);
obj.set("color", mapper.valueToTree(color));
stateObj.put("lastUsed", lastUsed.toString()); stateObj.put("lastUsed", lastUsed.toString());
stateObj.put("lastModified", lastModified.toString()); stateObj.put("lastModified", lastModified.toString());
stateObj.put("sortMode", sortMode.getId()); stateObj.put("sortMode", sortMode.getId());

View file

@ -59,9 +59,6 @@ public class DataStoreEntry extends StorageElement {
@NonFinal @NonFinal
JsonNode storePersistentStateNode; JsonNode storePersistentStateNode;
@NonFinal
DataStoreColor color;
@NonFinal @NonFinal
@Setter @Setter
Set<DataStoreEntry> childrenCache = null; Set<DataStoreEntry> childrenCache = null;
@ -86,16 +83,15 @@ public class DataStoreEntry extends StorageElement {
Configuration configuration, Configuration configuration,
JsonNode storePersistentState, JsonNode storePersistentState,
boolean expanded, boolean expanded,
DataStoreColor color, DataColor color,
String notes, String notes,
Order explicitOrder) { Order explicitOrder) {
super(directory, uuid, name, lastUsed, lastModified, expanded, dirty); super(directory, uuid, name, lastUsed, lastModified, color, expanded, dirty);
this.categoryUuid = categoryUuid; this.categoryUuid = categoryUuid;
this.store = store; this.store = store;
this.storeNode = storeNode; this.storeNode = storeNode;
this.validity = validity; this.validity = validity;
this.configuration = configuration; this.configuration = configuration;
this.color = color;
this.explicitOrder = explicitOrder; this.explicitOrder = explicitOrder;
this.provider = store != null ? DataStoreProviders.byStore(store) : null; this.provider = store != null ? DataStoreProviders.byStore(store) : null;
this.storePersistentStateNode = storePersistentState; this.storePersistentStateNode = storePersistentState;
@ -111,7 +107,7 @@ public class DataStoreEntry extends StorageElement {
Instant lastModified, Instant lastModified,
DataStore store, DataStore store,
Order explicitOrder) { Order explicitOrder) {
super(directory, uuid, name, lastUsed, lastModified, false,false); super(directory, uuid, name, lastUsed, lastModified, null, false,false);
this.categoryUuid = categoryUuid; this.categoryUuid = categoryUuid;
this.store = store; this.store = store;
this.explicitOrder = explicitOrder; this.explicitOrder = explicitOrder;
@ -119,7 +115,6 @@ public class DataStoreEntry extends StorageElement {
this.validity = Validity.INCOMPLETE; this.validity = Validity.INCOMPLETE;
this.configuration = Configuration.defaultConfiguration(); this.configuration = Configuration.defaultConfiguration();
this.expanded = false; this.expanded = false;
this.color = null;
this.provider = null; this.provider = null;
this.storePersistentStateNode = null; this.storePersistentStateNode = null;
} }
@ -225,7 +220,7 @@ public class DataStoreEntry extends StorageElement {
var color = Optional.ofNullable(stateJson.get("color")) var color = Optional.ofNullable(stateJson.get("color"))
.map(node -> { .map(node -> {
try { try {
return mapper.treeToValue(node, DataStoreColor.class); return mapper.treeToValue(node, DataColor.class);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
return null; return null;
} }
@ -372,9 +367,9 @@ public class DataStoreEntry extends StorageElement {
obj.put("uuid", uuid.toString()); obj.put("uuid", uuid.toString());
obj.put("name", name); obj.put("name", name);
obj.put("categoryUuid", categoryUuid.toString()); obj.put("categoryUuid", categoryUuid.toString());
obj.set("color", mapper.valueToTree(color));
stateObj.put("lastUsed", lastUsed.toString()); stateObj.put("lastUsed", lastUsed.toString());
stateObj.put("lastModified", lastModified.toString()); stateObj.put("lastModified", lastModified.toString());
stateObj.set("color", mapper.valueToTree(color));
stateObj.set("persistentState", storePersistentStateNode); stateObj.set("persistentState", storePersistentStateNode);
obj.set("configuration", mapper.valueToTree(configuration)); obj.set("configuration", mapper.valueToTree(configuration));
stateObj.put("expanded", expanded); 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() { public boolean isDisabled() {
return validity == Validity.LOAD_FAILED; return validity == Validity.LOAD_FAILED;
} }

View file

@ -30,6 +30,7 @@ public class ImpersistentStorage extends DataStorage {
"Default", "Default",
Instant.now(), Instant.now(),
Instant.now(), Instant.now(),
null,
true, true,
ALL_CONNECTIONS_CATEGORY_UUID, ALL_CONNECTIONS_CATEGORY_UUID,
StoreSortMode.getDefault(), StoreSortMode.getDefault(),

View file

@ -223,7 +223,7 @@ public class StandardStorage extends DataStorage {
var local = DataStorage.get().getStoreEntry(LOCAL_ID); var local = DataStorage.get().getStoreEntry(LOCAL_ID);
if (storeEntriesSet.stream().noneMatch(entry -> entry.getColor() != null)) { 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 // Reload stores, this time with all entry refs present

View file

@ -13,6 +13,7 @@ import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
public abstract class StorageElement { public abstract class StorageElement {
@ -42,13 +43,18 @@ public abstract class StorageElement {
@Getter @Getter
protected boolean expanded; protected boolean expanded;
protected @NonFinal
@Getter DataColor color;
public StorageElement( 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.directory = directory;
this.uuid = uuid; this.uuid = uuid;
this.name = name; this.name = name;
this.lastUsed = lastUsed; this.lastUsed = lastUsed;
this.lastModified = lastModified; this.lastModified = lastModified;
this.color = color;
this.expanded = expanded; this.expanded = expanded;
this.dirty = dirty; this.dirty = dirty;
} }
@ -83,6 +89,14 @@ public abstract class StorageElement {
FileUtils.deleteDirectory(directory.toFile()); 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 abstract void writeDataToDisk() throws Exception;
public synchronized Instant getLastAccess() { public synchronized Instant getLastAccess() {

View file

@ -7,7 +7,7 @@ import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.ExternalApplicationType; 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.app.util.*;
import io.xpipe.core.process.*; import io.xpipe.core.process.*;
import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FilePath;
@ -1103,7 +1103,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Value @Value
class LaunchConfiguration { class LaunchConfiguration {
DataStoreColor color; DataColor color;
String coloredTitle; String coloredTitle;
String cleanTitle; String cleanTitle;

View file

@ -49,7 +49,7 @@ public class TerminalLauncher {
throw ErrorEvent.expected(new IllegalStateException(AppI18n.get("noTerminalSet"))); 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 prefix = entry != null && color != null && type.supportsColoredTitle() ? color.getEmoji() + " " : "";
var cleanTitle = (title != null ? title : entry != null ? entry.getName() : "?"); var cleanTitle = (title != null ? title : entry != null ? entry.getName() : "?");
var adjustedTitle = prefix + cleanTitle; var adjustedTitle = prefix + cleanTitle;

View file

@ -1,7 +1,8 @@
.category-button { .category-button {
-fx-opacity: 0.8;
-fx-border-width: 0;
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-radius: 4px;
-fx-border-radius: 4px;
-fx-border-width: 1px;
-fx-padding: 0 0 0 2; -fx-padding: 0 0 0 2;
-fx-background-insets: 0; -fx-background-insets: 0;
} }
@ -10,31 +11,43 @@
-fx-background-color: transparent; -fx-background-color: transparent;
} }
.category-button .settings {
-fx-opacity: 1.0;
}
.category-button:hover, .root:key-navigation .category-button:focused { .category-button:hover, .root:key-navigation .category-button:focused {
-fx-background-color: -color-bg-default; -fx-background-color: -color-bg-default;
} }
.category:selected .category-button { .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-border-color: -color-border-default;
-fx-background-color: -color-bg-default; -fx-background-color: -color-bg-default;
} }
.category .separator { .root:light .category.yellow > .category-button .expand-button .ikonli-font-icon {
-fx-padding: 0 5 0 5; -fx-icon-color: #888800;
} }
.root:light .category.green > .category-button .expand-button .ikonli-font-icon {
.category .separator .line { -fx-icon-color: #0d770d;
-fx-pref-height: 1;
-fx-background-color: -color-fg-default;
-fx-opacity: 0.5;
} }
.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;
}