update shell

This commit is contained in:
bQUARKz 2026-03-11 15:45:20 +00:00
parent 0e6a79020e
commit 19396aec9e
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 358 additions and 2 deletions

View File

@ -79,6 +79,7 @@ The exact event catalog may evolve incrementally.
Illustrative baseline categories include:
- project lifecycle events;
- project loading lifecycle events;
- workspace selection events;
- run and build intent events;
- activity publication events;
@ -86,6 +87,20 @@ Illustrative baseline categories include:
- diagnostics update events;
- asset selection events.
For project bootstrap and splash/loading integration, the Studio event catalog should reserve typed events for:
- project loading started;
- project loading progress;
- project loading completed;
- project loading failed.
Project loading progress events should carry at least:
- the project identity,
- a typed loading phase,
- a user-facing status message,
- and either determinate progress or an explicit indeterminate marker.
## Theme and i18n Compatibility
Shared Studio UI foundations must preserve:

View File

@ -0,0 +1,22 @@
package p.studio;
import java.util.Optional;
public final class StudioAppInfo {
private static final String VERSION_PROPERTY = "prometeu.studio.version";
private static final String FALLBACK_VERSION = "dev";
private StudioAppInfo() {
}
public static String version() {
final String explicit = System.getProperty(VERSION_PROPERTY);
if (explicit != null && !explicit.isBlank()) {
return explicit.trim();
}
return Optional.ofNullable(StudioAppInfo.class.getPackage().getImplementationVersion())
.filter(value -> !value.isBlank())
.orElse(FALLBACK_VERSION);
}
}

View File

@ -0,0 +1,11 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public record StudioProjectLoadingCompletedEvent(ProjectReference project) implements StudioEvent {
public StudioProjectLoadingCompletedEvent {
Objects.requireNonNull(project, "project");
}
}

View File

@ -0,0 +1,17 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public record StudioProjectLoadingFailedEvent(
ProjectReference project,
StudioProjectLoadingPhase phase,
String message) implements StudioEvent {
public StudioProjectLoadingFailedEvent {
Objects.requireNonNull(project, "project");
Objects.requireNonNull(phase, "phase");
Objects.requireNonNull(message, "message");
}
}

View File

@ -0,0 +1,9 @@
package p.studio.events;
public enum StudioProjectLoadingPhase {
RESOLVING_PROJECT,
RESTORING_WINDOW_STATE,
RESTORING_WORKSPACES,
INITIALIZING_SERVICES,
READY
}

View File

@ -0,0 +1,22 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public record StudioProjectLoadingProgressEvent(
ProjectReference project,
StudioProjectLoadingPhase phase,
String message,
double progress,
boolean indeterminate) implements StudioEvent {
public StudioProjectLoadingProgressEvent {
Objects.requireNonNull(project, "project");
Objects.requireNonNull(phase, "phase");
Objects.requireNonNull(message, "message");
if (!indeterminate && (progress < 0.0d || progress > 1.0d)) {
throw new IllegalArgumentException("progress must be between 0.0 and 1.0 when determinate");
}
}
}

View File

@ -0,0 +1,11 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public record StudioProjectLoadingStartedEvent(ProjectReference project) implements StudioEvent {
public StudioProjectLoadingStartedEvent {
Objects.requireNonNull(project, "project");
}
}

View File

