implements PR-05b asset navigator search filters and selection
This commit is contained in:
parent
75dc37d7e7
commit
17064aa505
@ -80,10 +80,20 @@ public enum I18n {
|
||||
WORKSPACE_ASSETS("workspace.assets"),
|
||||
ASSETS_NAVIGATOR_TITLE("assets.navigator.title"),
|
||||
ASSETS_DETAILS_TITLE("assets.details.title"),
|
||||
ASSETS_SEARCH_PROMPT("assets.search.prompt"),
|
||||
ASSETS_FILTER_MANAGED("assets.filter.managed"),
|
||||
ASSETS_FILTER_ORPHAN("assets.filter.orphan"),
|
||||
ASSETS_FILTER_DIAGNOSTICS("assets.filter.diagnostics"),
|
||||
ASSETS_FILTER_PRELOAD("assets.filter.preload"),
|
||||
ASSETS_STATE_LOADING("assets.state.loading"),
|
||||
ASSETS_STATE_EMPTY("assets.state.empty"),
|
||||
ASSETS_STATE_NO_RESULTS("assets.state.noResults"),
|
||||
ASSETS_STATE_READY("assets.state.ready"),
|
||||
ASSETS_STATE_ERROR("assets.state.error"),
|
||||
ASSETS_BADGE_MANAGED("assets.badge.managed"),
|
||||
ASSETS_BADGE_ORPHAN("assets.badge.orphan"),
|
||||
ASSETS_BADGE_PRELOAD("assets.badge.preload"),
|
||||
ASSETS_BADGE_DIAGNOSTICS("assets.badge.diagnostics"),
|
||||
ASSETS_SUMMARY_LOADING("assets.summary.loading"),
|
||||
ASSETS_SUMMARY_EMPTY("assets.summary.empty"),
|
||||
ASSETS_SUMMARY_READY("assets.summary.ready"),
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetNavigatorFilter {
|
||||
MANAGED,
|
||||
ORPHAN,
|
||||
DIAGNOSTICS,
|
||||
PRELOAD
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetNavigatorGroup(String label, List<AssetWorkspaceAssetSummary> assets) {
|
||||
public AssetNavigatorGroup {
|
||||
label = Objects.requireNonNull(label, "label").trim();
|
||||
assets = List.copyOf(Objects.requireNonNull(assets, "assets"));
|
||||
if (label.isBlank()) {
|
||||
throw new IllegalArgumentException("group label must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetNavigatorProjection(List<AssetNavigatorGroup> groups, int visibleAssetCount) {
|
||||
public AssetNavigatorProjection {
|
||||
groups = List.copyOf(Objects.requireNonNull(groups, "groups"));
|
||||
if (visibleAssetCount < 0) {
|
||||
throw new IllegalArgumentException("visibleAssetCount must not be negative");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return visibleAssetCount == 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public final class AssetNavigatorProjectionBuilder {
|
||||
private AssetNavigatorProjectionBuilder() {
|
||||
}
|
||||
|
||||
public static AssetNavigatorProjection build(
|
||||
List<AssetWorkspaceAssetSummary> assets,
|
||||
Path assetsRoot,
|
||||
String searchQuery,
|
||||
Set<AssetNavigatorFilter> filters) {
|
||||
Objects.requireNonNull(assets, "assets");
|
||||
final Path normalizedAssetsRoot = Objects.requireNonNull(assetsRoot, "assetsRoot").toAbsolutePath().normalize();
|
||||
final String normalizedQuery = normalizeQuery(searchQuery);
|
||||
final Set<AssetNavigatorFilter> normalizedFilters = filters == null || filters.isEmpty()
|
||||
? EnumSet.noneOf(AssetNavigatorFilter.class)
|
||||
: EnumSet.copyOf(filters);
|
||||
|
||||
final Map<String, List<AssetWorkspaceAssetSummary>> grouped = new LinkedHashMap<>();
|
||||
for (AssetWorkspaceAssetSummary asset : assets) {
|
||||
if (!matchesFilters(asset, normalizedFilters) || !matchesQuery(asset, normalizedAssetsRoot, normalizedQuery)) {
|
||||
continue;
|
||||
}
|
||||
grouped.computeIfAbsent(groupLabel(asset, normalizedAssetsRoot), ignored -> new ArrayList<>())
|
||||
.add(asset);
|
||||
}
|
||||
|
||||
final List<AssetNavigatorGroup> groups = grouped.entrySet().stream()
|
||||
.map(entry -> new AssetNavigatorGroup(entry.getKey(), entry.getValue()))
|
||||
.toList();
|
||||
final int visibleAssetCount = groups.stream().mapToInt(group -> group.assets().size()).sum();
|
||||
return new AssetNavigatorProjection(groups, visibleAssetCount);
|
||||
}
|
||||
|
||||
static String relativeRoot(AssetWorkspaceAssetSummary asset, Path assetsRoot) {
|
||||
return relativize(asset.assetRoot(), assetsRoot).toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
private static boolean matchesFilters(AssetWorkspaceAssetSummary asset, Set<AssetNavigatorFilter> filters) {
|
||||
if (filters.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final boolean includeManaged = filters.contains(AssetNavigatorFilter.MANAGED);
|
||||
final boolean includeOrphan = filters.contains(AssetNavigatorFilter.ORPHAN);
|
||||
if (includeManaged || includeOrphan) {
|
||||
final boolean stateMatches = (includeManaged && asset.state() == AssetWorkspaceAssetState.MANAGED)
|
||||
|| (includeOrphan && asset.state() == AssetWorkspaceAssetState.ORPHAN);
|
||||
if (!stateMatches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.contains(AssetNavigatorFilter.DIAGNOSTICS) && !asset.hasDiagnostics()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.contains(AssetNavigatorFilter.PRELOAD) && !asset.preload()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean matchesQuery(AssetWorkspaceAssetSummary asset, Path assetsRoot, String normalizedQuery) {
|
||||
if (normalizedQuery.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final String relativeRoot = relativeRoot(asset, assetsRoot);
|
||||
return asset.assetName().toLowerCase(Locale.ROOT).contains(normalizedQuery)
|
||||
|| asset.assetFamily().toLowerCase(Locale.ROOT).contains(normalizedQuery)
|
||||
|| relativeRoot.toLowerCase(Locale.ROOT).contains(normalizedQuery);
|
||||
}
|
||||
|
||||
private static String groupLabel(AssetWorkspaceAssetSummary asset, Path assetsRoot) {
|
||||
final Path relativeRoot = relativize(asset.assetRoot(), assetsRoot);
|
||||
final Path parent = relativeRoot.getParent();
|
||||
if (parent == null) {
|
||||
return "assets";
|
||||
}
|
||||
return parent.toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
private static Path relativize(Path assetRoot, Path assetsRoot) {
|
||||
try {
|
||||
return assetsRoot.relativize(assetRoot.toAbsolutePath().normalize());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return assetRoot.getFileName();
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeQuery(String query) {
|
||||
return query == null ? "" : query.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,10 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SplitPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import p.studio.Container;
|
||||
import p.studio.events.*;
|
||||
import p.studio.projects.ProjectReference;
|
||||
@ -14,17 +12,27 @@ import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.Workspace;
|
||||
import p.studio.workspaces.WorkspaceId;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
|
||||
public final class AssetWorkspace implements Workspace {
|
||||
private final BorderPane root = new BorderPane();
|
||||
private final ProjectReference projectReference;
|
||||
private final AssetWorkspaceService assetWorkspaceService;
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
private final TextField searchField = new TextField();
|
||||
private final FlowPane filterBar = new FlowPane();
|
||||
private final Label navigatorStateLabel = new Label();
|
||||
private final VBox navigatorContent = new VBox(8);
|
||||
private final Label detailStateLabel = new Label();
|
||||
private final Label workspaceSummaryLabel = new Label();
|
||||
private final Map<AssetNavigatorFilter, ToggleButton> filterButtons = new EnumMap<>(AssetNavigatorFilter.class);
|
||||
private final EnumSet<AssetNavigatorFilter> activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class);
|
||||
|
||||
private String searchQuery = "";
|
||||
private volatile AssetWorkspaceState state = AssetWorkspaceState.loading(null);
|
||||
|
||||
public AssetWorkspace(ProjectReference projectReference) {
|
||||
@ -70,9 +78,27 @@ public final class AssetWorkspace implements Workspace {
|
||||
final Label navigatorTitle = new Label();
|
||||
navigatorTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_NAVIGATOR_TITLE));
|
||||
navigatorTitle.getStyleClass().add("assets-workspace-pane-title");
|
||||
searchField.setPromptText(Container.i18n().text(I18n.ASSETS_SEARCH_PROMPT));
|
||||
searchField.getStyleClass().add("assets-workspace-search");
|
||||
searchField.textProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
final String previous = oldValue == null ? "" : oldValue;
|
||||
final String current = newValue == null ? "" : newValue;
|
||||
if (previous.equals(current)) {
|
||||
return;
|
||||
}
|
||||
searchQuery = current;
|
||||
renderState();
|
||||
});
|
||||
configureFilterBar();
|
||||
navigatorStateLabel.getStyleClass().add("assets-workspace-pane-body");
|
||||
navigatorContent.getStyleClass().add("assets-workspace-navigator-content");
|
||||
final ScrollPane navigatorScroll = new ScrollPane(navigatorContent);
|
||||
navigatorScroll.setFitToWidth(true);
|
||||
navigatorScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
navigatorScroll.getStyleClass().add("assets-workspace-navigator-scroll");
|
||||
navigatorPane.getStyleClass().add("assets-workspace-pane");
|
||||
navigatorPane.getChildren().addAll(navigatorTitle, navigatorStateLabel);
|
||||
navigatorPane.getChildren().addAll(navigatorTitle, searchField, filterBar, navigatorStateLabel, navigatorScroll);
|
||||
VBox.setVgrow(navigatorScroll, Priority.ALWAYS);
|
||||
|
||||
final VBox detailsPane = new VBox(8);
|
||||
final Label detailsTitle = new Label();
|
||||
@ -90,6 +116,33 @@ public final class AssetWorkspace implements Workspace {
|
||||
return splitPane;
|
||||
}
|
||||
|
||||
private void configureFilterBar() {
|
||||
filterBar.setHgap(6);
|
||||
filterBar.setVgap(6);
|
||||
filterBar.setPadding(new Insets(4, 0, 4, 0));
|
||||
filterBar.getStyleClass().add("assets-workspace-filter-bar");
|
||||
addFilterButton(AssetNavigatorFilter.MANAGED, I18n.ASSETS_FILTER_MANAGED);
|
||||
addFilterButton(AssetNavigatorFilter.ORPHAN, I18n.ASSETS_FILTER_ORPHAN);
|
||||
addFilterButton(AssetNavigatorFilter.DIAGNOSTICS, I18n.ASSETS_FILTER_DIAGNOSTICS);
|
||||
addFilterButton(AssetNavigatorFilter.PRELOAD, I18n.ASSETS_FILTER_PRELOAD);
|
||||
}
|
||||
|
||||
private void addFilterButton(AssetNavigatorFilter filter, I18n i18n) {
|
||||
final ToggleButton button = new ToggleButton();
|
||||
button.textProperty().bind(Container.i18n().bind(i18n));
|
||||
button.getStyleClass().add("assets-workspace-filter-button");
|
||||
button.selectedProperty().addListener((ignored, oldValue, selected) -> {
|
||||
if (selected) {
|
||||
activeFilters.add(filter);
|
||||
} else {
|
||||
activeFilters.remove(filter);
|
||||
}
|
||||
renderState();
|
||||
});
|
||||
filterButtons.put(filter, button);
|
||||
filterBar.getChildren().add(button);
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
state = AssetWorkspaceState.loading(state);
|
||||
renderState();
|
||||
@ -117,22 +170,36 @@ public final class AssetWorkspace implements Workspace {
|
||||
switch (state.status()) {
|
||||
case LOADING -> {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_LOADING));
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_LOADING)));
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
||||
detailStateLabel.setText(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING));
|
||||
}
|
||||
case EMPTY -> {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_EMPTY));
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_EMPTY)));
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
|
||||
detailStateLabel.setText(Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY));
|
||||
}
|
||||
case ERROR -> {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_ERROR) + "\n\n" + state.errorMessage());
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(state.errorMessage()));
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
|
||||
detailStateLabel.setText(state.errorMessage());
|
||||
}
|
||||
case READY -> {
|
||||
final int assetCount = state.assets().size();
|
||||
navigatorStateLabel.setText(Container.i18n().format(I18n.ASSETS_STATE_READY, assetCount));
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
state.assets(),
|
||||
assetsRoot(),
|
||||
searchQuery,
|
||||
activeFilters);
|
||||
if (projection.isEmpty()) {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_NO_RESULTS));
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_NO_RESULTS)));
|
||||
} else {
|
||||
navigatorStateLabel.setText(Container.i18n().format(I18n.ASSETS_STATE_READY, projection.visibleAssetCount(), assetCount));
|
||||
renderNavigatorProjection(projection);
|
||||
}
|
||||
workspaceSummaryLabel.setText(Container.i18n().format(I18n.ASSETS_SUMMARY_READY, assetCount));
|
||||
final String selectedDescription = state.selectedAsset()
|
||||
.map(asset -> Container.i18n().format(
|
||||
@ -146,6 +213,103 @@ public final class AssetWorkspace implements Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
private void renderNavigatorProjection(AssetNavigatorProjection projection) {
|
||||
navigatorContent.getChildren().clear();
|
||||
for (AssetNavigatorGroup group : projection.groups()) {
|
||||
final VBox groupBox = new VBox(6);
|
||||
groupBox.getStyleClass().add("assets-workspace-group");
|
||||
|
||||
final Label groupLabel = new Label(group.label());
|
||||
groupLabel.getStyleClass().add("assets-workspace-group-label");
|
||||
groupBox.getChildren().add(groupLabel);
|
||||
|
||||
for (AssetWorkspaceAssetSummary asset : group.assets()) {
|
||||
groupBox.getChildren().add(createAssetRow(asset));
|
||||
}
|
||||
|
||||
navigatorContent.getChildren().add(groupBox);
|
||||
}
|
||||
}
|
||||
|
||||
private Node createAssetRow(AssetWorkspaceAssetSummary asset) {
|
||||
final VBox row = new VBox(4);
|
||||
row.getStyleClass().add("assets-workspace-asset-row");
|
||||
if (asset.selectionKey().equals(state.selectedKey())) {
|
||||
row.getStyleClass().add("assets-workspace-asset-row-selected");
|
||||
}
|
||||
|
||||
final HBox topLine = new HBox(8);
|
||||
topLine.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
|
||||
final Label icon = new Label(assetIcon(asset));
|
||||
icon.getStyleClass().add("assets-workspace-asset-icon");
|
||||
final Label name = new Label(asset.assetName());
|
||||
name.getStyleClass().add("assets-workspace-asset-name");
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
final FlowPane badges = new FlowPane();
|
||||
badges.setHgap(6);
|
||||
badges.setVgap(4);
|
||||
badges.getChildren().add(createBadge(
|
||||
asset.state() == AssetWorkspaceAssetState.MANAGED
|
||||
? Container.i18n().text(I18n.ASSETS_BADGE_MANAGED)
|
||||
: Container.i18n().text(I18n.ASSETS_BADGE_ORPHAN),
|
||||
asset.state() == AssetWorkspaceAssetState.MANAGED
|
||||
? "assets-workspace-badge-managed"
|
||||
: "assets-workspace-badge-orphan"));
|
||||
badges.getChildren().add(createBadge(asset.assetFamily(), "assets-workspace-badge-family"));
|
||||
if (asset.preload()) {
|
||||
badges.getChildren().add(createBadge(Container.i18n().text(I18n.ASSETS_BADGE_PRELOAD), "assets-workspace-badge-preload"));
|
||||
}
|
||||
if (asset.hasDiagnostics()) {
|
||||
badges.getChildren().add(createBadge(Container.i18n().text(I18n.ASSETS_BADGE_DIAGNOSTICS), "assets-workspace-badge-diagnostics"));
|
||||
}
|
||||
topLine.getChildren().addAll(icon, name, spacer, badges);
|
||||
|
||||
final Label path = new Label(AssetNavigatorProjectionBuilder.relativeRoot(asset, assetsRoot()));
|
||||
path.getStyleClass().add("assets-workspace-asset-path");
|
||||
row.getChildren().addAll(topLine, path);
|
||||
row.setOnMouseClicked(event -> selectAsset(asset.selectionKey()));
|
||||
return row;
|
||||
}
|
||||
|
||||
private Node createBadge(String text, String styleClass) {
|
||||
final Label badge = new Label(text);
|
||||
badge.getStyleClass().add("assets-workspace-badge");
|
||||
badge.getStyleClass().add(styleClass);
|
||||
return badge;
|
||||
}
|
||||
|
||||
private Node createNavigatorMessage(String text) {
|
||||
final Label label = new Label(text);
|
||||
label.getStyleClass().add("assets-workspace-empty-state");
|
||||
label.setWrapText(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
private void selectAsset(AssetWorkspaceSelectionKey selectionKey) {
|
||||
state = state.withSelection(selectionKey);
|
||||
renderState();
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey));
|
||||
}
|
||||
|
||||
private String assetIcon(AssetWorkspaceAssetSummary asset) {
|
||||
final String family = asset.assetFamily().toLowerCase();
|
||||
if (family.contains("image")) {
|
||||
return "🖼";
|
||||
}
|
||||
if (family.contains("sound") || family.contains("audio")) {
|
||||
return "🔊";
|
||||
}
|
||||
if (family.contains("palette")) {
|
||||
return "🎨";
|
||||
}
|
||||
return "◈";
|
||||
}
|
||||
|
||||
private Path assetsRoot() {
|
||||
return projectReference.rootPath().resolve("assets").toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private String rootCauseMessage(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current.getCause() != null) {
|
||||
|
||||
@ -70,10 +70,20 @@ workspace.shipper.button.clear=Clear
|
||||
workspace.assets=Assets
|
||||
assets.navigator.title=Asset Navigator
|
||||
assets.details.title=Selected Asset
|
||||
assets.search.prompt=Search assets by name or path
|
||||
assets.filter.managed=Managed
|
||||
assets.filter.orphan=Orphan
|
||||
assets.filter.diagnostics=Diagnostics
|
||||
assets.filter.preload=Preload
|
||||
assets.state.loading=Loading assets...
|
||||
assets.state.empty=No managed or orphan assets were found in this project.
|
||||
assets.state.ready={0} assets loaded.
|
||||
assets.state.noResults=No assets match the current search or filters.
|
||||
assets.state.ready={0} visible assets ({1} total).
|
||||
assets.state.error=Asset workspace failed to load.
|
||||
assets.badge.managed=Managed
|
||||
assets.badge.orphan=Orphan
|
||||
assets.badge.preload=Preload
|
||||
assets.badge.diagnostics=Diagnostics
|
||||
assets.summary.loading=Hydrating asset workspace state...
|
||||
assets.summary.empty=No assets are currently available.
|
||||
assets.summary.ready=Navigator ready with {0} assets.
|
||||
|
||||
@ -133,6 +133,34 @@
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-workspace-search {
|
||||
-fx-background-color: #101419;
|
||||
-fx-text-fill: #f2f7fb;
|
||||
-fx-prompt-text-fill: #7f8da0;
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-color: #283240;
|
||||
-fx-border-radius: 10;
|
||||
}
|
||||
|
||||
.assets-workspace-filter-bar {
|
||||
-fx-padding: 4 0 2 0;
|
||||
}
|
||||
|
||||
.assets-workspace-filter-button {
|
||||
-fx-background-color: #12161d;
|
||||
-fx-text-fill: #c7d5e5;
|
||||
-fx-background-radius: 999;
|
||||
-fx-border-radius: 999;
|
||||
-fx-border-color: #2d3948;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.assets-workspace-filter-button:selected {
|
||||
-fx-background-color: #24415e;
|
||||
-fx-border-color: #4d88bc;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.assets-workspace-summary {
|
||||
-fx-text-fill: #9ecbff;
|
||||
-fx-font-size: 12px;
|
||||
@ -143,6 +171,107 @@
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-workspace-navigator-scroll {
|
||||
-fx-background-color: transparent;
|
||||
-fx-fit-to-width: true;
|
||||
}
|
||||
|
||||
.assets-workspace-navigator-scroll > .viewport {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.assets-workspace-navigator-content {
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.assets-workspace-group {
|
||||
-fx-spacing: 6;
|
||||
}
|
||||
|
||||
.assets-workspace-group-label {
|
||||
-fx-text-fill: #82a9d1;
|
||||
-fx-font-size: 11px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-color: #242d38;
|
||||
-fx-padding: 10;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row:hover {
|
||||
-fx-background-color: #17202a;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-selected {
|
||||
-fx-background-color: #1d2c3c;
|
||||
-fx-border-color: #4f8dc3;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-icon {
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name {
|
||||
-fx-text-fill: #f6fbff;
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-path {
|
||||
-fx-text-fill: #9eacbb;
|
||||
-fx-font-size: 11px;
|
||||
}
|
||||
|
||||
.assets-workspace-badge {
|
||||
-fx-font-size: 10px;
|
||||
-fx-padding: 3 7 3 7;
|
||||
-fx-background-radius: 999;
|
||||
-fx-border-radius: 999;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-managed {
|
||||
-fx-background-color: #153425;
|
||||
-fx-border-color: #2f8f59;
|
||||
-fx-text-fill: #8ce3ae;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-orphan {
|
||||
-fx-background-color: #3a2d14;
|
||||
-fx-border-color: #bc8a31;
|
||||
-fx-text-fill: #ffd27a;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-family {
|
||||
-fx-background-color: #1c2733;
|
||||
-fx-border-color: #38506a;
|
||||
-fx-text-fill: #b7d8f8;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-preload {
|
||||
-fx-background-color: #271747;
|
||||
-fx-border-color: #7f65cf;
|
||||
-fx-text-fill: #d9cbff;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-diagnostics {
|
||||
-fx-background-color: #4a1a1c;
|
||||
-fx-border-color: #cf6268;
|
||||
-fx-text-fill: #ffb4b8;
|
||||
}
|
||||
|
||||
.assets-workspace-empty-state {
|
||||
-fx-text-fill: #c4ced8;
|
||||
-fx-font-size: 12px;
|
||||
-fx-wrap-text: true;
|
||||
-fx-padding: 10 0 0 0;
|
||||
}
|
||||
|
||||
.studio-project-launcher {
|
||||
-fx-background-color: linear-gradient(to bottom, #20242c, #14181d);
|
||||
}
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
final class AssetNavigatorProjectionBuilderTest {
|
||||
@Test
|
||||
void groupsAssetsByParentPath() {
|
||||
final Path assetsRoot = Path.of("/tmp/project/assets");
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||
assetsRoot,
|
||||
"",
|
||||
EnumSet.noneOf(AssetNavigatorFilter.class));
|
||||
|
||||
assertEquals(2, projection.visibleAssetCount());
|
||||
assertEquals(List.of("audio", "ui"), projection.groups().stream().map(AssetNavigatorGroup::label).sorted().toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void managedAndOrphanFiltersBehaveAsStateFilterSet() {
|
||||
final Path assetsRoot = Path.of("/tmp/project/assets");
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||
assetsRoot,
|
||||
"",
|
||||
EnumSet.of(AssetNavigatorFilter.MANAGED));
|
||||
|
||||
assertEquals(1, projection.visibleAssetCount());
|
||||
assertEquals("ui_atlas", projection.groups().getFirst().assets().getFirst().assetName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void diagnosticsAndPreloadActAsAdditionalConstraints() {
|
||||
final Path assetsRoot = Path.of("/tmp/project/assets");
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, true),
|
||||
managedAsset(2, "bg_tiles", "image_bank", assetsRoot.resolve("bg/tiles"), true, false),
|
||||
managedAsset(3, "voice_bank", "sound_bank", assetsRoot.resolve("audio/voice"), false, true)),
|
||||
assetsRoot,
|
||||
"",
|
||||
EnumSet.of(AssetNavigatorFilter.MANAGED, AssetNavigatorFilter.PRELOAD, AssetNavigatorFilter.DIAGNOSTICS));
|
||||
|
||||
assertEquals(1, projection.visibleAssetCount());
|
||||
assertEquals("ui_atlas", projection.groups().getFirst().assets().getFirst().assetName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchMatchesAssetNameAndPathContext() {
|
||||
final Path assetsRoot = Path.of("/tmp/project/assets");
|
||||
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false));
|
||||
|
||||
final AssetNavigatorProjection byName = AssetNavigatorProjectionBuilder.build(
|
||||
assets,
|
||||
assetsRoot,
|
||||
"atlas",
|
||||
EnumSet.noneOf(AssetNavigatorFilter.class));
|
||||
final AssetNavigatorProjection byPath = AssetNavigatorProjectionBuilder.build(
|
||||
assets,
|
||||
assetsRoot,
|
||||
"audio",
|
||||
EnumSet.noneOf(AssetNavigatorFilter.class));
|
||||
|
||||
assertEquals(1, byName.visibleAssetCount());
|
||||
assertEquals("ui_atlas", byName.groups().getFirst().assets().getFirst().assetName());
|
||||
assertEquals(1, byPath.visibleAssetCount());
|
||||
assertEquals("menu_sounds", byPath.groups().getFirst().assets().getFirst().assetName());
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetSummary managedAsset(
|
||||
int assetId,
|
||||
String name,
|
||||
String family,
|
||||
Path root,
|
||||
boolean preload,
|
||||
boolean hasDiagnostics) {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(assetId),
|
||||
name,
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
assetId,
|
||||
family,
|
||||
root,
|
||||
preload,
|
||||
hasDiagnostics);
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetSummary orphanAsset(
|
||||
String name,
|
||||
String family,
|
||||
Path root,
|
||||
boolean preload,
|
||||
boolean hasDiagnostics) {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.OrphanAsset(root),
|
||||
name,
|
||||
AssetWorkspaceAssetState.ORPHAN,
|
||||
null,
|
||||
family,
|
||||
root,
|
||||
preload,
|
||||
hasDiagnostics);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user