update shell
This commit is contained in:
parent
f3eb114359
commit
824a39436a
@ -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());
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
package p.studio.events;
|
||||
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
public record StudioProjectCreatedEvent(ProjectReference project) implements StudioEvent {
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package p.studio.events;
|
||||
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
public record StudioProjectOpenedEvent(ProjectReference project) implements StudioEvent {
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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")));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user