update shell

This commit is contained in:
bQUARKz 2026-03-11 14:09:18 +00:00
parent f3eb114359
commit 824a39436a
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 396 additions and 13 deletions

View File

@ -4,7 +4,7 @@ import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import p.studio.utilities.i18n.I18n;
import p.studio.window.MainView;
import p.studio.window.StudioRootView;
public class App extends Application {
@ -16,7 +16,7 @@ public class App extends Application {
@Override
public void start(Stage stage) {
var root = new MainView();
var root = new StudioRootView();
var scene = new Scene(root, 1200, 800);
scene.getStylesheets().add(Container.theme().getDefaultTheme());

View File

@ -0,0 +1,6 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
public record StudioProjectCreatedEvent(ProjectReference project) implements StudioEvent {
}

View File

@ -0,0 +1,6 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
public record StudioProjectOpenedEvent(ProjectReference project) implements StudioEvent {
}

View File

@ -0,0 +1,80 @@
package p.studio.projects;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Stream;
public final class ProjectCatalogService {
private final Path projectsRoot;
public ProjectCatalogService(Path projectsRoot) {
this.projectsRoot = Objects.requireNonNull(projectsRoot, "projectsRoot").toAbsolutePath().normalize();
}
public Path projectsRoot() {
return projectsRoot;
}
public List<ProjectReference> listProjects() {
if (!Files.isDirectory(projectsRoot)) {
return List.of();
}
try (Stream<Path> children = Files.list(projectsRoot)) {
return children
.filter(Files::isDirectory)
.map(path -> new ProjectReference(path.getFileName().toString(), path))
.sorted(Comparator.comparing(ProjectReference::name, String.CASE_INSENSITIVE_ORDER))
.toList();
} catch (IOException ioException) {
throw new UncheckedIOException(ioException);
}
}
public ProjectReference openProject(Path projectPath) {
final Path normalized = Objects.requireNonNull(projectPath, "projectPath").toAbsolutePath().normalize();
if (!Files.isDirectory(normalized)) {
throw new IllegalArgumentException("project directory does not exist: " + normalized);
}
return new ProjectReference(normalized.getFileName().toString(), normalized);
}
public ProjectReference createProject(String projectName) {
final String sanitized = sanitizeProjectName(projectName);
if (sanitized.isBlank()) {
throw new IllegalArgumentException("project name must not be blank");
}
final Path projectRoot = projectsRoot.resolve(sanitized).normalize();
if (Files.exists(projectRoot)) {
throw new IllegalArgumentException("project already exists: " + sanitized);
}
try {
Files.createDirectories(projectRoot.resolve(".workspace"));
Files.createDirectories(projectRoot.resolve("src"));
Files.createDirectories(projectRoot.resolve("build"));
Files.createDirectories(projectRoot.resolve("cartridge"));
} catch (IOException ioException) {
throw new UncheckedIOException(ioException);
}
return new ProjectReference(sanitized, projectRoot);
}
private String sanitizeProjectName(String rawName) {
final String normalized = Objects.requireNonNull(rawName, "rawName")
.trim()
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+", "")
.replaceAll("-+$", "");
return normalized;
}
}

View File

@ -0,0 +1,10 @@
package p.studio.projects;
import java.nio.file.Path;
public record ProjectReference(String name, Path rootPath) {
@Override
public String toString() {
return name;
}
}

View File

@ -16,6 +16,14 @@ public enum I18n {
SHELL_ACTIVITY("shell.activity"),
LAUNCHER_TITLE("launcher.title"),
LAUNCHER_SUBTITLE("launcher.subtitle"),
LAUNCHER_EXISTING_PROJECTS("launcher.existingProjects"),
LAUNCHER_OPEN_PROJECT("launcher.openProject"),
LAUNCHER_CREATE_PROJECT("launcher.createProject"),
LAUNCHER_PROJECT_NAME_PROMPT("launcher.projectNamePrompt"),
LAUNCHER_CREATE_BUTTON("launcher.createButton"),
TOOLBAR_PLAY("toolbar.play"),
TOOLBAR_STOP("toolbar.stop"),
TOOLBAR_EXPORT("toolbar.export"),

View File

@ -4,6 +4,7 @@ import javafx.scene.layout.BorderPane;
import p.studio.Container;
import p.studio.controls.shell.*;
import p.studio.events.StudioWorkspaceSelectedEvent;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.PlaceholderWorkspace;
import p.studio.workspaces.WorkspaceHost;
@ -14,17 +15,19 @@ import p.studio.workspaces.editor.EditorWorkspace;
import java.util.List;
public final class MainView extends BorderPane {
private static final WorkspaceHost HOST = new WorkspaceHost();
private final WorkspaceHost host = new WorkspaceHost();
private final ProjectReference projectReference;
public MainView() {
public MainView(ProjectReference projectReference) {
this.projectReference = projectReference;
final var menuBar = new StudioShellMenuBarControl();
final var runSurface = new StudioRunSurfaceControl();
setTop(new StudioShellTopBarControl(menuBar, runSurface));
HOST.register(new EditorWorkspace());
HOST.register(new PlaceholderWorkspace(WorkspaceId.ASSETS, I18n.WORKSPACE_ASSETS, "Assets"));
HOST.register(new BuilderWorkspace());
HOST.register(new PlaceholderWorkspace(WorkspaceId.DEBUG, I18n.WORKSPACE_DEBUG, "Debug"));
host.register(new EditorWorkspace());
host.register(new PlaceholderWorkspace(WorkspaceId.ASSETS, I18n.WORKSPACE_ASSETS, "Assets"));
host.register(new BuilderWorkspace(projectReference));
host.register(new PlaceholderWorkspace(WorkspaceId.DEBUG, I18n.WORKSPACE_DEBUG, "Debug"));
final var workspaceRail = new StudioWorkspaceRailControl<>(
List.of(
@ -35,7 +38,7 @@ public final class MainView extends BorderPane {
),
this::showWorkspace);
setLeft(workspaceRail);
setCenter(HOST);
setCenter(host);
setRight(new StudioRightUtilityPanelControl(
Container.i18n().bind(I18n.SHELL_ACTIVITY),
StudioRightUtilityPanelControl.createPlaceholderContent(Container.i18n().bind(I18n.SHELL_ACTIVITY))));
@ -45,8 +48,12 @@ public final class MainView extends BorderPane {
showWorkspace(WorkspaceId.EDITOR);
}
public ProjectReference projectReference() {
return projectReference;
}
private void showWorkspace(WorkspaceId workspaceId) {
HOST.show(workspaceId);
host.show(workspaceId);
Container.events().publish(new StudioWorkspaceSelectedEvent(workspaceId));
}
}

View File

@ -0,0 +1,132 @@
package p.studio.window;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import p.studio.Container;
import p.studio.projects.ProjectCatalogService;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import java.util.Objects;
import java.util.function.Consumer;
public final class ProjectLauncherView extends BorderPane {
private final ProjectCatalogService projectCatalogService;
private final Consumer<ProjectReference> onOpenProject;
private final Consumer<ProjectReference> onCreateProject;
private final ListView<ProjectReference> projectList = new ListView<>();
private final TextField projectNameField = new TextField();
private final Label feedbackLabel = new Label();
public ProjectLauncherView(
ProjectCatalogService projectCatalogService,
Consumer<ProjectReference> onOpenProject,
Consumer<ProjectReference> onCreateProject) {
this.projectCatalogService = Objects.requireNonNull(projectCatalogService, "projectCatalogService");
this.onOpenProject = Objects.requireNonNull(onOpenProject, "onOpenProject");
this.onCreateProject = Objects.requireNonNull(onCreateProject, "onCreateProject");
getStyleClass().add("studio-project-launcher");
setPadding(new Insets(24));
setCenter(buildContent());
reloadProjects();
}
private VBox buildContent() {
final Label title = new Label();
title.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_TITLE));
title.getStyleClass().add("studio-launcher-title");
final Label subtitle = new Label();
subtitle.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_SUBTITLE));
subtitle.getStyleClass().add("studio-launcher-subtitle");
final Label listTitle = new Label();
listTitle.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_EXISTING_PROJECTS));
listTitle.getStyleClass().add("studio-launcher-section-title");
projectList.getStyleClass().add("studio-project-list");
VBox.setVgrow(projectList, Priority.ALWAYS);
final Button openButton = new Button();
openButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_OPEN_PROJECT));
openButton.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
openButton.setOnAction(ignored -> openSelectedProject());
final HBox openRow = new HBox(openButton);
openRow.setAlignment(Pos.CENTER_LEFT);
final Label createTitle = new Label();
createTitle.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_PROJECT));
createTitle.getStyleClass().add("studio-launcher-section-title");
projectNameField.promptTextProperty().bind(Container.i18n().bind(I18n.LAUNCHER_PROJECT_NAME_PROMPT));
HBox.setHgrow(projectNameField, Priority.ALWAYS);
final Button createButton = new Button();
createButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_BUTTON));
createButton.setOnAction(ignored -> createProject());
final HBox createRow = new HBox(12, projectNameField, createButton);
createRow.setAlignment(Pos.CENTER_LEFT);
feedbackLabel.getStyleClass().add("studio-launcher-feedback");
feedbackLabel.setWrapText(true);
final VBox content = new VBox(16,
title,
subtitle,
listTitle,
projectList,
openRow,
createTitle,
createRow,
feedbackLabel);
content.setMaxWidth(720);
return content;
}
private void reloadProjects() {
projectList.setItems(FXCollections.observableArrayList(projectCatalogService.listProjects()));
if (!projectList.getItems().isEmpty()) {
projectList.getSelectionModel().selectFirst();
}
}
private void openSelectedProject() {
final ProjectReference selected = projectList.getSelectionModel().getSelectedItem();
if (selected == null) {
feedbackLabel.textProperty().unbind();
feedbackLabel.setText("");
return;
}
feedbackLabel.setText("");
onOpenProject.accept(selected);
}
private void createProject() {
try {
final ProjectReference created = projectCatalogService.createProject(projectNameField.getText());
projectNameField.clear();
reloadProjects();
projectList.getSelectionModel().select(created);
feedbackLabel.textProperty().unbind();
feedbackLabel.setText("");
onCreateProject.accept(created);
} catch (IllegalArgumentException illegalArgumentException) {
feedbackLabel.textProperty().unbind();
feedbackLabel.setText(illegalArgumentException.getMessage());
}
}
}