@ -5,6 +5,16 @@ import lombok.Getter;
public enum I18n {
APP_TITLE("app.title"),
APP_PROJECT_TITLE("app.projectTitle"),
SHIELD_LOADING_PROJECT("shield.loadingProject"),
SHIELD_VERSION("shield.version"),
SHIELD_STATUS_INDEXING("shield.status.indexing"),
SHIELD_STATUS_RESTORING("shield.status.restoring"),
SHIELD_PHASE_INDEXING("shield.phase.indexing"),
SHIELD_PHASE_PREPARING("shield.phase.preparing"),
SHIELD_PHASE_RESTORING("shield.phase.restoring"),
SHIELD_TIP_SHORTCUTS("shield.tip.shortcuts"),
SHIELD_TIP_WORKSPACES("shield.tip.workspaces"),
SHIELD_TIP_ACTIVITY("shield.tip.activity"),
MENU_FILE("menu.file"),
MENU_FILE_NEWPROJECT("menu.file.newProject"),

View File

@ -0,0 +1,112 @@
package p.studio.window;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import p.studio.Container;
import p.studio.StudioAppInfo;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import java.util.Objects;
public final class ProjectLoadingShieldView extends VBox {
public ProjectLoadingShieldView(ProjectReference projectReference) {
Objects.requireNonNull(projectReference, "projectReference");
getStyleClass().add("studio-project-loading-shield");
setPadding(new Insets(28));
setSpacing(14);
setAlignment(Pos.CENTER_LEFT);
final Label markLabel = new Label("P");
markLabel.getStyleClass().add("studio-project-loading-mark");
final Label appTitle = new Label(Container.i18n().text(I18n.APP_TITLE));
appTitle.getStyleClass().add("studio-project-loading-title");
final Label appVersion = new Label(Container.i18n().format(I18n.SHIELD_VERSION, StudioAppInfo.version()));
appVersion.getStyleClass().add("studio-project-loading-version");
final VBox brandBlock = new VBox(4, appTitle, appVersion);
final HBox brandRow = new HBox(14, buildMark(markLabel), brandBlock);
brandRow.setAlignment(Pos.CENTER_LEFT);
final Label projectLabel = new Label(projectReference.name());
projectLabel.getStyleClass().add("studio-project-loading-project");
final Label statusLabel = new Label(Container.i18n().format(I18n.SHIELD_LOADING_PROJECT, projectReference.name()));
statusLabel.getStyleClass().add("studio-project-loading-status");
final ProgressBar progressBar = new ProgressBar(0.18);
progressBar.getStyleClass().add("studio-project-loading-progress");
progressBar.setMaxWidth(Double.MAX_VALUE);
final HBox phaseRow = new HBox(
8,
phasePill(Container.i18n().text(I18n.SHIELD_PHASE_INDEXING), true),
phasePill(Container.i18n().text(I18n.SHIELD_PHASE_PREPARING), false),
phasePill(Container.i18n().text(I18n.SHIELD_PHASE_RESTORING), false));
phaseRow.setAlignment(Pos.CENTER_LEFT);
final Label tipLabel = new Label(Container.i18n().text(I18n.SHIELD_TIP_SHORTCUTS));
tipLabel.getStyleClass().add("studio-project-loading-tip");
tipLabel.setWrapText(true);
final Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, ignored -> {
statusLabel.setText(Container.i18n().format(I18n.SHIELD_LOADING_PROJECT, projectReference.name()));
tipLabel.setText(Container.i18n().text(I18n.SHIELD_TIP_SHORTCUTS));
progressBar.setProgress(0.24);
}),
new KeyFrame(Duration.millis(500), ignored -> {
statusLabel.setText(Container.i18n().text(I18n.SHIELD_STATUS_INDEXING));
tipLabel.setText(Container.i18n().text(I18n.SHIELD_TIP_WORKSPACES));
progressBar.setProgress(0.58);
}),
new KeyFrame(Duration.millis(1000), ignored -> {
statusLabel.setText(Container.i18n().text(I18n.SHIELD_STATUS_RESTORING));
tipLabel.setText(Container.i18n().text(I18n.SHIELD_TIP_ACTIVITY));
progressBar.setProgress(0.86);
}));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
sceneProperty().addListener((ignored, oldScene, newScene) -> {
if (newScene == null) {
timeline.stop();
}
});
getChildren().addAll(
brandRow,
projectLabel,
statusLabel,
progressBar,
phaseRow,
tipLabel);
}
private StackPane buildMark(Label markLabel) {
final StackPane mark = new StackPane(markLabel);
mark.getStyleClass().add("studio-project-loading-mark-shell");
return mark;
}
private Region phasePill(String text, boolean active) {
final Label label = new Label(text);
label.getStyleClass().add("studio-project-loading-phase");
if (active) {
label.getStyleClass().add("studio-project-loading-phase-active");
}
return label;
}
}

View File

