diff --git a/prometeu-studio/src/main/java/p/studio/controls/banks/StudioDualListView.java b/prometeu-studio/src/main/java/p/studio/controls/banks/StudioDualListView.java index 676bfd46..2633ad19 100644 --- a/prometeu-studio/src/main/java/p/studio/controls/banks/StudioDualListView.java +++ b/prometeu-studio/src/main/java/p/studio/controls/banks/StudioDualListView.java @@ -22,6 +22,8 @@ public abstract class StudioDualListView extends HBox { private final ObservableList rightItems = FXCollections.observableArrayList(); private final ListView leftListView = new ListView<>(leftItems); private final ListView rightListView = new ListView<>(rightItems); + private final Label leftTitleLabel = new Label("Available"); + private final Label rightTitleLabel = new Label("Selected"); private final Button moveToRightButton = new Button(">"); private final Button moveToLeftButton = new Button("<"); private final Button moveUpButton = new Button("Up"); @@ -41,8 +43,8 @@ public abstract class StudioDualListView extends HBox { leftListView.setCellFactory(ignored -> createCell(false)); rightListView.setCellFactory(ignored -> createCell(true)); - final VBox leftColumn = createColumn("Available", leftListView); - final VBox rightColumn = createColumn("Selected", rightListView); + final VBox leftColumn = createColumn(leftTitleLabel, leftListView); + final VBox rightColumn = createColumn(rightTitleLabel, rightListView); final VBox centerActions = new VBox(8, moveToRightButton, moveToLeftButton, moveUpButton, moveDownButton); centerActions.setAlignment(Pos.CENTER); centerActions.getStyleClass().add("studio-dual-list-actions"); @@ -114,14 +116,21 @@ public abstract class StudioDualListView extends HBox { this.onMoveDown = Objects.requireNonNull(onMoveDown, "onMoveDown"); } + public void setLeftTitle(String title) { + leftTitleLabel.setText(Objects.requireNonNull(title, "title")); + } + + public void setRightTitle(String title) { + rightTitleLabel.setText(Objects.requireNonNull(title, "title")); + } + protected abstract String itemText(T item); protected Node itemGraphic(T item) { return null; } - private VBox createColumn(String title, ListView listView) { - final Label titleLabel = new Label(title); + private VBox createColumn(Label titleLabel, ListView listView) { titleLabel.getStyleClass().add("studio-dual-list-title"); final VBox column = new VBox(8, titleLabel, listView); column.getStyleClass().add("studio-dual-list-column"); diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index 1cb34462..b9befc19 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -96,6 +96,7 @@ public enum I18n { ASSETS_BADGE_DIAGNOSTICS("assets.badge.diagnostics"), ASSETS_SECTION_SUMMARY("assets.section.summary"), ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"), + ASSETS_SECTION_BANK_COMPOSITION("assets.section.bankComposition"), ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"), ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"), ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"), @@ -177,6 +178,10 @@ public enum I18n { ASSETS_DETAILS_EMPTY("assets.details.empty"), ASSETS_DETAILS_READY("assets.details.ready"), ASSETS_DETAILS_NO_SELECTION("assets.details.noSelection"), + ASSETS_DETAILS_BANK_COMPOSITION_AVAILABLE("assets.details.bankComposition.available"), + ASSETS_DETAILS_BANK_COMPOSITION_SELECTED("assets.details.bankComposition.selected"), + ASSETS_DETAILS_BANK_COMPOSITION_READONLY_HINT("assets.details.bankComposition.readonlyHint"), + ASSETS_DETAILS_BANK_COMPOSITION_EDITING_HINT("assets.details.bankComposition.editingHint"), ASSETS_DETAILS_CODEC_CONFIGURATION_EMPTY("assets.details.codecConfiguration.empty"), ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"), ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java index 4ec7199e..877f6b36 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java @@ -15,6 +15,7 @@ import p.studio.Container; import p.studio.events.StudioWorkspaceEventBus; import p.studio.projects.ProjectReference; import p.studio.utilities.i18n.I18n; +import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl; import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl; import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl; import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction; @@ -45,6 +46,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware private final ScrollPane detailsScroll = new ScrollPane(); private final AssetDetailsSummaryControl summaryControl; private final AssetDetailsContractControl contractControl; + private final AssetDetailsBankCompositionControl bankCompositionControl; private final VBox actionsContent = new VBox(10); private final ScrollPane actionsScroll = new ScrollPane(); private final VBox actionsSection; @@ -63,6 +65,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus"); this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus); this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus); + this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus); this.actionsSection = createActionsSection(); getStyleClass().add("assets-workspace-pane"); @@ -224,7 +227,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware renderActions(); if (!readyMounted) { readyMounted = true; - detailsContent.getChildren().setAll(primarySectionsRow, contractControl); + detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl); } syncActionsSectionHeight(summaryControl.getHeight()); } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java new file mode 100644 index 00000000..755a371e --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java @@ -0,0 +1,149 @@ +package p.studio.workspaces.assets.details.bank; + +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import p.studio.Container; +import p.studio.controls.banks.StudioAssetCapacityMeter; +import p.studio.controls.banks.StudioAssetCapacitySeverity; +import p.studio.controls.forms.StudioFormActionBar; +import p.studio.controls.forms.StudioFormMode; +import p.studio.controls.forms.StudioFormSession; +import p.studio.controls.lifecycle.StudioControlLifecycle; +import p.studio.controls.lifecycle.StudioControlLifecycleSupport; +import p.studio.events.StudioWorkspaceEventBus; +import p.studio.projects.ProjectReference; +import p.studio.utilities.i18n.I18n; +import p.studio.workspaces.assets.details.AssetDetailsUiSupport; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails; +import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState; +import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent; +import p.studio.workspaces.framework.StudioSubscriptionBag; + +import java.util.Objects; + +public final class AssetDetailsBankCompositionControl extends VBox implements StudioControlLifecycle { + private final StudioWorkspaceEventBus workspaceBus; + private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag(); + private final StudioFormActionBar actionBar = new StudioFormActionBar( + this::beginEdit, + this::applyDraft, + this::resetDraft, + this::cancelEdit); + private final AssetDetailsBankCompositionDualListView dualListView = new AssetDetailsBankCompositionDualListView(); + private final StudioAssetCapacityMeter capacityMeter = new StudioAssetCapacityMeter(); + + private AssetWorkspaceDetailsViewState viewState; + private StudioFormSession formSession; + + public AssetDetailsBankCompositionControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) { + StudioControlLifecycleSupport.install(this, this); + Objects.requireNonNull(projectReference, "projectReference"); + this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus"); + } + + @Override + public void subscribe() { + subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> { + viewState = event.viewState(); + syncFormSession(); + render(); + })); + } + + @Override + public void unsubscribe() { + subscriptions.clear(); + } + + private void syncFormSession() { + if (viewState == null || viewState.selectedAssetDetails() == null) { + formSession = null; + return; + } + + final AssetWorkspaceBankCompositionDetails source = viewState.selectedAssetDetails().bankComposition(); + if (formSession == null) { + formSession = new StudioFormSession<>(source); + return; + } + if (!Objects.equals(formSession.source(), source)) { + formSession.replaceSource(source); + } + } + + private void render() { + if (viewState == null || viewState.selectedAssetDetails() == null) { + getChildren().clear(); + return; + } + if (formSession == null) { + syncFormSession(); + } + if (formSession == null) { + getChildren().clear(); + return; + } + + final boolean editing = formSession.mode() == StudioFormMode.EDITING; + final AssetWorkspaceBankCompositionDetails draft = formSession.draft(); + + dualListView.setLeftItems(draft.availableFiles()); + dualListView.setRightItems(draft.selectedFiles()); + dualListView.setInteractionEnabled(false); + + capacityMeter.setProgress(0.0d); + capacityMeter.setSeverity(StudioAssetCapacitySeverity.GREEN); + capacityMeter.setLabelText(draft.measuredBankSizeBytes() + " bytes"); + capacityMeter.setHintText(editing + ? Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_EDITING_HINT) + : Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_READONLY_HINT)); + + final HBox body = new HBox(16, dualListView, capacityMeter); + body.getStyleClass().add("assets-details-bank-composition-body"); + HBox.setHgrow(dualListView, Priority.ALWAYS); + dualListView.setMaxWidth(Double.MAX_VALUE); + + final VBox content = new VBox(12, body); + actionBar.updateState(formSession.mode(), formSession.isDirty()); + content.getChildren().add(actionBar); + + final VBox section = AssetDetailsUiSupport.createSection( + Container.i18n().text(I18n.ASSETS_SECTION_BANK_COMPOSITION), + content); + section.getStyleClass().add("assets-details-bank-composition-section"); + getChildren().setAll(section); + } + + private void beginEdit() { + if (formSession == null) { + return; + } + formSession.beginEdit(); + render(); + } + + private void applyDraft() { + if (formSession == null) { + return; + } + formSession.apply(); + render(); + } + + private void resetDraft() { + if (formSession == null) { + return; + } + formSession.resetDraft(); + render(); + } + + private void cancelEdit() { + if (formSession == null) { + return; + } + formSession.cancelEdit(); + render(); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDualListView.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDualListView.java index 6e85e5c5..ee0eb61e 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDualListView.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDualListView.java @@ -1,9 +1,16 @@ package p.studio.workspaces.assets.details.bank; import p.studio.controls.banks.StudioDualListView; +import p.studio.Container; +import p.studio.utilities.i18n.I18n; import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; public final class AssetDetailsBankCompositionDualListView extends StudioDualListView { + public AssetDetailsBankCompositionDualListView() { + setLeftTitle(Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_AVAILABLE)); + setRightTitle(Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_SELECTED)); + } + @Override protected String itemText(AssetWorkspaceBankCompositionFile item) { if (item.displayName().equals(item.path())) { diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index 4a332468..c6a278da 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -86,6 +86,7 @@ assets.badge.preload=Preload assets.badge.diagnostics=Diagnostics assets.section.summary=Summary assets.section.runtimeContract=Runtime Contract +assets.section.bankComposition=Bank Composition assets.subsection.codecConfiguration=Codec Configuration assets.subsection.metadata=Metadata assets.section.inputsPreview=Inputs / Preview @@ -168,6 +169,10 @@ assets.details.loading=Waiting for asset data... assets.details.empty=Create or add assets to this project to start using the Assets workspace. assets.details.ready=Selected asset: {0}\nState: {1}\nRoot: {2} assets.details.noSelection=Select an asset from the navigator once assets are available. +assets.details.bankComposition.available=Available +assets.details.bankComposition.selected=Selected +assets.details.bankComposition.readonlyHint=Current bank composition state is shown read-only until editing begins. +assets.details.bankComposition.editingHint=Bank composition editing shell is active. Behavior wiring lands in the next slice. assets.details.codecConfiguration.empty=This codec does not expose configuration fields yet. assets.details.metadata.empty=This asset does not expose metadata fields yet. assets.addWizard.title=Add Asset