update shell
This commit is contained in:
parent
824a39436a
commit
0e6a79020e
@ -0,0 +1,60 @@
|
|||||||
|
# PR-04a - Project Launcher Window Lifecycle
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
The current project launcher already exists as a view before the main shell.
|
||||||
|
|
||||||
|
This PR evolves that flow into a separate launcher window so project selection and project editing no longer compete for the same application stage.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Move the project launcher into its own window lifecycle and define how it coordinates with the main Studio project window.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [`../specs/1. Studio Shell and Workspace Layout Specification.md`](../specs/1.%20Studio%20Shell%20and%20Workspace%20Layout%20Specification.md)
|
||||||
|
- [`../specs/2. Studio UI Foundations Specification.md`](../specs/2.%20Studio%20UI%20Foundations%20Specification.md)
|
||||||
|
- `PR-03 - Project Launcher and Project Entry Shell`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- introduce a dedicated launcher `Stage`
|
||||||
|
- open the main Studio shell in a separate project window
|
||||||
|
- define launcher visibility behavior when a project window opens
|
||||||
|
- define launcher restoration behavior when a project window closes
|
||||||
|
- keep event publication aligned with the Studio event model
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- redesigning the launcher content model
|
||||||
|
- implementing the new project wizard itself
|
||||||
|
- supporting multiple simultaneous project windows unless needed by the chosen lifecycle
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Extract launcher-stage creation from the current single-window flow.
|
||||||
|
2. Open the main Studio shell in its own project window after project selection or creation.
|
||||||
|
3. Hide the launcher window when a project window opens.
|
||||||
|
4. Restore or reshow the launcher window when the project window closes.
|
||||||
|
5. Keep the transition between launcher and project window intentional and easy to follow.
|
||||||
|
6. Preserve theme and i18n behavior in both windows.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- the launcher runs in its own window
|
||||||
|
- the Studio shell runs in a separate project window
|
||||||
|
- opening or creating a project transitions from launcher to project window cleanly
|
||||||
|
- closing the project window returns the user to the launcher window
|
||||||
|
- the flow remains aligned with current project-open and project-created events
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- compile validation of `prometeu-studio`
|
||||||
|
- smoke validation of launcher window opening
|
||||||
|
- smoke validation of launcher hide/show behavior when a project window opens or closes
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `prometeu-studio` application entry and stage lifecycle
|
||||||
|
- launcher and shell composition
|
||||||
|
- project-open window behavior
|
||||||
64
docs/studio/pull-requests/PR-04b-new-project-wizard.md
Normal file
64
docs/studio/pull-requests/PR-04b-new-project-wizard.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# PR-04b - New Project Wizard
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
The current project launcher can create a project directly from an inline text field.
|
||||||
|
|
||||||
|
This PR replaces that minimal path with a dedicated wizard so project creation becomes a guided flow rather than a single-form action.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Introduce a new project wizard that guides project creation before opening the Studio shell.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [`../specs/1. Studio Shell and Workspace Layout Specification.md`](../specs/1.%20Studio%20Shell%20and%20Workspace%20Layout%20Specification.md)
|
||||||
|
- [`../specs/2. Studio UI Foundations Specification.md`](../specs/2.%20Studio%20UI%20Foundations%20Specification.md)
|
||||||
|
- `PR-03 - Project Launcher and Project Entry Shell`
|
||||||
|
- `PR-04a - Project Launcher Window Lifecycle`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- replace inline project creation with a wizard flow
|
||||||
|
- support at least:
|
||||||
|
- project name
|
||||||
|
- project location
|
||||||
|
- final confirmation
|
||||||
|
- create the project through the existing project service layer
|
||||||
|
- open the project after successful creation
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- full template marketplace or template system
|
||||||
|
- advanced preset configuration
|
||||||
|
- remote project provisioning
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Introduce a `New Project Wizard` surface from the launcher.
|
||||||
|
2. Split project creation into explicit steps.
|
||||||
|
3. Validate project name and location before final confirmation.
|
||||||
|
4. Reuse the existing project creation service instead of duplicating creation logic in the UI.
|
||||||
|
5. Open the created project after the wizard completes successfully.
|
||||||
|
6. Keep the wizard aligned with Studio theming and i18n rules.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `Create New Project` opens a wizard instead of creating inline immediately
|
||||||
|
- the wizard supports name, location, and confirmation steps
|
||||||
|
- invalid project data is blocked before creation
|
||||||
|
- successful completion creates the project and opens it
|
||||||
|
- the flow remains consistent with the launcher/project-entry model
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- compile validation of `prometeu-studio`
|
||||||
|
- smoke validation of wizard opening and step progression
|
||||||
|
- test or smoke coverage for successful project creation
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- launcher UI
|
||||||
|
- project creation flow
|
||||||
|
- project service integration
|
||||||
|
- entry transition from launcher to shell
|
||||||
@ -41,3 +41,5 @@ The current Studio execution queue is:
|
|||||||
1. [`PR-01-studio-event-bus-foundation.md`](./PR-01-studio-event-bus-foundation.md)
|
1. [`PR-01-studio-event-bus-foundation.md`](./PR-01-studio-event-bus-foundation.md)
|
||||||
2. [`PR-02-studio-shell-components-wave-1.md`](./PR-02-studio-shell-components-wave-1.md)
|
2. [`PR-02-studio-shell-components-wave-1.md`](./PR-02-studio-shell-components-wave-1.md)
|
||||||
3. [`PR-03-project-launcher-and-project-entry-shell.md`](./PR-03-project-launcher-and-project-entry-shell.md)
|
3. [`PR-03-project-launcher-and-project-entry-shell.md`](./PR-03-project-launcher-and-project-entry-shell.md)
|
||||||
|
4. [`PR-04a-project-launcher-window-lifecycle.md`](./PR-04a-project-launcher-window-lifecycle.md)
|
||||||
|
5. [`PR-04b-new-project-wizard.md`](./PR-04b-new-project-wizard.md)
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package p.studio.compiler;
|
|||||||
import p.studio.compiler.models.FrontendSpec;
|
import p.studio.compiler.models.FrontendSpec;
|
||||||
import p.studio.utilities.structures.ReadOnlySet;
|
import p.studio.utilities.structures.ReadOnlySet;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class PBSDefinitions {
|
public class PBSDefinitions {
|
||||||
public static final FrontendSpec PBS = FrontendSpec
|
public static final FrontendSpec PBS = FrontendSpec
|
||||||
.builder()
|
.builder()
|
||||||
@ -10,5 +12,6 @@ public class PBSDefinitions {
|
|||||||
.allowedExtensions(ReadOnlySet.from("pbs", "barrel"))
|
.allowedExtensions(ReadOnlySet.from("pbs", "barrel"))
|
||||||
.sourceRoots(ReadOnlySet.from("src"))
|
.sourceRoots(ReadOnlySet.from("src"))
|
||||||
.entryPointCallableName("frame")
|
.entryPointCallableName("frame")
|
||||||
|
.stdlibVersions(List.of(FrontendSpec.Stdlib.asDefault(1)))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import lombok.Builder;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import p.studio.utilities.structures.ReadOnlySet;
|
import p.studio.utilities.structures.ReadOnlySet;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
@Getter
|
@Getter
|
||||||
public class FrontendSpec {
|
public class FrontendSpec {
|
||||||
@ -13,8 +15,34 @@ public class FrontendSpec {
|
|||||||
private final boolean caseSensitive;
|
private final boolean caseSensitive;
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private final String entryPointCallableName = "main";
|
private final String entryPointCallableName = "main";
|
||||||
|
@Builder.Default
|
||||||
|
private final List<Stdlib> stdlibVersions = List.of();
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("FrontendSpec(language=%s, entryPoint=%s)", languageId, entryPointCallableName);
|
return String.format("FrontendSpec(language=%s, entryPoint=%s)", languageId, entryPointCallableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum StdlibType {
|
||||||
|
DEFAULT,
|
||||||
|
STABLE,
|
||||||
|
EXPERIMENTAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Stdlib(int version, StdlibType type) {
|
||||||
|
public static Stdlib asDefault(final int version) {
|
||||||
|
return new Stdlib(version, StdlibType.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stdlib asStable(final int version) {
|
||||||
|
return new Stdlib(version, StdlibType.STABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stdlib of(final int version) {
|
||||||
|
return asStable(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stdlib asExperimental(final int version) {
|
||||||
|
return new Stdlib(version, StdlibType.EXPERIMENTAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import p.studio.compiler.services.FrontendPhaseService;
|
|||||||
import p.studio.compiler.services.PBSFrontendPhaseService;
|
import p.studio.compiler.services.PBSFrontendPhaseService;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@ -28,4 +29,10 @@ public class FrontendRegistryService {
|
|||||||
public static Optional<FrontendPhaseService> getFrontendPhaseService(final String languageId) {
|
public static Optional<FrontendPhaseService> getFrontendPhaseService(final String languageId) {
|
||||||
return Optional.ofNullable(FRONTEND_PHASE_SERVICES.get(languageId));
|
return Optional.ofNullable(FRONTEND_PHASE_SERVICES.get(languageId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<FrontendSpec> listFrontendSpecs() {
|
||||||
|
return FRONTEND_SPECS.values().stream()
|
||||||
|
.sorted((left, right) -> left.getLanguageId().compareToIgnoreCase(right.getLanguageId()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,7 @@ dependencies {
|
|||||||
implementation(project(":prometeu-infra"))
|
implementation(project(":prometeu-infra"))
|
||||||
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
||||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||||
|
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
|
||||||
implementation(libs.javafx.controls)
|
implementation(libs.javafx.controls)
|
||||||
implementation(libs.javafx.fxml)
|
implementation(libs.javafx.fxml)
|
||||||
implementation(libs.richtextfx)
|
implementation(libs.richtextfx)
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
package p.studio;
|
package p.studio;
|
||||||
|
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.window.StudioWindowCoordinator;
|
||||||
import p.studio.window.StudioRootView;
|
|
||||||
|
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
|
|
||||||
@ -16,13 +14,7 @@ public class App extends Application {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) {
|
public void start(Stage stage) {
|
||||||
var root = new StudioRootView();
|
new StudioWindowCoordinator(stage).showLauncher();
|
||||||
var scene = new Scene(root, 1200, 800);
|
|
||||||
|
|
||||||
scene.getStylesheets().add(Container.theme().getDefaultTheme());
|
|
||||||
stage.titleProperty().bind(Container.i18n().bind(I18n.APP_TITLE));
|
|
||||||
stage.setScene(scene);
|
|
||||||
stage.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@ -0,0 +1,98 @@
|
|||||||
|
package p.studio.projects;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class KnownProjectsService {
|
||||||
|
private final Path storageFile;
|
||||||
|
private final ProjectCatalogService projectCatalogService;
|
||||||
|
|
||||||
|
public KnownProjectsService(Path storageFile, ProjectCatalogService projectCatalogService) {
|
||||||
|
this.storageFile = Objects.requireNonNull(storageFile, "storageFile").toAbsolutePath().normalize();
|
||||||
|
this.projectCatalogService = Objects.requireNonNull(projectCatalogService, "projectCatalogService");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ProjectReference> listKnownProjects() {
|
||||||
|
final boolean storageExists = Files.isRegularFile(storageFile);
|
||||||
|
final Set<Path> knownPaths = readStoredPaths();
|
||||||
|
if (!storageExists && knownPaths.isEmpty()) {
|
||||||
|
final List<ProjectReference> discovered = projectCatalogService.listProjects();
|
||||||
|
writeStoredPaths(discovered.stream().map(ProjectReference::rootPath).toList());
|
||||||
|
return discovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ProjectReference> knownProjects = new ArrayList<>();
|
||||||
|
boolean mutated = false;
|
||||||
|
for (Path knownPath : knownPaths) {
|
||||||
|
if (!Files.isDirectory(knownPath)) {
|
||||||
|
mutated = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
knownProjects.add(projectCatalogService.openProject(knownPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
knownProjects.sort(Comparator.comparing(ProjectReference::name, String.CASE_INSENSITIVE_ORDER));
|
||||||
|
if (mutated) {
|
||||||
|
writeStoredPaths(knownProjects.stream().map(ProjectReference::rootPath).toList());
|
||||||
|
}
|
||||||
|
return List.copyOf(knownProjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remember(ProjectReference projectReference) {
|
||||||
|
final Set<Path> knownPaths = readStoredPaths();
|
||||||
|
knownPaths.add(normalize(projectReference.rootPath()));
|
||||||
|
writeStoredPaths(knownPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forget(ProjectReference projectReference) {
|
||||||
|
final Set<Path> knownPaths = readStoredPaths();
|
||||||
|
knownPaths.remove(normalize(projectReference.rootPath()));
|
||||||
|
writeStoredPaths(knownPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Path> readStoredPaths() {
|
||||||
|
if (!Files.isRegularFile(storageFile)) {
|
||||||
|
return new LinkedHashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final LinkedHashSet<Path> paths = new LinkedHashSet<>();
|
||||||
|
for (String line : Files.readAllLines(storageFile, StandardCharsets.UTF_8)) {
|
||||||
|
final String trimmed = line.trim();
|
||||||
|
if (trimmed.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
paths.add(normalize(Path.of(trimmed)));
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeStoredPaths(Iterable<Path> paths) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(storageFile.getParent());
|
||||||
|
final List<String> lines = new ArrayList<>();
|
||||||
|
for (Path path : paths) {
|
||||||
|
lines.add(normalize(path).toString());
|
||||||
|
}
|
||||||
|
Files.write(storageFile, lines, StandardCharsets.UTF_8);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path normalize(Path path) {
|
||||||
|
return Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package p.studio.projects;
|
package p.studio.projects;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -11,6 +14,8 @@ import java.util.Objects;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class ProjectCatalogService {
|
public final class ProjectCatalogService {
|
||||||
|
private static final String MANIFEST_FILE_NAME = "prometeu.json";
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private final Path projectsRoot;
|
private final Path projectsRoot;
|
||||||
|
|
||||||
public ProjectCatalogService(Path projectsRoot) {
|
public ProjectCatalogService(Path projectsRoot) {
|
||||||
@ -28,8 +33,8 @@ public final class ProjectCatalogService {
|
|||||||
|
|
||||||
try (Stream<Path> children = Files.list(projectsRoot)) {
|
try (Stream<Path> children = Files.list(projectsRoot)) {
|
||||||
return children
|
return children
|
||||||
.filter(Files::isDirectory)
|
.filter(this::isValidProjectRoot)
|
||||||
.map(path -> new ProjectReference(path.getFileName().toString(), path))
|
.map(this::buildReference)
|
||||||
.sorted(Comparator.comparing(ProjectReference::name, String.CASE_INSENSITIVE_ORDER))
|
.sorted(Comparator.comparing(ProjectReference::name, String.CASE_INSENSITIVE_ORDER))
|
||||||
.toList();
|
.toList();
|
||||||
} catch (IOException ioException) {
|
} catch (IOException ioException) {
|
||||||
@ -39,33 +44,146 @@ public final class ProjectCatalogService {
|
|||||||
|
|
||||||
public ProjectReference openProject(Path projectPath) {
|
public ProjectReference openProject(Path projectPath) {
|
||||||
final Path normalized = Objects.requireNonNull(projectPath, "projectPath").toAbsolutePath().normalize();
|
final Path normalized = Objects.requireNonNull(projectPath, "projectPath").toAbsolutePath().normalize();
|
||||||
if (!Files.isDirectory(normalized)) {
|
return openProjectManifest(manifestPath(normalized));
|
||||||
throw new IllegalArgumentException("project directory does not exist: " + normalized);
|
|
||||||
}
|
}
|
||||||
return new ProjectReference(normalized.getFileName().toString(), normalized);
|
|
||||||
|
public ProjectReference openProjectManifest(Path manifestPath) {
|
||||||
|
final Path normalizedManifest = Objects.requireNonNull(manifestPath, "manifestPath")
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
if (!normalizedManifest.getFileName().toString().equals(MANIFEST_FILE_NAME)) {
|
||||||
|
throw new IllegalArgumentException("expected " + MANIFEST_FILE_NAME + ": " + normalizedManifest);
|
||||||
|
}
|
||||||
|
if (!Files.isRegularFile(normalizedManifest)) {
|
||||||
|
throw new IllegalArgumentException("project manifest not found: expected " + normalizedManifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path projectRoot = normalizedManifest.getParent();
|
||||||
|
if (projectRoot == null || !Files.isDirectory(projectRoot)) {
|
||||||
|
throw new IllegalArgumentException("project root does not exist: " + normalizedManifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildReference(projectRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProjectReference createProject(String projectName) {
|
public ProjectReference createProject(String projectName) {
|
||||||
final String sanitized = sanitizeProjectName(projectName);
|
return createProject(new ProjectCreationRequest(projectName, projectsRoot, "pbs", 1, "src"));
|
||||||
if (sanitized.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("project name must not be blank");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Path projectRoot = projectsRoot.resolve(sanitized).normalize();
|
public ProjectReference createProject(String projectName, Path parentLocation) {
|
||||||
|
return createProject(new ProjectCreationRequest(projectName, parentLocation, "pbs", 1, "src"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectReference createProject(ProjectCreationRequest request) {
|
||||||
|
final String displayName = Objects.requireNonNull(request.projectName(), "request.projectName").trim();
|
||||||
|
final String sanitized = sanitizeProjectName(displayName);
|
||||||
|
if (displayName.isBlank() || sanitized.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("project name must not be blank");
|
||||||
|
}
|
||||||
|
final String languageId = Objects.requireNonNull(request.languageId(), "request.languageId").trim();
|
||||||
|
if (languageId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("project language must not be blank");
|
||||||
|
}
|
||||||
|
if (request.stdlib() <= 0) {
|
||||||
|
throw new IllegalArgumentException("project stdlib major must be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path sourceRoot = normalizeSourceRoot(request.sourceRoot());
|
||||||
|
final Path normalizedParent = Objects.requireNonNull(request.parentLocation(), "request.parentLocation")
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
if (Files.exists(normalizedParent) && !Files.isDirectory(normalizedParent)) {
|
||||||
|
throw new IllegalArgumentException("project location is not a directory: " + normalizedParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path projectRoot = normalizedParent.resolve(sanitized).normalize();
|
||||||
if (Files.exists(projectRoot)) {
|
if (Files.exists(projectRoot)) {
|
||||||
throw new IllegalArgumentException("project already exists: " + sanitized);
|
throw new IllegalArgumentException("project already exists: " + sanitized);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Files.createDirectories(normalizedParent);
|
||||||
Files.createDirectories(projectRoot.resolve(".workspace"));
|
Files.createDirectories(projectRoot.resolve(".workspace"));
|
||||||
Files.createDirectories(projectRoot.resolve("src"));
|
Files.createDirectories(projectRoot.resolve(sourceRoot));
|
||||||
|
Files.createDirectories(projectRoot.resolve("assets"));
|
||||||
Files.createDirectories(projectRoot.resolve("build"));
|
Files.createDirectories(projectRoot.resolve("build"));
|
||||||
Files.createDirectories(projectRoot.resolve("cartridge"));
|
Files.createDirectories(projectRoot.resolve("cartridge"));
|
||||||
|
Files.writeString(manifestPath(projectRoot), defaultManifest(displayName, languageId, request.stdlib()));
|
||||||
} catch (IOException ioException) {
|
} catch (IOException ioException) {
|
||||||
throw new UncheckedIOException(ioException);
|
throw new UncheckedIOException(ioException);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProjectReference(sanitized, projectRoot);
|
return openProject(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectReference buildReference(Path projectRoot) {
|
||||||
|
final Path normalizedRoot = projectRoot.toAbsolutePath().normalize();
|
||||||
|
final Path manifestPath = manifestPath(normalizedRoot);
|
||||||
|
final ProjectManifestSummary manifest;
|
||||||
|
try {
|
||||||
|
manifest = MAPPER.readValue(manifestPath.toFile(), ProjectManifestSummary.class);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.name() == null || manifest.name().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("project manifest missing name: " + manifestPath);
|
||||||
|
}
|
||||||
|
if (manifest.version() == null || manifest.version().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("project manifest missing version: " + manifestPath);
|
||||||
|
}
|
||||||
|
if (manifest.language() == null || manifest.language().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("project manifest missing language: " + manifestPath);
|
||||||
|
}
|
||||||
|
final int stdlibMajor;
|
||||||
|
try {
|
||||||
|
stdlibMajor = Integer.parseInt(Objects.requireNonNull(manifest.stdlib(), "manifest.stdlib").trim());
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
throw new IllegalArgumentException("project manifest invalid stdlib: " + manifestPath, runtimeException);
|
||||||
|
}
|
||||||
|
return new ProjectReference(
|
||||||
|
manifest.name().trim(),
|
||||||
|
manifest.version().trim(),
|
||||||
|
manifest.language().trim(),
|
||||||
|
stdlibMajor,
|
||||||
|
normalizedRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidProjectRoot(Path path) {
|
||||||
|
return Files.isDirectory(path) && Files.isRegularFile(manifestPath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path manifestPath(Path projectRoot) {
|
||||||
|
return projectRoot.resolve(MANIFEST_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path normalizeSourceRoot(String sourceRoot) {
|
||||||
|
final String raw = Objects.requireNonNull(sourceRoot, "sourceRoot").trim();
|
||||||
|
if (raw.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("project source root must not be blank");
|
||||||
|
}
|
||||||
|
final Path normalized = Path.of(raw).normalize();
|
||||||
|
if (normalized.isAbsolute() || normalized.startsWith("..")) {
|
||||||
|
throw new IllegalArgumentException("project source root must be a relative project path");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultManifest(String projectName, String languageId, int stdlibMajor) {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"name": "%s",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "%s",
|
||||||
|
"stdlib": "%d",
|
||||||
|
"dependencies": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(projectName, languageId, stdlibMajor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
private record ProjectManifestSummary(String name, String version, String language, String stdlib) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String sanitizeProjectName(String rawName) {
|
private String sanitizeProjectName(String rawName) {
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
package p.studio.projects;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public record ProjectCreationRequest(
|
||||||
|
String projectName,
|
||||||
|
Path parentLocation,
|
||||||
|
String languageId,
|
||||||
|
int stdlib,
|
||||||
|
String sourceRoot) {
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package p.studio.projects;
|
||||||
|
|
||||||
|
import p.studio.compiler.FrontendRegistryService;
|
||||||
|
import p.studio.compiler.models.FrontendSpec;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class ProjectLanguageCatalogService {
|
||||||
|
public List<ProjectLanguageTemplate> listLanguageTemplates() {
|
||||||
|
return FrontendRegistryService.listFrontendSpecs().stream()
|
||||||
|
.map(this::toTemplate)
|
||||||
|
.sorted(Comparator.comparing(ProjectLanguageTemplate::languageId, String.CASE_INSENSITIVE_ORDER))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLanguageTemplate getDefaultTemplate() {
|
||||||
|
return listLanguageTemplates().stream()
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("no frontend languages are registered"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectLanguageTemplate toTemplate(FrontendSpec frontendSpec) {
|
||||||
|
final List<String> sourceRoots = frontendSpec.getSourceRoots().stream()
|
||||||
|
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||||
|
.toList();
|
||||||
|
final List<ProjectLanguageTemplate.StdlibOption> stdlibOptions = frontendSpec.getStdlibVersions().stream()
|
||||||
|
.map(stdlib -> new ProjectLanguageTemplate.StdlibOption(stdlib.version(), stdlib.type()))
|
||||||
|
.sorted(Comparator.comparingInt(ProjectLanguageTemplate.StdlibOption::version))
|
||||||
|
.toList();
|
||||||
|
final ProjectLanguageTemplate.StdlibOption defaultStdlib = stdlibOptions.stream()
|
||||||
|
.filter(option -> option.type() == FrontendSpec.StdlibType.DEFAULT)
|
||||||
|
.findFirst()
|
||||||
|
.or(() -> stdlibOptions.stream().findFirst())
|
||||||
|
.orElse(new ProjectLanguageTemplate.StdlibOption(1, FrontendSpec.StdlibType.DEFAULT));
|
||||||
|
final String defaultSourceRoot = sourceRoots.stream()
|
||||||
|
.findFirst()
|
||||||
|
.orElse("src");
|
||||||
|
return new ProjectLanguageTemplate(
|
||||||
|
Objects.requireNonNull(frontendSpec.getLanguageId(), "frontendSpec.languageId"),
|
||||||
|
stdlibOptions,
|
||||||
|
defaultStdlib,
|
||||||
|
sourceRoots,
|
||||||
|
defaultSourceRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package p.studio.projects;
|
||||||
|
|
||||||
|
import p.studio.compiler.models.FrontendSpec;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record ProjectLanguageTemplate(
|
||||||
|
String languageId,
|
||||||
|
List<StdlibOption> stdlibOptions,
|
||||||
|
StdlibOption defaultStdlib,
|
||||||
|
List<String> sourceRoots,
|
||||||
|
String defaultSourceRoot) {
|
||||||
|
|
||||||
|
public record StdlibOption(int version, FrontendSpec.StdlibType type) {
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return switch (type) {
|
||||||
|
case DEFAULT -> version + " (default)";
|
||||||
|
case STABLE -> version + " (stable)";
|
||||||
|
case EXPERIMENTAL -> version + " (experimental)";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,12 @@ package p.studio.projects;
|
|||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public record ProjectReference(String name, Path rootPath) {
|
public record ProjectReference(
|
||||||
|
String name,
|
||||||
|
String version,
|
||||||
|
String languageId,
|
||||||
|
int stdlibVersion,
|
||||||
|
Path rootPath) {
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import lombok.Getter;
|
|||||||
|
|
||||||
public enum I18n {
|
public enum I18n {
|
||||||
APP_TITLE("app.title"),
|
APP_TITLE("app.title"),
|
||||||
|
APP_PROJECT_TITLE("app.projectTitle"),
|
||||||
|
|
||||||
MENU_FILE("menu.file"),
|
MENU_FILE("menu.file"),
|
||||||
MENU_FILE_NEWPROJECT("menu.file.newProject"),
|
MENU_FILE_NEWPROJECT("menu.file.newProject"),
|
||||||
@ -20,10 +21,41 @@ public enum I18n {
|
|||||||
LAUNCHER_SUBTITLE("launcher.subtitle"),
|
LAUNCHER_SUBTITLE("launcher.subtitle"),
|
||||||
LAUNCHER_EXISTING_PROJECTS("launcher.existingProjects"),
|
LAUNCHER_EXISTING_PROJECTS("launcher.existingProjects"),
|
||||||
LAUNCHER_OPEN_PROJECT("launcher.openProject"),
|
LAUNCHER_OPEN_PROJECT("launcher.openProject"),
|
||||||
|
LAUNCHER_ADD_PROJECT("launcher.addProject"),
|
||||||
|
LAUNCHER_FORGET_PROJECT("launcher.forgetProject"),
|
||||||
LAUNCHER_CREATE_PROJECT("launcher.createProject"),
|
LAUNCHER_CREATE_PROJECT("launcher.createProject"),
|
||||||
LAUNCHER_PROJECT_NAME_PROMPT("launcher.projectNamePrompt"),
|
LAUNCHER_PROJECT_NAME_PROMPT("launcher.projectNamePrompt"),
|
||||||
LAUNCHER_CREATE_BUTTON("launcher.createButton"),
|
LAUNCHER_CREATE_BUTTON("launcher.createButton"),
|
||||||
|
|
||||||
|
WIZARD_TITLE("wizard.title"),
|
||||||
|
WIZARD_BACK("wizard.back"),
|
||||||
|
WIZARD_NEXT("wizard.next"),
|
||||||
|
WIZARD_CREATE("wizard.create"),
|
||||||
|
WIZARD_CANCEL("wizard.cancel"),
|
||||||
|
WIZARD_BROWSE("wizard.browse"),
|
||||||
|
WIZARD_STEP_NAME_TITLE("wizard.step.name.title"),
|
||||||
|
WIZARD_STEP_NAME_DESCRIPTION("wizard.step.name.description"),
|
||||||
|
WIZARD_STEP_LANGUAGE_TITLE("wizard.step.language.title"),
|
||||||
|
WIZARD_STEP_LANGUAGE_DESCRIPTION("wizard.step.language.description"),
|
||||||
|
WIZARD_STEP_LOCATION_TITLE("wizard.step.location.title"),
|
||||||
|
WIZARD_STEP_LOCATION_DESCRIPTION("wizard.step.location.description"),
|
||||||
|
WIZARD_STEP_CONFIRM_TITLE("wizard.step.confirm.title"),
|
||||||
|
WIZARD_STEP_CONFIRM_DESCRIPTION("wizard.step.confirm.description"),
|
||||||
|
WIZARD_LANGUAGE_LABEL("wizard.language.label"),
|
||||||
|
WIZARD_STDLIB_LABEL("wizard.stdlib.label"),
|
||||||
|
WIZARD_SOURCE_ROOT_LABEL("wizard.sourceRoot.label"),
|
||||||
|
WIZARD_CONFIRM_NAME("wizard.confirm.name"),
|
||||||
|
WIZARD_CONFIRM_LANGUAGE("wizard.confirm.language"),
|
||||||
|
WIZARD_CONFIRM_STDLIB("wizard.confirm.stdlib"),
|
||||||
|
WIZARD_CONFIRM_SOURCE_ROOT("wizard.confirm.sourceRoot"),
|
||||||
|
WIZARD_CONFIRM_LOCATION("wizard.confirm.location"),
|
||||||
|
WIZARD_CONFIRM_ROOT("wizard.confirm.root"),
|
||||||
|
WIZARD_ERROR_NAME_REQUIRED("wizard.error.nameRequired"),
|
||||||
|
WIZARD_ERROR_LANGUAGE_REQUIRED("wizard.error.languageRequired"),
|
||||||
|
WIZARD_ERROR_STDLIB_REQUIRED("wizard.error.stdlibRequired"),
|
||||||
|
WIZARD_ERROR_SOURCE_ROOT_REQUIRED("wizard.error.sourceRootRequired"),
|
||||||
|
WIZARD_ERROR_LOCATION_REQUIRED("wizard.error.locationRequired"),
|
||||||
|
|
||||||
TOOLBAR_PLAY("toolbar.play"),
|
TOOLBAR_PLAY("toolbar.play"),
|
||||||
TOOLBAR_STOP("toolbar.stop"),
|
TOOLBAR_STOP("toolbar.stop"),
|
||||||
TOOLBAR_EXPORT("toolbar.export"),
|
TOOLBAR_EXPORT("toolbar.export"),
|
||||||
|
|||||||
@ -0,0 +1,339 @@
|
|||||||
|
package p.studio.window;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.DirectoryChooser;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
import p.studio.Container;
|
||||||
|
import p.studio.projects.ProjectCatalogService;
|
||||||
|
import p.studio.projects.ProjectCreationRequest;
|
||||||
|
import p.studio.projects.ProjectLanguageCatalogService;
|
||||||
|
import p.studio.projects.ProjectLanguageTemplate;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.utilities.i18n.I18n;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public final class NewProjectWizard {
|
||||||
|
private final ProjectCatalogService projectCatalogService;
|
||||||
|
private final ProjectLanguageCatalogService projectLanguageCatalogService;
|
||||||
|
private final Stage stage;
|
||||||
|
private final AtomicReference<ProjectReference> result = new AtomicReference<>();
|
||||||
|
private final Label stepTitle = new Label();
|
||||||
|
private final Label stepDescription = new Label();
|
||||||
|
private final VBox stepBody = new VBox(12);
|
||||||
|
private final Label feedbackLabel = new Label();
|
||||||
|
private final Button backButton = new Button();
|
||||||
|
private final Button nextButton = new Button();
|
||||||
|
private final Button createButton = new Button();
|
||||||
|
private final TextField projectNameField = new TextField();
|
||||||
|
private final TextField locationField = new TextField();
|
||||||
|
private final ComboBox<String> languageCombo = new ComboBox<>();
|
||||||
|
private final ComboBox<ProjectLanguageTemplate.StdlibOption> stdlibCombo = new ComboBox<>();
|
||||||
|
private final ComboBox<String> sourceRootCombo = new ComboBox<>();
|
||||||
|
private final Map<String, ProjectLanguageTemplate> languageTemplatesById = new LinkedHashMap<>();
|
||||||
|
private int stepIndex = 0;
|
||||||
|
|
||||||
|
private NewProjectWizard(Window owner, ProjectCatalogService projectCatalogService) {
|
||||||
|
this.projectCatalogService = Objects.requireNonNull(projectCatalogService, "projectCatalogService");
|
||||||
|
this.projectLanguageCatalogService = new ProjectLanguageCatalogService();
|
||||||
|
this.stage = new Stage();
|
||||||
|
stage.initOwner(owner);
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.setTitle(Container.i18n().text(I18n.WIZARD_TITLE));
|
||||||
|
stage.setScene(new Scene(buildRoot(), 700, 420));
|
||||||
|
stage.getScene().getStylesheets().add(Container.theme().getDefaultTheme());
|
||||||
|
|
||||||
|
projectNameField.promptTextProperty().bind(Container.i18n().bind(I18n.LAUNCHER_PROJECT_NAME_PROMPT));
|
||||||
|
locationField.setText(projectCatalogService.projectsRoot().toString());
|
||||||
|
initializeLanguageTemplates();
|
||||||
|
|
||||||
|
renderStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<ProjectReference> showAndWait(Window owner, ProjectCatalogService projectCatalogService) {
|
||||||
|
final NewProjectWizard wizard = new NewProjectWizard(owner, projectCatalogService);
|
||||||
|
wizard.stage.showAndWait();
|
||||||
|
return Optional.ofNullable(wizard.result.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildRoot() {
|
||||||
|
stepTitle.getStyleClass().add("studio-launcher-section-title");
|
||||||
|
stepDescription.getStyleClass().add("studio-launcher-subtitle");
|
||||||
|
feedbackLabel.getStyleClass().add("studio-launcher-feedback");
|
||||||
|
feedbackLabel.setWrapText(true);
|
||||||
|
|
||||||
|
backButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BACK));
|
||||||
|
backButton.setOnAction(ignored -> goBack());
|
||||||
|
|
||||||
|
nextButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_NEXT));
|
||||||
|
nextButton.setOnAction(ignored -> goNext());
|
||||||
|
|
||||||
|
createButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CREATE));
|
||||||
|
createButton.setOnAction(ignored -> finishCreate());
|
||||||
|
|
||||||
|
final Button cancelButton = new Button();
|
||||||
|
cancelButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CANCEL));
|
||||||
|
cancelButton.setOnAction(ignored -> stage.close());
|
||||||
|
|
||||||
|
final HBox actions = new HBox(12, backButton, nextButton, createButton, cancelButton);
|
||||||
|
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
final VBox root = new VBox(16, stepTitle, stepDescription, stepBody, feedbackLabel, actions);
|
||||||
|
root.setPadding(new Insets(24));
|
||||||
|
VBox.setVgrow(stepBody, Priority.ALWAYS);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderStep() {
|
||||||
|
feedbackLabel.setText("");
|
||||||
|
backButton.setDisable(stepIndex == 0);
|
||||||
|
nextButton.setVisible(stepIndex < 3);
|
||||||
|
nextButton.setManaged(stepIndex < 3);
|
||||||
|
createButton.setVisible(stepIndex == 3);
|
||||||
|
createButton.setManaged(stepIndex == 3);
|
||||||
|
|
||||||
|
switch (stepIndex) {
|
||||||
|
case 0 -> renderNameStep();
|
||||||
|
case 1 -> renderLanguageStep();
|
||||||
|
case 2 -> renderLocationStep();
|
||||||
|
case 3 -> renderConfirmStep();
|
||||||
|
default -> throw new IllegalStateException("unknown wizard step: " + stepIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderNameStep() {
|
||||||
|
stepTitle.textProperty().unbind();
|
||||||
|
stepDescription.textProperty().unbind();
|
||||||
|
stepTitle.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_NAME_TITLE));
|
||||||
|
stepDescription.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_NAME_DESCRIPTION));
|
||||||
|
stepBody.getChildren().setAll(projectNameField);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderLanguageStep() {
|
||||||
|
stepTitle.textProperty().unbind();
|
||||||
|
stepDescription.textProperty().unbind();
|
||||||
|
stepTitle.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_LANGUAGE_TITLE));
|
||||||
|
stepDescription.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_LANGUAGE_DESCRIPTION));
|
||||||
|
|
||||||
|
final Label languageLabel = new Label(Container.i18n().text(I18n.WIZARD_LANGUAGE_LABEL));
|
||||||
|
final Label stdlibLabel = new Label(Container.i18n().text(I18n.WIZARD_STDLIB_LABEL));
|
||||||
|
final Label sourceRootLabel = new Label(Container.i18n().text(I18n.WIZARD_SOURCE_ROOT_LABEL));
|
||||||
|
|
||||||
|
languageCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
stdlibCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
sourceRootCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
final VBox languageBox = new VBox(6, languageLabel, languageCombo);
|
||||||
|
final VBox stdlibBox = new VBox(6, stdlibLabel, stdlibCombo);
|
||||||
|
final VBox sourceRootBox = new VBox(6, sourceRootLabel, sourceRootCombo);
|
||||||
|
stepBody.getChildren().setAll(languageBox, stdlibBox, sourceRootBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderLocationStep() {
|
||||||
|
stepTitle.textProperty().unbind();
|
||||||
|
stepDescription.textProperty().unbind();
|
||||||
|
stepTitle.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_LOCATION_TITLE));
|
||||||
|
stepDescription.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_LOCATION_DESCRIPTION));
|
||||||
|
|
||||||
|
final Button browseButton = new Button();
|
||||||
|
browseButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BROWSE));
|
||||||
|
browseButton.setOnAction(ignored -> browseForLocation());
|
||||||
|
|
||||||
|
final HBox row = new HBox(12, locationField, browseButton);
|
||||||
|
HBox.setHgrow(locationField, Priority.ALWAYS);
|
||||||
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
stepBody.getChildren().setAll(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderConfirmStep() {
|
||||||
|
stepTitle.textProperty().unbind();
|
||||||
|
stepDescription.textProperty().unbind();
|
||||||
|
stepTitle.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_CONFIRM_TITLE));
|
||||||
|
stepDescription.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_CONFIRM_DESCRIPTION));
|
||||||
|
|
||||||
|
final Label projectName = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_NAME, sanitizedName()));
|
||||||
|
final Label projectLanguage = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_LANGUAGE, selectedLanguageId()));
|
||||||
|
final Label projectStdlib = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_STDLIB, selectedStdlibLabel()));
|
||||||
|
final Label projectSourceRoot = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_SOURCE_ROOT, selectedSourceRoot()));
|
||||||
|
final Label projectLocation = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_LOCATION, locationField.getText().trim()));
|
||||||
|
final Label projectRoot = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_ROOT, resolvedProjectRoot().toString()));
|
||||||
|
|
||||||
|
stepBody.getChildren().setAll(
|
||||||
|
projectName,
|
||||||
|
projectLanguage,
|
||||||
|
projectStdlib,
|
||||||
|
projectSourceRoot,
|
||||||
|
projectLocation,
|
||||||
|
projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void goBack() {
|
||||||
|
if (stepIndex == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stepIndex -= 1;
|
||||||
|
renderStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void goNext() {
|
||||||
|
if (!validateCurrentStep()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stepIndex += 1;
|
||||||
|
renderStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateCurrentStep() {
|
||||||
|
return switch (stepIndex) {
|
||||||
|
case 0 -> validateName();
|
||||||
|
case 1 -> validateLanguageSettings();
|
||||||
|
case 2 -> validateLocation();
|
||||||
|
default -> true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateName() {
|
||||||
|
if (sanitizedName().isBlank()) {
|
||||||
|
feedbackLabel.setText(Container.i18n().text(I18n.WIZARD_ERROR_NAME_REQUIRED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateLanguageSettings() {
|
||||||
|
if (selectedLanguageId().isBlank()) {
|
||||||
|
feedbackLabel.setText(Container.i18n().text(I18n.WIZARD_ERROR_LANGUAGE_REQUIRED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (selectedSourceRoot().isBlank()) {
|
||||||
|
feedbackLabel.setText(Container.i18n().text(I18n.WIZARD_ERROR_SOURCE_ROOT_REQUIRED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (selectedStdlib() == null) {
|
||||||
|
feedbackLabel.setText(Container.i18n().text(I18n.WIZARD_ERROR_STDLIB_REQUIRED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateLocation() {
|
||||||
|
final String rawLocation = locationField.getText().trim();
|
||||||
|
if (rawLocation.isBlank()) {
|
||||||
|
feedbackLabel.setText(Container.i18n().text(I18n.WIZARD_ERROR_LOCATION_REQUIRED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Path.of(rawLocation);
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
feedbackLabel.setText(runtimeException.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishCreate() {
|
||||||
|
if (!validateName() || !validateLanguageSettings() || !validateLocation()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result.set(projectCatalogService.createProject(new ProjectCreationRequest(
|
||||||
|
projectNameField.getText(),
|
||||||
|
Path.of(locationField.getText().trim()),
|
||||||
|
selectedLanguageId(),
|
||||||
|
selectedStdlib().version(),
|
||||||
|
selectedSourceRoot())));
|
||||||
|
stage.close();
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
feedbackLabel.setText(exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void browseForLocation() {
|
||||||
|
final DirectoryChooser chooser = new DirectoryChooser();
|
||||||
|
chooser.setTitle(Container.i18n().text(I18n.WIZARD_BROWSE));
|
||||||
|
|
||||||
|
final String currentText = locationField.getText().trim();
|
||||||
|
if (!currentText.isBlank()) {
|
||||||
|
final File currentDirectory = Path.of(currentText).toFile();
|
||||||
|
if (currentDirectory.isDirectory()) {
|
||||||
|
chooser.setInitialDirectory(currentDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final File selected = chooser.showDialog(stage);
|
||||||
|
if (selected != null) {
|
||||||
|
locationField.setText(selected.toPath().toAbsolutePath().normalize().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitizedName() {
|
||||||
|
return projectNameField.getText() == null ? "" : projectNameField.getText().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeLanguageTemplates() {
|
||||||
|
final List<ProjectLanguageTemplate> templates = projectLanguageCatalogService.listLanguageTemplates();
|
||||||
|
for (ProjectLanguageTemplate template : templates) {
|
||||||
|
languageTemplatesById.put(template.languageId(), template);
|
||||||
|
}
|
||||||
|
languageCombo.getItems().setAll(languageTemplatesById.keySet());
|
||||||
|
languageCombo.getSelectionModel().selectedItemProperty().addListener((ignored, oldValue, newValue) -> applyLanguageTemplate(newValue));
|
||||||
|
if (!templates.isEmpty()) {
|
||||||
|
languageCombo.getSelectionModel().select(projectLanguageCatalogService.getDefaultTemplate().languageId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyLanguageTemplate(String languageId) {
|
||||||
|
final ProjectLanguageTemplate template = languageTemplatesById.get(languageId);
|
||||||
|
if (template == null) {
|
||||||
|
stdlibCombo.getItems().clear();
|
||||||
|
stdlibCombo.getSelectionModel().clearSelection();
|
||||||
|
sourceRootCombo.getItems().clear();
|
||||||
|
sourceRootCombo.getSelectionModel().clearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stdlibCombo.getItems().setAll(template.stdlibOptions());
|
||||||
|
stdlibCombo.getSelectionModel().select(template.defaultStdlib());
|
||||||
|
sourceRootCombo.getItems().setAll(template.sourceRoots());
|
||||||
|
sourceRootCombo.getSelectionModel().select(template.defaultSourceRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectedLanguageId() {
|
||||||
|
return languageCombo.getValue() == null ? "" : languageCombo.getValue().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectedSourceRoot() {
|
||||||
|
return sourceRootCombo.getValue() == null ? "" : sourceRootCombo.getValue().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectLanguageTemplate.StdlibOption selectedStdlib() {
|
||||||
|
return stdlibCombo.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectedStdlibLabel() {
|
||||||
|
final ProjectLanguageTemplate.StdlibOption selectedStdlib = selectedStdlib();
|
||||||
|
return selectedStdlib == null ? "" : selectedStdlib.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolvedProjectRoot() {
|
||||||
|
return Path.of(locationField.getText().trim()).toAbsolutePath().normalize().resolve(sanitizedName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,43 +5,65 @@ import javafx.geometry.Insets;
|
|||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.control.ListView;
|
import javafx.scene.control.ListView;
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
|
import p.studio.projects.KnownProjectsService;
|
||||||
import p.studio.projects.ProjectCatalogService;
|
import p.studio.projects.ProjectCatalogService;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public final class ProjectLauncherView extends BorderPane {
|
public final class ProjectLauncherView extends BorderPane {
|
||||||
|
private final KnownProjectsService knownProjectsService;
|
||||||
private final ProjectCatalogService projectCatalogService;
|
private final ProjectCatalogService projectCatalogService;
|
||||||
private final Consumer<ProjectReference> onOpenProject;
|
private final Consumer<ProjectReference> onOpenProject;
|
||||||
|
private final Consumer<ProjectReference> onAddProject;
|
||||||
private final Consumer<ProjectReference> onCreateProject;
|
private final Consumer<ProjectReference> onCreateProject;
|
||||||
|
private final Consumer<ProjectReference> onForgetProject;
|
||||||
private final ListView<ProjectReference> projectList = new ListView<>();
|
private final ListView<ProjectReference> projectList = new ListView<>();
|
||||||
private final TextField projectNameField = new TextField();
|
|
||||||
private final Label feedbackLabel = new Label();
|
private final Label feedbackLabel = new Label();
|
||||||
|
|
||||||
public ProjectLauncherView(
|
public ProjectLauncherView(
|
||||||
|
KnownProjectsService knownProjectsService,
|
||||||
ProjectCatalogService projectCatalogService,
|
ProjectCatalogService projectCatalogService,
|
||||||
Consumer<ProjectReference> onOpenProject,
|
Consumer<ProjectReference> onOpenProject,
|
||||||
Consumer<ProjectReference> onCreateProject) {
|
Consumer<ProjectReference> onAddProject,
|
||||||
|
Consumer<ProjectReference> onCreateProject,
|
||||||
|
Consumer<ProjectReference> onForgetProject) {
|
||||||
|
this.knownProjectsService = Objects.requireNonNull(knownProjectsService, "knownProjectsService");
|
||||||
this.projectCatalogService = Objects.requireNonNull(projectCatalogService, "projectCatalogService");
|
this.projectCatalogService = Objects.requireNonNull(projectCatalogService, "projectCatalogService");
|
||||||
this.onOpenProject = Objects.requireNonNull(onOpenProject, "onOpenProject");
|
this.onOpenProject = Objects.requireNonNull(onOpenProject, "onOpenProject");
|
||||||
|
this.onAddProject = Objects.requireNonNull(onAddProject, "onAddProject");
|
||||||
this.onCreateProject = Objects.requireNonNull(onCreateProject, "onCreateProject");
|
this.onCreateProject = Objects.requireNonNull(onCreateProject, "onCreateProject");
|
||||||
|
this.onForgetProject = Objects.requireNonNull(onForgetProject, "onForgetProject");
|
||||||
|
|
||||||
getStyleClass().add("studio-project-launcher");
|
getStyleClass().add("studio-project-launcher");
|
||||||
setPadding(new Insets(24));
|
setPadding(new Insets(16));
|
||||||
setCenter(buildContent());
|
setCenter(buildCenteredContent());
|
||||||
|
|
||||||
reloadProjects();
|
reloadProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private StackPane buildCenteredContent() {
|
||||||
|
final VBox content = buildContent();
|
||||||
|
final StackPane wrapper = new StackPane(content);
|
||||||
|
wrapper.setPadding(new Insets(8, 12, 8, 12));
|
||||||
|
StackPane.setAlignment(content, Pos.CENTER);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
private VBox buildContent() {
|
private VBox buildContent() {
|
||||||
final Label title = new Label();
|
final Label title = new Label();
|
||||||
title.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_TITLE));
|
title.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_TITLE));
|
||||||
@ -56,6 +78,12 @@ public final class ProjectLauncherView extends BorderPane {
|
|||||||
listTitle.getStyleClass().add("studio-launcher-section-title");
|
listTitle.getStyleClass().add("studio-launcher-section-title");
|
||||||
|
|
||||||
projectList.getStyleClass().add("studio-project-list");
|
projectList.getStyleClass().add("studio-project-list");
|
||||||
|
projectList.setCellFactory(ignored -> new ProjectReferenceListCell());
|
||||||
|
projectList.setOnMouseClicked(event -> {
|
||||||
|
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||||
|
openSelectedProject();
|
||||||
|
}
|
||||||
|
});
|
||||||
VBox.setVgrow(projectList, Priority.ALWAYS);
|
VBox.setVgrow(projectList, Priority.ALWAYS);
|
||||||
|
|
||||||
final Button openButton = new Button();
|
final Button openButton = new Button();
|
||||||
@ -63,27 +91,33 @@ public final class ProjectLauncherView extends BorderPane {
|
|||||||
openButton.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
|
openButton.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
|
||||||
openButton.setOnAction(ignored -> openSelectedProject());
|
openButton.setOnAction(ignored -> openSelectedProject());
|
||||||
|
|
||||||
final HBox openRow = new HBox(openButton);
|
final Button addButton = new Button();
|
||||||
|
addButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_ADD_PROJECT));
|
||||||
|
addButton.setOnAction(ignored -> addExistingProject());
|
||||||
|
|
||||||
|
final Button forgetButton = new Button();
|
||||||
|
forgetButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_FORGET_PROJECT));
|
||||||
|
forgetButton.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
|
||||||
|
forgetButton.setOnAction(ignored -> forgetSelectedProject());
|
||||||
|
|
||||||
|
final HBox openRow = new HBox(10, openButton, addButton, forgetButton);
|
||||||
openRow.setAlignment(Pos.CENTER_LEFT);
|
openRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
final Label createTitle = new Label();
|
final Label createTitle = new Label();
|
||||||
createTitle.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_PROJECT));
|
createTitle.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_PROJECT));
|
||||||
createTitle.getStyleClass().add("studio-launcher-section-title");
|
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();
|
final Button createButton = new Button();
|
||||||
createButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_BUTTON));
|
createButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_BUTTON));
|
||||||
createButton.setOnAction(ignored -> createProject());
|
createButton.setOnAction(ignored -> openCreateWizard());
|
||||||
|
|
||||||
final HBox createRow = new HBox(12, projectNameField, createButton);
|
final HBox createRow = new HBox(10, createButton);
|
||||||
createRow.setAlignment(Pos.CENTER_LEFT);
|
createRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
feedbackLabel.getStyleClass().add("studio-launcher-feedback");
|
feedbackLabel.getStyleClass().add("studio-launcher-feedback");
|
||||||
feedbackLabel.setWrapText(true);
|
feedbackLabel.setWrapText(true);
|
||||||
|
|
||||||
final VBox content = new VBox(16,
|
final VBox content = new VBox(12,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
listTitle,
|
listTitle,
|
||||||
@ -92,12 +126,12 @@ public final class ProjectLauncherView extends BorderPane {
|
|||||||
createTitle,
|
createTitle,
|
||||||
createRow,
|
createRow,
|
||||||
feedbackLabel);
|
feedbackLabel);
|
||||||
content.setMaxWidth(720);
|
content.setMaxWidth(520);
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reloadProjects() {
|
public void reloadProjects() {
|
||||||
projectList.setItems(FXCollections.observableArrayList(projectCatalogService.listProjects()));
|
projectList.setItems(FXCollections.observableArrayList(knownProjectsService.listKnownProjects()));
|
||||||
if (!projectList.getItems().isEmpty()) {
|
if (!projectList.getItems().isEmpty()) {
|
||||||
projectList.getSelectionModel().selectFirst();
|
projectList.getSelectionModel().selectFirst();
|
||||||
}
|
}
|
||||||
@ -115,18 +149,87 @@ public final class ProjectLauncherView extends BorderPane {
|
|||||||
onOpenProject.accept(selected);
|
onOpenProject.accept(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createProject() {
|
private void forgetSelectedProject() {
|
||||||
try {
|
final ProjectReference selected = projectList.getSelectionModel().getSelectedItem();
|
||||||
final ProjectReference created = projectCatalogService.createProject(projectNameField.getText());
|
if (selected == null) {
|
||||||
projectNameField.clear();
|
feedbackLabel.setText("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
feedbackLabel.textProperty().unbind();
|
||||||
|
feedbackLabel.setText("");
|
||||||
|
onForgetProject.accept(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openCreateWizard() {
|
||||||
|
NewProjectWizard.showAndWait(getScene().getWindow(), projectCatalogService).ifPresent(created -> {
|
||||||
reloadProjects();
|
reloadProjects();
|
||||||
projectList.getSelectionModel().select(created);
|
projectList.getSelectionModel().select(created);
|
||||||
feedbackLabel.textProperty().unbind();
|
feedbackLabel.textProperty().unbind();
|
||||||
feedbackLabel.setText("");
|
feedbackLabel.setText("");
|
||||||
onCreateProject.accept(created);
|
onCreateProject.accept(created);
|
||||||
} catch (IllegalArgumentException illegalArgumentException) {
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addExistingProject() {
|
||||||
|
final FileChooser chooser = new FileChooser();
|
||||||
|
chooser.setTitle(Container.i18n().text(I18n.LAUNCHER_ADD_PROJECT));
|
||||||
|
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Prometeu Project", "prometeu.json"));
|
||||||
|
chooser.setInitialFileName("prometeu.json");
|
||||||
|
final File initialDirectory = projectCatalogService.projectsRoot().toFile();
|
||||||
|
if (initialDirectory.isDirectory()) {
|
||||||
|
chooser.setInitialDirectory(initialDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
final File selected = chooser.showOpenDialog(getScene().getWindow());
|
||||||
|
if (selected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ProjectReference project = projectCatalogService.openProjectManifest(Path.of(selected.toURI()));
|
||||||
|
onAddProject.accept(project);
|
||||||
|
reloadProjects();
|
||||||
|
projectList.getSelectionModel().select(project);
|
||||||
feedbackLabel.textProperty().unbind();
|
feedbackLabel.textProperty().unbind();
|
||||||
feedbackLabel.setText(illegalArgumentException.getMessage());
|
feedbackLabel.setText("");
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
feedbackLabel.textProperty().unbind();
|
||||||
|
feedbackLabel.setText(exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ProjectReferenceListCell extends ListCell<ProjectReference> {
|
||||||
|
private final Label nameLabel = new Label();
|
||||||
|
private final Label metadataLabel = new Label();
|
||||||
|
private final Label pathLabel = new Label();
|
||||||
|
private final VBox content = new VBox(3, nameLabel, metadataLabel, pathLabel);
|
||||||
|
|
||||||
|
private ProjectReferenceListCell() {
|
||||||
|
nameLabel.getStyleClass().add("studio-project-list-name");
|
||||||
|
metadataLabel.getStyleClass().add("studio-project-list-metadata");
|
||||||
|
pathLabel.getStyleClass().add("studio-project-list-path");
|
||||||
|
metadataLabel.setWrapText(true);
|
||||||
|
pathLabel.setWrapText(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(ProjectReference item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nameLabel.setText(item.name());
|
||||||
|
metadataLabel.setText("v%s | %s | stdlib %d".formatted(
|
||||||
|
item.version(),
|
||||||
|
item.languageId(),
|
||||||
|
item.stdlibVersion()));
|
||||||
|
pathLabel.setText(item.rootPath().toString());
|
||||||
|
setText(null);
|
||||||
|
setGraphic(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
package p.studio.window;
|
||||||
|
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import p.studio.Container;
|
||||||
|
import p.studio.events.StudioProjectCreatedEvent;
|
||||||
|
import p.studio.events.StudioProjectOpenedEvent;
|
||||||
|
import p.studio.projects.KnownProjectsService;
|
||||||
|
import p.studio.projects.ProjectCatalogService;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.utilities.i18n.I18n;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
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_WIDTH = 1280;
|
||||||
|
private static final double PROJECT_HEIGHT = 840;
|
||||||
|
|
||||||
|
private final Stage launcherStage;
|
||||||
|
private final ProjectCatalogService projectCatalogService;
|
||||||
|
private final KnownProjectsService knownProjectsService;
|
||||||
|
private final WindowStateService windowStateService;
|
||||||
|
private final ProjectLauncherView launcherView;
|
||||||
|
|
||||||
|
public StudioWindowCoordinator(Stage launcherStage) {
|
||||||
|
this.launcherStage = Objects.requireNonNull(launcherStage, "launcherStage");
|
||||||
|
this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot());
|
||||||
|
this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService);
|
||||||
|
this.windowStateService = new WindowStateService(resolveWindowStateStorage());
|
||||||
|
this.launcherView = new ProjectLauncherView(
|
||||||
|
knownProjectsService,
|
||||||
|
projectCatalogService,
|
||||||
|
this::openProject,
|
||||||
|
this::addProject,
|
||||||
|
this::createProject,
|
||||||
|
this::forgetProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showLauncher() {
|
||||||
|
final Scene scene = new Scene(launcherView, LAUNCHER_WIDTH, LAUNCHER_HEIGHT);
|
||||||
|
scene.getStylesheets().add(Container.theme().getDefaultTheme());
|
||||||
|
|
||||||
|
launcherStage.titleProperty().bind(Container.i18n().bind(I18n.APP_TITLE));
|
||||||
|
launcherStage.setScene(scene);
|
||||||
|
windowStateService.installLauncherState(launcherStage, LAUNCHER_WIDTH, LAUNCHER_HEIGHT);
|
||||||
|
launcherStage.setResizable(false);
|
||||||
|
launcherStage.setWidth(LAUNCHER_WIDTH);
|
||||||
|
launcherStage.setHeight(LAUNCHER_HEIGHT);
|
||||||
|
launcherStage.setMinWidth(LAUNCHER_WIDTH);
|
||||||
|
launcherStage.setMaxWidth(LAUNCHER_WIDTH);
|
||||||
|
launcherStage.setMinHeight(LAUNCHER_HEIGHT);
|
||||||
|
launcherStage.setMaxHeight(LAUNCHER_HEIGHT);
|
||||||
|
launcherStage.show();
|
||||||
|
launcherStage.centerOnScreen();
|
||||||
|
launcherStage.toFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openProject(ProjectReference projectReference) {
|
||||||
|
final ProjectReference opened = projectCatalogService.openProject(projectReference.rootPath());
|
||||||
|
knownProjectsService.remember(opened);
|
||||||
|
Container.events().publish(new StudioProjectOpenedEvent(opened));
|
||||||
|
openProjectWindow(opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createProject(ProjectReference projectReference) {
|
||||||
|
knownProjectsService.remember(projectReference);
|
||||||
|
Container.events().publish(new StudioProjectCreatedEvent(projectReference));
|
||||||
|
Container.events().publish(new StudioProjectOpenedEvent(projectReference));
|
||||||
|
openProjectWindow(projectReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addProject(ProjectReference projectReference) {
|
||||||
|
knownProjectsService.remember(projectReference);
|
||||||
|
launcherView.reloadProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forgetProject(ProjectReference projectReference) {
|
||||||
|
knownProjectsService.forget(projectReference);
|
||||||
|
launcherView.reloadProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openProjectWindow(ProjectReference projectReference) {
|
||||||
|
final Stage projectStage = new Stage();
|
||||||
|
final Scene scene = new Scene(new MainView(projectReference), PROJECT_WIDTH, PROJECT_HEIGHT);
|
||||||
|
scene.getStylesheets().add(Container.theme().getDefaultTheme());
|
||||||
|
|
||||||
|
projectStage.setTitle(Container.i18n().format(I18n.APP_PROJECT_TITLE, projectReference.name()));
|
||||||
|
projectStage.setScene(scene);
|
||||||
|
windowStateService.installProjectShellState(projectStage, projectReference, PROJECT_WIDTH, PROJECT_HEIGHT);
|
||||||
|
projectStage.setOnHidden(ignored -> {
|
||||||
|
launcherView.reloadProjects();
|
||||||
|
launcherStage.show();
|
||||||
|
launcherStage.centerOnScreen();
|
||||||
|
launcherStage.toFront();
|
||||||
|
});
|
||||||
|
|
||||||
|
launcherStage.hide();
|
||||||
|
projectStage.show();
|
||||||
|
projectStage.toFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveKnownProjectsStorage() {
|
||||||
|
return Path.of(System.getProperty("user.home"), ".prometeu-studio", "known-projects.txt")
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveWindowStateStorage() {
|
||||||
|
return Path.of(System.getProperty("user.home"), ".prometeu-studio", "window-state.json")
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,133 @@
|
|||||||
|
package p.studio.window;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class WindowStateService {
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper()
|
||||||
|
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
|
|
||||||
|
private final Path storageFile;
|
||||||
|
|
||||||
|
public WindowStateService(Path storageFile) {
|
||||||
|
this.storageFile = Objects.requireNonNull(storageFile, "storageFile").toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void installLauncherState(Stage stage, double defaultWidth, double defaultHeight) {
|
||||||
|
final WindowStateRecord launcherState = loadState().launcher();
|
||||||
|
stage.setWidth(defaultWidth);
|
||||||
|
stage.setHeight(defaultHeight);
|
||||||
|
if (launcherState != null && launcherState.maximized()) {
|
||||||
|
Platform.runLater(() -> stage.setMaximized(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
stage.maximizedProperty().addListener((ignored, oldValue, newValue) -> {
|
||||||
|
if (Boolean.TRUE.equals(newValue)) {
|
||||||
|
Platform.runLater(() -> stage.setMaximized(false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stage.fullScreenProperty().addListener((ignored, oldValue, newValue) -> {
|
||||||
|
if (Boolean.TRUE.equals(newValue)) {
|
||||||
|
Platform.runLater(() -> stage.setFullScreen(false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void installProjectShellState(
|
||||||
|
Stage stage,
|
||||||
|
ProjectReference projectReference,
|
||||||
|
double defaultWidth,
|
||||||
|
double defaultHeight) {
|
||||||
|
final String projectKey = normalizeProjectKey(projectReference);
|
||||||
|
final WindowStateRecord state = loadState().projects().get(projectKey);
|
||||||
|
applyState(stage, state, defaultWidth, defaultHeight);
|
||||||
|
|
||||||
|
stage.xProperty().addListener((ignored, oldValue, newValue) -> saveProjectState(projectKey, stage));
|
||||||
|
stage.yProperty().addListener((ignored, oldValue, newValue) -> saveProjectState(projectKey, stage));
|
||||||
|
stage.widthProperty().addListener((ignored, oldValue, newValue) -> saveProjectState(projectKey, stage));
|
||||||
|
stage.heightProperty().addListener((ignored, oldValue, newValue) -> saveProjectState(projectKey, stage));
|
||||||
|
stage.maximizedProperty().addListener((ignored, oldValue, newValue) -> saveProjectState(projectKey, stage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyState(Stage stage, WindowStateRecord state, double defaultWidth, double defaultHeight) {
|
||||||
|
stage.setWidth(state != null && state.width() > 0 ? state.width() : defaultWidth);
|
||||||
|
stage.setHeight(state != null && state.height() > 0 ? state.height() : defaultHeight);
|
||||||
|
if (state != null && !Double.isNaN(state.x())) {
|
||||||
|
stage.setX(state.x());
|
||||||
|
}
|
||||||
|
if (state != null && !Double.isNaN(state.y())) {
|
||||||
|
stage.setY(state.y());
|
||||||
|
}
|
||||||
|
if (state != null && state.maximized()) {
|
||||||
|
stage.setMaximized(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveProjectState(String projectKey, Stage stage) {
|
||||||
|
final WindowStateFile file = loadState();
|
||||||
|
file.projects().put(projectKey, new WindowStateRecord(
|
||||||
|
stage.getX(),
|
||||||
|
stage.getY(),
|
||||||
|
stage.getWidth(),
|
||||||
|
stage.getHeight(),
|
||||||
|
stage.isMaximized()));
|
||||||
|
storeState(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WindowStateFile loadState() {
|
||||||
|
if (!Files.isRegularFile(storageFile)) {
|
||||||
|
return WindowStateFile.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final WindowStateFile loaded = MAPPER.readValue(storageFile.toFile(), WindowStateFile.class);
|
||||||
|
return loaded == null ? WindowStateFile.empty() : loaded.normalize();
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeState(WindowStateFile file) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(storageFile.getParent());
|
||||||
|
MAPPER.writeValue(storageFile.toFile(), file.normalize());
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeProjectKey(ProjectReference projectReference) {
|
||||||
|
return Objects.requireNonNull(projectReference, "projectReference")
|
||||||
|
.rootPath()
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize()
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
private record WindowStateFile(WindowStateRecord launcher, Map<String, WindowStateRecord> projects) {
|
||||||
|
private static WindowStateFile empty() {
|
||||||
|
return new WindowStateFile(null, new LinkedHashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WindowStateFile normalize() {
|
||||||
|
return new WindowStateFile(launcher, projects == null ? new LinkedHashMap<>() : new LinkedHashMap<>(projects));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
private record WindowStateRecord(double x, double y, double width, double height, boolean maximized) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
app.title=Prometeu Studio
|
app.title=Prometeu Studio
|
||||||
|
app.projectTitle=Prometeu Studio - {0}
|
||||||
|
|
||||||
menu.file=File
|
menu.file=File
|
||||||
menu.file.newProject=New Project
|
menu.file.newProject=New Project
|
||||||
@ -13,9 +14,39 @@ launcher.title=Projects
|
|||||||
launcher.subtitle=Open an existing project or create a new one to enter the Studio shell.
|
launcher.subtitle=Open an existing project or create a new one to enter the Studio shell.
|
||||||
launcher.existingProjects=Existing Projects
|
launcher.existingProjects=Existing Projects
|
||||||
launcher.openProject=Open Project
|
launcher.openProject=Open Project
|
||||||
|
launcher.addProject=Add Project
|
||||||
|
launcher.forgetProject=Forget Project
|
||||||
launcher.createProject=Create New Project
|
launcher.createProject=Create New Project
|
||||||
launcher.projectNamePrompt=Project name
|
launcher.projectNamePrompt=Project name
|
||||||
launcher.createButton=Create
|
launcher.createButton=Create
|
||||||
|
wizard.title=New Project Wizard
|
||||||
|
wizard.back=Back
|
||||||
|
wizard.next=Next
|
||||||
|
wizard.create=Create Project
|
||||||
|
wizard.cancel=Cancel
|
||||||
|
wizard.browse=Browse
|
||||||
|
wizard.step.name.title=Project Name
|
||||||
|
wizard.step.name.description=Choose the new project's name.
|
||||||
|
wizard.step.language.title=Language and Project Layout
|
||||||
|
wizard.step.language.description=Choose the language, stdlib version, and default source root for the new project.
|
||||||
|
wizard.step.location.title=Project Location
|
||||||
|
wizard.step.location.description=Choose the parent directory where the project will be created.
|
||||||
|
wizard.step.confirm.title=Confirm Project Creation
|
||||||
|
wizard.step.confirm.description=Review the project details before creating it.
|
||||||
|
wizard.language.label=Language
|
||||||
|
wizard.stdlib.label=Stdlib Version
|
||||||
|
wizard.sourceRoot.label=Source Root
|
||||||
|
wizard.confirm.name=Name: {0}
|
||||||
|
wizard.confirm.language=Language: {0}
|
||||||
|
wizard.confirm.stdlib=Stdlib: {0}
|
||||||
|
wizard.confirm.sourceRoot=Source Root: {0}
|
||||||
|
wizard.confirm.location=Location: {0}
|
||||||
|
wizard.confirm.root=Project Root: {0}
|
||||||
|
wizard.error.nameRequired=Project name is required.
|
||||||
|
wizard.error.languageRequired=Project language is required.
|
||||||
|
wizard.error.stdlibRequired=Stdlib major must be a positive integer.
|
||||||
|
wizard.error.sourceRootRequired=Source root is required.
|
||||||
|
wizard.error.locationRequired=Project location is required.
|
||||||
|
|
||||||
toolbar.play=Play
|
toolbar.play=Play
|
||||||
toolbar.stop=Stop
|
toolbar.stop=Stop
|
||||||
|
|||||||
@ -73,30 +73,68 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.studio-project-launcher {
|
.studio-project-launcher {
|
||||||
-fx-background-color: linear-gradient(to bottom, #191919, #101010);
|
-fx-background-color: linear-gradient(to bottom, #20242c, #14181d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-launcher-title {
|
.studio-launcher-title {
|
||||||
-fx-font-size: 28px;
|
-fx-font-size: 24px;
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-text-fill: #f2f2f2;
|
-fx-text-fill: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-launcher-subtitle {
|
.studio-launcher-subtitle {
|
||||||
-fx-font-size: 14px;
|
-fx-font-size: 13px;
|
||||||
-fx-text-fill: #a6a6a6;
|
-fx-text-fill: #c3ccd6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-launcher-section-title {
|
.studio-launcher-section-title {
|
||||||
-fx-font-size: 15px;
|
-fx-font-size: 15px;
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-text-fill: #dcdcdc;
|
-fx-text-fill: #f0f4f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-project-list {
|
.studio-project-list {
|
||||||
-fx-pref-height: 320px;
|
-fx-pref-height: 220px;
|
||||||
|
-fx-background-color: #0f1317;
|
||||||
|
-fx-control-inner-background: #0f1317;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 10;
|
||||||
|
-fx-border-color: #37414d;
|
||||||
|
-fx-border-radius: 10;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-padding: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-project-list .list-cell {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-padding: 8 10 8 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-project-list .list-cell:hover {
|
||||||
|
-fx-background-color: #1c2530;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-project-list .list-cell:selected {
|
||||||
|
-fx-background-color: #27486a;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-project-list-name {
|
||||||
|
-fx-text-fill: #f7fbff;
|
||||||
|
-fx-font-size: 13px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-project-list-metadata {
|
||||||
|
-fx-text-fill: #b8d8f7;
|
||||||
|
-fx-font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-project-list-path {
|
||||||
|
-fx-text-fill: #c0c8d2;
|
||||||
|
-fx-font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-launcher-feedback {
|
.studio-launcher-feedback {
|
||||||
-fx-text-fill: #ff9b7d;
|
-fx-text-fill: #ffb08f;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
final class KnownProjectsServiceTest {
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void seedsKnownProjectsFromCatalogWhenStorageIsEmpty() throws Exception {
|
||||||
|
final Path projectsRoot = tempDir.resolve("projects");
|
||||||
|
Files.createDirectories(projectsRoot.resolve("alpha"));
|
||||||
|
Files.createDirectories(projectsRoot.resolve("beta"));
|
||||||
|
Files.writeString(projectsRoot.resolve("alpha").resolve("prometeu.json"), manifest("Alpha Project"));
|
||||||
|
Files.writeString(projectsRoot.resolve("beta").resolve("prometeu.json"), manifest("Beta Project"));
|
||||||
|
|
||||||
|
final KnownProjectsService service = new KnownProjectsService(
|
||||||
|
tempDir.resolve("known-projects.txt"),
|
||||||
|
new ProjectCatalogService(projectsRoot));
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
java.util.List.of("Alpha Project", "Beta Project"),
|
||||||
|
service.listKnownProjects().stream().map(ProjectReference::name).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canForgetProjectWithoutDeletingDirectory() throws Exception {
|
||||||
|
final Path projectsRoot = tempDir.resolve("projects");
|
||||||
|
Files.createDirectories(projectsRoot.resolve("alpha"));
|
||||||
|
Files.writeString(projectsRoot.resolve("alpha").resolve("prometeu.json"), manifest("Alpha Project"));
|
||||||
|
final ProjectReference alpha = new ProjectReference("Alpha Project", "1.0.0", "pbs", 1, projectsRoot.resolve("alpha"));
|
||||||
|
|
||||||
|
final KnownProjectsService service = new KnownProjectsService(
|
||||||
|
tempDir.resolve("known-projects.txt"),
|
||||||
|
new ProjectCatalogService(projectsRoot));
|
||||||
|
|
||||||
|
service.remember(alpha);
|
||||||
|
service.forget(alpha);
|
||||||
|
|
||||||
|
assertEquals(java.util.List.of(), service.listKnownProjects());
|
||||||
|
assertEquals(true, Files.isDirectory(alpha.rootPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canRememberProjectAgainAfterItWasForgotten() throws Exception {
|
||||||
|
final Path projectsRoot = tempDir.resolve("projects");
|
||||||
|
Files.createDirectories(projectsRoot.resolve("alpha"));
|
||||||
|
Files.writeString(projectsRoot.resolve("alpha").resolve("prometeu.json"), manifest("Alpha Project"));
|
||||||
|
final ProjectReference alpha = new ProjectReference("Alpha Project", "1.0.0", "pbs", 1, projectsRoot.resolve("alpha"));
|
||||||
|
|
||||||
|
final KnownProjectsService service = new KnownProjectsService(
|
||||||
|
tempDir.resolve("known-projects.txt"),
|
||||||
|
new ProjectCatalogService(projectsRoot));
|
||||||
|
|
||||||
|
service.remember(alpha);
|
||||||
|
service.forget(alpha);
|
||||||
|
service.remember(alpha);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
java.util.List.of("Alpha Project"),
|
||||||
|
service.listKnownProjects().stream().map(ProjectReference::name).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String manifest(String name) {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"name": "%s",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "pbs",
|
||||||
|
"stdlib": "1",
|
||||||
|
"dependencies": []
|
||||||
|
}
|
||||||
|
""".formatted(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
final class ProjectCatalogServiceTest {
|
final class ProjectCatalogServiceTest {
|
||||||
@ -17,12 +18,15 @@ final class ProjectCatalogServiceTest {
|
|||||||
void listsExistingProjectDirectories() throws Exception {
|
void listsExistingProjectDirectories() throws Exception {
|
||||||
Files.createDirectories(tempDir.resolve("alpha"));
|
Files.createDirectories(tempDir.resolve("alpha"));
|
||||||
Files.createDirectories(tempDir.resolve("beta"));
|
Files.createDirectories(tempDir.resolve("beta"));
|
||||||
|
Files.createDirectories(tempDir.resolve("scratch"));
|
||||||
|
Files.writeString(tempDir.resolve("alpha").resolve("prometeu.json"), manifest("Alpha Project"));
|
||||||
|
Files.writeString(tempDir.resolve("beta").resolve("prometeu.json"), manifest("Beta Project"));
|
||||||
Files.createDirectories(tempDir.resolve("alpha").resolve("src"));
|
Files.createDirectories(tempDir.resolve("alpha").resolve("src"));
|
||||||
|
|
||||||
final ProjectCatalogService service = new ProjectCatalogService(tempDir);
|
final ProjectCatalogService service = new ProjectCatalogService(tempDir);
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
java.util.List.of("alpha", "beta"),
|
java.util.List.of("Alpha Project", "Beta Project"),
|
||||||
service.listProjects().stream().map(ProjectReference::name).toList());
|
service.listProjects().stream().map(ProjectReference::name).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,11 +36,92 @@ final class ProjectCatalogServiceTest {
|
|||||||
|
|
||||||
final ProjectReference project = service.createProject("My New Project");
|
final ProjectReference project = service.createProject("My New Project");
|
||||||
|
|
||||||
assertEquals("my-new-project", project.name());
|
assertEquals("My New Project", project.name());
|
||||||
|
assertEquals("1.0.0", project.version());
|
||||||
|
assertEquals("pbs", project.languageId());
|
||||||
|
assertEquals(1, project.stdlibVersion());
|
||||||
assertTrue(Files.isDirectory(project.rootPath()));
|
assertTrue(Files.isDirectory(project.rootPath()));
|
||||||
|
assertTrue(Files.isRegularFile(project.rootPath().resolve("prometeu.json")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve(".workspace")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve(".workspace")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve("src")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve("src")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve("build")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve("build")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve("cartridge")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve("cartridge")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsProjectInsideExplicitParentLocation() {
|
||||||
|
final ProjectCatalogService service = new ProjectCatalogService(tempDir.resolve("ignored-default-root"));
|
||||||
|
final Path customParent = tempDir.resolve("custom-projects");
|
||||||
|
|
||||||
|
final ProjectReference project = service.createProject("Wizard Project", customParent);
|
||||||
|
|
||||||
|
assertEquals("Wizard Project", project.name());
|
||||||
|
assertEquals("1.0.0", project.version());
|
||||||
|
assertEquals("pbs", project.languageId());
|
||||||
|
assertEquals(1, project.stdlibVersion());
|
||||||
|
assertEquals(customParent.resolve("wizard-project"), project.rootPath());
|
||||||
|
assertTrue(Files.isDirectory(project.rootPath()));
|
||||||
|
assertTrue(Files.isRegularFile(project.rootPath().resolve("prometeu.json")));
|
||||||
|
assertTrue(Files.isDirectory(project.rootPath().resolve(".workspace")));
|
||||||
|
assertTrue(Files.isDirectory(project.rootPath().resolve("src")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsProjectUsingRequestedLanguageStdlibAndSourceRoot() throws Exception {
|
||||||
|
final ProjectCatalogService service = new ProjectCatalogService(tempDir);
|
||||||
|
|
||||||
|
final ProjectReference project = service.createProject(new ProjectCreationRequest(
|
||||||
|
"Wizard Layout Project",
|
||||||
|
tempDir,
|
||||||
|
"pbs",
|
||||||
|
7,
|
||||||
|
"code"));
|
||||||
|
|
||||||
|
assertEquals("Wizard Layout Project", project.name());
|
||||||
|
assertEquals("1.0.0", project.version());
|
||||||
|
assertEquals("pbs", project.languageId());
|
||||||
|
assertEquals(7, project.stdlibVersion());
|
||||||
|
assertTrue(Files.isDirectory(project.rootPath().resolve("code")));
|
||||||
|
assertTrue(Files.notExists(project.rootPath().resolve("src")));
|
||||||
|
final String manifestJson = Files.readString(project.rootPath().resolve("prometeu.json"));
|
||||||
|
assertTrue(manifestJson.contains("\"language\": \"pbs\""));
|
||||||
|
assertTrue(manifestJson.contains("\"stdlib\": \"7\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openProjectRejectsDirectoryWithoutPrometeuManifest() throws Exception {
|
||||||
|
final Path invalidProject = tempDir.resolve("invalid-project");
|
||||||
|
Files.createDirectories(invalidProject);
|
||||||
|
final ProjectCatalogService service = new ProjectCatalogService(tempDir);
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> service.openProject(invalidProject));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openProjectUsesManifestNameInsteadOfDirectoryName() throws Exception {
|
||||||
|
final Path projectRoot = tempDir.resolve("folder-name");
|
||||||
|
Files.createDirectories(projectRoot);
|
||||||
|
Files.writeString(projectRoot.resolve("prometeu.json"), manifest("Display Name"));
|
||||||
|
final ProjectCatalogService service = new ProjectCatalogService(tempDir);
|
||||||
|
|
||||||
|
final ProjectReference project = service.openProject(projectRoot);
|
||||||
|
|
||||||
|
assertEquals("Display Name", project.name());
|
||||||
|
assertEquals("1.0.0", project.version());
|
||||||
|
assertEquals("pbs", project.languageId());
|
||||||
|
assertEquals(1, project.stdlibVersion());
|
||||||
|
assertEquals(projectRoot, project.rootPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String manifest(String name) {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"name": "%s",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "pbs",
|
||||||
|
"stdlib": "1",
|
||||||
|
"dependencies": []
|
||||||
|
}
|
||||||
|
""".formatted(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
test-projects/tototo/prometeu.json
Normal file
8
test-projects/tototo/prometeu.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "tototo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "pbs",
|
||||||
|
"stdlib": "12346789",
|
||||||
|
"dependencies": [
|
||||||
|
]
|
||||||
|
}
|
||||||
8
test-projects/umbelivable/prometeu.json
Normal file
8
test-projects/umbelivable/prometeu.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "umbelivable",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "pbs",
|
||||||
|
"stdlib": "1",
|
||||||
|
"dependencies": [
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user