@ -1,7 +1,10 @@
package p.studio.window;
import javafx.animation.PauseTransition;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import p.studio.Container;
import p.studio.events.StudioProjectCreatedEvent;
import p.studio.events.StudioProjectOpenedEvent;
@ -16,8 +19,11 @@ import java.util.Objects;
public final class StudioWindowCoordinator {
private static final double LAUNCHER_WIDTH = 760;
private static final double LAUNCHER_HEIGHT = 750;
private static final double PROJECT_LOADING_WIDTH = 460;
private static final double PROJECT_LOADING_HEIGHT = 220;
private static final double PROJECT_WIDTH = 1280;
private static final double PROJECT_HEIGHT = 840;
private static final Duration PROJECT_OPEN_DELAY = Duration.seconds(1.5);
private final Stage launcherStage;
private final ProjectCatalogService projectCatalogService;
@ -83,6 +89,7 @@ public final class StudioWindowCoordinator {
}
private void openProjectWindow(ProjectReference projectReference) {
final Stage loadingStage = createProjectLoadingStage(projectReference);
final Stage projectStage = new Stage();
final Scene scene = new Scene(new MainView(projectReference), PROJECT_WIDTH, PROJECT_HEIGHT);
scene.getStylesheets().add(Container.theme().getDefaultTheme());
@ -98,8 +105,30 @@ public final class StudioWindowCoordinator {
});
launcherStage.hide();
loadingStage.show();
loadingStage.centerOnScreen();
loadingStage.toFront();
final PauseTransition delay = new PauseTransition(PROJECT_OPEN_DELAY);
delay.setOnFinished(ignored -> {
loadingStage.close();
projectStage.show();
projectStage.toFront();
});
delay.play();
}
private Stage createProjectLoadingStage(ProjectReference projectReference) {
final Stage loadingStage = new Stage(StageStyle.UNDECORATED);
loadingStage.initOwner(launcherStage);
loadingStage.setResizable(false);
final Scene scene = new Scene(
new ProjectLoadingShieldView(projectReference),
PROJECT_LOADING_WIDTH,
PROJECT_LOADING_HEIGHT);
scene.getStylesheets().add(Container.theme().getDefaultTheme());
loadingStage.setScene(scene);
return loadingStage;
}
private Path resolveDefaultProjectsRoot() {

View File

@ -1,5 +1,15 @@
app.title=Prometeu Studio
app.projectTitle=Prometeu Studio - {0}
shield.loadingProject=Loading project {0}...
shield.version=Version {0}
shield.status.indexing=Indexing sources and restoring services...
shield.status.restoring=Restoring workspace surfaces and editor state...
shield.phase.indexing=Indexing
shield.phase.preparing=Preparing UI
shield.phase.restoring=Restoring State
shield.tip.shortcuts=Tip: the Studio shell will grow around workspace-first shortcuts and actions.
shield.tip.workspaces=Tip: workspaces stay fixed on the left rail for fast switching.
shield.tip.activity=Tip: global activity lives on the right; local logs stay in each workspace.
menu.file=File
menu.file.newProject=New Project

View File

@ -138,3 +138,70 @@
.studio-launcher-feedback {
-fx-text-fill: #ffb08f;
}
.studio-project-loading-shield {
-fx-background-color: linear-gradient(to bottom right, #102236, #0c1016);
}
.studio-project-loading-mark-shell {
-fx-background-color: linear-gradient(to bottom right, #4aa3ff, #1f5e97);
-fx-background-radius: 16;
-fx-min-width: 46;
-fx-min-height: 46;
-fx-pref-width: 46;
-fx-pref-height: 46;
-fx-alignment: center;
}
.studio-project-loading-mark {
-fx-text-fill: #ffffff;
-fx-font-size: 24px;
-fx-font-weight: bold;
}
.studio-project-loading-title {
-fx-text-fill: #f7fbff;
-fx-font-size: 28px;
-fx-font-weight: bold;
}
.studio-project-loading-version {
-fx-text-fill: #9fc4ea;
-fx-font-size: 13px;
}
.studio-project-loading-project {
-fx-text-fill: #ffffff;
-fx-font-size: 20px;
-fx-font-weight: bold;
-fx-padding: 12 0 0 0;
}
.studio-project-loading-status {
-fx-text-fill: #d9e7f5;
-fx-font-size: 14px;
}
.studio-project-loading-progress {
-fx-accent: #5cb6ff;
-fx-pref-height: 10px;
-fx-max-width: 380px;
}
.studio-project-loading-phase {
-fx-background-color: rgba(255,255,255,0.08);
-fx-background-radius: 999;
-fx-text-fill: #d6e3f2;
-fx-font-size: 11px;
-fx-padding: 5 10 5 10;
}
.studio-project-loading-phase-active {
-fx-background-color: #2c6ba5;
-fx-text-fill: #ffffff;
}
.studio-project-loading-tip {
-fx-text-fill: #b9cae0;
-fx-font-size: 12px;
}

View File

@ -1,8 +1,10 @@
package p.studio.events;
import org.junit.jupiter.api.Test;
import p.studio.projects.ProjectReference;
import p.studio.workspaces.WorkspaceId;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@ -41,6 +43,25 @@ final class StudioWorkspaceEventBusTest {
assertEquals(List.of("global-only"), globalReceived);
}
@Test
void projectLoadingProgressEventsCanFlowThroughTheStudioBus() {
final StudioEventBus globalBus = new StudioEventBus();
final StudioWorkspaceEventBus workspaceBus = new StudioWorkspaceEventBus(WorkspaceId.ASSETS, globalBus);
final List<StudioProjectLoadingPhase> globalReceived = new CopyOnWriteArrayList<>();
final ProjectReference project = new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/main"));
globalBus.subscribe(StudioProjectLoadingProgressEvent.class, event -> globalReceived.add(event.phase()));
workspaceBus.publish(new StudioProjectLoadingProgressEvent(
project,
StudioProjectLoadingPhase.INITIALIZING_SERVICES,
"Initializing services",
0.5d,
false));
assertEquals(List.of(StudioProjectLoadingPhase.INITIALIZING_SERVICES), globalReceived);
}
private record TestStudioEvent(String name) implements StudioEvent {
}
}