editor with write capability

This commit is contained in:
bQUARKz 2026-04-04 12:21:47 +01:00
parent 2f8f374abc
commit dc62943a16
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
3 changed files with 237 additions and 24 deletions

View File

@ -1,5 +1,6 @@
package p.studio.workspaces.editor; package p.studio.workspaces.editor;
import javafx.css.PseudoClass;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CustomMenuItem; import javafx.scene.control.CustomMenuItem;
@ -7,9 +8,13 @@ import javafx.scene.control.Label;
import javafx.scene.control.MenuButton; import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.OverrunStyle; import javafx.scene.control.OverrunStyle;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import p.studio.Container; import p.studio.Container;
import p.studio.utilities.i18n.I18n; import p.studio.utilities.i18n.I18n;
@ -20,6 +25,7 @@ import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
public final class EditorTabStrip extends HBox { public final class EditorTabStrip extends HBox {
private static final PseudoClass PRESSED_PSEUDO_CLASS = PseudoClass.getPseudoClass("pressed");
private static final double TAB_WIDTH_HINT = 176.0; private static final double TAB_WIDTH_HINT = 176.0;
private static final double TAB_HEIGHT_HINT = 34.0; private static final double TAB_HEIGHT_HINT = 34.0;
private static final double STRIP_HEIGHT_HINT = 50.0; private static final double STRIP_HEIGHT_HINT = 50.0;
@ -30,6 +36,7 @@ public final class EditorTabStrip extends HBox {
private final Region spacer = new Region(); private final Region spacer = new Region();
private final List<EditorOpenFileBuffer> openFiles = new ArrayList<>(); private final List<EditorOpenFileBuffer> openFiles = new ArrayList<>();
private Consumer<Path> tabSelectionAction = path -> { }; private Consumer<Path> tabSelectionAction = path -> { };
private Consumer<Path> tabCloseAction = path -> { };
private Path activePath; private Path activePath;
public EditorTabStrip() { public EditorTabStrip() {
@ -56,6 +63,10 @@ public final class EditorTabStrip extends HBox {
this.tabSelectionAction = Objects.requireNonNull(tabSelectionAction, "tabSelectionAction"); this.tabSelectionAction = Objects.requireNonNull(tabSelectionAction, "tabSelectionAction");
} }
public void setTabCloseAction(final Consumer<Path> tabCloseAction) {
this.tabCloseAction = Objects.requireNonNull(tabCloseAction, "tabCloseAction");
}
public void showOpenFiles(final List<EditorOpenFileBuffer> files, final Path activePath) { public void showOpenFiles(final List<EditorOpenFileBuffer> files, final Path activePath) {
this.openFiles.clear(); this.openFiles.clear();
this.openFiles.addAll(Objects.requireNonNull(files, "files")); this.openFiles.addAll(Objects.requireNonNull(files, "files"));
@ -81,6 +92,10 @@ public final class EditorTabStrip extends HBox {
for (int index = startIndex; index < endIndex; index++) { for (int index = startIndex; index < endIndex; index++) {
final var fileBuffer = openFiles.get(index); final var fileBuffer = openFiles.get(index);
final var tabButton = new Button(fileBuffer.tabLabel()); final var tabButton = new Button(fileBuffer.tabLabel());
final var tabLabel = new Label(fileBuffer.tabLabel());
final var closeChip = new StackPane();
final var closeIcon = new SVGPath();
final var tabContent = new HBox();
tabButton.setFocusTraversable(false); tabButton.setFocusTraversable(false);
tabButton.getStyleClass().addAll( tabButton.getStyleClass().addAll(
"studio-button", "studio-button",
@ -90,11 +105,25 @@ public final class EditorTabStrip extends HBox {
fileBuffer.readOnly() fileBuffer.readOnly()
? "editor-workspace-tab-button-read-only" ? "editor-workspace-tab-button-read-only"
: "editor-workspace-tab-button-editable"); : "editor-workspace-tab-button-editable");
applyTabMetrics(tabButton); tabLabel.getStyleClass().add("editor-workspace-tab-label");
tabLabel.setTextOverrun(OverrunStyle.ELLIPSIS);
closeChip.getStyleClass().add("editor-workspace-tab-close-chip");
closeIcon.setContent("M 3 3 L 9 9 M 9 3 L 3 9");
closeIcon.getStyleClass().add("editor-workspace-tab-close-icon");
closeIcon.setMouseTransparent(true);
closeChip.getChildren().add(closeIcon);
tabContent.getStyleClass().add("editor-workspace-tab-content");
tabContent.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(tabLabel, Priority.ALWAYS);
tabContent.getChildren().addAll(tabLabel, closeChip);
tabButton.setText(null);
tabButton.setGraphic(tabContent);
applyTabMetrics(tabButton, tabLabel, closeChip);
if (fileBuffer.path().equals(activePath)) { if (fileBuffer.path().equals(activePath)) {
tabButton.getStyleClass().add("editor-workspace-tab-button-active"); tabButton.getStyleClass().add("editor-workspace-tab-button-active");
} }
tabButton.setOnAction(event -> tabSelectionAction.accept(fileBuffer.path())); tabButton.setOnAction(event -> tabSelectionAction.accept(fileBuffer.path()));
configureCloseChip(closeChip, fileBuffer.path());
getChildren().add(tabButton); getChildren().add(tabButton);
} }
@ -151,15 +180,23 @@ public final class EditorTabStrip extends HBox {
return Math.max(1, (int) Math.floor(usableWidth / TAB_WIDTH_HINT)); return Math.max(1, (int) Math.floor(usableWidth / TAB_WIDTH_HINT));
} }
private void applyTabMetrics(final Button button) { private void applyTabMetrics(final Button button, final Label tabLabel, final StackPane closeChip) {
button.setMinWidth(TAB_WIDTH_HINT); button.setMinWidth(TAB_WIDTH_HINT);
button.setPrefWidth(TAB_WIDTH_HINT); button.setPrefWidth(TAB_WIDTH_HINT);
button.setMaxWidth(TAB_WIDTH_HINT); button.setMaxWidth(TAB_WIDTH_HINT);
button.setMinHeight(TAB_HEIGHT_HINT); button.setMinHeight(TAB_HEIGHT_HINT);
button.setPrefHeight(TAB_HEIGHT_HINT); button.setPrefHeight(TAB_HEIGHT_HINT);
button.setMaxHeight(TAB_HEIGHT_HINT); button.setMaxHeight(TAB_HEIGHT_HINT);
button.setTextOverrun(OverrunStyle.ELLIPSIS);
button.setMnemonicParsing(false); button.setMnemonicParsing(false);
tabLabel.setMinWidth(0);
tabLabel.setPrefWidth(TAB_WIDTH_HINT - 30);
tabLabel.setMaxWidth(Double.MAX_VALUE);
closeChip.setMinWidth(14);
closeChip.setPrefWidth(14);
closeChip.setMaxWidth(14);
closeChip.setMinHeight(14);
closeChip.setPrefHeight(14);
closeChip.setMaxHeight(14);
} }
private void applyOverflowMetrics() { private void applyOverflowMetrics() {
@ -171,4 +208,31 @@ public final class EditorTabStrip extends HBox {
overflowButton.setMaxHeight(TAB_HEIGHT_HINT); overflowButton.setMaxHeight(TAB_HEIGHT_HINT);
overflowButton.setMnemonicParsing(false); overflowButton.setMnemonicParsing(false);
} }
private void configureCloseChip(final StackPane closeChip, final Path filePath) {
closeChip.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
event.consume();
if (event.getButton() == MouseButton.PRIMARY && closeChip.contains(event.getX(), event.getY())) {
closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, true);
}
});
closeChip.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
event.consume();
closeChip.pseudoClassStateChanged(
PRESSED_PSEUDO_CLASS,
event.isPrimaryButtonDown() && closeChip.contains(event.getX(), event.getY()));
});
closeChip.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {
event.consume();
final boolean shouldClose = event.getButton() == MouseButton.PRIMARY
&& closeChip.getPseudoClassStates().contains(PRESSED_PSEUDO_CLASS)
&& closeChip.contains(event.getX(), event.getY());
closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, false);
if (shouldClose) {
tabCloseAction.accept(filePath);
}
});
closeChip.addEventFilter(MouseEvent.MOUSE_EXITED, event ->
closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, false));
}
} }

