From 03ca81cd1e17c0ab3ef065843ed94fb44e28520b Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 12 Mar 2026 09:37:40 +0000 Subject: [PATCH] implements PR-07c asset details and form lifecycle --- .../assets/AssetWorkspaceDetailsControl.java | 509 ++---------------- .../AssetWorkspaceDetailsUiSupport.java | 125 +++++ .../AssetWorkspaceDiagnosticsControl.java | 85 +++ .../AssetWorkspaceInputsPreviewControl.java | 202 +++++++ .../AssetWorkspaceRuntimeContractControl.java | 87 +++ .../AssetWorkspaceSummaryActionsControl.java | 203 +++++++ 6 files changed, 734 insertions(+), 477 deletions(-) create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsUiSupport.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDiagnosticsControl.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceInputsPreviewControl.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceRuntimeContractControl.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceSummaryActionsControl.java 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 index 2d21aca3..1ce2ced7 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsControl.java @@ -1,22 +1,8 @@ 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; @@ -27,21 +13,22 @@ 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 final AssetWorkspaceSummaryActionsControl summaryActionsControl; + private final AssetWorkspaceRuntimeContractControl runtimeContractControl; + private final AssetWorkspaceInputsPreviewControl inputsPreviewControl; + private final AssetWorkspaceDiagnosticsControl diagnosticsControl; private AssetWorkspaceDetailsViewState viewState; + private boolean readyMounted; AssetWorkspaceDetailsControl( ProjectReference projectReference, @@ -50,7 +37,10 @@ final class AssetWorkspaceDetailsControl extends VBox implements StudioControlLi StudioControlLifecycleSupport.install(this, this); this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus"); - this.interactions = Objects.requireNonNull(interactions, "interactions"); + this.summaryActionsControl = new AssetWorkspaceSummaryActionsControl(projectReference, workspaceBus, interactions); + this.runtimeContractControl = new AssetWorkspaceRuntimeContractControl(projectReference, workspaceBus, interactions); + this.inputsPreviewControl = new AssetWorkspaceInputsPreviewControl(projectReference, workspaceBus, interactions); + this.diagnosticsControl = new AssetWorkspaceDiagnosticsControl(projectReference, workspaceBus); getStyleClass().add("assets-workspace-pane"); setSpacing(10); @@ -74,7 +64,8 @@ final class AssetWorkspaceDetailsControl extends VBox implements StudioControlLi public void subscribe() { subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> { if (projectReference.equals(event.project())) { - applyViewState(event.viewState()); + viewState = event.viewState(); + render(); } })); } @@ -88,483 +79,47 @@ final class AssetWorkspaceDetailsControl extends VBox implements StudioControlLi detailsScroll.setVvalue(0.0d); } - private void applyViewState(AssetWorkspaceDetailsViewState viewState) { - this.viewState = viewState; - render(); - } - private void render() { - detailsContent.getChildren().clear(); if (viewState == null) { + readyMounted = false; workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING)); - detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))); + detailsContent.getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))); return; } switch (viewState.workspaceState().status()) { case LOADING -> { + readyMounted = false; workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING)); - detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))); + detailsContent.getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))); } case EMPTY -> { + readyMounted = false; workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY)); - detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))); + detailsContent.getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))); } case ERROR -> { + readyMounted = false; workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR)); - detailsContent.getChildren().add(createSectionMessage(viewState.workspaceState().errorMessage())); + detailsContent.getChildren().setAll(AssetWorkspaceDetailsUiSupport.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"); + if (viewState.workspaceState().selectedAsset().isEmpty()) { + readyMounted = false; + detailsContent.getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION))); + return; + } + + if (!readyMounted) { + readyMounted = true; + detailsContent.getChildren().setAll( + summaryActionsControl, + runtimeContractControl, + inputsPreviewControl, + diagnosticsControl); } - 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/AssetWorkspaceDetailsUiSupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsUiSupport.java new file mode 100644 index 00000000..c0fee7f6 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDetailsUiSupport.java @@ -0,0 +1,125 @@ +package p.studio.workspaces.assets; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import p.studio.Container; +import p.studio.projects.ProjectReference; +import p.studio.utilities.i18n.I18n; + +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +final class AssetWorkspaceDetailsUiSupport { + private AssetWorkspaceDetailsUiSupport() { + } + + static 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; + } + + static Node createSectionMessage(String text) { + final Label label = new Label(text); + label.setWrapText(true); + label.getStyleClass().add("assets-details-section-message"); + return label; + } + + static 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); + } + + static 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; + } + + static 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); + }; + } + + static 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); + }; + } + + static String yesNo(boolean value) { + return value ? Container.i18n().text(I18n.ASSETS_VALUE_YES) : Container.i18n().text(I18n.ASSETS_VALUE_NO); + } + + static String projectRelativePath(ProjectReference projectReference, Path path) { + try { + return projectReference.rootPath().relativize(path.toAbsolutePath().normalize()).toString(); + } catch (RuntimeException runtimeException) { + return path.toString(); + } + } + + static 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); + }; + } + + static 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"; + }; + } + + static boolean containsInput(AssetWorkspaceAssetDetails details, Path input) { + return details.inputsByRole().values().stream().flatMap(List::stream).anyMatch(input::equals); + } + + static 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(Locale.ROOT); + } + + static boolean isImage(String extension) { + return extension.equals("png") || extension.equals("jpg") || extension.equals("jpeg") || extension.equals("gif"); + } + + static boolean isText(String extension) { + return extension.equals("txt") || extension.equals("json") || extension.equals("md") || extension.equals("xml") || extension.equals("csv"); + } + + static 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/AssetWorkspaceDiagnosticsControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDiagnosticsControl.java new file mode 100644 index 00000000..ee678a98 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceDiagnosticsControl.java @@ -0,0 +1,85 @@ +package p.studio.workspaces.assets; + +import javafx.scene.control.Label; +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.util.Objects; + +final class AssetWorkspaceDiagnosticsControl extends VBox implements StudioControlLifecycle { + private final ProjectReference projectReference; + private final StudioWorkspaceEventBus workspaceBus; + private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag(); + private AssetWorkspaceDetailsViewState viewState; + + AssetWorkspaceDiagnosticsControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) { + StudioControlLifecycleSupport.install(this, this); + this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); + this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus"); + } + + @Override + public void subscribe() { + subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> { + if (projectReference.equals(event.project())) { + viewState = event.viewState(); + render(); + } + })); + } + + @Override + public void unsubscribe() { + subscriptions.clear(); + } + + private void render() { + if (viewState == null || viewState.workspaceState().selectedAsset().isEmpty()) { + getChildren().clear(); + return; + } + + if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.LOADING) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)))); + return; + } + + if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.ERROR || viewState.selectedAssetDetails() == null) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))))); + return; + } + + if (viewState.selectedAssetDetails().diagnostics().isEmpty()) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_EMPTY)))); + return; + } + + final VBox diagnosticsBox = new VBox(8); + for (AssetWorkspaceDiagnostic diagnostic : viewState.selectedAssetDetails().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); + } + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection(Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), diagnosticsBox)); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceInputsPreviewControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceInputsPreviewControl.java new file mode 100644 index 00000000..3f92653f --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceInputsPreviewControl.java @@ -0,0 +1,202 @@ +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.Label; +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.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 AssetWorkspaceInputsPreviewControl extends VBox implements StudioControlLifecycle { + private final ProjectReference projectReference; + private final StudioWorkspaceEventBus workspaceBus; + private final AssetWorkspaceInteractionPort interactions; + private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag(); + private AssetWorkspaceDetailsViewState viewState; + + AssetWorkspaceInputsPreviewControl( + 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"); + } + + @Override + public void subscribe() { + subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> { + if (projectReference.equals(event.project())) { + viewState = event.viewState(); + render(); + } + })); + } + + @Override + public void unsubscribe() { + subscriptions.clear(); + } + + private void render() { + if (viewState == null || viewState.workspaceState().selectedAsset().isEmpty()) { + getChildren().clear(); + return; + } + + if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.LOADING) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)))); + return; + } + + if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.ERROR || viewState.selectedAssetDetails() == null) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))))); + return; + } + + final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails(); + if (details.inputsByRole().isEmpty()) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_INPUTS_EMPTY)))); + return; + } + + 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 SplitPane splitPane = new SplitPane(inputsList, createPreviewPane(selectedPreviewInput)); + splitPane.setOrientation(Orientation.HORIZONTAL); + splitPane.setDividerPositions(0.34); + splitPane.getStyleClass().add("assets-details-input-preview-split"); + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection(Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), splitPane)); + } + + private Path resolveSelectedPreviewInput(AssetWorkspaceAssetDetails details) { + final Path selectedPreviewInput = viewState.selectedPreviewInput(); + if (selectedPreviewInput == null || !AssetWorkspaceDetailsUiSupport.containsInput(details, selectedPreviewInput)) { + return details.inputsByRole().values().stream().flatMap(List::stream).findFirst().orElse(null); + } + return selectedPreviewInput; + } + + private Node createPreviewPane(Path input) { + final VBox previewBox = new VBox(10); + previewBox.getStyleClass().add("assets-details-preview-pane"); + if (input == null) { + previewBox.getChildren().add(AssetWorkspaceDetailsUiSupport.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 = AssetWorkspaceDetailsUiSupport.extensionOf(input); + if (AssetWorkspaceDetailsUiSupport.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(AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_PREVIEW_IMAGE_ERROR))); + } + previewBox.getChildren().add(AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetWorkspaceDetailsUiSupport.projectRelativePath(projectReference, input))); + return previewBox; + } + + if (AssetWorkspaceDetailsUiSupport.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 (AssetWorkspaceDetailsUiSupport.isAudio(extension)) { + previewBox.getChildren().add(AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_AUDIO_PLACEHOLDER, input.getFileName().toString()))); + previewBox.getChildren().add(AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetWorkspaceDetailsUiSupport.projectRelativePath(projectReference, input))); + return previewBox; + } + + previewBox.getChildren().add(AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_GENERIC_PLACEHOLDER, input.getFileName().toString()))); + previewBox.getChildren().add(AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetWorkspaceDetailsUiSupport.projectRelativePath(projectReference, 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; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceRuntimeContractControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceRuntimeContractControl.java new file mode 100644 index 00000000..29571aad --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceRuntimeContractControl.java @@ -0,0 +1,87 @@ +package p.studio.workspaces.assets; + +import javafx.scene.control.CheckBox; +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.util.Objects; + +final class AssetWorkspaceRuntimeContractControl extends VBox implements StudioControlLifecycle { + private final ProjectReference projectReference; + private final StudioWorkspaceEventBus workspaceBus; + private final AssetWorkspaceInteractionPort interactions; + private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag(); + private AssetWorkspaceDetailsViewState viewState; + + AssetWorkspaceRuntimeContractControl( + 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"); + } + + @Override + public void subscribe() { + subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> { + if (projectReference.equals(event.project())) { + viewState = event.viewState(); + render(); + } + })); + } + + @Override + public void unsubscribe() { + subscriptions.clear(); + } + + private void render() { + if (viewState == null || viewState.workspaceState().selectedAsset().isEmpty()) { + getChildren().clear(); + return; + } + + if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.LOADING) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)))); + return; + } + + if (viewState.detailsStatus() == AssetWorkspaceDetailsStatus.ERROR || viewState.selectedAssetDetails() == null) { + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), + AssetWorkspaceDetailsUiSupport.createSectionMessage(Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))))); + return; + } + + final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails(); + final VBox content = new VBox(8); + content.getChildren().addAll( + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_PRELOAD), createPreloadToggle(details)), + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_FORMAT), details.outputFormat()), + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_CODEC), details.outputCodec())); + getChildren().setAll(AssetWorkspaceDetailsUiSupport.createSection(Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), content)); + } + + private CheckBox createPreloadToggle(AssetWorkspaceAssetDetails details) { + final boolean currentValue = details.summary().preload(); + final CheckBox checkBox = new CheckBox(AssetWorkspaceDetailsUiSupport.yesNo(currentValue)); + checkBox.setSelected(currentValue); + checkBox.setFocusTraversable(false); + checkBox.getStyleClass().add("assets-details-readonly-check"); + checkBox.selectedProperty().addListener((ignored, previous, selected) -> checkBox.setText(AssetWorkspaceDetailsUiSupport.yesNo(selected))); + checkBox.setOnAction(event -> interactions.updatePreload(details, checkBox.isSelected(), checkBox)); + return checkBox; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceSummaryActionsControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceSummaryActionsControl.java new file mode 100644 index 00000000..fb13449b --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspaceSummaryActionsControl.java @@ -0,0 +1,203 @@ +package p.studio.workspaces.assets; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +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.util.List; +import java.util.Objects; + +final class AssetWorkspaceSummaryActionsControl extends HBox implements StudioControlLifecycle { + private final ProjectReference projectReference; + private final StudioWorkspaceEventBus workspaceBus; + private final AssetWorkspaceInteractionPort interactions; + private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag(); + private AssetWorkspaceDetailsViewState viewState; + + AssetWorkspaceSummaryActionsControl( + 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"); + setSpacing(12); + getStyleClass().add("assets-details-summary-actions-row"); + } + + @Override + public void subscribe() { + subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> { + if (projectReference.equals(event.project())) { + viewState = event.viewState(); + render(); + } + })); + } + + @Override + public void unsubscribe() { + subscriptions.clear(); + } + + private void render() { + getChildren().clear(); + if (viewState == null) { + return; + } + final AssetWorkspaceAssetSummary summary = viewState.workspaceState().selectedAsset().orElse(null); + if (summary == null) { + return; + } + + 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); + getChildren().setAll(summarySection, actionsSection); + } + + private VBox createSummarySection(AssetWorkspaceAssetSummary summary) { + final VBox content = new VBox(8); + content.getChildren().addAll( + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_NAME), summary.assetName()), + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION), AssetWorkspaceDetailsUiSupport.registrationLabel(summary.state())), + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_BUILD_PARTICIPATION), AssetWorkspaceDetailsUiSupport.buildParticipationLabel(summary.buildParticipation())), + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())), + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_TYPE), summary.assetFamily()), + AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetWorkspaceDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot()))); + return AssetWorkspaceDetailsUiSupport.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 AssetWorkspaceDetailsUiSupport.createSection(Container.i18n().text(I18n.ASSETS_SECTION_ACTIONS), content); + } + + private Button createActionButton(AssetWorkspaceAction action, boolean sensitive) { + final Button button = new Button(AssetWorkspaceDetailsUiSupport.actionLabel(action)); + button.getStyleClass().addAll("studio-button", AssetWorkspaceDetailsUiSupport.actionButtonVariant(action, sensitive)); + button.setMaxWidth(Double.MAX_VALUE); + button.setOnAction(event -> interactions.requestMutationPreview(action)); + return button; + } + + 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, AssetWorkspaceDetailsUiSupport.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(AssetWorkspaceDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_NAME), preview.asset().assetName())); + box.getChildren().add(AssetWorkspaceDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), + AssetWorkspaceDetailsUiSupport.projectRelativePath(projectReference, preview.asset().assetRoot()))); + if (preview.targetAssetRoot() != null) { + box.getChildren().add(AssetWorkspaceDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_LABEL_TARGET_LOCATION), + AssetWorkspaceDetailsUiSupport.projectRelativePath(projectReference, preview.targetAssetRoot()))); + } + return box; + } + + private Node createMutationChangesContent(List changes, String emptyText) { + if (changes.isEmpty()) { + return AssetWorkspaceDetailsUiSupport.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 AssetWorkspaceDetailsUiSupport.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; + } +}