implements PR-07a assets event topology and lifecycle foundation
This commit is contained in:
parent
e7670b5474
commit
c351814b6f
@ -0,0 +1,15 @@
|
|||||||
|
package p.studio.events;
|
||||||
|
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.workspaces.assets.AssetWorkspaceDetailsViewState;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record StudioAssetsDetailsViewStateChangedEvent(
|
||||||
|
ProjectReference project,
|
||||||
|
AssetWorkspaceDetailsViewState viewState) implements StudioEvent {
|
||||||
|
public StudioAssetsDetailsViewStateChangedEvent {
|
||||||
|
Objects.requireNonNull(project, "project");
|
||||||
|
Objects.requireNonNull(viewState, "viewState");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package p.studio.events;
|
||||||
|
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.workspaces.assets.AssetWorkspaceNavigatorViewState;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record StudioAssetsNavigatorViewStateChangedEvent(
|
||||||
|
ProjectReference project,
|
||||||
|
AssetWorkspaceNavigatorViewState viewState) implements StudioEvent {
|
||||||
|
public StudioAssetsNavigatorViewStateChangedEvent {
|
||||||
|
Objects.requireNonNull(project, "project");
|
||||||
|
Objects.requireNonNull(viewState, "viewState");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,7 +36,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public final class AssetWorkspace implements Workspace {
|
public final class AssetWorkspace implements Workspace, AssetWorkspaceInteractionPort {
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
private final BorderPane root = new BorderPane();
|
private final BorderPane root = new BorderPane();
|
||||||
@ -48,23 +48,14 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
private final FileSystemPackerBuildService packService;
|
private final FileSystemPackerBuildService packService;
|
||||||
private final StudioWorkspaceEventBus workspaceBus;
|
private final StudioWorkspaceEventBus workspaceBus;
|
||||||
|
|
||||||
private final TextField searchField = new TextField();
|
|
||||||
private final FlowPane filterBar = new FlowPane();
|
|
||||||
private final Button addAssetButton = new Button();
|
private final Button addAssetButton = new Button();
|
||||||
private final Button doctorButton = new Button();
|
private final Button doctorButton = new Button();
|
||||||
private final Button packButton = new Button();
|
private final Button packButton = new Button();
|
||||||
private final Label navigatorStateLabel = new Label();
|
|
||||||
private final VBox navigatorContent = new VBox(8);
|
|
||||||
private final Label inlineProgressLabel = new Label();
|
private final Label inlineProgressLabel = new Label();
|
||||||
private final ProgressBar inlineProgressBar = new ProgressBar();
|
private final ProgressBar inlineProgressBar = new ProgressBar();
|
||||||
|
|
||||||
private final VBox detailsContent = new VBox(12);
|
|
||||||
private final Label workspaceSummaryLabel = new Label();
|
|
||||||
private final TextArea logsArea = new TextArea();
|
private final TextArea logsArea = new TextArea();
|
||||||
private final ScrollPane detailsScroll = new ScrollPane();
|
private final AssetWorkspaceNavigatorControl navigatorControl;
|
||||||
|
private final AssetWorkspaceDetailsControl detailsControl;
|
||||||
private final Map<AssetNavigatorFilter, ToggleButton> filterButtons = new EnumMap<>(AssetNavigatorFilter.class);
|
|
||||||
private final Map<AssetWorkspaceSelectionKey, VBox> assetRowsBySelectionKey = new HashMap<>();
|
|
||||||
private final EnumSet<AssetNavigatorFilter> activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class);
|
private final EnumSet<AssetNavigatorFilter> activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class);
|
||||||
|
|
||||||
private volatile AssetWorkspaceState state = AssetWorkspaceState.loading(null);
|
private volatile AssetWorkspaceState state = AssetWorkspaceState.loading(null);
|
||||||
@ -115,6 +106,8 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
new p.packer.declarations.PackerAssetDetailsService(),
|
new p.packer.declarations.PackerAssetDetailsService(),
|
||||||
packerEventAdapter);
|
packerEventAdapter);
|
||||||
this.packService = new FileSystemPackerBuildService(new p.packer.building.PackerBuildPlanner(), packerEventAdapter);
|
this.packService = new FileSystemPackerBuildService(new p.packer.building.PackerBuildPlanner(), packerEventAdapter);
|
||||||
|
this.navigatorControl = new AssetWorkspaceNavigatorControl(this.projectReference, this.workspaceBus, this);
|
||||||
|
this.detailsControl = new AssetWorkspaceDetailsControl(this.projectReference, this.workspaceBus, this);
|
||||||
|
|
||||||
subscribeLocalEvents();
|
subscribeLocalEvents();
|
||||||
root.getStyleClass().add("assets-workspace");
|
root.getStyleClass().add("assets-workspace");
|
||||||
@ -157,16 +150,6 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
applySelectionRequest(event.selectionKey());
|
applySelectionRequest(event.selectionKey());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
workspaceBus.subscribe(StudioAssetsNavigatorRedrawRequestedEvent.class, event -> {
|
|
||||||
if (projectMatches(event.project())) {
|
|
||||||
renderNavigator();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
workspaceBus.subscribe(StudioAssetsDetailsRedrawRequestedEvent.class, event -> {
|
|
||||||
if (projectMatches(event.project())) {
|
|
||||||
renderDetails();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
workspaceBus.subscribe(StudioAssetsAssetSummaryPatchedEvent.class, event -> {
|
workspaceBus.subscribe(StudioAssetsAssetSummaryPatchedEvent.class, event -> {
|
||||||
if (projectMatches(event.project())) {
|
if (projectMatches(event.project())) {
|
||||||
applyAssetSummaryPatch(event.summary());
|
applyAssetSummaryPatch(event.summary());
|
||||||
@ -186,25 +169,6 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
inlineProgressBar.setManaged(false);
|
inlineProgressBar.setManaged(false);
|
||||||
final VBox topProgress = new VBox(6, inlineProgressLabel, inlineProgressBar);
|
final VBox topProgress = new VBox(6, inlineProgressLabel, inlineProgressBar);
|
||||||
topProgress.getStyleClass().add("assets-workspace-inline-progress");
|
topProgress.getStyleClass().add("assets-workspace-inline-progress");
|
||||||
|
|
||||||
final VBox navigatorPane = new VBox(8);
|
|
||||||
navigatorPane.getStyleClass().add("assets-workspace-pane");
|
|
||||||
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)) {
|
|
||||||
searchQuery = current;
|
|
||||||
requestNavigatorRedraw();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
configureFilterBar();
|
|
||||||
addAssetButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_NAVIGATOR_ACTION_ADD));
|
addAssetButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_NAVIGATOR_ACTION_ADD));
|
||||||
addAssetButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
addAssetButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||||
addAssetButton.setMaxWidth(Double.MAX_VALUE);
|
addAssetButton.setMaxWidth(Double.MAX_VALUE);
|
||||||
@ -215,35 +179,7 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
packButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_NAVIGATOR_ACTION_PACK));
|
packButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_NAVIGATOR_ACTION_PACK));
|
||||||
packButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
packButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||||
packButton.setOnAction(event -> runPack());
|
packButton.setOnAction(event -> runPack());
|
||||||
navigatorStateLabel.getStyleClass().add("assets-workspace-pane-body");
|
final SplitPane splitPane = new SplitPane(navigatorControl, detailsControl);
|
||||||
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.getChildren().addAll(
|
|
||||||
navigatorTitle,
|
|
||||||
searchField,
|
|
||||||
filterBar,
|
|
||||||
navigatorStateLabel,
|
|
||||||
navigatorScroll);
|
|
||||||
VBox.setVgrow(navigatorScroll, Priority.ALWAYS);
|
|
||||||
|
|
||||||
final VBox detailsPane = new VBox(10);
|
|
||||||
detailsPane.getStyleClass().add("assets-workspace-pane");
|
|
||||||
final Label detailsTitle = new Label();
|
|
||||||
detailsTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_DETAILS_TITLE));
|
|
||||||
detailsTitle.getStyleClass().add("assets-workspace-pane-title");
|
|
||||||
workspaceSummaryLabel.getStyleClass().add("assets-workspace-summary");
|
|
||||||
detailsContent.getStyleClass().add("assets-workspace-details-content");
|
|
||||||
detailsScroll.setContent(detailsContent);
|
|
||||||
detailsScroll.setFitToWidth(true);
|
|
||||||
detailsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
|
||||||
detailsScroll.getStyleClass().add("assets-workspace-details-scroll");
|
|
||||||
detailsPane.getChildren().addAll(detailsTitle, workspaceSummaryLabel, detailsScroll);
|
|
||||||
VBox.setVgrow(detailsScroll, Priority.ALWAYS);
|
|
||||||
|
|
||||||
final SplitPane splitPane = new SplitPane(navigatorPane, detailsPane);
|
|
||||||
splitPane.setDividerPositions(0.34);
|
splitPane.setDividerPositions(0.34);
|
||||||
splitPane.getStyleClass().add("assets-workspace-split");
|
splitPane.getStyleClass().add("assets-workspace-split");
|
||||||
final HBox workspaceActionBar = new HBox(8, addAssetButton, doctorButton, packButton);
|
final HBox workspaceActionBar = new HBox(8, addAssetButton, doctorButton, packButton);
|
||||||
@ -269,33 +205,6 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
return pane;
|
return pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
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.REGISTERED, I18n.ASSETS_FILTER_REGISTERED);
|
|
||||||
addFilterButton(AssetNavigatorFilter.UNREGISTERED, I18n.ASSETS_FILTER_UNREGISTERED);
|
|
||||||
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().addAll("studio-button", "studio-button-secondary", "studio-button-pill", "studio-button-toggle");
|
|
||||||
button.selectedProperty().addListener((ignored, oldValue, selected) -> {
|
|
||||||
if (selected) {
|
|
||||||
activeFilters.add(filter);
|
|
||||||
} else {
|
|
||||||
activeFilters.remove(filter);
|
|
||||||
}
|
|
||||||
requestNavigatorRedraw();
|
|
||||||
});
|
|
||||||
filterButtons.put(filter, button);
|
|
||||||
filterBar.getChildren().add(button);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refresh() {
|
private void refresh() {
|
||||||
final boolean preserveVisibleContent = hasVisibleWorkspaceContent();
|
final boolean preserveVisibleContent = hasVisibleWorkspaceContent();
|
||||||
if (!preserveVisibleContent) {
|
if (!preserveVisibleContent) {
|
||||||
@ -411,260 +320,53 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void renderState() {
|
private void renderState() {
|
||||||
renderNavigator();
|
publishNavigatorViewState();
|
||||||
renderDetails();
|
publishDetailsViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestRedraw() {
|
private void requestRedraw() {
|
||||||
requestNavigatorRedraw();
|
publishNavigatorViewState();
|
||||||
requestDetailsRedraw();
|
publishDetailsViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestNavigatorRedraw() {
|
private void requestNavigatorRedraw() {
|
||||||
workspaceBus.publish(new StudioAssetsNavigatorRedrawRequestedEvent(projectReference));
|
publishNavigatorViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestDetailsRedraw() {
|
private void requestDetailsRedraw() {
|
||||||
workspaceBus.publish(new StudioAssetsDetailsRedrawRequestedEvent(projectReference));
|
publishDetailsViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderNavigator() {
|
private void publishNavigatorViewState() {
|
||||||
assetRowsBySelectionKey.clear();
|
final AssetNavigatorProjection projection = state.status() == AssetWorkspaceStatus.READY
|
||||||
switch (state.status()) {
|
? AssetNavigatorProjectionBuilder.build(state.assets(), projectRoot(), searchQuery, activeFilters)
|
||||||
case LOADING -> {
|
: null;
|
||||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_LOADING));
|
workspaceBus.publish(new StudioAssetsNavigatorViewStateChangedEvent(
|
||||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_LOADING)));
|
projectReference,
|
||||||
}
|
new AssetWorkspaceNavigatorViewState(state, projection, navigatorMessage(projection))));
|
||||||
case EMPTY -> {
|
|
||||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_EMPTY));
|
|
||||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_EMPTY)));
|
|
||||||
}
|
|
||||||
case ERROR -> {
|
|
||||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_ERROR) + "\n\n" + state.errorMessage());
|
|
||||||
navigatorContent.getChildren().setAll(createNavigatorMessage(state.errorMessage()));
|
|
||||||
}
|
|
||||||
case READY -> {
|
|
||||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
|
||||||
state.assets(),
|
|
||||||
projectRoot(),
|
|
||||||
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(), state.assets().size()));
|
|
||||||
renderNavigatorProjection(projection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderDetails() {
|
private void publishDetailsViewState() {
|
||||||
detailsContent.getChildren().clear();
|
workspaceBus.publish(new StudioAssetsDetailsViewStateChangedEvent(
|
||||||
switch (state.status()) {
|
projectReference,
|
||||||
case LOADING -> {
|
new AssetWorkspaceDetailsViewState(
|
||||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
state,
|
||||||
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)));
|
detailsStatus,
|
||||||
}
|
selectedAssetDetails,
|
||||||
case EMPTY -> {
|
detailsErrorMessage,
|
||||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
|
stagedMutationPreview,
|
||||||
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)));
|
selectedPreviewInput,
|
||||||
}
|
selectedPreviewZoom)));
|
||||||
case ERROR -> {
|
|
||||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
|
|
||||||
detailsContent.getChildren().add(createSectionMessage(state.errorMessage()));
|
|
||||||
}
|
|
||||||
case READY -> {
|
|
||||||
workspaceSummaryLabel.setText(Container.i18n().format(I18n.ASSETS_SUMMARY_READY, state.assets().size()));
|
|
||||||
state.selectedAsset()
|
|
||||||
.ifPresentOrElse(this::renderSelectedAssetDetails, () ->
|
|
||||||
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderSelectedAssetDetails(AssetWorkspaceAssetSummary summary) {
|
private String navigatorMessage(AssetNavigatorProjection projection) {
|
||||||
detailsContent.getChildren().add(createSummaryActionsRow(summary));
|
return switch (state.status()) {
|
||||||
if (detailsStatus == AssetWorkspaceDetailsStatus.LOADING) {
|
case LOADING -> Container.i18n().text(I18n.ASSETS_STATE_LOADING);
|
||||||
detailsContent.getChildren().add(createSection(
|
case EMPTY -> Container.i18n().text(I18n.ASSETS_STATE_EMPTY);
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT),
|
case ERROR -> state.errorMessage();
|
||||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
case READY -> projection == null || projection.isEmpty()
|
||||||
detailsContent.getChildren().add(createSection(
|
? Container.i18n().text(I18n.ASSETS_STATE_NO_RESULTS)
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW),
|
: Container.i18n().format(I18n.ASSETS_STATE_READY, projection.visibleAssetCount(), state.assets().size());
|
||||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
|
||||||
detailsContent.getChildren().add(createSection(
|
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS),
|
|
||||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detailsStatus == AssetWorkspaceDetailsStatus.ERROR || selectedAssetDetails == null) {
|
|
||||||
detailsContent.getChildren().add(createSection(
|
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT),
|
|
||||||
createSectionMessage(Objects.requireNonNullElse(detailsErrorMessage, Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)))));
|
|
||||||
detailsContent.getChildren().add(createSection(
|
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW),
|
|
||||||
createSectionMessage(Objects.requireNonNullElse(detailsErrorMessage, Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)))));
|
|
||||||
detailsContent.getChildren().add(createSection(
|
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS),
|
|
||||||
createSectionMessage(Objects.requireNonNullElse(detailsErrorMessage, Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)))));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsContent.getChildren().add(createRuntimeContractSection(selectedAssetDetails));
|
|
||||||
detailsContent.getChildren().add(createInputsPreviewSection(selectedAssetDetails));
|
|
||||||
detailsContent.getChildren().add(createDiagnosticsSection(selectedAssetDetails));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createSummaryActionsRow(AssetWorkspaceAssetSummary summary) {
|
|
||||||
final HBox row = new HBox(12);
|
|
||||||
row.getStyleClass().add("assets-details-summary-actions-row");
|
|
||||||
|
|
||||||
final VBox summarySection = createSummarySection(summary);
|
|
||||||
final VBox actionsSection = createActionsSection(summary);
|
|
||||||
HBox.setHgrow(summarySection, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(actionsSection, Priority.NEVER);
|
|
||||||
summarySection.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
summarySection.setMinWidth(0);
|
|
||||||
actionsSection.setPrefWidth(280);
|
|
||||||
actionsSection.setMinWidth(240);
|
|
||||||
actionsSection.setMaxWidth(320);
|
|
||||||
|
|
||||||
row.getChildren().addAll(summarySection, actionsSection);
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VBox createSummarySection(AssetWorkspaceAssetSummary summary) {
|
|
||||||
final VBox content = new VBox(8);
|
|
||||||
content.getChildren().addAll(
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_NAME), summary.assetName()),
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION), registrationLabel(summary.state())),
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_BUILD_PARTICIPATION), buildParticipationLabel(summary.buildParticipation())),
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())),
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_TYPE), summary.assetFamily()),
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(summary.assetRoot())));
|
|
||||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_SUMMARY), content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private VBox createActionsSection(AssetWorkspaceAssetSummary summary) {
|
|
||||||
return createSection(
|
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_ACTIONS),
|
|
||||||
createActionsContent(summary));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createRuntimeContractSection(AssetWorkspaceAssetDetails details) {
|
|
||||||
final VBox content = new VBox(8);
|
|
||||||
content.getChildren().addAll(
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_PRELOAD), createPreloadToggle(details)),
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_FORMAT), details.outputFormat()),
|
|
||||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_CODEC), details.outputCodec()));
|
|
||||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createInputsPreviewSection(AssetWorkspaceAssetDetails details) {
|
|
||||||
if (details.inputsByRole().isEmpty()) {
|
|
||||||
return createSection(
|
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW),
|
|
||||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_INPUTS_EMPTY)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedPreviewInput == null || !containsInput(details, selectedPreviewInput)) {
|
|
||||||
selectedPreviewInput = firstPreviewInput(details);
|
|
||||||
selectedPreviewZoom = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
final VBox inputsList = new VBox(8);
|
|
||||||
for (Map.Entry<String, List<Path>> entry : details.inputsByRole().entrySet()) {
|
|
||||||
final VBox roleBox = new VBox(6);
|
|
||||||
final Label roleLabel = new Label(entry.getKey());
|
|
||||||
roleLabel.getStyleClass().add("assets-details-role-label");
|
|
||||||
roleBox.getChildren().add(roleLabel);
|
|
||||||
for (Path input : entry.getValue()) {
|
|
||||||
final Button inputButton = new Button(input.getFileName().toString());
|
|
||||||
inputButton.getStyleClass().addAll("assets-details-input-button", "studio-button", "studio-button-secondary");
|
|
||||||
if (input.equals(selectedPreviewInput)) {
|
|
||||||
inputButton.getStyleClass().add("studio-button-active");
|
|
||||||
}
|
|
||||||
inputButton.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
inputButton.setOnAction(event -> {
|
|
||||||
selectedPreviewInput = input;
|
|
||||||
selectedPreviewZoom = 1;
|
|
||||||
requestDetailsRedraw();
|
|
||||||
});
|
|
||||||
roleBox.getChildren().add(inputButton);
|
|
||||||
}
|
|
||||||
inputsList.getChildren().add(roleBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Node previewPane = createPreviewPane(selectedPreviewInput);
|
|
||||||
final SplitPane splitPane = new SplitPane(inputsList, previewPane);
|
|
||||||
splitPane.setOrientation(Orientation.HORIZONTAL);
|
|
||||||
splitPane.setDividerPositions(0.34);
|
|
||||||
splitPane.getStyleClass().add("assets-details-input-preview-split");
|
|
||||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), splitPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createDiagnosticsSection(AssetWorkspaceAssetDetails details) {
|
|
||||||
if (details.diagnostics().isEmpty()) {
|
|
||||||
return createSection(
|
|
||||||
Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS),
|
|
||||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_EMPTY)));
|
|
||||||
}
|
|
||||||
|
|
||||||
final VBox diagnosticsBox = new VBox(8);
|
|
||||||
for (AssetWorkspaceDiagnostic diagnostic : details.diagnostics()) {
|
|
||||||
final VBox card = new VBox(4);
|
|
||||||
card.getStyleClass().add("assets-details-diagnostic-card");
|
|
||||||
card.getStyleClass().add("assets-details-diagnostic-" + diagnostic.severity().name().toLowerCase());
|
|
||||||
final Label severity = new Label(diagnostic.severity().name());
|
|
||||||
severity.getStyleClass().add("assets-details-diagnostic-severity");
|
|
||||||
final Label message = new Label(diagnostic.message());
|
|
||||||
message.getStyleClass().add("assets-details-diagnostic-message");
|
|
||||||
message.setWrapText(true);
|
|
||||||
card.getChildren().addAll(severity, message);
|
|
||||||
diagnosticsBox.getChildren().add(card);
|
|
||||||
}
|
|
||||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), diagnosticsBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createActionsContent(AssetWorkspaceAssetSummary summary) {
|
|
||||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(summary);
|
|
||||||
final VBox content = new VBox(12);
|
|
||||||
for (AssetWorkspaceAction action : actionSet.primaryActions()) {
|
|
||||||
content.getChildren().add(createActionButton(action, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (AssetWorkspaceAction action : actionSet.sensitiveActions()) {
|
|
||||||
content.getChildren().add(createActionButton(action, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stagedMutationPreview != null && stagedMutationPreview.asset().selectionKey().equals(summary.selectionKey())) {
|
|
||||||
content.getChildren().add(createStagedMutationPanel(stagedMutationPreview));
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Button createActionButton(AssetWorkspaceAction action, boolean sensitive) {
|
|
||||||
final Button button = new Button(actionLabel(action));
|
|
||||||
button.getStyleClass().addAll("studio-button", actionButtonVariant(action, sensitive));
|
|
||||||
button.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
button.setDisable(!supportsAction(action));
|
|
||||||
if (!button.isDisable()) {
|
|
||||||
button.setOnAction(event -> requestMutationPreview(action));
|
|
||||||
}
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String actionButtonVariant(AssetWorkspaceAction action, boolean sensitive) {
|
|
||||||
if (!sensitive) {
|
|
||||||
return "studio-button-primary";
|
|
||||||
}
|
|
||||||
return switch (action) {
|
|
||||||
case RELOCATE -> "studio-button-warning";
|
|
||||||
case EXCLUDE_FROM_BUILD, REMOVE -> "studio-button-danger";
|
|
||||||
default -> "studio-button-secondary";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -678,88 +380,7 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node createPreviewPane(Path input) {
|
static void applyPreviewScale(Image image, ImageView imageView, int requestedZoom) {
|
||||||
final VBox previewBox = new VBox(10);
|
|
||||||
previewBox.getStyleClass().add("assets-details-preview-pane");
|
|
||||||
if (input == null) {
|
|
||||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_PREVIEW_EMPTY)));
|
|
||||||
return previewBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Label title = new Label(input.getFileName().toString());
|
|
||||||
title.getStyleClass().add("assets-details-preview-title");
|
|
||||||
previewBox.getChildren().add(title);
|
|
||||||
|
|
||||||
final String extension = extensionOf(input);
|
|
||||||
if (isImage(extension)) {
|
|
||||||
try {
|
|
||||||
final Image image = new Image(input.toUri().toString(), false);
|
|
||||||
final ImageView imageView = new ImageView(image);
|
|
||||||
imageView.setPreserveRatio(true);
|
|
||||||
imageView.setSmooth(false);
|
|
||||||
imageView.getStyleClass().add("assets-details-preview-image");
|
|
||||||
previewBox.getChildren().add(createPreviewZoomBar(image));
|
|
||||||
applyPreviewScale(image, imageView, selectedPreviewZoom);
|
|
||||||
previewBox.getChildren().add(imageView);
|
|
||||||
} catch (RuntimeException runtimeException) {
|
|
||||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_PREVIEW_IMAGE_ERROR)));
|
|
||||||
}
|
|
||||||
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(input)));
|
|
||||||
return previewBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isText(extension)) {
|
|
||||||
final TextArea textArea = new TextArea(readPreviewText(input));
|
|
||||||
textArea.setWrapText(true);
|
|
||||||
textArea.setEditable(false);
|
|
||||||
textArea.getStyleClass().add("assets-details-preview-text");
|
|
||||||
previewBox.getChildren().add(textArea);
|
|
||||||
return previewBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAudio(extension)) {
|
|
||||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_AUDIO_PLACEHOLDER, input.getFileName().toString())));
|
|
||||||
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(input)));
|
|
||||||
return previewBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_GENERIC_PLACEHOLDER, input.getFileName().toString())));
|
|
||||||
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(input)));
|
|
||||||
return previewBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createPreviewZoomBar(Image image) {
|
|
||||||
final HBox zoomBar = new HBox(8);
|
|
||||||
zoomBar.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
zoomBar.getStyleClass().add("assets-details-preview-zoom-bar");
|
|
||||||
|
|
||||||
final Label zoomLabel = new Label(Container.i18n().text(I18n.ASSETS_PREVIEW_ZOOM));
|
|
||||||
zoomLabel.getStyleClass().add("assets-details-preview-zoom-label");
|
|
||||||
zoomBar.getChildren().add(zoomLabel);
|
|
||||||
|
|
||||||
final ToggleGroup zoomGroup = new ToggleGroup();
|
|
||||||
final int maxZoom = maxPreviewZoom(image);
|
|
||||||
for (int zoom : List.of(1, 2, 4, 8)) {
|
|
||||||
final ToggleButton button = new ToggleButton("x" + zoom);
|
|
||||||
button.getStyleClass().addAll(
|
|
||||||
"assets-details-preview-zoom-button",
|
|
||||||
"studio-button",
|
|
||||||
"studio-button-secondary",
|
|
||||||
"studio-button-pill",
|
|
||||||
"studio-button-toggle");
|
|
||||||
button.setToggleGroup(zoomGroup);
|
|
||||||
button.setSelected(selectedPreviewZoom == zoom);
|
|
||||||
button.setDisable(zoom > maxZoom);
|
|
||||||
button.setOnAction(event -> {
|
|
||||||
selectedPreviewZoom = zoom;
|
|
||||||
requestDetailsRedraw();
|
|
||||||
});
|
|
||||||
zoomBar.getChildren().add(button);
|
|
||||||
}
|
|
||||||
return zoomBar;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyPreviewScale(Image image, ImageView imageView, int requestedZoom) {
|
|
||||||
final double width = image.getWidth();
|
final double width = image.getWidth();
|
||||||
final double height = image.getHeight();
|
final double height = image.getHeight();
|
||||||
if (width <= 0.0d || height <= 0.0d) {
|
if (width <= 0.0d || height <= 0.0d) {
|
||||||
@ -796,51 +417,8 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
return Math.max(1, (int) Math.floor(420.0d / longestEdge));
|
return Math.max(1, (int) Math.floor(420.0d / longestEdge));
|
||||||
}
|
}
|
||||||
|
|
||||||
private VBox createSection(String title, Node content) {
|
@Override
|
||||||
final VBox section = new VBox(10);
|
public void updatePreload(AssetWorkspaceAssetDetails details, boolean preloadEnabled, CheckBox checkBox) {
|
||||||
section.getStyleClass().add("assets-details-section");
|
|
||||||
final Label titleLabel = new Label(title);
|
|
||||||
titleLabel.getStyleClass().add("assets-details-section-title");
|
|
||||||
section.getChildren().addAll(titleLabel, content);
|
|
||||||
return section;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createSectionMessage(String text) {
|
|
||||||
final Label label = new Label(text);
|
|
||||||
label.setWrapText(true);
|
|
||||||
label.getStyleClass().add("assets-details-section-message");
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createKeyValueRow(String key, String value) {
|
|
||||||
final Label valueLabel = new Label(value);
|
|
||||||
valueLabel.getStyleClass().add("assets-details-value");
|
|
||||||
valueLabel.setWrapText(true);
|
|
||||||
return createKeyValueRow(key, valueLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createKeyValueRow(String key, Node valueNode) {
|
|
||||||
final HBox row = new HBox(12);
|
|
||||||
row.setAlignment(Pos.TOP_LEFT);
|
|
||||||
final Label keyLabel = new Label(key);
|
|
||||||
keyLabel.getStyleClass().add("assets-details-key");
|
|
||||||
HBox.setHgrow(valueNode, Priority.ALWAYS);
|
|
||||||
row.getChildren().addAll(keyLabel, valueNode);
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node createPreloadToggle(AssetWorkspaceAssetDetails details) {
|
|
||||||
final boolean currentValue = details.summary().preload();
|
|
||||||
final CheckBox checkBox = new CheckBox(yesNo(currentValue));
|
|
||||||
checkBox.setSelected(currentValue);
|
|
||||||
checkBox.setFocusTraversable(false);
|
|
||||||
checkBox.getStyleClass().add("assets-details-readonly-check");
|
|
||||||
checkBox.selectedProperty().addListener((ignored, previous, selected) -> checkBox.setText(yesNo(selected)));
|
|
||||||
checkBox.setOnAction(event -> updatePreload(details, checkBox.isSelected(), checkBox));
|
|
||||||
return checkBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePreload(AssetWorkspaceAssetDetails details, boolean preloadEnabled, CheckBox checkBox) {
|
|
||||||
checkBox.setDisable(true);
|
checkBox.setDisable(true);
|
||||||
setInlineProgress("Updating preload...", ProgressBar.INDETERMINATE_PROGRESS, true);
|
setInlineProgress("Updating preload...", ProgressBar.INDETERMINATE_PROGRESS, true);
|
||||||
appendLog("Updating preload for " + details.summary().assetName() + " to " + yesNo(preloadEnabled) + ".");
|
appendLog("Updating preload for " + details.summary().assetName() + " to " + yesNo(preloadEnabled) + ".");
|
||||||
@ -929,114 +507,8 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
requestRedraw();
|
requestRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderNavigatorProjection(AssetNavigatorProjection projection) {
|
@Override
|
||||||
navigatorContent.getChildren().clear();
|
public void selectAsset(AssetWorkspaceSelectionKey selectionKey) {
|
||||||
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");
|
|
||||||
row.getStyleClass().add(assetRowToneClass(asset.assetFamily()));
|
|
||||||
updateAssetRowSelection(row, asset.selectionKey().equals(state.selectedKey()));
|
|
||||||
|
|
||||||
final HBox topLine = new HBox(8);
|
|
||||||
topLine.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
final Label name = new Label(asset.assetName());
|
|
||||||
name.getStyleClass().add("assets-workspace-asset-name");
|
|
||||||
name.getStyleClass().add(assetNameToneClass(asset.assetFamily()));
|
|
||||||
final Region spacer = new Region();
|
|
||||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
|
||||||
final HBox badges = new HBox(6);
|
|
||||||
badges.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
badges.getStyleClass().add("assets-workspace-asset-badges");
|
|
||||||
if (asset.state() == AssetWorkspaceAssetState.UNREGISTERED) {
|
|
||||||
badges.getChildren().add(createBadge(
|
|
||||||
Container.i18n().text(I18n.ASSETS_BADGE_UNREGISTERED),
|
|
||||||
"assets-workspace-badge-orphan"));
|
|
||||||
} else if (asset.buildParticipation() == AssetWorkspaceBuildParticipation.INCLUDED) {
|
|
||||||
badges.getChildren().add(createBadge(
|
|
||||||
buildParticipationLabel(asset.buildParticipation()),
|
|
||||||
"assets-workspace-badge-preload"));
|
|
||||||
if (asset.preload()) {
|
|
||||||
badges.getChildren().add(createBadge(
|
|
||||||
Container.i18n().text(I18n.ASSETS_BADGE_PRELOAD),
|
|
||||||
"assets-workspace-badge-preload"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
badges.getChildren().add(createBadge(
|
|
||||||
buildParticipationLabel(asset.buildParticipation()),
|
|
||||||
"assets-workspace-badge-diagnostics"));
|
|
||||||
}
|
|
||||||
if (asset.hasDiagnostics()) {
|
|
||||||
badges.getChildren().add(createBadge(Container.i18n().text(I18n.ASSETS_BADGE_DIAGNOSTICS), "assets-workspace-badge-diagnostics"));
|
|
||||||
}
|
|
||||||
topLine.getChildren().addAll(name, spacer, badges);
|
|
||||||
|
|
||||||
final Label path = new Label(AssetNavigatorProjectionBuilder.relativeRoot(asset, projectRoot()));
|
|
||||||
path.getStyleClass().add("assets-workspace-asset-path");
|
|
||||||
row.getChildren().addAll(topLine, path);
|
|
||||||
assetRowsBySelectionKey.put(asset.selectionKey(), row);
|
|
||||||
row.setOnMouseClicked(event -> selectAsset(asset.selectionKey()));
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNavigatorSelection() {
|
|
||||||
assetRowsBySelectionKey.forEach((selectionKey, row) -> updateAssetRowSelection(row, selectionKey.equals(state.selectedKey())));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateAssetRowSelection(VBox row, boolean selected) {
|
|
||||||
if (selected) {
|
|
||||||
if (!row.getStyleClass().contains("assets-workspace-asset-row-selected")) {
|
|
||||||
row.getStyleClass().add("assets-workspace-asset-row-selected");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
row.getStyleClass().remove("assets-workspace-asset-row-selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 String registrationLabel(AssetWorkspaceAssetState state) {
|
|
||||||
return switch (state) {
|
|
||||||
case REGISTERED -> Container.i18n().text(I18n.ASSETS_VALUE_REGISTERED);
|
|
||||||
case UNREGISTERED -> Container.i18n().text(I18n.ASSETS_VALUE_UNREGISTERED);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildParticipationLabel(AssetWorkspaceBuildParticipation buildParticipation) {
|
|
||||||
return switch (buildParticipation) {
|
|
||||||
case INCLUDED -> Container.i18n().text(I18n.ASSETS_VALUE_INCLUDED);
|
|
||||||
case EXCLUDED -> Container.i18n().text(I18n.ASSETS_VALUE_EXCLUDED);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(projectReference, selectionKey));
|
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(projectReference, selectionKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1044,8 +516,7 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
state = state.withSelection(selectionKey);
|
state = state.withSelection(selectionKey);
|
||||||
stagedMutationPreview = null;
|
stagedMutationPreview = null;
|
||||||
appendLog("Selected asset " + selectionKey.stableKey() + ".");
|
appendLog("Selected asset " + selectionKey.stableKey() + ".");
|
||||||
updateNavigatorSelection();
|
requestRedraw();
|
||||||
requestDetailsRedraw();
|
|
||||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey));
|
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey));
|
||||||
loadSelectedAssetDetails(selectionKey);
|
loadSelectedAssetDetails(selectionKey);
|
||||||
}
|
}
|
||||||
@ -1122,7 +593,23 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
return new PackerProjectContext(projectReference.name(), projectReference.rootPath());
|
return new PackerProjectContext(projectReference.name(), projectReference.rootPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestMutationPreview(AssetWorkspaceAction action) {
|
@Override
|
||||||
|
public void updateSearchQuery(String searchQuery) {
|
||||||
|
this.searchQuery = Objects.requireNonNullElse(searchQuery, "");
|
||||||
|
requestNavigatorRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateActiveFilters(EnumSet<AssetNavigatorFilter> filters) {
|
||||||
|
activeFilters.clear();
|
||||||
|
if (filters != null) {
|
||||||
|
activeFilters.addAll(filters);
|
||||||
|
}
|
||||||
|
requestNavigatorRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestMutationPreview(AssetWorkspaceAction action) {
|
||||||
final AssetWorkspaceAssetSummary selectedAsset = state.selectedAsset().orElse(null);
|
final AssetWorkspaceAssetSummary selectedAsset = state.selectedAsset().orElse(null);
|
||||||
if (selectedAsset == null) {
|
if (selectedAsset == null) {
|
||||||
return;
|
return;
|
||||||
@ -1147,7 +634,7 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
stagedMutationPreview = mutationService.preview(projectReference, selectedAsset, action, null);
|
stagedMutationPreview = mutationService.preview(projectReference, selectedAsset, action, null);
|
||||||
appendLog("Preview ready for " + actionLabel(action) + ".");
|
appendLog("Preview ready for " + actionLabel(action) + ".");
|
||||||
requestDetailsRedraw();
|
requestDetailsRedraw();
|
||||||
Platform.runLater(() -> detailsScroll.setVvalue(0.0d));
|
Platform.runLater(detailsControl::scrollToTop);
|
||||||
} catch (RuntimeException runtimeException) {
|
} catch (RuntimeException runtimeException) {
|
||||||
final String message = rootCauseMessage(runtimeException);
|
final String message = rootCauseMessage(runtimeException);
|
||||||
appendLog("Preview failed: " + message);
|
appendLog("Preview failed: " + message);
|
||||||
@ -1157,6 +644,25 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelStagedMutationPreview() {
|
||||||
|
stagedMutationPreview = null;
|
||||||
|
requestDetailsRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updatePreviewInput(Path input) {
|
||||||
|
selectedPreviewInput = input;
|
||||||
|
selectedPreviewZoom = 1;
|
||||||
|
requestDetailsRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updatePreviewZoom(int zoom) {
|
||||||
|
selectedPreviewZoom = zoom;
|
||||||
|
requestDetailsRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
private void runRegisterFlow(AssetWorkspaceAssetSummary selectedAsset) {
|
private void runRegisterFlow(AssetWorkspaceAssetSummary selectedAsset) {
|
||||||
runDirectMutationFlow(selectedAsset, AssetWorkspaceAction.REGISTER);
|
runDirectMutationFlow(selectedAsset, AssetWorkspaceAction.REGISTER);
|
||||||
}
|
}
|
||||||
@ -1371,7 +877,8 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
return dialog.showAndWait().filter(ButtonType.OK::equals).isPresent();
|
return dialog.showAndWait().filter(ButtonType.OK::equals).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStagedMutation(AssetWorkspaceMutationPreview preview) {
|
@Override
|
||||||
|
public void applyStagedMutation(AssetWorkspaceMutationPreview preview) {
|
||||||
if (!preview.canApply()) {
|
if (!preview.canApply()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1399,7 +906,7 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
return details.inputsByRole().values().stream().flatMap(List::stream).anyMatch(input::equals);
|
return details.inputsByRole().values().stream().flatMap(List::stream).anyMatch(input::equals);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String readPreviewText(Path input) {
|
static String readPreviewText(Path input) {
|
||||||
try {
|
try {
|
||||||
final String text = Files.readString(input);
|
final String text = Files.readString(input);
|
||||||
return text.length() > 4000 ? text.substring(0, 4000) + "\n\n…" : text;
|
return text.length() > 4000 ? text.substring(0, 4000) + "\n\n…" : text;
|
||||||
@ -1442,6 +949,30 @@ public final class AssetWorkspace implements Workspace {
|
|||||||
|| extension.equals("mp3");
|
|| extension.equals("mp3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Node createSectionMessage(String text) {
|
||||||
|
final Label label = new Label(text);
|
||||||
|
label.setWrapText(true);
|
||||||
|
label.getStyleClass().add("assets-details-section-message");
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createKeyValueRow(String key, String value) {
|
||||||
|
final Label valueLabel = new Label(value);
|
||||||
|
valueLabel.getStyleClass().add("assets-details-value");
|
||||||
|
valueLabel.setWrapText(true);
|
||||||
|
return createKeyValueRow(key, valueLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createKeyValueRow(String key, Node valueNode) {
|
||||||
|
final HBox row = new HBox(12);
|
||||||
|
row.setAlignment(Pos.TOP_LEFT);
|
||||||
|
final Label keyLabel = new Label(key);
|
||||||
|
keyLabel.getStyleClass().add("assets-details-key");
|
||||||
|
HBox.setHgrow(valueNode, Priority.ALWAYS);
|
||||||
|
row.getChildren().addAll(keyLabel, valueNode);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
private String yesNo(boolean value) {
|
private String yesNo(boolean value) {
|
||||||
return value ? Container.i18n().text(I18n.ASSETS_VALUE_YES) : Container.i18n().text(I18n.ASSETS_VALUE_NO);
|
return value ? Container.i18n().text(I18n.ASSETS_VALUE_YES) : Container.i18n().text(I18n.ASSETS_VALUE_NO);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,570 @@
|
|||||||
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
|
import javafx.geometry.Orientation;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ProgressBar;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.SplitPane;
|
||||||
|
import javafx.scene.control.TextArea;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import p.studio.Container;
|
||||||
|
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||||
|
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
|
||||||
|
import p.studio.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||||
|
import p.studio.events.StudioWorkspaceEventBus;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.utilities.i18n.I18n;
|
||||||
|
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
final class AssetWorkspaceDetailsControl extends VBox implements StudioControlLifecycle {
|
||||||
|
private final ProjectReference projectReference;
|
||||||
|
private final StudioWorkspaceEventBus workspaceBus;
|
||||||
|
private final AssetWorkspaceInteractionPort interactions;
|
||||||
|
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||||
|
private final Label workspaceSummaryLabel = new Label();
|
||||||
|
private final VBox detailsContent = new VBox(12);
|
||||||
|
private final ScrollPane detailsScroll = new ScrollPane();
|
||||||
|
|
||||||
|
private AssetWorkspaceDetailsViewState viewState;
|
||||||
|
|
||||||
|
AssetWorkspaceDetailsControl(
|
||||||
|
ProjectReference projectReference,
|
||||||
|
StudioWorkspaceEventBus workspaceBus,
|
||||||
|
AssetWorkspaceInteractionPort interactions) {
|
||||||
|
StudioControlLifecycleSupport.install(this, this);
|
||||||
|
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
|
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||||
|
this.interactions = Objects.requireNonNull(interactions, "interactions");
|
||||||
|
|
||||||
|
getStyleClass().add("assets-workspace-pane");
|
||||||
|
setSpacing(10);
|
||||||
|
|
||||||
|
final Label detailsTitle = new Label();
|
||||||
|
detailsTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_DETAILS_TITLE));
|
||||||
|
detailsTitle.getStyleClass().add("assets-workspace-pane-title");
|
||||||
|
|
||||||
|
workspaceSummaryLabel.getStyleClass().add("assets-workspace-summary");
|
||||||
|
detailsContent.getStyleClass().add("assets-workspace-details-content");
|
||||||
|
detailsScroll.setContent(detailsContent);
|
||||||
|
detailsScroll.setFitToWidth(true);
|
||||||
|
detailsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
detailsScroll.getStyleClass().add("assets-workspace-details-scroll");
|
||||||
|
|
||||||
|
getChildren().addAll(detailsTitle, workspaceSummaryLabel, detailsScroll);
|
||||||
|
VBox.setVgrow(detailsScroll, Priority.ALWAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void subscribe() {
|
||||||
|
subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
|
||||||
|
if (projectReference.equals(event.project())) {
|
||||||
|
applyViewState(event.viewState());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unsubscribe() {
|
||||||
|
subscriptions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrollToTop() {
|
||||||
|
detailsScroll.setVvalue(0.0d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyViewState(AssetWorkspaceDetailsViewState viewState) {
|
||||||
|
this.viewState = viewState;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render() {
|
||||||
|
detailsContent.getChildren().clear();
|
||||||
|
if (viewState == null) {
|
||||||
|
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
||||||
|
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (viewState.workspaceState().status()) {
|
||||||
|
case LOADING -> {
|
||||||
|
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
||||||
|
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)));
|
||||||
|
}
|
||||||
|
case EMPTY -> {
|
||||||
|
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
|
||||||
|
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)));
|
||||||
|
}
|
||||||
|
case ERROR -> {
|
||||||
|
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
|
||||||
|
detailsContent.getChildren().add(createSectionMessage(viewState.workspaceState().errorMessage()));
|
||||||
|
}
|
||||||
|
case READY -> {
|
||||||
|
workspaceSummaryLabel.setText(Container.i18n().format(I18n.ASSETS_SUMMARY_READY, viewState.workspaceState().assets().size()));
|
||||||
|
viewState.workspaceState().selectedAsset()
|
||||||
|
.ifPresentOrElse(this::renderSelectedAssetDetails, () ->
|
||||||
|
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderSelectedAssetDetails(AssetWorkspaceAssetSummary summary) {
|
||||||
|
detailsContent.getChildren().add(createSummaryActionsRow(summary));
|
||||||
|
if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.LOADING) {
|
||||||
|
detailsContent.getChildren().add(createLoadingSections());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.ERROR || viewState.selectedAssetDetails() == null) {
|
||||||
|
final String message = Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY));
|
||||||
|
detailsContent.getChildren().add(createDetailsErrorSections(message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailsContent.getChildren().add(createRuntimeContractSection(viewState.selectedAssetDetails()));
|
||||||
|
detailsContent.getChildren().add(createInputsPreviewSection(viewState.selectedAssetDetails()));
|
||||||
|
detailsContent.getChildren().add(createDiagnosticsSection(viewState.selectedAssetDetails()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createLoadingSections() {
|
||||||
|
final VBox box = new VBox(10);
|
||||||
|
box.getChildren().add(createSection(Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
||||||
|
box.getChildren().add(createSection(Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
||||||
|
box.getChildren().add(createSection(Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createDetailsErrorSections(String message) {
|
||||||
|
final VBox box = new VBox(10);
|
||||||
|
box.getChildren().add(createSection(Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), createSectionMessage(message)));
|
||||||
|
box.getChildren().add(createSection(Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), createSectionMessage(message)));
|
||||||
|
box.getChildren().add(createSection(Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), createSectionMessage(message)));
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createSummaryActionsRow(AssetWorkspaceAssetSummary summary) {
|
||||||
|
final HBox row = new HBox(12);
|
||||||
|
row.getStyleClass().add("assets-details-summary-actions-row");
|
||||||
|
|
||||||
|
final VBox summarySection = createSummarySection(summary);
|
||||||
|
final VBox actionsSection = createActionsSection(summary);
|
||||||
|
HBox.setHgrow(summarySection, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(actionsSection, Priority.NEVER);
|
||||||
|
summarySection.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
summarySection.setMinWidth(0);
|
||||||
|
actionsSection.setPrefWidth(280);
|
||||||
|
actionsSection.setMinWidth(240);
|
||||||
|
actionsSection.setMaxWidth(320);
|
||||||
|
|
||||||
|
row.getChildren().addAll(summarySection, actionsSection);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox createSummarySection(AssetWorkspaceAssetSummary summary) {
|
||||||
|
final VBox content = new VBox(8);
|
||||||
|
content.getChildren().addAll(
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_NAME), summary.assetName()),
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION), registrationLabel(summary.state())),
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_BUILD_PARTICIPATION), buildParticipationLabel(summary.buildParticipation())),
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())),
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_TYPE), summary.assetFamily()),
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(summary.assetRoot())));
|
||||||
|
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_SUMMARY), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox createActionsSection(AssetWorkspaceAssetSummary summary) {
|
||||||
|
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(summary);
|
||||||
|
final VBox content = new VBox(12);
|
||||||
|
for (AssetWorkspaceAction action : actionSet.primaryActions()) {
|
||||||
|
content.getChildren().add(createActionButton(action, false));
|
||||||
|
}
|
||||||
|
for (AssetWorkspaceAction action : actionSet.sensitiveActions()) {
|
||||||
|
content.getChildren().add(createActionButton(action, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewState.stagedMutationPreview() != null
|
||||||
|
&& viewState.stagedMutationPreview().asset().selectionKey().equals(summary.selectionKey())) {
|
||||||
|
content.getChildren().add(createStagedMutationPanel(viewState.stagedMutationPreview()));
|
||||||
|
}
|
||||||
|
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_ACTIONS), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createActionButton(AssetWorkspaceAction action, boolean sensitive) {
|
||||||
|
final Button button = new Button(actionLabel(action));
|
||||||
|
button.getStyleClass().addAll("studio-button", actionButtonVariant(action, sensitive));
|
||||||
|
button.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
button.setOnAction(event -> interactions.requestMutationPreview(action));
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createRuntimeContractSection(AssetWorkspaceAssetDetails details) {
|
||||||
|
final VBox content = new VBox(8);
|
||||||
|
content.getChildren().addAll(
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_PRELOAD), createPreloadToggle(details)),
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_FORMAT), details.outputFormat()),
|
||||||
|
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_CODEC), details.outputCodec()));
|
||||||
|
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createInputsPreviewSection(AssetWorkspaceAssetDetails details) {
|
||||||
|
if (details.inputsByRole().isEmpty()) {
|
||||||
|
return createSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW),
|
||||||
|
createSectionMessage(Container.i18n().text(I18n.ASSETS_INPUTS_EMPTY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path selectedPreviewInput = resolveSelectedPreviewInput(details);
|
||||||
|
final VBox inputsList = new VBox(8);
|
||||||
|
for (Map.Entry<String, List<Path>> entry : details.inputsByRole().entrySet()) {
|
||||||
|
final VBox roleBox = new VBox(6);
|
||||||
|
final Label roleLabel = new Label(entry.getKey());
|
||||||
|
roleLabel.getStyleClass().add("assets-details-role-label");
|
||||||
|
roleBox.getChildren().add(roleLabel);
|
||||||
|
for (Path input : entry.getValue()) {
|
||||||
|
final Button inputButton = new Button(input.getFileName().toString());
|
||||||
|
inputButton.getStyleClass().addAll("assets-details-input-button", "studio-button", "studio-button-secondary");
|
||||||
|
if (input.equals(selectedPreviewInput)) {
|
||||||
|
inputButton.getStyleClass().add("studio-button-active");
|
||||||
|
}
|
||||||
|
inputButton.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
inputButton.setOnAction(event -> interactions.updatePreviewInput(input));
|
||||||
|
roleBox.getChildren().add(inputButton);
|
||||||
|
}
|
||||||
|
inputsList.getChildren().add(roleBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Node previewPane = createPreviewPane(selectedPreviewInput);
|
||||||
|
final SplitPane splitPane = new SplitPane(inputsList, previewPane);
|
||||||
|
splitPane.setOrientation(Orientation.HORIZONTAL);
|
||||||
|
splitPane.setDividerPositions(0.34);
|
||||||
|
splitPane.getStyleClass().add("assets-details-input-preview-split");
|
||||||
|
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), splitPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveSelectedPreviewInput(AssetWorkspaceAssetDetails details) {
|
||||||
|
final Path selectedPreviewInput = viewState.selectedPreviewInput();
|
||||||
|
if (selectedPreviewInput == null || !containsInput(details, selectedPreviewInput)) {
|
||||||
|
return details.inputsByRole().values().stream().flatMap(List::stream).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
return selectedPreviewInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createDiagnosticsSection(AssetWorkspaceAssetDetails details) {
|
||||||
|
if (details.diagnostics().isEmpty()) {
|
||||||
|
return createSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS),
|
||||||
|
createSectionMessage(Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_EMPTY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final VBox diagnosticsBox = new VBox(8);
|
||||||
|
for (AssetWorkspaceDiagnostic diagnostic : details.diagnostics()) {
|
||||||
|
final VBox card = new VBox(4);
|
||||||
|
card.getStyleClass().add("assets-details-diagnostic-card");
|
||||||
|
card.getStyleClass().add("assets-details-diagnostic-" + diagnostic.severity().name().toLowerCase());
|
||||||
|
final Label severity = new Label(diagnostic.severity().name());
|
||||||
|
severity.getStyleClass().add("assets-details-diagnostic-severity");
|
||||||
|
final Label message = new Label(diagnostic.message());
|
||||||
|
message.getStyleClass().add("assets-details-diagnostic-message");
|
||||||
|
message.setWrapText(true);
|
||||||
|
card.getChildren().addAll(severity, message);
|
||||||
|
diagnosticsBox.getChildren().add(card);
|
||||||
|
}
|
||||||
|
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), diagnosticsBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createPreloadToggle(AssetWorkspaceAssetDetails details) {
|
||||||
|
final boolean currentValue = details.summary().preload();
|
||||||
|
final CheckBox checkBox = new CheckBox(yesNo(currentValue));
|
||||||
|
checkBox.setSelected(currentValue);
|
||||||
|
checkBox.setFocusTraversable(false);
|
||||||
|
checkBox.getStyleClass().add("assets-details-readonly-check");
|
||||||
|
checkBox.selectedProperty().addListener((ignored, previous, selected) -> checkBox.setText(yesNo(selected)));
|
||||||
|
checkBox.setOnAction(event -> interactions.updatePreload(details, checkBox.isSelected(), checkBox));
|
||||||
|
return checkBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createPreviewPane(Path input) {
|
||||||
|
final VBox previewBox = new VBox(10);
|
||||||
|
previewBox.getStyleClass().add("assets-details-preview-pane");
|
||||||
|
if (input == null) {
|
||||||
|
previewBox.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_PREVIEW_EMPTY)));
|
||||||
|
return previewBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Label title = new Label(input.getFileName().toString());
|
||||||
|
title.getStyleClass().add("assets-details-preview-title");
|
||||||
|
previewBox.getChildren().add(title);
|
||||||
|
|
||||||
|
final String extension = extensionOf(input);
|
||||||
|
if (isImage(extension)) {
|
||||||
|
try {
|
||||||
|
final Image image = new Image(input.toUri().toString(), false);
|
||||||
|
final ImageView imageView = new ImageView(image);
|
||||||
|
imageView.setPreserveRatio(true);
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.getStyleClass().add("assets-details-preview-image");
|
||||||
|
previewBox.getChildren().add(createPreviewZoomBar(image));
|
||||||
|
AssetWorkspace.applyPreviewScale(image, imageView, viewState.selectedPreviewZoom());
|
||||||
|
previewBox.getChildren().add(imageView);
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
previewBox.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_PREVIEW_IMAGE_ERROR)));
|
||||||
|
}
|
||||||
|
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(input)));
|
||||||
|
return previewBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isText(extension)) {
|
||||||
|
final TextArea textArea = new TextArea(AssetWorkspace.readPreviewText(input));
|
||||||
|
textArea.setWrapText(true);
|
||||||
|
textArea.setEditable(false);
|
||||||
|
textArea.getStyleClass().add("assets-details-preview-text");
|
||||||
|
previewBox.getChildren().add(textArea);
|
||||||
|
return previewBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAudio(extension)) {
|
||||||
|
previewBox.getChildren().add(createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_AUDIO_PLACEHOLDER, input.getFileName().toString())));
|
||||||
|
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(input)));
|
||||||
|
return previewBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
previewBox.getChildren().add(createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_GENERIC_PLACEHOLDER, input.getFileName().toString())));
|
||||||
|
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(input)));
|
||||||
|
return previewBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createPreviewZoomBar(Image image) {
|
||||||
|
final HBox zoomBar = new HBox(8);
|
||||||
|
zoomBar.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
zoomBar.getStyleClass().add("assets-details-preview-zoom-bar");
|
||||||
|
|
||||||
|
final Label zoomLabel = new Label(Container.i18n().text(I18n.ASSETS_PREVIEW_ZOOM));
|
||||||
|
zoomLabel.getStyleClass().add("assets-details-preview-zoom-label");
|
||||||
|
zoomBar.getChildren().add(zoomLabel);
|
||||||
|
|
||||||
|
final ToggleGroup zoomGroup = new ToggleGroup();
|
||||||
|
final int maxZoom = AssetWorkspace.maxPreviewZoom(image);
|
||||||
|
for (int zoom : List.of(1, 2, 4, 8)) {
|
||||||
|
final ToggleButton button = new ToggleButton("x" + zoom);
|
||||||
|
button.getStyleClass().addAll(
|
||||||
|
"assets-details-preview-zoom-button",
|
||||||
|
"studio-button",
|
||||||
|
"studio-button-secondary",
|
||||||
|
"studio-button-pill",
|
||||||
|
"studio-button-toggle");
|
||||||
|
button.setToggleGroup(zoomGroup);
|
||||||
|
button.setSelected(viewState.selectedPreviewZoom() == zoom);
|
||||||
|
button.setDisable(zoom > maxZoom);
|
||||||
|
button.setOnAction(event -> interactions.updatePreviewZoom(zoom));
|
||||||
|
zoomBar.getChildren().add(button);
|
||||||
|
}
|
||||||
|
return zoomBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createStagedMutationPanel(AssetWorkspaceMutationPreview preview) {
|
||||||
|
final VBox panel = new VBox(10);
|
||||||
|
panel.getStyleClass().add("assets-mutation-panel");
|
||||||
|
|
||||||
|
final Label title = new Label(Container.i18n().format(I18n.ASSETS_MUTATION_PREVIEW_TITLE, actionLabel(preview.action())));
|
||||||
|
title.getStyleClass().add("assets-mutation-panel-title");
|
||||||
|
panel.getChildren().add(title);
|
||||||
|
panel.getChildren().add(createMutationSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_AFFECTED_ASSET),
|
||||||
|
createAffectedAssetContent(preview)));
|
||||||
|
|
||||||
|
final AssetWorkspaceMutationImpactViewModel impacts = AssetWorkspaceMutationImpactViewModel.from(preview);
|
||||||
|
panel.getChildren().add(createMutationSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_REGISTRY_IMPACT),
|
||||||
|
createMutationChangesContent(impacts.registryChanges(), Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_REGISTRY_IMPACT))));
|
||||||
|
panel.getChildren().add(createMutationSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_WORKSPACE_IMPACT),
|
||||||
|
createMutationChangesContent(impacts.workspaceChanges(), Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_WORKSPACE_IMPACT))));
|
||||||
|
panel.getChildren().add(createMutationSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_BLOCKERS),
|
||||||
|
createMutationMessages(preview.blockers(), "assets-mutation-message-blocker", Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_BLOCKERS))));
|
||||||
|
panel.getChildren().add(createMutationSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_WARNINGS),
|
||||||
|
createMutationMessages(preview.warnings(), "assets-mutation-message-warning", Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_WARNINGS))));
|
||||||
|
panel.getChildren().add(createMutationSection(
|
||||||
|
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_SAFE_FIXES),
|
||||||
|
createMutationMessages(preview.safeFixes(), "assets-mutation-message-safe-fix", Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_SAFE_FIXES))));
|
||||||
|
|
||||||
|
final HBox actions = new HBox(8);
|
||||||
|
final Button cancel = new Button(Container.i18n().text(I18n.ASSETS_MUTATION_CANCEL));
|
||||||
|
cancel.getStyleClass().addAll("studio-button", "studio-button-cancel");
|
||||||
|
cancel.setOnAction(event -> interactions.cancelStagedMutationPreview());
|
||||||
|
final Button apply = new Button(Container.i18n().text(I18n.ASSETS_MUTATION_APPLY));
|
||||||
|
apply.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||||
|
apply.setDisable(!preview.canApply());
|
||||||
|
apply.setOnAction(event -> interactions.applyStagedMutation(preview));
|
||||||
|
actions.getChildren().addAll(cancel, apply);
|
||||||
|
panel.getChildren().add(actions);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createMutationSection(String title, Node content) {
|
||||||
|
final VBox section = new VBox(6);
|
||||||
|
final Label label = new Label(title);
|
||||||
|
label.getStyleClass().add("assets-mutation-section-title");
|
||||||
|
section.getChildren().addAll(label, content);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createAffectedAssetContent(AssetWorkspaceMutationPreview preview) {
|
||||||
|
final VBox box = new VBox(6);
|
||||||
|
box.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_NAME), preview.asset().assetName()));
|
||||||
|
box.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), projectRelativePath(preview.asset().assetRoot())));
|
||||||
|
if (preview.targetAssetRoot() != null) {
|
||||||
|
box.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_TARGET_LOCATION), projectRelativePath(preview.targetAssetRoot())));
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createMutationChangesContent(List<AssetWorkspaceMutationChange> changes, String emptyText) {
|
||||||
|
if (changes.isEmpty()) {
|
||||||
|
return createSectionMessage(emptyText);
|
||||||
|
}
|
||||||
|
final VBox box = new VBox(6);
|
||||||
|
for (AssetWorkspaceMutationChange change : changes) {
|
||||||
|
final Label label = new Label(change.verb() + " · " + change.target());
|
||||||
|
label.getStyleClass().add("assets-mutation-change");
|
||||||
|
box.getChildren().add(label);
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createMutationMessages(List<String> messages, String styleClass, String emptyText) {
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
return createSectionMessage(emptyText);
|
||||||
|
}
|
||||||
|
final VBox box = new VBox(6);
|
||||||
|
for (String message : messages) {
|
||||||
|
final Label label = new Label(message);
|
||||||
|
label.setWrapText(true);
|
||||||
|
label.getStyleClass().add(styleClass);
|
||||||
|
box.getChildren().add(label);
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox createSection(String title, Node content) {
|
||||||
|
final VBox section = new VBox(10);
|
||||||
|
section.getStyleClass().add("assets-details-section");
|
||||||
|
final Label titleLabel = new Label(title);
|
||||||
|
titleLabel.getStyleClass().add("assets-details-section-title");
|
||||||
|
section.getChildren().addAll(titleLabel, content);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createSectionMessage(String text) {
|
||||||
|
final Label label = new Label(text);
|
||||||
|
label.setWrapText(true);
|
||||||
|
label.getStyleClass().add("assets-details-section-message");
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createKeyValueRow(String key, String value) {
|
||||||
|
final Label valueLabel = new Label(value);
|
||||||
|
valueLabel.getStyleClass().add("assets-details-value");
|
||||||
|
valueLabel.setWrapText(true);
|
||||||
|
return createKeyValueRow(key, valueLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createKeyValueRow(String key, Node valueNode) {
|
||||||
|
final HBox row = new HBox(12);
|
||||||
|
row.setAlignment(Pos.TOP_LEFT);
|
||||||
|
final Label keyLabel = new Label(key);
|
||||||
|
keyLabel.getStyleClass().add("assets-details-key");
|
||||||
|
HBox.setHgrow(valueNode, Priority.ALWAYS);
|
||||||
|
row.getChildren().addAll(keyLabel, valueNode);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String registrationLabel(AssetWorkspaceAssetState state) {
|
||||||
|
return switch (state) {
|
||||||
|
case REGISTERED -> Container.i18n().text(I18n.ASSETS_VALUE_REGISTERED);
|
||||||
|
case UNREGISTERED -> Container.i18n().text(I18n.ASSETS_VALUE_UNREGISTERED);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildParticipationLabel(AssetWorkspaceBuildParticipation buildParticipation) {
|
||||||
|
return switch (buildParticipation) {
|
||||||
|
case INCLUDED -> Container.i18n().text(I18n.ASSETS_VALUE_INCLUDED);
|
||||||
|
case EXCLUDED -> Container.i18n().text(I18n.ASSETS_VALUE_EXCLUDED);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String yesNo(boolean value) {
|
||||||
|
return value ? Container.i18n().text(I18n.ASSETS_VALUE_YES) : Container.i18n().text(I18n.ASSETS_VALUE_NO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String projectRelativePath(Path path) {
|
||||||
|
try {
|
||||||
|
return projectReference.rootPath().relativize(path.toAbsolutePath().normalize()).toString();
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
return path.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String actionLabel(AssetWorkspaceAction action) {
|
||||||
|
return switch (action) {
|
||||||
|
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);
|
||||||
|
case INCLUDE_IN_BUILD -> Container.i18n().text(I18n.ASSETS_ACTION_INCLUDE_IN_BUILD);
|
||||||
|
case EXCLUDE_FROM_BUILD -> Container.i18n().text(I18n.ASSETS_ACTION_EXCLUDE_FROM_BUILD);
|
||||||
|
case RELOCATE -> Container.i18n().text(I18n.ASSETS_ACTION_RELOCATE);
|
||||||
|
case REMOVE -> Container.i18n().text(I18n.ASSETS_ACTION_REMOVE);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String actionButtonVariant(AssetWorkspaceAction action, boolean sensitive) {
|
||||||
|
if (!sensitive) {
|
||||||
|
return "studio-button-primary";
|
||||||
|
}
|
||||||
|
return switch (action) {
|
||||||
|
case RELOCATE -> "studio-button-warning";
|
||||||
|
case EXCLUDE_FROM_BUILD, REMOVE -> "studio-button-danger";
|
||||||
|
default -> "studio-button-secondary";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsInput(AssetWorkspaceAssetDetails details, Path input) {
|
||||||
|
return details.inputsByRole().values().stream().flatMap(List::stream).anyMatch(input::equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extensionOf(Path input) {
|
||||||
|
final String fileName = input.getFileName().toString();
|
||||||
|
final int dot = fileName.lastIndexOf('.');
|
||||||
|
if (dot < 0 || dot == fileName.length() - 1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return fileName.substring(dot + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isImage(String extension) {
|
||||||
|
return extension.equals("png") || extension.equals("jpg") || extension.equals("jpeg") || extension.equals("gif");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isText(String extension) {
|
||||||
|
return extension.equals("txt") || extension.equals("json") || extension.equals("md") || extension.equals("xml") || extension.equals("csv");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAudio(String extension) {
|
||||||
|
return extension.equals("wav") || extension.equals("mp3") || extension.equals("ogg");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetWorkspaceDetailsViewState(
|
||||||
|
AssetWorkspaceState workspaceState,
|
||||||
|
AssetWorkspaceDetailsStatus detailsStatus,
|
||||||
|
AssetWorkspaceAssetDetails selectedAssetDetails,
|
||||||
|
String detailsErrorMessage,
|
||||||
|
AssetWorkspaceMutationPreview stagedMutationPreview,
|
||||||
|
Path selectedPreviewInput,
|
||||||
|
int selectedPreviewZoom) {
|
||||||
|
|
||||||
|
public AssetWorkspaceDetailsViewState {
|
||||||
|
Objects.requireNonNull(workspaceState, "workspaceState");
|
||||||
|
Objects.requireNonNull(detailsStatus, "detailsStatus");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
interface AssetWorkspaceInteractionPort {
|
||||||
|
void updateSearchQuery(String searchQuery);
|
||||||
|
void updateActiveFilters(EnumSet<AssetNavigatorFilter> filters);
|
||||||
|
void selectAsset(AssetWorkspaceSelectionKey selectionKey);
|
||||||
|
void requestMutationPreview(AssetWorkspaceAction action);
|
||||||
|
void cancelStagedMutationPreview();
|
||||||
|
void applyStagedMutation(AssetWorkspaceMutationPreview preview);
|
||||||
|
void updatePreviewInput(Path input);
|
||||||
|
void updatePreviewZoom(int zoom);
|
||||||
|
void updatePreload(AssetWorkspaceAssetDetails details, boolean preloadEnabled, CheckBox checkBox);
|
||||||
|
}
|
||||||
@ -0,0 +1,246 @@
|
|||||||
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.layout.FlowPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import p.studio.Container;
|
||||||
|
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||||
|
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
|
||||||
|
import p.studio.events.StudioAssetsNavigatorViewStateChangedEvent;
|
||||||
|
import p.studio.events.StudioWorkspaceEventBus;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.utilities.i18n.I18n;
|
||||||
|
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
final class AssetWorkspaceNavigatorControl extends VBox implements StudioControlLifecycle {
|
||||||
|
private final ProjectReference projectReference;
|
||||||
|
private final Path projectRoot;
|
||||||
|
private final StudioWorkspaceEventBus workspaceBus;
|
||||||
|
private final AssetWorkspaceInteractionPort interactions;
|
||||||
|
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||||
|
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 Map<AssetNavigatorFilter, ToggleButton> filterButtons = new EnumMap<>(AssetNavigatorFilter.class);
|
||||||
|
|
||||||
|
private AssetWorkspaceNavigatorViewState viewState;
|
||||||
|
private EnumSet<AssetNavigatorFilter> activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class);
|
||||||
|
|
||||||
|
AssetWorkspaceNavigatorControl(
|
||||||
|
ProjectReference projectReference,
|
||||||
|
StudioWorkspaceEventBus workspaceBus,
|
||||||
|
AssetWorkspaceInteractionPort interactions) {
|
||||||
|
StudioControlLifecycleSupport.install(this, this);
|
||||||
|
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
|
this.projectRoot = this.projectReference.rootPath();
|
||||||
|
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||||
|
this.interactions = Objects.requireNonNull(interactions, "interactions");
|
||||||
|
|
||||||
|
getStyleClass().add("assets-workspace-pane");
|
||||||
|
setSpacing(8);
|
||||||
|
|
||||||
|
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 current = newValue == null ? "" : newValue;
|
||||||
|
if (!Objects.equals(oldValue, current)) {
|
||||||
|
interactions.updateSearchQuery(current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
getChildren().addAll(navigatorTitle, searchField, filterBar, navigatorStateLabel, navigatorScroll);
|
||||||
|
VBox.setVgrow(navigatorScroll, Priority.ALWAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void subscribe() {
|
||||||
|
subscriptions.add(workspaceBus.subscribe(StudioAssetsNavigatorViewStateChangedEvent.class, event -> {
|
||||||
|
if (projectReference.equals(event.project())) {
|
||||||
|
applyViewState(event.viewState());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unsubscribe() {
|
||||||
|
subscriptions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyViewState(AssetWorkspaceNavigatorViewState viewState) {
|
||||||
|
this.viewState = viewState;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.REGISTERED, I18n.ASSETS_FILTER_REGISTERED);
|
||||||
|
addFilterButton(AssetNavigatorFilter.UNREGISTERED, I18n.ASSETS_FILTER_UNREGISTERED);
|
||||||
|
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().addAll("studio-button", "studio-button-secondary", "studio-button-pill", "studio-button-toggle");
|
||||||
|
button.selectedProperty().addListener((ignored, oldValue, selected) -> {
|
||||||
|
if (selected) {
|
||||||
|
activeFilters.add(filter);
|
||||||
|
} else {
|
||||||
|
activeFilters.remove(filter);
|
||||||
|
}
|
||||||
|
interactions.updateActiveFilters(activeFilters.isEmpty() ? EnumSet.noneOf(AssetNavigatorFilter.class) : EnumSet.copyOf(activeFilters));
|
||||||
|
});
|
||||||
|
filterButtons.put(filter, button);
|
||||||
|
filterBar.getChildren().add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render() {
|
||||||
|
if (viewState == null) {
|
||||||
|
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_LOADING));
|
||||||
|
navigatorContent.getChildren().setAll(createMessage(Container.i18n().text(I18n.ASSETS_STATE_LOADING)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigatorStateLabel.setText(viewState.message());
|
||||||
|
if (!viewState.hasProjection()) {
|
||||||
|
navigatorContent.getChildren().setAll(createMessage(viewState.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigatorContent.getChildren().clear();
|
||||||
|
for (AssetNavigatorGroup group : viewState.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, asset.selectionKey().equals(viewState.workspaceState().selectedKey())));
|
||||||
|
}
|
||||||
|
|
||||||
|
navigatorContent.getChildren().add(groupBox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createAssetRow(AssetWorkspaceAssetSummary asset, boolean selected) {
|
||||||
|
final VBox row = new VBox(4);
|
||||||
|
row.getStyleClass().add("assets-workspace-asset-row");
|
||||||
|
row.getStyleClass().add(assetRowToneClass(asset.assetFamily()));
|
||||||
|
updateAssetRowSelection(row, selected);
|
||||||
|
|
||||||
|
final HBox topLine = new HBox(8);
|
||||||
|
topLine.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
final Label name = new Label(asset.assetName());
|
||||||
|
name.getStyleClass().add("assets-workspace-asset-name");
|
||||||
|
name.getStyleClass().add(assetNameToneClass(asset.assetFamily()));
|
||||||
|
final Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
final HBox badges = new HBox(6);
|
||||||
|
badges.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
badges.getStyleClass().add("assets-workspace-asset-badges");
|
||||||
|
if (asset.state() == AssetWorkspaceAssetState.UNREGISTERED) {
|
||||||
|
badges.getChildren().add(createBadge(Container.i18n().text(I18n.ASSETS_BADGE_UNREGISTERED), "assets-workspace-badge-orphan"));
|
||||||
|
} else if (asset.buildParticipation() == AssetWorkspaceBuildParticipation.INCLUDED) {
|
||||||
|
badges.getChildren().add(createBadge(buildParticipationLabel(asset.buildParticipation()), "assets-workspace-badge-preload"));
|
||||||
|
if (asset.preload()) {
|
||||||
|
badges.getChildren().add(createBadge(Container.i18n().text(I18n.ASSETS_BADGE_PRELOAD), "assets-workspace-badge-preload"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
badges.getChildren().add(createBadge(buildParticipationLabel(asset.buildParticipation()), "assets-workspace-badge-diagnostics"));
|
||||||
|
}
|
||||||
|
if (asset.hasDiagnostics()) {
|
||||||
|
badges.getChildren().add(createBadge(Container.i18n().text(I18n.ASSETS_BADGE_DIAGNOSTICS), "assets-workspace-badge-diagnostics"));
|
||||||
|
}
|
||||||
|
topLine.getChildren().addAll(name, spacer, badges);
|
||||||
|
|
||||||
|
final Label path = new Label(AssetNavigatorProjectionBuilder.relativeRoot(asset, projectRoot));
|
||||||
|
path.getStyleClass().add("assets-workspace-asset-path");
|
||||||
|
row.getChildren().addAll(topLine, path);
|
||||||
|
row.setOnMouseClicked(event -> interactions.selectAsset(asset.selectionKey()));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAssetRowSelection(VBox row, boolean selected) {
|
||||||
|
if (selected) {
|
||||||
|
if (!row.getStyleClass().contains("assets-workspace-asset-row-selected")) {
|
||||||
|
row.getStyleClass().add("assets-workspace-asset-row-selected");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.getStyleClass().remove("assets-workspace-asset-row-selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createMessage(String text) {
|
||||||
|
final Label label = new Label(text);
|
||||||
|
label.getStyleClass().add("assets-workspace-empty-state");
|
||||||
|
label.setWrapText(true);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String buildParticipationLabel(AssetWorkspaceBuildParticipation buildParticipation) {
|
||||||
|
return switch (buildParticipation) {
|
||||||
|
case INCLUDED -> Container.i18n().text(I18n.ASSETS_VALUE_INCLUDED);
|
||||||
|
case EXCLUDED -> Container.i18n().text(I18n.ASSETS_VALUE_EXCLUDED);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String assetRowToneClass(String assetFamily) {
|
||||||
|
return switch (assetFamily == null ? "" : assetFamily.toLowerCase()) {
|
||||||
|
case "image_bank" -> "assets-workspace-asset-row-image";
|
||||||
|
case "palette_bank" -> "assets-workspace-asset-row-palette";
|
||||||
|
case "sound_bank" -> "assets-workspace-asset-row-sound";
|
||||||
|
default -> "assets-workspace-asset-row-generic";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String assetNameToneClass(String assetFamily) {
|
||||||
|
return switch (assetFamily == null ? "" : assetFamily.toLowerCase()) {
|
||||||
|
case "image_bank" -> "assets-workspace-asset-name-image";
|
||||||
|
case "palette_bank" -> "assets-workspace-asset-name-palette";
|
||||||
|
case "sound_bank" -> "assets-workspace-asset-name-sound";
|
||||||
|
default -> "assets-workspace-asset-name-generic";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetWorkspaceNavigatorViewState(
|
||||||
|
AssetWorkspaceState workspaceState,
|
||||||
|
AssetNavigatorProjection projection,
|
||||||
|
String message) {
|
||||||
|
|
||||||
|
public AssetWorkspaceNavigatorViewState {
|
||||||
|
Objects.requireNonNull(workspaceState, "workspaceState");
|
||||||
|
Objects.requireNonNull(message, "message");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasProjection() {
|
||||||
|
return projection != null && !projection.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package p.studio.workspaces.framework;
|
||||||
|
|
||||||
|
import p.studio.utilities.events.EventSubscription;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class StudioSubscriptionBag {
|
||||||
|
private final List<EventSubscription> subscriptions = new ArrayList<>();
|
||||||
|
|
||||||
|
public void add(EventSubscription subscription) {
|
||||||
|
subscriptions.add(Objects.requireNonNull(subscription, "subscription"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
subscriptions.forEach(EventSubscription::unsubscribe);
|
||||||
|
subscriptions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return subscriptions.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package p.studio.workspaces.framework;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import p.studio.utilities.events.EventSubscription;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
final class StudioSubscriptionBagTest {
|
||||||
|
@Test
|
||||||
|
void clearUnsubscribesAllRegisteredSubscriptions() {
|
||||||
|
final AtomicInteger unsubscribed = new AtomicInteger();
|
||||||
|
final StudioSubscriptionBag bag = new StudioSubscriptionBag();
|
||||||
|
|
||||||
|
bag.add(unsubscribed::incrementAndGet);
|
||||||
|
bag.add(unsubscribed::incrementAndGet);
|
||||||
|
|
||||||
|
assertFalse(bag.isEmpty());
|
||||||
|
|
||||||
|
bag.clear();
|
||||||
|
|
||||||
|
assertTrue(bag.isEmpty());
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(2, unsubscribed.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user