View File

@ -0,0 +1,53 @@
package p.studio.window;
import javafx.scene.layout.StackPane;
import p.studio.Container;
import p.studio.events.StudioProjectCreatedEvent;
import p.studio.events.StudioProjectOpenedEvent;
import p.studio.projects.ProjectCatalogService;
import p.studio.projects.ProjectReference;
import java.nio.file.Path;
public final class StudioRootView extends StackPane {
private final ProjectCatalogService projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot());
public StudioRootView() {
showLauncher();
}
private void showLauncher() {
getChildren().setAll(new ProjectLauncherView(
projectCatalogService,
this::openProject,
this::createProject));
}
private void openProject(ProjectReference projectReference) {
final ProjectReference opened = projectCatalogService.openProject(projectReference.rootPath());
Container.events().publish(new StudioProjectOpenedEvent(opened));
showShell(opened);
}
private void createProject(ProjectReference projectReference) {
Container.events().publish(new StudioProjectCreatedEvent(projectReference));
Container.events().publish(new StudioProjectOpenedEvent(projectReference));
showShell(projectReference);
}
private void showShell(ProjectReference projectReference) {
getChildren().setAll(new MainView(projectReference));
}
private Path resolveDefaultProjectsRoot() {
Path cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
while (cursor != null) {
if (cursor.resolve("settings.gradle.kts").toFile().exists()
&& cursor.resolve("test-projects").toFile().exists()) {
return cursor.resolve("test-projects");
}
cursor = cursor.getParent();
}
return Path.of("test-projects").toAbsolutePath().normalize();
}
}

