From bd753a8f1ecbd61d4d1a3a59eac2550323293785 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 19 Mar 2026 00:43:39 +0000 Subject: [PATCH] implements PR-10b bank composition base components --- .../banks/StudioAssetCapacityMeter.java | 74 ++++++++ .../banks/StudioAssetCapacitySeverity.java | 7 + .../controls/banks/StudioDualListView.java | 162 ++++++++++++++++++ ...setDetailsBankCompositionDualListView.java | 14 ++ 4 files changed, 257 insertions(+) create mode 100644 prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacityMeter.java create mode 100644 prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacitySeverity.java create mode 100644 prometeu-studio/src/main/java/p/studio/controls/banks/StudioDualListView.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDualListView.java diff --git a/prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacityMeter.java b/prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacityMeter.java new file mode 100644 index 00000000..4bf9abfc --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacityMeter.java @@ -0,0 +1,74 @@ +package p.studio.controls.banks; + +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +public final class StudioAssetCapacityMeter extends VBox { + private static final String BASE_SEVERITY_CLASS = "studio-asset-capacity-meter-fill-"; + + private final AnchorPane track = new AnchorPane(); + private final Region fill = new Region(); + private final Label label = new Label(); + private final Label hint = new Label(); + + private double progress; + private StudioAssetCapacitySeverity severity = StudioAssetCapacitySeverity.GREEN; + + public StudioAssetCapacityMeter() { + setAlignment(Pos.TOP_CENTER); + setSpacing(8); + getStyleClass().add("studio-asset-capacity-meter"); + + track.getStyleClass().add("studio-asset-capacity-meter-track"); + track.setMinWidth(28); + track.setPrefWidth(28); + track.setPrefHeight(180); + VBox.setVgrow(track, Priority.ALWAYS); + + fill.getStyleClass().addAll("studio-asset-capacity-meter-fill", BASE_SEVERITY_CLASS + severity.name().toLowerCase()); + fill.setManaged(false); + AnchorPane.setLeftAnchor(fill, 0.0d); + AnchorPane.setRightAnchor(fill, 0.0d); + AnchorPane.setBottomAnchor(fill, 0.0d); + track.getChildren().add(fill); + + label.getStyleClass().add("studio-asset-capacity-meter-label"); + hint.getStyleClass().add("studio-asset-capacity-meter-hint"); + hint.setWrapText(true); + + track.heightProperty().addListener((ignored, oldValue, newValue) -> refreshFillHeight(newValue.doubleValue())); + + getChildren().setAll(track, label, hint); + refreshFillHeight(track.getPrefHeight()); + } + + public void setProgress(double progress) { + this.progress = Math.max(0.0d, Math.min(1.0d, progress)); + refreshFillHeight(track.getHeight() <= 0.0d ? track.getPrefHeight() : track.getHeight()); + } + + public void setSeverity(StudioAssetCapacitySeverity severity) { + this.severity = severity == null ? StudioAssetCapacitySeverity.GREEN : severity; + fill.getStyleClass().removeIf(styleClass -> styleClass.startsWith(BASE_SEVERITY_CLASS)); + fill.getStyleClass().add(BASE_SEVERITY_CLASS + this.severity.name().toLowerCase()); + } + + public void setLabelText(String text) { + label.setText(text == null ? "" : text); + } + + public void setHintText(String text) { + final String safeText = text == null ? "" : text; + hint.setText(safeText); + hint.setVisible(!safeText.isBlank()); + hint.setManaged(!safeText.isBlank()); + } + + private void refreshFillHeight(double trackHeight) { + fill.setPrefHeight(Math.max(0.0d, trackHeight) * progress); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacitySeverity.java b/prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacitySeverity.java new file mode 100644 index 00000000..9659d5ce --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/controls/banks/StudioAssetCapacitySeverity.java @@ -0,0 +1,7 @@ +package p.studio.controls.banks; + +public enum StudioAssetCapacitySeverity { + GREEN, + ORANGE, + RED +} 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 new file mode 100644 index 00000000..676bfd46 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/controls/banks/StudioDualListView.java @@ -0,0 +1,162 @@ +package p.studio.controls.banks; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public abstract class StudioDualListView extends HBox { + private final ObservableList leftItems = FXCollections.observableArrayList(); + private final ObservableList rightItems = FXCollections.observableArrayList(); + private final ListView leftListView = new ListView<>(leftItems); + private final ListView rightListView = new ListView<>(rightItems); + private final Button moveToRightButton = new Button(">"); + private final Button moveToLeftButton = new Button("<"); + private final Button moveUpButton = new Button("Up"); + private final Button moveDownButton = new Button("Down"); + + private Consumer> onMoveToRight = ignored -> { }; + private Consumer> onMoveToLeft = ignored -> { }; + private IntConsumer onMoveUp = ignored -> { }; + private IntConsumer onMoveDown = ignored -> { }; + + protected StudioDualListView() { + setSpacing(12); + getStyleClass().add("studio-dual-list-view"); + + leftListView.getStyleClass().add("studio-dual-list-left"); + rightListView.getStyleClass().add("studio-dual-list-right"); + leftListView.setCellFactory(ignored -> createCell(false)); + rightListView.setCellFactory(ignored -> createCell(true)); + + final VBox leftColumn = createColumn("Available", leftListView); + final VBox rightColumn = createColumn("Selected", rightListView); + final VBox centerActions = new VBox(8, moveToRightButton, moveToLeftButton, moveUpButton, moveDownButton); + centerActions.setAlignment(Pos.CENTER); + centerActions.getStyleClass().add("studio-dual-list-actions"); + + HBox.setHgrow(leftColumn, Priority.ALWAYS); + HBox.setHgrow(rightColumn, Priority.ALWAYS); + getChildren().setAll(leftColumn, centerActions, rightColumn); + + moveToRightButton.getStyleClass().addAll("studio-button", "studio-button-secondary"); + moveToLeftButton.getStyleClass().addAll("studio-button", "studio-button-secondary"); + moveUpButton.getStyleClass().addAll("studio-button", "studio-button-secondary"); + moveDownButton.getStyleClass().addAll("studio-button", "studio-button-secondary"); + + moveToRightButton.setOnAction(ignored -> onMoveToRight.accept(List.copyOf(leftListView.getSelectionModel().getSelectedItems()))); + moveToLeftButton.setOnAction(ignored -> onMoveToLeft.accept(List.copyOf(rightListView.getSelectionModel().getSelectedItems()))); + moveUpButton.setOnAction(ignored -> { + final int selectedIndex = rightListView.getSelectionModel().getSelectedIndex(); + if (selectedIndex >= 0) { + onMoveUp.accept(selectedIndex); + } + }); + moveDownButton.setOnAction(ignored -> { + final int selectedIndex = rightListView.getSelectionModel().getSelectedIndex(); + if (selectedIndex >= 0) { + onMoveDown.accept(selectedIndex); + } + }); + + updateActionState(); + leftListView.getSelectionModel().selectedItemProperty().addListener((ignored, oldValue, newValue) -> updateActionState()); + rightListView.getSelectionModel().selectedItemProperty().addListener((ignored, oldValue, newValue) -> updateActionState()); + } + + public void setLeftItems(List items) { + leftItems.setAll(Objects.requireNonNull(items, "items")); + leftListView.refresh(); + updateActionState(); + } + + public void setRightItems(List items) { + rightItems.setAll(Objects.requireNonNull(items, "items")); + rightListView.refresh(); + updateActionState(); + } + + public void setInteractionEnabled(boolean enabled) { + leftListView.setDisable(!enabled); + rightListView.setDisable(!enabled); + moveToRightButton.setDisable(!enabled || leftListView.getSelectionModel().isEmpty()); + moveToLeftButton.setDisable(!enabled || rightListView.getSelectionModel().isEmpty()); + moveUpButton.setDisable(!enabled || rightListView.getSelectionModel().getSelectedIndex() <= 0); + moveDownButton.setDisable(!enabled || rightListView.getSelectionModel().getSelectedIndex() < 0 + || rightListView.getSelectionModel().getSelectedIndex() >= rightItems.size() - 1); + } + + public void setOnMoveToRight(Consumer> onMoveToRight) { + this.onMoveToRight = Objects.requireNonNull(onMoveToRight, "onMoveToRight"); + } + + public void setOnMoveToLeft(Consumer> onMoveToLeft) { + this.onMoveToLeft = Objects.requireNonNull(onMoveToLeft, "onMoveToLeft"); + } + + public void setOnMoveUp(IntConsumer onMoveUp) { + this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp"); + } + + public void setOnMoveDown(IntConsumer onMoveDown) { + this.onMoveDown = Objects.requireNonNull(onMoveDown, "onMoveDown"); + } + + 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); + titleLabel.getStyleClass().add("studio-dual-list-title"); + final VBox column = new VBox(8, titleLabel, listView); + column.getStyleClass().add("studio-dual-list-column"); + VBox.setVgrow(listView, Priority.ALWAYS); + return column; + } + + private ListCell createCell(boolean indexed) { + return new ListCell<>() { + @Override + protected void updateItem(T item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + final String text = indexed + ? (getIndex() + 1) + ". " + itemText(item) + : itemText(item); + setText(text); + setGraphic(itemGraphic(item)); + } + }; + } + + private void updateActionState() { + final boolean leftSelected = !leftListView.getSelectionModel().isEmpty(); + final int rightSelectedIndex = rightListView.getSelectionModel().getSelectedIndex(); + moveToRightButton.setDisable(!leftSelected || leftListView.isDisabled()); + moveToLeftButton.setDisable(rightSelectedIndex < 0 || rightListView.isDisabled()); + moveUpButton.setDisable(rightSelectedIndex <= 0 || rightListView.isDisabled()); + moveDownButton.setDisable( + rightSelectedIndex < 0 + || rightSelectedIndex >= rightItems.size() - 1 + || rightListView.isDisabled()); + } +} 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 new file mode 100644 index 00000000..6e85e5c5 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDualListView.java @@ -0,0 +1,14 @@ +package p.studio.workspaces.assets.details.bank; + +import p.studio.controls.banks.StudioDualListView; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +public final class AssetDetailsBankCompositionDualListView extends StudioDualListView { + @Override + protected String itemText(AssetWorkspaceBankCompositionFile item) { + if (item.displayName().equals(item.path())) { + return item.displayName(); + } + return item.displayName() + " [" + item.path() + "]"; + } +}