diff --git a/prometeu-studio/src/main/java/p/studio/events/StudioAssetsDetailsViewStateChangedEvent.java b/prometeu-studio/src/main/java/p/studio/events/StudioAssetsDetailsViewStateChangedEvent.java new file mode 100644 index 00000000..764e8fe2 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/events/StudioAssetsDetailsViewStateChangedEvent.java @@ -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"); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/events/StudioAssetsNavigatorViewStateChangedEvent.java b/prometeu-studio/src/main/java/p/studio/events/StudioAssetsNavigatorViewStateChangedEvent.java new file mode 100644 index 00000000..7e3f975c --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/events/StudioAssetsNavigatorViewStateChangedEvent.java @@ -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"); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java index f27f7c63..91156bc7 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java @@ -36,7 +36,7 @@ import java.nio.file.Path; import java.util.*; 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 final BorderPane root = new BorderPane(); @@ -48,23 +48,14 @@ public final class AssetWorkspace implements Workspace { private final FileSystemPackerBuildService packService; 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 doctorButton = 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 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 ScrollPane detailsScroll = new ScrollPane(); - - private final Map filterButtons = new EnumMap<>(AssetNavigatorFilter.class); - private final Map assetRowsBySelectionKey = new HashMap<>(); + private final AssetWorkspaceNavigatorControl navigatorControl; + private final AssetWorkspaceDetailsControl detailsControl; private final EnumSet activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class); private volatile AssetWorkspaceState state = AssetWorkspaceState.loading(null); @@ -115,6 +106,8 @@ public final class AssetWorkspace implements Workspace { new p.packer.declarations.PackerAssetDetailsService(), 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(); root.getStyleClass().add("assets-workspace"); @@ -157,16 +150,6 @@ public final class AssetWorkspace implements Workspace { 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 -> { if (projectMatches(event.project())) { applyAssetSummaryPatch(event.summary()); @@ -186,25 +169,6 @@ public final class AssetWorkspace implements Workspace { inlineProgressBar.setManaged(false); final VBox topProgress = new VBox(6, inlineProgressLabel, inlineProgressBar); 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.getStyleClass().addAll("studio-button", "studio-button-primary"); 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.getStyleClass().addAll("studio-button", "studio-button-secondary"); packButton.setOnAction(event -> runPack()); - navigatorStateLabel.getStyleClass().add("assets-workspace-pane-body"); - navigatorContent.getStyleClass().add("assets-workspace-navigator-content"); - final ScrollPane navigatorScroll = new ScrollPane(navigatorContent); - navigatorScroll.setFitToWidth(true); - navigatorScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - navigatorScroll.getStyleClass().add("assets-workspace-navigator-scroll"); - navigatorPane.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); + final SplitPane splitPane = new SplitPane(navigatorControl, detailsControl); splitPane.setDividerPositions(0.34); splitPane.getStyleClass().add("assets-workspace-split"); final HBox workspaceActionBar = new HBox(8, addAssetButton, doctorButton, packButton); @@ -269,33 +205,6 @@ public final class AssetWorkspace implements Workspace { 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() { final boolean preserveVisibleContent = hasVisibleWorkspaceContent(); if (!preserveVisibleContent) { @@ -411,260 +320,53 @@ public final class AssetWorkspace implements Workspace { } private void renderState() { - renderNavigator(); - renderDetails(); + publishNavigatorViewState(); + publishDetailsViewState(); } private void requestRedraw() { - requestNavigatorRedraw(); - requestDetailsRedraw(); + publishNavigatorViewState(); + publishDetailsViewState(); } private void requestNavigatorRedraw() { - workspaceBus.publish(new StudioAssetsNavigatorRedrawRequestedEvent(projectReference)); + publishNavigatorViewState(); } private void requestDetailsRedraw() { - workspaceBus.publish(new StudioAssetsDetailsRedrawRequestedEvent(projectReference)); + publishDetailsViewState(); } - private void renderNavigator() { - assetRowsBySelectionKey.clear(); - switch (state.status()) { - case LOADING -> { - navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_LOADING)); - navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_LOADING))); - } - 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 publishNavigatorViewState() { + final AssetNavigatorProjection projection = state.status() == AssetWorkspaceStatus.READY + ? AssetNavigatorProjectionBuilder.build(state.assets(), projectRoot(), searchQuery, activeFilters) + : null; + workspaceBus.publish(new StudioAssetsNavigatorViewStateChangedEvent( + projectReference, + new AssetWorkspaceNavigatorViewState(state, projection, navigatorMessage(projection)))); } - private void renderDetails() { - detailsContent.getChildren().clear(); - switch (state.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(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 publishDetailsViewState() { + workspaceBus.publish(new StudioAssetsDetailsViewStateChangedEvent( + projectReference, + new AssetWorkspaceDetailsViewState( + state, + detailsStatus, + selectedAssetDetails, + detailsErrorMessage, + stagedMutationPreview, + selectedPreviewInput, + selectedPreviewZoom))); } - private void renderSelectedAssetDetails(AssetWorkspaceAssetSummary summary) { - detailsContent.getChildren().add(createSummaryActionsRow(summary)); - if (detailsStatus == AssetWorkspaceDetailsStatus.LOADING) { - detailsContent.getChildren().add(createSection( - Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), - createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)))); - detailsContent.getChildren().add(createSection( - Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), - 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> 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"; + private String navigatorMessage(AssetNavigatorProjection projection) { + return switch (state.status()) { + case LOADING -> Container.i18n().text(I18n.ASSETS_STATE_LOADING); + case EMPTY -> Container.i18n().text(I18n.ASSETS_STATE_EMPTY); + case ERROR -> state.errorMessage(); + case READY -> projection == null || projection.isEmpty() + ? Container.i18n().text(I18n.ASSETS_STATE_NO_RESULTS) + : Container.i18n().format(I18n.ASSETS_STATE_READY, projection.visibleAssetCount(), state.assets().size()); }; } @@ -678,88 +380,7 @@ public final class AssetWorkspace implements Workspace { }; } - 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)); - 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) { + static void applyPreviewScale(Image image, ImageView imageView, int requestedZoom) { final double width = image.getWidth(); final double height = image.getHeight(); 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)); } - 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 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) { + @Override + public void updatePreload(AssetWorkspaceAssetDetails details, boolean preloadEnabled, CheckBox checkBox) { checkBox.setDisable(true); setInlineProgress("Updating preload...", ProgressBar.INDETERMINATE_PROGRESS, true); appendLog("Updating preload for " + details.summary().assetName() + " to " + yesNo(preloadEnabled) + "."); @@ -929,114 +507,8 @@ public final class AssetWorkspace implements Workspace { requestRedraw(); } - private void renderNavigatorProjection(AssetNavigatorProjection projection) { - navigatorContent.getChildren().clear(); - for (AssetNavigatorGroup group : projection.groups()) { - final VBox groupBox = new VBox(6); - groupBox.getStyleClass().add("assets-workspace-group"); - - final Label groupLabel = new Label(group.label()); - groupLabel.getStyleClass().add("assets-workspace-group-label"); - groupBox.getChildren().add(groupLabel); - - for (AssetWorkspaceAssetSummary asset : group.assets()) { - groupBox.getChildren().add(createAssetRow(asset)); - } - - navigatorContent.getChildren().add(groupBox); - } - } - - private Node createAssetRow(AssetWorkspaceAssetSummary asset) { - final VBox row = new VBox(4); - row.getStyleClass().add("assets-workspace-asset-row"); - 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) { + @Override + public void selectAsset(AssetWorkspaceSelectionKey selectionKey) { workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(projectReference, selectionKey)); } @@ -1044,8 +516,7 @@ public final class AssetWorkspace implements Workspace { state = state.withSelection(selectionKey); stagedMutationPreview = null; appendLog("Selected asset " + selectionKey.stableKey() + "."); - updateNavigatorSelection(); - requestDetailsRedraw(); + requestRedraw(); workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey)); loadSelectedAssetDetails(selectionKey); } @@ -1122,7 +593,23 @@ public final class AssetWorkspace implements Workspace { 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 filters) { + activeFilters.clear(); + if (filters != null) { + activeFilters.addAll(filters); + } + requestNavigatorRedraw(); + } + + @Override + public void requestMutationPreview(AssetWorkspaceAction action) { final AssetWorkspaceAssetSummary selectedAsset = state.selectedAsset().orElse(null); if (selectedAsset == null) { return; @@ -1147,7 +634,7 @@ public final class AssetWorkspace implements Workspace { stagedMutationPreview = mutationService.preview(projectReference, selectedAsset, action, null); appendLog("Preview ready for " + actionLabel(action) + "."); requestDetailsRedraw(); - Platform.runLater(() -> detailsScroll.setVvalue(0.0d)); + Platform.runLater(detailsControl::scrollToTop); } catch (RuntimeException runtimeException) { final String message = rootCauseMessage(runtimeException); 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) { runDirectMutationFlow(selectedAsset, AssetWorkspaceAction.REGISTER); } @@ -1371,7 +877,8 @@ public final class AssetWorkspace implements Workspace { return dialog.showAndWait().filter(ButtonType.OK::equals).isPresent(); } - private void applyStagedMutation(AssetWorkspaceMutationPreview preview) { + @Override + public void applyStagedMutation(AssetWorkspaceMutationPreview preview) { if (!preview.canApply()) { return; } @@ -1399,7 +906,7 @@ public final class AssetWorkspace implements Workspace { return details.inputsByRole().values().stream().flatMap(List::stream).anyMatch(input::equals); } - private String readPreviewText(Path input) { + static String readPreviewText(Path input) { try { final String text = Files.readString(input); return text.length() > 4000 ? text.substring(0, 4000) + "\n\n…" : text; @@ -1442,6 +949,30 @@ public final class AssetWorkspace implements Workspace { || 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) { return value ? Container.i18n().text(I18n.ASSETS_VALUE_YES) : Container.i18n().text(I18n.ASSETS_VALUE_NO); } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsControl.java new file mode 100644 index 00000000..2d21aca3 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsControl.java @@ -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> 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 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 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"); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsViewState.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsViewState.java new file mode 100644 index 00000000..3fba520e --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsViewState.java @@ -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"); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceInteractionPort.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceInteractionPort.java new file mode 100644 index 00000000..0e8bf3c5 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceInteractionPort.java @@ -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 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); +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceNavigatorControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceNavigatorControl.java new file mode 100644 index 00000000..ff7617c1 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceNavigatorControl.java @@ -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 filterButtons = new EnumMap<>(AssetNavigatorFilter.class); + + private AssetWorkspaceNavigatorViewState viewState; + private EnumSet 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"; + }; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceNavigatorViewState.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceNavigatorViewState.java new file mode 100644 index 00000000..a8b48a5a --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceNavigatorViewState.java @@ -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(); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/framework/StudioSubscriptionBag.java b/prometeu-studio/src/main/java/p/studio/workspaces/framework/StudioSubscriptionBag.java new file mode 100644 index 00000000..863386a4 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/framework/StudioSubscriptionBag.java @@ -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 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(); + } +} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/framework/StudioSubscriptionBagTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/framework/StudioSubscriptionBagTest.java new file mode 100644 index 00000000..c07ab103 --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/workspaces/framework/StudioSubscriptionBagTest.java @@ -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()); + } +}