View File

@ -8,6 +8,7 @@ import javafx.scene.layout.StackPane;
import p.studio.Container;
import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.workspaces.BuilderPipelineService;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.utilities.logs.LogAggregator;
import p.studio.workspaces.Workspace;
@ -18,6 +19,7 @@ public class BuilderWorkspace implements Workspace {
private final TextArea logs = new TextArea();
private final Button buildButton = new Button();
private final Button clearButton = new Button();
private final ProjectReference projectReference;
@Override
public WorkspaceId id() {
@ -34,7 +36,8 @@ public class BuilderWorkspace implements Workspace {
return root;
}
public BuilderWorkspace() {
public BuilderWorkspace(ProjectReference projectReference) {
this.projectReference = projectReference;
final var toolbar = buildToolBar();
root.setTop(toolbar);
@ -53,7 +56,7 @@ public class BuilderWorkspace implements Workspace {
buildButton.setOnAction(e -> {
logs.clear();
final var logAggregator = LogAggregator.with(logs::appendText);
final var config = new BuilderPipelineConfig(false, "../test-projects/main");
final var config = new BuilderPipelineConfig(false, projectReference.rootPath().toString());
BuilderPipelineService.INSTANCE.run(config, logAggregator);
});
@ -64,7 +67,7 @@ public class BuilderWorkspace implements Workspace {
}
private StackPane buildProjectArea() {
final var tmpLabel = new Label("Builder project area (WIP)");
final var tmpLabel = new Label("Project: " + projectReference.name());
return new StackPane(tmpLabel);
}

View File

@ -9,6 +9,13 @@ menu.view=View
menu.help=Help
shell.activity=Activity
launcher.title=Projects
launcher.subtitle=Open an existing project or create a new one to enter the Studio shell.
launcher.existingProjects=Existing Projects
launcher.openProject=Open Project
launcher.createProject=Create New Project
launcher.projectNamePrompt=Project name
launcher.createButton=Create
toolbar.play=Play
toolbar.stop=Stop

View File

@ -71,3 +71,32 @@
-fx-text-fill: #d4d4d4;
-fx-padding: 16;
}
.studio-project-launcher {
-fx-background-color: linear-gradient(to bottom, #191919, #101010);
}
.studio-launcher-title {
-fx-font-size: 28px;
-fx-font-weight: bold;
-fx-text-fill: #f2f2f2;
}
.studio-launcher-subtitle {
-fx-font-size: 14px;
-fx-text-fill: #a6a6a6;
}
.studio-launcher-section-title {
-fx-font-size: 15px;
-fx-font-weight: bold;
-fx-text-fill: #dcdcdc;
}
.studio-project-list {
-fx-pref-height: 320px;
}
.studio-launcher-feedback {
-fx-text-fill: #ff9b7d;
}

View File

@ -0,0 +1,42 @@
package p.studio.projects;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class ProjectCatalogServiceTest {
@TempDir
Path tempDir;
@Test
void listsExistingProjectDirectories() throws Exception {
Files.createDirectories(tempDir.resolve("alpha"));
Files.createDirectories(tempDir.resolve("beta"));
Files.createDirectories(tempDir.resolve("alpha").resolve("src"));
final ProjectCatalogService service = new ProjectCatalogService(tempDir);
assertEquals(
java.util.List.of("alpha", "beta"),
service.listProjects().stream().map(ProjectReference::name).toList());
}
@Test
void createsProjectWithExpectedStructure() {
final ProjectCatalogService service = new ProjectCatalogService(tempDir);
final ProjectReference project = service.createProject("My New Project");
assertEquals("my-new-project", project.name());
assertTrue(Files.isDirectory(project.rootPath()));
assertTrue(Files.isDirectory(project.rootPath().resolve(".workspace")));
assertTrue(Files.isDirectory(project.rootPath().resolve("src")));
assertTrue(Files.isDirectory(project.rootPath().resolve("build")));
assertTrue(Files.isDirectory(project.rootPath().resolve("cartridge")));
}
}