diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java index cac22323..668ac2a3 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java @@ -1,5 +1,6 @@ package p.studio.workspaces.editor; +import javafx.css.PseudoClass; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.CustomMenuItem; @@ -7,9 +8,13 @@ import javafx.scene.control.Label; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.control.OverrunStyle; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.SVGPath; import p.studio.Container; import p.studio.utilities.i18n.I18n; @@ -20,6 +25,7 @@ import java.util.Objects; import java.util.function.Consumer; 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_HEIGHT_HINT = 34.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 List openFiles = new ArrayList<>(); private Consumer tabSelectionAction = path -> { }; + private Consumer tabCloseAction = path -> { }; private Path activePath; public EditorTabStrip() { @@ -56,6 +63,10 @@ public final class EditorTabStrip extends HBox { this.tabSelectionAction = Objects.requireNonNull(tabSelectionAction, "tabSelectionAction"); } + public void setTabCloseAction(final Consumer tabCloseAction) { + this.tabCloseAction = Objects.requireNonNull(tabCloseAction, "tabCloseAction"); + } + public void showOpenFiles(final List files, final Path activePath) { this.openFiles.clear(); this.openFiles.addAll(Objects.requireNonNull(files, "files")); @@ -81,6 +92,10 @@ public final class EditorTabStrip extends HBox { for (int index = startIndex; index < endIndex; index++) { final var fileBuffer = openFiles.get(index); 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.getStyleClass().addAll( "studio-button", @@ -90,11 +105,25 @@ public final class EditorTabStrip extends HBox { fileBuffer.readOnly() ? "editor-workspace-tab-button-read-only" : "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)) { tabButton.getStyleClass().add("editor-workspace-tab-button-active"); } tabButton.setOnAction(event -> tabSelectionAction.accept(fileBuffer.path())); + configureCloseChip(closeChip, fileBuffer.path()); getChildren().add(tabButton); } @@ -151,15 +180,23 @@ public final class EditorTabStrip extends HBox { 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.setPrefWidth(TAB_WIDTH_HINT); button.setMaxWidth(TAB_WIDTH_HINT); button.setMinHeight(TAB_HEIGHT_HINT); button.setPrefHeight(TAB_HEIGHT_HINT); button.setMaxHeight(TAB_HEIGHT_HINT); - button.setTextOverrun(OverrunStyle.ELLIPSIS); 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() { @@ -171,4 +208,31 @@ public final class EditorTabStrip extends HBox { overflowButton.setMaxHeight(TAB_HEIGHT_HINT); 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)); + } } 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 4cff96f3..9e1a1dd4 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 @@ -123,6 +123,7 @@ public final class EditorWorkspace extends Workspace { notifyStateChanged(); renderSession(); }); + tabStrip.setTabCloseAction(this::requestCloseFile); root.setCenter(buildLayout()); statusBar.showPlaceholder(presentationRegistry.resolve("text")); @@ -449,6 +450,51 @@ public final class EditorWorkspace extends Workspace { 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) { if (SAVE_ALL_SHORTCUT.match(event)) { if (!saveAllButton.isDisabled()) { diff --git a/prometeu-studio/src/main/resources/themes/default-prometeu.css b/prometeu-studio/src/main/resources/themes/default-prometeu.css index 9890ba2e..4a634a1d 100644 --- a/prometeu-studio/src/main/resources/themes/default-prometeu.css +++ b/prometeu-studio/src/main/resources/themes/default-prometeu.css @@ -563,22 +563,6 @@ -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 { -fx-background-color: #171c22; -fx-font-size: 15px; @@ -589,7 +573,7 @@ .editor-workspace-tab-button { -fx-background-radius: 0; -fx-border-radius: 0; - -fx-padding: 0 12 0 12; + -fx-padding: 0 8 0 12; -fx-alignment: center-left; -fx-font-size: 12px; -fx-background-color: #20262f; @@ -598,6 +582,33 @@ -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 { -fx-background-color: #16283d; -fx-border-color: #8fc4f2 #516579 #516579 #516579; @@ -612,12 +623,48 @@ -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 { -fx-background-color: #2b323d; -fx-border-color: #5b6878; -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 { -fx-background-color: #16283d; -fx-border-color: #8fc4f2 #516579 #516579 #516579; @@ -637,12 +684,48 @@ -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 { -fx-background-color: #29412f; -fx-border-color: #6f957a; -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 { -fx-background-color: #1d3a2a; -fx-border-color: #8ad3a2 #587464 #587464 #587464; @@ -651,6 +734,30 @@ -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 { -fx-background-color: #234532; -fx-border-color: #a5efbd #688676 #688676 #688676; @@ -1850,7 +1957,3 @@ -fx-text-fill: #b9cae0; -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; -}