From 7bc5c5ba1144718a952fa820914d953bf6590266 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 2 Apr 2026 12:52:54 +0100 Subject: [PATCH] editor adjustments --- .../java/p/studio/utilities/i18n/I18n.java | 6 +- .../workspaces/editor/EditorHelperPanel.java | 73 ++++++++++++- .../workspaces/editor/EditorOutlinePanel.java | 35 ------ .../workspaces/editor/EditorStatusBar.java | 48 ++++---- .../workspaces/editor/EditorWarningStrip.java | 103 ++++++++++++++++++ .../workspaces/editor/EditorWorkspace.java | 69 ++++++++---- .../editor/EditorWorkspaceIcons.java | 31 ++++++ .../main/resources/i18n/messages.properties | 10 +- .../resources/icons/editor/lock-closed.svg | 3 + .../main/resources/icons/editor/lock-open.svg | 3 + .../resources/themes/default-prometeu.css | 65 ++++++++++- 11 files changed, 339 insertions(+), 107 deletions(-) create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java create mode 100644 prometeu-studio/src/main/resources/icons/editor/lock-closed.svg create mode 100644 prometeu-studio/src/main/resources/icons/editor/lock-open.svg diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index 7d8b328f..8e759fb2 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -78,12 +78,12 @@ public enum I18n { CODE_EDITOR_NAVIGATOR_DETAIL("codeEditor.navigator.detail"), CODE_EDITOR_OUTLINE_TITLE("codeEditor.outline.title"), CODE_EDITOR_OUTLINE_PLACEHOLDER("codeEditor.outline.placeholder"), - CODE_EDITOR_OUTLINE_DIAGNOSTICS("codeEditor.outline.diagnostics"), CODE_EDITOR_OUTLINE_SYMBOLS("codeEditor.outline.symbols"), - CODE_EDITOR_OUTLINE_EMPTY_DIAGNOSTICS("codeEditor.outline.emptyDiagnostics"), CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS("codeEditor.outline.emptySymbols"), CODE_EDITOR_HELPER_TITLE("codeEditor.helper.title"), CODE_EDITOR_HELPER_PLACEHOLDER("codeEditor.helper.placeholder"), + CODE_EDITOR_HELPER_DIAGNOSTICS("codeEditor.helper.diagnostics"), + CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS("codeEditor.helper.emptyDiagnostics"), CODE_EDITOR_TABS_PLACEHOLDER("codeEditor.tabs.placeholder"), CODE_EDITOR_TABS_OVERFLOW("codeEditor.tabs.overflow"), CODE_EDITOR_STATUS_BREADCRUMB("codeEditor.status.breadcrumb"), @@ -91,8 +91,6 @@ public enum I18n { CODE_EDITOR_STATUS_LINE_SEPARATOR("codeEditor.status.lineSeparator"), CODE_EDITOR_STATUS_INDENTATION("codeEditor.status.indentation"), CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"), - CODE_EDITOR_STATUS_EDITABLE("codeEditor.status.editable"), - CODE_EDITOR_STATUS_READ_ONLY("codeEditor.status.readOnly"), CODE_EDITOR_COMMAND_SAVE("codeEditor.command.save"), CODE_EDITOR_COMMAND_SAVE_ALL("codeEditor.command.saveAll"), CODE_EDITOR_WARNING_FRONTEND_READ_ONLY("codeEditor.warning.frontendReadOnly"), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java index 13b78e8e..ba8ef5ef 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java @@ -2,15 +2,23 @@ package p.studio.workspaces.editor; import javafx.geometry.Insets; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; import p.studio.Container; import p.studio.controls.WorkspaceDockPane; +import p.studio.lsp.dtos.LspDiagnosticDTO; import p.studio.utilities.i18n.I18n; +import java.nio.file.Path; +import java.util.List; + public final class EditorHelperPanel extends WorkspaceDockPane { public static final double COLLAPSED_HEIGHT = 34.0; public static final double MINIMUM_EXPANDED_HEIGHT = 120.0; public static final double DEFAULT_EXPANDED_HEIGHT = 180.0; + private final Label summary = new Label(); + private final Label diagnosticsTitle = new Label(); + private final VBox diagnosticsBox = new VBox(6); public EditorHelperPanel() { super( @@ -22,14 +30,67 @@ public final class EditorHelperPanel extends WorkspaceDockPane { "editor-workspace-helper-pane", "editor-workspace-helper-panel"); - final var placeholder = new Label(); - placeholder.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_HELPER_PLACEHOLDER)); - placeholder.getStyleClass().add("editor-workspace-placeholder"); - placeholder.setWrapText(true); + summary.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-helper-summary"); + summary.setWrapText(true); + diagnosticsTitle.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_HELPER_DIAGNOSTICS)); + diagnosticsTitle.getStyleClass().add("editor-workspace-outline-section-title"); + diagnosticsBox.getStyleClass().add("editor-workspace-outline-list"); - final var content = new VBox(8, placeholder); + final var content = new VBox(10, summary, diagnosticsTitle, diagnosticsBox); content.getStyleClass().add("editor-workspace-panel-content"); content.setPadding(new Insets(10, 16, 14, 16)); - setDockContent(content); + final var scrollPane = new ScrollPane(content); + scrollPane.setFitToWidth(true); + scrollPane.getStyleClass().add("editor-workspace-outline-scroll"); + setDockContent(scrollPane); + showPlaceholder(); + } + + public void showPlaceholder() { + summary.textProperty().unbind(); + summary.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_HELPER_PLACEHOLDER)); + diagnosticsBox.getChildren().setAll(placeholderLabel(I18n.CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS)); + } + + public void showSemanticReadResult( + final Path documentPath, + final List diagnostics) { + summary.textProperty().unbind(); + summary.setText(documentPath.getFileName() + " • semantic read-only"); + rebuildDiagnostics(diagnostics); + } + + private void rebuildDiagnostics(final List diagnostics) { + diagnosticsBox.getChildren().clear(); + if (diagnostics.isEmpty()) { + diagnosticsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS)); + return; + } + for (final LspDiagnosticDTO diagnostic : diagnostics) { + final var label = new Label(formatDiagnostic(diagnostic)); + label.setWrapText(true); + label.getStyleClass().addAll( + "editor-workspace-outline-item", + diagnostic.severity().name().equals("ERROR") + ? "editor-workspace-outline-diagnostic-error" + : "editor-workspace-outline-diagnostic-warning"); + diagnosticsBox.getChildren().add(label); + } + } + + private Label placeholderLabel(final I18n key) { + final var label = new Label(); + label.textProperty().bind(Container.i18n().bind(key)); + label.setWrapText(true); + label.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-item"); + return label; + } + + private String formatDiagnostic(final LspDiagnosticDTO diagnostic) { + return "%s [%d,%d) %s".formatted( + diagnostic.severity().name(), + diagnostic.range().startOffset(), + diagnostic.range().endOffset(), + diagnostic.message()); } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java index b6ad1d8e..042188b1 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java @@ -4,7 +4,6 @@ import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; -import p.studio.lsp.dtos.LspDiagnosticDTO; import p.studio.lsp.dtos.LspSymbolDTO; import p.studio.Container; import p.studio.controls.WorkspaceDockPane; @@ -18,7 +17,6 @@ public final class EditorOutlinePanel extends WorkspaceDockPane { private static final double MINIMUM_EXPANDED_HEIGHT = 120.0; private static final double DEFAULT_HEIGHT = 180.0; private final Label summary = new Label(); - private final VBox diagnosticsBox = new VBox(6); private final VBox symbolsBox = new VBox(6); public EditorOutlinePanel() { @@ -33,16 +31,12 @@ public final class EditorOutlinePanel extends WorkspaceDockPane { summary.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-summary"); summary.setWrapText(true); - final var diagnosticsTitle = sectionTitle(I18n.CODE_EDITOR_OUTLINE_DIAGNOSTICS); final var symbolsTitle = sectionTitle(I18n.CODE_EDITOR_OUTLINE_SYMBOLS); - diagnosticsBox.getStyleClass().add("editor-workspace-outline-list"); symbolsBox.getStyleClass().add("editor-workspace-outline-list"); final var content = new VBox(10, summary, - diagnosticsTitle, - diagnosticsBox, symbolsTitle, symbolsBox); content.getStyleClass().add("editor-workspace-panel-content"); @@ -57,38 +51,17 @@ public final class EditorOutlinePanel extends WorkspaceDockPane { public void showPlaceholder() { summary.textProperty().unbind(); summary.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_OUTLINE_PLACEHOLDER)); - diagnosticsBox.getChildren().setAll(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_DIAGNOSTICS)); symbolsBox.getChildren().setAll(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS)); } public void showSemanticReadResult( final Path documentPath, - final List diagnostics, final List symbols) { summary.textProperty().unbind(); summary.setText(documentPath.getFileName() + " • semantic read-only"); - rebuildDiagnostics(diagnostics); rebuildSymbols(symbols); } - private void rebuildDiagnostics(final List diagnostics) { - diagnosticsBox.getChildren().clear(); - if (diagnostics.isEmpty()) { - diagnosticsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_DIAGNOSTICS)); - return; - } - for (final LspDiagnosticDTO diagnostic : diagnostics) { - final var label = new Label(formatDiagnostic(diagnostic)); - label.setWrapText(true); - label.getStyleClass().addAll( - "editor-workspace-outline-item", - diagnostic.severity().name().equals("ERROR") - ? "editor-workspace-outline-diagnostic-error" - : "editor-workspace-outline-diagnostic-warning"); - diagnosticsBox.getChildren().add(label); - } - } - private void rebuildSymbols(final List symbols) { symbolsBox.getChildren().clear(); if (symbols.isEmpty()) { @@ -125,12 +98,4 @@ public final class EditorOutlinePanel extends WorkspaceDockPane { label.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-item"); return label; } - - private String formatDiagnostic(final LspDiagnosticDTO diagnostic) { - return "%s [%d,%d) %s".formatted( - diagnostic.severity().name(), - diagnostic.range().startOffset(), - diagnostic.range().endOffset(), - diagnostic.message()); - } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java index d97fe40d..fc814890 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java @@ -6,6 +6,7 @@ import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import p.studio.Container; import p.studio.projects.ProjectReference; import p.studio.utilities.i18n.I18n; @@ -19,7 +20,7 @@ public final class EditorStatusBar extends HBox { private final Label lineSeparator = new Label(); private final Label indentation = new Label(); private final Label language = new Label(); - private final Label readOnly = new Label(); + private final StackPane accessMode = new StackPane(); public EditorStatusBar() { setAlignment(Pos.CENTER_LEFT); @@ -40,8 +41,7 @@ public final class EditorStatusBar extends HBox { indentation.getStyleClass().add("editor-workspace-status-chip-indentation"); styleChip(language); language.getStyleClass().add("editor-workspace-status-chip-language"); - styleChip(readOnly); - readOnly.getStyleClass().add("editor-workspace-status-chip-read-only"); + accessMode.getStyleClass().addAll("editor-workspace-status-chip", "editor-workspace-status-chip-access-mode"); getChildren().addAll( breadcrumb, @@ -50,7 +50,7 @@ public final class EditorStatusBar extends HBox { lineSeparator, indentation, language, - readOnly + accessMode ); } @@ -66,7 +66,7 @@ public final class EditorStatusBar extends HBox { bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION); setText(language, fileBuffer.typeId()); EditorDocumentPresentationStyles.applyToStatusChip(language, presentation); - showAccessMode(fileBuffer); + showAccessMode(fileBuffer.readOnly()); } public void showPlaceholder(final EditorDocumentPresentation presentation) { @@ -77,11 +77,8 @@ public final class EditorStatusBar extends HBox { bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION); bindDefault(language, I18n.CODE_EDITOR_STATUS_LANGUAGE); EditorDocumentPresentationStyles.applyToStatusChip(language, presentation); - bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY); - readOnly.getStyleClass().remove("editor-workspace-status-chip-editable"); - if (!readOnly.getStyleClass().contains("editor-workspace-status-chip-read-only")) { - readOnly.getStyleClass().add("editor-workspace-status-chip-read-only"); - } + accessMode.getChildren().clear(); + accessMode.getStyleClass().removeAll("editor-workspace-status-chip-editable", "editor-workspace-status-chip-read-only"); } private void bindDefault(final Label label, final I18n key) { @@ -175,29 +172,22 @@ public final class EditorStatusBar extends HBox { setVisibleManaged(lineSeparator, visible); setVisibleManaged(indentation, visible); setVisibleManaged(language, visible); - setVisibleManaged(readOnly, visible); + setVisibleManaged(accessMode, visible); } - private void showAccessMode(final EditorOpenFileBuffer fileBuffer) { - if (fileBuffer.readOnly()) { - bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY); - readOnly.getStyleClass().remove("editor-workspace-status-chip-editable"); - if (!readOnly.getStyleClass().contains("editor-workspace-status-chip-read-only")) { - readOnly.getStyleClass().add("editor-workspace-status-chip-read-only"); - } - return; - } - - bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_EDITABLE); - readOnly.getStyleClass().remove("editor-workspace-status-chip-read-only"); - if (!readOnly.getStyleClass().contains("editor-workspace-status-chip-editable")) { - readOnly.getStyleClass().add("editor-workspace-status-chip-editable"); - } + private void showAccessMode(final boolean readOnlyMode) { + accessMode.getChildren().setAll(readOnlyMode + ? EditorWorkspaceIcons.lockClosed() + : EditorWorkspaceIcons.lockOpen()); + accessMode.getStyleClass().removeAll("editor-workspace-status-chip-editable", "editor-workspace-status-chip-read-only"); + accessMode.getStyleClass().add(readOnlyMode + ? "editor-workspace-status-chip-read-only" + : "editor-workspace-status-chip-editable"); } - private void setVisibleManaged(final Label label, final boolean visible) { - label.setVisible(visible); - label.setManaged(visible); + private void setVisibleManaged(final Node node, final boolean visible) { + node.setVisible(visible); + node.setManaged(visible); } private void styleChip(final Label label) { diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java new file mode 100644 index 00000000..615c205c --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java @@ -0,0 +1,103 @@ +package p.studio.workspaces.editor; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; + +import java.util.List; +import java.util.Objects; + +public final class EditorWarningStrip extends HBox { + private final Label message = new Label(); + private final Label counter = new Label(); + private final Button previousButton = new Button("‹"); + private final Button nextButton = new Button("›"); + private final Button closeButton = new Button("×"); + + private List warnings = List.of(); + private int activeIndex; + private boolean dismissed; + + public EditorWarningStrip() { + setAlignment(Pos.CENTER_LEFT); + getStyleClass().add("editor-workspace-warning"); + + message.setWrapText(true); + message.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(message, Priority.ALWAYS); + + counter.getStyleClass().add("editor-workspace-warning-counter"); + + previousButton.getStyleClass().add("editor-workspace-warning-button"); + nextButton.getStyleClass().add("editor-workspace-warning-button"); + closeButton.getStyleClass().add("editor-workspace-warning-button"); + + previousButton.setFocusTraversable(false); + nextButton.setFocusTraversable(false); + closeButton.setFocusTraversable(false); + + previousButton.setOnAction(event -> showIndex(activeIndex - 1)); + nextButton.setOnAction(event -> showIndex(activeIndex + 1)); + closeButton.setOnAction(event -> dismiss()); + + final var spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + getChildren().addAll(message, spacer, counter, previousButton, nextButton, closeButton); + setVisible(false); + setManaged(false); + } + + public void showWarnings(final List warningItems) { + warnings = List.copyOf(Objects.requireNonNull(warningItems, "warningItems")); + activeIndex = 0; + dismissed = false; + refresh(); + } + + public void clearWarnings() { + warnings = List.of(); + activeIndex = 0; + dismissed = false; + refresh(); + } + + private void dismiss() { + dismissed = true; + refresh(); + } + + private void showIndex(final int candidateIndex) { + if (warnings.isEmpty()) { + return; + } + activeIndex = Math.floorMod(candidateIndex, warnings.size()); + refresh(); + } + + private void refresh() { + final boolean visible = !dismissed && !warnings.isEmpty(); + setVisible(visible); + setManaged(visible); + if (!visible) { + message.setText(""); + counter.setText(""); + return; + } + + final WarningItem warning = warnings.get(activeIndex); + message.setText(warning.message()); + counter.setText(warnings.size() == 1 ? "1/1" : (activeIndex + 1) + "/" + warnings.size()); + final boolean canNavigate = warnings.size() > 1; + previousButton.setDisable(!canNavigate); + nextButton.setDisable(!canNavigate); + } + + public record WarningItem(String message) { + public WarningItem { + message = Objects.requireNonNull(message, "message"); + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java index f7b1bd6c..4ccf4d7a 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java @@ -1,11 +1,12 @@ package p.studio.workspaces.editor; +import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Button; -import javafx.scene.control.Label; import javafx.scene.control.SplitPane; import javafx.scene.layout.*; +import org.fxmisc.flowless.VirtualizedScrollPane; import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.LineNumberFactory; import p.studio.lsp.LspService; @@ -27,9 +28,10 @@ import java.util.Objects; public final class EditorWorkspace extends Workspace { private final BorderPane root = new BorderPane(); private final CodeArea codeArea = new CodeArea(); + private final VirtualizedScrollPane codeScroller = new VirtualizedScrollPane<>(codeArea); private final Button saveButton = new Button(); private final Button saveAllButton = new Button(); - private final Label readOnlyWarning = new Label(); + private final EditorWarningStrip warningStrip = new EditorWarningStrip(); private final EditorProjectNavigatorPanel navigatorPanel = new EditorProjectNavigatorPanel(); private final EditorOutlinePanel outlinePanel = new EditorOutlinePanel(); private final EditorHelperPanel helperPanel = new EditorHelperPanel(); @@ -135,9 +137,19 @@ public final class EditorWorkspace extends Workspace { try { codeArea.replaceText(fileBuffer.content()); codeArea.setStyleSpans(0, highlighting.styleSpans()); + codeArea.moveTo(0); + codeArea.requestFollowCaret(); } finally { syncingEditor = false; } + Platform.runLater(() -> { + codeArea.moveTo(0); + codeArea.showParagraphAtTop(0); + codeArea.requestFollowCaret(); + if (fileBuffer.editable()) { + codeArea.requestFocus(); + } + }); codeArea.setEditable(fileBuffer.editable()); EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); refreshCommandSurfaces(fileBuffer); @@ -164,25 +176,28 @@ public final class EditorWorkspace extends Workspace { private void showEditorPlaceholder() { final EditorDocumentPresentation presentation = presentationRegistry.resolve("text"); - final String placeholder = """ - // Controlled write wave - // Open a supported project document from the navigator. - """; applyPresentationStylesheets(presentation); syncingEditor = true; try { - codeArea.replaceText(placeholder); - codeArea.setStyleSpans(0, presentation.highlight(placeholder)); + codeArea.replaceText(""); + codeArea.setStyleSpans(0, presentation.highlight("")); + codeArea.moveTo(0); + codeArea.requestFollowCaret(); } finally { syncingEditor = false; } + Platform.runLater(() -> { + codeArea.moveTo(0); + codeArea.showParagraphAtTop(0); + codeArea.requestFollowCaret(); + }); codeArea.setEditable(false); EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); saveButton.setDisable(true); saveAllButton.setDisable(true); - readOnlyWarning.setVisible(false); - readOnlyWarning.setManaged(false); + warningStrip.clearWarnings(); outlinePanel.showPlaceholder(); + helperPanel.showPlaceholder(); } private void applyPresentationStylesheets(final EditorDocumentPresentation presentation) { @@ -210,9 +225,13 @@ public final class EditorWorkspace extends Workspace { } private VBox buildCenterColumn() { - final var centerColumn = new VBox(12, buildCommandBar(), readOnlyWarning, tabStrip, codeArea); + final var editorSurface = new VBox(0, warningStrip, codeScroller); + editorSurface.getStyleClass().add("editor-workspace-editor-surface"); + codeScroller.getStyleClass().add("editor-workspace-code-scroller"); + VBox.setVgrow(codeScroller, Priority.ALWAYS); + final var centerColumn = new VBox(12, buildCommandBar(), tabStrip, editorSurface); centerColumn.getStyleClass().add("editor-workspace-center-column"); - VBox.setVgrow(codeArea, Priority.ALWAYS); + VBox.setVgrow(editorSurface, Priority.ALWAYS); return centerColumn; } @@ -234,11 +253,7 @@ public final class EditorWorkspace extends Workspace { } private void configureWarning() { - readOnlyWarning.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_WARNING_FRONTEND_READ_ONLY)); - readOnlyWarning.getStyleClass().add("editor-workspace-warning"); - readOnlyWarning.setWrapText(true); - readOnlyWarning.setVisible(false); - readOnlyWarning.setManaged(false); + warningStrip.clearWarnings(); } private HBox buildCommandBar() { @@ -302,9 +317,16 @@ public final class EditorWorkspace extends Workspace { private void refreshCommandSurfaces(final EditorOpenFileBuffer fileBuffer) { saveButton.setDisable(!fileBuffer.saveEnabled()); saveAllButton.setDisable(!openFileSession.hasDirtyEditableFiles()); - final boolean showWarning = fileBuffer.frontendDocument() && fileBuffer.readOnly(); - readOnlyWarning.setVisible(showWarning); - readOnlyWarning.setManaged(showWarning); + final List warnings = new ArrayList<>(); + if (fileBuffer.frontendDocument() && fileBuffer.readOnly()) { + warnings.add(new EditorWarningStrip.WarningItem( + p.studio.Container.i18n().text(I18n.CODE_EDITOR_WARNING_FRONTEND_READ_ONLY))); + } + if (warnings.isEmpty()) { + warningStrip.clearWarnings(); + return; + } + warningStrip.showWarnings(warnings); } private EditorOpenFileBuffer bufferFrom(final VfsDocumentOpenResult.VfsTextDocument textDocument) { @@ -324,11 +346,10 @@ public final class EditorWorkspace extends Workspace { final LspAnalyzeDocumentResult analysis) { if (!fileBuffer.frontendDocument() || analysis == null) { outlinePanel.showPlaceholder(); + helperPanel.showPlaceholder(); return; } - outlinePanel.showSemanticReadResult( - fileBuffer.path(), - analysis.diagnostics(), - analysis.documentSymbols()); + outlinePanel.showSemanticReadResult(fileBuffer.path(), analysis.documentSymbols()); + helperPanel.showSemanticReadResult(fileBuffer.path(), analysis.diagnostics()); } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java index 84c54c2e..15d28072 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java @@ -4,7 +4,14 @@ import javafx.scene.Node; import javafx.scene.layout.StackPane; import javafx.scene.shape.SVGPath; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public final class EditorWorkspaceIcons { + private static final Pattern SVG_PATH_PATTERN = Pattern.compile("]*\\sd=\"([^\"]+)\""); private static final String FOLDER_PATH = "M1.5 4.5A1.5 1.5 0 0 1 3 3h3.2l1.1 1.4H13A1.5 1.5 0 0 1 14.5 5.9V12A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12z"; private static final String FILE_PATH = "M4 1.5h5l3 3V13A1.5 1.5 0 0 1 10.5 14.5h-6A1.5 1.5 0 0 1 3 13V3A1.5 1.5 0 0 1 4.5 1.5zm4.7 1.2V5h2.3z"; private static final String COG_PATH = "M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.24-.68-2.64.72-1.96 1.96l.17.31c.446.816.023 1.84-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1c.895.265 1.318 1.289.872 2.105l-.17.31c-.68 1.24.72 2.64 1.96 1.96l.31-.17a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.24.68 2.64-.72 1.96-1.96l-.17-.31a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.68-1.24-.72-2.64-1.96-1.96l-.31.17a1.464 1.464 0 0 1-2.105-.872z"; @@ -42,6 +49,14 @@ public final class EditorWorkspaceIcons { return icon(TARGET_PATH, "editor-workspace-icon-target"); } + public static Node lockClosed() { + return icon(iconPathFromResource("/icons/editor/lock-closed.svg"), "editor-workspace-icon-lock-closed"); + } + + public static Node lockOpen() { + return icon(iconPathFromResource("/icons/editor/lock-open.svg"), "editor-workspace-icon-lock-open"); + } + private static Node icon(final String path, final String toneClass) { final var shape = new SVGPath(); shape.setContent(path); @@ -51,4 +66,20 @@ public final class EditorWorkspaceIcons { icon.getStyleClass().add("editor-workspace-icon"); return icon; } + + private static String iconPathFromResource(final String resourcePath) { + try (InputStream inputStream = EditorWorkspaceIcons.class.getResourceAsStream(resourcePath)) { + if (inputStream == null) { + throw new IllegalStateException("Missing icon resource: " + resourcePath); + } + final String svg = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + final Matcher matcher = SVG_PATH_PATTERN.matcher(svg); + if (!matcher.find()) { + throw new IllegalStateException("Missing in icon resource: " + resourcePath); + } + return matcher.group(1); + } catch (IOException exception) { + throw new IllegalStateException("Unable to read icon resource: " + resourcePath, exception); + } + } } diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index b3d8cd16..095a74e9 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -68,13 +68,13 @@ codeEditor.navigator.revealActive=Reveal active file codeEditor.navigator.placeholder=Project navigation lands in the next implementation slice. codeEditor.navigator.detail=This first shell reserves the full navigator surface, its refresh action, and the left-column composition without wiring project-tree data yet. codeEditor.outline.title=Outline -codeEditor.outline.placeholder=Open a frontend document to inspect read-only diagnostics and symbols. -codeEditor.outline.diagnostics=Diagnostics +codeEditor.outline.placeholder=Open a frontend document to inspect semantic symbols. codeEditor.outline.symbols=Symbols -codeEditor.outline.emptyDiagnostics=No diagnostics for the active frontend document. codeEditor.outline.emptySymbols=No semantic symbols are currently available for the active frontend document. codeEditor.helper.title=Editor Helper -codeEditor.helper.placeholder=This region is intentionally passive in the first read-only wave. +codeEditor.helper.placeholder=Open a frontend document to inspect semantic diagnostics. +codeEditor.helper.diagnostics=Diagnostics +codeEditor.helper.emptyDiagnostics=No diagnostics for the active frontend document. codeEditor.tabs.placeholder=no-file-open.txt codeEditor.tabs.overflow=More codeEditor.status.breadcrumb=proj > src > file.pbs @@ -82,8 +82,6 @@ codeEditor.status.position=L:C codeEditor.status.lineSeparator=LF codeEditor.status.indentation=Spaces: 4 codeEditor.status.language=Text -codeEditor.status.editable=Editable -codeEditor.status.readOnly=Read-only codeEditor.command.save=Save codeEditor.command.saveAll=Save All codeEditor.warning.frontendReadOnly=This frontend file is read-only in this wave. It cannot be edited or saved yet. diff --git a/prometeu-studio/src/main/resources/icons/editor/lock-closed.svg b/prometeu-studio/src/main/resources/icons/editor/lock-closed.svg new file mode 100644 index 00000000..0bda1273 --- /dev/null +++ b/prometeu-studio/src/main/resources/icons/editor/lock-closed.svg @@ -0,0 +1,3 @@ + + + diff --git a/prometeu-studio/src/main/resources/icons/editor/lock-open.svg b/prometeu-studio/src/main/resources/icons/editor/lock-open.svg new file mode 100644 index 00000000..299e78be --- /dev/null +++ b/prometeu-studio/src/main/resources/icons/editor/lock-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/prometeu-studio/src/main/resources/themes/default-prometeu.css b/prometeu-studio/src/main/resources/themes/default-prometeu.css index 2abcd989..d0e9f2fb 100644 --- a/prometeu-studio/src/main/resources/themes/default-prometeu.css +++ b/prometeu-studio/src/main/resources/themes/default-prometeu.css @@ -506,6 +506,14 @@ -fx-fill: #eff5fb; } +.editor-workspace-icon-lock-closed { + -fx-fill: #f6d78f; +} + +.editor-workspace-icon-lock-open { + -fx-fill: #bde7c7; +} + .editor-workspace-outline-panel { -fx-min-height: 0; } @@ -686,6 +694,16 @@ -fx-fill: #f2f6fb; } +.editor-workspace-code-area .caret { + -fx-stroke: #ffd27a; +} + +.editor-workspace-editor-surface { + -fx-background-color: #171c22; + -fx-border-color: #2a313c; + -fx-border-width: 1; +} + .editor-workspace-code-area-type-text .text { -fx-fill: #eef3f8; } @@ -707,13 +725,43 @@ } .editor-workspace-warning { + -fx-alignment: center-left; + -fx-spacing: 8; -fx-padding: 8 12 8 12; -fx-background-color: #3b2a10; -fx-border-color: #8f6730; + -fx-border-width: 0 0 1 0; -fx-text-fill: #f7ddb0; -fx-font-size: 12px; } +.editor-workspace-warning-counter { + -fx-text-fill: #f3d8a7; + -fx-font-size: 11px; + -fx-font-weight: 700; +} + +.editor-workspace-warning-button { + -fx-min-width: 24; + -fx-pref-width: 24; + -fx-max-width: 24; + -fx-min-height: 24; + -fx-pref-height: 24; + -fx-max-height: 24; + -fx-padding: 0; + -fx-background-color: #4b3413; + -fx-border-color: #9a7440; + -fx-text-fill: #f7ddb0; + -fx-font-size: 11px; + -fx-background-radius: 4; + -fx-border-radius: 4; +} + +.editor-workspace-warning-button:hover { + -fx-background-color: #5a4018; + -fx-border-color: #b38749; +} + .workspace-dock-pane { -fx-collapsible: true; -fx-background-color: transparent; @@ -753,6 +801,10 @@ -fx-min-height: 0; } +.editor-workspace-helper-summary { + -fx-text-fill: #dce6f0; +} + .workspace-dock-pane-collapsed { -fx-background-insets: 0; } @@ -858,10 +910,17 @@ -fx-max-width: 72; } +.editor-workspace-status-chip-access-mode { + -fx-min-width: 36; + -fx-pref-width: 36; + -fx-max-width: 36; + -fx-padding: 0; +} + .editor-workspace-status-chip-read-only { - -fx-min-width: 88; - -fx-pref-width: 88; - -fx-max-width: 88; + -fx-background-color: #3b2a10; + -fx-border-color: #8f6730; + -fx-text-fill: #f7ddb0; } .editor-workspace-status-chip-editable {