View File

@ -123,6 +123,7 @@ public final class EditorWorkspace extends Workspace {
notifyStateChanged(); notifyStateChanged();
renderSession(); renderSession();
}); });
tabStrip.setTabCloseAction(this::requestCloseFile);
root.setCenter(buildLayout()); root.setCenter(buildLayout());
statusBar.showPlaceholder(presentationRegistry.resolve("text")); statusBar.showPlaceholder(presentationRegistry.resolve("text"));
@ -449,6 +450,51 @@ public final class EditorWorkspace extends Workspace {
renderSession(); renderSession();
} }
private void requestCloseFile(final Path path) {
final var fileBuffer = openFileSession.file(path).orElse(null);
if (fileBuffer == null) {
return;
}
if (fileBuffer.dirty() && !confirmDirtyFileClose(fileBuffer)) {
return;
}
openFileSession.close(path);
notifyStateChanged();
renderSession();
}
private boolean confirmDirtyFileClose(final EditorOpenFileBuffer fileBuffer) {
final var alert = new Alert(Alert.AlertType.CONFIRMATION);
if (root.getScene() != null) {
alert.initOwner(root.getScene().getWindow());
}
final var saveButtonType = new ButtonType(
p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_SAVE),
ButtonBar.ButtonData.YES);
final var discardButtonType = new ButtonType(
p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_DISCARD),
ButtonBar.ButtonData.NO);
final var cancelButtonType = new ButtonType(
p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_CANCEL),
ButtonBar.ButtonData.CANCEL_CLOSE);
alert.setTitle(p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_TITLE));
alert.setHeaderText(null);
alert.setContentText(p.studio.Container.i18n().format(
I18n.CODE_EDITOR_CLOSE_DIRTY_MESSAGE,
fileBuffer.tabLabel()));
alert.getButtonTypes().setAll(saveButtonType, discardButtonType, cancelButtonType);
final var result = alert.showAndWait().orElse(cancelButtonType);
if (result == saveButtonType) {
vfsProjectDocument.saveDocument(fileBuffer.path());
return true;
}
if (result == discardButtonType) {
vfsProjectDocument.discardDocument(fileBuffer.path());
return true;
}
return false;
}
private void handleWorkspaceShortcuts(final KeyEvent event) { private void handleWorkspaceShortcuts(final KeyEvent event) {
if (SAVE_ALL_SHORTCUT.match(event)) { if (SAVE_ALL_SHORTCUT.match(event)) {
if (!saveAllButton.isDisabled()) { if (!saveAllButton.isDisabled()) {

View File

@ -563,22 +563,6 @@
-fx-border-color: #2a313c; -fx-border-color: #2a313c;
} }
.editor-workspace-tab {
-fx-background-color: #11151b;
-fx-background-radius: 9;
-fx-border-radius: 9;
-fx-border-color: #2a313c;
-fx-padding: 8 12 8 12;
-fx-text-fill: #c5d2de;
-fx-font-size: 12px;
}
.editor-workspace-tab-active {
-fx-background-color: #223246;
-fx-border-color: #4a86be;
-fx-text-fill: #eef6ff;
}
.editor-workspace-code-area { .editor-workspace-code-area {
-fx-background-color: #171c22; -fx-background-color: #171c22;
-fx-font-size: 15px; -fx-font-size: 15px;
@ -589,7 +573,7 @@
.editor-workspace-tab-button { .editor-workspace-tab-button {
-fx-background-radius: 0; -fx-background-radius: 0;
-fx-border-radius: 0; -fx-border-radius: 0;
-fx-padding: 0 12 0 12; -fx-padding: 0 8 0 12;
-fx-alignment: center-left; -fx-alignment: center-left;
-fx-font-size: 12px; -fx-font-size: 12px;
-fx-background-color: #20262f; -fx-background-color: #20262f;
@ -598,6 +582,33 @@
-fx-text-fill: #d6dde6; -fx-text-fill: #d6dde6;
} }
.editor-workspace-tab-content {
-fx-spacing: 8;
-fx-alignment: center-left;
}
.editor-workspace-tab-label {
-fx-alignment: center-left;
-fx-font-size: 12px;
}
.editor-workspace-tab-close-chip {
-fx-alignment: center;
-fx-background-color: #131820;
-fx-background-radius: 999;
-fx-border-color: #485667;
-fx-border-radius: 999;
-fx-border-width: 1;
-fx-padding: 0;
-fx-cursor: hand;
}
.editor-workspace-tab-close-icon {
-fx-fill: transparent;
-fx-stroke: #cfd8e2;
-fx-stroke-width: 1.1;
}
.editor-workspace-tab-button-active { .editor-workspace-tab-button-active {
-fx-background-color: #16283d; -fx-background-color: #16283d;
-fx-border-color: #8fc4f2 #516579 #516579 #516579; -fx-border-color: #8fc4f2 #516579 #516579 #516579;
@ -612,12 +623,48 @@
-fx-text-fill: #d9dee5; -fx-text-fill: #d9dee5;
} }
.editor-workspace-tab-button-read-only .editor-workspace-tab-label {
-fx-text-fill: #d9dee5;
}
.editor-workspace-tab-button-read-only .editor-workspace-tab-close-icon {
-fx-stroke: #d9dee5;
}
.editor-workspace-tab-button-read-only .editor-workspace-tab-close-chip {
-fx-background-color: #171c23;
-fx-border-color: #4f5b68;
}
.editor-workspace-tab-button-read-only:hover { .editor-workspace-tab-button-read-only:hover {
-fx-background-color: #2b323d; -fx-background-color: #2b323d;
-fx-border-color: #5b6878; -fx-border-color: #5b6878;
-fx-text-fill: #eff4fa; -fx-text-fill: #eff4fa;
} }
.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-label {
-fx-text-fill: #eff4fa;
}
.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip {
-fx-background-color: #1c232c;
-fx-border-color: #6b7989;
}
.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip:hover {
-fx-background-color: #2a3440;
-fx-border-color: #90a2b5;
}
.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip:hover .editor-workspace-tab-close-icon {
-fx-stroke: #f7fbff;
}
.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip:pressed {
-fx-background-color: #364351;
-fx-border-color: #a8bbce;
}
.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active { .editor-workspace-tab-button-read-only.editor-workspace-tab-button-active {
-fx-background-color: #16283d; -fx-background-color: #16283d;
-fx-border-color: #8fc4f2 #516579 #516579 #516579; -fx-border-color: #8fc4f2 #516579 #516579 #516579;
@ -637,12 +684,48 @@
-fx-text-fill: #e8f6eb; -fx-text-fill: #e8f6eb;
} }
.editor-workspace-tab-button-editable .editor-workspace-tab-label {
-fx-text-fill: #e8f6eb;
}
.editor-workspace-tab-button-editable .editor-workspace-tab-close-icon {
-fx-stroke: #e8f6eb;
}
.editor-workspace-tab-button-editable .editor-workspace-tab-close-chip {
-fx-background-color: #15211a;
-fx-border-color: #557363;
}
.editor-workspace-tab-button-editable:hover { .editor-workspace-tab-button-editable:hover {
-fx-background-color: #29412f; -fx-background-color: #29412f;
-fx-border-color: #6f957a; -fx-border-color: #6f957a;
-fx-text-fill: #f4fff5; -fx-text-fill: #f4fff5;
} }
.editor-workspace-tab-button-editable:hover .editor-workspace-tab-label {
-fx-text-fill: #f4fff5;
}
.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip {
-fx-background-color: #192720;
-fx-border-color: #789886;
}
.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip:hover {
-fx-background-color: #27382d;
-fx-border-color: #9fc0ac;
}
.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip:hover .editor-workspace-tab-close-icon {
-fx-stroke: #ffffff;
}
.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip:pressed {
-fx-background-color: #314739;
-fx-border-color: #b2d2bf;
}
.editor-workspace-tab-button-editable.editor-workspace-tab-button-active { .editor-workspace-tab-button-editable.editor-workspace-tab-button-active {
-fx-background-color: #1d3a2a; -fx-background-color: #1d3a2a;
-fx-border-color: #8ad3a2 #587464 #587464 #587464; -fx-border-color: #8ad3a2 #587464 #587464 #587464;
@ -651,6 +734,30 @@
-fx-font-weight: bold; -fx-font-weight: bold;
} }
.editor-workspace-tab-button-active .editor-workspace-tab-label {
-fx-text-fill: #ffffff;
-fx-font-weight: bold;
}
.editor-workspace-tab-button-active .editor-workspace-tab-close-icon {
-fx-stroke: #ffffff;
}
.editor-workspace-tab-button-active .editor-workspace-tab-close-chip {
-fx-background-color: rgba(255, 255, 255, 0.08);
-fx-border-color: rgba(255, 255, 255, 0.22);
}
.editor-workspace-tab-button-active:hover .editor-workspace-tab-close-chip:hover {
-fx-background-color: rgba(255, 255, 255, 0.18);
-fx-border-color: rgba(255, 255, 255, 0.42);
}
.editor-workspace-tab-button-active:hover .editor-workspace-tab-close-chip:pressed {
-fx-background-color: rgba(255, 255, 255, 0.28);
-fx-border-color: rgba(255, 255, 255, 0.56);
}
.editor-workspace-tab-button-editable.editor-workspace-tab-button-active:hover { .editor-workspace-tab-button-editable.editor-workspace-tab-button-active:hover {
-fx-background-color: #234532; -fx-background-color: #234532;
-fx-border-color: #a5efbd #688676 #688676 #688676; -fx-border-color: #a5efbd #688676 #688676 #688676;
@ -1850,7 +1957,3 @@
-fx-text-fill: #b9cae0; -fx-text-fill: #b9cae0;
-fx-font-size: 12px; -fx-font-size: 12px;
} }
.editor-workspace-tab-label.editor-workspace-tab-button-editable.editor-workspace-tab-button-active {
-fx-text-fill: #ffffff;
-fx-font-weight: bold;
}