From 5babec2ad6f8abd893bd7f39ff48fe55602a0f5a Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Thu, 29 May 2025 00:24:14 -0400 Subject: [PATCH 01/50] feat: add initial implementation of walkthrough --- .../main/java/org/jabref/gui/JabRefGUI.java | 14 +- .../util/component/PulseAnimateIndicator.java | 172 +++++++++++++++ .../gui/walkthrough/WalkthroughManager.java | 204 ++++++++++++++++++ .../gui/walkthrough/WalkthroughOverlay.java | 190 ++++++++++++++++ .../gui/walkthrough/WalkthroughUIFactory.java | 188 ++++++++++++++++ .../declarative/InfoBlockContentBlock.java | 22 ++ .../declarative/NodeResolverFactory.java | 113 ++++++++++ .../gui/walkthrough/declarative/StepType.java | 31 +++ .../declarative/TextContentBlock.java | 21 ++ .../declarative/WalkthroughActionsConfig.java | 17 ++ .../declarative/WalkthroughContentBlock.java | 13 ++ .../declarative/WalkthroughStep.java | 36 ++++ .../main/resources/org/jabref/gui/Base.css | 96 +++++++++ .../logic/preferences/CliPreferences.java | 2 + .../preferences/JabRefCliPreferences.java | 20 ++ .../preferences/WalkthroughPreferences.java | 27 +++ .../main/resources/l10n/JabRef_en.properties | 23 ++ 17 files changed, 1188 insertions(+), 1 deletion(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughManager.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/StepType.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java create mode 100644 jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index 21aed8e8bfa..53d9f7cb2ae 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -28,6 +28,7 @@ import org.jabref.gui.util.DirectoryMonitor; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.gui.util.WebViewStore; +import org.jabref.gui.walkthrough.WalkthroughManager; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; import org.jabref.logic.journals.JournalAbbreviationLoader; @@ -35,6 +36,7 @@ import org.jabref.logic.l10n.Localization; import org.jabref.logic.net.ProxyRegisterer; import org.jabref.logic.os.OS; +import org.jabref.logic.preferences.WalkthroughPreferences; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.server.RemoteListenerServerManager; @@ -75,7 +77,7 @@ public class JabRefGUI extends Application { private static ClipBoardManager clipBoardManager; private static DialogService dialogService; private static JabRefFrame mainFrame; - + private static WalkthroughManager walkthroughManager; private static RemoteListenerServerManager remoteListenerServerManager; private Stage mainStage; @@ -187,6 +189,11 @@ public void initialize() { dialogService, taskExecutor); Injector.setModelOrService(AiService.class, aiService); + + // Initialize walkthrough manager + WalkthroughPreferences walkthroughPreferences = preferences.getWalkthroughPreferences(); + JabRefGUI.walkthroughManager = new WalkthroughManager(walkthroughPreferences); + Injector.setModelOrService(WalkthroughManager.class, walkthroughManager); } private void setupProxy() { @@ -295,6 +302,11 @@ public void onShowing(WindowEvent event) { if (stateManager.getOpenDatabases().isEmpty()) { mainFrame.showWelcomeTab(); } + + // Check if walkthrough should be shown + if (!walkthroughManager.isCompleted()) { + walkthroughManager.start(mainStage); + } }); } diff --git a/jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java b/jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java new file mode 100644 index 00000000000..9766b6ebd1d --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java @@ -0,0 +1,172 @@ +package org.jabref.gui.util.component; + +import java.util.ArrayList; +import java.util.List; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.value.ChangeListener; +import javafx.geometry.Bounds; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.util.Duration; + +/** + * A pulsing circular indicator that can be attached to a target node. + */ +public class PulseAnimateIndicator { + private Circle pulseIndicator; + private Timeline pulseAnimation; + private final Pane rootPane; + private Node attachedNode; + + private final List cleanupTasks = new ArrayList<>(); + private final ChangeListener updatePositionListener = (_, _, _) -> updatePosition(); + + public PulseAnimateIndicator(Pane rootPane) { + this.rootPane = rootPane; + } + + public void attachToNode(Node targetNode) { + stop(); + + this.attachedNode = targetNode; + setupIndicator(); + setupListeners(); + updatePosition(); + startAnimation(); + } + + private void setupIndicator() { + pulseIndicator = new Circle(8, Color.web("#50618F")); + pulseIndicator.setMouseTransparent(true); + pulseIndicator.setManaged(false); + + if (!rootPane.getChildren().contains(pulseIndicator)) { + rootPane.getChildren().add(pulseIndicator); + } + pulseIndicator.toFront(); + } + + private void setupListeners() { + attachedNode.boundsInLocalProperty().addListener(updatePositionListener); + cleanupTasks.add(() -> attachedNode.boundsInLocalProperty().removeListener(updatePositionListener)); + + attachedNode.localToSceneTransformProperty().addListener(updatePositionListener); + cleanupTasks.add(() -> attachedNode.localToSceneTransformProperty().removeListener(updatePositionListener)); + + ChangeListener sceneListener = (_, oldScene, newScene) -> { + if (oldScene != null) { + oldScene.widthProperty().removeListener(updatePositionListener); + oldScene.heightProperty().removeListener(updatePositionListener); + } + if (newScene != null) { + newScene.widthProperty().addListener(updatePositionListener); + newScene.heightProperty().addListener(updatePositionListener); + cleanupTasks.add(() -> { + newScene.widthProperty().removeListener(updatePositionListener); + newScene.heightProperty().removeListener(updatePositionListener); + }); + } + updatePosition(); + }; + + attachedNode.sceneProperty().addListener(sceneListener); + cleanupTasks.add(() -> attachedNode.sceneProperty().removeListener(sceneListener)); + + if (attachedNode.getScene() != null) { + sceneListener.changed(null, null, attachedNode.getScene()); + } + } + + private void startAnimation() { + pulseAnimation = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(pulseIndicator.opacityProperty(), 1.0), + new KeyValue(pulseIndicator.scaleXProperty(), 1.0), + new KeyValue(pulseIndicator.scaleYProperty(), 1.0)), + new KeyFrame(Duration.seconds(0.5), + new KeyValue(pulseIndicator.opacityProperty(), 0.6), + new KeyValue(pulseIndicator.scaleXProperty(), 1.3), + new KeyValue(pulseIndicator.scaleYProperty(), 1.3)), + new KeyFrame(Duration.seconds(1.0), + new KeyValue(pulseIndicator.opacityProperty(), 1.0), + new KeyValue(pulseIndicator.scaleXProperty(), 1.0), + new KeyValue(pulseIndicator.scaleYProperty(), 1.0)) + ); + pulseAnimation.setCycleCount(Timeline.INDEFINITE); + pulseAnimation.play(); + } + + private void updatePosition() { + if (pulseIndicator == null || attachedNode == null || !isNodeVisible()) { + setIndicatorVisible(false); + return; + } + + Bounds localBounds = attachedNode.getBoundsInLocal(); + if (localBounds.isEmpty()) { + setIndicatorVisible(false); + return; + } + + Bounds targetBoundsInScene = attachedNode.localToScene(localBounds); + if (targetBoundsInScene == null || rootPane.getScene() == null) { + setIndicatorVisible(false); + return; + } + + Bounds targetBoundsInRoot = rootPane.sceneToLocal(targetBoundsInScene); + if (targetBoundsInRoot == null) { + setIndicatorVisible(false); + return; + } + + setIndicatorVisible(true); + positionIndicator(targetBoundsInRoot); + } + + // FIXME: This check is still fail for some cases + private boolean isNodeVisible() { + return attachedNode.isVisible() && + attachedNode.getScene() != null && + attachedNode.getLayoutBounds().getWidth() > 0 && + attachedNode.getLayoutBounds().getHeight() > 0; + } + + private void setIndicatorVisible(boolean visible) { + if (pulseIndicator != null) { + pulseIndicator.setVisible(visible); + } + } + + private void positionIndicator(Bounds targetBounds) { + double indicatorX = targetBounds.getMaxX() - 5; + double indicatorY = targetBounds.getMinY() + 5; + + pulseIndicator.setLayoutX(indicatorX); + pulseIndicator.setLayoutY(indicatorY); + pulseIndicator.toFront(); + } + + public void stop() { + if (pulseAnimation != null) { + pulseAnimation.stop(); + pulseAnimation = null; + } + + if (pulseIndicator != null && pulseIndicator.getParent() instanceof Pane parentPane) { + parentPane.getChildren().remove(pulseIndicator); + pulseIndicator = null; + } + + cleanupTasks.forEach(Runnable::run); + cleanupTasks.clear(); + + attachedNode = null; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughManager.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughManager.java new file mode 100644 index 00000000000..32f46b9d183 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughManager.java @@ -0,0 +1,204 @@ +package org.jabref.gui.walkthrough; + +import java.util.List; +import java.util.Optional; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.stage.Stage; + +import org.jabref.gui.actions.StandardActions; +import org.jabref.gui.walkthrough.declarative.InfoBlockContentBlock; +import org.jabref.gui.walkthrough.declarative.NodeResolverFactory; +import org.jabref.gui.walkthrough.declarative.StepType; +import org.jabref.gui.walkthrough.declarative.TextContentBlock; +import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.WalkthroughStep; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.preferences.WalkthroughPreferences; + +/** + * Manages a walkthrough session by coordinating steps. + */ +public class WalkthroughManager { + private final WalkthroughPreferences preferences; + private final IntegerProperty currentStep; + private final IntegerProperty totalSteps; + private final BooleanProperty active; + private final WalkthroughStep[] steps; + private WalkthroughOverlay overlay; + private Stage currentStage; + + /** + * Creates a new walkthrough manager with the specified preferences. + * + * @param preferences The walkthrough preferences to use + */ + public WalkthroughManager(WalkthroughPreferences preferences) { + this.preferences = preferences; + this.currentStep = new SimpleIntegerProperty(0); + this.active = new SimpleBooleanProperty(false); + + this.steps = new WalkthroughStep[] { + new WalkthroughStep( + Localization.lang("Walkthrough welcome title"), + StepType.FULL_SCREEN, + List.of( + new TextContentBlock(Localization.lang("Walkthrough welcome intro")), + new InfoBlockContentBlock(Localization.lang("Walkthrough welcome tip"))), + new WalkthroughActionsConfig(Optional.of("Start walkthrough"), + Optional.of("Skip to finish"), Optional.empty())), + + new WalkthroughStep( + Localization.lang("Walkthrough create entry title"), + StepType.BOTTOM_PANEL, + List.of( + new TextContentBlock(Localization.lang("Walkthrough create entry description")), + new InfoBlockContentBlock(Localization.lang("Walkthrough create entry tip"))), + NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY) + ), + + new WalkthroughStep( + Localization.lang("Walkthrough save title"), + StepType.RIGHT_PANEL, + List.of( + new TextContentBlock(Localization.lang("Walkthrough save description")), + new InfoBlockContentBlock(Localization.lang("Walkthrough save important"))), + NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)), + + new WalkthroughStep( + Localization.lang("Walkthrough completion title"), + StepType.FULL_SCREEN, + List.of( + new TextContentBlock(Localization.lang("Walkthrough completion message")), + new TextContentBlock(Localization.lang("Walkthrough completion next_steps")), + new InfoBlockContentBlock(Localization.lang("Walkthrough completion resources"))), + new WalkthroughActionsConfig(Optional.of("Complete walkthrough"), Optional.empty(), + Optional.of("Back"))) + }; + + this.totalSteps = new SimpleIntegerProperty(steps.length); + } + + /** + * Gets the current step index property. + * + * @return The current step index property. + */ + public ReadOnlyIntegerProperty currentStepProperty() { + return currentStep; + } + + /** + * Gets the total number of steps property. + * + * @return The total steps property + */ + public ReadOnlyIntegerProperty totalStepsProperty() { + return totalSteps; + } + + /** + * Checks if the walkthrough is completed based on preferences. + * + * @return true if the walkthrough has been completed + */ + public boolean isCompleted() { + return preferences.isCompleted(); + } + + /** + * Starts the walkthrough from the first step. + * + * @param stage The stage to display the walkthrough on + */ + public void start(Stage stage) { + if (preferences.isCompleted()) { + return; + } + + if (currentStage != stage || overlay == null) { + if (overlay != null) { + overlay.detach(); + } + currentStage = stage; + overlay = new WalkthroughOverlay(stage, this); + } + + currentStep.set(0); + active.set(true); + overlay.displayStep(getCurrentStep()); + } + + /** + * Moves to the next step in the walkthrough. + */ + public void nextStep() { + int nextIndex = currentStep.get() + 1; + if (nextIndex < steps.length) { + currentStep.set(nextIndex); + if (overlay != null) { + overlay.displayStep(getCurrentStep()); + } + } else { + preferences.setCompleted(true); + stop(); + } + } + + /** + * Moves to the next step in the walkthrough with stage switching. + * This method handles stage changes by recreating the overlay on the new stage. + * + * @param stage The stage to display the next step on + */ + public void nextStep(Stage stage) { + if (currentStage != stage) { + if (overlay != null) { + overlay.detach(); + } + currentStage = stage; + overlay = new WalkthroughOverlay(stage, this); + } + nextStep(); + } + + /** + * Moves to the previous step in the walkthrough. + */ + public void previousStep() { + int prevIndex = currentStep.get() - 1; + if (prevIndex >= 0) { + currentStep.set(prevIndex); + if (overlay != null) { + overlay.displayStep(getCurrentStep()); + } + } + } + + /** + * Skips the walkthrough completely. + */ + public void skip() { + preferences.setCompleted(true); + stop(); + } + + private void stop() { + if (overlay != null) { + overlay.detach(); + } + active.set(false); + } + + private WalkthroughStep getCurrentStep() { + int index = currentStep.get(); + if (index >= 0 && index < steps.length) { + return steps[index]; + } + return null; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java new file mode 100644 index 00000000000..8c911295293 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -0,0 +1,190 @@ +package org.jabref.gui.walkthrough; + +import java.util.Optional; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import org.jabref.gui.util.component.PulseAnimateIndicator; +import org.jabref.gui.walkthrough.declarative.WalkthroughStep; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Display a walkthrough overlay on top of the main application window. + */ +public class WalkthroughOverlay { + private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); + private final Stage parentStage; + private final WalkthroughManager manager; + private final GridPane overlayPane; + private final PulseAnimateIndicator pulseIndicator; + private final Pane originalRoot; + private final StackPane stackContainer; + + public WalkthroughOverlay(Stage stage, WalkthroughManager manager) { + this.parentStage = stage; + this.manager = manager; + + overlayPane = new GridPane(); + overlayPane.setStyle("-fx-background-color: transparent;"); + overlayPane.setVisible(false); + overlayPane.setMaxWidth(Double.MAX_VALUE); + overlayPane.setMaxHeight(Double.MAX_VALUE); + + Scene scene = stage.getScene(); + if (scene == null) { + LOGGER.error("Parent stage's scene must not be null to initialize WalkthroughOverlay."); + throw new IllegalStateException("Parent stage's scene must not be null"); + } + + originalRoot = (Pane) scene.getRoot(); + stackContainer = new StackPane(); + + stackContainer.getChildren().add(originalRoot); + stackContainer.getChildren().add(overlayPane); + pulseIndicator = new PulseAnimateIndicator(stackContainer); + + scene.setRoot(stackContainer); + } + + public void displayStep(WalkthroughStep step) { + if (step == null) { + hide(); + return; + } + + show(); + + pulseIndicator.stop(); + overlayPane.getChildren().clear(); + + switch (step.stepType()) { + case FULL_SCREEN: + displayFullScreenStep(step); + break; + case LEFT_PANEL: + displayPanelStep(step, Pos.CENTER_LEFT); + break; + case RIGHT_PANEL: + displayPanelStep(step, Pos.CENTER_RIGHT); + break; + case TOP_PANEL: + displayPanelStep(step, Pos.TOP_CENTER); + break; + case BOTTOM_PANEL: + displayPanelStep(step, Pos.BOTTOM_CENTER); + break; + } + } + + private void displayFullScreenStep(WalkthroughStep step) { + Node fullScreenContent = WalkthroughUIFactory.createFullscreen(step, manager); + overlayPane.getChildren().clear(); + overlayPane.getChildren().add(fullScreenContent); + + overlayPane.getRowConstraints().clear(); + overlayPane.getColumnConstraints().clear(); + RowConstraints rc = new RowConstraints(); + rc.setVgrow(Priority.ALWAYS); + overlayPane.getRowConstraints().add(rc); + ColumnConstraints cc = new ColumnConstraints(); + cc.setHgrow(Priority.ALWAYS); + overlayPane.getColumnConstraints().add(cc); + + GridPane.setHgrow(fullScreenContent, Priority.ALWAYS); + GridPane.setVgrow(fullScreenContent, Priority.ALWAYS); + GridPane.setFillWidth(fullScreenContent, true); + GridPane.setFillHeight(fullScreenContent, true); + + overlayPane.setAlignment(Pos.CENTER); + overlayPane.setVisible(true); + } + + private void displayPanelStep(WalkthroughStep step, Pos position) { + Node panelContent = WalkthroughUIFactory.createSidePanel(step, manager); + overlayPane.getChildren().clear(); + overlayPane.getChildren().add(panelContent); + panelContent.setMouseTransparent(false); + + overlayPane.getRowConstraints().clear(); + overlayPane.getColumnConstraints().clear(); + + GridPane.setHgrow(panelContent, Priority.NEVER); + GridPane.setVgrow(panelContent, Priority.NEVER); + GridPane.setFillWidth(panelContent, false); + GridPane.setFillHeight(panelContent, false); + + RowConstraints rc = new RowConstraints(); + ColumnConstraints cc = new ColumnConstraints(); + + overlayPane.setAlignment(position); + + switch (position) { + case CENTER_LEFT: + case CENTER_RIGHT: + rc.setVgrow(Priority.ALWAYS); + cc.setHgrow(Priority.NEVER); + GridPane.setFillHeight(panelContent, true); + break; + case TOP_CENTER: + case BOTTOM_CENTER: + cc.setHgrow(Priority.ALWAYS); + rc.setVgrow(Priority.NEVER); + GridPane.setFillWidth(panelContent, true); + break; + default: + LOGGER.warn("Unsupported position for panel step: {}", position); + break; + } + + overlayPane.getRowConstraints().add(rc); + overlayPane.getColumnConstraints().add(cc); + overlayPane.setVisible(true); + + if (step.resolver().isPresent()) { + Optional targetNodeOpt = step.resolver().get().apply(parentStage.getScene()); + if (targetNodeOpt.isPresent()) { + Node targetNode = targetNodeOpt.get(); + pulseIndicator.attachToNode(targetNode); + } else { + LOGGER.warn("Could not resolve target node for step: {}", step.title()); + } + } + } + + /** + * Detaches the overlay and cleans up resources. + */ + public void detach() { + pulseIndicator.stop(); + + overlayPane.setVisible(false); + overlayPane.getChildren().clear(); + + Scene scene = parentStage.getScene(); + if (scene != null && originalRoot != null) { + stackContainer.getChildren().remove(originalRoot); + scene.setRoot(originalRoot); + LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); + } + } + + private void show() { + overlayPane.setVisible(true); + overlayPane.toFront(); + } + + private void hide() { + overlayPane.setVisible(false); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java new file mode 100644 index 00000000000..803884cd39b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java @@ -0,0 +1,188 @@ +package org.jabref.gui.walkthrough; + +import javafx.geometry.Pos; +import javafx.scene.Node; +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 javafx.scene.layout.VBox; + +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.icon.JabRefIconView; +import org.jabref.gui.walkthrough.declarative.InfoBlockContentBlock; +import org.jabref.gui.walkthrough.declarative.StepType; +import org.jabref.gui.walkthrough.declarative.TextContentBlock; +import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.WalkthroughContentBlock; +import org.jabref.gui.walkthrough.declarative.WalkthroughStep; +import org.jabref.logic.l10n.Localization; + +/** + * Factory for creating walkthrough UI components. + */ +public class WalkthroughUIFactory { + /** + * Creates a full-screen page using dynamic content from a walkthrough + * step. + */ + public static VBox createFullscreen(WalkthroughStep step, WalkthroughManager manager) { + VBox container = makePanel(); + container.setAlignment(Pos.CENTER); + VBox content = new VBox(); + content.getStyleClass().add("walkthrough-fullscreen-content"); + Label titleLabel = new Label(Localization.lang(step.title())); + titleLabel.getStyleClass().add("walkthrough-title"); + VBox contentContainer = makeContent(step); + content.getChildren().addAll(titleLabel, contentContainer, makeActions(step, manager)); + container.getChildren().add(content); + return container; + } + + /** + * Creates a side panel for walkthrough steps. + */ + public static VBox createSidePanel(WalkthroughStep step, WalkthroughManager manager) { + VBox panel = makePanel(); + + if (step.stepType() == StepType.LEFT_PANEL || step.stepType() == StepType.RIGHT_PANEL) { + panel.getStyleClass().add("walkthrough-side-panel-vertical"); + VBox.setVgrow(panel, Priority.ALWAYS); + panel.setMaxHeight(Double.MAX_VALUE); + } else if (step.stepType() == StepType.TOP_PANEL || step.stepType() == StepType.BOTTOM_PANEL) { + panel.getStyleClass().add("walkthrough-side-panel-horizontal"); + HBox.setHgrow(panel, Priority.ALWAYS); + panel.setMaxWidth(Double.MAX_VALUE); + } + + HBox header = new HBox(); + header.setAlignment(Pos.CENTER_LEFT); + + Label titleLabel = new Label(Localization.lang(step.title())); + titleLabel.getStyleClass().add("walkthrough-title"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + Label stepCounter = new Label(Localization.lang("Step of", + String.valueOf(manager.currentStepProperty().get() + 1), + String.valueOf(manager.totalStepsProperty().get()))); + stepCounter.getStyleClass().add("walkthrough-step-counter"); + + header.getChildren().addAll(titleLabel, spacer, stepCounter); + + VBox contentContainer = makeContent(step); + Region bottomSpacer = new Region(); + VBox.setVgrow(bottomSpacer, Priority.ALWAYS); + HBox actions = makeActions(step, manager); + panel.getChildren().addAll(header, contentContainer, bottomSpacer, actions); + + return panel; + } + + private static Node createContentBlock(WalkthroughContentBlock block) { + switch (block.getType()) { + case TEXT: + TextContentBlock textBlock = (TextContentBlock) block; + Label textLabel = new Label(Localization.lang(textBlock.getText())); + textLabel.getStyleClass().add("walkthrough-text-content"); + return textLabel; + case INFO_BLOCK: + InfoBlockContentBlock infoBlock = (InfoBlockContentBlock) block; + HBox infoContainer = new HBox(); + infoContainer.getStyleClass().add("walkthrough-info-container"); + infoContainer.setAlignment(Pos.CENTER_LEFT); + infoContainer.setSpacing(4); + + JabRefIconView icon = new JabRefIconView(IconTheme.JabRefIcons.INTEGRITY_INFO); + icon.getStyleClass().add("walkthrough-info-icon"); + + Label infoLabel = new Label(Localization.lang(infoBlock.getText())); + infoLabel.getStyleClass().add("walkthrough-info-label"); + HBox.setHgrow(infoLabel, Priority.ALWAYS); + + infoContainer.getChildren().addAll(icon, infoLabel); + + VBox infoWrapper = new VBox(infoContainer); + infoWrapper.setAlignment(Pos.CENTER_LEFT); + return infoWrapper; + } + return new Label("Impossible content block type: " + block.getType()); + } + + private static VBox makePanel() { + VBox container = new VBox(); + container.getStyleClass().add("walkthrough-panel"); + return container; + } + + private static HBox makeActions(WalkthroughStep step, WalkthroughManager manager) { + HBox actions = new HBox(); + actions.setAlignment(Pos.CENTER_LEFT); + actions.setSpacing(0); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + if (step.actions().flatMap(WalkthroughActionsConfig::backButtonText).isPresent()) { + actions.getChildren().add(makeBackButton(step, manager)); + } + HBox rightActions = new HBox(); + rightActions.setAlignment(Pos.CENTER_RIGHT); + rightActions.setSpacing(2.5); + if (step.actions().flatMap(WalkthroughActionsConfig::skipButtonText).isPresent()) { + rightActions.getChildren().add(makeSkipButton(step, manager)); + } + if (step.actions().flatMap(WalkthroughActionsConfig::continueButtonText).isPresent()) { + rightActions.getChildren().add(makeContinueButton(step, manager)); + } + + actions.getChildren().addAll(spacer, rightActions); + + return actions; + } + + private static VBox makeContent(WalkthroughStep step) { + VBox contentContainer = new VBox(); + contentContainer.setSpacing(12); + for (WalkthroughContentBlock contentBlock : step.content()) { + Node contentNode = createContentBlock(contentBlock); + contentContainer.getChildren().add(contentNode); + } + return contentContainer; + } + + private static Button makeContinueButton(WalkthroughStep step, WalkthroughManager manager) { + String buttonText = step.actions() + .flatMap(WalkthroughActionsConfig::continueButtonText) + .orElse("Walkthrough continue button"); + + Button continueButton = new Button(Localization.lang(buttonText)); + continueButton.getStyleClass().add("walkthrough-continue-button"); + continueButton.setOnAction(_ -> manager.nextStep()); + return continueButton; + } + + private static Button makeSkipButton(WalkthroughStep step, WalkthroughManager manager) { + String buttonText = step.actions() + .flatMap(WalkthroughActionsConfig::skipButtonText) + .orElse("Walkthrough skip to finish"); + + Button skipButton = new Button(Localization.lang(buttonText)); + skipButton.getStyleClass().add("walkthrough-skip-button"); + skipButton.setOnAction(_ -> manager.skip()); + return skipButton; + } + + private static Button makeBackButton(WalkthroughStep step, WalkthroughManager manager) { + String buttonText = step.actions() + .flatMap(WalkthroughActionsConfig::backButtonText) + .orElse("Walkthrough back button"); + + Button backButton = new Button(Localization.lang(buttonText)); + backButton.getStyleClass().add("walkthrough-back-button"); + backButton.setOnAction(_ -> manager.previousStep()); + return backButton; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java new file mode 100644 index 00000000000..e7e3d347722 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java @@ -0,0 +1,22 @@ +package org.jabref.gui.walkthrough.declarative; + +// FIXME: Use a record class +/** + * An info block content block for displaying highlighted information with icon in walkthrough steps. + */ +public class InfoBlockContentBlock extends WalkthroughContentBlock { + private final String text; + + public InfoBlockContentBlock(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @Override + public ContentBlockType getType() { + return ContentBlockType.INFO_BLOCK; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java new file mode 100644 index 00000000000..4159a094afc --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java @@ -0,0 +1,113 @@ +package org.jabref.gui.walkthrough.declarative; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Button; + +import org.jabref.gui.actions.StandardActions; + +/** + * Factory class for creating different types of node resolvers. + */ +public class NodeResolverFactory { + private NodeResolverFactory() { + } + + /** + * Creates a resolver that finds a node by CSS selector + * + * @param selector The CSS selector to find the node + * @return A function that resolves the node from a Scene + */ + public static Function> forSelector(String selector) { + return scene -> Optional.ofNullable(scene.lookup(selector)); + } + + /** + * Creates a resolver that finds a node by its fx:id + * + * @param fxId The fx:id of the node + * @return A function that resolves the node from a Scene + */ + public static Function> forFxId(String fxId) { + return scene -> Optional.ofNullable(scene.lookup("#" + fxId)); + } + + /** + * Creates a resolver that returns a node from a supplier + * + * @param nodeSupplier A supplier that provides the node + * @return A function that resolves the node from a Scene (ignoring the Scene parameter) + */ + public static Function> forNodeSupplier(Supplier nodeSupplier) { + return _ -> Optional.ofNullable(nodeSupplier.get()); + } + + /** + * Creates a resolver that finds a node by a predicate + * + * @param predicate The predicate to match the node + * @return A function that resolves the node from a Scene + */ + public static Function> forPredicate(Predicate predicate) { + return scene -> Optional.ofNullable(findNode(scene.getRoot(), predicate)); + } + + /** + * Creates a resolver that finds a button by its StandardAction + * + * @param action The StandardAction associated with the button + * @return A function that resolves the button from a Scene + */ + public static Function> forAction(StandardActions action) { + return scene -> Optional.ofNullable(findNodeByAction(scene, action)); + } + + private static Node findNodeByAction(Scene scene, StandardActions action) { + return findNode(scene.getRoot(), node -> { + if (node instanceof Button button) { + if (button.getTooltip() != null) { + String tooltipText = button.getTooltip().getText(); + if (tooltipText != null && tooltipText.equals(action.getText())) { + return true; + } + } + + if (button.getStyleClass().contains("icon-button")) { + String actionText = action.getText(); + if (button.getTooltip() != null && button.getTooltip().getText() != null) { + String tooltipText = button.getTooltip().getText(); + if (tooltipText.startsWith(actionText) || tooltipText.contains(actionText)) { + return true; + } + } + + return button.getText() != null && button.getText().equals(actionText); + } + } + return false; + }); + } + + private static Node findNode(Node root, Predicate predicate) { + if (predicate.test(root)) { + return root; + } + + if (root instanceof javafx.scene.Parent parent) { + for (Node child : parent.getChildrenUnmodifiable()) { + Node result = findNode(child, predicate); + if (result != null) { + return result; + } + } + } + + return null; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/StepType.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/StepType.java new file mode 100644 index 00000000000..04e25900103 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/StepType.java @@ -0,0 +1,31 @@ +package org.jabref.gui.walkthrough.declarative; + +/** + * Defines the different types of walkthrough steps. + */ +public enum StepType { + /** + * Full-screen page that covers the entire application. + */ + FULL_SCREEN, + + /** + * Panel that appears on the left side of the interface. + */ + LEFT_PANEL, + + /** + * Panel that appears on the right side of the interface. + */ + RIGHT_PANEL, + + /** + * Panel that appears at the top of the interface. + */ + TOP_PANEL, + + /** + * Panel that appears at the bottom of the interface. + */ + BOTTOM_PANEL +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java new file mode 100644 index 00000000000..570b80fd2e6 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java @@ -0,0 +1,21 @@ +package org.jabref.gui.walkthrough.declarative; + +/** + * A text content block for displaying plain text in walkthrough steps. + */ +public class TextContentBlock extends WalkthroughContentBlock { + private final String text; + + public TextContentBlock(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @Override + public ContentBlockType getType() { + return ContentBlockType.TEXT; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java new file mode 100644 index 00000000000..9b4baf4291a --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java @@ -0,0 +1,17 @@ +package org.jabref.gui.walkthrough.declarative; + +import java.util.Optional; + +/** + * Configuration for walkthrough step buttons. + * + * @param continueButtonText Optional text for the continue button. If empty, button is hidden. + * @param skipButtonText Optional text for the skip button. If empty, button is hidden. + * @param backButtonText Optional text for the back button. If empty, button is hidden. + */ +public record WalkthroughActionsConfig( + Optional continueButtonText, + Optional skipButtonText, + Optional backButtonText +) { +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java new file mode 100644 index 00000000000..48305eca30d --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java @@ -0,0 +1,13 @@ +package org.jabref.gui.walkthrough.declarative; + +/** + * Base class for walkthrough content blocks that can be displayed in walkthrough steps. + */ +public abstract class WalkthroughContentBlock { + public abstract ContentBlockType getType(); + + public enum ContentBlockType { + TEXT, + INFO_BLOCK + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java new file mode 100644 index 00000000000..672301d006b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java @@ -0,0 +1,36 @@ +package org.jabref.gui.walkthrough.declarative; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import javafx.scene.Node; +import javafx.scene.Scene; + +/** + * Represents a single step in a walkthrough with support for different step types and rich content. + * + * @param title The step title (internationalization key) + * @param stepType The type of step (full screen, panels) + * @param content List of content blocks for rich formatting + * @param resolver Optional function to resolve the target Node in the scene + * @param actions Optional custom button configuration + */ +public record WalkthroughStep( + String title, + StepType stepType, + List content, + Optional>> resolver, + Optional actions) { + + public WalkthroughStep(String title, StepType stepType, List content, + Function> resolver) { + this(title, stepType, content, Optional.of(resolver), Optional.of(new WalkthroughActionsConfig(Optional.of("Continue"), + Optional.of("Skip to finish"), Optional.of("Back")))); + } + + public WalkthroughStep(String title, StepType stepType, List content, + WalkthroughActionsConfig buttonConfig) { + this(title, stepType, content, Optional.empty(), Optional.of(buttonConfig)); + } +} diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index 6c0788b4788..9b17aedb680 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2519,3 +2519,99 @@ journalInfo .grid-cell-b { -fx-font-size: 1.25em; -fx-border-color: transparent; } + +/* Walkthrough Styles */ +.walkthrough-panel { + -fx-background-color: -jr-white; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0, 0, 2); + -fx-padding: 1em; + -fx-spacing: 1em; +} + +.walkthrough-title { + -fx-text-fill: -jr-theme; + -fx-font-size: 1.5em; + -fx-font-weight: bold; + -fx-text-alignment: center-left; +} + +.walkthrough-step-counter { + -fx-background-color: -jr-base; + -fx-text-fill: -jr-gray-3; + -fx-font-size: 1em; + -fx-background-radius: 100; + -fx-padding: 0.125em 0.25em; + -fx-alignment: center; +} + +.walkthrough-text-content { + -fx-text-fill: -jr-gray-3; + -fx-font-size: 1em; + -fx-wrap-text: true; + -fx-alignment: baseline-left; +} + +.walkthrough-info-container { + -fx-border-color: -jr-theme; + -fx-border-width: 0 0 0 1; + -fx-padding: 0 0 0 0.75em; + -fx-spacing: 0.25em; +} + +.walkthrough-info-icon { + -fx-text-fill: -jr-theme; +} + +.walkthrough-info-label { + -fx-text-fill: -jr-theme; + -fx-font-size: 1em; + -fx-wrap-text: true; + -fx-alignment: center-left; +} + +.walkthrough-continue-button { + -fx-background-color: -jr-theme; + -fx-text-fill: -jr-white; + -fx-font-size: 1.1em; + -fx-border-radius: 2; + -fx-padding: 0.25em 0.5em; +} + +.walkthrough-skip-button { + -fx-background-color: transparent; + -fx-text-fill: -jr-theme; + -fx-font-size: 1.1em; + -fx-border-color: -jr-theme; + -fx-border-width: 0.5; + -fx-border-radius: 2; + -fx-padding: 0.25em 0.5em; +} + +.walkthrough-back-button { + -fx-background-color: transparent; + -fx-text-fill: -jr-theme; + -fx-font-size: 1.1em; + -fx-border-color: -jr-gray-2; + -fx-border-width: 0.5; + -fx-border-radius: 2; + -fx-padding: 0.25em 0.5em; +} + +.walkthrough-fullscreen-content { + -fx-padding: 0.1875em; + -fx-spacing: 0.75em; + -fx-max-width: 32em; + -fx-fill-width: true; +} + +.walkthrough-side-panel-vertical { + -fx-pref-width: 24em; + -fx-max-width: 24em; + -fx-min-width: 24em; +} + +.walkthrough-side-panel-horizontal { + -fx-pref-height: 16em; + -fx-max-height: 16em; + -fx-min-height: 16em; +} \ No newline at end of file diff --git a/jablib/src/main/java/org/jabref/logic/preferences/CliPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/CliPreferences.java index ee84f03f632..412ba59d013 100644 --- a/jablib/src/main/java/org/jabref/logic/preferences/CliPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/preferences/CliPreferences.java @@ -113,5 +113,7 @@ public interface CliPreferences { LastFilesOpenedPreferences getLastFilesOpenedPreferences(); + WalkthroughPreferences getWalkthroughPreferences(); + OpenOfficePreferences getOpenOfficePreferences(JournalAbbreviationRepository journalAbbreviationRepository); } diff --git a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java index d9646e91fd3..85400667d0a 100644 --- a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java @@ -383,6 +383,8 @@ public class JabRefCliPreferences implements CliPreferences { private static final String OPEN_FILE_EXPLORER_IN_FILE_DIRECTORY = "openFileExplorerInFileDirectory"; private static final String OPEN_FILE_EXPLORER_IN_LAST_USED_DIRECTORY = "openFileExplorerInLastUsedDirectory"; + private static final String WALKTHROUGH_COMPLETED = "walkthroughCompleted"; + private static final Logger LOGGER = LoggerFactory.getLogger(JabRefCliPreferences.class); private static final Preferences PREFS_NODE = Preferences.userRoot().node("/org/jabref"); @@ -426,6 +428,7 @@ public class JabRefCliPreferences implements CliPreferences { private FieldPreferences fieldPreferences; private AiPreferences aiPreferences; private LastFilesOpenedPreferences lastFilesOpenedPreferences; + private WalkthroughPreferences walkthroughPreferences; /** * @implNote The constructor is made protected to enforce this as a singleton class: @@ -685,6 +688,9 @@ protected JabRefCliPreferences() { // endregion // endregion + + // WalkThrough + defaults.put(WALKTHROUGH_COMPLETED, Boolean.FALSE); } public void setLanguageDependentDefaultValues() { @@ -2264,4 +2270,18 @@ public OpenOfficePreferences getOpenOfficePreferences(JournalAbbreviationReposit return openOfficePreferences; } + + @Override + public WalkthroughPreferences getWalkthroughPreferences() { + if (walkthroughPreferences != null) { + return walkthroughPreferences; + } + + walkthroughPreferences = new WalkthroughPreferences(getBoolean(WALKTHROUGH_COMPLETED)); + + walkthroughPreferences.completedProperty().addListener((obs, oldValue, newValue) -> + putBoolean(WALKTHROUGH_COMPLETED, newValue)); + + return walkthroughPreferences; + } } diff --git a/jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java new file mode 100644 index 00000000000..442f079e7b0 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java @@ -0,0 +1,27 @@ +package org.jabref.logic.preferences; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +/** + * Preferences related to the application walkthrough functionality. + */ +public class WalkthroughPreferences { + private final BooleanProperty completed; + + public WalkthroughPreferences(boolean completed) { + this.completed = new SimpleBooleanProperty(completed); + } + + public BooleanProperty completedProperty() { + return completed; + } + + public boolean isCompleted() { + return completed.get(); + } + + public void setCompleted(boolean completed) { + this.completed.set(completed); + } +} diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index f25eb4ecdb7..8519492d295 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2921,6 +2921,28 @@ You\ must\ specify\ a\ Bib(La)TeX\ source.=You must specify a Bib(La)TeX source. You\ must\ specify\ an\ identifier.=You must specify an identifier. You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) citations. +# Walkthrough +Walkthrough\ welcome\ title=Welcome to JabRef! +Walkthrough\ welcome\ intro=This quick walkthrough will introduce you to some key features. +Walkthrough\ welcome\ tip=You can always access this walkthrough from the Help menu. +Walkthrough\ create\ entry\ title=Creating a New Entry +Walkthrough\ create\ entry\ description=Click the highlighted button to start creating a new bibliographic entry. +Walkthrough\ create\ entry\ tip=JabRef supports various entry types like articles, books, and more. +Walkthrough\ save\ title=Saving Your Work +Walkthrough\ save\ description=Don't forget to save your library. Click the save button. +Walkthrough\ save\ important=Regularly saving prevents data loss. +Walkthrough\ completion\ title=Walkthrough Complete! +Walkthrough\ completion\ message=You've completed the basic feature tour. +Walkthrough\ completion\ next_steps=Explore more features like groups, fetchers, and customization options. +Walkthrough\ completion\ resources=Check our documentation for detailed guides. +Skip=Skip +Finish=Finish +Skip\ to\ finish=Skip to Finish +Start\ walkthrough=Start Walkthrough +Step\ of=Step %0 of %1 +Complete\ walkthrough=Complete Walkthrough +Back=Back + # CommandLine Available\ export\ formats\:=Available export formats: Available\ import\ formats\:=Available import formats: @@ -2945,3 +2967,4 @@ The\ following\ providers\ are\ available\:=The following providers are availabl Unable\ to\ open\ file\ '%0'.=Unable to open file '%0'. Unknown\ export\ format\ '%0'.=Unknown export format '%0'. Updating\ PDF\ metadata.=Updating PDF metadata. + From 1230126f72218efe681e067d2ae2188ced4a17cc Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Mon, 2 Jun 2025 17:59:20 -0400 Subject: [PATCH 02/50] Fix issues mentioned in the PR --- .../main/java/org/jabref/gui/JabRefGUI.java | 10 +++--- ...lkthroughManager.java => Walkthrough.java} | 4 +-- .../gui/walkthrough/WalkthroughOverlay.java | 32 +++++++++---------- .../gui/walkthrough/WalkthroughUIFactory.java | 17 +++++----- .../declarative/NodeResolverFactory.java | 3 +- .../preferences/JabRefCliPreferences.java | 2 +- .../main/resources/l10n/JabRef_en.properties | 1 + 7 files changed, 35 insertions(+), 34 deletions(-) rename jabgui/src/main/java/org/jabref/gui/walkthrough/{WalkthroughManager.java => Walkthrough.java} (98%) diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index 53d9f7cb2ae..b1932297f0c 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -28,7 +28,7 @@ import org.jabref.gui.util.DirectoryMonitor; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.gui.util.WebViewStore; -import org.jabref.gui.walkthrough.WalkthroughManager; +import org.jabref.gui.walkthrough.Walkthrough; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; import org.jabref.logic.journals.JournalAbbreviationLoader; @@ -77,7 +77,7 @@ public class JabRefGUI extends Application { private static ClipBoardManager clipBoardManager; private static DialogService dialogService; private static JabRefFrame mainFrame; - private static WalkthroughManager walkthroughManager; + private static Walkthrough walkthroughManager; private static RemoteListenerServerManager remoteListenerServerManager; private Stage mainStage; @@ -192,13 +192,13 @@ public void initialize() { // Initialize walkthrough manager WalkthroughPreferences walkthroughPreferences = preferences.getWalkthroughPreferences(); - JabRefGUI.walkthroughManager = new WalkthroughManager(walkthroughPreferences); - Injector.setModelOrService(WalkthroughManager.class, walkthroughManager); + JabRefGUI.walkthroughManager = new Walkthrough(walkthroughPreferences); + Injector.setModelOrService(Walkthrough.class, walkthroughManager); } private void setupProxy() { if (!preferences.getProxyPreferences().shouldUseProxy() - || !preferences.getProxyPreferences().shouldUseAuthentication()) { + || !preferences.getProxyPreferences().shouldUseAuthentication()) { return; } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughManager.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java similarity index 98% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughManager.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 32f46b9d183..38def67481c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughManager.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -23,7 +23,7 @@ /** * Manages a walkthrough session by coordinating steps. */ -public class WalkthroughManager { +public class Walkthrough { private final WalkthroughPreferences preferences; private final IntegerProperty currentStep; private final IntegerProperty totalSteps; @@ -37,7 +37,7 @@ public class WalkthroughManager { * * @param preferences The walkthrough preferences to use */ - public WalkthroughManager(WalkthroughPreferences preferences) { + public Walkthrough(WalkthroughPreferences preferences) { this.preferences = preferences; this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 8c911295293..b8fca001a26 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -25,13 +25,13 @@ public class WalkthroughOverlay { private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); private final Stage parentStage; - private final WalkthroughManager manager; + private final Walkthrough manager; private final GridPane overlayPane; private final PulseAnimateIndicator pulseIndicator; private final Pane originalRoot; private final StackPane stackContainer; - public WalkthroughOverlay(Stage stage, WalkthroughManager manager) { + public WalkthroughOverlay(Stage stage, Walkthrough manager) { this.parentStage = stage; this.manager = manager; @@ -94,12 +94,12 @@ private void displayFullScreenStep(WalkthroughStep step) { overlayPane.getRowConstraints().clear(); overlayPane.getColumnConstraints().clear(); - RowConstraints rc = new RowConstraints(); - rc.setVgrow(Priority.ALWAYS); - overlayPane.getRowConstraints().add(rc); - ColumnConstraints cc = new ColumnConstraints(); - cc.setHgrow(Priority.ALWAYS); - overlayPane.getColumnConstraints().add(cc); + RowConstraints rowConstraints = new RowConstraints(); + rowConstraints.setVgrow(Priority.ALWAYS); + overlayPane.getRowConstraints().add(rowConstraints); + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setHgrow(Priority.ALWAYS); + overlayPane.getColumnConstraints().add(columnConstraints); GridPane.setHgrow(fullScreenContent, Priority.ALWAYS); GridPane.setVgrow(fullScreenContent, Priority.ALWAYS); @@ -124,22 +124,22 @@ private void displayPanelStep(WalkthroughStep step, Pos position) { GridPane.setFillWidth(panelContent, false); GridPane.setFillHeight(panelContent, false); - RowConstraints rc = new RowConstraints(); - ColumnConstraints cc = new ColumnConstraints(); + RowConstraints rowConstraints = new RowConstraints(); + ColumnConstraints columnConstraints = new ColumnConstraints(); overlayPane.setAlignment(position); switch (position) { case CENTER_LEFT: case CENTER_RIGHT: - rc.setVgrow(Priority.ALWAYS); - cc.setHgrow(Priority.NEVER); + rowConstraints.setVgrow(Priority.ALWAYS); + columnConstraints.setHgrow(Priority.NEVER); GridPane.setFillHeight(panelContent, true); break; case TOP_CENTER: case BOTTOM_CENTER: - cc.setHgrow(Priority.ALWAYS); - rc.setVgrow(Priority.NEVER); + columnConstraints.setHgrow(Priority.ALWAYS); + rowConstraints.setVgrow(Priority.NEVER); GridPane.setFillWidth(panelContent, true); break; default: @@ -147,8 +147,8 @@ private void displayPanelStep(WalkthroughStep step, Pos position) { break; } - overlayPane.getRowConstraints().add(rc); - overlayPane.getColumnConstraints().add(cc); + overlayPane.getRowConstraints().add(rowConstraints); + overlayPane.getColumnConstraints().add(columnConstraints); overlayPane.setVisible(true); if (step.resolver().isPresent()) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java index 803884cd39b..e569e48122d 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java @@ -24,10 +24,9 @@ */ public class WalkthroughUIFactory { /** - * Creates a full-screen page using dynamic content from a walkthrough - * step. + * Creates a full-screen page using dynamic content from a walkthrough step. */ - public static VBox createFullscreen(WalkthroughStep step, WalkthroughManager manager) { + public static VBox createFullscreen(WalkthroughStep step, Walkthrough manager) { VBox container = makePanel(); container.setAlignment(Pos.CENTER); VBox content = new VBox(); @@ -43,7 +42,7 @@ public static VBox createFullscreen(WalkthroughStep step, WalkthroughManager man /** * Creates a side panel for walkthrough steps. */ - public static VBox createSidePanel(WalkthroughStep step, WalkthroughManager manager) { + public static VBox createSidePanel(WalkthroughStep step, Walkthrough manager) { VBox panel = makePanel(); if (step.stepType() == StepType.LEFT_PANEL || step.stepType() == StepType.RIGHT_PANEL) { @@ -108,7 +107,7 @@ private static Node createContentBlock(WalkthroughContentBlock block) { infoWrapper.setAlignment(Pos.CENTER_LEFT); return infoWrapper; } - return new Label("Impossible content block type: " + block.getType()); + return new Label(Localization.lang("Impossible content block type", block.getType())); } private static VBox makePanel() { @@ -117,7 +116,7 @@ private static VBox makePanel() { return container; } - private static HBox makeActions(WalkthroughStep step, WalkthroughManager manager) { + private static HBox makeActions(WalkthroughStep step, Walkthrough manager) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); actions.setSpacing(0); @@ -153,7 +152,7 @@ private static VBox makeContent(WalkthroughStep step) { return contentContainer; } - private static Button makeContinueButton(WalkthroughStep step, WalkthroughManager manager) { + private static Button makeContinueButton(WalkthroughStep step, Walkthrough manager) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::continueButtonText) .orElse("Walkthrough continue button"); @@ -164,7 +163,7 @@ private static Button makeContinueButton(WalkthroughStep step, WalkthroughManage return continueButton; } - private static Button makeSkipButton(WalkthroughStep step, WalkthroughManager manager) { + private static Button makeSkipButton(WalkthroughStep step, Walkthrough manager) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::skipButtonText) .orElse("Walkthrough skip to finish"); @@ -175,7 +174,7 @@ private static Button makeSkipButton(WalkthroughStep step, WalkthroughManager ma return skipButton; } - private static Button makeBackButton(WalkthroughStep step, WalkthroughManager manager) { + private static Button makeBackButton(WalkthroughStep step, Walkthrough manager) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::backButtonText) .orElse("Walkthrough back button"); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java index 4159a094afc..d9b30537912 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java @@ -6,6 +6,7 @@ import java.util.function.Supplier; import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Button; @@ -99,7 +100,7 @@ private static Node findNode(Node root, Predicate predicate) { return root; } - if (root instanceof javafx.scene.Parent parent) { + if (root instanceof Parent parent) { for (Node child : parent.getChildrenUnmodifiable()) { Node result = findNode(child, predicate); if (result != null) { diff --git a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java index 8aef91dc038..b306c216167 100644 --- a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java @@ -2289,7 +2289,7 @@ public WalkthroughPreferences getWalkthroughPreferences() { walkthroughPreferences = new WalkthroughPreferences(getBoolean(WALKTHROUGH_COMPLETED)); - walkthroughPreferences.completedProperty().addListener((obs, oldValue, newValue) -> + walkthroughPreferences.completedProperty().addListener((_, _, newValue) -> putBoolean(WALKTHROUGH_COMPLETED, newValue)); return walkthroughPreferences; diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 3284ecbb8a6..09f68d836a1 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2949,6 +2949,7 @@ Start\ walkthrough=Start Walkthrough Step\ of=Step %0 of %1 Complete\ walkthrough=Complete Walkthrough Back=Back +Impossible\ content\ block\ type=Impossible content block type: %1 # CommandLine Available\ export\ formats\:=Available export formats: From 1b2af5713c3fee7b7fe9cf792d85a5bf3d1805dd Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Thu, 5 Jun 2025 20:37:21 -0400 Subject: [PATCH 03/50] Tweak Base.css --- .../main/resources/org/jabref/gui/Base.css | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index 9b17aedb680..86b3025d254 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2530,23 +2530,40 @@ journalInfo .grid-cell-b { .walkthrough-title { -fx-text-fill: -jr-theme; - -fx-font-size: 1.5em; + -fx-font-size: 3em; -fx-font-weight: bold; -fx-text-alignment: center-left; } +.walkthrough-side-panel-vertical .walkthrough-title, +.walkthrough-side-panel-horizontal .walkthrough-title { + -fx-font-size: 2em; +} + +.walkthrough-side-panel-vertical { + -fx-pref-width: 32em; + -fx-max-width: 32em; + -fx-min-width: 32em; +} + +.walkthrough-side-panel-horizontal { + -fx-pref-height: 16em; + -fx-max-height: 16em; + -fx-min-height: 16em; +} + .walkthrough-step-counter { -fx-background-color: -jr-base; -fx-text-fill: -jr-gray-3; -fx-font-size: 1em; -fx-background-radius: 100; - -fx-padding: 0.125em 0.25em; + -fx-padding: 0.25em 0.75em; -fx-alignment: center; } .walkthrough-text-content { -fx-text-fill: -jr-gray-3; - -fx-font-size: 1em; + -fx-font-size: 1.2em; -fx-wrap-text: true; -fx-alignment: baseline-left; } @@ -2559,12 +2576,13 @@ journalInfo .grid-cell-b { } .walkthrough-info-icon { - -fx-text-fill: -jr-theme; + -fx-icon-color: -jr-theme; + -fx-font-size: 1.2em; } .walkthrough-info-label { -fx-text-fill: -jr-theme; - -fx-font-size: 1em; + -fx-font-size: 1.2em; -fx-wrap-text: true; -fx-alignment: center-left; } @@ -2573,7 +2591,7 @@ journalInfo .grid-cell-b { -fx-background-color: -jr-theme; -fx-text-fill: -jr-white; -fx-font-size: 1.1em; - -fx-border-radius: 2; + -fx-border-radius: 4; -fx-padding: 0.25em 0.5em; } @@ -2583,7 +2601,7 @@ journalInfo .grid-cell-b { -fx-font-size: 1.1em; -fx-border-color: -jr-theme; -fx-border-width: 0.5; - -fx-border-radius: 2; + -fx-border-radius: 4; -fx-padding: 0.25em 0.5em; } @@ -2591,27 +2609,14 @@ journalInfo .grid-cell-b { -fx-background-color: transparent; -fx-text-fill: -jr-theme; -fx-font-size: 1.1em; - -fx-border-color: -jr-gray-2; - -fx-border-width: 0.5; - -fx-border-radius: 2; + -fx-border-radius: 4; -fx-padding: 0.25em 0.5em; } .walkthrough-fullscreen-content { -fx-padding: 0.1875em; - -fx-spacing: 0.75em; - -fx-max-width: 32em; + -fx-spacing: 1em; + -fx-max-width: 48em; -fx-fill-width: true; } -.walkthrough-side-panel-vertical { - -fx-pref-width: 24em; - -fx-max-width: 24em; - -fx-min-width: 24em; -} - -.walkthrough-side-panel-horizontal { - -fx-pref-height: 16em; - -fx-max-height: 16em; - -fx-min-height: 16em; -} \ No newline at end of file From 6e2117029a0c212e88e0dd3a98388258cf44fd4c Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Thu, 5 Jun 2025 20:38:11 -0400 Subject: [PATCH 04/50] Implement backdrop highlight effect. --- .../jabref/gui/util/BackdropHighlight.java | 154 ++++++++++++++++++ .../gui/walkthrough/WalkthroughOverlay.java | 24 +-- 2 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java diff --git a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java new file mode 100644 index 00000000000..18e16673ce1 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java @@ -0,0 +1,154 @@ +package org.jabref.gui.util; + +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.Shape; + +import org.jspecify.annotations.NonNull; + +/** + * Creates a backdrop highlight effect. + */ +public class BackdropHighlight { + private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); + + private final Pane pane; + private Node targetNode; + + private final Rectangle backdrop; + private final Rectangle hole; + private @NonNull Shape overlayShape; + + private final List cleanUpTasks = new ArrayList<>(); + private final InvalidationListener updateListener = _ -> updateOverlayLayout(); + + /** + * Constructs a BackdropHighlight instance that overlays on the specified pane. + * + * @param pane The pane onto which the overlay will be added. + */ + public BackdropHighlight(@NonNull Pane pane) { + this.pane = pane; + this.backdrop = new Rectangle(); + this.hole = new Rectangle(); + + this.overlayShape = Shape.subtract(backdrop, hole); + this.overlayShape.setFill(OVERLAY_COLOR); + this.overlayShape.setMouseTransparent(true); + this.overlayShape.setVisible(false); + + this.pane.getChildren().add(overlayShape); + } + + /** + * Attaches the highlight effect to a specific node. + * + * @param node The node to be "highlighted". + */ + public void attach(@NonNull Node node) { + detach(); + + this.targetNode = node; + updateOverlayLayout(); + + addListener(targetNode.localToSceneTransformProperty()); + addListener(targetNode.visibleProperty()); + addListener(pane.widthProperty()); + addListener(pane.heightProperty()); + addListener(pane.sceneProperty(), (_, _, newScene) -> { + if (newScene != null) { + addListener(newScene.widthProperty()); + addListener(newScene.heightProperty()); + } + updateOverlayLayout(); + }); + } + + /** + * Detaches the highlight effect from the target node. In other words, hide the + * overlay. + */ + public void detach() { + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); + overlayShape.setVisible(false); + this.targetNode = null; + } + + private void addListener(ObservableValue property) { + property.addListener(updateListener); + cleanUpTasks.add(() -> property.removeListener(updateListener)); + } + + private void addListener(ObservableValue property, ChangeListener listener) { + property.addListener(listener); + cleanUpTasks.add(() -> property.removeListener(listener)); + } + + private void updateOverlayLayout() { + if (targetNode == null || targetNode.getScene() == null || pane.getScene() == null) { + overlayShape.setVisible(false); + return; + } + + if (!targetNode.isVisible()) { + overlayShape.setVisible(false); + return; + } + + Bounds nodeBoundsInScene; + try { + nodeBoundsInScene = targetNode.localToScene(targetNode.getBoundsInLocal()); + } catch (IllegalStateException e) { + overlayShape.setVisible(false); + return; + } + + if (nodeBoundsInScene == null || nodeBoundsInScene.getWidth() <= 0 || nodeBoundsInScene.getHeight() <= 0) { + overlayShape.setVisible(false); + return; + } + + Scene currentScene = pane.getScene(); + Bounds sceneViewportBounds = new BoundingBox(0, 0, currentScene.getWidth(), currentScene.getHeight()); + if (!nodeBoundsInScene.intersects(sceneViewportBounds)) { + overlayShape.setVisible(false); + return; + } + + backdrop.setX(0); + backdrop.setY(0); + backdrop.setWidth(pane.getWidth()); + backdrop.setHeight(pane.getHeight()); + + Bounds nodeBoundsInRootPane = pane.sceneToLocal(nodeBoundsInScene); + hole.setX(nodeBoundsInRootPane.getMinX()); + hole.setY(nodeBoundsInRootPane.getMinY()); + hole.setWidth(nodeBoundsInRootPane.getWidth()); + hole.setHeight(nodeBoundsInRootPane.getHeight()); + + Shape oldOverlayShape = this.overlayShape; + int oldIndex = -1; + if (this.pane.getChildren().contains(oldOverlayShape)) { + oldIndex = this.pane.getChildren().indexOf(oldOverlayShape); + this.pane.getChildren().remove(oldIndex); + } + + this.overlayShape = Shape.subtract(backdrop, hole); + this.overlayShape.setFill(OVERLAY_COLOR); + this.overlayShape.setMouseTransparent(true); + this.overlayShape.setVisible(true); + this.pane.getChildren().add(oldIndex, this.overlayShape); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index b8fca001a26..4dde9cb6f7b 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -13,7 +13,7 @@ import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import org.jabref.gui.util.component.PulseAnimateIndicator; +import org.jabref.gui.util.BackdropHighlight; import org.jabref.gui.walkthrough.declarative.WalkthroughStep; import org.slf4j.Logger; @@ -27,9 +27,9 @@ public class WalkthroughOverlay { private final Stage parentStage; private final Walkthrough manager; private final GridPane overlayPane; - private final PulseAnimateIndicator pulseIndicator; + private final BackdropHighlight backdropHighlight; private final Pane originalRoot; - private final StackPane stackContainer; + private final StackPane stackPane; public WalkthroughOverlay(Stage stage, Walkthrough manager) { this.parentStage = stage; @@ -48,13 +48,13 @@ public WalkthroughOverlay(Stage stage, Walkthrough manager) { } originalRoot = (Pane) scene.getRoot(); - stackContainer = new StackPane(); + stackPane = new StackPane(); - stackContainer.getChildren().add(originalRoot); - stackContainer.getChildren().add(overlayPane); - pulseIndicator = new PulseAnimateIndicator(stackContainer); + stackPane.getChildren().add(originalRoot); + backdropHighlight = new BackdropHighlight(stackPane); + stackPane.getChildren().add(overlayPane); - scene.setRoot(stackContainer); + scene.setRoot(stackPane); } public void displayStep(WalkthroughStep step) { @@ -65,7 +65,7 @@ public void displayStep(WalkthroughStep step) { show(); - pulseIndicator.stop(); + backdropHighlight.detach(); overlayPane.getChildren().clear(); switch (step.stepType()) { @@ -155,7 +155,7 @@ private void displayPanelStep(WalkthroughStep step, Pos position) { Optional targetNodeOpt = step.resolver().get().apply(parentStage.getScene()); if (targetNodeOpt.isPresent()) { Node targetNode = targetNodeOpt.get(); - pulseIndicator.attachToNode(targetNode); + backdropHighlight.attach(targetNode); } else { LOGGER.warn("Could not resolve target node for step: {}", step.title()); } @@ -166,14 +166,14 @@ private void displayPanelStep(WalkthroughStep step, Pos position) { * Detaches the overlay and cleans up resources. */ public void detach() { - pulseIndicator.stop(); + backdropHighlight.detach(); overlayPane.setVisible(false); overlayPane.getChildren().clear(); Scene scene = parentStage.getScene(); if (scene != null && originalRoot != null) { - stackContainer.getChildren().remove(originalRoot); + stackPane.getChildren().remove(originalRoot); scene.setRoot(originalRoot); LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); } From bf87a523fc298121d124c150b591e92a3ae7fea4 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Thu, 5 Jun 2025 23:15:46 -0400 Subject: [PATCH 05/50] Use record classes --- .../jabref/gui/walkthrough/Walkthrough.java | 95 +++++++------ .../gui/walkthrough/WalkthroughOverlay.java | 76 +++++------ ...IFactory.java => WalkthroughRenderer.java} | 125 +++++++++--------- .../declarative/InfoBlockContentBlock.java | 22 --- .../declarative/TextContentBlock.java | 21 --- .../declarative/WalkthroughContentBlock.java | 13 -- .../declarative/WalkthroughStep.java | 36 ----- .../richtext/ArbitraryJFXBlock.java | 11 ++ .../declarative/richtext/InfoBlock.java | 4 + .../declarative/richtext/TextBlock.java | 4 + .../richtext/WalkthroughRichTextBlock.java | 4 + .../declarative/step/FullScreenStep.java | 30 +++++ .../declarative/step/PanelStep.java | 33 +++++ .../declarative/{ => step}/StepType.java | 2 +- .../declarative/step/WalkthroughNode.java | 31 +++++ .../main/resources/org/jabref/gui/Base.css | 5 +- .../main/resources/l10n/JabRef_en.properties | 1 + 17 files changed, 263 insertions(+), 250 deletions(-) rename jabgui/src/main/java/org/jabref/gui/walkthrough/{WalkthroughUIFactory.java => WalkthroughRenderer.java} (56%) delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/WalkthroughRichTextBlock.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java rename jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/{ => step}/StepType.java (91%) create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 38def67481c..e1a17462a55 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -11,12 +11,13 @@ import javafx.stage.Stage; import org.jabref.gui.actions.StandardActions; -import org.jabref.gui.walkthrough.declarative.InfoBlockContentBlock; import org.jabref.gui.walkthrough.declarative.NodeResolverFactory; -import org.jabref.gui.walkthrough.declarative.StepType; -import org.jabref.gui.walkthrough.declarative.TextContentBlock; import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; -import org.jabref.gui.walkthrough.declarative.WalkthroughStep; +import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; +import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; +import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; +import org.jabref.gui.walkthrough.declarative.step.PanelStep; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; import org.jabref.logic.l10n.Localization; import org.jabref.logic.preferences.WalkthroughPreferences; @@ -28,8 +29,8 @@ public class Walkthrough { private final IntegerProperty currentStep; private final IntegerProperty totalSteps; private final BooleanProperty active; - private final WalkthroughStep[] steps; - private WalkthroughOverlay overlay; + private final WalkthroughNode[] steps; + private Optional overlay = Optional.empty(); private Stage currentStage; /** @@ -42,40 +43,38 @@ public Walkthrough(WalkthroughPreferences preferences) { this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); - this.steps = new WalkthroughStep[] { - new WalkthroughStep( + this.steps = new WalkthroughNode[] { + new FullScreenStep( Localization.lang("Walkthrough welcome title"), - StepType.FULL_SCREEN, List.of( - new TextContentBlock(Localization.lang("Walkthrough welcome intro")), - new InfoBlockContentBlock(Localization.lang("Walkthrough welcome tip"))), + new TextBlock(Localization.lang("Walkthrough welcome intro")), + new InfoBlock(Localization.lang("Walkthrough welcome tip"))), new WalkthroughActionsConfig(Optional.of("Start walkthrough"), Optional.of("Skip to finish"), Optional.empty())), - new WalkthroughStep( + new PanelStep( Localization.lang("Walkthrough create entry title"), - StepType.BOTTOM_PANEL, List.of( - new TextContentBlock(Localization.lang("Walkthrough create entry description")), - new InfoBlockContentBlock(Localization.lang("Walkthrough create entry tip"))), - NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY) + new TextBlock(Localization.lang("Walkthrough create entry description")), + new InfoBlock(Localization.lang("Walkthrough create entry tip"))), + NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY), + javafx.geometry.Pos.BOTTOM_CENTER ), - new WalkthroughStep( + new PanelStep( Localization.lang("Walkthrough save title"), - StepType.RIGHT_PANEL, List.of( - new TextContentBlock(Localization.lang("Walkthrough save description")), - new InfoBlockContentBlock(Localization.lang("Walkthrough save important"))), - NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)), + new TextBlock(Localization.lang("Walkthrough save description")), + new InfoBlock(Localization.lang("Walkthrough save important"))), + NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY), + javafx.geometry.Pos.CENTER_RIGHT), - new WalkthroughStep( + new FullScreenStep( Localization.lang("Walkthrough completion title"), - StepType.FULL_SCREEN, List.of( - new TextContentBlock(Localization.lang("Walkthrough completion message")), - new TextContentBlock(Localization.lang("Walkthrough completion next_steps")), - new InfoBlockContentBlock(Localization.lang("Walkthrough completion resources"))), + new TextBlock(Localization.lang("Walkthrough completion message")), + new TextBlock(Localization.lang("Walkthrough completion next_steps")), + new InfoBlock(Localization.lang("Walkthrough completion resources"))), new WalkthroughActionsConfig(Optional.of("Complete walkthrough"), Optional.empty(), Optional.of("Back"))) }; @@ -120,17 +119,17 @@ public void start(Stage stage) { return; } - if (currentStage != stage || overlay == null) { - if (overlay != null) { - overlay.detach(); - } + if (currentStage != stage) { + overlay.ifPresent(WalkthroughOverlay::detach); currentStage = stage; - overlay = new WalkthroughOverlay(stage, this); + overlay = Optional.of(new WalkthroughOverlay(stage, this)); } currentStep.set(0); active.set(true); - overlay.displayStep(getCurrentStep()); + getCurrentStep().ifPresent((step) -> overlay.ifPresent( + overlay -> overlay.displayStep(step) + )); } /** @@ -140,9 +139,9 @@ public void nextStep() { int nextIndex = currentStep.get() + 1; if (nextIndex < steps.length) { currentStep.set(nextIndex); - if (overlay != null) { - overlay.displayStep(getCurrentStep()); - } + getCurrentStep().ifPresent((step) -> overlay.ifPresent( + overlay -> overlay.displayStep(step) + )); } else { preferences.setCompleted(true); stop(); @@ -150,18 +149,16 @@ public void nextStep() { } /** - * Moves to the next step in the walkthrough with stage switching. - * This method handles stage changes by recreating the overlay on the new stage. + * Moves to the next step in the walkthrough with stage switching. This method + * handles stage changes by recreating the overlay on the new stage. * * @param stage The stage to display the next step on */ public void nextStep(Stage stage) { if (currentStage != stage) { - if (overlay != null) { - overlay.detach(); - } + overlay.ifPresent(WalkthroughOverlay::detach); currentStage = stage; - overlay = new WalkthroughOverlay(stage, this); + overlay = Optional.of(new WalkthroughOverlay(stage, this)); } nextStep(); } @@ -173,9 +170,9 @@ public void previousStep() { int prevIndex = currentStep.get() - 1; if (prevIndex >= 0) { currentStep.set(prevIndex); - if (overlay != null) { - overlay.displayStep(getCurrentStep()); - } + getCurrentStep().ifPresent((step) -> overlay.ifPresent( + overlay -> overlay.displayStep(step) + )); } } @@ -188,17 +185,15 @@ public void skip() { } private void stop() { - if (overlay != null) { - overlay.detach(); - } + overlay.ifPresent(WalkthroughOverlay::detach); active.set(false); } - private WalkthroughStep getCurrentStep() { + private Optional getCurrentStep() { int index = currentStep.get(); if (index >= 0 && index < steps.length) { - return steps[index]; + return Optional.of(steps[index]); } - return null; + return Optional.empty(); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 4dde9cb6f7b..569ffa3879e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -1,5 +1,6 @@ package org.jabref.gui.walkthrough; +import java.util.Objects; import java.util.Optional; import javafx.geometry.Pos; @@ -14,7 +15,10 @@ import javafx.stage.Stage; import org.jabref.gui.util.BackdropHighlight; -import org.jabref.gui.walkthrough.declarative.WalkthroughStep; +import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; +import org.jabref.gui.walkthrough.declarative.step.PanelStep; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.logic.l10n.Localization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,10 +34,12 @@ public class WalkthroughOverlay { private final BackdropHighlight backdropHighlight; private final Pane originalRoot; private final StackPane stackPane; + private final WalkthroughRenderer uiFactory; public WalkthroughOverlay(Stage stage, Walkthrough manager) { this.parentStage = stage; this.manager = manager; + this.uiFactory = new WalkthroughRenderer(); overlayPane = new GridPane(); overlayPane.setStyle("-fx-background-color: transparent;"); @@ -42,10 +48,7 @@ public WalkthroughOverlay(Stage stage, Walkthrough manager) { overlayPane.setMaxHeight(Double.MAX_VALUE); Scene scene = stage.getScene(); - if (scene == null) { - LOGGER.error("Parent stage's scene must not be null to initialize WalkthroughOverlay."); - throw new IllegalStateException("Parent stage's scene must not be null"); - } + Objects.requireNonNull(scene); originalRoot = (Pane) scene.getRoot(); stackPane = new StackPane(); @@ -57,7 +60,7 @@ public WalkthroughOverlay(Stage stage, Walkthrough manager) { scene.setRoot(stackPane); } - public void displayStep(WalkthroughStep step) { + public void displayStep(WalkthroughNode step) { if (step == null) { hide(); return; @@ -68,29 +71,31 @@ public void displayStep(WalkthroughStep step) { backdropHighlight.detach(); overlayPane.getChildren().clear(); - switch (step.stepType()) { - case FULL_SCREEN: - displayFullScreenStep(step); - break; - case LEFT_PANEL: - displayPanelStep(step, Pos.CENTER_LEFT); - break; - case RIGHT_PANEL: - displayPanelStep(step, Pos.CENTER_RIGHT); - break; - case TOP_PANEL: - displayPanelStep(step, Pos.TOP_CENTER); - break; - case BOTTOM_PANEL: - displayPanelStep(step, Pos.BOTTOM_CENTER); - break; + Node stepContent; + if (step instanceof FullScreenStep fullScreenStep) { + stepContent = uiFactory.render(fullScreenStep, manager); + displayFullScreenContent(stepContent); + } else if (step instanceof PanelStep panelStep) { + stepContent = uiFactory.render(panelStep, manager); + displayPanelContent(stepContent, panelStep.position()); + + if (step.resolver().isPresent()) { + Optional targetNodeOpt = step.resolver().get().apply(parentStage.getScene()); + if (targetNodeOpt.isPresent()) { + Node targetNode = targetNodeOpt.get(); + backdropHighlight.attach(targetNode); + step.clickOnNodeAction().ifPresent(action -> + targetNode.setOnMouseClicked(_ -> action.accept(manager))); + } else { + LOGGER.warn(Localization.lang("Could not resolve target node for step", step.title())); + } + } } } - private void displayFullScreenStep(WalkthroughStep step) { - Node fullScreenContent = WalkthroughUIFactory.createFullscreen(step, manager); + private void displayFullScreenContent(Node content) { overlayPane.getChildren().clear(); - overlayPane.getChildren().add(fullScreenContent); + overlayPane.getChildren().add(content); overlayPane.getRowConstraints().clear(); overlayPane.getColumnConstraints().clear(); @@ -101,17 +106,16 @@ private void displayFullScreenStep(WalkthroughStep step) { columnConstraints.setHgrow(Priority.ALWAYS); overlayPane.getColumnConstraints().add(columnConstraints); - GridPane.setHgrow(fullScreenContent, Priority.ALWAYS); - GridPane.setVgrow(fullScreenContent, Priority.ALWAYS); - GridPane.setFillWidth(fullScreenContent, true); - GridPane.setFillHeight(fullScreenContent, true); + GridPane.setHgrow(content, Priority.ALWAYS); + GridPane.setVgrow(content, Priority.ALWAYS); + GridPane.setFillWidth(content, true); + GridPane.setFillHeight(content, true); overlayPane.setAlignment(Pos.CENTER); overlayPane.setVisible(true); } - private void displayPanelStep(WalkthroughStep step, Pos position) { - Node panelContent = WalkthroughUIFactory.createSidePanel(step, manager); + private void displayPanelContent(Node panelContent, Pos position) { overlayPane.getChildren().clear(); overlayPane.getChildren().add(panelContent); panelContent.setMouseTransparent(false); @@ -150,16 +154,6 @@ private void displayPanelStep(WalkthroughStep step, Pos position) { overlayPane.getRowConstraints().add(rowConstraints); overlayPane.getColumnConstraints().add(columnConstraints); overlayPane.setVisible(true); - - if (step.resolver().isPresent()) { - Optional targetNodeOpt = step.resolver().get().apply(parentStage.getScene()); - if (targetNodeOpt.isPresent()) { - Node targetNode = targetNodeOpt.get(); - backdropHighlight.attach(targetNode); - } else { - LOGGER.warn("Could not resolve target node for step: {}", step.title()); - } - } } /** diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java similarity index 56% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index e569e48122d..5598baa0edf 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUIFactory.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -11,45 +11,40 @@ import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIconView; -import org.jabref.gui.walkthrough.declarative.InfoBlockContentBlock; -import org.jabref.gui.walkthrough.declarative.StepType; -import org.jabref.gui.walkthrough.declarative.TextContentBlock; import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; -import org.jabref.gui.walkthrough.declarative.WalkthroughContentBlock; -import org.jabref.gui.walkthrough.declarative.WalkthroughStep; +import org.jabref.gui.walkthrough.declarative.richtext.ArbitraryJFXBlock; +import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; +import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; +import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; +import org.jabref.gui.walkthrough.declarative.step.PanelStep; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; import org.jabref.logic.l10n.Localization; /** - * Factory for creating walkthrough UI components. + * Renders the walkthrough steps and content blocks into JavaFX Nodes. */ -public class WalkthroughUIFactory { - /** - * Creates a full-screen page using dynamic content from a walkthrough step. - */ - public static VBox createFullscreen(WalkthroughStep step, Walkthrough manager) { +public class WalkthroughRenderer { + public Node render(FullScreenStep step, Walkthrough manager) { VBox container = makePanel(); container.setAlignment(Pos.CENTER); VBox content = new VBox(); content.getStyleClass().add("walkthrough-fullscreen-content"); Label titleLabel = new Label(Localization.lang(step.title())); titleLabel.getStyleClass().add("walkthrough-title"); - VBox contentContainer = makeContent(step); + VBox contentContainer = makeContent(step, manager); content.getChildren().addAll(titleLabel, contentContainer, makeActions(step, manager)); container.getChildren().add(content); return container; } - /** - * Creates a side panel for walkthrough steps. - */ - public static VBox createSidePanel(WalkthroughStep step, Walkthrough manager) { + public Node render(PanelStep step, Walkthrough manager) { VBox panel = makePanel(); - if (step.stepType() == StepType.LEFT_PANEL || step.stepType() == StepType.RIGHT_PANEL) { + if (step.position() == Pos.CENTER_LEFT || step.position() == Pos.CENTER_RIGHT) { panel.getStyleClass().add("walkthrough-side-panel-vertical"); VBox.setVgrow(panel, Priority.ALWAYS); panel.setMaxHeight(Double.MAX_VALUE); - } else if (step.stepType() == StepType.TOP_PANEL || step.stepType() == StepType.BOTTOM_PANEL) { + } else if (step.position() == Pos.TOP_CENTER || step.position() == Pos.BOTTOM_CENTER) { panel.getStyleClass().add("walkthrough-side-panel-horizontal"); HBox.setHgrow(panel, Priority.ALWAYS); panel.setMaxWidth(Double.MAX_VALUE); @@ -71,7 +66,7 @@ public static VBox createSidePanel(WalkthroughStep step, Walkthrough manager) { header.getChildren().addAll(titleLabel, spacer, stepCounter); - VBox contentContainer = makeContent(step); + VBox contentContainer = makeContent(step, manager); Region bottomSpacer = new Region(); VBox.setVgrow(bottomSpacer, Priority.ALWAYS); HBox actions = makeActions(step, manager); @@ -80,43 +75,35 @@ public static VBox createSidePanel(WalkthroughStep step, Walkthrough manager) { return panel; } - private static Node createContentBlock(WalkthroughContentBlock block) { - switch (block.getType()) { - case TEXT: - TextContentBlock textBlock = (TextContentBlock) block; - Label textLabel = new Label(Localization.lang(textBlock.getText())); - textLabel.getStyleClass().add("walkthrough-text-content"); - return textLabel; - case INFO_BLOCK: - InfoBlockContentBlock infoBlock = (InfoBlockContentBlock) block; - HBox infoContainer = new HBox(); - infoContainer.getStyleClass().add("walkthrough-info-container"); - infoContainer.setAlignment(Pos.CENTER_LEFT); - infoContainer.setSpacing(4); - - JabRefIconView icon = new JabRefIconView(IconTheme.JabRefIcons.INTEGRITY_INFO); - icon.getStyleClass().add("walkthrough-info-icon"); - - Label infoLabel = new Label(Localization.lang(infoBlock.getText())); - infoLabel.getStyleClass().add("walkthrough-info-label"); - HBox.setHgrow(infoLabel, Priority.ALWAYS); - - infoContainer.getChildren().addAll(icon, infoLabel); - - VBox infoWrapper = new VBox(infoContainer); - infoWrapper.setAlignment(Pos.CENTER_LEFT); - return infoWrapper; - } - return new Label(Localization.lang("Impossible content block type", block.getType())); + public Node render(ArbitraryJFXBlock block, Walkthrough manager) { + return block.componentFactory().apply(manager); + } + + public Node render(TextBlock textBlock) { + Label textLabel = new Label(Localization.lang(textBlock.text())); + textLabel.getStyleClass().add("walkthrough-text-content"); + return textLabel; } - private static VBox makePanel() { + public Node render(InfoBlock infoBlock) { + HBox infoContainer = new HBox(); + infoContainer.getStyleClass().add("walkthrough-info-container"); + JabRefIconView icon = new JabRefIconView(IconTheme.JabRefIcons.INTEGRITY_INFO); + Label infoLabel = new Label(Localization.lang(infoBlock.text())); + HBox.setHgrow(infoLabel, Priority.ALWAYS); + infoContainer.getChildren().addAll(icon, infoLabel); + VBox infoWrapper = new VBox(infoContainer); + infoWrapper.setAlignment(Pos.CENTER_LEFT); + return infoWrapper; + } + + private VBox makePanel() { VBox container = new VBox(); container.getStyleClass().add("walkthrough-panel"); return container; } - private static HBox makeActions(WalkthroughStep step, Walkthrough manager) { + private HBox makeActions(WalkthroughNode step, Walkthrough manager) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); actions.setSpacing(0); @@ -129,7 +116,7 @@ private static HBox makeActions(WalkthroughStep step, Walkthrough manager) { } HBox rightActions = new HBox(); rightActions.setAlignment(Pos.CENTER_RIGHT); - rightActions.setSpacing(2.5); + rightActions.setSpacing(4); if (step.actions().flatMap(WalkthroughActionsConfig::skipButtonText).isPresent()) { rightActions.getChildren().add(makeSkipButton(step, manager)); } @@ -142,46 +129,56 @@ private static HBox makeActions(WalkthroughStep step, Walkthrough manager) { return actions; } - private static VBox makeContent(WalkthroughStep step) { - VBox contentContainer = new VBox(); - contentContainer.setSpacing(12); - for (WalkthroughContentBlock contentBlock : step.content()) { - Node contentNode = createContentBlock(contentBlock); - contentContainer.getChildren().add(contentNode); - } - return contentContainer; + private VBox makeContent(WalkthroughNode step, Walkthrough manager) { + return new VBox(12, step.content().stream().map(block -> + switch (block) { + case TextBlock textBlock -> render(textBlock); + case InfoBlock infoBlock -> render(infoBlock); + case ArbitraryJFXBlock arbitraryBlock -> + render(arbitraryBlock, manager); + } + ).toArray(Node[]::new)); } - private static Button makeContinueButton(WalkthroughStep step, Walkthrough manager) { + private Button makeContinueButton(WalkthroughNode step, Walkthrough manager) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::continueButtonText) .orElse("Walkthrough continue button"); Button continueButton = new Button(Localization.lang(buttonText)); continueButton.getStyleClass().add("walkthrough-continue-button"); - continueButton.setOnAction(_ -> manager.nextStep()); + continueButton.setOnAction(_ -> { + step.nextStepAction().ifPresent(action -> action.accept(manager)); + manager.nextStep(); + }); return continueButton; } - private static Button makeSkipButton(WalkthroughStep step, Walkthrough manager) { + private Button makeSkipButton(WalkthroughNode step, Walkthrough manager) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::skipButtonText) .orElse("Walkthrough skip to finish"); Button skipButton = new Button(Localization.lang(buttonText)); skipButton.getStyleClass().add("walkthrough-skip-button"); - skipButton.setOnAction(_ -> manager.skip()); + skipButton.setOnAction(_ -> { + step.skipAction().ifPresent(action -> action.accept(manager)); + manager.skip(); + }); return skipButton; } - private static Button makeBackButton(WalkthroughStep step, Walkthrough manager) { + private Button makeBackButton(WalkthroughNode step, Walkthrough manager) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::backButtonText) .orElse("Walkthrough back button"); Button backButton = new Button(Localization.lang(buttonText)); backButton.getStyleClass().add("walkthrough-back-button"); - backButton.setOnAction(_ -> manager.previousStep()); + backButton.setOnAction(_ -> { + step.previousStepAction().ifPresent(action -> action.accept(manager)); + manager.previousStep(); + }); return backButton; } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java deleted file mode 100644 index e7e3d347722..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/InfoBlockContentBlock.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.jabref.gui.walkthrough.declarative; - -// FIXME: Use a record class -/** - * An info block content block for displaying highlighted information with icon in walkthrough steps. - */ -public class InfoBlockContentBlock extends WalkthroughContentBlock { - private final String text; - - public InfoBlockContentBlock(String text) { - this.text = text; - } - - public String getText() { - return text; - } - - @Override - public ContentBlockType getType() { - return ContentBlockType.INFO_BLOCK; - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java deleted file mode 100644 index 570b80fd2e6..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/TextContentBlock.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.jabref.gui.walkthrough.declarative; - -/** - * A text content block for displaying plain text in walkthrough steps. - */ -public class TextContentBlock extends WalkthroughContentBlock { - private final String text; - - public TextContentBlock(String text) { - this.text = text; - } - - public String getText() { - return text; - } - - @Override - public ContentBlockType getType() { - return ContentBlockType.TEXT; - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java deleted file mode 100644 index 48305eca30d..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughContentBlock.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.jabref.gui.walkthrough.declarative; - -/** - * Base class for walkthrough content blocks that can be displayed in walkthrough steps. - */ -public abstract class WalkthroughContentBlock { - public abstract ContentBlockType getType(); - - public enum ContentBlockType { - TEXT, - INFO_BLOCK - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java deleted file mode 100644 index 672301d006b..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughStep.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.jabref.gui.walkthrough.declarative; - -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import javafx.scene.Node; -import javafx.scene.Scene; - -/** - * Represents a single step in a walkthrough with support for different step types and rich content. - * - * @param title The step title (internationalization key) - * @param stepType The type of step (full screen, panels) - * @param content List of content blocks for rich formatting - * @param resolver Optional function to resolve the target Node in the scene - * @param actions Optional custom button configuration - */ -public record WalkthroughStep( - String title, - StepType stepType, - List content, - Optional>> resolver, - Optional actions) { - - public WalkthroughStep(String title, StepType stepType, List content, - Function> resolver) { - this(title, stepType, content, Optional.of(resolver), Optional.of(new WalkthroughActionsConfig(Optional.of("Continue"), - Optional.of("Skip to finish"), Optional.of("Back")))); - } - - public WalkthroughStep(String title, StepType stepType, List content, - WalkthroughActionsConfig buttonConfig) { - this(title, stepType, content, Optional.empty(), Optional.of(buttonConfig)); - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java new file mode 100644 index 00000000000..8b27f2fa081 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java @@ -0,0 +1,11 @@ +package org.jabref.gui.walkthrough.declarative.richtext; + +import java.util.function.Function; + +import javafx.scene.Node; + +import org.jabref.gui.walkthrough.Walkthrough; + +public record ArbitraryJFXBlock(Function componentFactory) + implements WalkthroughRichTextBlock { +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java new file mode 100644 index 00000000000..dd608b794cc --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java @@ -0,0 +1,4 @@ +package org.jabref.gui.walkthrough.declarative.richtext; + +public record InfoBlock(String text) implements WalkthroughRichTextBlock { +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java new file mode 100644 index 00000000000..1b498e03035 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java @@ -0,0 +1,4 @@ +package org.jabref.gui.walkthrough.declarative.richtext; + +public record TextBlock(String text) implements WalkthroughRichTextBlock { +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/WalkthroughRichTextBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/WalkthroughRichTextBlock.java new file mode 100644 index 00000000000..04fa0403a07 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/WalkthroughRichTextBlock.java @@ -0,0 +1,4 @@ +package org.jabref.gui.walkthrough.declarative.richtext; + +public sealed interface WalkthroughRichTextBlock permits InfoBlock, TextBlock, ArbitraryJFXBlock { +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java new file mode 100644 index 00000000000..38bdc46fca9 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java @@ -0,0 +1,30 @@ +package org.jabref.gui.walkthrough.declarative.step; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import javafx.scene.Node; +import javafx.scene.Scene; + +import org.jabref.gui.walkthrough.Walkthrough; +import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; + +public record FullScreenStep( + String title, + List content, + Optional>> resolver, + Optional actions, + Optional> nextStepAction, + Optional> previousStepAction, + Optional> skipAction, + Optional> clickOnNodeAction +) implements WalkthroughNode { + public FullScreenStep(String title, List content, WalkthroughActionsConfig actions) { + this(title, content, Optional.empty(), Optional.of(actions), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + } +} + diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java new file mode 100644 index 00000000000..5557e2b1e7b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -0,0 +1,33 @@ +package org.jabref.gui.walkthrough.declarative.step; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; + +import org.jabref.gui.walkthrough.Walkthrough; +import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; + +public record PanelStep( + String title, + List content, + Optional>> resolver, + Optional actions, + Optional> nextStepAction, + Optional> previousStepAction, + Optional> skipAction, + Optional> clickOnNodeAction, + Pos position +) implements WalkthroughNode { + public PanelStep(String title, List content, + Function> resolver, Pos position) { + this(title, content, Optional.of(resolver), + Optional.of(new WalkthroughActionsConfig(Optional.of("Continue"), Optional.of("Skip"), Optional.of("Back"))), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), position); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/StepType.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/StepType.java similarity index 91% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/StepType.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/StepType.java index 04e25900103..72193d13258 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/StepType.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/StepType.java @@ -1,4 +1,4 @@ -package org.jabref.gui.walkthrough.declarative; +package org.jabref.gui.walkthrough.declarative.step; /** * Defines the different types of walkthrough steps. diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java new file mode 100644 index 00000000000..a21993162ba --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java @@ -0,0 +1,31 @@ +package org.jabref.gui.walkthrough.declarative.step; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import javafx.scene.Node; +import javafx.scene.Scene; + +import org.jabref.gui.walkthrough.Walkthrough; +import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; + +public sealed interface WalkthroughNode permits PanelStep, FullScreenStep { + String title(); + + List content(); + + Optional>> resolver(); + + Optional actions(); + + Optional> nextStepAction(); + + Optional> previousStepAction(); + + Optional> skipAction(); + + Optional> clickOnNodeAction(); +} diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index 86b3025d254..f63918b4f30 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2573,14 +2573,15 @@ journalInfo .grid-cell-b { -fx-border-width: 0 0 0 1; -fx-padding: 0 0 0 0.75em; -fx-spacing: 0.25em; + -fx-alignment: center-left; } -.walkthrough-info-icon { +.walkthrough-info-container .ikonli-font-icon { -fx-icon-color: -jr-theme; -fx-font-size: 1.2em; } -.walkthrough-info-label { +.walkthrough-info-container label { -fx-text-fill: -jr-theme; -fx-font-size: 1.2em; -fx-wrap-text: true; diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 260b7b99fe8..562fb60605a 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2953,6 +2953,7 @@ Step\ of=Step %0 of %1 Complete\ walkthrough=Complete Walkthrough Back=Back Impossible\ content\ block\ type=Impossible content block type: %1 +Could\ not\ resolve\ target\ node\ for\ step=Could not resolve target node for step: %1 # CommandLine Available\ export\ formats\:=Available export formats: From 2039dfc73918460e5f372f43c8b436b9270abf03 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Thu, 5 Jun 2025 23:44:20 -0400 Subject: [PATCH 06/50] Fix backdrop highlight showing on invisible node --- .../org/jabref/gui/util/BackdropHighlight.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java index 18e16673ce1..07fae49b4c5 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java @@ -15,6 +15,7 @@ import javafx.scene.shape.Rectangle; import javafx.scene.shape.Shape; +import com.sun.javafx.scene.NodeHelper; import org.jspecify.annotations.NonNull; /** @@ -67,11 +68,17 @@ public void attach(@NonNull Node node) { addListener(pane.widthProperty()); addListener(pane.heightProperty()); addListener(pane.sceneProperty(), (_, _, newScene) -> { - if (newScene != null) { - addListener(newScene.widthProperty()); - addListener(newScene.heightProperty()); - } updateOverlayLayout(); + if (newScene == null) { + return; + } + addListener(newScene.heightProperty()); + addListener(newScene.widthProperty()); + if (newScene.getWindow() == null) { + return; + } + addListener(newScene.getWindow().widthProperty()); + addListener(newScene.getWindow().heightProperty()); }); } @@ -102,7 +109,8 @@ private void updateOverlayLayout() { return; } - if (!targetNode.isVisible()) { + // ref: https://stackoverflow.com/questions/43887427/alternative-for-removed-impl-istreevisible + if (!targetNode.isVisible() || !NodeHelper.isTreeVisible(targetNode)) { overlayShape.setVisible(false); return; } From 264c3c465237d73d443a62382fbcd40f9a99f059 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 01:00:12 -0400 Subject: [PATCH 07/50] Builder refactor and adding paper directory setup --- .../jabref/gui/walkthrough/Walkthrough.java | 124 ++++++++++-------- .../components/PaperDirectoryChooser.java | 101 ++++++++++++++ .../declarative/WalkthroughActionsConfig.java | 42 +++++- .../declarative/step/FullScreenStep.java | 65 ++++++++- .../declarative/step/PanelStep.java | 73 ++++++++++- .../declarative/step/WalkthroughNode.java | 9 ++ .../main/resources/l10n/JabRef_en.properties | 33 +++-- 7 files changed, 371 insertions(+), 76 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index e1a17462a55..9c6032bcf5b 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -11,12 +11,12 @@ import javafx.stage.Stage; import org.jabref.gui.actions.StandardActions; +import org.jabref.gui.walkthrough.components.PaperDirectoryChooser; import org.jabref.gui.walkthrough.declarative.NodeResolverFactory; import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.richtext.ArbitraryJFXBlock; import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; -import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; -import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; import org.jabref.logic.l10n.Localization; import org.jabref.logic.preferences.WalkthroughPreferences; @@ -29,7 +29,8 @@ public class Walkthrough { private final IntegerProperty currentStep; private final IntegerProperty totalSteps; private final BooleanProperty active; - private final WalkthroughNode[] steps; + // TODO: Consider using Graph instead for complex walkthrough routing e.g., pro user show no walkthrough, new user show full walkthrough, etc. + private final List steps; private Optional overlay = Optional.empty(); private Stage currentStage; @@ -43,43 +44,68 @@ public Walkthrough(WalkthroughPreferences preferences) { this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); - this.steps = new WalkthroughNode[] { - new FullScreenStep( - Localization.lang("Walkthrough welcome title"), - List.of( - new TextBlock(Localization.lang("Walkthrough welcome intro")), - new InfoBlock(Localization.lang("Walkthrough welcome tip"))), - new WalkthroughActionsConfig(Optional.of("Start walkthrough"), - Optional.of("Skip to finish"), Optional.empty())), - - new PanelStep( - Localization.lang("Walkthrough create entry title"), - List.of( - new TextBlock(Localization.lang("Walkthrough create entry description")), - new InfoBlock(Localization.lang("Walkthrough create entry tip"))), - NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY), - javafx.geometry.Pos.BOTTOM_CENTER - ), - - new PanelStep( - Localization.lang("Walkthrough save title"), - List.of( - new TextBlock(Localization.lang("Walkthrough save description")), - new InfoBlock(Localization.lang("Walkthrough save important"))), - NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY), - javafx.geometry.Pos.CENTER_RIGHT), - - new FullScreenStep( - Localization.lang("Walkthrough completion title"), - List.of( - new TextBlock(Localization.lang("Walkthrough completion message")), - new TextBlock(Localization.lang("Walkthrough completion next_steps")), - new InfoBlock(Localization.lang("Walkthrough completion resources"))), - new WalkthroughActionsConfig(Optional.of("Complete walkthrough"), Optional.empty(), - Optional.of("Back"))) - }; - - this.totalSteps = new SimpleIntegerProperty(steps.length); + this.steps = List.of( + WalkthroughNode.fullScreen(Localization.lang("Welcome to JabRef!")) + .content( + new TextBlock(Localization.lang("This quick walkthrough will introduce you to some key features.")), + new InfoBlock(Localization.lang("You can always access this walkthrough from the Help menu.")) + ) + .actions(WalkthroughActionsConfig.builder() + .continueButton(Localization.lang("Start walkthrough")) + .skipButton(Localization.lang("Skip to finish")) + .build()) + .build(), + + WalkthroughNode.fullScreen(Localization.lang("Configure Paper Directory")) + .content( + new TextBlock(Localization.lang("Set up your main file directory where JabRef will look for and store your PDF files and other associated documents.")), + new InfoBlock(Localization.lang("This directory helps JabRef organize your paper files. You can change this later in Preferences.")), + new ArbitraryJFXBlock(_ -> new PaperDirectoryChooser()) + ) + .actions(WalkthroughActionsConfig.all( + Localization.lang("Continue"), + Localization.lang("Skip for Now"), + Localization.lang("Back"))) + .build(), + + WalkthroughNode.panel(Localization.lang("Creating a New Entry")) + .content( + new TextBlock(Localization.lang("Click the highlighted button to start creating a new bibliographic entry.")), + new InfoBlock(Localization.lang("JabRef supports various entry types like articles, books, and more.")) + ) + .resolver(NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY)) + .position(javafx.geometry.Pos.BOTTOM_CENTER) + .actions(WalkthroughActionsConfig.all( + Localization.lang("Continue"), + Localization.lang("Skip for Now"), + Localization.lang("Back"))) + .build(), + + WalkthroughNode.panel(Localization.lang("Saving Your Work")) + .content( + new TextBlock(Localization.lang("Don't forget to save your library. Click the save button.")), + new InfoBlock(Localization.lang("Regularly saving prevents data loss.")) + ) + .resolver(NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)) + .position(javafx.geometry.Pos.CENTER_RIGHT) + .actions(WalkthroughActionsConfig.all( + Localization.lang("Continue"), + Localization.lang("Skip for Now"), + Localization.lang("Back"))) + .build(), + + WalkthroughNode.fullScreen(Localization.lang("Walkthrough Complete!")) + .content( + new TextBlock(Localization.lang("You've completed the basic feature tour.")), + new TextBlock(Localization.lang("Explore more features like groups, fetchers, and customization options.")), + new InfoBlock(Localization.lang("Check our documentation for detailed guides.")) + ) + .actions(WalkthroughActionsConfig.builder() + .continueButton(Localization.lang("Complete walkthrough")) + .backButton(Localization.lang("Back")).build()) + .build() + ); + this.totalSteps = new SimpleIntegerProperty(steps.size()); } /** @@ -127,9 +153,7 @@ public void start(Stage stage) { currentStep.set(0); active.set(true); - getCurrentStep().ifPresent((step) -> overlay.ifPresent( - overlay -> overlay.displayStep(step) - )); + getCurrentStep().ifPresent((step) -> overlay.ifPresent(overlay -> overlay.displayStep(step))); } /** @@ -137,11 +161,9 @@ public void start(Stage stage) { */ public void nextStep() { int nextIndex = currentStep.get() + 1; - if (nextIndex < steps.length) { + if (nextIndex < steps.size()) { currentStep.set(nextIndex); - getCurrentStep().ifPresent((step) -> overlay.ifPresent( - overlay -> overlay.displayStep(step) - )); + getCurrentStep().ifPresent((step) -> overlay.ifPresent(overlay -> overlay.displayStep(step))); } else { preferences.setCompleted(true); stop(); @@ -170,9 +192,7 @@ public void previousStep() { int prevIndex = currentStep.get() - 1; if (prevIndex >= 0) { currentStep.set(prevIndex); - getCurrentStep().ifPresent((step) -> overlay.ifPresent( - overlay -> overlay.displayStep(step) - )); + getCurrentStep().ifPresent((step) -> overlay.ifPresent(overlay -> overlay.displayStep(step))); } } @@ -191,8 +211,8 @@ private void stop() { private Optional getCurrentStep() { int index = currentStep.get(); - if (index >= 0 && index < steps.length) { - return Optional.of(steps[index]); + if (index >= 0 && index < steps.size()) { + return Optional.of(steps.get(index)); } return Optional.empty(); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java new file mode 100644 index 00000000000..e0ee5bdcf1d --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java @@ -0,0 +1,101 @@ +package org.jabref.gui.walkthrough.components; + +import java.io.File; +import java.nio.file.Path; +import java.util.Optional; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.stage.DirectoryChooser; +import javafx.stage.Window; + +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.FilePreferences; +import org.jabref.logic.l10n.Localization; + +import com.airhacks.afterburner.injection.Injector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Chooses the main directory for storing and searching PDF files in JabRef. + */ +public class PaperDirectoryChooser extends HBox { + private static final Logger LOGGER = LoggerFactory.getLogger(PaperDirectoryChooser.class); + private final Label currentDirectoryLabel; + private final StringProperty currentDirectory; + + public PaperDirectoryChooser() { + setSpacing(4); + setPrefHeight(32); + + this.currentDirectory = new SimpleStringProperty(); + currentDirectoryLabel = new Label(); + currentDirectoryLabel.setWrapText(true); + currentDirectoryLabel.setPrefHeight(32); + HBox.setHgrow(currentDirectoryLabel, Priority.ALWAYS); + currentDirectoryLabel.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + + Button browseButton = new Button(Localization.lang("Browse...")); + browseButton.setOnAction(_ -> showDirectoryChooser()); + + getChildren().addAll(browseButton, currentDirectoryLabel); + currentDirectory.addListener((_, _, newVal) -> updateDirectoryDisplay(newVal)); + updateCurrentDirectory(); + } + + private void updateCurrentDirectory() { + try { + GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); + FilePreferences filePreferences = guiPreferences.getFilePreferences(); + Optional mainFileDirectory = filePreferences.getMainFileDirectory(); + currentDirectory.set(mainFileDirectory + .map(Path::toString) + .orElse("")); + } catch (Exception e) { + LOGGER.error("Error while updating current directory", e); + currentDirectory.set(""); + } + } + + private void updateDirectoryDisplay(String directory) { + if (!directory.trim().isEmpty()) { + currentDirectoryLabel.setText(Localization.lang("Current paper directory: %0", directory)); + } else { + currentDirectoryLabel.setText(Localization.lang("No directory currently set")); + } + } + + private void showDirectoryChooser() { + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle(Localization.lang("Choose directory")); + + String currentDir = currentDirectory.get(); + if (currentDir != null && !currentDir.trim().isEmpty()) { + File currentFile = new File(currentDir); + if (currentFile.exists() && currentFile.isDirectory()) { + directoryChooser.setInitialDirectory(currentFile); + } + } + + Window ownerWindow = getScene() != null ? getScene().getWindow() : null; + File selectedDirectory = directoryChooser.showDialog(ownerWindow); + + if (selectedDirectory == null) { + return; + } + + try { + GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); + FilePreferences filePreferences = guiPreferences.getFilePreferences(); + filePreferences.setMainFileDirectory(selectedDirectory.getAbsolutePath()); + currentDirectory.set(selectedDirectory.getAbsolutePath()); + } catch (Exception e) { + currentDirectory.set(selectedDirectory.getAbsolutePath()); + } + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java index 9b4baf4291a..38128811b40 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java @@ -5,13 +5,49 @@ /** * Configuration for walkthrough step buttons. * - * @param continueButtonText Optional text for the continue button. If empty, button is hidden. - * @param skipButtonText Optional text for the skip button. If empty, button is hidden. - * @param backButtonText Optional text for the back button. If empty, button is hidden. + * @param continueButtonText Optional text for the continue button. If empty, button is + * hidden. + * @param skipButtonText Optional text for the skip button. If empty, button is + * hidden. + * @param backButtonText Optional text for the back button. If empty, button is + * hidden. */ public record WalkthroughActionsConfig( Optional continueButtonText, Optional skipButtonText, Optional backButtonText ) { + + public static Builder builder() { + return new Builder(); + } + + public static WalkthroughActionsConfig all(String continueText, String skipText, String backText) { + return new WalkthroughActionsConfig(Optional.of(continueText), Optional.of(skipText), Optional.of(backText)); + } + + public static class Builder { + private Optional continueButtonText = Optional.empty(); + private Optional skipButtonText = Optional.empty(); + private Optional backButtonText = Optional.empty(); + + public Builder continueButton(String text) { + this.continueButtonText = Optional.of(text); + return this; + } + + public Builder skipButton(String text) { + this.skipButtonText = Optional.of(text); + return this; + } + + public Builder backButton(String text) { + this.backButtonText = Optional.of(text); + return this; + } + + public WalkthroughActionsConfig build() { + return new WalkthroughActionsConfig(continueButtonText, skipButtonText, backButtonText); + } + } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java index 38bdc46fca9..9e1d4f5ba69 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java @@ -22,9 +22,68 @@ public record FullScreenStep( Optional> skipAction, Optional> clickOnNodeAction ) implements WalkthroughNode { - public FullScreenStep(String title, List content, WalkthroughActionsConfig actions) { - this(title, content, Optional.empty(), Optional.of(actions), - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + public static Builder builder(String title) { + return new Builder(title); + } + + public static class Builder { + private final String title; + private List content = List.of(); + private Optional>> resolver = Optional.empty(); + private Optional actions = Optional.empty(); + private Optional> nextStepAction = Optional.empty(); + private Optional> previousStepAction = Optional.empty(); + private Optional> skipAction = Optional.empty(); + private Optional> clickOnNodeAction = Optional.empty(); + + private Builder(String title) { + this.title = title; + } + + public Builder content(WalkthroughRichTextBlock... blocks) { + this.content = List.of(blocks); + return this; + } + + public Builder content(List content) { + this.content = content; + return this; + } + + public Builder resolver(Function> resolver) { + this.resolver = Optional.of(resolver); + return this; + } + + public Builder actions(WalkthroughActionsConfig actions) { + this.actions = Optional.of(actions); + return this; + } + + public Builder nextStepAction(Consumer nextStepAction) { + this.nextStepAction = Optional.of(nextStepAction); + return this; + } + + public Builder previousStepAction(Consumer previousStepAction) { + this.previousStepAction = Optional.of(previousStepAction); + return this; + } + + public Builder skipAction(Consumer skipAction) { + this.skipAction = Optional.of(skipAction); + return this; + } + + public Builder clickOnNodeAction(Consumer clickOnNodeAction) { + this.clickOnNodeAction = Optional.of(clickOnNodeAction); + return this; + } + + public FullScreenStep build() { + return new FullScreenStep(title, content, resolver, actions, nextStepAction, + previousStepAction, skipAction, clickOnNodeAction); + } } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index 5557e2b1e7b..480d65ece1c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -24,10 +24,73 @@ public record PanelStep( Optional> clickOnNodeAction, Pos position ) implements WalkthroughNode { - public PanelStep(String title, List content, - Function> resolver, Pos position) { - this(title, content, Optional.of(resolver), - Optional.of(new WalkthroughActionsConfig(Optional.of("Continue"), Optional.of("Skip"), Optional.of("Back"))), - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), position); + public static Builder builder(String title) { + return new Builder(title); + } + + public static class Builder { + private final String title; + private List content = List.of(); + private Optional>> resolver = Optional.empty(); + private Optional actions = Optional.empty(); + private Optional> nextStepAction = Optional.empty(); + private Optional> previousStepAction = Optional.empty(); + private Optional> skipAction = Optional.empty(); + private Optional> clickOnNodeAction = Optional.empty(); + private Pos position = Pos.CENTER; + + private Builder(String title) { + this.title = title; + } + + public Builder content(WalkthroughRichTextBlock... blocks) { + this.content = List.of(blocks); + return this; + } + + public Builder content(List content) { + this.content = content; + return this; + } + + public Builder resolver(Function> resolver) { + this.resolver = Optional.of(resolver); + return this; + } + + public Builder actions(WalkthroughActionsConfig actions) { + this.actions = Optional.of(actions); + return this; + } + + public Builder nextStepAction(Consumer nextStepAction) { + this.nextStepAction = Optional.of(nextStepAction); + return this; + } + + public Builder previousStepAction(Consumer previousStepAction) { + this.previousStepAction = Optional.of(previousStepAction); + return this; + } + + public Builder skipAction(Consumer skipAction) { + this.skipAction = Optional.of(skipAction); + return this; + } + + public Builder clickOnNodeAction(Consumer clickOnNodeAction) { + this.clickOnNodeAction = Optional.of(clickOnNodeAction); + return this; + } + + public Builder position(Pos position) { + this.position = position; + return this; + } + + public PanelStep build() { + return new PanelStep(title, content, resolver, actions, nextStepAction, + previousStepAction, skipAction, clickOnNodeAction, position); + } } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java index a21993162ba..673b8051902 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java @@ -28,4 +28,13 @@ public sealed interface WalkthroughNode permits PanelStep, FullScreenStep { Optional> skipAction(); Optional> clickOnNodeAction(); + + // Static factory methods for builders + static FullScreenStep.Builder fullScreen(String key) { + return FullScreenStep.builder(key); + } + + static PanelStep.Builder panel(String title) { + return PanelStep.builder(title); + } } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 562fb60605a..07062d3adb7 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2932,19 +2932,26 @@ You\ must\ specify\ an\ identifier.=You must specify an identifier. You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) citations. # Walkthrough -Walkthrough\ welcome\ title=Welcome to JabRef! -Walkthrough\ welcome\ intro=This quick walkthrough will introduce you to some key features. -Walkthrough\ welcome\ tip=You can always access this walkthrough from the Help menu. -Walkthrough\ create\ entry\ title=Creating a New Entry -Walkthrough\ create\ entry\ description=Click the highlighted button to start creating a new bibliographic entry. -Walkthrough\ create\ entry\ tip=JabRef supports various entry types like articles, books, and more. -Walkthrough\ save\ title=Saving Your Work -Walkthrough\ save\ description=Don't forget to save your library. Click the save button. -Walkthrough\ save\ important=Regularly saving prevents data loss. -Walkthrough\ completion\ title=Walkthrough Complete! -Walkthrough\ completion\ message=You've completed the basic feature tour. -Walkthrough\ completion\ next_steps=Explore more features like groups, fetchers, and customization options. -Walkthrough\ completion\ resources=Check our documentation for detailed guides. +Welcome\ to\ JabRef!=Welcome to JabRef! +This\ quick\ walkthrough\ will\ introduce\ you\ to\ some\ key\ features.=This quick walkthrough will introduce you to some key features. +You\ can\ always\ access\ this\ walkthrough\ from\ the\ Help\ menu.=You can always access this walkthrough from the Help menu. +Creating\ a\ New\ Entry=Creating a New Entry +Click\ the\ highlighted\ button\ to\ start\ creating\ a\ new\ bibliographic\ entry.=Click the highlighted button to start creating a new bibliographic entry. +JabRef\ supports\ various\ entry\ types\ like\ articles,\ books,\ and\ more.=JabRef supports various entry types like articles, books, and more. +Saving\ Your\ Work=Saving Your Work +Don't\ forget\ to\ save\ your\ library.\ Click\ the\ save\ button.=Don't forget to save your library. Click the save button. +Regularly\ saving\ prevents\ data\ loss.=Regularly saving prevents data loss. +Configure\ Paper\ Directory=Configure Paper Directory +Set\ up\ your\ main\ file\ directory\ where\ JabRef\ will\ look\ for\ and\ store\ your\ PDF\ files\ and\ other\ associated\ documents.=Set up your main file directory where JabRef will look for and store your PDF files and other associated documents. +This\ directory\ helps\ JabRef\ organize\ your\ paper\ files.\ You\ can\ change\ this\ later\ in\ Preferences.=This directory helps JabRef organize your paper files. You can change this later in Preferences. +Skip\ for\ Now=Skip for Now +Browse...=Browse... +Current\ paper\ directory\:\ %0=Current paper directory: %0 +No\ directory\ currently\ set=No directory currently set +Walkthrough\ Complete!=Walkthrough Complete! +You've\ completed\ the\ basic\ feature\ tour.=You've completed the basic feature tour. +Explore\ more\ features\ like\ groups,\ fetchers,\ and\ customization\ options.=Explore more features like groups, fetchers, and customization options. +Check\ our\ documentation\ for\ detailed\ guides.=Check our documentation for detailed guides. Skip=Skip Finish=Finish Skip\ to\ finish=Skip to Finish From 6f80afe572a0748de54ae98aa433f84ffcbdaf8e Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 09:20:32 -0400 Subject: [PATCH 08/50] Resolve PR comments --- .../src/main/java/org/jabref/gui/walkthrough/Walkthrough.java | 2 +- .../java/org/jabref/gui/walkthrough/WalkthroughOverlay.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 9c6032bcf5b..86e3543e4ba 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -35,7 +35,7 @@ public class Walkthrough { private Stage currentStage; /** - * Creates a new walkthrough manager with the specified preferences. + * Creates a new walkthrough with the specified preferences. * * @param preferences The walkthrough preferences to use */ diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 569ffa3879e..92a63472b5a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -1,6 +1,5 @@ package org.jabref.gui.walkthrough; -import java.util.Objects; import java.util.Optional; import javafx.geometry.Pos; @@ -48,7 +47,7 @@ public WalkthroughOverlay(Stage stage, Walkthrough manager) { overlayPane.setMaxHeight(Double.MAX_VALUE); Scene scene = stage.getScene(); - Objects.requireNonNull(scene); + assert scene != null; originalRoot = (Pane) scene.getRoot(); stackPane = new StackPane(); From f2c4add44fc151d3e8d52af718964870a45dae1c Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 09:23:35 -0400 Subject: [PATCH 09/50] Update JabRef_en keys --- .../java/org/jabref/gui/walkthrough/WalkthroughOverlay.java | 2 +- .../java/org/jabref/gui/walkthrough/WalkthroughRenderer.java | 2 +- jablib/src/main/resources/l10n/JabRef_en.properties | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 92a63472b5a..3ec4b1626ab 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -86,7 +86,7 @@ public void displayStep(WalkthroughNode step) { step.clickOnNodeAction().ifPresent(action -> targetNode.setOnMouseClicked(_ -> action.accept(manager))); } else { - LOGGER.warn(Localization.lang("Could not resolve target node for step", step.title())); + LOGGER.warn(Localization.lang("Could not resolve target node for step: %1", step.title())); } } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index 5598baa0edf..d61d334de5e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -59,7 +59,7 @@ public Node render(PanelStep step, Walkthrough manager) { Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); - Label stepCounter = new Label(Localization.lang("Step of", + Label stepCounter = new Label(Localization.lang("Step %0 of %1", String.valueOf(manager.currentStepProperty().get() + 1), String.valueOf(manager.totalStepsProperty().get()))); stepCounter.getStyleClass().add("walkthrough-step-counter"); diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 07062d3adb7..8cafd812045 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2956,11 +2956,10 @@ Skip=Skip Finish=Finish Skip\ to\ finish=Skip to Finish Start\ walkthrough=Start Walkthrough -Step\ of=Step %0 of %1 +Step\ %0\ of\ %1=Step %0 of %1 Complete\ walkthrough=Complete Walkthrough Back=Back -Impossible\ content\ block\ type=Impossible content block type: %1 -Could\ not\ resolve\ target\ node\ for\ step=Could not resolve target node for step: %1 +Could\ not\ resolve\ target\ node\ for\ step\:\ %1=Could not resolve target node for step: %1 # CommandLine Available\ export\ formats\:=Available export formats: From da595c4586303bdbed2ba77f11f8401849428353 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 09:25:33 -0400 Subject: [PATCH 10/50] Update JabRef_en keys and remove title case --- .../java/org/jabref/gui/walkthrough/Walkthrough.java | 10 +++++----- jablib/src/main/resources/l10n/JabRef_en.properties | 7 +++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 86e3543e4ba..a60c93be984 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -45,7 +45,7 @@ public Walkthrough(WalkthroughPreferences preferences) { this.active = new SimpleBooleanProperty(false); this.steps = List.of( - WalkthroughNode.fullScreen(Localization.lang("Welcome to JabRef!")) + WalkthroughNode.fullScreen(Localization.lang("Welcome to JabRef")) .content( new TextBlock(Localization.lang("This quick walkthrough will introduce you to some key features.")), new InfoBlock(Localization.lang("You can always access this walkthrough from the Help menu.")) @@ -56,7 +56,7 @@ public Walkthrough(WalkthroughPreferences preferences) { .build()) .build(), - WalkthroughNode.fullScreen(Localization.lang("Configure Paper Directory")) + WalkthroughNode.fullScreen(Localization.lang("Configure paper directory")) .content( new TextBlock(Localization.lang("Set up your main file directory where JabRef will look for and store your PDF files and other associated documents.")), new InfoBlock(Localization.lang("This directory helps JabRef organize your paper files. You can change this later in Preferences.")), @@ -68,7 +68,7 @@ public Walkthrough(WalkthroughPreferences preferences) { Localization.lang("Back"))) .build(), - WalkthroughNode.panel(Localization.lang("Creating a New Entry")) + WalkthroughNode.panel(Localization.lang("Creating a new entry")) .content( new TextBlock(Localization.lang("Click the highlighted button to start creating a new bibliographic entry.")), new InfoBlock(Localization.lang("JabRef supports various entry types like articles, books, and more.")) @@ -81,7 +81,7 @@ public Walkthrough(WalkthroughPreferences preferences) { Localization.lang("Back"))) .build(), - WalkthroughNode.panel(Localization.lang("Saving Your Work")) + WalkthroughNode.panel(Localization.lang("Saving your work")) .content( new TextBlock(Localization.lang("Don't forget to save your library. Click the save button.")), new InfoBlock(Localization.lang("Regularly saving prevents data loss.")) @@ -94,7 +94,7 @@ public Walkthrough(WalkthroughPreferences preferences) { Localization.lang("Back"))) .build(), - WalkthroughNode.fullScreen(Localization.lang("Walkthrough Complete!")) + WalkthroughNode.fullScreen(Localization.lang("Walkthrough complete")) .content( new TextBlock(Localization.lang("You've completed the basic feature tour.")), new TextBlock(Localization.lang("Explore more features like groups, fetchers, and customization options.")), diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 8cafd812045..be70ce33161 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2932,23 +2932,22 @@ You\ must\ specify\ an\ identifier.=You must specify an identifier. You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) citations. # Walkthrough -Welcome\ to\ JabRef!=Welcome to JabRef! This\ quick\ walkthrough\ will\ introduce\ you\ to\ some\ key\ features.=This quick walkthrough will introduce you to some key features. You\ can\ always\ access\ this\ walkthrough\ from\ the\ Help\ menu.=You can always access this walkthrough from the Help menu. -Creating\ a\ New\ Entry=Creating a New Entry +Creating\ a\ new\ entry=Creating a new entry Click\ the\ highlighted\ button\ to\ start\ creating\ a\ new\ bibliographic\ entry.=Click the highlighted button to start creating a new bibliographic entry. JabRef\ supports\ various\ entry\ types\ like\ articles,\ books,\ and\ more.=JabRef supports various entry types like articles, books, and more. Saving\ Your\ Work=Saving Your Work Don't\ forget\ to\ save\ your\ library.\ Click\ the\ save\ button.=Don't forget to save your library. Click the save button. Regularly\ saving\ prevents\ data\ loss.=Regularly saving prevents data loss. -Configure\ Paper\ Directory=Configure Paper Directory +Configure\ paper\ directory=Configure paper directory Set\ up\ your\ main\ file\ directory\ where\ JabRef\ will\ look\ for\ and\ store\ your\ PDF\ files\ and\ other\ associated\ documents.=Set up your main file directory where JabRef will look for and store your PDF files and other associated documents. This\ directory\ helps\ JabRef\ organize\ your\ paper\ files.\ You\ can\ change\ this\ later\ in\ Preferences.=This directory helps JabRef organize your paper files. You can change this later in Preferences. Skip\ for\ Now=Skip for Now Browse...=Browse... Current\ paper\ directory\:\ %0=Current paper directory: %0 No\ directory\ currently\ set=No directory currently set -Walkthrough\ Complete!=Walkthrough Complete! +Walkthrough\ complete=Walkthrough complete You've\ completed\ the\ basic\ feature\ tour.=You've completed the basic feature tour. Explore\ more\ features\ like\ groups,\ fetchers,\ and\ customization\ options.=Explore more features like groups, fetchers, and customization options. Check\ our\ documentation\ for\ detailed\ guides.=Check our documentation for detailed guides. From b43936f6772c66c18a1cf9ba76b5ea876d015f12 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 09:28:19 -0400 Subject: [PATCH 11/50] Fix paper directory picker --- .../components/PaperDirectoryChooser.java | 17 +++++------------ .../main/resources/l10n/JabRef_en.properties | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java index e0ee5bdcf1d..9bcdbe9c8b2 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java @@ -49,24 +49,17 @@ public PaperDirectoryChooser() { } private void updateCurrentDirectory() { - try { - GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); - FilePreferences filePreferences = guiPreferences.getFilePreferences(); - Optional mainFileDirectory = filePreferences.getMainFileDirectory(); - currentDirectory.set(mainFileDirectory - .map(Path::toString) - .orElse("")); - } catch (Exception e) { - LOGGER.error("Error while updating current directory", e); - currentDirectory.set(""); - } + GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); + FilePreferences filePreferences = guiPreferences.getFilePreferences(); + Optional mainFileDirectory = filePreferences.getMainFileDirectory(); + currentDirectory.set(mainFileDirectory.map(Path::toString).orElse("")); } private void updateDirectoryDisplay(String directory) { if (!directory.trim().isEmpty()) { currentDirectoryLabel.setText(Localization.lang("Current paper directory: %0", directory)); } else { - currentDirectoryLabel.setText(Localization.lang("No directory currently set")); + currentDirectoryLabel.setText(Localization.lang("No directory currently set.")); } } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index be70ce33161..1518e73d391 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2946,7 +2946,7 @@ This\ directory\ helps\ JabRef\ organize\ your\ paper\ files.\ You\ can\ change\ Skip\ for\ Now=Skip for Now Browse...=Browse... Current\ paper\ directory\:\ %0=Current paper directory: %0 -No\ directory\ currently\ set=No directory currently set +No\ directory\ currently\ set.=No directory currently set. Walkthrough\ complete=Walkthrough complete You've\ completed\ the\ basic\ feature\ tour.=You've completed the basic feature tour. Explore\ more\ features\ like\ groups,\ fetchers,\ and\ customization\ options.=Explore more features like groups, fetchers, and customization options. From cc8a553b97edbd13352aff7e448f4d86d1edc94d Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 09:40:27 -0400 Subject: [PATCH 12/50] Fix modernizer --- .../components/PaperDirectoryChooser.java | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java index 9bcdbe9c8b2..2d267798115 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java @@ -1,6 +1,7 @@ package org.jabref.gui.walkthrough.components; import java.io.File; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; @@ -18,14 +19,11 @@ import org.jabref.logic.l10n.Localization; import com.airhacks.afterburner.injection.Injector; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Chooses the main directory for storing and searching PDF files in JabRef. */ public class PaperDirectoryChooser extends HBox { - private static final Logger LOGGER = LoggerFactory.getLogger(PaperDirectoryChooser.class); private final Label currentDirectoryLabel; private final StringProperty currentDirectory; @@ -69,9 +67,9 @@ private void showDirectoryChooser() { String currentDir = currentDirectory.get(); if (currentDir != null && !currentDir.trim().isEmpty()) { - File currentFile = new File(currentDir); - if (currentFile.exists() && currentFile.isDirectory()) { - directoryChooser.setInitialDirectory(currentFile); + Path currentPath = Path.of(currentDir); + if (Files.exists(currentPath) && Files.isDirectory(currentPath)) { + directoryChooser.setInitialDirectory(currentPath.toFile()); } } @@ -82,13 +80,9 @@ private void showDirectoryChooser() { return; } - try { - GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); - FilePreferences filePreferences = guiPreferences.getFilePreferences(); - filePreferences.setMainFileDirectory(selectedDirectory.getAbsolutePath()); - currentDirectory.set(selectedDirectory.getAbsolutePath()); - } catch (Exception e) { - currentDirectory.set(selectedDirectory.getAbsolutePath()); - } + GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); + FilePreferences filePreferences = guiPreferences.getFilePreferences(); + filePreferences.setMainFileDirectory(selectedDirectory.getAbsolutePath()); + currentDirectory.set(selectedDirectory.getAbsolutePath()); } } From 197f6d8e60befbb15e8c91329d2fa782964f2c27 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 13:38:44 -0400 Subject: [PATCH 13/50] Add edit entry step (and bugs on IndexOutOfBound --- .../main/java/org/jabref/gui/JabRefGUI.java | 11 +- .../org/jabref/gui/frame/JabRefFrame.java | 18 +- .../jabref/gui/util/BackdropHighlight.java | 16 +- .../jabref/gui/walkthrough/Walkthrough.java | 228 +++++++++++++----- .../gui/walkthrough/WalkthroughOverlay.java | 92 ++++--- .../gui/walkthrough/WalkthroughRenderer.java | 56 ++--- .../main/resources/org/jabref/gui/Base.css | 12 +- .../org/jabref/logic/l10n/Localization.java | 42 ++-- .../main/resources/l10n/JabRef_en.properties | 6 +- 9 files changed, 299 insertions(+), 182 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index b1932297f0c..f0d483dcba7 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -36,7 +36,6 @@ import org.jabref.logic.l10n.Localization; import org.jabref.logic.net.ProxyRegisterer; import org.jabref.logic.os.OS; -import org.jabref.logic.preferences.WalkthroughPreferences; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.server.RemoteListenerServerManager; @@ -77,7 +76,6 @@ public class JabRefGUI extends Application { private static ClipBoardManager clipBoardManager; private static DialogService dialogService; private static JabRefFrame mainFrame; - private static Walkthrough walkthroughManager; private static RemoteListenerServerManager remoteListenerServerManager; private Stage mainStage; @@ -189,11 +187,6 @@ public void initialize() { dialogService, taskExecutor); Injector.setModelOrService(AiService.class, aiService); - - // Initialize walkthrough manager - WalkthroughPreferences walkthroughPreferences = preferences.getWalkthroughPreferences(); - JabRefGUI.walkthroughManager = new Walkthrough(walkthroughPreferences); - Injector.setModelOrService(Walkthrough.class, walkthroughManager); } private void setupProxy() { @@ -304,8 +297,8 @@ public void onShowing(WindowEvent event) { } // Check if walkthrough should be shown - if (!walkthroughManager.isCompleted()) { - walkthroughManager.start(mainStage); + if (!preferences.getWalkthroughPreferences().isCompleted()) { + mainFrame.showWalkthrough(); } }); } diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java index 52ba69e2ca7..d8992f540a7 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -54,6 +54,7 @@ import org.jabref.gui.undo.RedoAction; import org.jabref.gui.undo.UndoAction; import org.jabref.gui.util.BindingsHelper; +import org.jabref.gui.walkthrough.Walkthrough; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; import org.jabref.logic.journals.JournalAbbreviationRepository; @@ -492,6 +493,14 @@ public void showLibraryTab(@NonNull LibraryTab libraryTab) { tabbedPane.getSelectionModel().select(libraryTab); } + public void showWalkthrough() { + Walkthrough walkthrough = new Walkthrough( + preferences.getWalkthroughPreferences(), + this + ); + walkthrough.start(mainStage); + } + public void showWelcomeTab() { // The loop iterates through all tabs in tabbedPane to check if a WelcomeTab already exists. If yes, it is selected. for (Tab tab : tabbedPane.getTabs()) { @@ -521,9 +530,9 @@ public void showWelcomeTab() { } /** - * Opens a new tab with existing data. - * Asynchronous loading is done at {@link LibraryTab#createLibraryTab}. - * Similar method: {@link OpenDatabaseAction#openTheFile(Path)} + * Opens a new tab with existing data. Asynchronous loading is done at + * {@link LibraryTab#createLibraryTab}. Similar method: + * {@link OpenDatabaseAction#openTheFile(Path)} */ public void addTab(@NonNull BibDatabaseContext databaseContext, boolean raisePanel) { Objects.requireNonNull(databaseContext); @@ -681,7 +690,8 @@ public CloseDatabaseAction(LibraryTabContainer tabContainer, LibraryTab libraryT } /** - * Using this constructor will result in executing the command on the currently open library tab + * Using this constructor will result in executing the command on the currently + * open library tab */ public CloseDatabaseAction(LibraryTabContainer tabContainer, StateManager stateManager) { this(tabContainer, null, stateManager); diff --git a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java index 07fae49b4c5..33a1ecdca53 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java @@ -6,10 +6,8 @@ import javafx.beans.InvalidationListener; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; -import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; import javafx.scene.Node; -import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; @@ -46,7 +44,6 @@ public BackdropHighlight(@NonNull Pane pane) { this.overlayShape = Shape.subtract(backdrop, hole); this.overlayShape.setFill(OVERLAY_COLOR); - this.overlayShape.setMouseTransparent(true); this.overlayShape.setVisible(false); this.pane.getChildren().add(overlayShape); @@ -62,7 +59,6 @@ public void attach(@NonNull Node node) { this.targetNode = node; updateOverlayLayout(); - addListener(targetNode.localToSceneTransformProperty()); addListener(targetNode.visibleProperty()); addListener(pane.widthProperty()); @@ -104,13 +100,13 @@ private void addListener(ObservableValue property, ChangeListener list } private void updateOverlayLayout() { - if (targetNode == null || targetNode.getScene() == null || pane.getScene() == null) { + if (targetNode == null || targetNode.getScene() == null) { overlayShape.setVisible(false); return; } // ref: https://stackoverflow.com/questions/43887427/alternative-for-removed-impl-istreevisible - if (!targetNode.isVisible() || !NodeHelper.isTreeVisible(targetNode)) { + if (!NodeHelper.isTreeVisible(targetNode)) { overlayShape.setVisible(false); return; } @@ -128,13 +124,6 @@ private void updateOverlayLayout() { return; } - Scene currentScene = pane.getScene(); - Bounds sceneViewportBounds = new BoundingBox(0, 0, currentScene.getWidth(), currentScene.getHeight()); - if (!nodeBoundsInScene.intersects(sceneViewportBounds)) { - overlayShape.setVisible(false); - return; - } - backdrop.setX(0); backdrop.setY(0); backdrop.setWidth(pane.getWidth()); @@ -155,7 +144,6 @@ private void updateOverlayLayout() { this.overlayShape = Shape.subtract(backdrop, hole); this.overlayShape.setFill(OVERLAY_COLOR); - this.overlayShape.setMouseTransparent(true); this.overlayShape.setVisible(true); this.pane.getChildren().add(oldIndex, this.overlayShape); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index a60c93be984..d47fb4579dd 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -8,18 +8,40 @@ import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; +import javafx.geometry.Pos; +import javafx.scene.control.Button; import javafx.stage.Stage; +import org.jabref.gui.ClipBoardManager; +import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTab; +import org.jabref.gui.StateManager; import org.jabref.gui.actions.StandardActions; +import org.jabref.gui.frame.JabRefFrame; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.walkthrough.components.PaperDirectoryChooser; import org.jabref.gui.walkthrough.declarative.NodeResolverFactory; import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; import org.jabref.gui.walkthrough.declarative.richtext.ArbitraryJFXBlock; import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; +import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; +import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.logic.ai.AiService; import org.jabref.logic.l10n.Localization; import org.jabref.logic.preferences.WalkthroughPreferences; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.util.FileUpdateMonitor; + +import com.airhacks.afterburner.injection.Injector; +import org.jspecify.annotations.NonNull; /** * Manages a walkthrough session by coordinating steps. @@ -29,82 +51,164 @@ public class Walkthrough { private final IntegerProperty currentStep; private final IntegerProperty totalSteps; private final BooleanProperty active; + // TODO: Consider using Graph instead for complex walkthrough routing e.g., pro user show no walkthrough, new user show full walkthrough, etc. private final List steps; private Optional overlay = Optional.empty(); private Stage currentStage; + private final BibDatabaseContext database; /** * Creates a new walkthrough with the specified preferences. * * @param preferences The walkthrough preferences to use */ - public Walkthrough(WalkthroughPreferences preferences) { + public Walkthrough(WalkthroughPreferences preferences, @NonNull JabRefFrame frame) { this.preferences = preferences; + this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); + this.database = new BibDatabaseContext(); + + FullScreenStep welcomeNode = WalkthroughNode + .fullScreen(Localization.lang("Welcome to JabRef")) + .content( + new TextBlock(Localization.lang("This quick walkthrough will introduce you to some key features.")), + new InfoBlock(Localization.lang("You can always access this walkthrough from the Help menu.")) + ) + .actions(WalkthroughActionsConfig.builder() + .continueButton(Localization.lang("Start walkthrough")) + .skipButton(Localization.lang("Skip to finish")) + .build()) + .build(); + WalkthroughActionsConfig actions = WalkthroughActionsConfig.all( + Localization.lang("Continue"), + Localization.lang("Skip for Now"), + Localization.lang("Back")); + FullScreenStep paperDirectoryNode = WalkthroughNode + .fullScreen(Localization.lang("Configure paper directory")) + .content( + new TextBlock(Localization.lang("Set up your main file directory where JabRef will look for and store your PDF files and other associated documents.")), + new InfoBlock(Localization.lang("This directory helps JabRef organize your paper files. You can change this later in Preferences.")), + new ArbitraryJFXBlock(_ -> new PaperDirectoryChooser()) + ) + .actions(actions) + .nextStepAction(walkthrough -> { + if (frame.getCurrentLibraryTab() != null) { + walkthrough.nextStep(); + return; + } + + DialogService dialogService = Injector.instantiateModelOrService(DialogService.class); + AiService aiService = Injector.instantiateModelOrService(AiService.class); + GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); + StateManager stateManager = Injector.instantiateModelOrService(StateManager.class); + FileUpdateMonitor fileUpdateMonitor = Injector.instantiateModelOrService(FileUpdateMonitor.class); + BibEntryTypesManager entryTypesManager = Injector.instantiateModelOrService(BibEntryTypesManager.class); + CountingUndoManager undoManager = Injector.instantiateModelOrService(CountingUndoManager.class); + ClipBoardManager clipBoardManager = Injector.instantiateModelOrService(ClipBoardManager.class); + TaskExecutor taskExecutor = Injector.instantiateModelOrService(TaskExecutor.class); - this.steps = List.of( - WalkthroughNode.fullScreen(Localization.lang("Welcome to JabRef")) - .content( - new TextBlock(Localization.lang("This quick walkthrough will introduce you to some key features.")), - new InfoBlock(Localization.lang("You can always access this walkthrough from the Help menu.")) - ) - .actions(WalkthroughActionsConfig.builder() - .continueButton(Localization.lang("Start walkthrough")) - .skipButton(Localization.lang("Skip to finish")) - .build()) - .build(), - - WalkthroughNode.fullScreen(Localization.lang("Configure paper directory")) - .content( - new TextBlock(Localization.lang("Set up your main file directory where JabRef will look for and store your PDF files and other associated documents.")), - new InfoBlock(Localization.lang("This directory helps JabRef organize your paper files. You can change this later in Preferences.")), - new ArbitraryJFXBlock(_ -> new PaperDirectoryChooser()) - ) - .actions(WalkthroughActionsConfig.all( - Localization.lang("Continue"), - Localization.lang("Skip for Now"), - Localization.lang("Back"))) - .build(), - - WalkthroughNode.panel(Localization.lang("Creating a new entry")) - .content( - new TextBlock(Localization.lang("Click the highlighted button to start creating a new bibliographic entry.")), - new InfoBlock(Localization.lang("JabRef supports various entry types like articles, books, and more.")) - ) - .resolver(NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY)) - .position(javafx.geometry.Pos.BOTTOM_CENTER) - .actions(WalkthroughActionsConfig.all( - Localization.lang("Continue"), - Localization.lang("Skip for Now"), - Localization.lang("Back"))) - .build(), - - WalkthroughNode.panel(Localization.lang("Saving your work")) - .content( - new TextBlock(Localization.lang("Don't forget to save your library. Click the save button.")), - new InfoBlock(Localization.lang("Regularly saving prevents data loss.")) - ) - .resolver(NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)) - .position(javafx.geometry.Pos.CENTER_RIGHT) - .actions(WalkthroughActionsConfig.all( - Localization.lang("Continue"), - Localization.lang("Skip for Now"), - Localization.lang("Back"))) - .build(), - - WalkthroughNode.fullScreen(Localization.lang("Walkthrough complete")) - .content( - new TextBlock(Localization.lang("You've completed the basic feature tour.")), - new TextBlock(Localization.lang("Explore more features like groups, fetchers, and customization options.")), - new InfoBlock(Localization.lang("Check our documentation for detailed guides.")) - ) - .actions(WalkthroughActionsConfig.builder() - .continueButton(Localization.lang("Complete walkthrough")) - .backButton(Localization.lang("Back")).build()) - .build() - ); + LibraryTab libraryTab = LibraryTab.createLibraryTab( + this.database, + frame, + dialogService, + aiService, + guiPreferences, + stateManager, + fileUpdateMonitor, + entryTypesManager, + undoManager, + clipBoardManager, + taskExecutor); + frame.addTab(libraryTab, true); + walkthrough.nextStep(); + }) + .build(); + PanelStep createNode = WalkthroughNode + .panel(Localization.lang("Creating a new entry")) + .content( + new TextBlock(Localization.lang("Click the highlighted button to start creating a new bibliographic entry.")), + new InfoBlock(Localization.lang("JabRef supports various entry types like articles, books, and more.")), + new TextBlock(Localization.lang("In the entry editor that opens after clicking the button, choose \"Article\" as the entry type.")) + ) + .resolver(NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY)) + .position(Pos.BOTTOM_CENTER) + .actions(actions) + .nextStepAction(walkthrough -> { + NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY).apply(walkthrough.currentStage.getScene()).ifPresent(node -> { + if (node instanceof Button button) { + button.fire(); + } + }); + walkthrough.nextStep(frame.getMainStage()); + }) + .build(); + // FIXME: Index out of bound. + PanelStep editNode = WalkthroughNode + .panel(Localization.lang("Fill in the entry details")) + .content( + new TextBlock(Localization.lang("In the title field, enter \"JabRef: BibTeX-based literature management software\".")), + new TextBlock(Localization.lang("In the journal field, enter \"TUGboat\".")), + new InfoBlock(Localization.lang("You can fill in more details later. JabRef supports many entry types and fields.")) + ) + .resolver(NodeResolverFactory.forSelector(".editorPane")) + .position(Pos.TOP_CENTER) + .actions(actions) + .nextStepAction(walkthrough -> { + BibEntry exampleEntry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Oliver Kopp and Carl Christian Snethlage and Christoph Schwentker") + .withField(StandardField.TITLE, "JabRef: BibTeX-based literature management software") + .withField(StandardField.JOURNAL, "TUGboat") + .withField(StandardField.VOLUME, "44") + .withField(StandardField.NUMBER, "3") + .withField(StandardField.PAGES, "441--447") + .withField(StandardField.DOI, "10.47397/tb/44-3/tb138kopp-jabref") + .withField(StandardField.ISSN, "0896-3207") + .withField(StandardField.ISSUE, "138") + .withField(StandardField.YEAR, "2023") + .withChanged(true); + var db = database.getDatabase(); + for (BibEntry entry : db.getEntries()) { + db.removeEntry(entry); + } + db.insertEntry(exampleEntry); + walkthrough.nextStep(frame.getMainStage()); + }) + .build(); + // FIXME: Index out of bound. + PanelStep saveNode = WalkthroughNode + .panel(Localization.lang("Saving your work")) + .content( + new TextBlock(Localization.lang("Don't forget to save your library. Click the save button.")), + new InfoBlock(Localization.lang("Regularly saving prevents data loss.")) + ) + .resolver(NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)) + .position(Pos.CENTER_RIGHT) + .actions(actions) + .nextStepAction(walkthrough -> { + NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY) + .apply(walkthrough.currentStage.getScene()) + .ifPresent(node -> { + if (node instanceof Button button) { + button.fire(); + } + }); + walkthrough.nextStep(); + }) + .build(); + FullScreenStep completeNode = WalkthroughNode + .fullScreen(Localization.lang("Walkthrough complete")) + .content( + new TextBlock(Localization.lang("You've completed the basic feature tour.")), + new TextBlock(Localization.lang("Explore more features like groups, fetchers, and customization options.")), + new InfoBlock(Localization.lang("Check our documentation for detailed guides.")) + ) + .actions(WalkthroughActionsConfig.builder() + .continueButton(Localization.lang("Complete walkthrough")) + .backButton(Localization.lang("Back")).build()) + .build(); + this.steps = List.of(welcomeNode, paperDirectoryNode, createNode, editNode, saveNode, completeNode); this.totalSteps = new SimpleIntegerProperty(steps.size()); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 3ec4b1626ab..ae2b4afe181 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -1,23 +1,29 @@ package org.jabref.gui.walkthrough; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; +import javafx.geometry.Bounds; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.RowConstraints; import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import org.jabref.gui.util.BackdropHighlight; import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; -import org.jabref.logic.l10n.Localization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,23 +34,28 @@ public class WalkthroughOverlay { private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); private final Stage parentStage; - private final Walkthrough manager; + private final Walkthrough walkthrough; private final GridPane overlayPane; private final BackdropHighlight backdropHighlight; private final Pane originalRoot; private final StackPane stackPane; - private final WalkthroughRenderer uiFactory; + private final WalkthroughRenderer renderer; + private final List cleanUpTasks = new ArrayList<>(); - public WalkthroughOverlay(Stage stage, Walkthrough manager) { + public WalkthroughOverlay(Stage stage, Walkthrough walkthrough) { this.parentStage = stage; - this.manager = manager; - this.uiFactory = new WalkthroughRenderer(); + this.walkthrough = walkthrough; + this.renderer = new WalkthroughRenderer(); overlayPane = new GridPane(); overlayPane.setStyle("-fx-background-color: transparent;"); - overlayPane.setVisible(false); + overlayPane.setPickOnBounds(false); overlayPane.setMaxWidth(Double.MAX_VALUE); overlayPane.setMaxHeight(Double.MAX_VALUE); + overlayPane.setOnMouseClicked(event -> { + System.out.println("Event clicked!"); + System.out.println(event); + }); Scene scene = stage.getScene(); assert scene != null; @@ -60,35 +71,41 @@ public WalkthroughOverlay(Stage stage, Walkthrough manager) { } public void displayStep(WalkthroughNode step) { + backdropHighlight.detach(); + overlayPane.getChildren().clear(); + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); + if (step == null) { - hide(); return; } - show(); - - backdropHighlight.detach(); - overlayPane.getChildren().clear(); - Node stepContent; if (step instanceof FullScreenStep fullScreenStep) { - stepContent = uiFactory.render(fullScreenStep, manager); + stepContent = renderer.render(fullScreenStep, walkthrough); displayFullScreenContent(stepContent); } else if (step instanceof PanelStep panelStep) { - stepContent = uiFactory.render(panelStep, manager); + stepContent = renderer.render(panelStep, walkthrough); displayPanelContent(stepContent, panelStep.position()); - if (step.resolver().isPresent()) { - Optional targetNodeOpt = step.resolver().get().apply(parentStage.getScene()); - if (targetNodeOpt.isPresent()) { - Node targetNode = targetNodeOpt.get(); - backdropHighlight.attach(targetNode); - step.clickOnNodeAction().ifPresent(action -> - targetNode.setOnMouseClicked(_ -> action.accept(manager))); - } else { - LOGGER.warn(Localization.lang("Could not resolve target node for step: %1", step.title())); - } - } + step.resolver().ifPresent( + resolver -> + resolver.apply(parentStage.getScene()).ifPresentOrElse( + node -> { + backdropHighlight.attach(node); + step.clickOnNodeAction().ifPresent( + action -> { + EventHandler originalHandler = node.getOnMouseClicked(); + node.setOnMouseClicked(event -> { + Optional.ofNullable(originalHandler).ifPresent(handler -> handler.handle(event)); + action.accept(walkthrough); + }); + cleanUpTasks.add(() -> node.setOnMouseClicked(originalHandler)); + }); + }, + () -> LOGGER.warn("Could not resolve target node for step: {}", step.title()) + ) + ); } } @@ -111,13 +128,19 @@ private void displayFullScreenContent(Node content) { GridPane.setFillHeight(content, true); overlayPane.setAlignment(Pos.CENTER); - overlayPane.setVisible(true); } private void displayPanelContent(Node panelContent, Pos position) { overlayPane.getChildren().clear(); overlayPane.getChildren().add(panelContent); - panelContent.setMouseTransparent(false); + + ChangeListener listener = (_, _, bounds) -> { + Rectangle clip = new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); + overlayPane.setClip(clip); + }; + panelContent.boundsInParentProperty().addListener(listener); + cleanUpTasks.add(() -> panelContent.boundsInParentProperty().removeListener(listener)); + cleanUpTasks.add(() -> overlayPane.setClip(null)); overlayPane.getRowConstraints().clear(); overlayPane.getColumnConstraints().clear(); @@ -160,9 +183,9 @@ private void displayPanelContent(Node panelContent, Pos position) { */ public void detach() { backdropHighlight.detach(); - - overlayPane.setVisible(false); overlayPane.getChildren().clear(); + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); Scene scene = parentStage.getScene(); if (scene != null && originalRoot != null) { @@ -171,13 +194,4 @@ public void detach() { LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); } } - - private void show() { - overlayPane.setVisible(true); - overlayPane.toFront(); - } - - private void hide() { - overlayPane.setVisible(false); - } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index d61d334de5e..7056af89e66 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -24,20 +24,20 @@ * Renders the walkthrough steps and content blocks into JavaFX Nodes. */ public class WalkthroughRenderer { - public Node render(FullScreenStep step, Walkthrough manager) { + public Node render(FullScreenStep step, Walkthrough walkthrough) { VBox container = makePanel(); container.setAlignment(Pos.CENTER); VBox content = new VBox(); content.getStyleClass().add("walkthrough-fullscreen-content"); Label titleLabel = new Label(Localization.lang(step.title())); titleLabel.getStyleClass().add("walkthrough-title"); - VBox contentContainer = makeContent(step, manager); - content.getChildren().addAll(titleLabel, contentContainer, makeActions(step, manager)); + VBox contentContainer = makeContent(step, walkthrough); + content.getChildren().addAll(titleLabel, contentContainer, makeActions(step, walkthrough)); container.getChildren().add(content); return container; } - public Node render(PanelStep step, Walkthrough manager) { + public Node render(PanelStep step, Walkthrough walkthrough) { VBox panel = makePanel(); if (step.position() == Pos.CENTER_LEFT || step.position() == Pos.CENTER_RIGHT) { @@ -60,23 +60,23 @@ public Node render(PanelStep step, Walkthrough manager) { HBox.setHgrow(spacer, Priority.ALWAYS); Label stepCounter = new Label(Localization.lang("Step %0 of %1", - String.valueOf(manager.currentStepProperty().get() + 1), - String.valueOf(manager.totalStepsProperty().get()))); + String.valueOf(walkthrough.currentStepProperty().get() + 1), + String.valueOf(walkthrough.totalStepsProperty().get()))); stepCounter.getStyleClass().add("walkthrough-step-counter"); header.getChildren().addAll(titleLabel, spacer, stepCounter); - VBox contentContainer = makeContent(step, manager); + VBox contentContainer = makeContent(step, walkthrough); Region bottomSpacer = new Region(); VBox.setVgrow(bottomSpacer, Priority.ALWAYS); - HBox actions = makeActions(step, manager); + HBox actions = makeActions(step, walkthrough); panel.getChildren().addAll(header, contentContainer, bottomSpacer, actions); return panel; } - public Node render(ArbitraryJFXBlock block, Walkthrough manager) { - return block.componentFactory().apply(manager); + public Node render(ArbitraryJFXBlock block, Walkthrough walkthrough) { + return block.componentFactory().apply(walkthrough); } public Node render(TextBlock textBlock) { @@ -103,7 +103,7 @@ private VBox makePanel() { return container; } - private HBox makeActions(WalkthroughNode step, Walkthrough manager) { + private HBox makeActions(WalkthroughNode step, Walkthrough walkthrough) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); actions.setSpacing(0); @@ -112,16 +112,16 @@ private HBox makeActions(WalkthroughNode step, Walkthrough manager) { HBox.setHgrow(spacer, Priority.ALWAYS); if (step.actions().flatMap(WalkthroughActionsConfig::backButtonText).isPresent()) { - actions.getChildren().add(makeBackButton(step, manager)); + actions.getChildren().add(makeBackButton(step, walkthrough)); } HBox rightActions = new HBox(); rightActions.setAlignment(Pos.CENTER_RIGHT); rightActions.setSpacing(4); if (step.actions().flatMap(WalkthroughActionsConfig::skipButtonText).isPresent()) { - rightActions.getChildren().add(makeSkipButton(step, manager)); + rightActions.getChildren().add(makeSkipButton(step, walkthrough)); } if (step.actions().flatMap(WalkthroughActionsConfig::continueButtonText).isPresent()) { - rightActions.getChildren().add(makeContinueButton(step, manager)); + rightActions.getChildren().add(makeContinueButton(step, walkthrough)); } actions.getChildren().addAll(spacer, rightActions); @@ -129,56 +129,50 @@ private HBox makeActions(WalkthroughNode step, Walkthrough manager) { return actions; } - private VBox makeContent(WalkthroughNode step, Walkthrough manager) { + private VBox makeContent(WalkthroughNode step, Walkthrough walkthrough) { return new VBox(12, step.content().stream().map(block -> switch (block) { case TextBlock textBlock -> render(textBlock); case InfoBlock infoBlock -> render(infoBlock); case ArbitraryJFXBlock arbitraryBlock -> - render(arbitraryBlock, manager); + render(arbitraryBlock, walkthrough); } ).toArray(Node[]::new)); } - private Button makeContinueButton(WalkthroughNode step, Walkthrough manager) { + private Button makeContinueButton(WalkthroughNode step, Walkthrough walkthrough) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::continueButtonText) .orElse("Walkthrough continue button"); Button continueButton = new Button(Localization.lang(buttonText)); continueButton.getStyleClass().add("walkthrough-continue-button"); - continueButton.setOnAction(_ -> { - step.nextStepAction().ifPresent(action -> action.accept(manager)); - manager.nextStep(); - }); + continueButton.setOnAction(_ -> step.nextStepAction().ifPresentOrElse( + action -> action.accept(walkthrough), walkthrough::nextStep)); return continueButton; } - private Button makeSkipButton(WalkthroughNode step, Walkthrough manager) { + private Button makeSkipButton(WalkthroughNode step, Walkthrough walkthrough) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::skipButtonText) .orElse("Walkthrough skip to finish"); Button skipButton = new Button(Localization.lang(buttonText)); skipButton.getStyleClass().add("walkthrough-skip-button"); - skipButton.setOnAction(_ -> { - step.skipAction().ifPresent(action -> action.accept(manager)); - manager.skip(); - }); + skipButton.setOnAction(_ -> step.skipAction().ifPresentOrElse( + action -> action.accept(walkthrough), walkthrough::skip)); return skipButton; } - private Button makeBackButton(WalkthroughNode step, Walkthrough manager) { + private Button makeBackButton(WalkthroughNode step, Walkthrough walkthrough) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::backButtonText) .orElse("Walkthrough back button"); Button backButton = new Button(Localization.lang(buttonText)); backButton.getStyleClass().add("walkthrough-back-button"); - backButton.setOnAction(_ -> { - step.previousStepAction().ifPresent(action -> action.accept(manager)); - manager.previousStep(); - }); + backButton.setOnAction(_ -> step.previousStepAction().ifPresentOrElse( + action -> action.accept(walkthrough), walkthrough::previousStep)); return backButton; } } diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index f63918b4f30..5e88f68f8f6 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2532,7 +2532,6 @@ journalInfo .grid-cell-b { -fx-text-fill: -jr-theme; -fx-font-size: 3em; -fx-font-weight: bold; - -fx-text-alignment: center-left; } .walkthrough-side-panel-vertical .walkthrough-title, @@ -2540,6 +2539,11 @@ journalInfo .grid-cell-b { -fx-font-size: 2em; } +.walkthrough-side-panel-vertical, +.walkthrough-side-panel-horizontal { + -fx-padding: 2em; +} + .walkthrough-side-panel-vertical { -fx-pref-width: 32em; -fx-max-width: 32em; @@ -2547,9 +2551,9 @@ journalInfo .grid-cell-b { } .walkthrough-side-panel-horizontal { - -fx-pref-height: 16em; - -fx-max-height: 16em; - -fx-min-height: 16em; + -fx-pref-height: 18em; + -fx-max-height: 18em; + -fx-min-height: 18em; } .walkthrough-step-counter { diff --git a/jablib/src/main/java/org/jabref/logic/l10n/Localization.java b/jablib/src/main/java/org/jabref/logic/l10n/Localization.java index 8e2d3d789c8..26f124ca4db 100644 --- a/jablib/src/main/java/org/jabref/logic/l10n/Localization.java +++ b/jablib/src/main/java/org/jabref/logic/l10n/Localization.java @@ -18,19 +18,22 @@ import org.slf4j.LoggerFactory; -/// Provides handling for messages and menu entries in the preferred language of the user. +/// Provides handling for messages and menu entries in the preferred language of the +/// user. /// -/// Notes: All messages and menu-entries in JabRef are stored in escaped form like "This_is_a_message". This message -/// serves as key inside the `l10n` properties files that hold the translation for many languages. When a message -/// is accessed, it needs to be unescaped and possible parameters that can appear in a message need to be filled with -/// values. +/// Notes: All messages and menu-entries in JabRef are stored in escaped form like +/// "This_is_a_message". This message serves as key inside the `l10n` properties files +/// that hold the translation for many languages. When a message is accessed, it needs +/// to be unescaped and possible parameters that can appear in a message need to be +/// filled with values. /// -/// This implementation loads the appropriate language by importing all keys/values from the correct bundle and stores -/// them in unescaped form inside a [LocalizationBundle] which provides fast access because it caches the key-value -/// pairs. +/// This implementation loads the appropriate language by importing all keys/values from +/// the correct bundle and stores them in unescaped form inside a [LocalizationBundle] +/// which provides fast access because it caches the key-value pairs. /// -/// The access to this is given by the functions [#lang(String,String...)] and -/// that developers should use whenever they use strings for the e.g. GUI that need to be translatable. +/// The access to this is given by the functions [#lang(String,String...)] and that +/// developers should use whenever they use strings for the e.g. GUI that need to be +/// translatable. @AllowedToUseStandardStreams("Needs to have acess to System.err because it's called very early before our loggers") public class Localization { static final String RESOURCE_PREFIX = "l10n/JabRef"; @@ -57,8 +60,8 @@ public static String lang(String key, Object... params) { } /** - * Sets the language and loads the appropriate translations. Note, that this function should be called before any - * other function of this class. + * Sets the language and loads the appropriate translations. Note, that this + * function should be called before any other function of this class. * * @param language Language identifier like "en", "de", etc. */ @@ -101,9 +104,10 @@ public static LocalizationBundle getMessages() { } /** - * Creates and caches the language bundles used in JabRef for a particular language. This function first loads - * correct version of the "escaped" bundles that are given in {@link l10n}. After that, it stores the unescaped - * version in a cached {@link LocalizationBundle} for fast access. + * Creates and caches the language bundles used in JabRef for a particular language. + * This function first loads correct version of the "escaped" bundles that are given + * in {@link l10n}. After that, it stores the unescaped version in a cached + * {@link LocalizationBundle} for fast access. * * @param locale Localization to use. */ @@ -130,10 +134,12 @@ private static Map createLookupMap(ResourceBundle baseBundle) { } /** - * This looks up a key in the bundle and replaces parameters %0, ..., %9 with the respective params given. Note that - * the keys are the "unescaped" strings from the bundle property files. + * This looks up a key in the bundle and replaces parameters %0, ..., %9 with the + * respective params given. Note that the keys are the "unescaped" strings from the + * bundle property files. * - * @param bundle The {@link LocalizationBundle} which is usually {@link Localization#localizedMessages}. + * @param bundle The {@link LocalizationBundle} which is usually + * {@link Localization#localizedMessages}. * @param key The lookup key. * @param params The parameters that should be inserted into the message * @return The final message with replaced parameters. diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 1518e73d391..a75cfe85827 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2937,6 +2937,11 @@ You\ can\ always\ access\ this\ walkthrough\ from\ the\ Help\ menu.=You can alwa Creating\ a\ new\ entry=Creating a new entry Click\ the\ highlighted\ button\ to\ start\ creating\ a\ new\ bibliographic\ entry.=Click the highlighted button to start creating a new bibliographic entry. JabRef\ supports\ various\ entry\ types\ like\ articles,\ books,\ and\ more.=JabRef supports various entry types like articles, books, and more. +In\ the\ entry\ editor\ that\ opens\ after\ clicking\ the\ button,\ choose\ "Article"\ as\ the\ entry\ type.=In the entry editor that opens after clicking the button, choose "Article" as the entry type. +Fill\ in\ the\ entry\ details=Fill in the entry details +In\ the\ title\ field,\ enter\ "JabRef\:\ BibTeX-based\ literature\ management\ software".=In the title field, enter "JabRef: BibTeX-based literature management software". +In\ the\ journal\ field,\ enter\ "TUGboat".="In the journal field, enter "TUGboat". +You\ can\ fill\ in\ more\ details\ later.\ JabRef\ supports\ many\ entry\ types\ and\ fields.=You can fill in more details later. JabRef supports many entry types and fields. Saving\ Your\ Work=Saving Your Work Don't\ forget\ to\ save\ your\ library.\ Click\ the\ save\ button.=Don't forget to save your library. Click the save button. Regularly\ saving\ prevents\ data\ loss.=Regularly saving prevents data loss. @@ -2958,7 +2963,6 @@ Start\ walkthrough=Start Walkthrough Step\ %0\ of\ %1=Step %0 of %1 Complete\ walkthrough=Complete Walkthrough Back=Back -Could\ not\ resolve\ target\ node\ for\ step\:\ %1=Could not resolve target node for step: %1 # CommandLine Available\ export\ formats\:=Available export formats: From d944011e88583a17c412e09275baba19b4843767 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 13:38:44 -0400 Subject: [PATCH 14/50] Add edit entry step --- .../main/java/org/jabref/gui/JabRefGUI.java | 11 +- .../org/jabref/gui/frame/JabRefFrame.java | 18 +- .../jabref/gui/util/BackdropHighlight.java | 16 +- .../jabref/gui/walkthrough/Walkthrough.java | 228 +++++++++++++----- .../gui/walkthrough/WalkthroughOverlay.java | 92 ++++--- .../gui/walkthrough/WalkthroughRenderer.java | 56 ++--- .../main/resources/org/jabref/gui/Base.css | 12 +- .../main/resources/l10n/JabRef_en.properties | 6 +- 8 files changed, 275 insertions(+), 164 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index b1932297f0c..f0d483dcba7 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -36,7 +36,6 @@ import org.jabref.logic.l10n.Localization; import org.jabref.logic.net.ProxyRegisterer; import org.jabref.logic.os.OS; -import org.jabref.logic.preferences.WalkthroughPreferences; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.server.RemoteListenerServerManager; @@ -77,7 +76,6 @@ public class JabRefGUI extends Application { private static ClipBoardManager clipBoardManager; private static DialogService dialogService; private static JabRefFrame mainFrame; - private static Walkthrough walkthroughManager; private static RemoteListenerServerManager remoteListenerServerManager; private Stage mainStage; @@ -189,11 +187,6 @@ public void initialize() { dialogService, taskExecutor); Injector.setModelOrService(AiService.class, aiService); - - // Initialize walkthrough manager - WalkthroughPreferences walkthroughPreferences = preferences.getWalkthroughPreferences(); - JabRefGUI.walkthroughManager = new Walkthrough(walkthroughPreferences); - Injector.setModelOrService(Walkthrough.class, walkthroughManager); } private void setupProxy() { @@ -304,8 +297,8 @@ public void onShowing(WindowEvent event) { } // Check if walkthrough should be shown - if (!walkthroughManager.isCompleted()) { - walkthroughManager.start(mainStage); + if (!preferences.getWalkthroughPreferences().isCompleted()) { + mainFrame.showWalkthrough(); } }); } diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java index 52ba69e2ca7..d8992f540a7 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -54,6 +54,7 @@ import org.jabref.gui.undo.RedoAction; import org.jabref.gui.undo.UndoAction; import org.jabref.gui.util.BindingsHelper; +import org.jabref.gui.walkthrough.Walkthrough; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; import org.jabref.logic.journals.JournalAbbreviationRepository; @@ -492,6 +493,14 @@ public void showLibraryTab(@NonNull LibraryTab libraryTab) { tabbedPane.getSelectionModel().select(libraryTab); } + public void showWalkthrough() { + Walkthrough walkthrough = new Walkthrough( + preferences.getWalkthroughPreferences(), + this + ); + walkthrough.start(mainStage); + } + public void showWelcomeTab() { // The loop iterates through all tabs in tabbedPane to check if a WelcomeTab already exists. If yes, it is selected. for (Tab tab : tabbedPane.getTabs()) { @@ -521,9 +530,9 @@ public void showWelcomeTab() { } /** - * Opens a new tab with existing data. - * Asynchronous loading is done at {@link LibraryTab#createLibraryTab}. - * Similar method: {@link OpenDatabaseAction#openTheFile(Path)} + * Opens a new tab with existing data. Asynchronous loading is done at + * {@link LibraryTab#createLibraryTab}. Similar method: + * {@link OpenDatabaseAction#openTheFile(Path)} */ public void addTab(@NonNull BibDatabaseContext databaseContext, boolean raisePanel) { Objects.requireNonNull(databaseContext); @@ -681,7 +690,8 @@ public CloseDatabaseAction(LibraryTabContainer tabContainer, LibraryTab libraryT } /** - * Using this constructor will result in executing the command on the currently open library tab + * Using this constructor will result in executing the command on the currently + * open library tab */ public CloseDatabaseAction(LibraryTabContainer tabContainer, StateManager stateManager) { this(tabContainer, null, stateManager); diff --git a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java index 07fae49b4c5..33a1ecdca53 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java @@ -6,10 +6,8 @@ import javafx.beans.InvalidationListener; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; -import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; import javafx.scene.Node; -import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; @@ -46,7 +44,6 @@ public BackdropHighlight(@NonNull Pane pane) { this.overlayShape = Shape.subtract(backdrop, hole); this.overlayShape.setFill(OVERLAY_COLOR); - this.overlayShape.setMouseTransparent(true); this.overlayShape.setVisible(false); this.pane.getChildren().add(overlayShape); @@ -62,7 +59,6 @@ public void attach(@NonNull Node node) { this.targetNode = node; updateOverlayLayout(); - addListener(targetNode.localToSceneTransformProperty()); addListener(targetNode.visibleProperty()); addListener(pane.widthProperty()); @@ -104,13 +100,13 @@ private void addListener(ObservableValue property, ChangeListener list } private void updateOverlayLayout() { - if (targetNode == null || targetNode.getScene() == null || pane.getScene() == null) { + if (targetNode == null || targetNode.getScene() == null) { overlayShape.setVisible(false); return; } // ref: https://stackoverflow.com/questions/43887427/alternative-for-removed-impl-istreevisible - if (!targetNode.isVisible() || !NodeHelper.isTreeVisible(targetNode)) { + if (!NodeHelper.isTreeVisible(targetNode)) { overlayShape.setVisible(false); return; } @@ -128,13 +124,6 @@ private void updateOverlayLayout() { return; } - Scene currentScene = pane.getScene(); - Bounds sceneViewportBounds = new BoundingBox(0, 0, currentScene.getWidth(), currentScene.getHeight()); - if (!nodeBoundsInScene.intersects(sceneViewportBounds)) { - overlayShape.setVisible(false); - return; - } - backdrop.setX(0); backdrop.setY(0); backdrop.setWidth(pane.getWidth()); @@ -155,7 +144,6 @@ private void updateOverlayLayout() { this.overlayShape = Shape.subtract(backdrop, hole); this.overlayShape.setFill(OVERLAY_COLOR); - this.overlayShape.setMouseTransparent(true); this.overlayShape.setVisible(true); this.pane.getChildren().add(oldIndex, this.overlayShape); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index a60c93be984..d47fb4579dd 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -8,18 +8,40 @@ import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; +import javafx.geometry.Pos; +import javafx.scene.control.Button; import javafx.stage.Stage; +import org.jabref.gui.ClipBoardManager; +import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTab; +import org.jabref.gui.StateManager; import org.jabref.gui.actions.StandardActions; +import org.jabref.gui.frame.JabRefFrame; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.walkthrough.components.PaperDirectoryChooser; import org.jabref.gui.walkthrough.declarative.NodeResolverFactory; import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; import org.jabref.gui.walkthrough.declarative.richtext.ArbitraryJFXBlock; import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; +import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; +import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.logic.ai.AiService; import org.jabref.logic.l10n.Localization; import org.jabref.logic.preferences.WalkthroughPreferences; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.util.FileUpdateMonitor; + +import com.airhacks.afterburner.injection.Injector; +import org.jspecify.annotations.NonNull; /** * Manages a walkthrough session by coordinating steps. @@ -29,82 +51,164 @@ public class Walkthrough { private final IntegerProperty currentStep; private final IntegerProperty totalSteps; private final BooleanProperty active; + // TODO: Consider using Graph instead for complex walkthrough routing e.g., pro user show no walkthrough, new user show full walkthrough, etc. private final List steps; private Optional overlay = Optional.empty(); private Stage currentStage; + private final BibDatabaseContext database; /** * Creates a new walkthrough with the specified preferences. * * @param preferences The walkthrough preferences to use */ - public Walkthrough(WalkthroughPreferences preferences) { + public Walkthrough(WalkthroughPreferences preferences, @NonNull JabRefFrame frame) { this.preferences = preferences; + this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); + this.database = new BibDatabaseContext(); + + FullScreenStep welcomeNode = WalkthroughNode + .fullScreen(Localization.lang("Welcome to JabRef")) + .content( + new TextBlock(Localization.lang("This quick walkthrough will introduce you to some key features.")), + new InfoBlock(Localization.lang("You can always access this walkthrough from the Help menu.")) + ) + .actions(WalkthroughActionsConfig.builder() + .continueButton(Localization.lang("Start walkthrough")) + .skipButton(Localization.lang("Skip to finish")) + .build()) + .build(); + WalkthroughActionsConfig actions = WalkthroughActionsConfig.all( + Localization.lang("Continue"), + Localization.lang("Skip for Now"), + Localization.lang("Back")); + FullScreenStep paperDirectoryNode = WalkthroughNode + .fullScreen(Localization.lang("Configure paper directory")) + .content( + new TextBlock(Localization.lang("Set up your main file directory where JabRef will look for and store your PDF files and other associated documents.")), + new InfoBlock(Localization.lang("This directory helps JabRef organize your paper files. You can change this later in Preferences.")), + new ArbitraryJFXBlock(_ -> new PaperDirectoryChooser()) + ) + .actions(actions) + .nextStepAction(walkthrough -> { + if (frame.getCurrentLibraryTab() != null) { + walkthrough.nextStep(); + return; + } + + DialogService dialogService = Injector.instantiateModelOrService(DialogService.class); + AiService aiService = Injector.instantiateModelOrService(AiService.class); + GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); + StateManager stateManager = Injector.instantiateModelOrService(StateManager.class); + FileUpdateMonitor fileUpdateMonitor = Injector.instantiateModelOrService(FileUpdateMonitor.class); + BibEntryTypesManager entryTypesManager = Injector.instantiateModelOrService(BibEntryTypesManager.class); + CountingUndoManager undoManager = Injector.instantiateModelOrService(CountingUndoManager.class); + ClipBoardManager clipBoardManager = Injector.instantiateModelOrService(ClipBoardManager.class); + TaskExecutor taskExecutor = Injector.instantiateModelOrService(TaskExecutor.class); - this.steps = List.of( - WalkthroughNode.fullScreen(Localization.lang("Welcome to JabRef")) - .content( - new TextBlock(Localization.lang("This quick walkthrough will introduce you to some key features.")), - new InfoBlock(Localization.lang("You can always access this walkthrough from the Help menu.")) - ) - .actions(WalkthroughActionsConfig.builder() - .continueButton(Localization.lang("Start walkthrough")) - .skipButton(Localization.lang("Skip to finish")) - .build()) - .build(), - - WalkthroughNode.fullScreen(Localization.lang("Configure paper directory")) - .content( - new TextBlock(Localization.lang("Set up your main file directory where JabRef will look for and store your PDF files and other associated documents.")), - new InfoBlock(Localization.lang("This directory helps JabRef organize your paper files. You can change this later in Preferences.")), - new ArbitraryJFXBlock(_ -> new PaperDirectoryChooser()) - ) - .actions(WalkthroughActionsConfig.all( - Localization.lang("Continue"), - Localization.lang("Skip for Now"), - Localization.lang("Back"))) - .build(), - - WalkthroughNode.panel(Localization.lang("Creating a new entry")) - .content( - new TextBlock(Localization.lang("Click the highlighted button to start creating a new bibliographic entry.")), - new InfoBlock(Localization.lang("JabRef supports various entry types like articles, books, and more.")) - ) - .resolver(NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY)) - .position(javafx.geometry.Pos.BOTTOM_CENTER) - .actions(WalkthroughActionsConfig.all( - Localization.lang("Continue"), - Localization.lang("Skip for Now"), - Localization.lang("Back"))) - .build(), - - WalkthroughNode.panel(Localization.lang("Saving your work")) - .content( - new TextBlock(Localization.lang("Don't forget to save your library. Click the save button.")), - new InfoBlock(Localization.lang("Regularly saving prevents data loss.")) - ) - .resolver(NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)) - .position(javafx.geometry.Pos.CENTER_RIGHT) - .actions(WalkthroughActionsConfig.all( - Localization.lang("Continue"), - Localization.lang("Skip for Now"), - Localization.lang("Back"))) - .build(), - - WalkthroughNode.fullScreen(Localization.lang("Walkthrough complete")) - .content( - new TextBlock(Localization.lang("You've completed the basic feature tour.")), - new TextBlock(Localization.lang("Explore more features like groups, fetchers, and customization options.")), - new InfoBlock(Localization.lang("Check our documentation for detailed guides.")) - ) - .actions(WalkthroughActionsConfig.builder() - .continueButton(Localization.lang("Complete walkthrough")) - .backButton(Localization.lang("Back")).build()) - .build() - ); + LibraryTab libraryTab = LibraryTab.createLibraryTab( + this.database, + frame, + dialogService, + aiService, + guiPreferences, + stateManager, + fileUpdateMonitor, + entryTypesManager, + undoManager, + clipBoardManager, + taskExecutor); + frame.addTab(libraryTab, true); + walkthrough.nextStep(); + }) + .build(); + PanelStep createNode = WalkthroughNode + .panel(Localization.lang("Creating a new entry")) + .content( + new TextBlock(Localization.lang("Click the highlighted button to start creating a new bibliographic entry.")), + new InfoBlock(Localization.lang("JabRef supports various entry types like articles, books, and more.")), + new TextBlock(Localization.lang("In the entry editor that opens after clicking the button, choose \"Article\" as the entry type.")) + ) + .resolver(NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY)) + .position(Pos.BOTTOM_CENTER) + .actions(actions) + .nextStepAction(walkthrough -> { + NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY).apply(walkthrough.currentStage.getScene()).ifPresent(node -> { + if (node instanceof Button button) { + button.fire(); + } + }); + walkthrough.nextStep(frame.getMainStage()); + }) + .build(); + // FIXME: Index out of bound. + PanelStep editNode = WalkthroughNode + .panel(Localization.lang("Fill in the entry details")) + .content( + new TextBlock(Localization.lang("In the title field, enter \"JabRef: BibTeX-based literature management software\".")), + new TextBlock(Localization.lang("In the journal field, enter \"TUGboat\".")), + new InfoBlock(Localization.lang("You can fill in more details later. JabRef supports many entry types and fields.")) + ) + .resolver(NodeResolverFactory.forSelector(".editorPane")) + .position(Pos.TOP_CENTER) + .actions(actions) + .nextStepAction(walkthrough -> { + BibEntry exampleEntry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Oliver Kopp and Carl Christian Snethlage and Christoph Schwentker") + .withField(StandardField.TITLE, "JabRef: BibTeX-based literature management software") + .withField(StandardField.JOURNAL, "TUGboat") + .withField(StandardField.VOLUME, "44") + .withField(StandardField.NUMBER, "3") + .withField(StandardField.PAGES, "441--447") + .withField(StandardField.DOI, "10.47397/tb/44-3/tb138kopp-jabref") + .withField(StandardField.ISSN, "0896-3207") + .withField(StandardField.ISSUE, "138") + .withField(StandardField.YEAR, "2023") + .withChanged(true); + var db = database.getDatabase(); + for (BibEntry entry : db.getEntries()) { + db.removeEntry(entry); + } + db.insertEntry(exampleEntry); + walkthrough.nextStep(frame.getMainStage()); + }) + .build(); + // FIXME: Index out of bound. + PanelStep saveNode = WalkthroughNode + .panel(Localization.lang("Saving your work")) + .content( + new TextBlock(Localization.lang("Don't forget to save your library. Click the save button.")), + new InfoBlock(Localization.lang("Regularly saving prevents data loss.")) + ) + .resolver(NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)) + .position(Pos.CENTER_RIGHT) + .actions(actions) + .nextStepAction(walkthrough -> { + NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY) + .apply(walkthrough.currentStage.getScene()) + .ifPresent(node -> { + if (node instanceof Button button) { + button.fire(); + } + }); + walkthrough.nextStep(); + }) + .build(); + FullScreenStep completeNode = WalkthroughNode + .fullScreen(Localization.lang("Walkthrough complete")) + .content( + new TextBlock(Localization.lang("You've completed the basic feature tour.")), + new TextBlock(Localization.lang("Explore more features like groups, fetchers, and customization options.")), + new InfoBlock(Localization.lang("Check our documentation for detailed guides.")) + ) + .actions(WalkthroughActionsConfig.builder() + .continueButton(Localization.lang("Complete walkthrough")) + .backButton(Localization.lang("Back")).build()) + .build(); + this.steps = List.of(welcomeNode, paperDirectoryNode, createNode, editNode, saveNode, completeNode); this.totalSteps = new SimpleIntegerProperty(steps.size()); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 3ec4b1626ab..ae2b4afe181 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -1,23 +1,29 @@ package org.jabref.gui.walkthrough; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; +import javafx.geometry.Bounds; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.RowConstraints; import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import org.jabref.gui.util.BackdropHighlight; import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; -import org.jabref.logic.l10n.Localization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,23 +34,28 @@ public class WalkthroughOverlay { private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); private final Stage parentStage; - private final Walkthrough manager; + private final Walkthrough walkthrough; private final GridPane overlayPane; private final BackdropHighlight backdropHighlight; private final Pane originalRoot; private final StackPane stackPane; - private final WalkthroughRenderer uiFactory; + private final WalkthroughRenderer renderer; + private final List cleanUpTasks = new ArrayList<>(); - public WalkthroughOverlay(Stage stage, Walkthrough manager) { + public WalkthroughOverlay(Stage stage, Walkthrough walkthrough) { this.parentStage = stage; - this.manager = manager; - this.uiFactory = new WalkthroughRenderer(); + this.walkthrough = walkthrough; + this.renderer = new WalkthroughRenderer(); overlayPane = new GridPane(); overlayPane.setStyle("-fx-background-color: transparent;"); - overlayPane.setVisible(false); + overlayPane.setPickOnBounds(false); overlayPane.setMaxWidth(Double.MAX_VALUE); overlayPane.setMaxHeight(Double.MAX_VALUE); + overlayPane.setOnMouseClicked(event -> { + System.out.println("Event clicked!"); + System.out.println(event); + }); Scene scene = stage.getScene(); assert scene != null; @@ -60,35 +71,41 @@ public WalkthroughOverlay(Stage stage, Walkthrough manager) { } public void displayStep(WalkthroughNode step) { + backdropHighlight.detach(); + overlayPane.getChildren().clear(); + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); + if (step == null) { - hide(); return; } - show(); - - backdropHighlight.detach(); - overlayPane.getChildren().clear(); - Node stepContent; if (step instanceof FullScreenStep fullScreenStep) { - stepContent = uiFactory.render(fullScreenStep, manager); + stepContent = renderer.render(fullScreenStep, walkthrough); displayFullScreenContent(stepContent); } else if (step instanceof PanelStep panelStep) { - stepContent = uiFactory.render(panelStep, manager); + stepContent = renderer.render(panelStep, walkthrough); displayPanelContent(stepContent, panelStep.position()); - if (step.resolver().isPresent()) { - Optional targetNodeOpt = step.resolver().get().apply(parentStage.getScene()); - if (targetNodeOpt.isPresent()) { - Node targetNode = targetNodeOpt.get(); - backdropHighlight.attach(targetNode); - step.clickOnNodeAction().ifPresent(action -> - targetNode.setOnMouseClicked(_ -> action.accept(manager))); - } else { - LOGGER.warn(Localization.lang("Could not resolve target node for step: %1", step.title())); - } - } + step.resolver().ifPresent( + resolver -> + resolver.apply(parentStage.getScene()).ifPresentOrElse( + node -> { + backdropHighlight.attach(node); + step.clickOnNodeAction().ifPresent( + action -> { + EventHandler originalHandler = node.getOnMouseClicked(); + node.setOnMouseClicked(event -> { + Optional.ofNullable(originalHandler).ifPresent(handler -> handler.handle(event)); + action.accept(walkthrough); + }); + cleanUpTasks.add(() -> node.setOnMouseClicked(originalHandler)); + }); + }, + () -> LOGGER.warn("Could not resolve target node for step: {}", step.title()) + ) + ); } } @@ -111,13 +128,19 @@ private void displayFullScreenContent(Node content) { GridPane.setFillHeight(content, true); overlayPane.setAlignment(Pos.CENTER); - overlayPane.setVisible(true); } private void displayPanelContent(Node panelContent, Pos position) { overlayPane.getChildren().clear(); overlayPane.getChildren().add(panelContent); - panelContent.setMouseTransparent(false); + + ChangeListener listener = (_, _, bounds) -> { + Rectangle clip = new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); + overlayPane.setClip(clip); + }; + panelContent.boundsInParentProperty().addListener(listener); + cleanUpTasks.add(() -> panelContent.boundsInParentProperty().removeListener(listener)); + cleanUpTasks.add(() -> overlayPane.setClip(null)); overlayPane.getRowConstraints().clear(); overlayPane.getColumnConstraints().clear(); @@ -160,9 +183,9 @@ private void displayPanelContent(Node panelContent, Pos position) { */ public void detach() { backdropHighlight.detach(); - - overlayPane.setVisible(false); overlayPane.getChildren().clear(); + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); Scene scene = parentStage.getScene(); if (scene != null && originalRoot != null) { @@ -171,13 +194,4 @@ public void detach() { LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); } } - - private void show() { - overlayPane.setVisible(true); - overlayPane.toFront(); - } - - private void hide() { - overlayPane.setVisible(false); - } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index d61d334de5e..7056af89e66 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -24,20 +24,20 @@ * Renders the walkthrough steps and content blocks into JavaFX Nodes. */ public class WalkthroughRenderer { - public Node render(FullScreenStep step, Walkthrough manager) { + public Node render(FullScreenStep step, Walkthrough walkthrough) { VBox container = makePanel(); container.setAlignment(Pos.CENTER); VBox content = new VBox(); content.getStyleClass().add("walkthrough-fullscreen-content"); Label titleLabel = new Label(Localization.lang(step.title())); titleLabel.getStyleClass().add("walkthrough-title"); - VBox contentContainer = makeContent(step, manager); - content.getChildren().addAll(titleLabel, contentContainer, makeActions(step, manager)); + VBox contentContainer = makeContent(step, walkthrough); + content.getChildren().addAll(titleLabel, contentContainer, makeActions(step, walkthrough)); container.getChildren().add(content); return container; } - public Node render(PanelStep step, Walkthrough manager) { + public Node render(PanelStep step, Walkthrough walkthrough) { VBox panel = makePanel(); if (step.position() == Pos.CENTER_LEFT || step.position() == Pos.CENTER_RIGHT) { @@ -60,23 +60,23 @@ public Node render(PanelStep step, Walkthrough manager) { HBox.setHgrow(spacer, Priority.ALWAYS); Label stepCounter = new Label(Localization.lang("Step %0 of %1", - String.valueOf(manager.currentStepProperty().get() + 1), - String.valueOf(manager.totalStepsProperty().get()))); + String.valueOf(walkthrough.currentStepProperty().get() + 1), + String.valueOf(walkthrough.totalStepsProperty().get()))); stepCounter.getStyleClass().add("walkthrough-step-counter"); header.getChildren().addAll(titleLabel, spacer, stepCounter); - VBox contentContainer = makeContent(step, manager); + VBox contentContainer = makeContent(step, walkthrough); Region bottomSpacer = new Region(); VBox.setVgrow(bottomSpacer, Priority.ALWAYS); - HBox actions = makeActions(step, manager); + HBox actions = makeActions(step, walkthrough); panel.getChildren().addAll(header, contentContainer, bottomSpacer, actions); return panel; } - public Node render(ArbitraryJFXBlock block, Walkthrough manager) { - return block.componentFactory().apply(manager); + public Node render(ArbitraryJFXBlock block, Walkthrough walkthrough) { + return block.componentFactory().apply(walkthrough); } public Node render(TextBlock textBlock) { @@ -103,7 +103,7 @@ private VBox makePanel() { return container; } - private HBox makeActions(WalkthroughNode step, Walkthrough manager) { + private HBox makeActions(WalkthroughNode step, Walkthrough walkthrough) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); actions.setSpacing(0); @@ -112,16 +112,16 @@ private HBox makeActions(WalkthroughNode step, Walkthrough manager) { HBox.setHgrow(spacer, Priority.ALWAYS); if (step.actions().flatMap(WalkthroughActionsConfig::backButtonText).isPresent()) { - actions.getChildren().add(makeBackButton(step, manager)); + actions.getChildren().add(makeBackButton(step, walkthrough)); } HBox rightActions = new HBox(); rightActions.setAlignment(Pos.CENTER_RIGHT); rightActions.setSpacing(4); if (step.actions().flatMap(WalkthroughActionsConfig::skipButtonText).isPresent()) { - rightActions.getChildren().add(makeSkipButton(step, manager)); + rightActions.getChildren().add(makeSkipButton(step, walkthrough)); } if (step.actions().flatMap(WalkthroughActionsConfig::continueButtonText).isPresent()) { - rightActions.getChildren().add(makeContinueButton(step, manager)); + rightActions.getChildren().add(makeContinueButton(step, walkthrough)); } actions.getChildren().addAll(spacer, rightActions); @@ -129,56 +129,50 @@ private HBox makeActions(WalkthroughNode step, Walkthrough manager) { return actions; } - private VBox makeContent(WalkthroughNode step, Walkthrough manager) { + private VBox makeContent(WalkthroughNode step, Walkthrough walkthrough) { return new VBox(12, step.content().stream().map(block -> switch (block) { case TextBlock textBlock -> render(textBlock); case InfoBlock infoBlock -> render(infoBlock); case ArbitraryJFXBlock arbitraryBlock -> - render(arbitraryBlock, manager); + render(arbitraryBlock, walkthrough); } ).toArray(Node[]::new)); } - private Button makeContinueButton(WalkthroughNode step, Walkthrough manager) { + private Button makeContinueButton(WalkthroughNode step, Walkthrough walkthrough) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::continueButtonText) .orElse("Walkthrough continue button"); Button continueButton = new Button(Localization.lang(buttonText)); continueButton.getStyleClass().add("walkthrough-continue-button"); - continueButton.setOnAction(_ -> { - step.nextStepAction().ifPresent(action -> action.accept(manager)); - manager.nextStep(); - }); + continueButton.setOnAction(_ -> step.nextStepAction().ifPresentOrElse( + action -> action.accept(walkthrough), walkthrough::nextStep)); return continueButton; } - private Button makeSkipButton(WalkthroughNode step, Walkthrough manager) { + private Button makeSkipButton(WalkthroughNode step, Walkthrough walkthrough) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::skipButtonText) .orElse("Walkthrough skip to finish"); Button skipButton = new Button(Localization.lang(buttonText)); skipButton.getStyleClass().add("walkthrough-skip-button"); - skipButton.setOnAction(_ -> { - step.skipAction().ifPresent(action -> action.accept(manager)); - manager.skip(); - }); + skipButton.setOnAction(_ -> step.skipAction().ifPresentOrElse( + action -> action.accept(walkthrough), walkthrough::skip)); return skipButton; } - private Button makeBackButton(WalkthroughNode step, Walkthrough manager) { + private Button makeBackButton(WalkthroughNode step, Walkthrough walkthrough) { String buttonText = step.actions() .flatMap(WalkthroughActionsConfig::backButtonText) .orElse("Walkthrough back button"); Button backButton = new Button(Localization.lang(buttonText)); backButton.getStyleClass().add("walkthrough-back-button"); - backButton.setOnAction(_ -> { - step.previousStepAction().ifPresent(action -> action.accept(manager)); - manager.previousStep(); - }); + backButton.setOnAction(_ -> step.previousStepAction().ifPresentOrElse( + action -> action.accept(walkthrough), walkthrough::previousStep)); return backButton; } } diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index f63918b4f30..5e88f68f8f6 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2532,7 +2532,6 @@ journalInfo .grid-cell-b { -fx-text-fill: -jr-theme; -fx-font-size: 3em; -fx-font-weight: bold; - -fx-text-alignment: center-left; } .walkthrough-side-panel-vertical .walkthrough-title, @@ -2540,6 +2539,11 @@ journalInfo .grid-cell-b { -fx-font-size: 2em; } +.walkthrough-side-panel-vertical, +.walkthrough-side-panel-horizontal { + -fx-padding: 2em; +} + .walkthrough-side-panel-vertical { -fx-pref-width: 32em; -fx-max-width: 32em; @@ -2547,9 +2551,9 @@ journalInfo .grid-cell-b { } .walkthrough-side-panel-horizontal { - -fx-pref-height: 16em; - -fx-max-height: 16em; - -fx-min-height: 16em; + -fx-pref-height: 18em; + -fx-max-height: 18em; + -fx-min-height: 18em; } .walkthrough-step-counter { diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 1518e73d391..a75cfe85827 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2937,6 +2937,11 @@ You\ can\ always\ access\ this\ walkthrough\ from\ the\ Help\ menu.=You can alwa Creating\ a\ new\ entry=Creating a new entry Click\ the\ highlighted\ button\ to\ start\ creating\ a\ new\ bibliographic\ entry.=Click the highlighted button to start creating a new bibliographic entry. JabRef\ supports\ various\ entry\ types\ like\ articles,\ books,\ and\ more.=JabRef supports various entry types like articles, books, and more. +In\ the\ entry\ editor\ that\ opens\ after\ clicking\ the\ button,\ choose\ "Article"\ as\ the\ entry\ type.=In the entry editor that opens after clicking the button, choose "Article" as the entry type. +Fill\ in\ the\ entry\ details=Fill in the entry details +In\ the\ title\ field,\ enter\ "JabRef\:\ BibTeX-based\ literature\ management\ software".=In the title field, enter "JabRef: BibTeX-based literature management software". +In\ the\ journal\ field,\ enter\ "TUGboat".="In the journal field, enter "TUGboat". +You\ can\ fill\ in\ more\ details\ later.\ JabRef\ supports\ many\ entry\ types\ and\ fields.=You can fill in more details later. JabRef supports many entry types and fields. Saving\ Your\ Work=Saving Your Work Don't\ forget\ to\ save\ your\ library.\ Click\ the\ save\ button.=Don't forget to save your library. Click the save button. Regularly\ saving\ prevents\ data\ loss.=Regularly saving prevents data loss. @@ -2958,7 +2963,6 @@ Start\ walkthrough=Start Walkthrough Step\ %0\ of\ %1=Step %0 of %1 Complete\ walkthrough=Complete Walkthrough Back=Back -Could\ not\ resolve\ target\ node\ for\ step\:\ %1=Could not resolve target node for step: %1 # CommandLine Available\ export\ formats\:=Available export formats: From 61b596373c7703f93fde06c85d6b2070e7636262 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 6 Jun 2025 17:19:58 -0400 Subject: [PATCH 15/50] Remove change to localization --- .../org/jabref/logic/l10n/Localization.java | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/jablib/src/main/java/org/jabref/logic/l10n/Localization.java b/jablib/src/main/java/org/jabref/logic/l10n/Localization.java index 26f124ca4db..8e2d3d789c8 100644 --- a/jablib/src/main/java/org/jabref/logic/l10n/Localization.java +++ b/jablib/src/main/java/org/jabref/logic/l10n/Localization.java @@ -18,22 +18,19 @@ import org.slf4j.LoggerFactory; -/// Provides handling for messages and menu entries in the preferred language of the -/// user. +/// Provides handling for messages and menu entries in the preferred language of the user. /// -/// Notes: All messages and menu-entries in JabRef are stored in escaped form like -/// "This_is_a_message". This message serves as key inside the `l10n` properties files -/// that hold the translation for many languages. When a message is accessed, it needs -/// to be unescaped and possible parameters that can appear in a message need to be -/// filled with values. +/// Notes: All messages and menu-entries in JabRef are stored in escaped form like "This_is_a_message". This message +/// serves as key inside the `l10n` properties files that hold the translation for many languages. When a message +/// is accessed, it needs to be unescaped and possible parameters that can appear in a message need to be filled with +/// values. /// -/// This implementation loads the appropriate language by importing all keys/values from -/// the correct bundle and stores them in unescaped form inside a [LocalizationBundle] -/// which provides fast access because it caches the key-value pairs. +/// This implementation loads the appropriate language by importing all keys/values from the correct bundle and stores +/// them in unescaped form inside a [LocalizationBundle] which provides fast access because it caches the key-value +/// pairs. /// -/// The access to this is given by the functions [#lang(String,String...)] and that -/// developers should use whenever they use strings for the e.g. GUI that need to be -/// translatable. +/// The access to this is given by the functions [#lang(String,String...)] and +/// that developers should use whenever they use strings for the e.g. GUI that need to be translatable. @AllowedToUseStandardStreams("Needs to have acess to System.err because it's called very early before our loggers") public class Localization { static final String RESOURCE_PREFIX = "l10n/JabRef"; @@ -60,8 +57,8 @@ public static String lang(String key, Object... params) { } /** - * Sets the language and loads the appropriate translations. Note, that this - * function should be called before any other function of this class. + * Sets the language and loads the appropriate translations. Note, that this function should be called before any + * other function of this class. * * @param language Language identifier like "en", "de", etc. */ @@ -104,10 +101,9 @@ public static LocalizationBundle getMessages() { } /** - * Creates and caches the language bundles used in JabRef for a particular language. - * This function first loads correct version of the "escaped" bundles that are given - * in {@link l10n}. After that, it stores the unescaped version in a cached - * {@link LocalizationBundle} for fast access. + * Creates and caches the language bundles used in JabRef for a particular language. This function first loads + * correct version of the "escaped" bundles that are given in {@link l10n}. After that, it stores the unescaped + * version in a cached {@link LocalizationBundle} for fast access. * * @param locale Localization to use. */ @@ -134,12 +130,10 @@ private static Map createLookupMap(ResourceBundle baseBundle) { } /** - * This looks up a key in the bundle and replaces parameters %0, ..., %9 with the - * respective params given. Note that the keys are the "unescaped" strings from the - * bundle property files. + * This looks up a key in the bundle and replaces parameters %0, ..., %9 with the respective params given. Note that + * the keys are the "unescaped" strings from the bundle property files. * - * @param bundle The {@link LocalizationBundle} which is usually - * {@link Localization#localizedMessages}. + * @param bundle The {@link LocalizationBundle} which is usually {@link Localization#localizedMessages}. * @param key The lookup key. * @param params The parameters that should be inserted into the message * @return The final message with replaced parameters. From 878d22bae28058a80d79c8499332acd310e4f6fb Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Sat, 7 Jun 2025 09:55:53 -0400 Subject: [PATCH 16/50] Preliminary implementation of main file directory walkthrough --- .../main/java/org/jabref/gui/JabRefGUI.java | 6 - .../jabref/gui/actions/StandardActions.java | 3 + .../org/jabref/gui/frame/JabRefFrame.java | 9 - .../java/org/jabref/gui/frame/MainMenu.java | 5 + .../preferences/PreferencesDialogView.java | 2 +- .../jabref/gui/util/BackdropHighlight.java | 150 --------- .../util/component/PulseAnimateIndicator.java | 172 ---------- .../gui/walkthrough/HighlightManager.java | 131 ++++++++ .../MultiWindowWalkthroughOverlay.java | 139 ++++++++ .../gui/walkthrough/SingleWindowOverlay.java | 310 ++++++++++++++++++ .../jabref/gui/walkthrough/Walkthrough.java | 254 ++------------ .../gui/walkthrough/WalkthroughAction.java | 97 ++++++ .../gui/walkthrough/WalkthroughOverlay.java | 197 ----------- .../gui/walkthrough/WalkthroughRenderer.java | 135 ++++---- .../components/BackdropHighlight.java | 111 +++++++ .../components/FullScreenDarken.java | 64 ++++ .../components/PaperDirectoryChooser.java | 88 ----- .../components/PulseAnimateIndicator.java | 107 ++++++ .../components/WalkthroughEffect.java | 116 +++++++ .../declarative/NavigationPredicate.java | 189 +++++++++++ .../walkthrough/declarative/NodeResolver.java | 142 ++++++++ .../declarative/NodeResolverFactory.java | 114 ------- .../declarative/WalkthroughActionsConfig.java | 53 --- .../declarative/WindowResolver.java | 38 +++ .../declarative/effect/HighlightEffect.java | 23 ++ .../effect/MultiWindowHighlight.java | 28 ++ .../declarative/effect/WindowEffect.java | 23 ++ .../declarative/step/FullScreenStep.java | 89 ----- .../declarative/step/PanelStep.java | 107 ++++-- .../declarative/step/StepType.java | 31 -- .../declarative/step/TooltipPosition.java | 9 + .../declarative/step/TooltipStep.java | 133 ++++++++ .../declarative/step/WalkthroughNode.java | 39 ++- .../main/resources/org/jabref/gui/Base.css | 19 ++ .../preferences/JabRefCliPreferences.java | 66 ++-- .../preferences/WalkthroughPreferences.java | 16 +- .../main/resources/l10n/JabRef_en.properties | 21 -- 37 files changed, 1941 insertions(+), 1295 deletions(-) delete mode 100644 jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/StepType.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipPosition.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index f0d483dcba7..b0e4da53b6d 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -28,7 +28,6 @@ import org.jabref.gui.util.DirectoryMonitor; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.gui.util.WebViewStore; -import org.jabref.gui.walkthrough.Walkthrough; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; import org.jabref.logic.journals.JournalAbbreviationLoader; @@ -295,11 +294,6 @@ public void onShowing(WindowEvent event) { if (stateManager.getOpenDatabases().isEmpty()) { mainFrame.showWelcomeTab(); } - - // Check if walkthrough should be shown - if (!preferences.getWalkthroughPreferences().isCompleted()) { - mainFrame.showWalkthrough(); - } }); } diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index a59125033e7..814610880ca 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -184,6 +184,9 @@ public enum StandardActions implements Action { OPEN_DEV_VERSION_LINK(Localization.lang("Development version"), Localization.lang("Opens a link where the current development version can be downloaded")), OPEN_CHANGELOG(Localization.lang("View change log"), Localization.lang("See what has been changed in the JabRef versions")), OPEN_GITHUB("GitHub", Localization.lang("Opens JabRef's GitHub page"), IconTheme.JabRefIcons.GITHUB), + WALKTHROUGH_MENU(Localization.lang("Walkthroughs")), + MAIN_FILE_DIRECTORY_WALKTHROUGH(Localization.lang("Configure Main File Directory"), IconTheme.JabRefIcons.LATEX_FILE_DIRECTORY), + EDIT_ENTRY_WALKTHROUGH(Localization.lang("Edit entry"), IconTheme.JabRefIcons.EDIT_ENTRY), DONATE(Localization.lang("Donate to JabRef"), Localization.lang("Donate to JabRef"), IconTheme.JabRefIcons.DONATE), OPEN_FORUM(Localization.lang("Community forum"), Localization.lang("Community forum"), IconTheme.JabRefIcons.FORUM), ERROR_CONSOLE(Localization.lang("View event log"), Localization.lang("Display all error messages")), diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java index d8992f540a7..04d30daf4a0 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -54,7 +54,6 @@ import org.jabref.gui.undo.RedoAction; import org.jabref.gui.undo.UndoAction; import org.jabref.gui.util.BindingsHelper; -import org.jabref.gui.walkthrough.Walkthrough; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; import org.jabref.logic.journals.JournalAbbreviationRepository; @@ -493,14 +492,6 @@ public void showLibraryTab(@NonNull LibraryTab libraryTab) { tabbedPane.getSelectionModel().select(libraryTab); } - public void showWalkthrough() { - Walkthrough walkthrough = new Walkthrough( - preferences.getWalkthroughPreferences(), - this - ); - walkthrough.start(mainStage); - } - public void showWelcomeTab() { // The loop iterates through all tabs in tabbedPane to check if a WelcomeTab already exists. If yes, it is selected. for (Tab tab : tabbedPane.getTabs()) { diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 97f49422141..1cbb64eb515 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -75,6 +75,7 @@ import org.jabref.gui.undo.UndoAction; import org.jabref.gui.util.URLs; import org.jabref.gui.util.UiTaskExecutor; +import org.jabref.gui.walkthrough.WalkthroughAction; import org.jabref.logic.ai.AiService; import org.jabref.logic.citationstyle.CitationStyleOutputFormat; import org.jabref.logic.help.HelpFile; @@ -374,6 +375,10 @@ private void createMenu() { factory.createMenuItem(StandardActions.OPEN_DEV_VERSION_LINK, new OpenBrowserAction(URLs.DEV_VERSION_LINK_URL, dialogService, preferences.getExternalApplicationsPreferences())), factory.createMenuItem(StandardActions.OPEN_CHANGELOG, new OpenBrowserAction(URLs.CHANGELOG_URL, dialogService, preferences.getExternalApplicationsPreferences())) ), + factory.createSubMenu(StandardActions.WALKTHROUGH_MENU, + factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory", frame)), + factory.createMenuItem(StandardActions.EDIT_ENTRY_WALKTHROUGH, new WalkthroughAction("editEntry", frame)) + ), factory.createMenuItem(StandardActions.OPEN_WELCOME_TAB, new SimpleCommand() { @Override diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java index dfebe248e69..4403cbac153 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java @@ -1,4 +1,4 @@ -package org.jabref.gui.preferences; + package org.jabref.gui.preferences; import java.util.Locale; import java.util.Optional; diff --git a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java deleted file mode 100644 index 33a1ecdca53..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/util/BackdropHighlight.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.jabref.gui.util; - -import java.util.ArrayList; -import java.util.List; - -import javafx.beans.InvalidationListener; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.geometry.Bounds; -import javafx.scene.Node; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.scene.shape.Rectangle; -import javafx.scene.shape.Shape; - -import com.sun.javafx.scene.NodeHelper; -import org.jspecify.annotations.NonNull; - -/** - * Creates a backdrop highlight effect. - */ -public class BackdropHighlight { - private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); - - private final Pane pane; - private Node targetNode; - - private final Rectangle backdrop; - private final Rectangle hole; - private @NonNull Shape overlayShape; - - private final List cleanUpTasks = new ArrayList<>(); - private final InvalidationListener updateListener = _ -> updateOverlayLayout(); - - /** - * Constructs a BackdropHighlight instance that overlays on the specified pane. - * - * @param pane The pane onto which the overlay will be added. - */ - public BackdropHighlight(@NonNull Pane pane) { - this.pane = pane; - this.backdrop = new Rectangle(); - this.hole = new Rectangle(); - - this.overlayShape = Shape.subtract(backdrop, hole); - this.overlayShape.setFill(OVERLAY_COLOR); - this.overlayShape.setVisible(false); - - this.pane.getChildren().add(overlayShape); - } - - /** - * Attaches the highlight effect to a specific node. - * - * @param node The node to be "highlighted". - */ - public void attach(@NonNull Node node) { - detach(); - - this.targetNode = node; - updateOverlayLayout(); - addListener(targetNode.localToSceneTransformProperty()); - addListener(targetNode.visibleProperty()); - addListener(pane.widthProperty()); - addListener(pane.heightProperty()); - addListener(pane.sceneProperty(), (_, _, newScene) -> { - updateOverlayLayout(); - if (newScene == null) { - return; - } - addListener(newScene.heightProperty()); - addListener(newScene.widthProperty()); - if (newScene.getWindow() == null) { - return; - } - addListener(newScene.getWindow().widthProperty()); - addListener(newScene.getWindow().heightProperty()); - }); - } - - /** - * Detaches the highlight effect from the target node. In other words, hide the - * overlay. - */ - public void detach() { - cleanUpTasks.forEach(Runnable::run); - cleanUpTasks.clear(); - overlayShape.setVisible(false); - this.targetNode = null; - } - - private void addListener(ObservableValue property) { - property.addListener(updateListener); - cleanUpTasks.add(() -> property.removeListener(updateListener)); - } - - private void addListener(ObservableValue property, ChangeListener listener) { - property.addListener(listener); - cleanUpTasks.add(() -> property.removeListener(listener)); - } - - private void updateOverlayLayout() { - if (targetNode == null || targetNode.getScene() == null) { - overlayShape.setVisible(false); - return; - } - - // ref: https://stackoverflow.com/questions/43887427/alternative-for-removed-impl-istreevisible - if (!NodeHelper.isTreeVisible(targetNode)) { - overlayShape.setVisible(false); - return; - } - - Bounds nodeBoundsInScene; - try { - nodeBoundsInScene = targetNode.localToScene(targetNode.getBoundsInLocal()); - } catch (IllegalStateException e) { - overlayShape.setVisible(false); - return; - } - - if (nodeBoundsInScene == null || nodeBoundsInScene.getWidth() <= 0 || nodeBoundsInScene.getHeight() <= 0) { - overlayShape.setVisible(false); - return; - } - - backdrop.setX(0); - backdrop.setY(0); - backdrop.setWidth(pane.getWidth()); - backdrop.setHeight(pane.getHeight()); - - Bounds nodeBoundsInRootPane = pane.sceneToLocal(nodeBoundsInScene); - hole.setX(nodeBoundsInRootPane.getMinX()); - hole.setY(nodeBoundsInRootPane.getMinY()); - hole.setWidth(nodeBoundsInRootPane.getWidth()); - hole.setHeight(nodeBoundsInRootPane.getHeight()); - - Shape oldOverlayShape = this.overlayShape; - int oldIndex = -1; - if (this.pane.getChildren().contains(oldOverlayShape)) { - oldIndex = this.pane.getChildren().indexOf(oldOverlayShape); - this.pane.getChildren().remove(oldIndex); - } - - this.overlayShape = Shape.subtract(backdrop, hole); - this.overlayShape.setFill(OVERLAY_COLOR); - this.overlayShape.setVisible(true); - this.pane.getChildren().add(oldIndex, this.overlayShape); - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java b/jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java deleted file mode 100644 index 9766b6ebd1d..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/util/component/PulseAnimateIndicator.java +++ /dev/null @@ -1,172 +0,0 @@ -package org.jabref.gui.util.component; - -import java.util.ArrayList; -import java.util.List; - -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.animation.Timeline; -import javafx.beans.value.ChangeListener; -import javafx.geometry.Bounds; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.scene.shape.Circle; -import javafx.util.Duration; - -/** - * A pulsing circular indicator that can be attached to a target node. - */ -public class PulseAnimateIndicator { - private Circle pulseIndicator; - private Timeline pulseAnimation; - private final Pane rootPane; - private Node attachedNode; - - private final List cleanupTasks = new ArrayList<>(); - private final ChangeListener updatePositionListener = (_, _, _) -> updatePosition(); - - public PulseAnimateIndicator(Pane rootPane) { - this.rootPane = rootPane; - } - - public void attachToNode(Node targetNode) { - stop(); - - this.attachedNode = targetNode; - setupIndicator(); - setupListeners(); - updatePosition(); - startAnimation(); - } - - private void setupIndicator() { - pulseIndicator = new Circle(8, Color.web("#50618F")); - pulseIndicator.setMouseTransparent(true); - pulseIndicator.setManaged(false); - - if (!rootPane.getChildren().contains(pulseIndicator)) { - rootPane.getChildren().add(pulseIndicator); - } - pulseIndicator.toFront(); - } - - private void setupListeners() { - attachedNode.boundsInLocalProperty().addListener(updatePositionListener); - cleanupTasks.add(() -> attachedNode.boundsInLocalProperty().removeListener(updatePositionListener)); - - attachedNode.localToSceneTransformProperty().addListener(updatePositionListener); - cleanupTasks.add(() -> attachedNode.localToSceneTransformProperty().removeListener(updatePositionListener)); - - ChangeListener sceneListener = (_, oldScene, newScene) -> { - if (oldScene != null) { - oldScene.widthProperty().removeListener(updatePositionListener); - oldScene.heightProperty().removeListener(updatePositionListener); - } - if (newScene != null) { - newScene.widthProperty().addListener(updatePositionListener); - newScene.heightProperty().addListener(updatePositionListener); - cleanupTasks.add(() -> { - newScene.widthProperty().removeListener(updatePositionListener); - newScene.heightProperty().removeListener(updatePositionListener); - }); - } - updatePosition(); - }; - - attachedNode.sceneProperty().addListener(sceneListener); - cleanupTasks.add(() -> attachedNode.sceneProperty().removeListener(sceneListener)); - - if (attachedNode.getScene() != null) { - sceneListener.changed(null, null, attachedNode.getScene()); - } - } - - private void startAnimation() { - pulseAnimation = new Timeline( - new KeyFrame(Duration.ZERO, - new KeyValue(pulseIndicator.opacityProperty(), 1.0), - new KeyValue(pulseIndicator.scaleXProperty(), 1.0), - new KeyValue(pulseIndicator.scaleYProperty(), 1.0)), - new KeyFrame(Duration.seconds(0.5), - new KeyValue(pulseIndicator.opacityProperty(), 0.6), - new KeyValue(pulseIndicator.scaleXProperty(), 1.3), - new KeyValue(pulseIndicator.scaleYProperty(), 1.3)), - new KeyFrame(Duration.seconds(1.0), - new KeyValue(pulseIndicator.opacityProperty(), 1.0), - new KeyValue(pulseIndicator.scaleXProperty(), 1.0), - new KeyValue(pulseIndicator.scaleYProperty(), 1.0)) - ); - pulseAnimation.setCycleCount(Timeline.INDEFINITE); - pulseAnimation.play(); - } - - private void updatePosition() { - if (pulseIndicator == null || attachedNode == null || !isNodeVisible()) { - setIndicatorVisible(false); - return; - } - - Bounds localBounds = attachedNode.getBoundsInLocal(); - if (localBounds.isEmpty()) { - setIndicatorVisible(false); - return; - } - - Bounds targetBoundsInScene = attachedNode.localToScene(localBounds); - if (targetBoundsInScene == null || rootPane.getScene() == null) { - setIndicatorVisible(false); - return; - } - - Bounds targetBoundsInRoot = rootPane.sceneToLocal(targetBoundsInScene); - if (targetBoundsInRoot == null) { - setIndicatorVisible(false); - return; - } - - setIndicatorVisible(true); - positionIndicator(targetBoundsInRoot); - } - - // FIXME: This check is still fail for some cases - private boolean isNodeVisible() { - return attachedNode.isVisible() && - attachedNode.getScene() != null && - attachedNode.getLayoutBounds().getWidth() > 0 && - attachedNode.getLayoutBounds().getHeight() > 0; - } - - private void setIndicatorVisible(boolean visible) { - if (pulseIndicator != null) { - pulseIndicator.setVisible(visible); - } - } - - private void positionIndicator(Bounds targetBounds) { - double indicatorX = targetBounds.getMaxX() - 5; - double indicatorY = targetBounds.getMinY() + 5; - - pulseIndicator.setLayoutX(indicatorX); - pulseIndicator.setLayoutY(indicatorY); - pulseIndicator.toFront(); - } - - public void stop() { - if (pulseAnimation != null) { - pulseAnimation.stop(); - pulseAnimation = null; - } - - if (pulseIndicator != null && pulseIndicator.getParent() instanceof Pane parentPane) { - parentPane.getChildren().remove(pulseIndicator); - pulseIndicator = null; - } - - cleanupTasks.forEach(Runnable::run); - cleanupTasks.clear(); - - attachedNode = null; - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java new file mode 100644 index 00000000000..68d82871c5e --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java @@ -0,0 +1,131 @@ +package org.jabref.gui.walkthrough; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.stage.Window; + +import org.jabref.gui.walkthrough.components.BackdropHighlight; +import org.jabref.gui.walkthrough.components.FullScreenDarken; +import org.jabref.gui.walkthrough.components.PulseAnimateIndicator; +import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; +import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; + +import org.jspecify.annotations.NonNull; + +/** + * Manages highlight effects across multiple windows for walkthrough steps. + */ +public class HighlightManager { + private final Map backdropHighlights = new HashMap<>(); + private final Map pulseIndicators = new HashMap<>(); + private final Map fullScreenDarkens = new HashMap<>(); + + /** + * Applies the specified highlight configuration. + * + * @param mainScene The primary scene to apply the highlight to. + * @param highlightConfig The optional highlight configuration to apply. Default to + * BackdropHighlight on the primary windows if empty. + * @param fallbackTarget The optional fallback target node to use if no highlight + * configuration is provided. + */ + public void applyHighlight(@NonNull Scene mainScene, + Optional highlightConfig, + Optional fallbackTarget) { + detachAll(); + + highlightConfig.ifPresentOrElse( + c -> { + if (c.windowEffects().isEmpty() && c.fallbackEffect().isPresent()) { + applyEffect(mainScene.getWindow(), c.fallbackEffect().get(), fallbackTarget); + return; + } + c.windowEffects().forEach(effect -> { + Window window = effect.windowResolver().resolve().orElse(mainScene.getWindow()); + Optional targetNode = effect.targetNodeResolver() + .map(resolver -> + resolver.resolve(Optional.ofNullable(window.getScene()).orElse(mainScene))) + .orElse(fallbackTarget); + applyEffect(window, effect.effect(), targetNode); + }); + }, + () -> fallbackTarget.ifPresent(node -> applyBackdropHighlight(mainScene.getWindow(), node)) + ); + } + + /** + * Detaches all active highlight effects. + */ + public void detachAll() { + backdropHighlights.values().forEach(BackdropHighlight::detach); + backdropHighlights.clear(); + + pulseIndicators.values().forEach(PulseAnimateIndicator::detach); + pulseIndicators.clear(); + + fullScreenDarkens.values().forEach(FullScreenDarken::detach); + fullScreenDarkens.clear(); + } + + private void applyEffect(Window window, HighlightEffect effect, Optional targetNode) { + switch (effect) { + case BACKDROP_HIGHLIGHT -> + targetNode.ifPresent(node -> applyBackdropHighlight(window, node)); + case ANIMATED_PULSE -> + targetNode.ifPresent(node -> applyPulseAnimation(window, node)); + case FULL_SCREEN_DARKEN -> applyFullScreenDarken(window); + case NONE -> { + if (backdropHighlights.containsKey(window)) { + backdropHighlights.get(window).detach(); + backdropHighlights.remove(window); + } + if (pulseIndicators.containsKey(window)) { + pulseIndicators.get(window).detach(); + pulseIndicators.remove(window); + } + if (fullScreenDarkens.containsKey(window)) { + fullScreenDarkens.get(window).detach(); + fullScreenDarkens.remove(window); + } + } + } + } + + private void applyBackdropHighlight(Window window, Node targetNode) { + Scene scene = window.getScene(); + if (scene == null || !(scene.getRoot() instanceof Pane pane)) { + return; + } + + BackdropHighlight backdrop = new BackdropHighlight(pane); + backdrop.attach(targetNode); + backdropHighlights.put(window, backdrop); + } + + private void applyPulseAnimation(Window window, Node targetNode) { + Scene scene = window.getScene(); + if (scene == null || !(scene.getRoot() instanceof Pane pane)) { + return; + } + + PulseAnimateIndicator pulse = new PulseAnimateIndicator(pane); + pulse.attach(targetNode); + pulseIndicators.put(window, pulse); + } + + private void applyFullScreenDarken(Window window) { + Scene scene = window.getScene(); + if (scene == null || !(scene.getRoot() instanceof Pane pane)) { + return; + } + + FullScreenDarken fullDarken = new FullScreenDarken(pane); + fullDarken.attach(); + fullScreenDarkens.put(window, fullDarken); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java new file mode 100644 index 00000000000..8e36b8787e7 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java @@ -0,0 +1,139 @@ +package org.jabref.gui.walkthrough; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.Window; +import javafx.util.Duration; + +import org.jabref.gui.walkthrough.declarative.WindowResolver; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; + +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages walkthrough overlays and highlights across multiple windows. + */ +public class MultiWindowWalkthroughOverlay { + private static final Logger LOGGER = LoggerFactory.getLogger(MultiWindowWalkthroughOverlay.class); + + private final Map overlays = new HashMap<>(); + private final Stage mainStage; + private final HighlightManager highlightManager; + private final Walkthrough walkthrough; + private Timeline nodePollingTimeline; + + public MultiWindowWalkthroughOverlay(Stage mainStage, Walkthrough walkthrough) { + this.mainStage = mainStage; + this.walkthrough = walkthrough; + this.highlightManager = new HighlightManager(); + } + + public void displayStep(@NonNull WalkthroughNode step) { + overlays.values().forEach(SingleWindowOverlay::hide); + Window activeWindow = step.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); + Scene scene = activeWindow.getScene(); + + Optional targetNode = step.resolver().resolve(scene); + if (targetNode.isEmpty()) { + if (step.autoFallback()) { + tryRevertToPreviousResolvableStep(); + } else { + startNodePolling(step, activeWindow); + } + return; + } + + if (step.highlight().isPresent()) { + highlightManager.applyHighlight(scene, step.highlight(), targetNode); + } + + SingleWindowOverlay overlay = getOrCreateOverlay(activeWindow); + overlay.displayStep(step, targetNode.get(), walkthrough); + } + + public void detachAll() { + stopNodePolling(); + highlightManager.detachAll(); + overlays.values().forEach(SingleWindowOverlay::detach); + overlays.clear(); + } + + private void tryRevertToPreviousResolvableStep() { + LOGGER.info("Attempting to revert to previous resolvable step"); + + int currentIndex = walkthrough.currentStepProperty().get(); + for (int i = currentIndex - 1; i >= 0; i--) { + WalkthroughNode previousStep = getStepAtIndex(i); + if (previousStep != null) { + Window activeWindow = previousStep.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); + Scene scene = activeWindow.getScene(); + if (scene != null && previousStep.resolver().resolve(scene).isPresent()) { + LOGGER.info("Reverting to step {} from step {}", i, currentIndex); + walkthrough.goToStep(i); + return; + } + } + } + + LOGGER.warn("No previous resolvable step found, staying at current step"); + } + + private void startNodePolling(WalkthroughNode step, Window activeWindow) { + LOGGER.info("Auto-fallback disabled for step: {}, starting node polling", step.title()); + stopNodePolling(); + + nodePollingTimeline = new Timeline(new KeyFrame(Duration.millis(100), _ -> { + Scene scene = activeWindow.getScene(); + if (scene != null) { + Optional targetNode = step.resolver().resolve(scene); + if (targetNode.isPresent()) { + LOGGER.info("Target node found for step: {}, displaying step", step.title()); + stopNodePolling(); + + if (step.highlight().isPresent()) { + highlightManager.applyHighlight(scene, step.highlight(), targetNode); + } + + SingleWindowOverlay overlay = getOrCreateOverlay(activeWindow); + overlay.displayStep(step, targetNode.get(), walkthrough); + } + } + })); + + nodePollingTimeline.setCycleCount(Timeline.INDEFINITE); + nodePollingTimeline.play(); + } + + private void stopNodePolling() { + if (nodePollingTimeline != null) { + nodePollingTimeline.stop(); + nodePollingTimeline = null; + } + } + + private WalkthroughNode getStepAtIndex(int index) { + try { + return walkthrough.getSteps().get(index); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + private SingleWindowOverlay getOrCreateOverlay(Window window) { + return overlays.computeIfAbsent(window, w -> { + if (w instanceof Stage stage) { + return new SingleWindowOverlay(stage); + } + throw new IllegalArgumentException("Only Stage windows are supported for overlays"); + }); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java new file mode 100644 index 00000000000..65d82d8b0e7 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java @@ -0,0 +1,310 @@ +package org.jabref.gui.walkthrough; + +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.value.ChangeListener; +import javafx.geometry.Bounds; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Polygon; +import javafx.scene.shape.Rectangle; +import javafx.stage.Stage; + +import org.jabref.gui.walkthrough.declarative.step.PanelStep; +import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; +import org.jabref.gui.walkthrough.declarative.step.TooltipStep; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the overlay for displaying walkthrough steps in a single window. + */ +public class SingleWindowOverlay { + private static final Logger LOGGER = LoggerFactory.getLogger(SingleWindowOverlay.class); + private static final double MARGIN = 10.0; + private static final double ARROW_OVERLAP = 3.0; + private final Stage parentStage; + private final GridPane overlayPane; + private final Pane originalRoot; + private final StackPane stackPane; + private final WalkthroughRenderer renderer; + private final List cleanUpTasks = new ArrayList<>(); + + public SingleWindowOverlay(Stage stage) { + this.parentStage = stage; + this.renderer = new WalkthroughRenderer(); + + overlayPane = new GridPane(); + overlayPane.setStyle("-fx-background-color: transparent;"); + overlayPane.setPickOnBounds(false); + overlayPane.setMaxWidth(Double.MAX_VALUE); + overlayPane.setMaxHeight(Double.MAX_VALUE); + + Scene scene = stage.getScene(); + assert scene != null; + + originalRoot = (Pane) scene.getRoot(); + stackPane = new StackPane(); + + stackPane.getChildren().add(originalRoot); + stackPane.getChildren().add(overlayPane); + + scene.setRoot(stackPane); + } + + /** + * Displays a walkthrough step with the specified target node. + */ + public void displayStep(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { + overlayPane.getChildren().clear(); + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); + + displayStepContent(step, targetNode, walkthrough); + overlayPane.toFront(); + } + + /** + * Hide the overlay and clean up any resources. + */ + public void hide() { + overlayPane.getChildren().clear(); + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); + } + + /** + * Detaches the overlay and restores the original scene root. + */ + public void detach() { + hide(); + + Scene scene = parentStage.getScene(); + if (scene != null && originalRoot != null) { + stackPane.getChildren().remove(originalRoot); + scene.setRoot(originalRoot); + LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); + } + } + + private void displayStepContent(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { + Node stepContent; + if (step instanceof TooltipStep tooltipStep) { + stepContent = renderer.render(tooltipStep, walkthrough); + displayTooltipContent(stepContent, targetNode, tooltipStep); + } else if (step instanceof PanelStep panelStep) { + stepContent = renderer.render(panelStep, walkthrough); + displayPanelContent(stepContent, panelStep.position()); + } + + step.navigationPredicate().ifPresent(predicate -> cleanUpTasks.add(predicate.attachListeners(targetNode, walkthrough::nextStep))); + } + + private void displayTooltipContent(Node content, Node targetNode, TooltipStep step) { + StackPane tooltipContainer = new StackPane(); + tooltipContainer.setStyle("-fx-background-color: transparent;"); + + Polygon arrow = createArrow(); + arrow.getStyleClass().add("walkthrough-tooltip-arrow"); + arrow.setStyle("-fx-fill: white; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 4, 0, 0, 2);"); + + content.setStyle(content.getStyle() + "; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 4, 0, 0, 2);"); + + tooltipContainer.getChildren().addAll(content, arrow); + + overlayPane.getChildren().clear(); + overlayPane.setAlignment(Pos.TOP_LEFT); + overlayPane.getChildren().add(tooltipContainer); + + setupClipping(tooltipContainer); + + ChangeListener layoutListener = (_, _, newBounds) -> { + if (newBounds.getWidth() > 0 && newBounds.getHeight() > 0) { + positionTooltipWithArrow(tooltipContainer, content, arrow, targetNode, step); + } + }; + content.boundsInLocalProperty().addListener(layoutListener); + + if (content.getBoundsInLocal().getWidth() > 0) { + positionTooltipWithArrow(tooltipContainer, content, arrow, targetNode, step); + } + + cleanUpTasks.add(() -> content.boundsInLocalProperty().removeListener(layoutListener)); + } + + private void displayPanelContent(Node content, Pos position) { + overlayPane.getChildren().clear(); + overlayPane.getChildren().add(content); + + setupClipping(content); + + overlayPane.getRowConstraints().clear(); + overlayPane.getColumnConstraints().clear(); + overlayPane.setAlignment(position); + + GridPane.setHgrow(content, Priority.NEVER); + GridPane.setVgrow(content, Priority.NEVER); + GridPane.setFillWidth(content, false); + GridPane.setFillHeight(content, false); + + RowConstraints rowConstraints = new RowConstraints(); + ColumnConstraints columnConstraints = new ColumnConstraints(); + + switch (position) { + case CENTER_LEFT: + case CENTER_RIGHT: + rowConstraints.setVgrow(Priority.ALWAYS); + columnConstraints.setHgrow(Priority.NEVER); + GridPane.setFillHeight(content, true); + break; + case TOP_CENTER: + case BOTTOM_CENTER: + columnConstraints.setHgrow(Priority.ALWAYS); + rowConstraints.setVgrow(Priority.NEVER); + GridPane.setFillWidth(content, true); + break; + default: + LOGGER.warn("Unsupported position for panel step: {}", position); + break; + } + + overlayPane.getRowConstraints().add(rowConstraints); + overlayPane.getColumnConstraints().add(columnConstraints); + } + + private void setupClipping(Node node) { + ChangeListener listener = (_, _, bounds) -> { + Rectangle clip = new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); + overlayPane.setClip(clip); + }; + node.boundsInParentProperty().addListener(listener); + cleanUpTasks.add(() -> node.boundsInParentProperty().removeListener(listener)); + cleanUpTasks.add(() -> overlayPane.setClip(null)); + } + + private Polygon createArrow() { + Polygon arrow = new Polygon(); + arrow.getPoints().addAll(new Double[] { + 0.0, 0.0, + 12.0, 15.0, + -12.0, 15.0 + }); + return arrow; + } + + private void positionTooltipWithArrow(StackPane container, Node tooltip, Node arrow, Node target, TooltipStep step) { + Scene scene = parentStage.getScene(); + if (scene == null) { + return; + } + + Bounds targetBounds = target.localToScene(target.getBoundsInLocal()); + if (targetBounds == null) { + LOGGER.warn("Could not determine bounds for target node."); + return; + } + + Bounds tooltipBounds = tooltip.getBoundsInLocal(); + double tooltipWidth = tooltipBounds.getWidth(); + double tooltipHeight = tooltipBounds.getHeight(); + + TooltipPosition finalPosition = determinePosition(step.position(), targetBounds, tooltipWidth, tooltipHeight, scene); + + double tooltipX = switch (finalPosition) { + case TOP, BOTTOM -> + targetBounds.getMinX() + (targetBounds.getWidth() - tooltipWidth) / 2; + case LEFT -> targetBounds.getMinX() - tooltipWidth - MARGIN; + case RIGHT, AUTO -> targetBounds.getMaxX() + MARGIN; + }; + + double tooltipY = switch (finalPosition) { + case TOP -> targetBounds.getMinY() - tooltipHeight - MARGIN; + case BOTTOM -> targetBounds.getMaxY() + MARGIN; + case LEFT, RIGHT, AUTO -> + targetBounds.getMinY() + (targetBounds.getHeight() - tooltipHeight) / 2; + }; + + if (finalPosition == TooltipPosition.TOP || finalPosition == TooltipPosition.BOTTOM) { + tooltipX = clamp(tooltipX, MARGIN, scene.getWidth() - tooltipWidth - MARGIN); + } else { + tooltipY = clamp(tooltipY, MARGIN, scene.getHeight() - tooltipHeight - MARGIN); + } + + container.setTranslateX(tooltipX); + container.setTranslateY(tooltipY); + + double targetCenterX = targetBounds.getMinX() + targetBounds.getWidth() / 2; + double targetCenterY = targetBounds.getMinY() + targetBounds.getHeight() / 2; + + positionArrow(arrow, finalPosition, tooltipWidth, tooltipHeight, + targetCenterX - tooltipX, targetCenterY - tooltipY); + } + + private TooltipPosition determinePosition(TooltipPosition position, Bounds targetBounds, + double tooltipWidth, double tooltipHeight, Scene scene) { + if (position != TooltipPosition.AUTO) { + return position; + } + + if (targetBounds.getMaxY() + tooltipHeight + MARGIN < scene.getHeight()) { + return TooltipPosition.BOTTOM; + } else if (targetBounds.getMinY() - tooltipHeight - MARGIN > 0) { + return TooltipPosition.TOP; + } else if (targetBounds.getMaxX() + tooltipWidth + MARGIN < scene.getWidth()) { + return TooltipPosition.RIGHT; + } else if (targetBounds.getMinX() - tooltipWidth - MARGIN > 0) { + return TooltipPosition.LEFT; + } + return TooltipPosition.BOTTOM; + } + + private double clamp(double value, double min, double max) { + return Math.max(min, Math.min(max, value)); + } + + // FIXME: Tweak the padding/margin values to ensure the arrow is positioned correctly + private void positionArrow(Node arrow, TooltipPosition position, double tooltipWidth, + double tooltipHeight, double targetRelX, double targetRelY) { + double arrowX = 0; + double arrowY = 0; + double rotation = 0; + + switch (position) { + case TOP -> { + arrowX = clamp(targetRelX, 10, tooltipWidth - 30); + arrowY = tooltipHeight - ARROW_OVERLAP; + rotation = 180; + } + case BOTTOM -> { + arrowX = clamp(targetRelX, 10, tooltipWidth - 30); + arrowY = -15 + ARROW_OVERLAP; + rotation = 0; + } + case LEFT -> { + arrowX = tooltipWidth - ARROW_OVERLAP; + arrowY = clamp(targetRelY, 10, tooltipHeight - 30); + rotation = 90; + } + case RIGHT, AUTO -> { + arrowX = -15 + ARROW_OVERLAP; + arrowY = clamp(targetRelY, 10, tooltipHeight - 30); + rotation = -90; + } + } + + arrow.setManaged(false); + arrow.setLayoutX(arrowX); + arrow.setLayoutY(arrowY); + arrow.setRotate(rotation); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index d47fb4579dd..e77fd93ba1a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -8,210 +8,37 @@ import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.geometry.Pos; -import javafx.scene.control.Button; +import javafx.scene.Scene; import javafx.stage.Stage; -import org.jabref.gui.ClipBoardManager; -import org.jabref.gui.DialogService; -import org.jabref.gui.LibraryTab; -import org.jabref.gui.StateManager; -import org.jabref.gui.actions.StandardActions; -import org.jabref.gui.frame.JabRefFrame; -import org.jabref.gui.preferences.GuiPreferences; -import org.jabref.gui.undo.CountingUndoManager; -import org.jabref.gui.walkthrough.components.PaperDirectoryChooser; -import org.jabref.gui.walkthrough.declarative.NodeResolverFactory; -import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; -import org.jabref.gui.walkthrough.declarative.richtext.ArbitraryJFXBlock; -import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; -import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; -import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; -import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; -import org.jabref.logic.ai.AiService; -import org.jabref.logic.l10n.Localization; -import org.jabref.logic.preferences.WalkthroughPreferences; -import org.jabref.logic.util.TaskExecutor; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.BibEntryTypesManager; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.types.StandardEntryType; -import org.jabref.model.util.FileUpdateMonitor; - -import com.airhacks.afterburner.injection.Injector; -import org.jspecify.annotations.NonNull; /** - * Manages a walkthrough session by coordinating steps. + * Maintains the state of a walkthrough. */ public class Walkthrough { - private final WalkthroughPreferences preferences; private final IntegerProperty currentStep; private final IntegerProperty totalSteps; private final BooleanProperty active; - // TODO: Consider using Graph instead for complex walkthrough routing e.g., pro user show no walkthrough, new user show full walkthrough, etc. private final List steps; - private Optional overlay = Optional.empty(); + private Optional overlayManager = Optional.empty(); private Stage currentStage; - private final BibDatabaseContext database; /** * Creates a new walkthrough with the specified preferences. - * - * @param preferences The walkthrough preferences to use */ - public Walkthrough(WalkthroughPreferences preferences, @NonNull JabRefFrame frame) { - this.preferences = preferences; - + public Walkthrough(List steps) { this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); - this.database = new BibDatabaseContext(); - - FullScreenStep welcomeNode = WalkthroughNode - .fullScreen(Localization.lang("Welcome to JabRef")) - .content( - new TextBlock(Localization.lang("This quick walkthrough will introduce you to some key features.")), - new InfoBlock(Localization.lang("You can always access this walkthrough from the Help menu.")) - ) - .actions(WalkthroughActionsConfig.builder() - .continueButton(Localization.lang("Start walkthrough")) - .skipButton(Localization.lang("Skip to finish")) - .build()) - .build(); - WalkthroughActionsConfig actions = WalkthroughActionsConfig.all( - Localization.lang("Continue"), - Localization.lang("Skip for Now"), - Localization.lang("Back")); - FullScreenStep paperDirectoryNode = WalkthroughNode - .fullScreen(Localization.lang("Configure paper directory")) - .content( - new TextBlock(Localization.lang("Set up your main file directory where JabRef will look for and store your PDF files and other associated documents.")), - new InfoBlock(Localization.lang("This directory helps JabRef organize your paper files. You can change this later in Preferences.")), - new ArbitraryJFXBlock(_ -> new PaperDirectoryChooser()) - ) - .actions(actions) - .nextStepAction(walkthrough -> { - if (frame.getCurrentLibraryTab() != null) { - walkthrough.nextStep(); - return; - } - - DialogService dialogService = Injector.instantiateModelOrService(DialogService.class); - AiService aiService = Injector.instantiateModelOrService(AiService.class); - GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); - StateManager stateManager = Injector.instantiateModelOrService(StateManager.class); - FileUpdateMonitor fileUpdateMonitor = Injector.instantiateModelOrService(FileUpdateMonitor.class); - BibEntryTypesManager entryTypesManager = Injector.instantiateModelOrService(BibEntryTypesManager.class); - CountingUndoManager undoManager = Injector.instantiateModelOrService(CountingUndoManager.class); - ClipBoardManager clipBoardManager = Injector.instantiateModelOrService(ClipBoardManager.class); - TaskExecutor taskExecutor = Injector.instantiateModelOrService(TaskExecutor.class); - - LibraryTab libraryTab = LibraryTab.createLibraryTab( - this.database, - frame, - dialogService, - aiService, - guiPreferences, - stateManager, - fileUpdateMonitor, - entryTypesManager, - undoManager, - clipBoardManager, - taskExecutor); - frame.addTab(libraryTab, true); - walkthrough.nextStep(); - }) - .build(); - PanelStep createNode = WalkthroughNode - .panel(Localization.lang("Creating a new entry")) - .content( - new TextBlock(Localization.lang("Click the highlighted button to start creating a new bibliographic entry.")), - new InfoBlock(Localization.lang("JabRef supports various entry types like articles, books, and more.")), - new TextBlock(Localization.lang("In the entry editor that opens after clicking the button, choose \"Article\" as the entry type.")) - ) - .resolver(NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY)) - .position(Pos.BOTTOM_CENTER) - .actions(actions) - .nextStepAction(walkthrough -> { - NodeResolverFactory.forAction(StandardActions.CREATE_ENTRY).apply(walkthrough.currentStage.getScene()).ifPresent(node -> { - if (node instanceof Button button) { - button.fire(); - } - }); - walkthrough.nextStep(frame.getMainStage()); - }) - .build(); - // FIXME: Index out of bound. - PanelStep editNode = WalkthroughNode - .panel(Localization.lang("Fill in the entry details")) - .content( - new TextBlock(Localization.lang("In the title field, enter \"JabRef: BibTeX-based literature management software\".")), - new TextBlock(Localization.lang("In the journal field, enter \"TUGboat\".")), - new InfoBlock(Localization.lang("You can fill in more details later. JabRef supports many entry types and fields.")) - ) - .resolver(NodeResolverFactory.forSelector(".editorPane")) - .position(Pos.TOP_CENTER) - .actions(actions) - .nextStepAction(walkthrough -> { - BibEntry exampleEntry = new BibEntry(StandardEntryType.Article) - .withField(StandardField.AUTHOR, "Oliver Kopp and Carl Christian Snethlage and Christoph Schwentker") - .withField(StandardField.TITLE, "JabRef: BibTeX-based literature management software") - .withField(StandardField.JOURNAL, "TUGboat") - .withField(StandardField.VOLUME, "44") - .withField(StandardField.NUMBER, "3") - .withField(StandardField.PAGES, "441--447") - .withField(StandardField.DOI, "10.47397/tb/44-3/tb138kopp-jabref") - .withField(StandardField.ISSN, "0896-3207") - .withField(StandardField.ISSUE, "138") - .withField(StandardField.YEAR, "2023") - .withChanged(true); - var db = database.getDatabase(); - for (BibEntry entry : db.getEntries()) { - db.removeEntry(entry); - } - db.insertEntry(exampleEntry); - walkthrough.nextStep(frame.getMainStage()); - }) - .build(); - // FIXME: Index out of bound. - PanelStep saveNode = WalkthroughNode - .panel(Localization.lang("Saving your work")) - .content( - new TextBlock(Localization.lang("Don't forget to save your library. Click the save button.")), - new InfoBlock(Localization.lang("Regularly saving prevents data loss.")) - ) - .resolver(NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY)) - .position(Pos.CENTER_RIGHT) - .actions(actions) - .nextStepAction(walkthrough -> { - NodeResolverFactory.forAction(StandardActions.SAVE_LIBRARY) - .apply(walkthrough.currentStage.getScene()) - .ifPresent(node -> { - if (node instanceof Button button) { - button.fire(); - } - }); - walkthrough.nextStep(); - }) - .build(); - FullScreenStep completeNode = WalkthroughNode - .fullScreen(Localization.lang("Walkthrough complete")) - .content( - new TextBlock(Localization.lang("You've completed the basic feature tour.")), - new TextBlock(Localization.lang("Explore more features like groups, fetchers, and customization options.")), - new InfoBlock(Localization.lang("Check our documentation for detailed guides.")) - ) - .actions(WalkthroughActionsConfig.builder() - .continueButton(Localization.lang("Complete walkthrough")) - .backButton(Localization.lang("Back")).build()) - .build(); - this.steps = List.of(welcomeNode, paperDirectoryNode, createNode, editNode, saveNode, completeNode); + this.steps = steps; this.totalSteps = new SimpleIntegerProperty(steps.size()); } + public Walkthrough(WalkthroughNode... steps) { + this(List.of(steps)); + } + /** * Gets the current step index property. * @@ -230,34 +57,21 @@ public ReadOnlyIntegerProperty totalStepsProperty() { return totalSteps; } - /** - * Checks if the walkthrough is completed based on preferences. - * - * @return true if the walkthrough has been completed - */ - public boolean isCompleted() { - return preferences.isCompleted(); - } - /** * Starts the walkthrough from the first step. * * @param stage The stage to display the walkthrough on */ public void start(Stage stage) { - if (preferences.isCompleted()) { - return; - } - if (currentStage != stage) { - overlay.ifPresent(WalkthroughOverlay::detach); + overlayManager.ifPresent(MultiWindowWalkthroughOverlay::detachAll); currentStage = stage; - overlay = Optional.of(new WalkthroughOverlay(stage, this)); + overlayManager = Optional.of(new MultiWindowWalkthroughOverlay(stage, this)); } currentStep.set(0); active.set(true); - getCurrentStep().ifPresent((step) -> overlay.ifPresent(overlay -> overlay.displayStep(step))); + getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); } /** @@ -267,28 +81,12 @@ public void nextStep() { int nextIndex = currentStep.get() + 1; if (nextIndex < steps.size()) { currentStep.set(nextIndex); - getCurrentStep().ifPresent((step) -> overlay.ifPresent(overlay -> overlay.displayStep(step))); + getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); } else { - preferences.setCompleted(true); stop(); } } - /** - * Moves to the next step in the walkthrough with stage switching. This method - * handles stage changes by recreating the overlay on the new stage. - * - * @param stage The stage to display the next step on - */ - public void nextStep(Stage stage) { - if (currentStage != stage) { - overlay.ifPresent(WalkthroughOverlay::detach); - currentStage = stage; - overlay = Optional.of(new WalkthroughOverlay(stage, this)); - } - nextStep(); - } - /** * Moves to the previous step in the walkthrough. */ @@ -296,23 +94,37 @@ public void previousStep() { int prevIndex = currentStep.get() - 1; if (prevIndex >= 0) { currentStep.set(prevIndex); - getCurrentStep().ifPresent((step) -> overlay.ifPresent(overlay -> overlay.displayStep(step))); + getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); } } /** - * Skips the walkthrough completely. + * Get scene of the current stage. */ - public void skip() { - preferences.setCompleted(true); - stop(); + public Optional getScene() { + return Optional.ofNullable(currentStage).map(Stage::getScene); } private void stop() { - overlay.ifPresent(WalkthroughOverlay::detach); + overlayManager.ifPresent(MultiWindowWalkthroughOverlay::detachAll); active.set(false); } + public void goToStep(int stepIndex) { + if (stepIndex >= 0 && stepIndex < steps.size()) { + currentStep.set(stepIndex); + getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); + } + } + + public List getSteps() { + return steps; + } + + public void skip() { + stop(); + } + private Optional getCurrentStep() { int index = currentStep.get(); if (index >= 0 && index < steps.size()) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java new file mode 100644 index 00000000000..a5e306b8356 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -0,0 +1,97 @@ +package org.jabref.gui.walkthrough; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javafx.scene.control.ContextMenu; +import javafx.stage.Window; + +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.frame.JabRefFrame; +import org.jabref.gui.walkthrough.declarative.NavigationPredicate; +import org.jabref.gui.walkthrough.declarative.NodeResolver; +import org.jabref.gui.walkthrough.declarative.WindowResolver; +import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; +import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; +import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; +import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; +import org.jabref.gui.walkthrough.declarative.step.TooltipStep; + +public class WalkthroughAction extends SimpleCommand { + private static final Map WALKTHROUGH_REGISTRY = buildRegistry(); + + private final Walkthrough walkthrough; + private final JabRefFrame frame; + + public WalkthroughAction(String name, JabRefFrame frame) { + this.walkthrough = WALKTHROUGH_REGISTRY.get(name); + this.frame = frame; + } + + @Override + public void execute() { + walkthrough.start(frame.getMainStage()); + } + + private static Map buildRegistry() { + Map registry = new HashMap<>(); + + // FIXME: Not internationalized. + TooltipStep step1 = TooltipStep.builder("Hover over \"File\" menu") + .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.BACKDROP_HIGHLIGHT) + .build(); + + TooltipStep step2 = TooltipStep.builder("Select \"Preferences\"") + .resolver(NodeResolver.menuItem("Preferences")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.RIGHT) + .highlight(MultiWindowHighlight.multiple( + new WindowEffect(Optional::empty, HighlightEffect.FULL_SCREEN_DARKEN), + new WindowEffect( + () -> Window.getWindows().stream() + .filter(w -> w instanceof ContextMenu cm && cm.isShowing()) + .findFirst(), + HighlightEffect.ANIMATED_PULSE, + NodeResolver.menuItem("Preferences") + ) + )) + .build(); + + TooltipStep step3 = TooltipStep.builder("Select \"Linked files\" tab") + .resolver(NodeResolver.predicate(node -> + node.getStyleClass().contains("list-cell") && + node.toString().contains("Linked files"))) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.AUTO) + .highlight(MultiWindowHighlight.multiple( + new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) + )) + .autoFallback(false) + .activeWindow(WindowResolver.title("JabRef preferences")) + .build(); + + TooltipStep step4 = TooltipStep.builder("Choose to use main file directory") + .resolver(NodeResolver.fxId("useMainFileDirectory")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.AUTO) + .highlight(MultiWindowHighlight.multiple( + new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) + )) + .activeWindow(WindowResolver.title("JabRef preferences")) + .build(); + + Walkthrough mainFileDirectory = new Walkthrough(List.of(step1, step2, step3, step4)); + registry.put("mainFileDirectory", mainFileDirectory); + + Walkthrough editEntry = new Walkthrough(List.of()); + registry.put("editEntry", editEntry); + return registry; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java deleted file mode 100644 index ae2b4afe181..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.jabref.gui.walkthrough; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import javafx.beans.value.ChangeListener; -import javafx.event.EventHandler; -import javafx.geometry.Bounds; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.RowConstraints; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.Rectangle; -import javafx.stage.Stage; - -import org.jabref.gui.util.BackdropHighlight; -import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; -import org.jabref.gui.walkthrough.declarative.step.PanelStep; -import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Display a walkthrough overlay on top of the main application window. - */ -public class WalkthroughOverlay { - private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); - private final Stage parentStage; - private final Walkthrough walkthrough; - private final GridPane overlayPane; - private final BackdropHighlight backdropHighlight; - private final Pane originalRoot; - private final StackPane stackPane; - private final WalkthroughRenderer renderer; - private final List cleanUpTasks = new ArrayList<>(); - - public WalkthroughOverlay(Stage stage, Walkthrough walkthrough) { - this.parentStage = stage; - this.walkthrough = walkthrough; - this.renderer = new WalkthroughRenderer(); - - overlayPane = new GridPane(); - overlayPane.setStyle("-fx-background-color: transparent;"); - overlayPane.setPickOnBounds(false); - overlayPane.setMaxWidth(Double.MAX_VALUE); - overlayPane.setMaxHeight(Double.MAX_VALUE); - overlayPane.setOnMouseClicked(event -> { - System.out.println("Event clicked!"); - System.out.println(event); - }); - - Scene scene = stage.getScene(); - assert scene != null; - - originalRoot = (Pane) scene.getRoot(); - stackPane = new StackPane(); - - stackPane.getChildren().add(originalRoot); - backdropHighlight = new BackdropHighlight(stackPane); - stackPane.getChildren().add(overlayPane); - - scene.setRoot(stackPane); - } - - public void displayStep(WalkthroughNode step) { - backdropHighlight.detach(); - overlayPane.getChildren().clear(); - cleanUpTasks.forEach(Runnable::run); - cleanUpTasks.clear(); - - if (step == null) { - return; - } - - Node stepContent; - if (step instanceof FullScreenStep fullScreenStep) { - stepContent = renderer.render(fullScreenStep, walkthrough); - displayFullScreenContent(stepContent); - } else if (step instanceof PanelStep panelStep) { - stepContent = renderer.render(panelStep, walkthrough); - displayPanelContent(stepContent, panelStep.position()); - - step.resolver().ifPresent( - resolver -> - resolver.apply(parentStage.getScene()).ifPresentOrElse( - node -> { - backdropHighlight.attach(node); - step.clickOnNodeAction().ifPresent( - action -> { - EventHandler originalHandler = node.getOnMouseClicked(); - node.setOnMouseClicked(event -> { - Optional.ofNullable(originalHandler).ifPresent(handler -> handler.handle(event)); - action.accept(walkthrough); - }); - cleanUpTasks.add(() -> node.setOnMouseClicked(originalHandler)); - }); - }, - () -> LOGGER.warn("Could not resolve target node for step: {}", step.title()) - ) - ); - } - } - - private void displayFullScreenContent(Node content) { - overlayPane.getChildren().clear(); - overlayPane.getChildren().add(content); - - overlayPane.getRowConstraints().clear(); - overlayPane.getColumnConstraints().clear(); - RowConstraints rowConstraints = new RowConstraints(); - rowConstraints.setVgrow(Priority.ALWAYS); - overlayPane.getRowConstraints().add(rowConstraints); - ColumnConstraints columnConstraints = new ColumnConstraints(); - columnConstraints.setHgrow(Priority.ALWAYS); - overlayPane.getColumnConstraints().add(columnConstraints); - - GridPane.setHgrow(content, Priority.ALWAYS); - GridPane.setVgrow(content, Priority.ALWAYS); - GridPane.setFillWidth(content, true); - GridPane.setFillHeight(content, true); - - overlayPane.setAlignment(Pos.CENTER); - } - - private void displayPanelContent(Node panelContent, Pos position) { - overlayPane.getChildren().clear(); - overlayPane.getChildren().add(panelContent); - - ChangeListener listener = (_, _, bounds) -> { - Rectangle clip = new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); - overlayPane.setClip(clip); - }; - panelContent.boundsInParentProperty().addListener(listener); - cleanUpTasks.add(() -> panelContent.boundsInParentProperty().removeListener(listener)); - cleanUpTasks.add(() -> overlayPane.setClip(null)); - - overlayPane.getRowConstraints().clear(); - overlayPane.getColumnConstraints().clear(); - - GridPane.setHgrow(panelContent, Priority.NEVER); - GridPane.setVgrow(panelContent, Priority.NEVER); - GridPane.setFillWidth(panelContent, false); - GridPane.setFillHeight(panelContent, false); - - RowConstraints rowConstraints = new RowConstraints(); - ColumnConstraints columnConstraints = new ColumnConstraints(); - - overlayPane.setAlignment(position); - - switch (position) { - case CENTER_LEFT: - case CENTER_RIGHT: - rowConstraints.setVgrow(Priority.ALWAYS); - columnConstraints.setHgrow(Priority.NEVER); - GridPane.setFillHeight(panelContent, true); - break; - case TOP_CENTER: - case BOTTOM_CENTER: - columnConstraints.setHgrow(Priority.ALWAYS); - rowConstraints.setVgrow(Priority.NEVER); - GridPane.setFillWidth(panelContent, true); - break; - default: - LOGGER.warn("Unsupported position for panel step: {}", position); - break; - } - - overlayPane.getRowConstraints().add(rowConstraints); - overlayPane.getColumnConstraints().add(columnConstraints); - overlayPane.setVisible(true); - } - - /** - * Detaches the overlay and cleans up resources. - */ - public void detach() { - backdropHighlight.detach(); - overlayPane.getChildren().clear(); - cleanUpTasks.forEach(Runnable::run); - cleanUpTasks.clear(); - - Scene scene = parentStage.getScene(); - if (scene != null && originalRoot != null) { - stackPane.getChildren().remove(originalRoot); - scene.setRoot(originalRoot); - LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); - } - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index 7056af89e66..85becb6912a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -11,81 +11,111 @@ import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIconView; -import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; import org.jabref.gui.walkthrough.declarative.richtext.ArbitraryJFXBlock; import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; -import org.jabref.gui.walkthrough.declarative.step.FullScreenStep; import org.jabref.gui.walkthrough.declarative.step.PanelStep; +import org.jabref.gui.walkthrough.declarative.step.TooltipStep; import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; import org.jabref.logic.l10n.Localization; +import org.jspecify.annotations.NonNull; + /** * Renders the walkthrough steps and content blocks into JavaFX Nodes. */ public class WalkthroughRenderer { - public Node render(FullScreenStep step, Walkthrough walkthrough) { - VBox container = makePanel(); - container.setAlignment(Pos.CENTER); - VBox content = new VBox(); - content.getStyleClass().add("walkthrough-fullscreen-content"); - Label titleLabel = new Label(Localization.lang(step.title())); - titleLabel.getStyleClass().add("walkthrough-title"); - VBox contentContainer = makeContent(step, walkthrough); - content.getChildren().addAll(titleLabel, contentContainer, makeActions(step, walkthrough)); - container.getChildren().add(content); - return container; + /** + * Renders a tooltip step into a JavaFX Node. + * + * @param step The tooltip step to render + * @param walkthrough The walkthrough context for navigation + * @return The rendered tooltip Node + */ + public Node render(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) { + return createTooltip(step, walkthrough); } - public Node render(PanelStep step, Walkthrough walkthrough) { + /** + * Renders a panel step into a JavaFX Node. + * + * @param step The panel step to render + * @param walkthrough The walkthrough context for navigation + * @return The rendered panel Node + */ + public Node render(@NonNull PanelStep step, @NonNull Walkthrough walkthrough) { VBox panel = makePanel(); if (step.position() == Pos.CENTER_LEFT || step.position() == Pos.CENTER_RIGHT) { panel.getStyleClass().add("walkthrough-side-panel-vertical"); VBox.setVgrow(panel, Priority.ALWAYS); panel.setMaxHeight(Double.MAX_VALUE); + + step.preferredWidth().ifPresent(width -> { + panel.setPrefWidth(width); + panel.setMaxWidth(width); + panel.setMinWidth(width); + }); } else if (step.position() == Pos.TOP_CENTER || step.position() == Pos.BOTTOM_CENTER) { panel.getStyleClass().add("walkthrough-side-panel-horizontal"); HBox.setHgrow(panel, Priority.ALWAYS); panel.setMaxWidth(Double.MAX_VALUE); - } - HBox header = new HBox(); - header.setAlignment(Pos.CENTER_LEFT); + step.preferredHeight().ifPresent(height -> { + panel.setPrefHeight(height); + panel.setMaxHeight(height); + panel.setMinHeight(height); + }); + } Label titleLabel = new Label(Localization.lang(step.title())); titleLabel.getStyleClass().add("walkthrough-title"); - Region spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); + VBox contentContainer = makeContent(step, walkthrough); + HBox actionsContainer = makeActions(step, walkthrough); - Label stepCounter = new Label(Localization.lang("Step %0 of %1", - String.valueOf(walkthrough.currentStepProperty().get() + 1), - String.valueOf(walkthrough.totalStepsProperty().get()))); - stepCounter.getStyleClass().add("walkthrough-step-counter"); + panel.getChildren().addAll(titleLabel, contentContainer, actionsContainer); - header.getChildren().addAll(titleLabel, spacer, stepCounter); + return panel; + } + + private Node createTooltip(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) { + VBox tooltip = new VBox(6); + tooltip.getStyleClass().add("walkthrough-tooltip"); + + double prefWidth = step.preferredWidth().orElse(300.0); + double prefHeight = step.preferredHeight().orElse(200.0); + + tooltip.setPrefWidth(prefWidth); + tooltip.setPrefHeight(prefHeight); + tooltip.setMaxWidth(prefWidth); + tooltip.setMaxHeight(prefHeight); + + Label titleLabel = new Label(Localization.lang(step.title())); + titleLabel.getStyleClass().add("walkthrough-tooltip-title"); VBox contentContainer = makeContent(step, walkthrough); - Region bottomSpacer = new Region(); - VBox.setVgrow(bottomSpacer, Priority.ALWAYS); - HBox actions = makeActions(step, walkthrough); - panel.getChildren().addAll(header, contentContainer, bottomSpacer, actions); + contentContainer.getStyleClass().add("walkthrough-tooltip-content"); + VBox.setVgrow(contentContainer, Priority.ALWAYS); - return panel; + HBox actionsContainer = makeActions(step, walkthrough); + + tooltip.getChildren().addAll(titleLabel, contentContainer, actionsContainer); + + return tooltip; } - public Node render(ArbitraryJFXBlock block, Walkthrough walkthrough) { + private Node render(@NonNull ArbitraryJFXBlock block, @NonNull Walkthrough walkthrough) { return block.componentFactory().apply(walkthrough); } - public Node render(TextBlock textBlock) { + private Node render(@NonNull TextBlock textBlock) { Label textLabel = new Label(Localization.lang(textBlock.text())); textLabel.getStyleClass().add("walkthrough-text-content"); return textLabel; } - public Node render(InfoBlock infoBlock) { + private Node render(@NonNull InfoBlock infoBlock) { HBox infoContainer = new HBox(); infoContainer.getStyleClass().add("walkthrough-info-container"); JabRefIconView icon = new JabRefIconView(IconTheme.JabRefIcons.INTEGRITY_INFO); @@ -103,7 +133,7 @@ private VBox makePanel() { return container; } - private HBox makeActions(WalkthroughNode step, Walkthrough walkthrough) { + private HBox makeActions(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); actions.setSpacing(0); @@ -111,16 +141,16 @@ private HBox makeActions(WalkthroughNode step, Walkthrough walkthrough) { Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); - if (step.actions().flatMap(WalkthroughActionsConfig::backButtonText).isPresent()) { + if (step.backButtonText().isPresent()) { actions.getChildren().add(makeBackButton(step, walkthrough)); } HBox rightActions = new HBox(); rightActions.setAlignment(Pos.CENTER_RIGHT); rightActions.setSpacing(4); - if (step.actions().flatMap(WalkthroughActionsConfig::skipButtonText).isPresent()) { + if (step.skipButtonText().isPresent()) { rightActions.getChildren().add(makeSkipButton(step, walkthrough)); } - if (step.actions().flatMap(WalkthroughActionsConfig::continueButtonText).isPresent()) { + if (step.continueButtonText().isPresent()) { rightActions.getChildren().add(makeContinueButton(step, walkthrough)); } @@ -129,50 +159,45 @@ private HBox makeActions(WalkthroughNode step, Walkthrough walkthrough) { return actions; } - private VBox makeContent(WalkthroughNode step, Walkthrough walkthrough) { - return new VBox(12, step.content().stream().map(block -> + private VBox makeContent(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + VBox contentBox = new VBox(8); + contentBox.getChildren().addAll(step.content().stream().map(block -> switch (block) { case TextBlock textBlock -> render(textBlock); case InfoBlock infoBlock -> render(infoBlock); - case ArbitraryJFXBlock arbitraryBlock -> - render(arbitraryBlock, walkthrough); + case ArbitraryJFXBlock arbitraryBlock -> render(arbitraryBlock, walkthrough); } ).toArray(Node[]::new)); + return contentBox; } - private Button makeContinueButton(WalkthroughNode step, Walkthrough walkthrough) { - String buttonText = step.actions() - .flatMap(WalkthroughActionsConfig::continueButtonText) + private Button makeContinueButton(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + String buttonText = step.continueButtonText() .orElse("Walkthrough continue button"); Button continueButton = new Button(Localization.lang(buttonText)); continueButton.getStyleClass().add("walkthrough-continue-button"); - continueButton.setOnAction(_ -> step.nextStepAction().ifPresentOrElse( - action -> action.accept(walkthrough), walkthrough::nextStep)); + continueButton.setOnAction(_ -> walkthrough.nextStep()); return continueButton; } - private Button makeSkipButton(WalkthroughNode step, Walkthrough walkthrough) { - String buttonText = step.actions() - .flatMap(WalkthroughActionsConfig::skipButtonText) + private Button makeSkipButton(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + String buttonText = step.skipButtonText() .orElse("Walkthrough skip to finish"); Button skipButton = new Button(Localization.lang(buttonText)); skipButton.getStyleClass().add("walkthrough-skip-button"); - skipButton.setOnAction(_ -> step.skipAction().ifPresentOrElse( - action -> action.accept(walkthrough), walkthrough::skip)); + skipButton.setOnAction(_ -> walkthrough.skip()); return skipButton; } - private Button makeBackButton(WalkthroughNode step, Walkthrough walkthrough) { - String buttonText = step.actions() - .flatMap(WalkthroughActionsConfig::backButtonText) + private Button makeBackButton(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + String buttonText = step.backButtonText() .orElse("Walkthrough back button"); Button backButton = new Button(Localization.lang(buttonText)); backButton.getStyleClass().add("walkthrough-back-button"); - backButton.setOnAction(_ -> step.previousStepAction().ifPresentOrElse( - action -> action.accept(walkthrough), walkthrough::previousStep)); + backButton.setOnAction(_ -> walkthrough.previousStep()); return backButton; } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java new file mode 100644 index 00000000000..02611d16192 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java @@ -0,0 +1,111 @@ +package org.jabref.gui.walkthrough.components; + +import javafx.geometry.Bounds; +import javafx.scene.Node; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.Shape; + +import org.jspecify.annotations.NonNull; + +/** + * Creates a backdrop highlight effect. + */ +public class BackdropHighlight extends WalkthroughEffect { + private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); + + private Node node; + private Rectangle backdrop; + private Rectangle hole; + private Shape overlayShape; + + public BackdropHighlight(@NonNull Pane pane) { + super(pane); + } + + /** + * Attaches the backdrop highlight to the specified node. + * + * @param node The node to attach the backdrop highlight to. + */ + public void attach(@NonNull Node node) { + detach(); + if (overlayShape == null) { + initializeEffect(); + } + this.node = node; + setupNodeListeners(this.node); + setupPaneListeners(); + updateLayout(); + } + + @Override + public void detach() { + super.detach(); + if (overlayShape != null && overlayShape.getParent() instanceof Pane parentPane) { + parentPane.getChildren().remove(overlayShape); + overlayShape = null; + } + this.node = null; + } + + @Override + protected void initializeEffect() { + this.backdrop = new Rectangle(); + this.hole = new Rectangle(); + this.overlayShape = Shape.subtract(backdrop, hole); + this.overlayShape.setFill(OVERLAY_COLOR); + this.overlayShape.setVisible(false); + this.pane.getChildren().add(overlayShape); + } + + @Override + protected void updateLayout() { + if (cannotPositionNode(node)) { + hideEffect(); + return; + } + + Bounds nodeBoundsInScene; + try { + nodeBoundsInScene = node.localToScene(node.getBoundsInLocal()); + } catch (IllegalStateException e) { + hideEffect(); + return; + } + + if (nodeBoundsInScene == null || nodeBoundsInScene.getWidth() <= 0 || nodeBoundsInScene.getHeight() <= 0) { + hideEffect(); + return; + } + + backdrop.setX(0); + backdrop.setY(0); + backdrop.setWidth(pane.getWidth()); + backdrop.setHeight(pane.getHeight()); + + Bounds nodeBoundsInRootPane = pane.sceneToLocal(nodeBoundsInScene); + hole.setX(nodeBoundsInRootPane.getMinX()); + hole.setY(nodeBoundsInRootPane.getMinY()); + hole.setWidth(nodeBoundsInRootPane.getWidth()); + hole.setHeight(nodeBoundsInRootPane.getHeight()); + + Shape oldOverlayShape = this.overlayShape; + int oldIndex = -1; + if (this.pane.getChildren().contains(oldOverlayShape)) { + oldIndex = this.pane.getChildren().indexOf(oldOverlayShape); + this.pane.getChildren().remove(oldIndex); + } + + this.overlayShape = Shape.subtract(backdrop, hole); + this.overlayShape.setFill(OVERLAY_COLOR); + this.overlayShape.setVisible(true); + this.pane.getChildren().add(oldIndex, this.overlayShape); + } + + @Override + protected void hideEffect() { + overlayShape.setVisible(false); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java new file mode 100644 index 00000000000..471015a2ed7 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java @@ -0,0 +1,64 @@ +package org.jabref.gui.walkthrough.components; + +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; + +import org.jspecify.annotations.NonNull; + +/** + * Creates a full screen darken effect. Usually used to force user to ignore certain + * window and focus on the modal. + */ +public class FullScreenDarken extends WalkthroughEffect { + private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); + + private Rectangle overlay; + + public FullScreenDarken(@NonNull Pane pane) { + super(pane); + } + + @Override + protected void initializeEffect() { + this.overlay = new Rectangle(); + this.overlay.setFill(OVERLAY_COLOR); + this.overlay.setVisible(false); + this.pane.getChildren().add(overlay); + } + + /** + * Attaches the effect to the pane + */ + public void attach() { + cleanUp(); + if (overlay == null) { + initializeEffect(); + } + setupPaneListeners(); + updateLayout(); + } + + @Override + public void detach() { + if (overlay != null && overlay.getParent() != null) { + overlay.setVisible(false); + pane.getChildren().remove(overlay); + } + super.detach(); + } + + @Override + protected void updateLayout() { + overlay.setX(0); + overlay.setY(0); + overlay.setWidth(pane.getWidth()); + overlay.setHeight(pane.getHeight()); + overlay.setVisible(true); + } + + @Override + protected void hideEffect() { + overlay.setVisible(false); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java deleted file mode 100644 index 2d267798115..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PaperDirectoryChooser.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.jabref.gui.walkthrough.components; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; - -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.stage.DirectoryChooser; -import javafx.stage.Window; - -import org.jabref.gui.preferences.GuiPreferences; -import org.jabref.logic.FilePreferences; -import org.jabref.logic.l10n.Localization; - -import com.airhacks.afterburner.injection.Injector; - -/** - * Chooses the main directory for storing and searching PDF files in JabRef. - */ -public class PaperDirectoryChooser extends HBox { - private final Label currentDirectoryLabel; - private final StringProperty currentDirectory; - - public PaperDirectoryChooser() { - setSpacing(4); - setPrefHeight(32); - - this.currentDirectory = new SimpleStringProperty(); - currentDirectoryLabel = new Label(); - currentDirectoryLabel.setWrapText(true); - currentDirectoryLabel.setPrefHeight(32); - HBox.setHgrow(currentDirectoryLabel, Priority.ALWAYS); - currentDirectoryLabel.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - - Button browseButton = new Button(Localization.lang("Browse...")); - browseButton.setOnAction(_ -> showDirectoryChooser()); - - getChildren().addAll(browseButton, currentDirectoryLabel); - currentDirectory.addListener((_, _, newVal) -> updateDirectoryDisplay(newVal)); - updateCurrentDirectory(); - } - - private void updateCurrentDirectory() { - GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); - FilePreferences filePreferences = guiPreferences.getFilePreferences(); - Optional mainFileDirectory = filePreferences.getMainFileDirectory(); - currentDirectory.set(mainFileDirectory.map(Path::toString).orElse("")); - } - - private void updateDirectoryDisplay(String directory) { - if (!directory.trim().isEmpty()) { - currentDirectoryLabel.setText(Localization.lang("Current paper directory: %0", directory)); - } else { - currentDirectoryLabel.setText(Localization.lang("No directory currently set.")); - } - } - - private void showDirectoryChooser() { - DirectoryChooser directoryChooser = new DirectoryChooser(); - directoryChooser.setTitle(Localization.lang("Choose directory")); - - String currentDir = currentDirectory.get(); - if (currentDir != null && !currentDir.trim().isEmpty()) { - Path currentPath = Path.of(currentDir); - if (Files.exists(currentPath) && Files.isDirectory(currentPath)) { - directoryChooser.setInitialDirectory(currentPath.toFile()); - } - } - - Window ownerWindow = getScene() != null ? getScene().getWindow() : null; - File selectedDirectory = directoryChooser.showDialog(ownerWindow); - - if (selectedDirectory == null) { - return; - } - - GuiPreferences guiPreferences = Injector.instantiateModelOrService(GuiPreferences.class); - FilePreferences filePreferences = guiPreferences.getFilePreferences(); - filePreferences.setMainFileDirectory(selectedDirectory.getAbsolutePath()); - currentDirectory.set(selectedDirectory.getAbsolutePath()); - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java new file mode 100644 index 00000000000..d96aad76790 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java @@ -0,0 +1,107 @@ +package org.jabref.gui.walkthrough.components; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.geometry.Bounds; +import javafx.scene.Node; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.util.Duration; + +import org.jspecify.annotations.NonNull; + +/** + * A pulsing circular indicator that can be attached to a target node. + */ +public class PulseAnimateIndicator extends WalkthroughEffect { + private Circle pulseIndicator; + private Timeline pulseAnimation; + private Node node; + + public PulseAnimateIndicator(@NonNull Pane pane) { + super(pane); + } + + /** + * Attaches the pulse indicator to the specified node. + * + * @param node The node to attach the pulse indicator to. + */ + public void attach(@NonNull Node node) { + cleanUp(); + if (pulseIndicator == null) { + initializeEffect(); + } + this.node = node; + setupNodeListeners(this.node); + updateLayout(); + } + + @Override + public void detach() { + pulseAnimation.stop(); + if (pulseIndicator.getParent() instanceof Pane parentPane) { + parentPane.getChildren().remove(pulseIndicator); + } + super.detach(); + node = null; + } + + @Override + protected void initializeEffect() { + pulseIndicator = new Circle(8, Color.web("#50618F")); + pulseIndicator.setMouseTransparent(true); + pulseIndicator.setManaged(false); + + pane.getChildren().add(pulseIndicator); + + pulseAnimation = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(pulseIndicator.opacityProperty(), 1.0), + new KeyValue(pulseIndicator.scaleXProperty(), 1.0), + new KeyValue(pulseIndicator.scaleYProperty(), 1.0)), + new KeyFrame(Duration.seconds(0.5), + new KeyValue(pulseIndicator.opacityProperty(), 0.6), + new KeyValue(pulseIndicator.scaleXProperty(), 1.3), + new KeyValue(pulseIndicator.scaleYProperty(), 1.3)), + new KeyFrame(Duration.seconds(1.0), + new KeyValue(pulseIndicator.opacityProperty(), 1.0), + new KeyValue(pulseIndicator.scaleXProperty(), 1.0), + new KeyValue(pulseIndicator.scaleYProperty(), 1.0)) + ); + + pulseAnimation.setCycleCount(Timeline.INDEFINITE); + pulseAnimation.play(); + } + + @Override + protected void updateLayout() { + if (cannotPositionNode(node)) { + hideEffect(); + return; + } + + Bounds localBounds = node.getBoundsInLocal(); + Bounds targetBounds = pane.sceneToLocal(node.localToScene(localBounds)); + if (targetBounds == null) { + hideEffect(); + return; + } + + pulseIndicator.setVisible(true); + + double indicatorX = targetBounds.getMaxX() - 5; + double indicatorY = targetBounds.getMinY() + 5; + + pulseIndicator.setLayoutX(indicatorX); + pulseIndicator.setLayoutY(indicatorY); + pulseIndicator.toFront(); + } + + @Override + protected void hideEffect() { + pulseIndicator.setVisible(false); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java new file mode 100644 index 00000000000..43c3ddffacd --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java @@ -0,0 +1,116 @@ +package org.jabref.gui.walkthrough.components; + +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.stage.Window; + +import com.sun.javafx.scene.NodeHelper; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Base class for walkthrough effects with common listener management and positioning. + */ +public abstract class WalkthroughEffect { + protected final Pane pane; + protected final List cleanupTasks = new ArrayList<>(); + protected final InvalidationListener updateListener = _ -> updateLayout(); + + protected WalkthroughEffect(@NonNull Pane pane) { + this.pane = pane; + initializeEffect(); + } + + protected abstract void initializeEffect(); + + protected abstract void updateLayout(); + + protected abstract void hideEffect(); + + /** + * Detaches the effect, cleaning up listeners and hiding the effect. + */ + public void detach() { + cleanUp(); + hideEffect(); + } + + protected void cleanUp() { + cleanupTasks.forEach(Runnable::run); + cleanupTasks.clear(); + } + + protected void addListener(ObservableValue property) { + property.addListener(updateListener); + cleanupTasks.add(() -> property.removeListener(updateListener)); + } + + protected void addListener(ObservableValue property, ChangeListener listener) { + property.addListener(listener); + cleanupTasks.add(() -> property.removeListener(listener)); + } + + protected void setupNodeListeners(@NonNull Node node) { + addListener(node.boundsInLocalProperty()); + addListener(node.localToSceneTransformProperty()); + addListener(node.visibleProperty()); + + ChangeListener sceneListener = (_, oldScene, newScene) -> { + if (oldScene != null) { + oldScene.widthProperty().removeListener(updateListener); + oldScene.heightProperty().removeListener(updateListener); + } + if (newScene != null) { + addListener(newScene.widthProperty()); + addListener(newScene.heightProperty()); + if (newScene.getWindow() != null) { + Window window = newScene.getWindow(); + addListener(window.widthProperty()); + addListener(window.heightProperty()); + addListener(window.showingProperty()); + } + } + updateLayout(); + }; + + addListener(node.sceneProperty(), sceneListener); + if (node.getScene() != null) { + sceneListener.changed(null, null, node.getScene()); + } + } + + protected void setupPaneListeners() { + addListener(pane.widthProperty()); + addListener(pane.heightProperty()); + addListener(pane.sceneProperty(), (_, _, newScene) -> { + updateLayout(); + if (newScene == null) { + return; + } + addListener(newScene.heightProperty()); + addListener(newScene.widthProperty()); + if (newScene.getWindow() != null) { + addListener(newScene.getWindow().widthProperty()); + addListener(newScene.getWindow().heightProperty()); + } + }); + } + + protected boolean isNodeVisible(@Nullable Node node) { + return node != null && NodeHelper.isTreeVisible(node); + } + + protected boolean cannotPositionNode(@Nullable Node node) { + return node == null || + node.getScene() == null || + !isNodeVisible(node) || + node.getBoundsInLocal().isEmpty(); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java new file mode 100644 index 00000000000..6029638502e --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java @@ -0,0 +1,189 @@ +package org.jabref.gui.walkthrough.declarative; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; +import javafx.event.ActionEvent; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TextInputControl; +import javafx.scene.input.MouseEvent; +import javafx.stage.Window; + +import org.jabref.gui.frame.MainMenu; + +import com.sun.javafx.scene.control.ContextMenuContent; + +/** + * Defines a predicate for when navigation should occur on a target node. + */ +@FunctionalInterface +public interface NavigationPredicate { + /** + * Attaches the navigation listeners to the target node. + * + * @param targetNode the node to attach the listeners to + * @param onNavigate the runnable to execute when navigation occurs + * @return a runnable to clean up the listeners + */ + Runnable attachListeners(Node targetNode, Runnable onNavigate); + + static NavigationPredicate onClick() { + return (targetNode, onNavigate) -> { + EventHandler onClicked = targetNode.getOnMouseClicked(); + targetNode.setOnMouseClicked(ConcurrentNavigationRunner.decorate(onClicked, onNavigate)); + + Optional item = resolveMenuItem(targetNode); + if (item.isEmpty()) { + return () -> targetNode.setOnMouseClicked(onClicked); + } + + System.out.println("TargetNode is a MenuItem: " + targetNode); + System.out.println("MenuItem found: " + item.get().getText()); + EventHandler onAction = item.get().getOnAction(); + item.get().setOnAction(ConcurrentNavigationRunner.decorate(onAction, onNavigate)); + + return () -> { + targetNode.setOnMouseClicked(onClicked); + item.get().setOnAction(onAction); + }; + }; + } + + static NavigationPredicate onHover() { + return (targetNode, onNavigate) -> { + EventHandler onEnter = targetNode.getOnMouseEntered(); + targetNode.setOnMouseEntered(ConcurrentNavigationRunner.decorate(onEnter, onNavigate)); + + Optional item = resolveMenuItem(targetNode); + if (item.isPresent()) { + throw new IllegalArgumentException("onHover cannot be used with MenuItems"); + } + + return () -> targetNode.setOnMouseEntered(onEnter); + }; + } + + static NavigationPredicate onTextInput() { + return (targetNode, onNavigate) -> { + if (targetNode instanceof TextInputControl textInput) { + ChangeListener listener = (_, _, newText) -> { + if (!newText.trim().isEmpty()) { + onNavigate.run(); + } + }; + textInput.textProperty().addListener(listener); + return () -> textInput.textProperty().removeListener(listener); + } + throw new IllegalArgumentException("onTextInput can only be used with TextInputControl"); + }; + } + + static NavigationPredicate manual() { + return (_, _) -> () -> { + }; + } + + static NavigationPredicate auto() { + return (targetNode, onNavigate) -> { + onNavigate.run(); + return () -> { + }; + }; + } + + private static Optional resolveMenuItem(Node node) { + if (!(node instanceof ContextMenuContent) && Stream.iterate(node.getParent(), Objects::nonNull, Parent::getParent) + .noneMatch(ContextMenuContent.class::isInstance)) { + return Optional.empty(); + } + + return Window.getWindows().stream() + .map(Window::getScene) + .filter(Objects::nonNull) + .map(scene -> scene.lookup(".mainMenu")) + .filter(MainMenu.class::isInstance) + .map(MainMenu.class::cast) + .flatMap(menu -> menu.getMenus().stream()) + .flatMap(topLevelMenu -> topLevelMenu.getItems().stream()) + .filter(menuItem -> Optional.ofNullable(menuItem.getGraphic()) + .map(graphic -> graphic.equals(node) || Stream.iterate(graphic, Objects::nonNull, Node::getParent) + .anyMatch(cm -> cm.equals(node))) + .orElse(false)) + .findFirst(); + } + + class ConcurrentNavigationRunner { + private static final long HANDLER_TIMEOUT_MS = 1000; + + static EventHandler decorate( + EventHandler originalHandler, + Runnable onNavigate) { + return event -> navigate(originalHandler, event, onNavigate); + } + + static void navigate( + EventHandler originalHandler, + T event, + Runnable onNavigate) { + + System.out.println("Navigation started for event: " + event); + + CompletableFuture handlerFuture = new CompletableFuture<>(); + + if (originalHandler != null) { + Platform.runLater(() -> { + try { + originalHandler.handle(event); + } finally { + handlerFuture.complete(null); + } + }); + } else { + handlerFuture.complete(null); + } + + CompletableFuture windowFuture = new CompletableFuture<>(); + + ListChangeListener listener = new ListChangeListener<>() { + @Override + public void onChanged(Change change) { + while (change.next()) { + if (change.wasAdded()) { + Window.getWindows().removeListener(this); + windowFuture.complete(null); + return; + } + } + } + }; + + Platform.runLater(() -> Window.getWindows().addListener(listener)); + + CompletableFuture timeoutFuture = CompletableFuture.runAsync(() -> { + try { + Thread.sleep(HANDLER_TIMEOUT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // FIXME: The onNavigate function is ran without any of those futures being completed? + CompletableFuture.anyOf(handlerFuture, windowFuture, timeoutFuture) + .whenComplete((_, _) -> { + Platform.runLater(onNavigate); + timeoutFuture.cancel(true); + Platform.runLater(() -> Window.getWindows().removeListener(listener)); + windowFuture.cancel(true); + }); + } + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java new file mode 100644 index 00000000000..02ca0a2682f --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java @@ -0,0 +1,142 @@ +package org.jabref.gui.walkthrough.declarative; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; +import javafx.stage.Window; + +import org.jabref.gui.actions.StandardActions; +import org.jabref.logic.l10n.Localization; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Resolves nodes from a Scene + */ +@FunctionalInterface +public interface NodeResolver { + /** + * Resolves a node from the given scene. + * + * @param scene the scene to search in + * @return an optional containing the found node, or empty if not found + */ + Optional resolve(@NonNull Scene scene); + + /** + * Creates a resolver that finds a node by CSS selector. + * + * @param selector the CSS selector to find the node + * @return a resolver that finds the node by selector + */ + static NodeResolver selector(@NonNull String selector) { + return scene -> Optional.ofNullable(scene.lookup(selector)); + } + + /** + * Creates a resolver that finds a node by its fx:id. + * + * @param fxId the fx:id of the node + * @return a resolver that finds the node by fx:id + */ + static NodeResolver fxId(@NonNull String fxId) { + return scene -> Optional.ofNullable(scene.lookup("#" + fxId)); + } + + /** + * Creates a resolver that finds a node by a predicate. + * + * @param predicate the predicate to match the node + * @return a resolver that finds the node matching the predicate + */ + static NodeResolver predicate(@NonNull Predicate predicate) { + return scene -> Optional.ofNullable(findNode(scene.getRoot(), predicate)); + } + + /** + * Creates a resolver that finds a button by its StandardAction. + * + * @param action the StandardAction associated with the button + * @return a resolver that finds the button by action + */ + static NodeResolver action(@NonNull StandardActions action) { + return scene -> Optional.ofNullable(findNodeByAction(scene, action)); + } + + /** + * Creates a resolver that finds a menu item by its language key. + * + * @param key the language key of the menu item + * @return a resolver that finds the menu item by language key + */ + static NodeResolver menuItem(@NonNull String key) { + return scene -> Window.getWindows().stream().flatMap(window -> { + if (window instanceof ContextMenu menu && menu.isShowing()) { + return menu.getItems().stream() + .filter(item -> Optional.ofNullable(item.getText()) + .map(str -> str.contains(Localization.lang(key))) + .orElse(false)) + .map(item -> Stream.iterate(item.getGraphic(), Objects::nonNull, Node::getParent) + .filter(node -> node.getStyleClass().contains("menu-item")) + .findFirst() + .orElse(null)); + } + return window.getScene().getRoot().lookupAll(".menu-item").stream() + .filter(node -> node.getStyleClass().contains("menu-item") && + node.toString().contains(Localization.lang(key))); + }).findFirst(); + } + + @Nullable + private static Node findNodeByAction(@NonNull Scene scene, @NonNull StandardActions action) { + return findNode(scene.getRoot(), node -> { + if (node instanceof Button button) { + if (button.getTooltip() != null) { + String tooltipText = button.getTooltip().getText(); + if (tooltipText != null && tooltipText.equals(action.getText())) { + return true; + } + } + + if (button.getStyleClass().contains("icon-button")) { + String actionText = action.getText(); + if (button.getTooltip() != null && button.getTooltip().getText() != null) { + String tooltipText = button.getTooltip().getText(); + if (tooltipText.startsWith(actionText) || tooltipText.contains(actionText)) { + return true; + } + } + + return button.getText() != null && button.getText().equals(actionText); + } + } + return false; + }); + } + + @Nullable + private static Node findNode(@NonNull Node root, @NonNull Predicate predicate) { + if (predicate.test(root)) { + return root; + } + + if (root instanceof Parent parent) { + for (Node child : parent.getChildrenUnmodifiable()) { + Node result = findNode(child, predicate); + if (result != null) { + return result; + } + } + } + + return null; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java deleted file mode 100644 index d9b30537912..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolverFactory.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.jabref.gui.walkthrough.declarative; - -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; - -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.Button; - -import org.jabref.gui.actions.StandardActions; - -/** - * Factory class for creating different types of node resolvers. - */ -public class NodeResolverFactory { - private NodeResolverFactory() { - } - - /** - * Creates a resolver that finds a node by CSS selector - * - * @param selector The CSS selector to find the node - * @return A function that resolves the node from a Scene - */ - public static Function> forSelector(String selector) { - return scene -> Optional.ofNullable(scene.lookup(selector)); - } - - /** - * Creates a resolver that finds a node by its fx:id - * - * @param fxId The fx:id of the node - * @return A function that resolves the node from a Scene - */ - public static Function> forFxId(String fxId) { - return scene -> Optional.ofNullable(scene.lookup("#" + fxId)); - } - - /** - * Creates a resolver that returns a node from a supplier - * - * @param nodeSupplier A supplier that provides the node - * @return A function that resolves the node from a Scene (ignoring the Scene parameter) - */ - public static Function> forNodeSupplier(Supplier nodeSupplier) { - return _ -> Optional.ofNullable(nodeSupplier.get()); - } - - /** - * Creates a resolver that finds a node by a predicate - * - * @param predicate The predicate to match the node - * @return A function that resolves the node from a Scene - */ - public static Function> forPredicate(Predicate predicate) { - return scene -> Optional.ofNullable(findNode(scene.getRoot(), predicate)); - } - - /** - * Creates a resolver that finds a button by its StandardAction - * - * @param action The StandardAction associated with the button - * @return A function that resolves the button from a Scene - */ - public static Function> forAction(StandardActions action) { - return scene -> Optional.ofNullable(findNodeByAction(scene, action)); - } - - private static Node findNodeByAction(Scene scene, StandardActions action) { - return findNode(scene.getRoot(), node -> { - if (node instanceof Button button) { - if (button.getTooltip() != null) { - String tooltipText = button.getTooltip().getText(); - if (tooltipText != null && tooltipText.equals(action.getText())) { - return true; - } - } - - if (button.getStyleClass().contains("icon-button")) { - String actionText = action.getText(); - if (button.getTooltip() != null && button.getTooltip().getText() != null) { - String tooltipText = button.getTooltip().getText(); - if (tooltipText.startsWith(actionText) || tooltipText.contains(actionText)) { - return true; - } - } - - return button.getText() != null && button.getText().equals(actionText); - } - } - return false; - }); - } - - private static Node findNode(Node root, Predicate predicate) { - if (predicate.test(root)) { - return root; - } - - if (root instanceof Parent parent) { - for (Node child : parent.getChildrenUnmodifiable()) { - Node result = findNode(child, predicate); - if (result != null) { - return result; - } - } - } - - return null; - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java deleted file mode 100644 index 38128811b40..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WalkthroughActionsConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.jabref.gui.walkthrough.declarative; - -import java.util.Optional; - -/** - * Configuration for walkthrough step buttons. - * - * @param continueButtonText Optional text for the continue button. If empty, button is - * hidden. - * @param skipButtonText Optional text for the skip button. If empty, button is - * hidden. - * @param backButtonText Optional text for the back button. If empty, button is - * hidden. - */ -public record WalkthroughActionsConfig( - Optional continueButtonText, - Optional skipButtonText, - Optional backButtonText -) { - - public static Builder builder() { - return new Builder(); - } - - public static WalkthroughActionsConfig all(String continueText, String skipText, String backText) { - return new WalkthroughActionsConfig(Optional.of(continueText), Optional.of(skipText), Optional.of(backText)); - } - - public static class Builder { - private Optional continueButtonText = Optional.empty(); - private Optional skipButtonText = Optional.empty(); - private Optional backButtonText = Optional.empty(); - - public Builder continueButton(String text) { - this.continueButtonText = Optional.of(text); - return this; - } - - public Builder skipButton(String text) { - this.skipButtonText = Optional.of(text); - return this; - } - - public Builder backButton(String text) { - this.backButtonText = Optional.of(text); - return this; - } - - public WalkthroughActionsConfig build() { - return new WalkthroughActionsConfig(continueButtonText, skipButtonText, backButtonText); - } - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java new file mode 100644 index 00000000000..714aef718fa --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java @@ -0,0 +1,38 @@ +package org.jabref.gui.walkthrough.declarative; + +import java.util.Optional; + +import javafx.stage.Stage; +import javafx.stage.Window; + +import org.jabref.logic.l10n.Localization; + +import org.jspecify.annotations.NonNull; + +/** + * Resolves windows using various strategies. + */ +@FunctionalInterface +public interface WindowResolver { + /** + * Resolves a window. + * + * @return an optional containing the found window, or empty if not found + */ + Optional resolve(); + + /** + * Creates a resolver that finds a window by its title. + * + * @param key the language key of the window title + * @return a resolver that finds the window by title + */ + static WindowResolver title(@NonNull String key) { + return () -> Window.getWindows().stream() + .filter(Stage.class::isInstance) + .map(Stage.class::cast) + .filter(stage -> stage.getTitle().contains(Localization.lang(key))) + .map(Window.class::cast) + .findFirst(); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java new file mode 100644 index 00000000000..ecfe78ea355 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java @@ -0,0 +1,23 @@ +package org.jabref.gui.walkthrough.declarative.effect; + +public enum HighlightEffect { + /** + * See {@link org.jabref.gui.walkthrough.components.BackdropHighlight} + */ + BACKDROP_HIGHLIGHT, + + /** + * See {@link org.jabref.gui.walkthrough.components.PulseAnimateIndicator} + */ + ANIMATED_PULSE, + + /** + * See {@link org.jabref.gui.walkthrough.components.FullScreenDarken} + */ + FULL_SCREEN_DARKEN, + + /** + * No highlight effect is applied. + */ + NONE +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java new file mode 100644 index 00000000000..7406e4902ea --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java @@ -0,0 +1,28 @@ +package org.jabref.gui.walkthrough.declarative.effect; + +import java.util.List; +import java.util.Optional; + +/** + * Highlighting effects across multiple windows. + */ +public record MultiWindowHighlight( + List windowEffects, + Optional fallbackEffect +) { + public static MultiWindowHighlight single(WindowEffect windowEffect) { + return single(windowEffect, HighlightEffect.FULL_SCREEN_DARKEN); + } + + public static MultiWindowHighlight single(WindowEffect windowEffect, HighlightEffect fallback) { + return new MultiWindowHighlight(List.of(windowEffect), Optional.of(fallback)); + } + + public static MultiWindowHighlight multiple(WindowEffect... windowEffects) { + return multiple(HighlightEffect.FULL_SCREEN_DARKEN, windowEffects); + } + + public static MultiWindowHighlight multiple(HighlightEffect fallback, WindowEffect... windowEffects) { + return new MultiWindowHighlight(List.of(windowEffects), Optional.of(fallback)); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java new file mode 100644 index 00000000000..fb775df6b9b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java @@ -0,0 +1,23 @@ +package org.jabref.gui.walkthrough.declarative.effect; + +import java.util.Optional; + +import org.jabref.gui.walkthrough.declarative.NodeResolver; +import org.jabref.gui.walkthrough.declarative.WindowResolver; + +/** + * Represents a highlight effect configuration for a specific window. + */ +public record WindowEffect( + WindowResolver windowResolver, + HighlightEffect effect, + Optional targetNodeResolver +) { + public WindowEffect(WindowResolver windowResolver, HighlightEffect effect) { + this(windowResolver, effect, Optional.empty()); + } + + public WindowEffect(WindowResolver windowResolver, HighlightEffect effect, NodeResolver targetNodeResolver) { + this(windowResolver, effect, Optional.of(targetNodeResolver)); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java deleted file mode 100644 index 9e1d4f5ba69..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/FullScreenStep.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.jabref.gui.walkthrough.declarative.step; - -import java.util.List; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; - -import javafx.scene.Node; -import javafx.scene.Scene; - -import org.jabref.gui.walkthrough.Walkthrough; -import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; -import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; - -public record FullScreenStep( - String title, - List content, - Optional>> resolver, - Optional actions, - Optional> nextStepAction, - Optional> previousStepAction, - Optional> skipAction, - Optional> clickOnNodeAction -) implements WalkthroughNode { - public static Builder builder(String title) { - return new Builder(title); - } - - public static class Builder { - private final String title; - private List content = List.of(); - private Optional>> resolver = Optional.empty(); - private Optional actions = Optional.empty(); - private Optional> nextStepAction = Optional.empty(); - private Optional> previousStepAction = Optional.empty(); - private Optional> skipAction = Optional.empty(); - private Optional> clickOnNodeAction = Optional.empty(); - - private Builder(String title) { - this.title = title; - } - - public Builder content(WalkthroughRichTextBlock... blocks) { - this.content = List.of(blocks); - return this; - } - - public Builder content(List content) { - this.content = content; - return this; - } - - public Builder resolver(Function> resolver) { - this.resolver = Optional.of(resolver); - return this; - } - - public Builder actions(WalkthroughActionsConfig actions) { - this.actions = Optional.of(actions); - return this; - } - - public Builder nextStepAction(Consumer nextStepAction) { - this.nextStepAction = Optional.of(nextStepAction); - return this; - } - - public Builder previousStepAction(Consumer previousStepAction) { - this.previousStepAction = Optional.of(previousStepAction); - return this; - } - - public Builder skipAction(Consumer skipAction) { - this.skipAction = Optional.of(skipAction); - return this; - } - - public Builder clickOnNodeAction(Consumer clickOnNodeAction) { - this.clickOnNodeAction = Optional.of(clickOnNodeAction); - return this; - } - - public FullScreenStep build() { - return new FullScreenStep(title, content, resolver, actions, nextStepAction, - previousStepAction, skipAction, clickOnNodeAction); - } - } -} - diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index 480d65ece1c..1e667f87373 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -2,27 +2,33 @@ import java.util.List; import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.Scene; -import org.jabref.gui.walkthrough.Walkthrough; -import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.NavigationPredicate; +import org.jabref.gui.walkthrough.declarative.NodeResolver; +import org.jabref.gui.walkthrough.declarative.WindowResolver; +import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; +import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; +import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; +import org.jspecify.annotations.NonNull; + public record PanelStep( String title, List content, - Optional>> resolver, - Optional actions, - Optional> nextStepAction, - Optional> previousStepAction, - Optional> skipAction, - Optional> clickOnNodeAction, - Pos position + NodeResolver resolver, + Optional continueButtonText, + Optional skipButtonText, + Optional backButtonText, + Optional navigationPredicate, + Pos position, + Optional preferredWidth, + Optional preferredHeight, + Optional highlight, + boolean autoFallback, + Optional activeWindowResolver ) implements WalkthroughNode { public static Builder builder(String title) { return new Builder(title); @@ -31,13 +37,17 @@ public static Builder builder(String title) { public static class Builder { private final String title; private List content = List.of(); - private Optional>> resolver = Optional.empty(); - private Optional actions = Optional.empty(); - private Optional> nextStepAction = Optional.empty(); - private Optional> previousStepAction = Optional.empty(); - private Optional> skipAction = Optional.empty(); - private Optional> clickOnNodeAction = Optional.empty(); + private NodeResolver resolver; + private Optional continueButtonText = Optional.empty(); + private Optional skipButtonText = Optional.empty(); + private Optional backButtonText = Optional.empty(); + private Optional navigationPredicate = Optional.empty(); private Pos position = Pos.CENTER; + private Optional preferredWidth = Optional.empty(); + private Optional preferredHeight = Optional.empty(); + private Optional highlight = Optional.empty(); + private boolean autoFallback = true; + private Optional activeWindowResolver = Optional.empty(); private Builder(String title) { this.title = title; @@ -53,44 +63,71 @@ public Builder content(List content) { return this; } - public Builder resolver(Function> resolver) { - this.resolver = Optional.of(resolver); + public Builder resolver(@NonNull NodeResolver resolver) { + this.resolver = resolver; return this; } - public Builder actions(WalkthroughActionsConfig actions) { - this.actions = Optional.of(actions); + public Builder continueButton(@NonNull String text) { + this.continueButtonText = Optional.of(text); return this; } - public Builder nextStepAction(Consumer nextStepAction) { - this.nextStepAction = Optional.of(nextStepAction); + public Builder skipButton(@NonNull String text) { + this.skipButtonText = Optional.of(text); return this; } - public Builder previousStepAction(Consumer previousStepAction) { - this.previousStepAction = Optional.of(previousStepAction); + public Builder backButton(@NonNull String text) { + this.backButtonText = Optional.of(text); return this; } - public Builder skipAction(Consumer skipAction) { - this.skipAction = Optional.of(skipAction); + public Builder navigation(@NonNull NavigationPredicate navigationPredicate) { + this.navigationPredicate = Optional.of(navigationPredicate); return this; } - public Builder clickOnNodeAction(Consumer clickOnNodeAction) { - this.clickOnNodeAction = Optional.of(clickOnNodeAction); + public Builder position(@NonNull Pos position) { + this.position = position; return this; } - public Builder position(Pos position) { - this.position = position; + public Builder preferredWidth(double width) { + this.preferredWidth = Optional.of(width); + return this; + } + + public Builder preferredHeight(double height) { + this.preferredHeight = Optional.of(height); + return this; + } + + public Builder highlight(@NonNull MultiWindowHighlight highlight) { + this.highlight = Optional.of(highlight); + return this; + } + + public Builder highlight(@NonNull HighlightEffect effect) { + this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(() -> Optional.empty())::resolve, effect, Optional.empty()))); + return this; + } + + public Builder autoFallback(boolean autoFallback) { + this.autoFallback = autoFallback; + return this; + } + + public Builder activeWindow(@NonNull WindowResolver activeWindowResolver) { + this.activeWindowResolver = Optional.of(activeWindowResolver); return this; } public PanelStep build() { - return new PanelStep(title, content, resolver, actions, nextStepAction, - previousStepAction, skipAction, clickOnNodeAction, position); + if (resolver == null) { + throw new IllegalStateException("Node resolver is required for PanelStep"); + } + return new PanelStep(title, content, resolver, continueButtonText, skipButtonText, backButtonText, navigationPredicate, position, preferredWidth, preferredHeight, highlight, autoFallback, activeWindowResolver); } } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/StepType.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/StepType.java deleted file mode 100644 index 72193d13258..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/StepType.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.jabref.gui.walkthrough.declarative.step; - -/** - * Defines the different types of walkthrough steps. - */ -public enum StepType { - /** - * Full-screen page that covers the entire application. - */ - FULL_SCREEN, - - /** - * Panel that appears on the left side of the interface. - */ - LEFT_PANEL, - - /** - * Panel that appears on the right side of the interface. - */ - RIGHT_PANEL, - - /** - * Panel that appears at the top of the interface. - */ - TOP_PANEL, - - /** - * Panel that appears at the bottom of the interface. - */ - BOTTOM_PANEL -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipPosition.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipPosition.java new file mode 100644 index 00000000000..eb59728cefd --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipPosition.java @@ -0,0 +1,9 @@ +package org.jabref.gui.walkthrough.declarative.step; + +public enum TooltipPosition { + AUTO, + TOP, + BOTTOM, + LEFT, + RIGHT +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java new file mode 100644 index 00000000000..ee09c85769a --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -0,0 +1,133 @@ +package org.jabref.gui.walkthrough.declarative.step; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.walkthrough.declarative.NavigationPredicate; +import org.jabref.gui.walkthrough.declarative.NodeResolver; +import org.jabref.gui.walkthrough.declarative.WindowResolver; +import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; +import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; +import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; +import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; +import org.jabref.logic.l10n.Localization; + +import org.jspecify.annotations.NonNull; + +public record TooltipStep( + String title, + List content, + NodeResolver resolver, + Optional continueButtonText, + Optional skipButtonText, + Optional backButtonText, + Optional navigationPredicate, + TooltipPosition position, + Optional preferredWidth, + Optional preferredHeight, + Optional highlight, + boolean autoFallback, + Optional activeWindowResolver +) implements WalkthroughNode { + public static Builder builder(String key, Object... params) { + return new Builder(Localization.lang(key, params)); + } + + public static class Builder { + private final String title; + private List content = List.of(); + private NodeResolver resolver; + private Optional continueButtonText = Optional.empty(); + private Optional skipButtonText = Optional.empty(); + private Optional backButtonText = Optional.empty(); + private Optional navigationPredicate = Optional.empty(); + private TooltipPosition position = TooltipPosition.AUTO; + private Optional preferredWidth = Optional.empty(); + private Optional preferredHeight = Optional.empty(); + private Optional highlight = Optional.empty(); + private boolean autoFallback = true; + private Optional activeWindowResolver = Optional.empty(); + + private Builder(String title) { + this.title = title; + } + + public Builder content(WalkthroughRichTextBlock... blocks) { + this.content = List.of(blocks); + return this; + } + + public Builder content(List content) { + this.content = content; + return this; + } + + public Builder resolver(@NonNull NodeResolver resolver) { + this.resolver = resolver; + return this; + } + + public Builder continueButton(@NonNull String text) { + this.continueButtonText = Optional.of(text); + return this; + } + + public Builder skipButton(@NonNull String text) { + this.skipButtonText = Optional.of(text); + return this; + } + + public Builder backButton(@NonNull String text) { + this.backButtonText = Optional.of(text); + return this; + } + + public Builder navigation(@NonNull NavigationPredicate navigationPredicate) { + this.navigationPredicate = Optional.of(navigationPredicate); + return this; + } + + public Builder position(@NonNull TooltipPosition position) { + this.position = position; + return this; + } + + public Builder preferredWidth(double width) { + this.preferredWidth = Optional.of(width); + return this; + } + + public Builder preferredHeight(double height) { + this.preferredHeight = Optional.of(height); + return this; + } + + public Builder highlight(@NonNull MultiWindowHighlight highlight) { + this.highlight = Optional.of(highlight); + return this; + } + + public Builder highlight(@NonNull HighlightEffect effect) { + this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(() -> Optional.empty())::resolve, effect, Optional.empty()))); + return this; + } + + public Builder autoFallback(boolean autoFallback) { + this.autoFallback = autoFallback; + return this; + } + + public Builder activeWindow(@NonNull WindowResolver activeWindowResolver) { + this.activeWindowResolver = Optional.of(activeWindowResolver); + return this; + } + + public TooltipStep build() { + if (resolver == null) { + throw new IllegalStateException("Node resolver is required for TooltipStep"); + } + return new TooltipStep(title, content, resolver, continueButtonText, skipButtonText, backButtonText, navigationPredicate, position, preferredWidth, preferredHeight, highlight, autoFallback, activeWindowResolver); + } + } +} + diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java index 673b8051902..c5674d1641c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java @@ -2,36 +2,41 @@ import java.util.List; import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import javafx.scene.Node; -import javafx.scene.Scene; - -import org.jabref.gui.walkthrough.Walkthrough; -import org.jabref.gui.walkthrough.declarative.WalkthroughActionsConfig; +import org.jabref.gui.walkthrough.declarative.NavigationPredicate; +import org.jabref.gui.walkthrough.declarative.NodeResolver; +import org.jabref.gui.walkthrough.declarative.WindowResolver; +import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; -public sealed interface WalkthroughNode permits PanelStep, FullScreenStep { +public sealed interface WalkthroughNode permits PanelStep, TooltipStep { String title(); List content(); - Optional>> resolver(); + // FIXME: Refactor to make this optional + NodeResolver resolver(); + + Optional continueButtonText(); + + Optional skipButtonText(); + + Optional backButtonText(); + + Optional navigationPredicate(); - Optional actions(); + Optional preferredWidth(); - Optional> nextStepAction(); + Optional preferredHeight(); - Optional> previousStepAction(); + boolean autoFallback(); - Optional> skipAction(); + Optional highlight(); - Optional> clickOnNodeAction(); + Optional activeWindowResolver(); - // Static factory methods for builders - static FullScreenStep.Builder fullScreen(String key) { - return FullScreenStep.builder(key); + static TooltipStep.Builder tooltip(String key) { + return TooltipStep.builder(key); } static PanelStep.Builder panel(String title) { diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index 5e88f68f8f6..e95053f419a 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2625,3 +2625,22 @@ journalInfo .grid-cell-b { -fx-fill-width: true; } +.walkthrough-tooltip { + -fx-background-color: -jr-white; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0, 0, 2); + -fx-background-radius: 8; + -fx-padding: 1em; + -fx-spacing: 0.75em; + -fx-border-radius: 8; +} + +.walkthrough-tooltip-title { + -fx-text-fill: -jr-theme; + -fx-font-size: 1.4em; + -fx-font-weight: bold; +} + +.walkthrough-tooltip-content { + -fx-spacing: 0.5em; +} + diff --git a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java index 4cf676e514e..78a8c9bc440 100644 --- a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java @@ -119,17 +119,17 @@ import org.slf4j.LoggerFactory; /** - * The {@code JabRefPreferences} class provides the preferences and their defaults using the JDK {@code java.util.prefs} - * class. + * The {@code JabRefPreferences} class provides the preferences and their defaults using + * the JDK {@code java.util.prefs} class. *

- * Internally it defines symbols used to pick a value from the {@code java.util.prefs} interface and keeps a hashmap - * with all the default values. + * Internally it defines symbols used to pick a value from the {@code java.util.prefs} + * interface and keeps a hashmap with all the default values. *

- * There are still some similar preferences classes ({@link OpenOfficePreferences} and {@link SharedDatabasePreferences}) which also use - * the {@code java.util.prefs} API. + * There are still some similar preferences classes ({@link OpenOfficePreferences} and + * {@link SharedDatabasePreferences}) which also use the {@code java.util.prefs} API. *

- * contents of the defaults HashMap that are defined in this class. - * There are more default parameters in this map which belong to separate preference classes. + * contents of the defaults HashMap that are defined in this class. There are more + * default parameters in this map which belong to separate preference classes. */ @Singleton public class JabRefCliPreferences implements CliPreferences { @@ -267,7 +267,15 @@ public class JabRefCliPreferences implements CliPreferences { public static final String SHOW_SCITE_TAB = "showSciteTab"; /** - * The OpenOffice/LibreOffice connection preferences are: OO_PATH main directory for OO/LO installation, used to detect location on Win/macOS when using manual connect OO_EXECUTABLE_PATH path to soffice-file OO_JARS_PATH directory that contains juh.jar, jurt.jar, ridl.jar, unoil.jar OO_SYNC_WHEN_CITING true if the reference list is updated when adding a new citation OO_SHOW_PANEL true if the OO panel is shown on startup OO_USE_ALL_OPEN_DATABASES true if all databases should be used when citing OO_BIBLIOGRAPHY_STYLE_FILE path to the used style file OO_EXTERNAL_STYLE_FILES list with paths to external style files STYLES_*_* size and position of "Select style" dialog + * The OpenOffice/LibreOffice connection preferences are: OO_PATH main directory for + * OO/LO installation, used to detect location on Win/macOS when using manual + * connect OO_EXECUTABLE_PATH path to soffice-file OO_JARS_PATH directory that + * contains juh.jar, jurt.jar, ridl.jar, unoil.jar OO_SYNC_WHEN_CITING true if the + * reference list is updated when adding a new citation OO_SHOW_PANEL true if the OO + * panel is shown on startup OO_USE_ALL_OPEN_DATABASES true if all databases should + * be used when citing OO_BIBLIOGRAPHY_STYLE_FILE path to the used style file + * OO_EXTERNAL_STYLE_FILES list with paths to external style files STYLES_*_* size + * and position of "Select style" dialog */ public static final String OO_EXECUTABLE_PATH = "ooExecutablePath"; public static final String OO_SYNC_WHEN_CITING = "syncOOWhenCiting"; @@ -391,7 +399,7 @@ public class JabRefCliPreferences implements CliPreferences { private static final String OPEN_FILE_EXPLORER_IN_FILE_DIRECTORY = "openFileExplorerInFileDirectory"; private static final String OPEN_FILE_EXPLORER_IN_LAST_USED_DIRECTORY = "openFileExplorerInLastUsedDirectory"; - private static final String WALKTHROUGH_COMPLETED = "walkthroughCompleted"; + private static final String MAIN_FILE_DIRECTORY_WALKTHROUGH_COMPLETED = "mainFileDirectoryWalkthroughCompleted"; private static final Logger LOGGER = LoggerFactory.getLogger(JabRefCliPreferences.class); private static final Preferences PREFS_NODE = Preferences.userRoot().node("/org/jabref"); @@ -439,7 +447,8 @@ public class JabRefCliPreferences implements CliPreferences { private WalkthroughPreferences walkthroughPreferences; /** - * @implNote The constructor is made protected to enforce this as a singleton class: + * @implNote The constructor is made protected to enforce this as a singleton + * class: */ protected JabRefCliPreferences() { try { @@ -705,7 +714,7 @@ protected JabRefCliPreferences() { // endregion // WalkThrough - defaults.put(WALKTHROUGH_COMPLETED, Boolean.FALSE); + defaults.put(MAIN_FILE_DIRECTORY_WALKTHROUGH_COMPLETED, Boolean.FALSE); } public void setLanguageDependentDefaultValues() { @@ -720,9 +729,10 @@ public void setLanguageDependentDefaultValues() { } /** - * @deprecated Never ever add a call to this method. There should be only one caller. - * All other usages should get the preferences passed (or injected). - * The JabRef team leaves the {@code @deprecated} annotation to have IntelliJ listing this method with a strike-through. + * @deprecated Never ever add a call to this method. There should be only one + * caller. All other usages should get the preferences passed (or injected). The + * JabRef team leaves the {@code @deprecated} annotation to have IntelliJ listing + * this method with a strike-through. */ @Deprecated public static JabRefCliPreferences getInstance() { @@ -838,7 +848,9 @@ protected void remove(String key) { } /** - * Puts a list of strings into the Preferences, by linking its elements with a STRINGLIST_DELIMITER into a single string. Escape characters make the process transparent even if strings contains a STRINGLIST_DELIMITER. + * Puts a list of strings into the Preferences, by linking its elements with a + * STRINGLIST_DELIMITER into a single string. Escape characters make the process + * transparent even if strings contains a STRINGLIST_DELIMITER. */ public void putStringList(String key, List value) { if (value == null) { @@ -867,7 +879,8 @@ private Path getPath(String key, Path defaultValue) { /** * Clear all preferences. * - * @throws BackingStoreException if JabRef is unable to write to the registry/the preferences storage + * @throws BackingStoreException if JabRef is unable to write to the registry/the + * preferences storage */ @Override public void clear() throws BackingStoreException { @@ -975,7 +988,8 @@ protected List getSeries(String key) { } /** - * Removes all entries keyed by prefix+number, where number is equal to or higher than the given number. + * Removes all entries keyed by prefix+number, where number is equal to or higher + * than the given number. * * @param number or higher. */ @@ -1010,7 +1024,8 @@ public void exportPreferences(Path path) throws JabRefException { * Imports Preferences from an XML file. * * @param file Path of file to import from - * @throws JabRefException thrown if importing the preferences failed due to an InvalidPreferencesFormatException or an IOException + * @throws JabRefException thrown if importing the preferences failed due to an + * InvalidPreferencesFormatException or an IOException */ @Override public void importPreferences(Path file) throws JabRefException { @@ -1685,10 +1700,8 @@ public SelfContainedSaveConfiguration getSelfContainedExportConfiguration() { LOGGER.warn("Table sort order requested, but JabRef is in CLI mode. Falling back to defeault save order"); yield SaveOrder.getDefaultSaveOrder(); } - case SPECIFIED -> - SelfContainedSaveOrder.of(exportSaveOrder); - case ORIGINAL -> - SaveOrder.getDefaultSaveOrder(); + case SPECIFIED -> SelfContainedSaveOrder.of(exportSaveOrder); + case ORIGINAL -> SaveOrder.getDefaultSaveOrder(); }; return new SelfContainedSaveConfiguration( @@ -2303,11 +2316,8 @@ public WalkthroughPreferences getWalkthroughPreferences() { return walkthroughPreferences; } - walkthroughPreferences = new WalkthroughPreferences(getBoolean(WALKTHROUGH_COMPLETED)); - - walkthroughPreferences.completedProperty().addListener((_, _, newValue) -> - putBoolean(WALKTHROUGH_COMPLETED, newValue)); - + walkthroughPreferences = new WalkthroughPreferences(getBoolean(MAIN_FILE_DIRECTORY_WALKTHROUGH_COMPLETED)); + EasyBind.listen(walkthroughPreferences.mainFileDirectoryCompletedProperty(), (_, _, newValue) -> putBoolean(MAIN_FILE_DIRECTORY_WALKTHROUGH_COMPLETED, newValue)); return walkthroughPreferences; } } diff --git a/jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java index 442f079e7b0..57eba1c4c57 100644 --- a/jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/preferences/WalkthroughPreferences.java @@ -7,21 +7,21 @@ * Preferences related to the application walkthrough functionality. */ public class WalkthroughPreferences { - private final BooleanProperty completed; + private final BooleanProperty mainFileDirectoryCompleted; public WalkthroughPreferences(boolean completed) { - this.completed = new SimpleBooleanProperty(completed); + this.mainFileDirectoryCompleted = new SimpleBooleanProperty(completed); } - public BooleanProperty completedProperty() { - return completed; + public BooleanProperty mainFileDirectoryCompletedProperty() { + return mainFileDirectoryCompleted; } - public boolean isCompleted() { - return completed.get(); + public boolean getMainFileDirectoryCompleted() { + return mainFileDirectoryCompleted.get(); } - public void setCompleted(boolean completed) { - this.completed.set(completed); + public void setMainFileDirectoryCompleted(boolean mainFileDirectoryCompleted) { + this.mainFileDirectoryCompleted.set(mainFileDirectoryCompleted); } } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index a75cfe85827..1d20e5fdc6b 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2932,30 +2932,9 @@ You\ must\ specify\ an\ identifier.=You must specify an identifier. You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) citations. # Walkthrough -This\ quick\ walkthrough\ will\ introduce\ you\ to\ some\ key\ features.=This quick walkthrough will introduce you to some key features. -You\ can\ always\ access\ this\ walkthrough\ from\ the\ Help\ menu.=You can always access this walkthrough from the Help menu. -Creating\ a\ new\ entry=Creating a new entry -Click\ the\ highlighted\ button\ to\ start\ creating\ a\ new\ bibliographic\ entry.=Click the highlighted button to start creating a new bibliographic entry. -JabRef\ supports\ various\ entry\ types\ like\ articles,\ books,\ and\ more.=JabRef supports various entry types like articles, books, and more. -In\ the\ entry\ editor\ that\ opens\ after\ clicking\ the\ button,\ choose\ "Article"\ as\ the\ entry\ type.=In the entry editor that opens after clicking the button, choose "Article" as the entry type. -Fill\ in\ the\ entry\ details=Fill in the entry details -In\ the\ title\ field,\ enter\ "JabRef\:\ BibTeX-based\ literature\ management\ software".=In the title field, enter "JabRef: BibTeX-based literature management software". -In\ the\ journal\ field,\ enter\ "TUGboat".="In the journal field, enter "TUGboat". -You\ can\ fill\ in\ more\ details\ later.\ JabRef\ supports\ many\ entry\ types\ and\ fields.=You can fill in more details later. JabRef supports many entry types and fields. Saving\ Your\ Work=Saving Your Work -Don't\ forget\ to\ save\ your\ library.\ Click\ the\ save\ button.=Don't forget to save your library. Click the save button. -Regularly\ saving\ prevents\ data\ loss.=Regularly saving prevents data loss. -Configure\ paper\ directory=Configure paper directory -Set\ up\ your\ main\ file\ directory\ where\ JabRef\ will\ look\ for\ and\ store\ your\ PDF\ files\ and\ other\ associated\ documents.=Set up your main file directory where JabRef will look for and store your PDF files and other associated documents. -This\ directory\ helps\ JabRef\ organize\ your\ paper\ files.\ You\ can\ change\ this\ later\ in\ Preferences.=This directory helps JabRef organize your paper files. You can change this later in Preferences. Skip\ for\ Now=Skip for Now Browse...=Browse... -Current\ paper\ directory\:\ %0=Current paper directory: %0 -No\ directory\ currently\ set.=No directory currently set. -Walkthrough\ complete=Walkthrough complete -You've\ completed\ the\ basic\ feature\ tour.=You've completed the basic feature tour. -Explore\ more\ features\ like\ groups,\ fetchers,\ and\ customization\ options.=Explore more features like groups, fetchers, and customization options. -Check\ our\ documentation\ for\ detailed\ guides.=Check our documentation for detailed guides. Skip=Skip Finish=Finish Skip\ to\ finish=Skip to Finish From 9aab16784720a70ca8e91e991a587645bb70b56a Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 13 Jun 2025 15:25:02 -0400 Subject: [PATCH 17/50] Implement fix according to subhramit --- .../gui/walkthrough/HighlightManager.java | 8 ++--- .../MultiWindowWalkthroughOverlay.java | 29 ++++++------------- .../gui/walkthrough/SingleWindowOverlay.java | 20 ++++++------- .../components/WalkthroughEffect.java | 2 +- .../declarative/NavigationPredicate.java | 2 -- .../declarative/step/PanelStep.java | 17 +++++++++-- .../declarative/step/TooltipStep.java | 14 ++++++++- 7 files changed, 50 insertions(+), 42 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java index 68d82871c5e..05a255e863c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java @@ -40,12 +40,12 @@ public void applyHighlight(@NonNull Scene mainScene, detachAll(); highlightConfig.ifPresentOrElse( - c -> { - if (c.windowEffects().isEmpty() && c.fallbackEffect().isPresent()) { - applyEffect(mainScene.getWindow(), c.fallbackEffect().get(), fallbackTarget); + config -> { + if (config.windowEffects().isEmpty() && config.fallbackEffect().isPresent()) { + applyEffect(mainScene.getWindow(), config.fallbackEffect().get(), fallbackTarget); return; } - c.windowEffects().forEach(effect -> { + config.windowEffects().forEach(effect -> { Window window = effect.windowResolver().resolve().orElse(mainScene.getWindow()); Optional targetNode = effect.targetNodeResolver() .map(resolver -> diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java index 8e36b8787e7..8280e03ff16 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java @@ -73,14 +73,12 @@ private void tryRevertToPreviousResolvableStep() { int currentIndex = walkthrough.currentStepProperty().get(); for (int i = currentIndex - 1; i >= 0; i--) { WalkthroughNode previousStep = getStepAtIndex(i); - if (previousStep != null) { - Window activeWindow = previousStep.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); - Scene scene = activeWindow.getScene(); - if (scene != null && previousStep.resolver().resolve(scene).isPresent()) { - LOGGER.info("Reverting to step {} from step {}", i, currentIndex); - walkthrough.goToStep(i); - return; - } + Window activeWindow = previousStep.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); + Scene scene = activeWindow.getScene(); + if (scene != null && previousStep.resolver().resolve(scene).isPresent()) { + LOGGER.info("Reverting to step {} from step {}", i, currentIndex); + walkthrough.goToStep(i); + return; } } @@ -120,20 +118,11 @@ private void stopNodePolling() { } } - private WalkthroughNode getStepAtIndex(int index) { - try { - return walkthrough.getSteps().get(index); - } catch (IndexOutOfBoundsException e) { - return null; - } + private @NonNull WalkthroughNode getStepAtIndex(int index) { + return walkthrough.getSteps().get(index); } private SingleWindowOverlay getOrCreateOverlay(Window window) { - return overlays.computeIfAbsent(window, w -> { - if (w instanceof Stage stage) { - return new SingleWindowOverlay(stage); - } - throw new IllegalArgumentException("Only Stage windows are supported for overlays"); - }); + return overlays.computeIfAbsent(window, SingleWindowOverlay::new); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java index 65d82d8b0e7..fa0ed9e1398 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java @@ -16,7 +16,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.shape.Polygon; import javafx.scene.shape.Rectangle; -import javafx.stage.Stage; +import javafx.stage.Window; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; @@ -33,15 +33,15 @@ public class SingleWindowOverlay { private static final Logger LOGGER = LoggerFactory.getLogger(SingleWindowOverlay.class); private static final double MARGIN = 10.0; private static final double ARROW_OVERLAP = 3.0; - private final Stage parentStage; + private final Window window; private final GridPane overlayPane; private final Pane originalRoot; private final StackPane stackPane; private final WalkthroughRenderer renderer; private final List cleanUpTasks = new ArrayList<>(); - public SingleWindowOverlay(Stage stage) { - this.parentStage = stage; + public SingleWindowOverlay(Window window) { + this.window = window; this.renderer = new WalkthroughRenderer(); overlayPane = new GridPane(); @@ -50,7 +50,7 @@ public SingleWindowOverlay(Stage stage) { overlayPane.setMaxWidth(Double.MAX_VALUE); overlayPane.setMaxHeight(Double.MAX_VALUE); - Scene scene = stage.getScene(); + Scene scene = window.getScene(); assert scene != null; originalRoot = (Pane) scene.getRoot(); @@ -89,7 +89,7 @@ public void hide() { public void detach() { hide(); - Scene scene = parentStage.getScene(); + Scene scene = window.getScene(); if (scene != null && originalRoot != null) { stackPane.getChildren().remove(originalRoot); scene.setRoot(originalRoot); @@ -194,16 +194,14 @@ private void setupClipping(Node node) { private Polygon createArrow() { Polygon arrow = new Polygon(); - arrow.getPoints().addAll(new Double[] { - 0.0, 0.0, + arrow.getPoints().addAll(0.0, 0.0, 12.0, 15.0, - -12.0, 15.0 - }); + -12.0, 15.0); return arrow; } private void positionTooltipWithArrow(StackPane container, Node tooltip, Node arrow, Node target, TooltipStep step) { - Scene scene = parentStage.getScene(); + Scene scene = window.getScene(); if (scene == null) { return; } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java index 43c3ddffacd..97ba9ecf3eb 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java @@ -20,7 +20,7 @@ */ public abstract class WalkthroughEffect { protected final Pane pane; - protected final List cleanupTasks = new ArrayList<>(); + protected final List cleanupTasks = new ArrayList<>(); // needs to be mutable protected final InvalidationListener updateListener = _ -> updateLayout(); protected WalkthroughEffect(@NonNull Pane pane) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java index 6029638502e..32064a3ea0c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java @@ -46,8 +46,6 @@ static NavigationPredicate onClick() { return () -> targetNode.setOnMouseClicked(onClicked); } - System.out.println("TargetNode is a MenuItem: " + targetNode); - System.out.println("MenuItem found: " + item.get().getText()); EventHandler onAction = item.get().getOnAction(); item.get().setOnAction(ConcurrentNavigationRunner.decorate(onAction, onNavigate)); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index 1e667f87373..c6e29eea1fa 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -28,8 +28,7 @@ public record PanelStep( Optional preferredHeight, Optional highlight, boolean autoFallback, - Optional activeWindowResolver -) implements WalkthroughNode { + Optional activeWindowResolver) implements WalkthroughNode { public static Builder builder(String title) { return new Builder(title); } @@ -127,7 +126,19 @@ public PanelStep build() { if (resolver == null) { throw new IllegalStateException("Node resolver is required for PanelStep"); } - return new PanelStep(title, content, resolver, continueButtonText, skipButtonText, backButtonText, navigationPredicate, position, preferredWidth, preferredHeight, highlight, autoFallback, activeWindowResolver); + return new PanelStep(title, + content, + resolver, + continueButtonText, + skipButtonText, + backButtonText, + navigationPredicate, + position, + preferredWidth, + preferredHeight, + highlight, + autoFallback, + activeWindowResolver); } } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index ee09c85769a..be686f086bb 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -126,7 +126,19 @@ public TooltipStep build() { if (resolver == null) { throw new IllegalStateException("Node resolver is required for TooltipStep"); } - return new TooltipStep(title, content, resolver, continueButtonText, skipButtonText, backButtonText, navigationPredicate, position, preferredWidth, preferredHeight, highlight, autoFallback, activeWindowResolver); + return new TooltipStep(title, + content, + resolver, + continueButtonText, + skipButtonText, + backButtonText, + navigationPredicate, + position, + preferredWidth, + preferredHeight, + highlight, + autoFallback, + activeWindowResolver); } } } From 405cf0ece40f3cc705ba79b8b330ce84510b3499 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 13 Jun 2025 15:25:56 -0400 Subject: [PATCH 18/50] Resolve warnings from Intellij --- .../jabref/gui/walkthrough/declarative/NavigationPredicate.java | 2 +- .../org/jabref/gui/walkthrough/declarative/step/PanelStep.java | 2 +- .../jabref/gui/walkthrough/declarative/step/TooltipStep.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java index 32064a3ea0c..a9ef63e6a21 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java @@ -91,7 +91,7 @@ static NavigationPredicate manual() { } static NavigationPredicate auto() { - return (targetNode, onNavigate) -> { + return (_, onNavigate) -> { onNavigate.run(); return () -> { }; diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index c6e29eea1fa..07aac4fd501 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -108,7 +108,7 @@ public Builder highlight(@NonNull MultiWindowHighlight highlight) { } public Builder highlight(@NonNull HighlightEffect effect) { - this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(() -> Optional.empty())::resolve, effect, Optional.empty()))); + this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(Optional::empty), effect, Optional.empty()))); return this; } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index be686f086bb..201b471974a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -108,7 +108,7 @@ public Builder highlight(@NonNull MultiWindowHighlight highlight) { } public Builder highlight(@NonNull HighlightEffect effect) { - this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(() -> Optional.empty())::resolve, effect, Optional.empty()))); + this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(Optional::empty), effect, Optional.empty()))); return this; } From ec2a0369cc0d2b719ccd4fed0c235bd51b600a00 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Mon, 16 Jun 2025 08:27:34 -0400 Subject: [PATCH 19/50] Remove whitespace change to JabRefGUI --- jabgui/src/main/java/org/jabref/gui/JabRefGUI.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index b0e4da53b6d..21aed8e8bfa 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -75,6 +75,7 @@ public class JabRefGUI extends Application { private static ClipBoardManager clipBoardManager; private static DialogService dialogService; private static JabRefFrame mainFrame; + private static RemoteListenerServerManager remoteListenerServerManager; private Stage mainStage; @@ -190,7 +191,7 @@ public void initialize() { private void setupProxy() { if (!preferences.getProxyPreferences().shouldUseProxy() - || !preferences.getProxyPreferences().shouldUseAuthentication()) { + || !preferences.getProxyPreferences().shouldUseAuthentication()) { return; } From eb8d4fbf9d0a5c9206d929ac046460327cd9b095 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Mon, 16 Jun 2025 08:41:07 -0400 Subject: [PATCH 20/50] Remove whitespace changes to JabRefFrame --- .../src/main/java/org/jabref/gui/frame/JabRefFrame.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java index 04d30daf4a0..52ba69e2ca7 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -521,9 +521,9 @@ public void showWelcomeTab() { } /** - * Opens a new tab with existing data. Asynchronous loading is done at - * {@link LibraryTab#createLibraryTab}. Similar method: - * {@link OpenDatabaseAction#openTheFile(Path)} + * Opens a new tab with existing data. + * Asynchronous loading is done at {@link LibraryTab#createLibraryTab}. + * Similar method: {@link OpenDatabaseAction#openTheFile(Path)} */ public void addTab(@NonNull BibDatabaseContext databaseContext, boolean raisePanel) { Objects.requireNonNull(databaseContext); @@ -681,8 +681,7 @@ public CloseDatabaseAction(LibraryTabContainer tabContainer, LibraryTab libraryT } /** - * Using this constructor will result in executing the command on the currently - * open library tab + * Using this constructor will result in executing the command on the currently open library tab */ public CloseDatabaseAction(LibraryTabContainer tabContainer, StateManager stateManager) { this(tabContainer, null, stateManager); From 547c819e4e0ea94d78f52bc57fc480985a78fbb0 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Mon, 16 Jun 2025 08:41:49 -0400 Subject: [PATCH 21/50] Remove whitespace change to PreferencesDialogView --- .../java/org/jabref/gui/preferences/PreferencesDialogView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java index 4403cbac153..dfebe248e69 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java @@ -1,4 +1,4 @@ - package org.jabref.gui.preferences; +package org.jabref.gui.preferences; import java.util.Locale; import java.util.Optional; From 770258b411bc9f695b9b65ce0d0a76274dc99b2f Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Mon, 16 Jun 2025 08:47:26 -0400 Subject: [PATCH 22/50] Rename the Manager to WalkthroughHighlighter --- .../{HighlightManager.java => WalkthroughHighlighter.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename jabgui/src/main/java/org/jabref/gui/walkthrough/{HighlightManager.java => WalkthroughHighlighter.java} (99%) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java similarity index 99% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java index 05a255e863c..11740e2a8dc 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java @@ -20,7 +20,7 @@ /** * Manages highlight effects across multiple windows for walkthrough steps. */ -public class HighlightManager { +public class WalkthroughHighlighter { private final Map backdropHighlights = new HashMap<>(); private final Map pulseIndicators = new HashMap<>(); private final Map fullScreenDarkens = new HashMap<>(); From d32c14b21a63dacba1089d617608234d85d97c23 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Mon, 16 Jun 2025 08:59:46 -0400 Subject: [PATCH 23/50] Use PopOver class, rather than custom arrow position algorithm --- .../gui/walkthrough/SingleWindowOverlay.java | 308 ------------------ .../SingleWindowWalkthroughOverlay.java | 260 +++++++++++++++ .../jabref/gui/walkthrough/Walkthrough.java | 8 +- .../gui/walkthrough/WalkthroughAction.java | 110 ++++--- ...ghOverlay.java => WalkthroughOverlay.java} | 30 +- .../gui/walkthrough/WalkthroughRenderer.java | 26 +- .../main/resources/org/jabref/gui/Base.css | 59 ++-- 7 files changed, 393 insertions(+), 408 deletions(-) delete mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java rename jabgui/src/main/java/org/jabref/gui/walkthrough/{MultiWindowWalkthroughOverlay.java => WalkthroughOverlay.java} (77%) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java deleted file mode 100644 index fa0ed9e1398..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowOverlay.java +++ /dev/null @@ -1,308 +0,0 @@ -package org.jabref.gui.walkthrough; - -import java.util.ArrayList; -import java.util.List; - -import javafx.beans.value.ChangeListener; -import javafx.geometry.Bounds; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.RowConstraints; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.Polygon; -import javafx.scene.shape.Rectangle; -import javafx.stage.Window; - -import org.jabref.gui.walkthrough.declarative.step.PanelStep; -import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; -import org.jabref.gui.walkthrough.declarative.step.TooltipStep; -import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Manages the overlay for displaying walkthrough steps in a single window. - */ -public class SingleWindowOverlay { - private static final Logger LOGGER = LoggerFactory.getLogger(SingleWindowOverlay.class); - private static final double MARGIN = 10.0; - private static final double ARROW_OVERLAP = 3.0; - private final Window window; - private final GridPane overlayPane; - private final Pane originalRoot; - private final StackPane stackPane; - private final WalkthroughRenderer renderer; - private final List cleanUpTasks = new ArrayList<>(); - - public SingleWindowOverlay(Window window) { - this.window = window; - this.renderer = new WalkthroughRenderer(); - - overlayPane = new GridPane(); - overlayPane.setStyle("-fx-background-color: transparent;"); - overlayPane.setPickOnBounds(false); - overlayPane.setMaxWidth(Double.MAX_VALUE); - overlayPane.setMaxHeight(Double.MAX_VALUE); - - Scene scene = window.getScene(); - assert scene != null; - - originalRoot = (Pane) scene.getRoot(); - stackPane = new StackPane(); - - stackPane.getChildren().add(originalRoot); - stackPane.getChildren().add(overlayPane); - - scene.setRoot(stackPane); - } - - /** - * Displays a walkthrough step with the specified target node. - */ - public void displayStep(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { - overlayPane.getChildren().clear(); - cleanUpTasks.forEach(Runnable::run); - cleanUpTasks.clear(); - - displayStepContent(step, targetNode, walkthrough); - overlayPane.toFront(); - } - - /** - * Hide the overlay and clean up any resources. - */ - public void hide() { - overlayPane.getChildren().clear(); - cleanUpTasks.forEach(Runnable::run); - cleanUpTasks.clear(); - } - - /** - * Detaches the overlay and restores the original scene root. - */ - public void detach() { - hide(); - - Scene scene = window.getScene(); - if (scene != null && originalRoot != null) { - stackPane.getChildren().remove(originalRoot); - scene.setRoot(originalRoot); - LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); - } - } - - private void displayStepContent(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { - Node stepContent; - if (step instanceof TooltipStep tooltipStep) { - stepContent = renderer.render(tooltipStep, walkthrough); - displayTooltipContent(stepContent, targetNode, tooltipStep); - } else if (step instanceof PanelStep panelStep) { - stepContent = renderer.render(panelStep, walkthrough); - displayPanelContent(stepContent, panelStep.position()); - } - - step.navigationPredicate().ifPresent(predicate -> cleanUpTasks.add(predicate.attachListeners(targetNode, walkthrough::nextStep))); - } - - private void displayTooltipContent(Node content, Node targetNode, TooltipStep step) { - StackPane tooltipContainer = new StackPane(); - tooltipContainer.setStyle("-fx-background-color: transparent;"); - - Polygon arrow = createArrow(); - arrow.getStyleClass().add("walkthrough-tooltip-arrow"); - arrow.setStyle("-fx-fill: white; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 4, 0, 0, 2);"); - - content.setStyle(content.getStyle() + "; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 4, 0, 0, 2);"); - - tooltipContainer.getChildren().addAll(content, arrow); - - overlayPane.getChildren().clear(); - overlayPane.setAlignment(Pos.TOP_LEFT); - overlayPane.getChildren().add(tooltipContainer); - - setupClipping(tooltipContainer); - - ChangeListener layoutListener = (_, _, newBounds) -> { - if (newBounds.getWidth() > 0 && newBounds.getHeight() > 0) { - positionTooltipWithArrow(tooltipContainer, content, arrow, targetNode, step); - } - }; - content.boundsInLocalProperty().addListener(layoutListener); - - if (content.getBoundsInLocal().getWidth() > 0) { - positionTooltipWithArrow(tooltipContainer, content, arrow, targetNode, step); - } - - cleanUpTasks.add(() -> content.boundsInLocalProperty().removeListener(layoutListener)); - } - - private void displayPanelContent(Node content, Pos position) { - overlayPane.getChildren().clear(); - overlayPane.getChildren().add(content); - - setupClipping(content); - - overlayPane.getRowConstraints().clear(); - overlayPane.getColumnConstraints().clear(); - overlayPane.setAlignment(position); - - GridPane.setHgrow(content, Priority.NEVER); - GridPane.setVgrow(content, Priority.NEVER); - GridPane.setFillWidth(content, false); - GridPane.setFillHeight(content, false); - - RowConstraints rowConstraints = new RowConstraints(); - ColumnConstraints columnConstraints = new ColumnConstraints(); - - switch (position) { - case CENTER_LEFT: - case CENTER_RIGHT: - rowConstraints.setVgrow(Priority.ALWAYS); - columnConstraints.setHgrow(Priority.NEVER); - GridPane.setFillHeight(content, true); - break; - case TOP_CENTER: - case BOTTOM_CENTER: - columnConstraints.setHgrow(Priority.ALWAYS); - rowConstraints.setVgrow(Priority.NEVER); - GridPane.setFillWidth(content, true); - break; - default: - LOGGER.warn("Unsupported position for panel step: {}", position); - break; - } - - overlayPane.getRowConstraints().add(rowConstraints); - overlayPane.getColumnConstraints().add(columnConstraints); - } - - private void setupClipping(Node node) { - ChangeListener listener = (_, _, bounds) -> { - Rectangle clip = new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); - overlayPane.setClip(clip); - }; - node.boundsInParentProperty().addListener(listener); - cleanUpTasks.add(() -> node.boundsInParentProperty().removeListener(listener)); - cleanUpTasks.add(() -> overlayPane.setClip(null)); - } - - private Polygon createArrow() { - Polygon arrow = new Polygon(); - arrow.getPoints().addAll(0.0, 0.0, - 12.0, 15.0, - -12.0, 15.0); - return arrow; - } - - private void positionTooltipWithArrow(StackPane container, Node tooltip, Node arrow, Node target, TooltipStep step) { - Scene scene = window.getScene(); - if (scene == null) { - return; - } - - Bounds targetBounds = target.localToScene(target.getBoundsInLocal()); - if (targetBounds == null) { - LOGGER.warn("Could not determine bounds for target node."); - return; - } - - Bounds tooltipBounds = tooltip.getBoundsInLocal(); - double tooltipWidth = tooltipBounds.getWidth(); - double tooltipHeight = tooltipBounds.getHeight(); - - TooltipPosition finalPosition = determinePosition(step.position(), targetBounds, tooltipWidth, tooltipHeight, scene); - - double tooltipX = switch (finalPosition) { - case TOP, BOTTOM -> - targetBounds.getMinX() + (targetBounds.getWidth() - tooltipWidth) / 2; - case LEFT -> targetBounds.getMinX() - tooltipWidth - MARGIN; - case RIGHT, AUTO -> targetBounds.getMaxX() + MARGIN; - }; - - double tooltipY = switch (finalPosition) { - case TOP -> targetBounds.getMinY() - tooltipHeight - MARGIN; - case BOTTOM -> targetBounds.getMaxY() + MARGIN; - case LEFT, RIGHT, AUTO -> - targetBounds.getMinY() + (targetBounds.getHeight() - tooltipHeight) / 2; - }; - - if (finalPosition == TooltipPosition.TOP || finalPosition == TooltipPosition.BOTTOM) { - tooltipX = clamp(tooltipX, MARGIN, scene.getWidth() - tooltipWidth - MARGIN); - } else { - tooltipY = clamp(tooltipY, MARGIN, scene.getHeight() - tooltipHeight - MARGIN); - } - - container.setTranslateX(tooltipX); - container.setTranslateY(tooltipY); - - double targetCenterX = targetBounds.getMinX() + targetBounds.getWidth() / 2; - double targetCenterY = targetBounds.getMinY() + targetBounds.getHeight() / 2; - - positionArrow(arrow, finalPosition, tooltipWidth, tooltipHeight, - targetCenterX - tooltipX, targetCenterY - tooltipY); - } - - private TooltipPosition determinePosition(TooltipPosition position, Bounds targetBounds, - double tooltipWidth, double tooltipHeight, Scene scene) { - if (position != TooltipPosition.AUTO) { - return position; - } - - if (targetBounds.getMaxY() + tooltipHeight + MARGIN < scene.getHeight()) { - return TooltipPosition.BOTTOM; - } else if (targetBounds.getMinY() - tooltipHeight - MARGIN > 0) { - return TooltipPosition.TOP; - } else if (targetBounds.getMaxX() + tooltipWidth + MARGIN < scene.getWidth()) { - return TooltipPosition.RIGHT; - } else if (targetBounds.getMinX() - tooltipWidth - MARGIN > 0) { - return TooltipPosition.LEFT; - } - return TooltipPosition.BOTTOM; - } - - private double clamp(double value, double min, double max) { - return Math.max(min, Math.min(max, value)); - } - - // FIXME: Tweak the padding/margin values to ensure the arrow is positioned correctly - private void positionArrow(Node arrow, TooltipPosition position, double tooltipWidth, - double tooltipHeight, double targetRelX, double targetRelY) { - double arrowX = 0; - double arrowY = 0; - double rotation = 0; - - switch (position) { - case TOP -> { - arrowX = clamp(targetRelX, 10, tooltipWidth - 30); - arrowY = tooltipHeight - ARROW_OVERLAP; - rotation = 180; - } - case BOTTOM -> { - arrowX = clamp(targetRelX, 10, tooltipWidth - 30); - arrowY = -15 + ARROW_OVERLAP; - rotation = 0; - } - case LEFT -> { - arrowX = tooltipWidth - ARROW_OVERLAP; - arrowY = clamp(targetRelY, 10, tooltipHeight - 30); - rotation = 90; - } - case RIGHT, AUTO -> { - arrowX = -15 + ARROW_OVERLAP; - arrowY = clamp(targetRelY, 10, tooltipHeight - 30); - rotation = -90; - } - } - - arrow.setManaged(false); - arrow.setLayoutX(arrowX); - arrow.setLayoutY(arrowY); - arrow.setRotate(rotation); - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java new file mode 100644 index 00000000000..c53e297fd26 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -0,0 +1,260 @@ +package org.jabref.gui.walkthrough; + +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.value.ChangeListener; +import javafx.geometry.Bounds; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; +import javafx.stage.Window; + +import org.jabref.gui.walkthrough.declarative.step.PanelStep; +import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; +import org.jabref.gui.walkthrough.declarative.step.TooltipStep; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; + +import org.controlsfx.control.PopOver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the overlay for displaying walkthrough steps in a single window. + */ +public class SingleWindowWalkthroughOverlay { + private static final Logger LOGGER = LoggerFactory.getLogger(SingleWindowWalkthroughOverlay.class); + + private final Window window; + private final GridPane overlayPane; + private final Pane originalRoot; + private final StackPane stackPane; + private final WalkthroughRenderer renderer; + private final List cleanUpTasks = new ArrayList<>(); + private PopOver currentPopOver; + + public SingleWindowWalkthroughOverlay(Window window) { + this.window = window; + this.renderer = new WalkthroughRenderer(); + + overlayPane = new GridPane(); + overlayPane.getStyleClass().add("walkthrough-overlay"); + overlayPane.setPickOnBounds(false); + overlayPane.setMaxWidth(Double.MAX_VALUE); + overlayPane.setMaxHeight(Double.MAX_VALUE); + + Scene scene = window.getScene(); + assert scene != null; + + originalRoot = (Pane) scene.getRoot(); + stackPane = new StackPane(); + + stackPane.getChildren().add(originalRoot); + stackPane.getChildren().add(overlayPane); + + scene.setRoot(stackPane); + } + + /** + * Displays a walkthrough step with the specified target node. + */ + public void displayStep(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { + hide(); + displayStepContent(step, targetNode, walkthrough); + overlayPane.toFront(); + } + + /** + * Hide the overlay and clean up any resources. + */ + public void hide() { + if (currentPopOver != null) { + currentPopOver.hide(); + currentPopOver = null; + } + + overlayPane.getChildren().clear(); + overlayPane.setClip(null); + overlayPane.setVisible(true); + + cleanUpTasks.forEach(Runnable::run); + cleanUpTasks.clear(); + } + + /** + * Detaches the overlay and restores the original scene root. + */ + public void detach() { + hide(); + + Scene scene = window.getScene(); + if (scene != null && originalRoot != null) { + stackPane.getChildren().remove(originalRoot); + scene.setRoot(originalRoot); + LOGGER.debug("Restored original scene root: {}", originalRoot.getClass().getName()); + } + } + + private void displayStepContent(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { + switch (step) { + case TooltipStep tooltipStep -> { + displayTooltipStep(tooltipStep, targetNode, walkthrough); + hideOverlayPane(); + } + case PanelStep panelStep -> { + Node content = renderer.render(panelStep, walkthrough); + displayPanelStep(content, panelStep); + setupClipping(content); + } + } + + step.navigationPredicate().ifPresent(predicate -> + cleanUpTasks.add(predicate.attachListeners(targetNode, walkthrough::nextStep))); + } + + private void displayTooltipStep(TooltipStep step, Node targetNode, Walkthrough walkthrough) { + Node content = renderer.render(step, walkthrough); + + currentPopOver = new PopOver(); + currentPopOver.setContentNode(content); + currentPopOver.setDetachable(false); + currentPopOver.setCloseButtonEnabled(false); + currentPopOver.setHeaderAlwaysVisible(false); + + PopOver.ArrowLocation arrowLocation = mapToArrowLocation(step.position()); + if (arrowLocation != null) { + currentPopOver.setArrowLocation(arrowLocation); + } + + step.preferredWidth().ifPresent(width -> { + currentPopOver.setPrefWidth(width); + currentPopOver.setMinWidth(width); + }); + step.preferredHeight().ifPresent(height -> { + currentPopOver.setPrefHeight(height); + currentPopOver.setMinHeight(height); + }); + + currentPopOver.show(targetNode); + + cleanUpTasks.add(() -> { + if (currentPopOver != null) { + currentPopOver.hide(); + currentPopOver = null; + } + }); + } + + private void displayPanelStep(Node content, PanelStep step) { + overlayPane.getChildren().clear(); + overlayPane.getRowConstraints().clear(); + overlayPane.getColumnConstraints().clear(); + + configurePanelLayout(step.position()); + + overlayPane.getChildren().add(content); + GridPane.setHgrow(content, Priority.NEVER); + GridPane.setVgrow(content, Priority.NEVER); + + switch (step.position()) { + case CENTER_LEFT -> { + overlayPane.setAlignment(Pos.CENTER_LEFT); + GridPane.setVgrow(content, Priority.ALWAYS); + GridPane.setFillHeight(content, true); + } + case CENTER_RIGHT -> { + overlayPane.setAlignment(Pos.CENTER_RIGHT); + GridPane.setVgrow(content, Priority.ALWAYS); + GridPane.setFillHeight(content, true); + } + case TOP_CENTER -> { + overlayPane.setAlignment(Pos.TOP_CENTER); + GridPane.setHgrow(content, Priority.ALWAYS); + GridPane.setFillWidth(content, true); + } + case BOTTOM_CENTER -> { + overlayPane.setAlignment(Pos.BOTTOM_CENTER); + GridPane.setHgrow(content, Priority.ALWAYS); + GridPane.setFillWidth(content, true); + } + default -> { + LOGGER.warn("Unsupported position for panel step: {}", step.position()); + overlayPane.setAlignment(Pos.CENTER); + } + } + } + + private void configurePanelLayout(Pos position) { + RowConstraints rowConstraints = new RowConstraints(); + ColumnConstraints columnConstraints = new ColumnConstraints(); + + switch (position) { + case CENTER_LEFT, + CENTER_RIGHT -> { + rowConstraints.setVgrow(Priority.ALWAYS); + columnConstraints.setHgrow(Priority.NEVER); + } + case TOP_CENTER, + BOTTOM_CENTER -> { + columnConstraints.setHgrow(Priority.ALWAYS); + rowConstraints.setVgrow(Priority.NEVER); + } + default -> { + rowConstraints.setVgrow(Priority.NEVER); + columnConstraints.setHgrow(Priority.NEVER); + } + } + + overlayPane.getRowConstraints().add(rowConstraints); + overlayPane.getColumnConstraints().add(columnConstraints); + } + + private PopOver.ArrowLocation mapToArrowLocation(TooltipPosition position) { + return switch (position) { + case TOP -> + PopOver.ArrowLocation.TOP_CENTER; + case BOTTOM -> + PopOver.ArrowLocation.BOTTOM_CENTER; + case LEFT -> + PopOver.ArrowLocation.LEFT_CENTER; + case RIGHT -> + PopOver.ArrowLocation.RIGHT_CENTER; + case AUTO -> + null; + }; + } + + private void hideOverlayPane() { + overlayPane.setVisible(false); + cleanUpTasks.add(() -> overlayPane.setVisible(true)); + } + + private void setupClipping(Node node) { + ChangeListener listener = (_, _, bounds) -> { + if (bounds != null && bounds.getWidth() > 0 && bounds.getHeight() > 0) { + Rectangle clip = new Rectangle(bounds.getMinX(), bounds.getMinY(), + bounds.getWidth(), bounds.getHeight()); + overlayPane.setClip(clip); + } + }; + + node.boundsInParentProperty().addListener(listener); + + Bounds initialBounds = node.getBoundsInParent(); + if (initialBounds.getWidth() > 0 && initialBounds.getHeight() > 0) { + Rectangle clip = new Rectangle(initialBounds.getMinX(), initialBounds.getMinY(), + initialBounds.getWidth(), initialBounds.getHeight()); + overlayPane.setClip(clip); + } + + cleanUpTasks.add(() -> node.boundsInParentProperty().removeListener(listener)); + cleanUpTasks.add(() -> overlayPane.setClip(null)); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index e77fd93ba1a..b487777be7d 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -22,7 +22,7 @@ public class Walkthrough { private final BooleanProperty active; private final List steps; - private Optional overlayManager = Optional.empty(); + private Optional overlayManager = Optional.empty(); private Stage currentStage; /** @@ -64,9 +64,9 @@ public ReadOnlyIntegerProperty totalStepsProperty() { */ public void start(Stage stage) { if (currentStage != stage) { - overlayManager.ifPresent(MultiWindowWalkthroughOverlay::detachAll); + overlayManager.ifPresent(WalkthroughOverlay::detachAll); currentStage = stage; - overlayManager = Optional.of(new MultiWindowWalkthroughOverlay(stage, this)); + overlayManager = Optional.of(new WalkthroughOverlay(stage, this)); } currentStep.set(0); @@ -106,7 +106,7 @@ public Optional getScene() { } private void stop() { - overlayManager.ifPresent(MultiWindowWalkthroughOverlay::detachAll); + overlayManager.ifPresent(WalkthroughOverlay::detachAll); active.set(false); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index a5e306b8356..6264a8f99bc 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Optional; +import javafx.geometry.Pos; import javafx.scene.control.ContextMenu; import javafx.stage.Window; @@ -16,8 +17,11 @@ import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; +import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; import org.jabref.gui.walkthrough.declarative.step.TooltipStep; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.logic.l10n.Localization; public class WalkthroughAction extends SimpleCommand { private static final Map WALKTHROUGH_REGISTRY = buildRegistry(); @@ -39,55 +43,71 @@ private static Map buildRegistry() { Map registry = new HashMap<>(); // FIXME: Not internationalized. - TooltipStep step1 = TooltipStep.builder("Hover over \"File\" menu") - .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) - .navigation(NavigationPredicate.onClick()) - .position(TooltipPosition.BOTTOM) - .highlight(HighlightEffect.BACKDROP_HIGHLIGHT) - .build(); + WalkthroughNode step1 = TooltipStep + .builder("Hover over \"File\" menu") + .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.BACKDROP_HIGHLIGHT) + .build(); - TooltipStep step2 = TooltipStep.builder("Select \"Preferences\"") - .resolver(NodeResolver.menuItem("Preferences")) - .navigation(NavigationPredicate.onClick()) - .position(TooltipPosition.RIGHT) - .highlight(MultiWindowHighlight.multiple( - new WindowEffect(Optional::empty, HighlightEffect.FULL_SCREEN_DARKEN), - new WindowEffect( - () -> Window.getWindows().stream() - .filter(w -> w instanceof ContextMenu cm && cm.isShowing()) - .findFirst(), - HighlightEffect.ANIMATED_PULSE, - NodeResolver.menuItem("Preferences") - ) - )) - .build(); + WalkthroughNode step2 = TooltipStep + .builder("Select \"Preferences\"") + .resolver(NodeResolver.menuItem("Preferences")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.RIGHT) + .highlight(MultiWindowHighlight.multiple( + new WindowEffect(Optional::empty, HighlightEffect.FULL_SCREEN_DARKEN), + new WindowEffect( + () -> Window.getWindows().stream() + .filter(w -> w instanceof ContextMenu cm && cm.isShowing()) + .findFirst(), + HighlightEffect.ANIMATED_PULSE, + NodeResolver.menuItem("Preferences") + ) + )) + .build(); - TooltipStep step3 = TooltipStep.builder("Select \"Linked files\" tab") - .resolver(NodeResolver.predicate(node -> - node.getStyleClass().contains("list-cell") && - node.toString().contains("Linked files"))) - .navigation(NavigationPredicate.onClick()) - .position(TooltipPosition.AUTO) - .highlight(MultiWindowHighlight.multiple( - new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), - new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) - )) - .autoFallback(false) - .activeWindow(WindowResolver.title("JabRef preferences")) - .build(); + WalkthroughNode step3 = TooltipStep + .builder("Select \"Linked files\" tab") + .resolver(NodeResolver.predicate(node -> + node.getStyleClass().contains("list-cell") && + node.toString().contains("Linked files"))) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.AUTO) + .highlight(MultiWindowHighlight.multiple( + new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) + )) + .autoFallback(false) + .activeWindow(WindowResolver.title("JabRef preferences")) + .build(); - TooltipStep step4 = TooltipStep.builder("Choose to use main file directory") - .resolver(NodeResolver.fxId("useMainFileDirectory")) - .navigation(NavigationPredicate.onClick()) - .position(TooltipPosition.AUTO) - .highlight(MultiWindowHighlight.multiple( - new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), - new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) - )) - .activeWindow(WindowResolver.title("JabRef preferences")) - .build(); + WalkthroughNode step4 = TooltipStep + .builder("Choose to use main file directory") + .resolver(NodeResolver.fxId("useMainFileDirectory")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.AUTO) + .highlight(MultiWindowHighlight.multiple( + new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) + )) + .activeWindow(WindowResolver.title("JabRef preferences")) + .build(); - Walkthrough mainFileDirectory = new Walkthrough(List.of(step1, step2, step3, step4)); + WalkthroughNode step5 = PanelStep + .builder("Click \"OK\" to save changes") + .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("button") && node.toString().contains(Localization.lang("Save")))) + .navigation(NavigationPredicate.onClick()) + .position(Pos.TOP_CENTER) + .highlight(MultiWindowHighlight.multiple( + new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) + )) + .activeWindow(WindowResolver.title("JabRef preferences")) + .build(); + + Walkthrough mainFileDirectory = new Walkthrough(List.of(step1, step2, step3, step4, step5)); registry.put("mainFileDirectory", mainFileDirectory); Walkthrough editEntry = new Walkthrough(List.of()); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java similarity index 77% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 8280e03ff16..d963530cd88 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/MultiWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -22,23 +22,23 @@ /** * Manages walkthrough overlays and highlights across multiple windows. */ -public class MultiWindowWalkthroughOverlay { - private static final Logger LOGGER = LoggerFactory.getLogger(MultiWindowWalkthroughOverlay.class); +public class WalkthroughOverlay { + private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); - private final Map overlays = new HashMap<>(); + private final Map overlays = new HashMap<>(); private final Stage mainStage; - private final HighlightManager highlightManager; + private final WalkthroughHighlighter walkthroughHighlighter; private final Walkthrough walkthrough; private Timeline nodePollingTimeline; - public MultiWindowWalkthroughOverlay(Stage mainStage, Walkthrough walkthrough) { + public WalkthroughOverlay(Stage mainStage, Walkthrough walkthrough) { this.mainStage = mainStage; this.walkthrough = walkthrough; - this.highlightManager = new HighlightManager(); + this.walkthroughHighlighter = new WalkthroughHighlighter(); } public void displayStep(@NonNull WalkthroughNode step) { - overlays.values().forEach(SingleWindowOverlay::hide); + overlays.values().forEach(SingleWindowWalkthroughOverlay::hide); Window activeWindow = step.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); Scene scene = activeWindow.getScene(); @@ -53,17 +53,17 @@ public void displayStep(@NonNull WalkthroughNode step) { } if (step.highlight().isPresent()) { - highlightManager.applyHighlight(scene, step.highlight(), targetNode); + walkthroughHighlighter.applyHighlight(scene, step.highlight(), targetNode); } - SingleWindowOverlay overlay = getOrCreateOverlay(activeWindow); + SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(activeWindow); overlay.displayStep(step, targetNode.get(), walkthrough); } public void detachAll() { stopNodePolling(); - highlightManager.detachAll(); - overlays.values().forEach(SingleWindowOverlay::detach); + walkthroughHighlighter.detachAll(); + overlays.values().forEach(SingleWindowWalkthroughOverlay::detach); overlays.clear(); } @@ -98,10 +98,10 @@ private void startNodePolling(WalkthroughNode step, Window activeWindow) { stopNodePolling(); if (step.highlight().isPresent()) { - highlightManager.applyHighlight(scene, step.highlight(), targetNode); + walkthroughHighlighter.applyHighlight(scene, step.highlight(), targetNode); } - SingleWindowOverlay overlay = getOrCreateOverlay(activeWindow); + SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(activeWindow); overlay.displayStep(step, targetNode.get(), walkthrough); } } @@ -122,7 +122,7 @@ private void stopNodePolling() { return walkthrough.getSteps().get(index); } - private SingleWindowOverlay getOrCreateOverlay(Window window) { - return overlays.computeIfAbsent(window, SingleWindowOverlay::new); + private SingleWindowWalkthroughOverlay getOrCreateOverlay(Window window) { + return overlays.computeIfAbsent(window, SingleWindowWalkthroughOverlay::new); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index 85becb6912a..5e500b52f4c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -30,10 +30,10 @@ public class WalkthroughRenderer { * * @param step The tooltip step to render * @param walkthrough The walkthrough context for navigation - * @return The rendered tooltip Node + * @return The rendered tooltip content Node */ public Node render(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) { - return createTooltip(step, walkthrough); + return createTooltipContent(step, walkthrough); } /** @@ -79,17 +79,9 @@ public Node render(@NonNull PanelStep step, @NonNull Walkthrough walkthrough) { return panel; } - private Node createTooltip(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) { - VBox tooltip = new VBox(6); - tooltip.getStyleClass().add("walkthrough-tooltip"); - - double prefWidth = step.preferredWidth().orElse(300.0); - double prefHeight = step.preferredHeight().orElse(200.0); - - tooltip.setPrefWidth(prefWidth); - tooltip.setPrefHeight(prefHeight); - tooltip.setMaxWidth(prefWidth); - tooltip.setMaxHeight(prefHeight); + private Node createTooltipContent(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) { + VBox tooltip = new VBox(); + tooltip.getStyleClass().add("walkthrough-tooltip-content-container"); Label titleLabel = new Label(Localization.lang(step.title())); titleLabel.getStyleClass().add("walkthrough-tooltip-title"); @@ -99,6 +91,7 @@ private Node createTooltip(@NonNull TooltipStep step, @NonNull Walkthrough walkt VBox.setVgrow(contentContainer, Priority.ALWAYS); HBox actionsContainer = makeActions(step, walkthrough); + actionsContainer.getStyleClass().add("walkthrough-tooltip-actions"); tooltip.getChildren().addAll(titleLabel, contentContainer, actionsContainer); @@ -136,7 +129,7 @@ private VBox makePanel() { private HBox makeActions(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); - actions.setSpacing(0); + actions.getStyleClass().add("walkthrough-actions"); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); @@ -146,7 +139,7 @@ private HBox makeActions(@NonNull WalkthroughNode step, @NonNull Walkthrough wal } HBox rightActions = new HBox(); rightActions.setAlignment(Pos.CENTER_RIGHT); - rightActions.setSpacing(4); + rightActions.getStyleClass().add("walkthrough-right-actions"); if (step.skipButtonText().isPresent()) { rightActions.getChildren().add(makeSkipButton(step, walkthrough)); } @@ -160,7 +153,8 @@ private HBox makeActions(@NonNull WalkthroughNode step, @NonNull Walkthrough wal } private VBox makeContent(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { - VBox contentBox = new VBox(8); + VBox contentBox = new VBox(); + contentBox.getStyleClass().add("walkthrough-content"); contentBox.getChildren().addAll(step.content().stream().map(block -> switch (block) { case TextBlock textBlock -> render(textBlock); diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index e95053f419a..48d71077b9c 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2521,6 +2521,30 @@ journalInfo .grid-cell-b { } /* Walkthrough Styles */ +.walkthrough-overlay { + -fx-background-color: transparent; +} + +/* Tooltip Content */ +.walkthrough-tooltip-content-container { + -fx-spacing: 0.75em; + -fx-padding: 1em; +} + +.walkthrough-tooltip-title { + -fx-text-fill: -jr-theme; + -fx-font-size: 1.4em; + -fx-font-weight: bold; +} + +.walkthrough-tooltip-content { + -fx-spacing: 0.5em; +} + +.walkthrough-tooltip-actions { + -fx-spacing: 0; +} + .walkthrough-panel { -fx-background-color: -jr-white; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0, 0, 2); @@ -2556,6 +2580,10 @@ journalInfo .grid-cell-b { -fx-min-height: 18em; } +.walkthrough-content { + -fx-spacing: 0.8em; +} + .walkthrough-step-counter { -fx-background-color: -jr-base; -fx-text-fill: -jr-gray-3; @@ -2592,11 +2620,20 @@ journalInfo .grid-cell-b { -fx-alignment: center-left; } +.walkthrough-actions { + -fx-spacing: 0; +} + +.walkthrough-right-actions { + -fx-spacing: 4; +} + .walkthrough-continue-button { -fx-background-color: -jr-theme; -fx-text-fill: -jr-white; -fx-font-size: 1.1em; -fx-border-radius: 4; + -fx-background-radius: 4; -fx-padding: 0.25em 0.5em; } @@ -2607,6 +2644,7 @@ journalInfo .grid-cell-b { -fx-border-color: -jr-theme; -fx-border-width: 0.5; -fx-border-radius: 4; + -fx-background-radius: 4; -fx-padding: 0.25em 0.5em; } @@ -2615,6 +2653,7 @@ journalInfo .grid-cell-b { -fx-text-fill: -jr-theme; -fx-font-size: 1.1em; -fx-border-radius: 4; + -fx-background-radius: 4; -fx-padding: 0.25em 0.5em; } @@ -2624,23 +2663,3 @@ journalInfo .grid-cell-b { -fx-max-width: 48em; -fx-fill-width: true; } - -.walkthrough-tooltip { - -fx-background-color: -jr-white; - -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0, 0, 2); - -fx-background-radius: 8; - -fx-padding: 1em; - -fx-spacing: 0.75em; - -fx-border-radius: 8; -} - -.walkthrough-tooltip-title { - -fx-text-fill: -jr-theme; - -fx-font-size: 1.4em; - -fx-font-weight: bold; -} - -.walkthrough-tooltip-content { - -fx-spacing: 0.5em; -} - From ca33ea64deb7f1c4662e5368f6302b80652b218c Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Wed, 18 Jun 2025 09:53:08 -0400 Subject: [PATCH 24/50] Change excess usage of Optional to Nullable --- .../SingleWindowWalkthroughOverlay.java | 34 ++--- .../jabref/gui/walkthrough/Walkthrough.java | 106 ++++++++++------ .../gui/walkthrough/WalkthroughAction.java | 22 ++-- .../walkthrough/WalkthroughHighlighter.java | 82 ++++++------ .../gui/walkthrough/WalkthroughOverlay.java | 49 ++++---- .../gui/walkthrough/WalkthroughRenderer.java | 17 +-- .../declarative/effect/WindowEffect.java | 6 +- .../declarative/step/PanelPosition.java | 8 ++ .../declarative/step/PanelStep.java | 119 ++++++++++++------ .../declarative/step/TooltipStep.java | 111 +++++++++++----- ...kthroughNode.java => WalkthroughStep.java} | 10 +- 11 files changed, 347 insertions(+), 217 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelPosition.java rename jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/{WalkthroughNode.java => WalkthroughStep.java} (82%) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index c53e297fd26..69ac1eb752e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -17,12 +17,14 @@ import javafx.scene.shape.Rectangle; import javafx.stage.Window; +import org.jabref.gui.walkthrough.declarative.step.PanelPosition; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; import org.jabref.gui.walkthrough.declarative.step.TooltipStep; -import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.controlsfx.control.PopOver; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,7 +67,7 @@ public SingleWindowWalkthroughOverlay(Window window) { /** * Displays a walkthrough step with the specified target node. */ - public void displayStep(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { + public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Walkthrough walkthrough) { hide(); displayStepContent(step, targetNode, walkthrough); overlayPane.toFront(); @@ -102,7 +104,7 @@ public void detach() { } } - private void displayStepContent(WalkthroughNode step, Node targetNode, Walkthrough walkthrough) { + private void displayStepContent(WalkthroughStep step, Node targetNode, Walkthrough walkthrough) { switch (step) { case TooltipStep tooltipStep -> { displayTooltipStep(tooltipStep, targetNode, walkthrough); @@ -164,22 +166,22 @@ private void displayPanelStep(Node content, PanelStep step) { GridPane.setVgrow(content, Priority.NEVER); switch (step.position()) { - case CENTER_LEFT -> { + case LEFT -> { overlayPane.setAlignment(Pos.CENTER_LEFT); GridPane.setVgrow(content, Priority.ALWAYS); GridPane.setFillHeight(content, true); } - case CENTER_RIGHT -> { + case RIGHT -> { overlayPane.setAlignment(Pos.CENTER_RIGHT); GridPane.setVgrow(content, Priority.ALWAYS); GridPane.setFillHeight(content, true); } - case TOP_CENTER -> { + case TOP -> { overlayPane.setAlignment(Pos.TOP_CENTER); GridPane.setHgrow(content, Priority.ALWAYS); GridPane.setFillWidth(content, true); } - case BOTTOM_CENTER -> { + case BOTTOM -> { overlayPane.setAlignment(Pos.BOTTOM_CENTER); GridPane.setHgrow(content, Priority.ALWAYS); GridPane.setFillWidth(content, true); @@ -191,18 +193,18 @@ private void displayPanelStep(Node content, PanelStep step) { } } - private void configurePanelLayout(Pos position) { + private void configurePanelLayout(PanelPosition position) { RowConstraints rowConstraints = new RowConstraints(); ColumnConstraints columnConstraints = new ColumnConstraints(); switch (position) { - case CENTER_LEFT, - CENTER_RIGHT -> { + case LEFT, + RIGHT -> { rowConstraints.setVgrow(Priority.ALWAYS); columnConstraints.setHgrow(Priority.NEVER); } - case TOP_CENTER, - BOTTOM_CENTER -> { + case TOP, + BOTTOM -> { columnConstraints.setHgrow(Priority.ALWAYS); rowConstraints.setVgrow(Priority.NEVER); } @@ -219,13 +221,13 @@ private void configurePanelLayout(Pos position) { private PopOver.ArrowLocation mapToArrowLocation(TooltipPosition position) { return switch (position) { case TOP -> - PopOver.ArrowLocation.TOP_CENTER; - case BOTTOM -> PopOver.ArrowLocation.BOTTOM_CENTER; + case BOTTOM -> + PopOver.ArrowLocation.TOP_CENTER; case LEFT -> - PopOver.ArrowLocation.LEFT_CENTER; - case RIGHT -> PopOver.ArrowLocation.RIGHT_CENTER; + case RIGHT -> + PopOver.ArrowLocation.LEFT_CENTER; case AUTO -> null; }; diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index b487777be7d..6a52dbf53b9 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -11,31 +11,38 @@ import javafx.scene.Scene; import javafx.stage.Stage; -import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Maintains the state of a walkthrough. */ public class Walkthrough { + private static final Logger LOGGER = LoggerFactory.getLogger(Walkthrough.class); + private final IntegerProperty currentStep; - private final IntegerProperty totalSteps; private final BooleanProperty active; - private final List steps; - private Optional overlayManager = Optional.empty(); + private final List steps; + private @Nullable WalkthroughOverlay overlay; private Stage currentStage; /** - * Creates a new walkthrough with the specified preferences. + * Creates a new walkthrough with steps */ - public Walkthrough(List steps) { + public Walkthrough(List steps) { this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); this.steps = steps; - this.totalSteps = new SimpleIntegerProperty(steps.size()); } - public Walkthrough(WalkthroughNode... steps) { + /** + * Creates a new walkthrough with steps + */ + public Walkthrough(WalkthroughStep... steps) { this(List.of(steps)); } @@ -48,15 +55,6 @@ public ReadOnlyIntegerProperty currentStepProperty() { return currentStep; } - /** - * Gets the total number of steps property. - * - * @return The total steps property - */ - public ReadOnlyIntegerProperty totalStepsProperty() { - return totalSteps; - } - /** * Starts the walkthrough from the first step. * @@ -64,14 +62,23 @@ public ReadOnlyIntegerProperty totalStepsProperty() { */ public void start(Stage stage) { if (currentStage != stage) { - overlayManager.ifPresent(WalkthroughOverlay::detachAll); + if (overlay != null) { + overlay.detachAll(); + } currentStage = stage; - overlayManager = Optional.of(new WalkthroughOverlay(stage, this)); + overlay = new WalkthroughOverlay(stage, this); } currentStep.set(0); active.set(true); - getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); + + if (overlay == null) { + LOGGER.warn("Overlay is null after initialization, cannot display step"); + return; + } + + WalkthroughStep step = getCurrentStep(); + overlay.displayStep(step); } /** @@ -79,12 +86,19 @@ public void start(Stage stage) { */ public void nextStep() { int nextIndex = currentStep.get() + 1; - if (nextIndex < steps.size()) { - currentStep.set(nextIndex); - getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); - } else { + if (nextIndex >= steps.size()) { stop(); + return; } + + currentStep.set(nextIndex); + if (overlay == null) { + LOGGER.warn("Overlay is null, cannot display next step"); + return; + } + + WalkthroughStep step = getCurrentStep(); + overlay.displayStep(step); } /** @@ -92,10 +106,18 @@ public void nextStep() { */ public void previousStep() { int prevIndex = currentStep.get() - 1; - if (prevIndex >= 0) { - currentStep.set(prevIndex); - getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); + if (prevIndex < 0) { + return; } + + currentStep.set(prevIndex); + if (overlay == null) { + LOGGER.warn("Overlay is null, cannot display previous step"); + return; + } + + WalkthroughStep step = getCurrentStep(); + overlay.displayStep(step); } /** @@ -106,18 +128,29 @@ public Optional getScene() { } private void stop() { - overlayManager.ifPresent(WalkthroughOverlay::detachAll); + if (overlay != null) { + overlay.detachAll(); + } active.set(false); } public void goToStep(int stepIndex) { - if (stepIndex >= 0 && stepIndex < steps.size()) { - currentStep.set(stepIndex); - getCurrentStep().ifPresent((step) -> overlayManager.ifPresent(manager -> manager.displayStep(step))); + if (stepIndex < 0 || stepIndex >= steps.size()) { + LOGGER.debug("Invalid step index: {}. Valid range is 0 to {}.", stepIndex, steps.size() - 1); + return; + } + + currentStep.set(stepIndex); + if (overlay == null) { + LOGGER.warn("Overlay is null, cannot go to step {}", stepIndex); + return; } + + WalkthroughStep step = getCurrentStep(); + overlay.displayStep(step); } - public List getSteps() { + public List getSteps() { return steps; } @@ -125,11 +158,8 @@ public void skip() { stop(); } - private Optional getCurrentStep() { - int index = currentStep.get(); - if (index >= 0 && index < steps.size()) { - return Optional.of(steps.get(index)); - } - return Optional.empty(); + private WalkthroughStep getCurrentStep() { + return steps.get(currentStep.get()); } } + diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 6264a8f99bc..9dfcfbcb404 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -1,11 +1,9 @@ package org.jabref.gui.walkthrough; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; -import javafx.geometry.Pos; import javafx.scene.control.ContextMenu; import javafx.stage.Window; @@ -17,10 +15,11 @@ import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; +import org.jabref.gui.walkthrough.declarative.step.PanelPosition; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; import org.jabref.gui.walkthrough.declarative.step.TooltipStep; -import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.jabref.logic.l10n.Localization; public class WalkthroughAction extends SimpleCommand { @@ -43,7 +42,7 @@ private static Map buildRegistry() { Map registry = new HashMap<>(); // FIXME: Not internationalized. - WalkthroughNode step1 = TooltipStep + WalkthroughStep step1 = TooltipStep .builder("Hover over \"File\" menu") .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) .navigation(NavigationPredicate.onClick()) @@ -51,7 +50,7 @@ private static Map buildRegistry() { .highlight(HighlightEffect.BACKDROP_HIGHLIGHT) .build(); - WalkthroughNode step2 = TooltipStep + WalkthroughStep step2 = TooltipStep .builder("Select \"Preferences\"") .resolver(NodeResolver.menuItem("Preferences")) .navigation(NavigationPredicate.onClick()) @@ -68,7 +67,7 @@ private static Map buildRegistry() { )) .build(); - WalkthroughNode step3 = TooltipStep + WalkthroughStep step3 = TooltipStep .builder("Select \"Linked files\" tab") .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("list-cell") && @@ -83,7 +82,7 @@ private static Map buildRegistry() { .activeWindow(WindowResolver.title("JabRef preferences")) .build(); - WalkthroughNode step4 = TooltipStep + WalkthroughStep step4 = TooltipStep .builder("Choose to use main file directory") .resolver(NodeResolver.fxId("useMainFileDirectory")) .navigation(NavigationPredicate.onClick()) @@ -95,11 +94,11 @@ private static Map buildRegistry() { .activeWindow(WindowResolver.title("JabRef preferences")) .build(); - WalkthroughNode step5 = PanelStep + WalkthroughStep step5 = PanelStep .builder("Click \"OK\" to save changes") .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("button") && node.toString().contains(Localization.lang("Save")))) .navigation(NavigationPredicate.onClick()) - .position(Pos.TOP_CENTER) + .position(PanelPosition.TOP) .highlight(MultiWindowHighlight.multiple( new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) @@ -107,11 +106,8 @@ private static Map buildRegistry() { .activeWindow(WindowResolver.title("JabRef preferences")) .build(); - Walkthrough mainFileDirectory = new Walkthrough(List.of(step1, step2, step3, step4, step5)); + Walkthrough mainFileDirectory = new Walkthrough(step1, step2, step3, step4, step5); registry.put("mainFileDirectory", mainFileDirectory); - - Walkthrough editEntry = new Walkthrough(List.of()); - registry.put("editEntry", editEntry); return registry; } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java index 11740e2a8dc..1e691ed5ca0 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java @@ -2,7 +2,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.Optional; import javafx.scene.Node; import javafx.scene.Scene; @@ -12,10 +11,12 @@ import org.jabref.gui.walkthrough.components.BackdropHighlight; import org.jabref.gui.walkthrough.components.FullScreenDarken; import org.jabref.gui.walkthrough.components.PulseAnimateIndicator; +import org.jabref.gui.walkthrough.declarative.WindowResolver; import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * Manages highlight effects across multiple windows for walkthrough steps. @@ -29,33 +30,34 @@ public class WalkthroughHighlighter { * Applies the specified highlight configuration. * * @param mainScene The primary scene to apply the highlight to. - * @param highlightConfig The optional highlight configuration to apply. Default to - * BackdropHighlight on the primary windows if empty. - * @param fallbackTarget The optional fallback target node to use if no highlight + * @param highlightConfig The highlight configuration to apply. Default to + * BackdropHighlight on the primary windows if null. + * @param fallbackTarget The fallback target node to use if no highlight * configuration is provided. */ public void applyHighlight(@NonNull Scene mainScene, - Optional highlightConfig, - Optional fallbackTarget) { + @Nullable MultiWindowHighlight highlightConfig, + @Nullable Node fallbackTarget) { detachAll(); - highlightConfig.ifPresentOrElse( - config -> { - if (config.windowEffects().isEmpty() && config.fallbackEffect().isPresent()) { - applyEffect(mainScene.getWindow(), config.fallbackEffect().get(), fallbackTarget); - return; - } - config.windowEffects().forEach(effect -> { - Window window = effect.windowResolver().resolve().orElse(mainScene.getWindow()); - Optional targetNode = effect.targetNodeResolver() - .map(resolver -> - resolver.resolve(Optional.ofNullable(window.getScene()).orElse(mainScene))) - .orElse(fallbackTarget); - applyEffect(window, effect.effect(), targetNode); - }); - }, - () -> fallbackTarget.ifPresent(node -> applyBackdropHighlight(mainScene.getWindow(), node)) - ); + if (highlightConfig != null) { + if (highlightConfig.windowEffects().isEmpty() && highlightConfig.fallbackEffect().isPresent()) { + applyEffect(mainScene.getWindow(), highlightConfig.fallbackEffect().get(), fallbackTarget); + return; + } + highlightConfig.windowEffects().forEach(effect -> { + Window window = effect.windowResolver().flatMap(WindowResolver::resolve).orElse(mainScene.getWindow()); + Node targetNode = effect + .targetNodeResolver() + .flatMap(resolver -> resolver.resolve(window.getScene() != null ? window.getScene() : mainScene)) + .orElse(fallbackTarget); + applyEffect(window, effect.effect(), targetNode); + }); + } else { + if (fallbackTarget != null) { + applyBackdropHighlight(mainScene.getWindow(), fallbackTarget); + } + } } /** @@ -72,13 +74,20 @@ public void detachAll() { fullScreenDarkens.clear(); } - private void applyEffect(Window window, HighlightEffect effect, Optional targetNode) { + private void applyEffect(@NonNull Window window, @NonNull HighlightEffect effect, @Nullable Node targetNode) { switch (effect) { - case BACKDROP_HIGHLIGHT -> - targetNode.ifPresent(node -> applyBackdropHighlight(window, node)); - case ANIMATED_PULSE -> - targetNode.ifPresent(node -> applyPulseAnimation(window, node)); - case FULL_SCREEN_DARKEN -> applyFullScreenDarken(window); + case BACKDROP_HIGHLIGHT -> { + if (targetNode != null) { + applyBackdropHighlight(window, targetNode); + } + } + case ANIMATED_PULSE -> { + if (targetNode != null) { + applyPulseAnimation(window, targetNode); + } + } + case FULL_SCREEN_DARKEN -> + applyFullScreenDarken(window); case NONE -> { if (backdropHighlights.containsKey(window)) { backdropHighlights.get(window).detach(); @@ -96,36 +105,33 @@ private void applyEffect(Window window, HighlightEffect effect, Optional t } } - private void applyBackdropHighlight(Window window, Node targetNode) { + private void applyBackdropHighlight(@NonNull Window window, @NonNull Node targetNode) { Scene scene = window.getScene(); if (scene == null || !(scene.getRoot() instanceof Pane pane)) { return; } - BackdropHighlight backdrop = new BackdropHighlight(pane); + BackdropHighlight backdrop = backdropHighlights.computeIfAbsent(window, _ -> new BackdropHighlight(pane)); backdrop.attach(targetNode); - backdropHighlights.put(window, backdrop); } - private void applyPulseAnimation(Window window, Node targetNode) { + private void applyPulseAnimation(@NonNull Window window, @NonNull Node targetNode) { Scene scene = window.getScene(); if (scene == null || !(scene.getRoot() instanceof Pane pane)) { return; } - PulseAnimateIndicator pulse = new PulseAnimateIndicator(pane); + PulseAnimateIndicator pulse = pulseIndicators.computeIfAbsent(window, _ -> new PulseAnimateIndicator(pane)); pulse.attach(targetNode); - pulseIndicators.put(window, pulse); } - private void applyFullScreenDarken(Window window) { + private void applyFullScreenDarken(@NonNull Window window) { Scene scene = window.getScene(); if (scene == null || !(scene.getRoot() instanceof Pane pane)) { return; } - FullScreenDarken fullDarken = new FullScreenDarken(pane); + FullScreenDarken fullDarken = fullScreenDarkens.computeIfAbsent(window, _ -> new FullScreenDarken(pane)); fullDarken.attach(); - fullScreenDarkens.put(window, fullDarken); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index d963530cd88..7839ce2f025 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -13,7 +13,7 @@ import javafx.util.Duration; import org.jabref.gui.walkthrough.declarative.WindowResolver; -import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.jspecify.annotations.NonNull; import org.slf4j.Logger; @@ -37,13 +37,13 @@ public WalkthroughOverlay(Stage mainStage, Walkthrough walkthrough) { this.walkthroughHighlighter = new WalkthroughHighlighter(); } - public void displayStep(@NonNull WalkthroughNode step) { + public void displayStep(@NonNull WalkthroughStep step) { overlays.values().forEach(SingleWindowWalkthroughOverlay::hide); Window activeWindow = step.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); Scene scene = activeWindow.getScene(); - Optional targetNode = step.resolver().resolve(scene); - if (targetNode.isEmpty()) { + Optional targetNode = step.resolver().flatMap(resolver -> resolver.resolve(scene)); + if (step.resolver().isPresent() && targetNode.isEmpty()) { if (step.autoFallback()) { tryRevertToPreviousResolvableStep(); } else { @@ -52,12 +52,9 @@ public void displayStep(@NonNull WalkthroughNode step) { return; } - if (step.highlight().isPresent()) { - walkthroughHighlighter.applyHighlight(scene, step.highlight(), targetNode); - } - + walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), targetNode.orElse(null)); SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(activeWindow); - overlay.displayStep(step, targetNode.get(), walkthrough); + overlay.displayStep(step, targetNode.orElse(null), walkthrough); } public void detachAll() { @@ -72,10 +69,10 @@ private void tryRevertToPreviousResolvableStep() { int currentIndex = walkthrough.currentStepProperty().get(); for (int i = currentIndex - 1; i >= 0; i--) { - WalkthroughNode previousStep = getStepAtIndex(i); + WalkthroughStep previousStep = getStepAtIndex(i); Window activeWindow = previousStep.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); Scene scene = activeWindow.getScene(); - if (scene != null && previousStep.resolver().resolve(scene).isPresent()) { + if (scene != null && (previousStep.resolver().isEmpty() || previousStep.resolver().get().resolve(scene).isPresent())) { LOGGER.info("Reverting to step {} from step {}", i, currentIndex); walkthrough.goToStep(i); return; @@ -85,26 +82,26 @@ private void tryRevertToPreviousResolvableStep() { LOGGER.warn("No previous resolvable step found, staying at current step"); } - private void startNodePolling(WalkthroughNode step, Window activeWindow) { + private void startNodePolling(WalkthroughStep step, Window activeWindow) { LOGGER.info("Auto-fallback disabled for step: {}, starting node polling", step.title()); stopNodePolling(); nodePollingTimeline = new Timeline(new KeyFrame(Duration.millis(100), _ -> { Scene scene = activeWindow.getScene(); - if (scene != null) { - Optional targetNode = step.resolver().resolve(scene); - if (targetNode.isPresent()) { - LOGGER.info("Target node found for step: {}, displaying step", step.title()); - stopNodePolling(); - - if (step.highlight().isPresent()) { - walkthroughHighlighter.applyHighlight(scene, step.highlight(), targetNode); - } - - SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(activeWindow); - overlay.displayStep(step, targetNode.get(), walkthrough); - } + if (scene == null) { + return; } + Optional targetNode = step.resolver().flatMap(resolver -> resolver.resolve(scene)); + if (targetNode.isEmpty()) { + return; + } + + LOGGER.info("Target node found for step: {}, displaying step", step.title()); + stopNodePolling(); + + walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), targetNode.orElse(null)); + SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(activeWindow); + overlay.displayStep(step, targetNode.get(), walkthrough); })); nodePollingTimeline.setCycleCount(Timeline.INDEFINITE); @@ -118,7 +115,7 @@ private void stopNodePolling() { } } - private @NonNull WalkthroughNode getStepAtIndex(int index) { + private @NonNull WalkthroughStep getStepAtIndex(int index) { return walkthrough.getSteps().get(index); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index 5e500b52f4c..f69411c9bdc 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -14,9 +14,10 @@ import org.jabref.gui.walkthrough.declarative.richtext.ArbitraryJFXBlock; import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; +import org.jabref.gui.walkthrough.declarative.step.PanelPosition; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.TooltipStep; -import org.jabref.gui.walkthrough.declarative.step.WalkthroughNode; +import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.jabref.logic.l10n.Localization; import org.jspecify.annotations.NonNull; @@ -46,7 +47,7 @@ public Node render(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) public Node render(@NonNull PanelStep step, @NonNull Walkthrough walkthrough) { VBox panel = makePanel(); - if (step.position() == Pos.CENTER_LEFT || step.position() == Pos.CENTER_RIGHT) { + if (step.position() == PanelPosition.LEFT || step.position() == PanelPosition.RIGHT) { panel.getStyleClass().add("walkthrough-side-panel-vertical"); VBox.setVgrow(panel, Priority.ALWAYS); panel.setMaxHeight(Double.MAX_VALUE); @@ -56,7 +57,7 @@ public Node render(@NonNull PanelStep step, @NonNull Walkthrough walkthrough) { panel.setMaxWidth(width); panel.setMinWidth(width); }); - } else if (step.position() == Pos.TOP_CENTER || step.position() == Pos.BOTTOM_CENTER) { + } else if (step.position() == PanelPosition.TOP || step.position() == PanelPosition.BOTTOM) { panel.getStyleClass().add("walkthrough-side-panel-horizontal"); HBox.setHgrow(panel, Priority.ALWAYS); panel.setMaxWidth(Double.MAX_VALUE); @@ -126,7 +127,7 @@ private VBox makePanel() { return container; } - private HBox makeActions(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + private HBox makeActions(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); actions.getStyleClass().add("walkthrough-actions"); @@ -152,7 +153,7 @@ private HBox makeActions(@NonNull WalkthroughNode step, @NonNull Walkthrough wal return actions; } - private VBox makeContent(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + private VBox makeContent(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { VBox contentBox = new VBox(); contentBox.getStyleClass().add("walkthrough-content"); contentBox.getChildren().addAll(step.content().stream().map(block -> @@ -165,7 +166,7 @@ private VBox makeContent(@NonNull WalkthroughNode step, @NonNull Walkthrough wal return contentBox; } - private Button makeContinueButton(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + private Button makeContinueButton(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { String buttonText = step.continueButtonText() .orElse("Walkthrough continue button"); @@ -175,7 +176,7 @@ private Button makeContinueButton(@NonNull WalkthroughNode step, @NonNull Walkth return continueButton; } - private Button makeSkipButton(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + private Button makeSkipButton(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { String buttonText = step.skipButtonText() .orElse("Walkthrough skip to finish"); @@ -185,7 +186,7 @@ private Button makeSkipButton(@NonNull WalkthroughNode step, @NonNull Walkthroug return skipButton; } - private Button makeBackButton(@NonNull WalkthroughNode step, @NonNull Walkthrough walkthrough) { + private Button makeBackButton(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { String buttonText = step.backButtonText() .orElse("Walkthrough back button"); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java index fb775df6b9b..a413a11c802 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java @@ -9,15 +9,15 @@ * Represents a highlight effect configuration for a specific window. */ public record WindowEffect( - WindowResolver windowResolver, + Optional windowResolver, HighlightEffect effect, Optional targetNodeResolver ) { public WindowEffect(WindowResolver windowResolver, HighlightEffect effect) { - this(windowResolver, effect, Optional.empty()); + this(Optional.of(windowResolver), effect, Optional.empty()); } public WindowEffect(WindowResolver windowResolver, HighlightEffect effect, NodeResolver targetNodeResolver) { - this(windowResolver, effect, Optional.of(targetNodeResolver)); + this(Optional.of(windowResolver), effect, Optional.of(targetNodeResolver)); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelPosition.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelPosition.java new file mode 100644 index 00000000000..475524a6f64 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelPosition.java @@ -0,0 +1,8 @@ +package org.jabref.gui.walkthrough.declarative.step; + +public enum PanelPosition { + TOP, + BOTTOM, + LEFT, + RIGHT +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index 07aac4fd501..db581b161db 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -2,8 +2,7 @@ import java.util.List; import java.util.Optional; - -import javafx.geometry.Pos; +import java.util.OptionalDouble; import org.jabref.gui.walkthrough.declarative.NavigationPredicate; import org.jabref.gui.walkthrough.declarative.NodeResolver; @@ -14,21 +13,68 @@ import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; public record PanelStep( String title, List content, - NodeResolver resolver, - Optional continueButtonText, - Optional skipButtonText, - Optional backButtonText, - Optional navigationPredicate, - Pos position, - Optional preferredWidth, - Optional preferredHeight, - Optional highlight, + @Nullable NodeResolver resolverValue, + @Nullable String continueButtonTextValue, + @Nullable String skipButtonTextValue, + @Nullable String backButtonTextValue, + @Nullable NavigationPredicate navigationPredicateValue, + PanelPosition position, + @Nullable Double preferredWidthValue, + @Nullable Double preferredHeightValue, + @Nullable MultiWindowHighlight highlightValue, boolean autoFallback, - Optional activeWindowResolver) implements WalkthroughNode { + @Nullable WindowResolver activeWindowResolverValue) implements WalkthroughStep { + + @Override + public Optional resolver() { + return Optional.ofNullable(resolverValue); + } + + @Override + public Optional continueButtonText() { + return Optional.ofNullable(continueButtonTextValue); + } + + @Override + public Optional skipButtonText() { + return Optional.ofNullable(skipButtonTextValue); + } + + @Override + public Optional backButtonText() { + return Optional.ofNullable(backButtonTextValue); + } + + @Override + public Optional navigationPredicate() { + return Optional.ofNullable(navigationPredicateValue); + } + + @Override + public OptionalDouble preferredWidth() { + return preferredWidthValue != null ? OptionalDouble.of(preferredWidthValue) : OptionalDouble.empty(); + } + + @Override + public OptionalDouble preferredHeight() { + return preferredHeightValue != null ? OptionalDouble.of(preferredHeightValue) : OptionalDouble.empty(); + } + + @Override + public Optional highlight() { + return Optional.ofNullable(highlightValue); + } + + @Override + public Optional activeWindowResolver() { + return Optional.ofNullable(activeWindowResolverValue); + } + public static Builder builder(String title) { return new Builder(title); } @@ -36,28 +82,28 @@ public static Builder builder(String title) { public static class Builder { private final String title; private List content = List.of(); - private NodeResolver resolver; - private Optional continueButtonText = Optional.empty(); - private Optional skipButtonText = Optional.empty(); - private Optional backButtonText = Optional.empty(); - private Optional navigationPredicate = Optional.empty(); - private Pos position = Pos.CENTER; - private Optional preferredWidth = Optional.empty(); - private Optional preferredHeight = Optional.empty(); - private Optional highlight = Optional.empty(); + private @Nullable NodeResolver resolver; + private @Nullable String continueButtonText; + private @Nullable String skipButtonText; + private @Nullable String backButtonText; + private @Nullable NavigationPredicate navigationPredicate; + private PanelPosition position = PanelPosition.LEFT; + private @Nullable Double preferredWidth; + private @Nullable Double preferredHeight; + private @Nullable MultiWindowHighlight highlight; private boolean autoFallback = true; - private Optional activeWindowResolver = Optional.empty(); + private @Nullable WindowResolver activeWindowResolver; - private Builder(String title) { + private Builder(@NonNull String title) { this.title = title; } - public Builder content(WalkthroughRichTextBlock... blocks) { + public Builder content(@NonNull WalkthroughRichTextBlock... blocks) { this.content = List.of(blocks); return this; } - public Builder content(List content) { + public Builder content(@NonNull List content) { this.content = content; return this; } @@ -68,47 +114,47 @@ public Builder resolver(@NonNull NodeResolver resolver) { } public Builder continueButton(@NonNull String text) { - this.continueButtonText = Optional.of(text); + this.continueButtonText = text; return this; } public Builder skipButton(@NonNull String text) { - this.skipButtonText = Optional.of(text); + this.skipButtonText = text; return this; } public Builder backButton(@NonNull String text) { - this.backButtonText = Optional.of(text); + this.backButtonText = text; return this; } public Builder navigation(@NonNull NavigationPredicate navigationPredicate) { - this.navigationPredicate = Optional.of(navigationPredicate); + this.navigationPredicate = navigationPredicate; return this; } - public Builder position(@NonNull Pos position) { + public Builder position(@NonNull PanelPosition position) { this.position = position; return this; } public Builder preferredWidth(double width) { - this.preferredWidth = Optional.of(width); + this.preferredWidth = width; return this; } public Builder preferredHeight(double height) { - this.preferredHeight = Optional.of(height); + this.preferredHeight = height; return this; } public Builder highlight(@NonNull MultiWindowHighlight highlight) { - this.highlight = Optional.of(highlight); + this.highlight = highlight; return this; } public Builder highlight(@NonNull HighlightEffect effect) { - this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(Optional::empty), effect, Optional.empty()))); + this.highlight = MultiWindowHighlight.single(new WindowEffect(activeWindowResolver, effect)); return this; } @@ -118,14 +164,11 @@ public Builder autoFallback(boolean autoFallback) { } public Builder activeWindow(@NonNull WindowResolver activeWindowResolver) { - this.activeWindowResolver = Optional.of(activeWindowResolver); + this.activeWindowResolver = activeWindowResolver; return this; } public PanelStep build() { - if (resolver == null) { - throw new IllegalStateException("Node resolver is required for PanelStep"); - } return new PanelStep(title, content, resolver, diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index 201b471974a..dea35d1712b 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.OptionalDouble; import org.jabref.gui.walkthrough.declarative.NavigationPredicate; import org.jabref.gui.walkthrough.declarative.NodeResolver; @@ -13,22 +14,69 @@ import org.jabref.logic.l10n.Localization; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; public record TooltipStep( String title, List content, - NodeResolver resolver, - Optional continueButtonText, - Optional skipButtonText, - Optional backButtonText, - Optional navigationPredicate, + @Nullable NodeResolver resolverValue, + @Nullable String continueButtonTextValue, + @Nullable String skipButtonTextValue, + @Nullable String backButtonTextValue, + @Nullable NavigationPredicate navigationPredicateValue, TooltipPosition position, - Optional preferredWidth, - Optional preferredHeight, - Optional highlight, + @Nullable Double preferredWidthValue, + @Nullable Double preferredHeightValue, + @Nullable MultiWindowHighlight highlightValue, boolean autoFallback, - Optional activeWindowResolver -) implements WalkthroughNode { + @Nullable WindowResolver activeWindowResolverValue +) implements WalkthroughStep { + + @Override + public Optional resolver() { + return Optional.ofNullable(resolverValue); + } + + @Override + public Optional continueButtonText() { + return Optional.ofNullable(continueButtonTextValue); + } + + @Override + public Optional skipButtonText() { + return Optional.ofNullable(skipButtonTextValue); + } + + @Override + public Optional backButtonText() { + return Optional.ofNullable(backButtonTextValue); + } + + @Override + public Optional navigationPredicate() { + return Optional.ofNullable(navigationPredicateValue); + } + + @Override + public OptionalDouble preferredWidth() { + return preferredWidthValue != null ? OptionalDouble.of(preferredWidthValue) : OptionalDouble.empty(); + } + + @Override + public OptionalDouble preferredHeight() { + return preferredHeightValue != null ? OptionalDouble.of(preferredHeightValue) : OptionalDouble.empty(); + } + + @Override + public Optional highlight() { + return Optional.ofNullable(highlightValue); + } + + @Override + public Optional activeWindowResolver() { + return Optional.ofNullable(activeWindowResolverValue); + } + public static Builder builder(String key, Object... params) { return new Builder(Localization.lang(key, params)); } @@ -36,28 +84,28 @@ public static Builder builder(String key, Object... params) { public static class Builder { private final String title; private List content = List.of(); - private NodeResolver resolver; - private Optional continueButtonText = Optional.empty(); - private Optional skipButtonText = Optional.empty(); - private Optional backButtonText = Optional.empty(); - private Optional navigationPredicate = Optional.empty(); + private @Nullable NodeResolver resolver; + private @Nullable String continueButtonText; + private @Nullable String skipButtonText; + private @Nullable String backButtonText; + private @Nullable NavigationPredicate navigationPredicate; private TooltipPosition position = TooltipPosition.AUTO; - private Optional preferredWidth = Optional.empty(); - private Optional preferredHeight = Optional.empty(); - private Optional highlight = Optional.empty(); + private @Nullable Double preferredWidth; + private @Nullable Double preferredHeight; + private @Nullable MultiWindowHighlight highlight; private boolean autoFallback = true; - private Optional activeWindowResolver = Optional.empty(); + private @Nullable WindowResolver activeWindowResolver; - private Builder(String title) { + private Builder(@NonNull String title) { this.title = title; } - public Builder content(WalkthroughRichTextBlock... blocks) { + public Builder content(@NonNull WalkthroughRichTextBlock... blocks) { this.content = List.of(blocks); return this; } - public Builder content(List content) { + public Builder content(@NonNull List content) { this.content = content; return this; } @@ -68,22 +116,22 @@ public Builder resolver(@NonNull NodeResolver resolver) { } public Builder continueButton(@NonNull String text) { - this.continueButtonText = Optional.of(text); + this.continueButtonText = text; return this; } public Builder skipButton(@NonNull String text) { - this.skipButtonText = Optional.of(text); + this.skipButtonText = text; return this; } public Builder backButton(@NonNull String text) { - this.backButtonText = Optional.of(text); + this.backButtonText = text; return this; } public Builder navigation(@NonNull NavigationPredicate navigationPredicate) { - this.navigationPredicate = Optional.of(navigationPredicate); + this.navigationPredicate = navigationPredicate; return this; } @@ -93,22 +141,22 @@ public Builder position(@NonNull TooltipPosition position) { } public Builder preferredWidth(double width) { - this.preferredWidth = Optional.of(width); + this.preferredWidth = width; return this; } public Builder preferredHeight(double height) { - this.preferredHeight = Optional.of(height); + this.preferredHeight = height; return this; } public Builder highlight(@NonNull MultiWindowHighlight highlight) { - this.highlight = Optional.of(highlight); + this.highlight = highlight; return this; } public Builder highlight(@NonNull HighlightEffect effect) { - this.highlight = Optional.of(MultiWindowHighlight.single(new WindowEffect(activeWindowResolver.orElse(Optional::empty), effect, Optional.empty()))); + this.highlight = MultiWindowHighlight.single(new WindowEffect(activeWindowResolver, effect)); return this; } @@ -118,7 +166,7 @@ public Builder autoFallback(boolean autoFallback) { } public Builder activeWindow(@NonNull WindowResolver activeWindowResolver) { - this.activeWindowResolver = Optional.of(activeWindowResolver); + this.activeWindowResolver = activeWindowResolver; return this; } @@ -142,4 +190,3 @@ public TooltipStep build() { } } } - diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java similarity index 82% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java index c5674d1641c..d780f6ca808 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughNode.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.OptionalDouble; import org.jabref.gui.walkthrough.declarative.NavigationPredicate; import org.jabref.gui.walkthrough.declarative.NodeResolver; @@ -9,13 +10,12 @@ import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; -public sealed interface WalkthroughNode permits PanelStep, TooltipStep { +public sealed interface WalkthroughStep permits PanelStep, TooltipStep { String title(); List content(); - // FIXME: Refactor to make this optional - NodeResolver resolver(); + Optional resolver(); Optional continueButtonText(); @@ -25,9 +25,9 @@ public sealed interface WalkthroughNode permits PanelStep, TooltipStep { Optional navigationPredicate(); - Optional preferredWidth(); + OptionalDouble preferredWidth(); - Optional preferredHeight(); + OptionalDouble preferredHeight(); boolean autoFallback(); From 570bfabd4c83b2c537b7fbdeff1f372862e06d97 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Thu, 19 Jun 2025 11:06:52 -0400 Subject: [PATCH 25/50] Refactor highlight effect --- .../jabref/gui/actions/StandardActions.java | 3 +- .../java/org/jabref/gui/frame/MainMenu.java | 10 ++++--- .../gui/walkthrough/WalkthroughAction.java | 28 ++++++++----------- .../declarative/WindowResolver.java | 14 ++++++++++ .../effect/MultiWindowHighlight.java | 20 +++++++------ .../declarative/effect/WindowEffect.java | 4 +++ .../declarative/step/PanelStep.java | 7 +++-- .../declarative/step/TooltipStep.java | 7 +++-- .../main/resources/l10n/JabRef_en.properties | 1 + 9 files changed, 60 insertions(+), 34 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 814610880ca..f518196c95f 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -184,9 +184,8 @@ public enum StandardActions implements Action { OPEN_DEV_VERSION_LINK(Localization.lang("Development version"), Localization.lang("Opens a link where the current development version can be downloaded")), OPEN_CHANGELOG(Localization.lang("View change log"), Localization.lang("See what has been changed in the JabRef versions")), OPEN_GITHUB("GitHub", Localization.lang("Opens JabRef's GitHub page"), IconTheme.JabRefIcons.GITHUB), - WALKTHROUGH_MENU(Localization.lang("Walkthroughs")), + WALKTHROUGH_MENU(Localization.lang("Walkthroughs"), IconTheme.JabRefIcons.BOOK), MAIN_FILE_DIRECTORY_WALKTHROUGH(Localization.lang("Configure Main File Directory"), IconTheme.JabRefIcons.LATEX_FILE_DIRECTORY), - EDIT_ENTRY_WALKTHROUGH(Localization.lang("Edit entry"), IconTheme.JabRefIcons.EDIT_ENTRY), DONATE(Localization.lang("Donate to JabRef"), Localization.lang("Donate to JabRef"), IconTheme.JabRefIcons.DONATE), OPEN_FORUM(Localization.lang("Community forum"), Localization.lang("Community forum"), IconTheme.JabRefIcons.FORUM), ERROR_CONSOLE(Localization.lang("View event log"), Localization.lang("Display all error messages")), diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 1cbb64eb515..5a408b4af98 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -356,6 +356,12 @@ private void createMenu() { new SeparatorMenuItem(), + factory.createSubMenu(StandardActions.WALKTHROUGH_MENU, + factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory", frame)) + ), + + new SeparatorMenuItem(), + factory.createMenuItem(StandardActions.ERROR_CONSOLE, new ErrorConsoleAction()), new SeparatorMenuItem(), @@ -375,10 +381,6 @@ private void createMenu() { factory.createMenuItem(StandardActions.OPEN_DEV_VERSION_LINK, new OpenBrowserAction(URLs.DEV_VERSION_LINK_URL, dialogService, preferences.getExternalApplicationsPreferences())), factory.createMenuItem(StandardActions.OPEN_CHANGELOG, new OpenBrowserAction(URLs.CHANGELOG_URL, dialogService, preferences.getExternalApplicationsPreferences())) ), - factory.createSubMenu(StandardActions.WALKTHROUGH_MENU, - factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory", frame)), - factory.createMenuItem(StandardActions.EDIT_ENTRY_WALKTHROUGH, new WalkthroughAction("editEntry", frame)) - ), factory.createMenuItem(StandardActions.OPEN_WELCOME_TAB, new SimpleCommand() { @Override diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 9dfcfbcb404..6a41e779628 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -2,10 +2,8 @@ import java.util.HashMap; import java.util.Map; -import java.util.Optional; import javafx.scene.control.ContextMenu; -import javafx.stage.Window; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.frame.JabRefFrame; @@ -55,15 +53,13 @@ private static Map buildRegistry() { .resolver(NodeResolver.menuItem("Preferences")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.RIGHT) - .highlight(MultiWindowHighlight.multiple( - new WindowEffect(Optional::empty, HighlightEffect.FULL_SCREEN_DARKEN), + .highlight(new MultiWindowHighlight( new WindowEffect( - () -> Window.getWindows().stream() - .filter(w -> w instanceof ContextMenu cm && cm.isShowing()) - .findFirst(), + WindowResolver.clazz(ContextMenu.class), HighlightEffect.ANIMATED_PULSE, NodeResolver.menuItem("Preferences") - ) + ), + new WindowEffect(HighlightEffect.FULL_SCREEN_DARKEN) )) .build(); @@ -74,12 +70,12 @@ private static Map buildRegistry() { node.toString().contains("Linked files"))) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) - .highlight(MultiWindowHighlight.multiple( - new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), - new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) - )) .autoFallback(false) .activeWindow(WindowResolver.title("JabRef preferences")) + .highlight(new MultiWindowHighlight( + new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) + )) .build(); WalkthroughStep step4 = TooltipStep @@ -87,8 +83,8 @@ private static Map buildRegistry() { .resolver(NodeResolver.fxId("useMainFileDirectory")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) - .highlight(MultiWindowHighlight.multiple( - new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), + .highlight(new MultiWindowHighlight( + new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) )) .activeWindow(WindowResolver.title("JabRef preferences")) @@ -99,8 +95,8 @@ private static Map buildRegistry() { .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("button") && node.toString().contains(Localization.lang("Save")))) .navigation(NavigationPredicate.onClick()) .position(PanelPosition.TOP) - .highlight(MultiWindowHighlight.multiple( - new WindowEffect(Optional::empty, HighlightEffect.BACKDROP_HIGHLIGHT), + .highlight(new MultiWindowHighlight( + new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) )) .activeWindow(WindowResolver.title("JabRef preferences")) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java index 714aef718fa..8a87f16043e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/WindowResolver.java @@ -29,10 +29,24 @@ public interface WindowResolver { */ static WindowResolver title(@NonNull String key) { return () -> Window.getWindows().stream() + .filter(Window::isShowing) .filter(Stage.class::isInstance) .map(Stage.class::cast) .filter(stage -> stage.getTitle().contains(Localization.lang(key))) .map(Window.class::cast) .findFirst(); } + + /** + * Create a resolver that finds a window by its class. + * + * @param clazz the class of the window + * @return a resolver that finds the window by class + */ + static WindowResolver clazz(@NonNull Class clazz) { + return () -> Window.getWindows().stream() + .filter(clazz::isInstance) + .filter(Window::isShowing) + .findFirst(); + } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java index 7406e4902ea..57fb6efda45 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java @@ -10,19 +10,23 @@ public record MultiWindowHighlight( List windowEffects, Optional fallbackEffect ) { - public static MultiWindowHighlight single(WindowEffect windowEffect) { - return single(windowEffect, HighlightEffect.FULL_SCREEN_DARKEN); + public MultiWindowHighlight(HighlightEffect effect) { + this(new WindowEffect(effect)); } - public static MultiWindowHighlight single(WindowEffect windowEffect, HighlightEffect fallback) { - return new MultiWindowHighlight(List.of(windowEffect), Optional.of(fallback)); + public MultiWindowHighlight(WindowEffect windowEffect) { + this(windowEffect, HighlightEffect.FULL_SCREEN_DARKEN); } - public static MultiWindowHighlight multiple(WindowEffect... windowEffects) { - return multiple(HighlightEffect.FULL_SCREEN_DARKEN, windowEffects); + public MultiWindowHighlight(WindowEffect windowEffect, HighlightEffect fallback) { + this(List.of(windowEffect), Optional.of(fallback)); } - public static MultiWindowHighlight multiple(HighlightEffect fallback, WindowEffect... windowEffects) { - return new MultiWindowHighlight(List.of(windowEffects), Optional.of(fallback)); + public MultiWindowHighlight(WindowEffect... windowEffects) { + this(HighlightEffect.FULL_SCREEN_DARKEN, windowEffects); + } + + public MultiWindowHighlight(HighlightEffect fallback, WindowEffect... windowEffects) { + this(List.of(windowEffects), Optional.of(fallback)); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java index a413a11c802..935a7562271 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java @@ -13,6 +13,10 @@ public record WindowEffect( HighlightEffect effect, Optional targetNodeResolver ) { + public WindowEffect(HighlightEffect effect) { + this(Optional.empty(), effect, Optional.empty()); + } + public WindowEffect(WindowResolver windowResolver, HighlightEffect effect) { this(Optional.of(windowResolver), effect, Optional.empty()); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index db581b161db..75865d4a5be 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -153,9 +153,12 @@ public Builder highlight(@NonNull MultiWindowHighlight highlight) { return this; } + public Builder highlight(@NonNull WindowEffect effect) { + return highlight(new MultiWindowHighlight(effect)); + } + public Builder highlight(@NonNull HighlightEffect effect) { - this.highlight = MultiWindowHighlight.single(new WindowEffect(activeWindowResolver, effect)); - return this; + return highlight(new WindowEffect(effect)); } public Builder autoFallback(boolean autoFallback) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index dea35d1712b..955e1a72c52 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -155,9 +155,12 @@ public Builder highlight(@NonNull MultiWindowHighlight highlight) { return this; } + public Builder highlight(@NonNull WindowEffect effect) { + return highlight(new MultiWindowHighlight(effect)); + } + public Builder highlight(@NonNull HighlightEffect effect) { - this.highlight = MultiWindowHighlight.single(new WindowEffect(activeWindowResolver, effect)); - return this; + return highlight(new WindowEffect(effect)); } public Builder autoFallback(boolean autoFallback) { diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 1d20e5fdc6b..ccf1b21828c 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2932,6 +2932,7 @@ You\ must\ specify\ an\ identifier.=You must specify an identifier. You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) citations. # Walkthrough +Walkthroughs=Walkthroughs Saving\ Your\ Work=Saving Your Work Skip\ for\ Now=Skip for Now Browse...=Browse... From d5eb6fbad82c47c7f1a6e093054a8ad018586893 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 10:08:10 -0400 Subject: [PATCH 26/50] Add before navigate hook to prevent unwanted revert --- .../SingleWindowWalkthroughOverlay.java | 101 ++++++----- .../jabref/gui/walkthrough/Walkthrough.java | 1 + .../gui/walkthrough/WalkthroughAction.java | 9 +- .../gui/walkthrough/WalkthroughOverlay.java | 104 +++++++---- .../gui/walkthrough/WalkthroughRenderer.java | 163 ++++++++---------- .../components/FullScreenDarken.java | 16 +- .../components/WalkthroughEffect.java | 21 +++ .../declarative/NavigationPredicate.java | 77 ++++++--- .../walkthrough/declarative/NodeResolver.java | 34 ++-- .../richtext/ArbitraryJFXBlock.java | 5 +- 10 files changed, 312 insertions(+), 219 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 69ac1eb752e..e17d53cc3c0 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import javafx.beans.value.ChangeListener; import javafx.geometry.Bounds; @@ -27,6 +28,7 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javafx.application.Platform; /** * Manages the overlay for displaying walkthrough steps in a single window. @@ -36,11 +38,12 @@ public class SingleWindowWalkthroughOverlay { private final Window window; private final GridPane overlayPane; + private final PopOver popover; private final Pane originalRoot; private final StackPane stackPane; private final WalkthroughRenderer renderer; private final List cleanUpTasks = new ArrayList<>(); - private PopOver currentPopOver; + private @Nullable Node node; public SingleWindowWalkthroughOverlay(Window window) { this.window = window; @@ -52,6 +55,8 @@ public SingleWindowWalkthroughOverlay(Window window) { overlayPane.setMaxWidth(Double.MAX_VALUE); overlayPane.setMaxHeight(Double.MAX_VALUE); + popover = new PopOver(); + Scene scene = window.getScene(); assert scene != null; @@ -67,20 +72,17 @@ public SingleWindowWalkthroughOverlay(Window window) { /** * Displays a walkthrough step with the specified target node. */ - public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Walkthrough walkthrough) { + public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { hide(); - displayStepContent(step, targetNode, walkthrough); - overlayPane.toFront(); + displayStepContent(step, targetNode, beforeNavigate, walkthrough); + node = targetNode; } /** * Hide the overlay and clean up any resources. */ public void hide() { - if (currentPopOver != null) { - currentPopOver.hide(); - currentPopOver = null; - } + popover.hide(); overlayPane.getChildren().clear(); overlayPane.setClip(null); @@ -104,54 +106,75 @@ public void detach() { } } - private void displayStepContent(WalkthroughStep step, Node targetNode, Walkthrough walkthrough) { + private void displayStepContent(WalkthroughStep step, Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { switch (step) { case TooltipStep tooltipStep -> { - displayTooltipStep(tooltipStep, targetNode, walkthrough); + Node content = renderer.render(tooltipStep, walkthrough, beforeNavigate); + displayTooltipStep(content, targetNode, tooltipStep); hideOverlayPane(); } case PanelStep panelStep -> { - Node content = renderer.render(panelStep, walkthrough); + Node content = renderer.render(panelStep, walkthrough, beforeNavigate); displayPanelStep(content, panelStep); setupClipping(content); + overlayPane.toFront(); } } - step.navigationPredicate().ifPresent(predicate -> - cleanUpTasks.add(predicate.attachListeners(targetNode, walkthrough::nextStep))); + step.navigationPredicate().ifPresent(predicate -> { + if (targetNode == null) { + return; + } + cleanUpTasks.add(predicate.attachListeners(targetNode, beforeNavigate, walkthrough::nextStep)); + }); } - private void displayTooltipStep(TooltipStep step, Node targetNode, Walkthrough walkthrough) { - Node content = renderer.render(step, walkthrough); - - currentPopOver = new PopOver(); - currentPopOver.setContentNode(content); - currentPopOver.setDetachable(false); - currentPopOver.setCloseButtonEnabled(false); - currentPopOver.setHeaderAlwaysVisible(false); - - PopOver.ArrowLocation arrowLocation = mapToArrowLocation(step.position()); - if (arrowLocation != null) { - currentPopOver.setArrowLocation(arrowLocation); - } + private void displayTooltipStep(Node content, @Nullable Node targetNode, TooltipStep step) { + popover.setContentNode(content); + popover.setDetachable(false); + popover.setCloseButtonEnabled(false); + popover.setHeaderAlwaysVisible(false); + popover.setAutoFix(true); + popover.setAutoHide(false); + mapToArrowLocation(step.position()).ifPresent(popover::setArrowLocation); step.preferredWidth().ifPresent(width -> { - currentPopOver.setPrefWidth(width); - currentPopOver.setMinWidth(width); + popover.setPrefWidth(width); + popover.setMinWidth(width); }); step.preferredHeight().ifPresent(height -> { - currentPopOver.setPrefHeight(height); - currentPopOver.setMinHeight(height); + popover.setPrefHeight(height); + popover.setMinHeight(height); }); - currentPopOver.show(targetNode); - - cleanUpTasks.add(() -> { - if (currentPopOver != null) { - currentPopOver.hide(); - currentPopOver = null; + // Defer showing the popover until the next pulse to ensure the + // target node (or window) has been fully laid out. This prevents + // situations where the pop-over fails to appear because the node + // is not yet ready (for example directly after a scene change). + Platform.runLater(() -> { + if (targetNode != null) { + popover.show(targetNode); + } else { + popover.show(window); } }); + + ChangeListener listener = (_, _, focused) -> { + if (focused && !popover.isShowing()) { + LOGGER.debug("Window gained focus, ensuring tooltip is visible"); + if (node != null) { + popover.show(node); + } else { + popover.show(window); + } + } + }; + + window.focusedProperty().addListener(listener); + cleanUpTasks.add(() -> window.focusedProperty().removeListener(listener)); + popover.showingProperty().addListener(listener); + cleanUpTasks.add(() -> popover.showingProperty().removeListener(listener)); + cleanUpTasks.add(popover::hide); } private void displayPanelStep(Node content, PanelStep step) { @@ -218,8 +241,8 @@ private void configurePanelLayout(PanelPosition position) { overlayPane.getColumnConstraints().add(columnConstraints); } - private PopOver.ArrowLocation mapToArrowLocation(TooltipPosition position) { - return switch (position) { + private Optional mapToArrowLocation(TooltipPosition position) { + return Optional.ofNullable(switch (position) { case TOP -> PopOver.ArrowLocation.BOTTOM_CENTER; case BOTTOM -> @@ -230,7 +253,7 @@ private PopOver.ArrowLocation mapToArrowLocation(TooltipPosition position) { PopOver.ArrowLocation.LEFT_CENTER; case AUTO -> null; - }; + }); } private void hideOverlayPane() { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 6a52dbf53b9..86acff125cc 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -64,6 +64,7 @@ public void start(Stage stage) { if (currentStage != stage) { if (overlay != null) { overlay.detachAll(); + overlay = null; } currentStage = stage; overlay = new WalkthroughOverlay(stage, this); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 6a41e779628..1ec07dee8c1 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -53,13 +53,10 @@ private static Map buildRegistry() { .resolver(NodeResolver.menuItem("Preferences")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.RIGHT) + .activeWindow(WindowResolver.clazz(ContextMenu.class)) .highlight(new MultiWindowHighlight( - new WindowEffect( - WindowResolver.clazz(ContextMenu.class), - HighlightEffect.ANIMATED_PULSE, - NodeResolver.menuItem("Preferences") - ), - new WindowEffect(HighlightEffect.FULL_SCREEN_DARKEN) + new WindowEffect(HighlightEffect.ANIMATED_PULSE), + new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) )) .build(); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 7839ce2f025..df6f4031dd0 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import javafx.animation.KeyFrame; import javafx.animation.Timeline; @@ -16,6 +17,7 @@ import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,37 +28,38 @@ public class WalkthroughOverlay { private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); private final Map overlays = new HashMap<>(); - private final Stage mainStage; + private final Stage stage; private final WalkthroughHighlighter walkthroughHighlighter; private final Walkthrough walkthrough; - private Timeline nodePollingTimeline; + private @Nullable Timeline nodePollingTimeline; - public WalkthroughOverlay(Stage mainStage, Walkthrough walkthrough) { - this.mainStage = mainStage; + public WalkthroughOverlay(Stage stage, Walkthrough walkthrough) { + this.stage = stage; this.walkthrough = walkthrough; this.walkthroughHighlighter = new WalkthroughHighlighter(); } + /** + * Displays the specified walkthrough step in the appropriate window. + */ public void displayStep(@NonNull WalkthroughStep step) { overlays.values().forEach(SingleWindowWalkthroughOverlay::hide); - Window activeWindow = step.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); - Scene scene = activeWindow.getScene(); + Window window = step.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(stage); + Scene scene = window.getScene(); Optional targetNode = step.resolver().flatMap(resolver -> resolver.resolve(scene)); - if (step.resolver().isPresent() && targetNode.isEmpty()) { - if (step.autoFallback()) { - tryRevertToPreviousResolvableStep(); - } else { - startNodePolling(step, activeWindow); - } - return; - } - walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), targetNode.orElse(null)); - SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(activeWindow); - overlay.displayStep(step, targetNode.orElse(null), walkthrough); + if (step.resolver().isPresent()) { + startNodePolling(step, window, targetNode.orElse(null)); + } else { + walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), null); + displayStep(step, window, null); + } } + /** + * Detaches all overlays + */ public void detachAll() { stopNodePolling(); walkthroughHighlighter.detachAll(); @@ -68,9 +71,10 @@ private void tryRevertToPreviousResolvableStep() { LOGGER.info("Attempting to revert to previous resolvable step"); int currentIndex = walkthrough.currentStepProperty().get(); + for (int i = currentIndex - 1; i >= 0; i--) { WalkthroughStep previousStep = getStepAtIndex(i); - Window activeWindow = previousStep.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(mainStage); + Window activeWindow = previousStep.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(stage); Scene scene = activeWindow.getScene(); if (scene != null && (previousStep.resolver().isEmpty() || previousStep.resolver().get().resolve(scene).isPresent())) { LOGGER.info("Reverting to step {} from step {}", i, currentIndex); @@ -82,26 +86,54 @@ private void tryRevertToPreviousResolvableStep() { LOGGER.warn("No previous resolvable step found, staying at current step"); } - private void startNodePolling(WalkthroughStep step, Window activeWindow) { - LOGGER.info("Auto-fallback disabled for step: {}, starting node polling", step.title()); + private void displayStep(WalkthroughStep step, Window window, @Nullable Node node) { + getOrCreateOverlay(window).displayStep(step, node, this::stopNodePolling, walkthrough); + } + + private void startNodePolling(WalkthroughStep step, Window window, @Nullable Node node) { stopNodePolling(); - nodePollingTimeline = new Timeline(new KeyFrame(Duration.millis(100), _ -> { - Scene scene = activeWindow.getScene(); + AtomicBoolean nodeEverResolved = new AtomicBoolean(node != null); + + Scene initialScene = window.getScene(); + + walkthroughHighlighter.applyHighlight(initialScene, step.highlight().orElse(null), node); + displayStep(step, window, node); + + LOGGER.info("Starting continuous node polling for step: {}", step.title()); + + nodePollingTimeline = new Timeline(new KeyFrame(Duration.millis(500), _ -> { + Scene scene = window.getScene(); if (scene == null) { return; } - Optional targetNode = step.resolver().flatMap(resolver -> resolver.resolve(scene)); - if (targetNode.isEmpty()) { - return; - } - LOGGER.info("Target node found for step: {}, displaying step", step.title()); - stopNodePolling(); - - walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), targetNode.orElse(null)); - SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(activeWindow); - overlay.displayStep(step, targetNode.get(), walkthrough); + step.resolver().flatMap(resolver -> resolver.resolve(scene)).ifPresentOrElse( + (currentNode) -> { + if (!nodeEverResolved.get()) { + LOGGER.info("Target node found for step: {}, updating display", step.title()); + walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), currentNode); + displayStep(step, window, currentNode); + nodeEverResolved.set(true); + } + }, + () -> { + if (!nodeEverResolved.get()) { + return; + } + + if (step.autoFallback()) { + LOGGER.info("Node disappeared for step: {}, auto-falling back", step.title()); + stopNodePolling(); + tryRevertToPreviousResolvableStep(); + } else { + LOGGER.info("Node disappeared for step: {}, showing step without node", step.title()); + walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), null); + displayStep(step, window, null); + nodeEverResolved.set(false); + } + } + ); })); nodePollingTimeline.setCycleCount(Timeline.INDEFINITE); @@ -109,10 +141,12 @@ private void startNodePolling(WalkthroughStep step, Window activeWindow) { } private void stopNodePolling() { - if (nodePollingTimeline != null) { - nodePollingTimeline.stop(); - nodePollingTimeline = null; + LOGGER.info("Stopping node polling for step."); + if (nodePollingTimeline == null) { + return; } + nodePollingTimeline.stop(); + nodePollingTimeline = null; } private @NonNull WalkthroughStep getStepAtIndex(int index) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index f69411c9bdc..cbc9cdde599 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -20,8 +20,6 @@ import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.jabref.logic.l10n.Localization; -import org.jspecify.annotations.NonNull; - /** * Renders the walkthrough steps and content blocks into JavaFX Nodes. */ @@ -29,29 +27,58 @@ public class WalkthroughRenderer { /** * Renders a tooltip step into a JavaFX Node. * - * @param step The tooltip step to render - * @param walkthrough The walkthrough context for navigation + * @param step The tooltip step to render + * @param walkthrough The walkthrough context for navigation + * @param beforeNavigate Runnable to execute before any navigation action * @return The rendered tooltip content Node */ - public Node render(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) { - return createTooltipContent(step, walkthrough); + public Node render(TooltipStep step, Walkthrough walkthrough, Runnable beforeNavigate) { + VBox tooltip = new VBox(); + tooltip.getStyleClass().add("walkthrough-tooltip-content-container"); + + Label titleLabel = new Label(Localization.lang(step.title())); + titleLabel.getStyleClass().add("walkthrough-tooltip-title"); + + VBox contentContainer = makeContent(step, walkthrough, beforeNavigate); + contentContainer.getStyleClass().add("walkthrough-tooltip-content"); + VBox.setVgrow(contentContainer, Priority.ALWAYS); + + HBox actionsContainer = makeActions(step, walkthrough, beforeNavigate); + actionsContainer.getStyleClass().add("walkthrough-tooltip-actions"); + + tooltip.getChildren().addAll(titleLabel, contentContainer, actionsContainer); + return tooltip; } /** * Renders a panel step into a JavaFX Node. * - * @param step The panel step to render - * @param walkthrough The walkthrough context for navigation + * @param step The panel step to render + * @param walkthrough The walkthrough context for navigation + * @param beforeNavigate Runnable to execute before any navigation action * @return The rendered panel Node */ - public Node render(@NonNull PanelStep step, @NonNull Walkthrough walkthrough) { + public Node render(PanelStep step, Walkthrough walkthrough, Runnable beforeNavigate) { VBox panel = makePanel(); + configurePanelSize(panel, step); - if (step.position() == PanelPosition.LEFT || step.position() == PanelPosition.RIGHT) { + Label titleLabel = new Label(Localization.lang(step.title())); + titleLabel.getStyleClass().add("walkthrough-title"); + + VBox contentContainer = makeContent(step, walkthrough, beforeNavigate); + HBox actionsContainer = makeActions(step, walkthrough, beforeNavigate); + + panel.getChildren().addAll(titleLabel, contentContainer, actionsContainer); + return panel; + } + + private void configurePanelSize(VBox panel, PanelStep step) { + boolean isVertical = step.position() == PanelPosition.LEFT || step.position() == PanelPosition.RIGHT; + + if (isVertical) { panel.getStyleClass().add("walkthrough-side-panel-vertical"); VBox.setVgrow(panel, Priority.ALWAYS); panel.setMaxHeight(Double.MAX_VALUE); - step.preferredWidth().ifPresent(width -> { panel.setPrefWidth(width); panel.setMaxWidth(width); @@ -61,64 +88,32 @@ public Node render(@NonNull PanelStep step, @NonNull Walkthrough walkthrough) { panel.getStyleClass().add("walkthrough-side-panel-horizontal"); HBox.setHgrow(panel, Priority.ALWAYS); panel.setMaxWidth(Double.MAX_VALUE); - step.preferredHeight().ifPresent(height -> { panel.setPrefHeight(height); panel.setMaxHeight(height); panel.setMinHeight(height); }); } - - Label titleLabel = new Label(Localization.lang(step.title())); - titleLabel.getStyleClass().add("walkthrough-title"); - - VBox contentContainer = makeContent(step, walkthrough); - HBox actionsContainer = makeActions(step, walkthrough); - - panel.getChildren().addAll(titleLabel, contentContainer, actionsContainer); - - return panel; - } - - private Node createTooltipContent(@NonNull TooltipStep step, @NonNull Walkthrough walkthrough) { - VBox tooltip = new VBox(); - tooltip.getStyleClass().add("walkthrough-tooltip-content-container"); - - Label titleLabel = new Label(Localization.lang(step.title())); - titleLabel.getStyleClass().add("walkthrough-tooltip-title"); - - VBox contentContainer = makeContent(step, walkthrough); - contentContainer.getStyleClass().add("walkthrough-tooltip-content"); - VBox.setVgrow(contentContainer, Priority.ALWAYS); - - HBox actionsContainer = makeActions(step, walkthrough); - actionsContainer.getStyleClass().add("walkthrough-tooltip-actions"); - - tooltip.getChildren().addAll(titleLabel, contentContainer, actionsContainer); - - return tooltip; } - private Node render(@NonNull ArbitraryJFXBlock block, @NonNull Walkthrough walkthrough) { - return block.componentFactory().apply(walkthrough); + private Node render(ArbitraryJFXBlock block, Walkthrough walkthrough, Runnable beforeNavigate) { + return block.componentFactory().apply(walkthrough, beforeNavigate); } - private Node render(@NonNull TextBlock textBlock) { + private Node render(TextBlock textBlock) { Label textLabel = new Label(Localization.lang(textBlock.text())); textLabel.getStyleClass().add("walkthrough-text-content"); return textLabel; } - private Node render(@NonNull InfoBlock infoBlock) { + private Node render(InfoBlock infoBlock) { HBox infoContainer = new HBox(); infoContainer.getStyleClass().add("walkthrough-info-container"); JabRefIconView icon = new JabRefIconView(IconTheme.JabRefIcons.INTEGRITY_INFO); Label infoLabel = new Label(Localization.lang(infoBlock.text())); HBox.setHgrow(infoLabel, Priority.ALWAYS); infoContainer.getChildren().addAll(icon, infoLabel); - VBox infoWrapper = new VBox(infoContainer); - infoWrapper.setAlignment(Pos.CENTER_LEFT); - return infoWrapper; + return infoContainer; } private VBox makePanel() { @@ -127,7 +122,7 @@ private VBox makePanel() { return container; } - private HBox makeActions(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { + private HBox makeActions(WalkthroughStep step, Walkthrough walkthrough, Runnable beforeNavigate) { HBox actions = new HBox(); actions.setAlignment(Pos.CENTER_LEFT); actions.getStyleClass().add("walkthrough-actions"); @@ -135,64 +130,50 @@ private HBox makeActions(@NonNull WalkthroughStep step, @NonNull Walkthrough wal Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); - if (step.backButtonText().isPresent()) { - actions.getChildren().add(makeBackButton(step, walkthrough)); - } + step.backButtonText() + .ifPresent(text -> + actions.getChildren() + .add(makeButton(text, "walkthrough-back-button", beforeNavigate, walkthrough::previousStep))); + HBox rightActions = new HBox(); rightActions.setAlignment(Pos.CENTER_RIGHT); rightActions.getStyleClass().add("walkthrough-right-actions"); - if (step.skipButtonText().isPresent()) { - rightActions.getChildren().add(makeSkipButton(step, walkthrough)); - } - if (step.continueButtonText().isPresent()) { - rightActions.getChildren().add(makeContinueButton(step, walkthrough)); - } + step.skipButtonText() + .ifPresent(text -> + rightActions.getChildren() + .add(makeButton(text, "walkthrough-skip-button", beforeNavigate, walkthrough::skip))); + step.continueButtonText() + .ifPresent(text -> + rightActions.getChildren() + .add(makeButton(text, "walkthrough-continue-button", beforeNavigate, walkthrough::nextStep))); actions.getChildren().addAll(spacer, rightActions); - return actions; } - private VBox makeContent(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { + private VBox makeContent(WalkthroughStep step, Walkthrough walkthrough, Runnable beforeNavigate) { VBox contentBox = new VBox(); contentBox.getStyleClass().add("walkthrough-content"); contentBox.getChildren().addAll(step.content().stream().map(block -> switch (block) { - case TextBlock textBlock -> render(textBlock); - case InfoBlock infoBlock -> render(infoBlock); - case ArbitraryJFXBlock arbitraryBlock -> render(arbitraryBlock, walkthrough); + case TextBlock textBlock -> + render(textBlock); + case InfoBlock infoBlock -> + render(infoBlock); + case ArbitraryJFXBlock arbitraryBlock -> + render(arbitraryBlock, walkthrough, beforeNavigate); } ).toArray(Node[]::new)); return contentBox; } - private Button makeContinueButton(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { - String buttonText = step.continueButtonText() - .orElse("Walkthrough continue button"); - - Button continueButton = new Button(Localization.lang(buttonText)); - continueButton.getStyleClass().add("walkthrough-continue-button"); - continueButton.setOnAction(_ -> walkthrough.nextStep()); - return continueButton; - } - - private Button makeSkipButton(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { - String buttonText = step.skipButtonText() - .orElse("Walkthrough skip to finish"); - - Button skipButton = new Button(Localization.lang(buttonText)); - skipButton.getStyleClass().add("walkthrough-skip-button"); - skipButton.setOnAction(_ -> walkthrough.skip()); - return skipButton; - } - - private Button makeBackButton(@NonNull WalkthroughStep step, @NonNull Walkthrough walkthrough) { - String buttonText = step.backButtonText() - .orElse("Walkthrough back button"); - - Button backButton = new Button(Localization.lang(buttonText)); - backButton.getStyleClass().add("walkthrough-back-button"); - backButton.setOnAction(_ -> walkthrough.previousStep()); - return backButton; + private Button makeButton(String text, String styleClass, Runnable beforeNavigate, Runnable navigationAction) { + Button button = new Button(Localization.lang(text)); + button.getStyleClass().add(styleClass); + button.setOnAction(_ -> { + beforeNavigate.run(); + navigationAction.run(); + }); + return button; } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java index 471015a2ed7..edad165f3aa 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java @@ -5,6 +5,7 @@ import javafx.scene.shape.Rectangle; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * Creates a full screen darken effect. Usually used to force user to ignore certain @@ -13,7 +14,7 @@ public class FullScreenDarken extends WalkthroughEffect { private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); - private Rectangle overlay; + private @Nullable Rectangle overlay; public FullScreenDarken(@NonNull Pane pane) { super(pane); @@ -24,6 +25,7 @@ protected void initializeEffect() { this.overlay = new Rectangle(); this.overlay.setFill(OVERLAY_COLOR); this.overlay.setVisible(false); + this.overlay.setManaged(false); this.pane.getChildren().add(overlay); } @@ -31,25 +33,26 @@ protected void initializeEffect() { * Attaches the effect to the pane */ public void attach() { - cleanUp(); if (overlay == null) { initializeEffect(); } + cleanUp(); setupPaneListeners(); updateLayout(); } @Override public void detach() { - if (overlay != null && overlay.getParent() != null) { - overlay.setVisible(false); - pane.getChildren().remove(overlay); - } super.detach(); + assert overlay != null : "Run attach() before detach()"; + overlay.setVisible(false); + pane.getChildren().remove(overlay); + overlay = null; } @Override protected void updateLayout() { + assert overlay != null : "Run attach() before updateLayout()"; overlay.setX(0); overlay.setY(0); overlay.setWidth(pane.getWidth()); @@ -59,6 +62,7 @@ protected void updateLayout() { @Override protected void hideEffect() { + assert overlay != null : "Run attach() before hideEffect()"; overlay.setVisible(false); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java index 97ba9ecf3eb..24d7b0c62ee 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java @@ -8,6 +8,9 @@ import javafx.beans.value.ObservableValue; import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.control.ListView; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.ScrollPane; import javafx.scene.layout.Pane; import javafx.stage.Window; @@ -61,6 +64,7 @@ protected void setupNodeListeners(@NonNull Node node) { addListener(node.boundsInLocalProperty()); addListener(node.localToSceneTransformProperty()); addListener(node.visibleProperty()); + setupScrollListeners(node); ChangeListener sceneListener = (_, oldScene, newScene) -> { if (oldScene != null) { @@ -103,6 +107,23 @@ protected void setupPaneListeners() { }); } + protected void setupScrollListeners(@NonNull Node node) { + Node current = node.getParent(); + while (current != null) { + if (current instanceof ScrollPane scrollPane) { + addListener(scrollPane.hvalueProperty()); + addListener(scrollPane.vvalueProperty()); + } + if (current instanceof ListView listView) { + // ref: https://stackoverflow.com/questions/49425888/javafx-listview-on-scroll-show + listView.lookupAll(".scroll-bar") + .stream().filter(ScrollBar.class::isInstance) + .forEach(bar -> addListener(bar.visibleProperty())); + } + current = current.getParent(); + } + } + protected boolean isNodeVisible(@Nullable Node node) { return node != null && NodeHelper.isTreeVisible(node); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java index a9ef63e6a21..bb631dea37b 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java @@ -13,6 +13,7 @@ import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.Parent; +import javafx.scene.control.ButtonBase; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; import javafx.scene.input.MouseEvent; @@ -21,6 +22,7 @@ import org.jabref.gui.frame.MainMenu; import com.sun.javafx.scene.control.ContextMenuContent; +import org.jspecify.annotations.NonNull; /** * Defines a predicate for when navigation should occur on a target node. @@ -30,36 +32,56 @@ public interface NavigationPredicate { /** * Attaches the navigation listeners to the target node. * - * @param targetNode the node to attach the listeners to - * @param onNavigate the runnable to execute when navigation occurs + * @param targetNode the node to attach the listeners to + * @param beforeNavigate the runnable to execute before navigation + * @param onNavigate the runnable to execute when navigation occurs * @return a runnable to clean up the listeners */ - Runnable attachListeners(Node targetNode, Runnable onNavigate); + Runnable attachListeners(@NonNull Node targetNode, Runnable beforeNavigate, Runnable onNavigate); static NavigationPredicate onClick() { - return (targetNode, onNavigate) -> { + return (targetNode, beforeNavigate, onNavigate) -> { EventHandler onClicked = targetNode.getOnMouseClicked(); - targetNode.setOnMouseClicked(ConcurrentNavigationRunner.decorate(onClicked, onNavigate)); + targetNode.setOnMouseClicked(ConcurrentNavigationRunner.decorate(beforeNavigate, onClicked, onNavigate)); Optional item = resolveMenuItem(targetNode); - if (item.isEmpty()) { - return () -> targetNode.setOnMouseClicked(onClicked); + if (item.isPresent()) { + MenuItem menuItem = item.get(); + // Note MenuItem doesn't extend Node, so the duplication between MenuItem and ButtonBase cannot be removed + EventHandler onAction = menuItem.getOnAction(); + EventHandler decoratedAction = ConcurrentNavigationRunner.decorate(beforeNavigate, onAction, onNavigate); + menuItem.setOnAction(decoratedAction); + menuItem.addEventFilter(ActionEvent.ACTION, decoratedAction); + + return () -> { + targetNode.setOnMouseClicked(onClicked); + menuItem.setOnAction(onAction); + menuItem.removeEventFilter(ActionEvent.ACTION, decoratedAction); + }; } - EventHandler onAction = item.get().getOnAction(); - item.get().setOnAction(ConcurrentNavigationRunner.decorate(onAction, onNavigate)); + if (targetNode instanceof ButtonBase button) { + EventHandler onAction = button.getOnAction(); + EventHandler decoratedAction = ConcurrentNavigationRunner.decorate(beforeNavigate, onAction, onNavigate); - return () -> { - targetNode.setOnMouseClicked(onClicked); - item.get().setOnAction(onAction); - }; + button.setOnAction(decoratedAction); + button.addEventFilter(ActionEvent.ACTION, decoratedAction); + + return () -> { + targetNode.setOnMouseClicked(onClicked); + button.setOnAction(onAction); + button.removeEventFilter(ActionEvent.ACTION, decoratedAction); + }; + } + + return () -> targetNode.setOnMouseClicked(onClicked); }; } static NavigationPredicate onHover() { - return (targetNode, onNavigate) -> { + return (targetNode, beforeNavigate, onNavigate) -> { EventHandler onEnter = targetNode.getOnMouseEntered(); - targetNode.setOnMouseEntered(ConcurrentNavigationRunner.decorate(onEnter, onNavigate)); + targetNode.setOnMouseEntered(ConcurrentNavigationRunner.decorate(beforeNavigate, onEnter, onNavigate)); Optional item = resolveMenuItem(targetNode); if (item.isPresent()) { @@ -71,10 +93,11 @@ static NavigationPredicate onHover() { } static NavigationPredicate onTextInput() { - return (targetNode, onNavigate) -> { + return (targetNode, beforeNavigate, onNavigate) -> { if (targetNode instanceof TextInputControl textInput) { ChangeListener listener = (_, _, newText) -> { if (!newText.trim().isEmpty()) { + beforeNavigate.run(); onNavigate.run(); } }; @@ -86,12 +109,12 @@ static NavigationPredicate onTextInput() { } static NavigationPredicate manual() { - return (_, _) -> () -> { + return (_, _, _) -> () -> { }; } static NavigationPredicate auto() { - return (_, onNavigate) -> { + return (_, _, onNavigate) -> { onNavigate.run(); return () -> { }; @@ -99,8 +122,9 @@ static NavigationPredicate auto() { } private static Optional resolveMenuItem(Node node) { - if (!(node instanceof ContextMenuContent) && Stream.iterate(node.getParent(), Objects::nonNull, Parent::getParent) - .noneMatch(ContextMenuContent.class::isInstance)) { + if (!(node instanceof ContextMenuContent) + && Stream.iterate(node.getParent(), Objects::nonNull, Parent::getParent) + .noneMatch(ContextMenuContent.class::isInstance)) { return Optional.empty(); } @@ -113,8 +137,9 @@ private static Optional resolveMenuItem(Node node) { .flatMap(menu -> menu.getMenus().stream()) .flatMap(topLevelMenu -> topLevelMenu.getItems().stream()) .filter(menuItem -> Optional.ofNullable(menuItem.getGraphic()) - .map(graphic -> graphic.equals(node) || Stream.iterate(graphic, Objects::nonNull, Node::getParent) - .anyMatch(cm -> cm.equals(node))) + .map(graphic -> graphic.equals(node) + || Stream.iterate(graphic, Objects::nonNull, Node::getParent) + .anyMatch(cm -> cm.equals(node))) .orElse(false)) .findFirst(); } @@ -123,17 +148,20 @@ class ConcurrentNavigationRunner { private static final long HANDLER_TIMEOUT_MS = 1000; static EventHandler decorate( + Runnable beforeNavigate, EventHandler originalHandler, Runnable onNavigate) { - return event -> navigate(originalHandler, event, onNavigate); + return event -> navigate(beforeNavigate, originalHandler, event, onNavigate); } static void navigate( + Runnable beforeNavigate, EventHandler originalHandler, T event, Runnable onNavigate) { - System.out.println("Navigation started for event: " + event); + event.consume(); + beforeNavigate.run(); CompletableFuture handlerFuture = new CompletableFuture<>(); @@ -174,7 +202,6 @@ public void onChanged(Change change) { } }); - // FIXME: The onNavigate function is ran without any of those futures being completed? CompletableFuture.anyOf(handlerFuture, windowFuture, timeoutFuture) .whenComplete((_, _) -> { Platform.runLater(onNavigate); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java index 02ca0a2682f..3f12bd49b7b 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NodeResolver.java @@ -10,7 +10,6 @@ import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; -import javafx.stage.Window; import org.jabref.gui.actions.StandardActions; import org.jabref.logic.l10n.Localization; @@ -78,21 +77,26 @@ static NodeResolver action(@NonNull StandardActions action) { * @return a resolver that finds the menu item by language key */ static NodeResolver menuItem(@NonNull String key) { - return scene -> Window.getWindows().stream().flatMap(window -> { - if (window instanceof ContextMenu menu && menu.isShowing()) { - return menu.getItems().stream() - .filter(item -> Optional.ofNullable(item.getText()) - .map(str -> str.contains(Localization.lang(key))) - .orElse(false)) - .map(item -> Stream.iterate(item.getGraphic(), Objects::nonNull, Node::getParent) - .filter(node -> node.getStyleClass().contains("menu-item")) - .findFirst() - .orElse(null)); + return scene -> { + if (!(scene.getWindow() instanceof ContextMenu menu)) { + return Optional.empty(); } - return window.getScene().getRoot().lookupAll(".menu-item").stream() - .filter(node -> node.getStyleClass().contains("menu-item") && - node.toString().contains(Localization.lang(key))); - }).findFirst(); + + if (!menu.isShowing()) { + return Optional.empty(); + } + + return menu.getItems().stream() + .filter(item -> Optional + .ofNullable(item.getText()) + .map(str -> str.contains(Localization.lang(key))) + .orElse(false)) + .flatMap(item -> Stream + .iterate(item.getGraphic(), Objects::nonNull, Node::getParent) + .filter(node -> node.getStyleClass().contains("menu-item")) + .findFirst().stream() + ).findFirst(); + }; } @Nullable diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java index 8b27f2fa081..959fa18ed75 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java @@ -1,11 +1,12 @@ package org.jabref.gui.walkthrough.declarative.richtext; -import java.util.function.Function; +import java.util.function.BiFunction; import javafx.scene.Node; import org.jabref.gui.walkthrough.Walkthrough; -public record ArbitraryJFXBlock(Function componentFactory) +public record ArbitraryJFXBlock( + BiFunction componentFactory) implements WalkthroughRichTextBlock { } From b5a10c1a0b9561aaccb5e1c19d0005d4b8a85690 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 13:29:17 -0400 Subject: [PATCH 27/50] Fix Checkstyle and Intellij --- .../gui/walkthrough/SingleWindowWalkthroughOverlay.java | 2 +- .../java/org/jabref/gui/walkthrough/Walkthrough.java | 9 --------- .../declarative/effect/MultiWindowHighlight.java | 4 ---- .../gui/walkthrough/declarative/richtext/InfoBlock.java | 3 ++- .../gui/walkthrough/declarative/richtext/TextBlock.java | 3 ++- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index e17d53cc3c0..359f5ca7167 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Optional; +import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.geometry.Bounds; import javafx.geometry.Pos; @@ -28,7 +29,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javafx.application.Platform; /** * Manages the overlay for displaying walkthrough steps in a single window. diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 86acff125cc..7dd8a7da97a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -1,14 +1,12 @@ package org.jabref.gui.walkthrough; import java.util.List; -import java.util.Optional; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.scene.Scene; import javafx.stage.Stage; import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; @@ -121,13 +119,6 @@ public void previousStep() { overlay.displayStep(step); } - /** - * Get scene of the current stage. - */ - public Optional getScene() { - return Optional.ofNullable(currentStage).map(Stage::getScene); - } - private void stop() { if (overlay != null) { overlay.detachAll(); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java index 57fb6efda45..ee84947e6ae 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java @@ -10,10 +10,6 @@ public record MultiWindowHighlight( List windowEffects, Optional fallbackEffect ) { - public MultiWindowHighlight(HighlightEffect effect) { - this(new WindowEffect(effect)); - } - public MultiWindowHighlight(WindowEffect windowEffect) { this(windowEffect, HighlightEffect.FULL_SCREEN_DARKEN); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java index dd608b794cc..156505cae5c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java @@ -1,4 +1,5 @@ package org.jabref.gui.walkthrough.declarative.richtext; -public record InfoBlock(String text) implements WalkthroughRichTextBlock { +public record InfoBlock( + String text) implements WalkthroughRichTextBlock { } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java index 1b498e03035..9b7df19b6af 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java @@ -1,4 +1,5 @@ package org.jabref.gui.walkthrough.declarative.richtext; -public record TextBlock(String text) implements WalkthroughRichTextBlock { +public record TextBlock( + String text) implements WalkthroughRichTextBlock { } From d589f9d41800540a70443bc72e850937f7fbd6d7 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 14:29:07 -0400 Subject: [PATCH 28/50] Update the width and height --- .../SingleWindowWalkthroughOverlay.java | 32 ------------- .../gui/walkthrough/WalkthroughAction.java | 19 ++++++-- .../walkthrough/WalkthroughHighlighter.java | 45 ++++++++++--------- .../gui/walkthrough/WalkthroughOverlay.java | 8 ++-- .../gui/walkthrough/WalkthroughRenderer.java | 15 ++++++- .../components/WalkthroughEffect.java | 23 +--------- .../declarative/richtext/TextBlock.java | 3 +- .../declarative/step/PanelStep.java | 34 ++++++++------ .../declarative/step/TooltipStep.java | 28 ++++++------ .../declarative/step/WalkthroughStep.java | 4 +- 10 files changed, 94 insertions(+), 117 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 359f5ca7167..1e5451645b7 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -43,7 +43,6 @@ public class SingleWindowWalkthroughOverlay { private final StackPane stackPane; private final WalkthroughRenderer renderer; private final List cleanUpTasks = new ArrayList<>(); - private @Nullable Node node; public SingleWindowWalkthroughOverlay(Window window) { this.window = window; @@ -75,7 +74,6 @@ public SingleWindowWalkthroughOverlay(Window window) { public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { hide(); displayStepContent(step, targetNode, beforeNavigate, walkthrough); - node = targetNode; } /** @@ -138,19 +136,6 @@ private void displayTooltipStep(Node content, @Nullable Node targetNode, Tooltip popover.setAutoHide(false); mapToArrowLocation(step.position()).ifPresent(popover::setArrowLocation); - step.preferredWidth().ifPresent(width -> { - popover.setPrefWidth(width); - popover.setMinWidth(width); - }); - step.preferredHeight().ifPresent(height -> { - popover.setPrefHeight(height); - popover.setMinHeight(height); - }); - - // Defer showing the popover until the next pulse to ensure the - // target node (or window) has been fully laid out. This prevents - // situations where the pop-over fails to appear because the node - // is not yet ready (for example directly after a scene change). Platform.runLater(() -> { if (targetNode != null) { popover.show(targetNode); @@ -158,23 +143,6 @@ private void displayTooltipStep(Node content, @Nullable Node targetNode, Tooltip popover.show(window); } }); - - ChangeListener listener = (_, _, focused) -> { - if (focused && !popover.isShowing()) { - LOGGER.debug("Window gained focus, ensuring tooltip is visible"); - if (node != null) { - popover.show(node); - } else { - popover.show(window); - } - } - }; - - window.focusedProperty().addListener(listener); - cleanUpTasks.add(() -> window.focusedProperty().removeListener(listener)); - popover.showingProperty().addListener(listener); - cleanUpTasks.add(() -> popover.showingProperty().removeListener(listener)); - cleanUpTasks.add(popover::hide); } private void displayPanelStep(Node content, PanelStep step) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 1ec07dee8c1..849a4b938ae 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -13,6 +13,8 @@ import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; +import org.jabref.gui.walkthrough.declarative.richtext.InfoBlock; +import org.jabref.gui.walkthrough.declarative.richtext.TextBlock; import org.jabref.gui.walkthrough.declarative.step.PanelPosition; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; @@ -41,7 +43,7 @@ private static Map buildRegistry() { // FIXME: Not internationalized. WalkthroughStep step1 = TooltipStep - .builder("Hover over \"File\" menu") + .builder("Click on \"File\" menu") .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.BOTTOM) @@ -49,7 +51,7 @@ private static Map buildRegistry() { .build(); WalkthroughStep step2 = TooltipStep - .builder("Select \"Preferences\"") + .builder("Click on \"Preferences\"") .resolver(NodeResolver.menuItem("Preferences")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.RIGHT) @@ -61,7 +63,9 @@ private static Map buildRegistry() { .build(); WalkthroughStep step3 = TooltipStep - .builder("Select \"Linked files\" tab") + .builder("Select the 'Linked files' tab") + .content(new TextBlock("This section manages how JabRef handles your PDF files and other documents.")) + .width(400) .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("list-cell") && node.toString().contains("Linked files"))) @@ -76,7 +80,9 @@ private static Map buildRegistry() { .build(); WalkthroughStep step4 = TooltipStep - .builder("Choose to use main file directory") + .builder("Enable 'Main file directory' option") + .content(new TextBlock("Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step.")) + .width(400) .resolver(NodeResolver.fxId("useMainFileDirectory")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) @@ -89,6 +95,11 @@ private static Map buildRegistry() { WalkthroughStep step5 = PanelStep .builder("Click \"OK\" to save changes") + .content( + new TextBlock("Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents."), + new InfoBlock("Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks") + ) + .height(180) .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("button") && node.toString().contains(Localization.lang("Save")))) .navigation(NavigationPredicate.onClick()) .position(PanelPosition.TOP) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java index 1e691ed5ca0..f5a0ca79dae 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java @@ -29,35 +29,36 @@ public class WalkthroughHighlighter { /** * Applies the specified highlight configuration. * - * @param mainScene The primary scene to apply the highlight to. - * @param highlightConfig The highlight configuration to apply. Default to - * BackdropHighlight on the primary windows if null. - * @param fallbackTarget The fallback target node to use if no highlight - * configuration is provided. + * @param config The highlight configuration to apply. Default to + * BackdropHighlight on the primary windows if null. + * @param fallbackWindow The primary scene to apply the highlight to. + * @param fallbackTarget The fallback target node to use if no highlight + * configuration is provided. */ - public void applyHighlight(@NonNull Scene mainScene, - @Nullable MultiWindowHighlight highlightConfig, + public void applyHighlight(@Nullable MultiWindowHighlight config, @NonNull Scene fallbackWindow, @Nullable Node fallbackTarget) { detachAll(); - if (highlightConfig != null) { - if (highlightConfig.windowEffects().isEmpty() && highlightConfig.fallbackEffect().isPresent()) { - applyEffect(mainScene.getWindow(), highlightConfig.fallbackEffect().get(), fallbackTarget); - return; - } - highlightConfig.windowEffects().forEach(effect -> { - Window window = effect.windowResolver().flatMap(WindowResolver::resolve).orElse(mainScene.getWindow()); - Node targetNode = effect - .targetNodeResolver() - .flatMap(resolver -> resolver.resolve(window.getScene() != null ? window.getScene() : mainScene)) - .orElse(fallbackTarget); - applyEffect(window, effect.effect(), targetNode); - }); - } else { + if (config == null) { if (fallbackTarget != null) { - applyBackdropHighlight(mainScene.getWindow(), fallbackTarget); + applyBackdropHighlight(fallbackWindow.getWindow(), fallbackTarget); } + return; + } + + if (config.windowEffects().isEmpty() && config.fallbackEffect().isPresent()) { + applyEffect(fallbackWindow.getWindow(), config.fallbackEffect().get(), fallbackTarget); + return; } + + config.windowEffects().forEach(effect -> { + Window window = effect.windowResolver().flatMap(WindowResolver::resolve).orElse(fallbackWindow.getWindow()); + Node targetNode = effect + .targetNodeResolver() + .flatMap(resolver -> resolver.resolve(window.getScene() != null ? window.getScene() : fallbackWindow)) + .orElse(fallbackTarget); + applyEffect(window, effect.effect(), targetNode); + }); } /** diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index df6f4031dd0..ea6ce4a005e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -52,7 +52,7 @@ public void displayStep(@NonNull WalkthroughStep step) { if (step.resolver().isPresent()) { startNodePolling(step, window, targetNode.orElse(null)); } else { - walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), null); + walkthroughHighlighter.applyHighlight(step.highlight().orElse(null), scene, null); displayStep(step, window, null); } } @@ -97,7 +97,7 @@ private void startNodePolling(WalkthroughStep step, Window window, @Nullable Nod Scene initialScene = window.getScene(); - walkthroughHighlighter.applyHighlight(initialScene, step.highlight().orElse(null), node); + walkthroughHighlighter.applyHighlight(step.highlight().orElse(null), initialScene, node); displayStep(step, window, node); LOGGER.info("Starting continuous node polling for step: {}", step.title()); @@ -112,7 +112,7 @@ private void startNodePolling(WalkthroughStep step, Window window, @Nullable Nod (currentNode) -> { if (!nodeEverResolved.get()) { LOGGER.info("Target node found for step: {}, updating display", step.title()); - walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), currentNode); + walkthroughHighlighter.applyHighlight(step.highlight().orElse(null), scene, currentNode); displayStep(step, window, currentNode); nodeEverResolved.set(true); } @@ -128,7 +128,7 @@ private void startNodePolling(WalkthroughStep step, Window window, @Nullable Nod tryRevertToPreviousResolvableStep(); } else { LOGGER.info("Node disappeared for step: {}, showing step without node", step.title()); - walkthroughHighlighter.applyHighlight(scene, step.highlight().orElse(null), null); + walkthroughHighlighter.applyHighlight(step.highlight().orElse(null), scene, null); displayStep(step, window, null); nodeEverResolved.set(false); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index cbc9cdde599..672edea7149 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -46,6 +46,17 @@ public Node render(TooltipStep step, Walkthrough walkthrough, Runnable beforeNav HBox actionsContainer = makeActions(step, walkthrough, beforeNavigate); actionsContainer.getStyleClass().add("walkthrough-tooltip-actions"); + step.height().ifPresent(height -> { + tooltip.setPrefHeight(height); + tooltip.setMaxHeight(height); + tooltip.setMinHeight(height); + }); + step.width().ifPresent(width -> { + tooltip.setPrefWidth(width); + tooltip.setMaxWidth(width); + tooltip.setMinWidth(width); + }); + tooltip.getChildren().addAll(titleLabel, contentContainer, actionsContainer); return tooltip; } @@ -79,7 +90,7 @@ private void configurePanelSize(VBox panel, PanelStep step) { panel.getStyleClass().add("walkthrough-side-panel-vertical"); VBox.setVgrow(panel, Priority.ALWAYS); panel.setMaxHeight(Double.MAX_VALUE); - step.preferredWidth().ifPresent(width -> { + step.width().ifPresent(width -> { panel.setPrefWidth(width); panel.setMaxWidth(width); panel.setMinWidth(width); @@ -88,7 +99,7 @@ private void configurePanelSize(VBox panel, PanelStep step) { panel.getStyleClass().add("walkthrough-side-panel-horizontal"); HBox.setHgrow(panel, Priority.ALWAYS); panel.setMaxWidth(Double.MAX_VALUE); - step.preferredHeight().ifPresent(height -> { + step.height().ifPresent(height -> { panel.setPrefHeight(height); panel.setMaxHeight(height); panel.setMinHeight(height); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java index 24d7b0c62ee..4c68163dbe5 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java @@ -8,9 +8,6 @@ import javafx.beans.value.ObservableValue; import javafx.scene.Node; import javafx.scene.Scene; -import javafx.scene.control.ListView; -import javafx.scene.control.ScrollBar; -import javafx.scene.control.ScrollPane; import javafx.scene.layout.Pane; import javafx.stage.Window; @@ -63,8 +60,9 @@ protected void addListener(ObservableValue property, ChangeListener li protected void setupNodeListeners(@NonNull Node node) { addListener(node.boundsInLocalProperty()); addListener(node.localToSceneTransformProperty()); + addListener(node.boundsInParentProperty()); + addListener(node.layoutBoundsProperty()); addListener(node.visibleProperty()); - setupScrollListeners(node); ChangeListener sceneListener = (_, oldScene, newScene) -> { if (oldScene != null) { @@ -107,23 +105,6 @@ protected void setupPaneListeners() { }); } - protected void setupScrollListeners(@NonNull Node node) { - Node current = node.getParent(); - while (current != null) { - if (current instanceof ScrollPane scrollPane) { - addListener(scrollPane.hvalueProperty()); - addListener(scrollPane.vvalueProperty()); - } - if (current instanceof ListView listView) { - // ref: https://stackoverflow.com/questions/49425888/javafx-listview-on-scroll-show - listView.lookupAll(".scroll-bar") - .stream().filter(ScrollBar.class::isInstance) - .forEach(bar -> addListener(bar.visibleProperty())); - } - current = current.getParent(); - } - } - protected boolean isNodeVisible(@Nullable Node node) { return node != null && NodeHelper.isTreeVisible(node); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java index 9b7df19b6af..1b498e03035 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java @@ -1,5 +1,4 @@ package org.jabref.gui.walkthrough.declarative.richtext; -public record TextBlock( - String text) implements WalkthroughRichTextBlock { +public record TextBlock(String text) implements WalkthroughRichTextBlock { } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index 75865d4a5be..dcfbbf322e2 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -24,8 +24,8 @@ public record PanelStep( @Nullable String backButtonTextValue, @Nullable NavigationPredicate navigationPredicateValue, PanelPosition position, - @Nullable Double preferredWidthValue, - @Nullable Double preferredHeightValue, + @Nullable Double widthValue, + @Nullable Double heightValue, @Nullable MultiWindowHighlight highlightValue, boolean autoFallback, @Nullable WindowResolver activeWindowResolverValue) implements WalkthroughStep { @@ -56,13 +56,13 @@ public Optional navigationPredicate() { } @Override - public OptionalDouble preferredWidth() { - return preferredWidthValue != null ? OptionalDouble.of(preferredWidthValue) : OptionalDouble.empty(); + public OptionalDouble width() { + return widthValue != null ? OptionalDouble.of(widthValue) : OptionalDouble.empty(); } @Override - public OptionalDouble preferredHeight() { - return preferredHeightValue != null ? OptionalDouble.of(preferredHeightValue) : OptionalDouble.empty(); + public OptionalDouble height() { + return heightValue != null ? OptionalDouble.of(heightValue) : OptionalDouble.empty(); } @Override @@ -88,8 +88,8 @@ public static class Builder { private @Nullable String backButtonText; private @Nullable NavigationPredicate navigationPredicate; private PanelPosition position = PanelPosition.LEFT; - private @Nullable Double preferredWidth; - private @Nullable Double preferredHeight; + private @Nullable Double width; + private @Nullable Double height; private @Nullable MultiWindowHighlight highlight; private boolean autoFallback = true; private @Nullable WindowResolver activeWindowResolver; @@ -138,13 +138,13 @@ public Builder position(@NonNull PanelPosition position) { return this; } - public Builder preferredWidth(double width) { - this.preferredWidth = width; + public Builder width(double width) { + this.width = width; return this; } - public Builder preferredHeight(double height) { - this.preferredHeight = height; + public Builder height(double height) { + this.height = height; return this; } @@ -172,6 +172,12 @@ public Builder activeWindow(@NonNull WindowResolver activeWindowResolver) { } public PanelStep build() { + if (height != null && (position == PanelPosition.LEFT || position == PanelPosition.RIGHT)) { + throw new IllegalArgumentException("Height is not applicable for left/right positioned panels."); + } + if (width != null && (position == PanelPosition.TOP || position == PanelPosition.BOTTOM)) { + throw new IllegalArgumentException("Width is not applicable for top/bottom positioned panels."); + } return new PanelStep(title, content, resolver, @@ -180,8 +186,8 @@ public PanelStep build() { backButtonText, navigationPredicate, position, - preferredWidth, - preferredHeight, + width, + height, highlight, autoFallback, activeWindowResolver); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index 955e1a72c52..77624e87bf5 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -25,8 +25,8 @@ public record TooltipStep( @Nullable String backButtonTextValue, @Nullable NavigationPredicate navigationPredicateValue, TooltipPosition position, - @Nullable Double preferredWidthValue, - @Nullable Double preferredHeightValue, + @Nullable Double widthValue, + @Nullable Double heightValue, @Nullable MultiWindowHighlight highlightValue, boolean autoFallback, @Nullable WindowResolver activeWindowResolverValue @@ -58,13 +58,13 @@ public Optional navigationPredicate() { } @Override - public OptionalDouble preferredWidth() { - return preferredWidthValue != null ? OptionalDouble.of(preferredWidthValue) : OptionalDouble.empty(); + public OptionalDouble width() { + return widthValue != null ? OptionalDouble.of(widthValue) : OptionalDouble.empty(); } @Override - public OptionalDouble preferredHeight() { - return preferredHeightValue != null ? OptionalDouble.of(preferredHeightValue) : OptionalDouble.empty(); + public OptionalDouble height() { + return heightValue != null ? OptionalDouble.of(heightValue) : OptionalDouble.empty(); } @Override @@ -90,8 +90,8 @@ public static class Builder { private @Nullable String backButtonText; private @Nullable NavigationPredicate navigationPredicate; private TooltipPosition position = TooltipPosition.AUTO; - private @Nullable Double preferredWidth; - private @Nullable Double preferredHeight; + private @Nullable Double width; + private @Nullable Double height; private @Nullable MultiWindowHighlight highlight; private boolean autoFallback = true; private @Nullable WindowResolver activeWindowResolver; @@ -140,13 +140,13 @@ public Builder position(@NonNull TooltipPosition position) { return this; } - public Builder preferredWidth(double width) { - this.preferredWidth = width; + public Builder width(double width) { + this.width = width; return this; } - public Builder preferredHeight(double height) { - this.preferredHeight = height; + public Builder height(double height) { + this.height = height; return this; } @@ -185,8 +185,8 @@ public TooltipStep build() { backButtonText, navigationPredicate, position, - preferredWidth, - preferredHeight, + width, + height, highlight, autoFallback, activeWindowResolver); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java index d780f6ca808..019c771e2b0 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java @@ -25,9 +25,9 @@ public sealed interface WalkthroughStep permits PanelStep, TooltipStep { Optional navigationPredicate(); - OptionalDouble preferredWidth(); + OptionalDouble width(); - OptionalDouble preferredHeight(); + OptionalDouble height(); boolean autoFallback(); From 487cc605ed4e0613fb3d9b4a810e7ab4d9d46eaa Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 14:35:29 -0400 Subject: [PATCH 29/50] Internationalize WalkthroughAction --- .../SingleWindowWalkthroughOverlay.java | 22 ++++++++++++++++++- .../gui/walkthrough/WalkthroughAction.java | 7 +++--- .../main/resources/l10n/JabRef_en.properties | 9 ++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 1e5451645b7..808c5ca1936 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -138,7 +138,22 @@ private void displayTooltipStep(Node content, @Nullable Node targetNode, Tooltip Platform.runLater(() -> { if (targetNode != null) { - popover.show(targetNode); + if (isNodeReady(targetNode)) { + popover.show(targetNode); + } else { + ChangeListener boundsListener = new ChangeListener<>() { + @Override + public void changed(javafx.beans.value.ObservableValue observable, + Bounds oldValue, Bounds newValue) { + if (newValue.getWidth() > 0 && newValue.getHeight() > 0) { + Platform.runLater(() -> popover.show(targetNode)); + targetNode.boundsInParentProperty().removeListener(this); + } + } + }; + targetNode.boundsInParentProperty().addListener(boundsListener); + cleanUpTasks.add(() -> targetNode.boundsInParentProperty().removeListener(boundsListener)); + } } else { popover.show(window); } @@ -250,4 +265,9 @@ private void setupClipping(Node node) { cleanUpTasks.add(() -> node.boundsInParentProperty().removeListener(listener)); cleanUpTasks.add(() -> overlayPane.setClip(null)); } + + private boolean isNodeReady(Node node) { + Bounds bounds = node.getBoundsInParent(); + return bounds.getWidth() > 0 && bounds.getHeight() > 0; + } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 849a4b938ae..20957c7bc3c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -41,7 +41,6 @@ public void execute() { private static Map buildRegistry() { Map registry = new HashMap<>(); - // FIXME: Not internationalized. WalkthroughStep step1 = TooltipStep .builder("Click on \"File\" menu") .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) @@ -63,8 +62,8 @@ private static Map buildRegistry() { .build(); WalkthroughStep step3 = TooltipStep - .builder("Select the 'Linked files' tab") - .content(new TextBlock("This section manages how JabRef handles your PDF files and other documents.")) + .builder("Select the \"Linked files\" tab") + .content(new TextBlock(Localization.lang("This section manages how JabRef handles your PDF files and other documents."))) .width(400) .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("list-cell") && @@ -80,7 +79,7 @@ private static Map buildRegistry() { .build(); WalkthroughStep step4 = TooltipStep - .builder("Enable 'Main file directory' option") + .builder("Enable \"Main file directory\" option") .content(new TextBlock("Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step.")) .width(400) .resolver(NodeResolver.fxId("useMainFileDirectory")) diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index ccf1b21828c..5a094733b46 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2943,6 +2943,15 @@ Start\ walkthrough=Start Walkthrough Step\ %0\ of\ %1=Step %0 of %1 Complete\ walkthrough=Complete Walkthrough Back=Back +Click\ on\ "File"\ menu=Click on "File" menu +Click\ on\ "Preferences"=Click on "Preferences" +Select\ the\ "Linked\ files"\ tab=Select the "Linked files" tab +This\ section\ manages\ how\ JabRef\ handles\ your\ PDF\ files\ and\ other\ documents.=This section manages how JabRef handles your PDF files and other documents. +Enable\ "Main\ file\ directory"\ option=Enable "Main file directory" option +Choose\ this\ option\ to\ tell\ JabRef\ where\ your\ research\ files\ are\ stored.\ This\ makes\ it\ easy\ to\ attach\ PDFs\ and\ other\ documents\ to\ your\ bibliography\ entries.\ You\ can\ browse\ to\ select\ your\ preferred\ folder\ in\ the\ next\ step.=Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step. +Click\ "OK"\ to\ save\ changes=Click "OK" to save changes +Congratulations!\ Your\ main\ file\ directory\ is\ now\ configured.\ JabRef\ will\ use\ this\ location\ to\ automatically\ find\ and\ organize\ your\ research\ documents.=Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents. +Additional\ information\ on\ main\ file\ directory\ can\ be\ found\ in\ https\:\\\\docs.jabref.org\\v5\\finding-sorting-and-cleaning-entries\\filelinks=Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks # CommandLine Available\ export\ formats\:=Available export formats: From 323b353c4f0be9f1738dd518285610d2ace62f5b Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 15:06:47 -0400 Subject: [PATCH 30/50] Fix according to Trag bot. --- .../jabref/gui/actions/StandardActions.java | 2 +- .../SingleWindowWalkthroughOverlay.java | 4 +- .../jabref/gui/walkthrough/Walkthrough.java | 4 +- .../gui/walkthrough/WalkthroughAction.java | 6 +- .../walkthrough/WalkthroughHighlighter.java | 24 ++-- .../gui/walkthrough/WalkthroughOverlay.java | 6 +- .../components/BackdropHighlight.java | 7 +- .../components/PulseAnimateIndicator.java | 10 +- .../declarative/NavigationPredicate.java | 118 +++++++++--------- .../main/resources/l10n/JabRef_en.properties | 1 + 10 files changed, 87 insertions(+), 95 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index f518196c95f..e14e39fd05f 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -185,7 +185,7 @@ public enum StandardActions implements Action { OPEN_CHANGELOG(Localization.lang("View change log"), Localization.lang("See what has been changed in the JabRef versions")), OPEN_GITHUB("GitHub", Localization.lang("Opens JabRef's GitHub page"), IconTheme.JabRefIcons.GITHUB), WALKTHROUGH_MENU(Localization.lang("Walkthroughs"), IconTheme.JabRefIcons.BOOK), - MAIN_FILE_DIRECTORY_WALKTHROUGH(Localization.lang("Configure Main File Directory"), IconTheme.JabRefIcons.LATEX_FILE_DIRECTORY), + MAIN_FILE_DIRECTORY_WALKTHROUGH(Localization.lang("Configure main file directory"), IconTheme.JabRefIcons.LATEX_FILE_DIRECTORY), DONATE(Localization.lang("Donate to JabRef"), Localization.lang("Donate to JabRef"), IconTheme.JabRefIcons.DONATE), OPEN_FORUM(Localization.lang("Community forum"), Localization.lang("Community forum"), IconTheme.JabRefIcons.FORUM), ERROR_CONSOLE(Localization.lang("View event log"), Localization.lang("Display all error messages")), diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 808c5ca1936..fa59db4737a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -57,6 +57,7 @@ public SingleWindowWalkthroughOverlay(Window window) { popover = new PopOver(); Scene scene = window.getScene(); + // This basically never happens, so only a development time check is needed assert scene != null; originalRoot = (Pane) scene.getRoot(); @@ -73,6 +74,7 @@ public SingleWindowWalkthroughOverlay(Window window) { */ public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { hide(); + // race condition with PopOver showing displayStepContent(step, targetNode, beforeNavigate, walkthrough); } @@ -104,7 +106,7 @@ public void detach() { } } - private void displayStepContent(WalkthroughStep step, Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { + private void displayStepContent(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { switch (step) { case TooltipStep tooltipStep -> { Node content = renderer.render(tooltipStep, walkthrough, beforeNavigate); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 7dd8a7da97a..f260e994d1e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -142,8 +142,8 @@ public void goToStep(int stepIndex) { overlay.displayStep(step); } - public List getSteps() { - return steps; + public WalkthroughStep getStepAtIndex(int index) { + return steps.get(index); } public void skip() { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 20957c7bc3c..d36acaea1fd 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; import javafx.scene.control.ContextMenu; @@ -30,6 +31,7 @@ public class WalkthroughAction extends SimpleCommand { public WalkthroughAction(String name, JabRefFrame frame) { this.walkthrough = WALKTHROUGH_REGISTRY.get(name); + Objects.requireNonNull(this.walkthrough); this.frame = frame; } @@ -80,7 +82,9 @@ private static Map buildRegistry() { WalkthroughStep step4 = TooltipStep .builder("Enable \"Main file directory\" option") - .content(new TextBlock("Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step.")) + .content(new TextBlock(""" + Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step. + """)) .width(400) .resolver(NodeResolver.fxId("useMainFileDirectory")) .navigation(NavigationPredicate.onClick()) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java index f5a0ca79dae..99d05ddc75f 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java @@ -90,18 +90,18 @@ private void applyEffect(@NonNull Window window, @NonNull HighlightEffect effect case FULL_SCREEN_DARKEN -> applyFullScreenDarken(window); case NONE -> { - if (backdropHighlights.containsKey(window)) { - backdropHighlights.get(window).detach(); - backdropHighlights.remove(window); - } - if (pulseIndicators.containsKey(window)) { - pulseIndicators.get(window).detach(); - pulseIndicators.remove(window); - } - if (fullScreenDarkens.containsKey(window)) { - fullScreenDarkens.get(window).detach(); - fullScreenDarkens.remove(window); - } + backdropHighlights.computeIfPresent(window, (_, highlight) -> { + highlight.detach(); + return null; + }); + pulseIndicators.computeIfPresent(window, (_, indicator) -> { + indicator.detach(); + return null; + }); + fullScreenDarkens.computeIfPresent(window, (_, darken) -> { + darken.detach(); + return null; + }); } } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index ea6ce4a005e..5fb53f6755f 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -73,7 +73,7 @@ private void tryRevertToPreviousResolvableStep() { int currentIndex = walkthrough.currentStepProperty().get(); for (int i = currentIndex - 1; i >= 0; i--) { - WalkthroughStep previousStep = getStepAtIndex(i); + WalkthroughStep previousStep = walkthrough.getStepAtIndex(i); Window activeWindow = previousStep.activeWindowResolver().flatMap(WindowResolver::resolve).orElse(stage); Scene scene = activeWindow.getScene(); if (scene != null && (previousStep.resolver().isEmpty() || previousStep.resolver().get().resolve(scene).isPresent())) { @@ -149,10 +149,6 @@ private void stopNodePolling() { nodePollingTimeline = null; } - private @NonNull WalkthroughStep getStepAtIndex(int index) { - return walkthrough.getSteps().get(index); - } - private SingleWindowWalkthroughOverlay getOrCreateOverlay(Window window) { return overlays.computeIfAbsent(window, SingleWindowWalkthroughOverlay::new); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java index 02611d16192..ec9c82cb1f2 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java @@ -10,7 +10,7 @@ import org.jspecify.annotations.NonNull; /** - * Creates a backdrop highlight effect. + * A backdrop highlight effect that dims the background and highlights a specific node. */ public class BackdropHighlight extends WalkthroughEffect { private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); @@ -24,11 +24,6 @@ public BackdropHighlight(@NonNull Pane pane) { super(pane); } - /** - * Attaches the backdrop highlight to the specified node. - * - * @param node The node to attach the backdrop highlight to. - */ public void attach(@NonNull Node node) { detach(); if (overlayShape == null) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java index d96aad76790..d0d24f3f07d 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java @@ -16,6 +16,7 @@ * A pulsing circular indicator that can be attached to a target node. */ public class PulseAnimateIndicator extends WalkthroughEffect { + public static final int INDICATOR_OFFSET = 5; private Circle pulseIndicator; private Timeline pulseAnimation; private Node node; @@ -24,11 +25,6 @@ public PulseAnimateIndicator(@NonNull Pane pane) { super(pane); } - /** - * Attaches the pulse indicator to the specified node. - * - * @param node The node to attach the pulse indicator to. - */ public void attach(@NonNull Node node) { cleanUp(); if (pulseIndicator == null) { @@ -92,8 +88,8 @@ protected void updateLayout() { pulseIndicator.setVisible(true); - double indicatorX = targetBounds.getMaxX() - 5; - double indicatorY = targetBounds.getMinY() + 5; + double indicatorX = targetBounds.getMaxX() - INDICATOR_OFFSET; + double indicatorY = targetBounds.getMinY() + INDICATOR_OFFSET; pulseIndicator.setLayoutX(indicatorX); pulseIndicator.setLayoutY(indicatorY); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java index bb631dea37b..ed4e4e7cd05 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/NavigationPredicate.java @@ -29,6 +29,8 @@ */ @FunctionalInterface public interface NavigationPredicate { + long HANDLER_TIMEOUT_MS = 1000; + /** * Attaches the navigation listeners to the target node. * @@ -42,14 +44,14 @@ public interface NavigationPredicate { static NavigationPredicate onClick() { return (targetNode, beforeNavigate, onNavigate) -> { EventHandler onClicked = targetNode.getOnMouseClicked(); - targetNode.setOnMouseClicked(ConcurrentNavigationRunner.decorate(beforeNavigate, onClicked, onNavigate)); + targetNode.setOnMouseClicked(decorate(beforeNavigate, onClicked, onNavigate)); Optional item = resolveMenuItem(targetNode); if (item.isPresent()) { MenuItem menuItem = item.get(); // Note MenuItem doesn't extend Node, so the duplication between MenuItem and ButtonBase cannot be removed EventHandler onAction = menuItem.getOnAction(); - EventHandler decoratedAction = ConcurrentNavigationRunner.decorate(beforeNavigate, onAction, onNavigate); + EventHandler decoratedAction = decorate(beforeNavigate, onAction, onNavigate); menuItem.setOnAction(decoratedAction); menuItem.addEventFilter(ActionEvent.ACTION, decoratedAction); @@ -62,7 +64,7 @@ static NavigationPredicate onClick() { if (targetNode instanceof ButtonBase button) { EventHandler onAction = button.getOnAction(); - EventHandler decoratedAction = ConcurrentNavigationRunner.decorate(beforeNavigate, onAction, onNavigate); + EventHandler decoratedAction = decorate(beforeNavigate, onAction, onNavigate); button.setOnAction(decoratedAction); button.addEventFilter(ActionEvent.ACTION, decoratedAction); @@ -81,7 +83,7 @@ static NavigationPredicate onClick() { static NavigationPredicate onHover() { return (targetNode, beforeNavigate, onNavigate) -> { EventHandler onEnter = targetNode.getOnMouseEntered(); - targetNode.setOnMouseEntered(ConcurrentNavigationRunner.decorate(beforeNavigate, onEnter, onNavigate)); + targetNode.setOnMouseEntered(decorate(beforeNavigate, onEnter, onNavigate)); Optional item = resolveMenuItem(targetNode); if (item.isPresent()) { @@ -144,71 +146,67 @@ private static Optional resolveMenuItem(Node node) { .findFirst(); } - class ConcurrentNavigationRunner { - private static final long HANDLER_TIMEOUT_MS = 1000; + static EventHandler decorate( + Runnable beforeNavigate, + EventHandler originalHandler, + Runnable onNavigate) { + return event -> navigate(beforeNavigate, originalHandler, event, onNavigate); + } - static EventHandler decorate( - Runnable beforeNavigate, - EventHandler originalHandler, - Runnable onNavigate) { - return event -> navigate(beforeNavigate, originalHandler, event, onNavigate); - } + static void navigate( + Runnable beforeNavigate, + EventHandler originalHandler, + T event, + Runnable onNavigate) { - static void navigate( - Runnable beforeNavigate, - EventHandler originalHandler, - T event, - Runnable onNavigate) { + event.consume(); + beforeNavigate.run(); - event.consume(); - beforeNavigate.run(); + CompletableFuture handlerFuture = new CompletableFuture<>(); - CompletableFuture handlerFuture = new CompletableFuture<>(); + if (originalHandler != null) { + Platform.runLater(() -> { + try { + originalHandler.handle(event); + } finally { + handlerFuture.complete(null); + } + }); + } else { + handlerFuture.complete(null); + } - if (originalHandler != null) { - Platform.runLater(() -> { - try { - originalHandler.handle(event); - } finally { - handlerFuture.complete(null); - } - }); - } else { - handlerFuture.complete(null); - } + CompletableFuture windowFuture = new CompletableFuture<>(); - CompletableFuture windowFuture = new CompletableFuture<>(); - - ListChangeListener listener = new ListChangeListener<>() { - @Override - public void onChanged(Change change) { - while (change.next()) { - if (change.wasAdded()) { - Window.getWindows().removeListener(this); - windowFuture.complete(null); - return; - } + ListChangeListener listener = new ListChangeListener<>() { + @Override + public void onChanged(Change change) { + while (change.next()) { + if (change.wasAdded()) { + Window.getWindows().removeListener(this); + windowFuture.complete(null); + return; } } - }; + } + }; - Platform.runLater(() -> Window.getWindows().addListener(listener)); + Platform.runLater(() -> Window.getWindows().addListener(listener)); - CompletableFuture timeoutFuture = CompletableFuture.runAsync(() -> { - try { - Thread.sleep(HANDLER_TIMEOUT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - - CompletableFuture.anyOf(handlerFuture, windowFuture, timeoutFuture) - .whenComplete((_, _) -> { - Platform.runLater(onNavigate); - timeoutFuture.cancel(true); - Platform.runLater(() -> Window.getWindows().removeListener(listener)); - windowFuture.cancel(true); - }); - } + CompletableFuture timeoutFuture = CompletableFuture.runAsync(() -> { + try { + Thread.sleep(HANDLER_TIMEOUT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + CompletableFuture.anyOf(handlerFuture, windowFuture, timeoutFuture) + .whenComplete((_, _) -> { + Platform.runLater(onNavigate); + timeoutFuture.cancel(true); + Platform.runLater(() -> Window.getWindows().removeListener(listener)); + windowFuture.cancel(true); + }); } } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 5a094733b46..9f97c97d509 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2932,6 +2932,7 @@ You\ must\ specify\ an\ identifier.=You must specify an identifier. You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) citations. # Walkthrough +Configure\ main\ file\ directory=Configure main file directory Walkthroughs=Walkthroughs Saving\ Your\ Work=Saving Your Work Skip\ for\ Now=Skip for Now From 40a22ff92f4109caa4c52d4911b3b5446d5f1551 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 15:14:42 -0400 Subject: [PATCH 31/50] Fix according to Trag bot. --- .../declarative/effect/MultiWindowHighlight.java | 7 +++---- .../walkthrough/declarative/effect/WindowEffect.java | 11 +++++------ .../declarative/richtext/ArbitraryJFXBlock.java | 4 +++- .../walkthrough/declarative/richtext/InfoBlock.java | 4 +++- .../walkthrough/declarative/richtext/TextBlock.java | 5 ++++- .../gui/walkthrough/declarative/step/TooltipStep.java | 2 +- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java index ee84947e6ae..0f6ff713c80 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/MultiWindowHighlight.java @@ -3,11 +3,10 @@ import java.util.List; import java.util.Optional; -/** - * Highlighting effects across multiple windows. - */ +import org.jspecify.annotations.NonNull; + public record MultiWindowHighlight( - List windowEffects, + @NonNull List windowEffects, Optional fallbackEffect ) { public MultiWindowHighlight(WindowEffect windowEffect) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java index 935a7562271..3fb5df99dab 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/WindowEffect.java @@ -5,13 +5,12 @@ import org.jabref.gui.walkthrough.declarative.NodeResolver; import org.jabref.gui.walkthrough.declarative.WindowResolver; -/** - * Represents a highlight effect configuration for a specific window. - */ +import org.jspecify.annotations.NonNull; + public record WindowEffect( - Optional windowResolver, - HighlightEffect effect, - Optional targetNodeResolver + @NonNull Optional windowResolver, + @NonNull HighlightEffect effect, + @NonNull Optional targetNodeResolver ) { public WindowEffect(HighlightEffect effect) { this(Optional.empty(), effect, Optional.empty()); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java index 959fa18ed75..77a76bfec4a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/ArbitraryJFXBlock.java @@ -6,7 +6,9 @@ import org.jabref.gui.walkthrough.Walkthrough; +import org.jspecify.annotations.NonNull; + public record ArbitraryJFXBlock( - BiFunction componentFactory) + @NonNull BiFunction componentFactory) implements WalkthroughRichTextBlock { } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java index 156505cae5c..36488282843 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/InfoBlock.java @@ -1,5 +1,7 @@ package org.jabref.gui.walkthrough.declarative.richtext; +import org.jspecify.annotations.NonNull; + public record InfoBlock( - String text) implements WalkthroughRichTextBlock { + @NonNull String text) implements WalkthroughRichTextBlock { } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java index 1b498e03035..6f888888323 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/richtext/TextBlock.java @@ -1,4 +1,7 @@ package org.jabref.gui.walkthrough.declarative.richtext; -public record TextBlock(String text) implements WalkthroughRichTextBlock { +import org.jspecify.annotations.NonNull; + +public record TextBlock( + @NonNull String text) implements WalkthroughRichTextBlock { } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index 77624e87bf5..4ac07b75459 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -19,7 +19,7 @@ public record TooltipStep( String title, List content, - @Nullable NodeResolver resolverValue, + NodeResolver resolverValue, @Nullable String continueButtonTextValue, @Nullable String skipButtonTextValue, @Nullable String backButtonTextValue, From da7e8a5359572bd3a55479068be956d4dd2a31bc Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 15:16:58 -0400 Subject: [PATCH 32/50] Fix according to Trag bot. --- .../jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java | 2 +- .../src/main/java/org/jabref/gui/walkthrough/Walkthrough.java | 3 --- .../org/jabref/gui/walkthrough/WalkthroughHighlighter.java | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index fa59db4737a..94078f0230a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -42,7 +42,7 @@ public class SingleWindowWalkthroughOverlay { private final Pane originalRoot; private final StackPane stackPane; private final WalkthroughRenderer renderer; - private final List cleanUpTasks = new ArrayList<>(); + private final List cleanUpTasks = new ArrayList<>(); // needs to be mutable public SingleWindowWalkthroughOverlay(Window window) { this.window = window; diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index f260e994d1e..1fe8638e80f 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -28,9 +28,6 @@ public class Walkthrough { private @Nullable WalkthroughOverlay overlay; private Stage currentStage; - /** - * Creates a new walkthrough with steps - */ public Walkthrough(List steps) { this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java index 99d05ddc75f..2aa6687e505 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java @@ -22,6 +22,7 @@ * Manages highlight effects across multiple windows for walkthrough steps. */ public class WalkthroughHighlighter { + // backdropHighlights, pulseIndicators, and fullScreenDarkens needs to be mutable private final Map backdropHighlights = new HashMap<>(); private final Map pulseIndicators = new HashMap<>(); private final Map fullScreenDarkens = new HashMap<>(); From a184af61b16060ecb66eb6ddac3e8cb8b418925d Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 15:31:53 -0400 Subject: [PATCH 33/50] Fix improper PopOver display --- .../SingleWindowWalkthroughOverlay.java | 55 +++++-------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 94078f0230a..d3184c1d778 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Optional; -import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.geometry.Bounds; import javafx.geometry.Pos; @@ -38,7 +37,6 @@ public class SingleWindowWalkthroughOverlay { private final Window window; private final GridPane overlayPane; - private final PopOver popover; private final Pane originalRoot; private final StackPane stackPane; private final WalkthroughRenderer renderer; @@ -54,8 +52,6 @@ public SingleWindowWalkthroughOverlay(Window window) { overlayPane.setMaxWidth(Double.MAX_VALUE); overlayPane.setMaxHeight(Double.MAX_VALUE); - popover = new PopOver(); - Scene scene = window.getScene(); // This basically never happens, so only a development time check is needed assert scene != null; @@ -74,7 +70,6 @@ public SingleWindowWalkthroughOverlay(Window window) { */ public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { hide(); - // race condition with PopOver showing displayStepContent(step, targetNode, beforeNavigate, walkthrough); } @@ -82,8 +77,6 @@ public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnabl * Hide the overlay and clean up any resources. */ public void hide() { - popover.hide(); - overlayPane.getChildren().clear(); overlayPane.setClip(null); overlayPane.setVisible(true); @@ -121,45 +114,30 @@ private void displayStepContent(WalkthroughStep step, @Nullable Node targetNode, } } - step.navigationPredicate().ifPresent(predicate -> { - if (targetNode == null) { - return; - } - cleanUpTasks.add(predicate.attachListeners(targetNode, beforeNavigate, walkthrough::nextStep)); - }); + if (targetNode == null) { + return; + } + + step.navigationPredicate().ifPresent(predicate -> cleanUpTasks.add(predicate.attachListeners(targetNode, beforeNavigate, walkthrough::nextStep))); } private void displayTooltipStep(Node content, @Nullable Node targetNode, TooltipStep step) { + PopOver popover = new PopOver(); popover.setContentNode(content); popover.setDetachable(false); popover.setCloseButtonEnabled(false); popover.setHeaderAlwaysVisible(false); + mapToArrowLocation(step.position()).ifPresent(popover::setArrowLocation); popover.setAutoFix(true); popover.setAutoHide(false); - mapToArrowLocation(step.position()).ifPresent(popover::setArrowLocation); - Platform.runLater(() -> { - if (targetNode != null) { - if (isNodeReady(targetNode)) { - popover.show(targetNode); - } else { - ChangeListener boundsListener = new ChangeListener<>() { - @Override - public void changed(javafx.beans.value.ObservableValue observable, - Bounds oldValue, Bounds newValue) { - if (newValue.getWidth() > 0 && newValue.getHeight() > 0) { - Platform.runLater(() -> popover.show(targetNode)); - targetNode.boundsInParentProperty().removeListener(this); - } - } - }; - targetNode.boundsInParentProperty().addListener(boundsListener); - cleanUpTasks.add(() -> targetNode.boundsInParentProperty().removeListener(boundsListener)); - } - } else { - popover.show(window); - } - }); + if (targetNode != null) { + popover.show(targetNode); + } else { + popover.show(window); + } + + cleanUpTasks.add(popover::hide); } private void displayPanelStep(Node content, PanelStep step) { @@ -267,9 +245,4 @@ private void setupClipping(Node node) { cleanUpTasks.add(() -> node.boundsInParentProperty().removeListener(listener)); cleanUpTasks.add(() -> overlayPane.setClip(null)); } - - private boolean isNodeReady(Node node) { - Bounds bounds = node.getBoundsInParent(); - return bounds.getWidth() > 0 && bounds.getHeight() > 0; - } } From cb1d9a1288cd1d9abf022ea86054ad53a0925cbb Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 17:03:47 -0400 Subject: [PATCH 34/50] Fix bug for scrolling and window effect --- .../SingleWindowWalkthroughOverlay.java | 52 +++--- .../gui/walkthrough/WalkthroughAction.java | 1 - .../gui/walkthrough/WalkthroughOverlay.java | 13 +- .../gui/walkthrough/WalkthroughUpdater.java | 155 ++++++++++++++++++ .../components/BackdropHighlight.java | 4 +- .../components/FullScreenDarken.java | 2 +- .../components/PulseAnimateIndicator.java | 6 +- .../components/WalkthroughEffect.java | 88 ++-------- .../declarative/step/PanelStep.java | 8 - .../declarative/step/TooltipStep.java | 8 - .../declarative/step/WalkthroughStep.java | 2 - 11 files changed, 202 insertions(+), 137 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index d3184c1d778..339345fdfb2 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -1,7 +1,5 @@ package org.jabref.gui.walkthrough; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; import javafx.beans.value.ChangeListener; @@ -40,7 +38,7 @@ public class SingleWindowWalkthroughOverlay { private final Pane originalRoot; private final StackPane stackPane; private final WalkthroughRenderer renderer; - private final List cleanUpTasks = new ArrayList<>(); // needs to be mutable + private final WalkthroughUpdater updater = new WalkthroughUpdater(); public SingleWindowWalkthroughOverlay(Window window) { this.window = window; @@ -68,7 +66,8 @@ public SingleWindowWalkthroughOverlay(Window window) { /** * Displays a walkthrough step with the specified target node. */ - public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { + public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, + Walkthrough walkthrough) { hide(); displayStepContent(step, targetNode, beforeNavigate, walkthrough); } @@ -80,9 +79,7 @@ public void hide() { overlayPane.getChildren().clear(); overlayPane.setClip(null); overlayPane.setVisible(true); - - cleanUpTasks.forEach(Runnable::run); - cleanUpTasks.clear(); + updater.cleanup(); } /** @@ -99,7 +96,10 @@ public void detach() { } } - private void displayStepContent(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { + private void displayStepContent(WalkthroughStep step, + @Nullable Node targetNode, + Runnable beforeNavigate, + Walkthrough walkthrough) { switch (step) { case TooltipStep tooltipStep -> { Node content = renderer.render(tooltipStep, walkthrough, beforeNavigate); @@ -118,7 +118,8 @@ private void displayStepContent(WalkthroughStep step, @Nullable Node targetNode, return; } - step.navigationPredicate().ifPresent(predicate -> cleanUpTasks.add(predicate.attachListeners(targetNode, beforeNavigate, walkthrough::nextStep))); + step.navigationPredicate().ifPresent(predicate -> updater + .addCleanupTask(predicate.attachListeners(targetNode, beforeNavigate, walkthrough::nextStep))); } private void displayTooltipStep(Node content, @Nullable Node targetNode, TooltipStep step) { @@ -128,16 +129,23 @@ private void displayTooltipStep(Node content, @Nullable Node targetNode, Tooltip popover.setCloseButtonEnabled(false); popover.setHeaderAlwaysVisible(false); mapToArrowLocation(step.position()).ifPresent(popover::setArrowLocation); - popover.setAutoFix(true); popover.setAutoHide(false); + popover.setAutoFix(true); - if (targetNode != null) { - popover.show(targetNode); - } else { + if (targetNode == null) { popover.show(window); + return; } - cleanUpTasks.add(popover::hide); + popover.show(targetNode); + updater.addCleanupTask(popover::hide); + Runnable showPopover = () -> { + if (WalkthroughUpdater.cannotPositionNode(targetNode)) { + return; + } + popover.show(targetNode); + }; + updater.setupScrollContainerListeners(targetNode, showPopover); } private void displayPanelStep(Node content, PanelStep step) { @@ -221,7 +229,7 @@ private Optional mapToArrowLocation(TooltipPosition posit private void hideOverlayPane() { overlayPane.setVisible(false); - cleanUpTasks.add(() -> overlayPane.setVisible(true)); + updater.addCleanupTask(() -> overlayPane.setVisible(true)); } private void setupClipping(Node node) { @@ -232,17 +240,7 @@ private void setupClipping(Node node) { overlayPane.setClip(clip); } }; - - node.boundsInParentProperty().addListener(listener); - - Bounds initialBounds = node.getBoundsInParent(); - if (initialBounds.getWidth() > 0 && initialBounds.getHeight() > 0) { - Rectangle clip = new Rectangle(initialBounds.getMinX(), initialBounds.getMinY(), - initialBounds.getWidth(), initialBounds.getHeight()); - overlayPane.setClip(clip); - } - - cleanUpTasks.add(() -> node.boundsInParentProperty().removeListener(listener)); - cleanUpTasks.add(() -> overlayPane.setClip(null)); + updater.listen(node.boundsInLocalProperty(), listener); + listener.changed(null, null, node.getBoundsInParent()); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index d36acaea1fd..0dfce907bfb 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -72,7 +72,6 @@ private static Map buildRegistry() { node.toString().contains("Linked files"))) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) - .autoFallback(false) .activeWindow(WindowResolver.title("JabRef preferences")) .highlight(new MultiWindowHighlight( new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 5fb53f6755f..561173f9a59 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -122,16 +122,9 @@ private void startNodePolling(WalkthroughStep step, Window window, @Nullable Nod return; } - if (step.autoFallback()) { - LOGGER.info("Node disappeared for step: {}, auto-falling back", step.title()); - stopNodePolling(); - tryRevertToPreviousResolvableStep(); - } else { - LOGGER.info("Node disappeared for step: {}, showing step without node", step.title()); - walkthroughHighlighter.applyHighlight(step.highlight().orElse(null), scene, null); - displayStep(step, window, null); - nodeEverResolved.set(false); - } + LOGGER.info("Node disappeared for step: {}, auto-falling back", step.title()); + stopNodePolling(); + tryRevertToPreviousResolvableStep(); } ); })); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java new file mode 100644 index 00000000000..579ab36635b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java @@ -0,0 +1,155 @@ +package org.jabref.gui.walkthrough; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.ListView; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.ScrollEvent; +import javafx.stage.Window; +import javafx.util.Duration; + +import com.sun.javafx.scene.NodeHelper; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Managing listeners and automatic cleanup in walkthrough + */ +public class WalkthroughUpdater { + private final List cleanupTasks = new ArrayList<>(); // has to be mutable + private final Timeline updateTimeline = new Timeline(); + + /** + * Adds a cleanup task to be executed when cleanup() is called. + */ + public void addCleanupTask(Runnable task) { + cleanupTasks.add(task); + } + + public void listen(@NonNull ObservableValue property, @NonNull InvalidationListener listener) { + property.addListener(listener); + cleanupTasks.add(() -> property.removeListener(listener)); + } + + public void listen(@NonNull ObservableValue property, @NonNull ChangeListener listener) { + property.addListener(listener); + cleanupTasks.add(() -> property.removeListener(listener)); + } + + /** + * Handles scroll events by executing the provided handler and scheduling + * follow-up updates using a timeline. + */ + public void handleScrollEvent(@NonNull Runnable handler) { + handler.run(); + // Schedule updates at 50ms intervals for up to 250ms to handle scroll overshoot + // debounced to prevent excessive updates + for (int i = 0; i <= Math.min(5, 5 - updateTimeline.getKeyFrames().size()); i++) { + KeyFrame keyFrame = new KeyFrame(Duration.millis(i * 50), _ -> handler.run()); + updateTimeline.getKeyFrames().add(keyFrame); + } + updateTimeline.play(); + } + + /** + * Sets up listeners for a node + */ + public void setupNodeListeners(@NonNull Node node, @NonNull Runnable updateHandler) { + InvalidationListener updateListener = _ -> updateHandler.run(); + listen(node.boundsInLocalProperty(), updateListener); + listen(node.localToSceneTransformProperty(), updateListener); + listen(node.boundsInParentProperty(), updateListener); + listen(node.visibleProperty(), updateListener); + listen(node.layoutBoundsProperty(), updateListener); + listen(node.managedProperty(), updateListener); + setupScrollContainerListeners(node, updateHandler); + setupSceneListeners(node, updateHandler); + } + + /** + * Sets up listeners for scroll containers (ScrollPane, ListView) + */ + public void setupScrollContainerListeners(@NonNull Node node, @NonNull Runnable updateHandler) { + EventHandler scrollHandler = _ -> handleScrollEvent(updateHandler); + + Stream.iterate(node, Objects::nonNull, Node::getParent).filter(p -> p instanceof ScrollPane || p instanceof ListView).findFirst().ifPresent(parent -> { + if (parent instanceof ScrollPane scrollPane) { + ChangeListener scrollListener = (_, _, _) -> handleScrollEvent(updateHandler); + listen(scrollPane.vvalueProperty(), scrollListener); + } else if (parent instanceof ListView listView) { + listView.addEventFilter(ScrollEvent.ANY, scrollHandler); + cleanupTasks.add(() -> listView.removeEventFilter(ScrollEvent.ANY, scrollHandler)); + listen(listView.focusModelProperty(), _ -> updateHandler.run()); + } + }); + + node.addEventFilter(ScrollEvent.ANY, scrollHandler); + cleanupTasks.add(() -> node.removeEventFilter(ScrollEvent.ANY, scrollHandler)); + } + + /** + * Sets up listeners for scene and window property changes + */ + public void setupSceneListeners(@NonNull Node node, @NonNull Runnable updateHandler) { + ChangeListener sceneListener = (_, _, scene) -> { + updateHandler.run(); + if (scene == null) { + return; + } + listen(scene.widthProperty(), _ -> updateHandler.run()); + listen(scene.heightProperty(), _ -> updateHandler.run()); + + if (scene.getWindow() != null) { + setupWindowListeners(scene.getWindow(), updateHandler); + } + }; + + listen(node.sceneProperty(), sceneListener); + if (node.getScene() != null) { + sceneListener.changed(null, null, node.getScene()); + } + } + + /** + * Sets up listeners for window property changes. + */ + public void setupWindowListeners(@NonNull Window window, @NonNull Runnable updateHandler) { + listen(window.widthProperty(), _ -> updateHandler.run()); + listen(window.heightProperty(), _ -> updateHandler.run()); + listen(window.showingProperty(), _ -> updateHandler.run()); + } + + /** + * Check if a node is visible in the scene graph + */ + public static boolean isNodeVisible(@Nullable Node node) { + return node != null && NodeHelper.isTreeVisible(node); + } + + /** + * Utility method to check if a node cannot be positioned + */ + public static boolean cannotPositionNode(@Nullable Node node) { + return node == null || node.getScene() == null || !isNodeVisible(node) || node.getBoundsInLocal().isEmpty(); + } + + /** + * Cleans up all registered listeners and stops any active timelines + */ + public void cleanup() { + updateTimeline.stop(); + cleanupTasks.forEach(Runnable::run); + cleanupTasks.clear(); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java index ec9c82cb1f2..9ef65e8bfcf 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java @@ -7,6 +7,8 @@ import javafx.scene.shape.Rectangle; import javafx.scene.shape.Shape; +import org.jabref.gui.walkthrough.WalkthroughUpdater; + import org.jspecify.annotations.NonNull; /** @@ -57,7 +59,7 @@ protected void initializeEffect() { @Override protected void updateLayout() { - if (cannotPositionNode(node)) { + if (WalkthroughUpdater.cannotPositionNode(node)) { hideEffect(); return; } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java index edad165f3aa..fd085a3ed51 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java @@ -33,10 +33,10 @@ protected void initializeEffect() { * Attaches the effect to the pane */ public void attach() { + updater.cleanup(); if (overlay == null) { initializeEffect(); } - cleanUp(); setupPaneListeners(); updateLayout(); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java index d0d24f3f07d..32132195512 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java @@ -10,6 +10,8 @@ import javafx.scene.shape.Circle; import javafx.util.Duration; +import org.jabref.gui.walkthrough.WalkthroughUpdater; + import org.jspecify.annotations.NonNull; /** @@ -26,7 +28,7 @@ public PulseAnimateIndicator(@NonNull Pane pane) { } public void attach(@NonNull Node node) { - cleanUp(); + updater.cleanup(); if (pulseIndicator == null) { initializeEffect(); } @@ -74,7 +76,7 @@ protected void initializeEffect() { @Override protected void updateLayout() { - if (cannotPositionNode(node)) { + if (WalkthroughUpdater.cannotPositionNode(node)) { hideEffect(); return; } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java index 4c68163dbe5..21aa0360ec4 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java @@ -1,27 +1,18 @@ package org.jabref.gui.walkthrough.components; -import java.util.ArrayList; -import java.util.List; - -import javafx.beans.InvalidationListener; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.scene.Node; -import javafx.scene.Scene; import javafx.scene.layout.Pane; -import javafx.stage.Window; -import com.sun.javafx.scene.NodeHelper; +import org.jabref.gui.walkthrough.WalkthroughUpdater; + import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; /** * Base class for walkthrough effects with common listener management and positioning. */ public abstract class WalkthroughEffect { protected final Pane pane; - protected final List cleanupTasks = new ArrayList<>(); // needs to be mutable - protected final InvalidationListener updateListener = _ -> updateLayout(); + protected final WalkthroughUpdater updater = new WalkthroughUpdater(); protected WalkthroughEffect(@NonNull Pane pane) { this.pane = pane; @@ -34,85 +25,28 @@ protected WalkthroughEffect(@NonNull Pane pane) { protected abstract void hideEffect(); - /** - * Detaches the effect, cleaning up listeners and hiding the effect. - */ public void detach() { - cleanUp(); + updater.cleanup(); hideEffect(); } - protected void cleanUp() { - cleanupTasks.forEach(Runnable::run); - cleanupTasks.clear(); - } - - protected void addListener(ObservableValue property) { - property.addListener(updateListener); - cleanupTasks.add(() -> property.removeListener(updateListener)); - } - - protected void addListener(ObservableValue property, ChangeListener listener) { - property.addListener(listener); - cleanupTasks.add(() -> property.removeListener(listener)); - } - protected void setupNodeListeners(@NonNull Node node) { - addListener(node.boundsInLocalProperty()); - addListener(node.localToSceneTransformProperty()); - addListener(node.boundsInParentProperty()); - addListener(node.layoutBoundsProperty()); - addListener(node.visibleProperty()); - - ChangeListener sceneListener = (_, oldScene, newScene) -> { - if (oldScene != null) { - oldScene.widthProperty().removeListener(updateListener); - oldScene.heightProperty().removeListener(updateListener); - } - if (newScene != null) { - addListener(newScene.widthProperty()); - addListener(newScene.heightProperty()); - if (newScene.getWindow() != null) { - Window window = newScene.getWindow(); - addListener(window.widthProperty()); - addListener(window.heightProperty()); - addListener(window.showingProperty()); - } - } - updateLayout(); - }; - - addListener(node.sceneProperty(), sceneListener); - if (node.getScene() != null) { - sceneListener.changed(null, null, node.getScene()); - } + updater.setupNodeListeners(node, this::updateLayout); } protected void setupPaneListeners() { - addListener(pane.widthProperty()); - addListener(pane.heightProperty()); - addListener(pane.sceneProperty(), (_, _, newScene) -> { + updater.listen(pane.widthProperty(), _ -> updateLayout()); + updater.listen(pane.heightProperty(), _ -> updateLayout()); + updater.listen(pane.sceneProperty(), (_, _, newScene) -> { updateLayout(); if (newScene == null) { return; } - addListener(newScene.heightProperty()); - addListener(newScene.widthProperty()); + updater.listen(newScene.heightProperty(), _ -> updateLayout()); + updater.listen(newScene.widthProperty(), _ -> updateLayout()); if (newScene.getWindow() != null) { - addListener(newScene.getWindow().widthProperty()); - addListener(newScene.getWindow().heightProperty()); + updater.setupWindowListeners(newScene.getWindow(), this::updateLayout); } }); } - - protected boolean isNodeVisible(@Nullable Node node) { - return node != null && NodeHelper.isTreeVisible(node); - } - - protected boolean cannotPositionNode(@Nullable Node node) { - return node == null || - node.getScene() == null || - !isNodeVisible(node) || - node.getBoundsInLocal().isEmpty(); - } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index dcfbbf322e2..7e386e126f3 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -27,7 +27,6 @@ public record PanelStep( @Nullable Double widthValue, @Nullable Double heightValue, @Nullable MultiWindowHighlight highlightValue, - boolean autoFallback, @Nullable WindowResolver activeWindowResolverValue) implements WalkthroughStep { @Override @@ -91,7 +90,6 @@ public static class Builder { private @Nullable Double width; private @Nullable Double height; private @Nullable MultiWindowHighlight highlight; - private boolean autoFallback = true; private @Nullable WindowResolver activeWindowResolver; private Builder(@NonNull String title) { @@ -161,11 +159,6 @@ public Builder highlight(@NonNull HighlightEffect effect) { return highlight(new WindowEffect(effect)); } - public Builder autoFallback(boolean autoFallback) { - this.autoFallback = autoFallback; - return this; - } - public Builder activeWindow(@NonNull WindowResolver activeWindowResolver) { this.activeWindowResolver = activeWindowResolver; return this; @@ -189,7 +182,6 @@ public PanelStep build() { width, height, highlight, - autoFallback, activeWindowResolver); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index 4ac07b75459..8287143def0 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -28,7 +28,6 @@ public record TooltipStep( @Nullable Double widthValue, @Nullable Double heightValue, @Nullable MultiWindowHighlight highlightValue, - boolean autoFallback, @Nullable WindowResolver activeWindowResolverValue ) implements WalkthroughStep { @@ -93,7 +92,6 @@ public static class Builder { private @Nullable Double width; private @Nullable Double height; private @Nullable MultiWindowHighlight highlight; - private boolean autoFallback = true; private @Nullable WindowResolver activeWindowResolver; private Builder(@NonNull String title) { @@ -163,11 +161,6 @@ public Builder highlight(@NonNull HighlightEffect effect) { return highlight(new WindowEffect(effect)); } - public Builder autoFallback(boolean autoFallback) { - this.autoFallback = autoFallback; - return this; - } - public Builder activeWindow(@NonNull WindowResolver activeWindowResolver) { this.activeWindowResolver = activeWindowResolver; return this; @@ -188,7 +181,6 @@ public TooltipStep build() { width, height, highlight, - autoFallback, activeWindowResolver); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java index 019c771e2b0..79ba5343f98 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/WalkthroughStep.java @@ -29,8 +29,6 @@ public sealed interface WalkthroughStep permits PanelStep, TooltipStep { OptionalDouble height(); - boolean autoFallback(); - Optional highlight(); Optional activeWindowResolver(); From bfe9be7288b5c64de98bd04de5bb7d7caaddc297 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 17:16:32 -0400 Subject: [PATCH 35/50] Fix according to Trag --- .../SingleWindowWalkthroughOverlay.java | 52 +++++++++---------- .../jabref/gui/walkthrough/Walkthrough.java | 3 -- .../gui/walkthrough/WalkthroughAction.java | 8 +-- .../walkthrough/WalkthroughHighlighter.java | 6 +-- .../gui/walkthrough/WalkthroughOverlay.java | 6 --- .../declarative/effect/HighlightEffect.java | 6 +-- .../BackdropHighlight.java | 29 ++++------- .../FullScreenDarken.java | 5 +- .../PulseAnimateIndicator.java | 5 +- .../WalkthroughEffect.java | 17 +++++- .../main/resources/l10n/JabRef_en.properties | 2 +- 11 files changed, 60 insertions(+), 79 deletions(-) rename jabgui/src/main/java/org/jabref/gui/walkthrough/{components => effects}/BackdropHighlight.java (77%) rename jabgui/src/main/java/org/jabref/gui/walkthrough/{components => effects}/FullScreenDarken.java (94%) rename jabgui/src/main/java/org/jabref/gui/walkthrough/{components => effects}/PulseAnimateIndicator.java (96%) rename jabgui/src/main/java/org/jabref/gui/walkthrough/{components => effects}/WalkthroughEffect.java (68%) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 339345fdfb2..93631813981 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -66,10 +66,32 @@ public SingleWindowWalkthroughOverlay(Window window) { /** * Displays a walkthrough step with the specified target node. */ - public void displayStep(WalkthroughStep step, @Nullable Node targetNode, Runnable beforeNavigate, + public void displayStep(WalkthroughStep step, + @Nullable Node targetNode, + Runnable beforeNavigate, Walkthrough walkthrough) { hide(); - displayStepContent(step, targetNode, beforeNavigate, walkthrough); + + switch (step) { + case TooltipStep tooltipStep -> { + Node content = renderer.render(tooltipStep, walkthrough, beforeNavigate); + displayTooltipStep(content, targetNode, tooltipStep); + hideOverlayPane(); + } + case PanelStep panelStep -> { + Node content = renderer.render(panelStep, walkthrough, beforeNavigate); + displayPanelStep(content, panelStep); + setupClipping(content); + overlayPane.toFront(); + } + } + + if (targetNode == null) { + return; + } + + step.navigationPredicate().ifPresent(predicate -> updater + .addCleanupTask(predicate.attachListeners(targetNode, beforeNavigate, walkthrough::nextStep))); } /** @@ -96,32 +118,6 @@ public void detach() { } } - private void displayStepContent(WalkthroughStep step, - @Nullable Node targetNode, - Runnable beforeNavigate, - Walkthrough walkthrough) { - switch (step) { - case TooltipStep tooltipStep -> { - Node content = renderer.render(tooltipStep, walkthrough, beforeNavigate); - displayTooltipStep(content, targetNode, tooltipStep); - hideOverlayPane(); - } - case PanelStep panelStep -> { - Node content = renderer.render(panelStep, walkthrough, beforeNavigate); - displayPanelStep(content, panelStep); - setupClipping(content); - overlayPane.toFront(); - } - } - - if (targetNode == null) { - return; - } - - step.navigationPredicate().ifPresent(predicate -> updater - .addCleanupTask(predicate.attachListeners(targetNode, beforeNavigate, walkthrough::nextStep))); - } - private void displayTooltipStep(Node content, @Nullable Node targetNode, TooltipStep step) { PopOver popover = new PopOver(); popover.setContentNode(content); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 1fe8638e80f..770f4362c68 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -34,9 +34,6 @@ public Walkthrough(List steps) { this.steps = steps; } - /** - * Creates a new walkthrough with steps - */ public Walkthrough(WalkthroughStep... steps) { this(List.of(steps)); } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 0dfce907bfb..63f1664c5d8 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -1,6 +1,5 @@ package org.jabref.gui.walkthrough; -import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -41,8 +40,6 @@ public void execute() { } private static Map buildRegistry() { - Map registry = new HashMap<>(); - WalkthroughStep step1 = TooltipStep .builder("Click on \"File\" menu") .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) @@ -96,7 +93,7 @@ private static Map buildRegistry() { .build(); WalkthroughStep step5 = PanelStep - .builder("Click \"OK\" to save changes") + .builder("Click \"Save\" to save changes") .content( new TextBlock("Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents."), new InfoBlock("Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks") @@ -113,7 +110,6 @@ private static Map buildRegistry() { .build(); Walkthrough mainFileDirectory = new Walkthrough(step1, step2, step3, step4, step5); - registry.put("mainFileDirectory", mainFileDirectory); - return registry; + return Map.of("mainFileDirectory", mainFileDirectory); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java index 2aa6687e505..9560a947f0f 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java @@ -8,12 +8,12 @@ import javafx.scene.layout.Pane; import javafx.stage.Window; -import org.jabref.gui.walkthrough.components.BackdropHighlight; -import org.jabref.gui.walkthrough.components.FullScreenDarken; -import org.jabref.gui.walkthrough.components.PulseAnimateIndicator; import org.jabref.gui.walkthrough.declarative.WindowResolver; import org.jabref.gui.walkthrough.declarative.effect.HighlightEffect; import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; +import org.jabref.gui.walkthrough.effects.BackdropHighlight; +import org.jabref.gui.walkthrough.effects.FullScreenDarken; +import org.jabref.gui.walkthrough.effects.PulseAnimateIndicator; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 561173f9a59..d1f95ab0511 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -21,9 +21,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Manages walkthrough overlays and highlights across multiple windows. - */ public class WalkthroughOverlay { private static final Logger LOGGER = LoggerFactory.getLogger(WalkthroughOverlay.class); @@ -57,9 +54,6 @@ public void displayStep(@NonNull WalkthroughStep step) { } } - /** - * Detaches all overlays - */ public void detachAll() { stopNodePolling(); walkthroughHighlighter.detachAll(); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java index ecfe78ea355..6cb59a6db47 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/effect/HighlightEffect.java @@ -2,17 +2,17 @@ public enum HighlightEffect { /** - * See {@link org.jabref.gui.walkthrough.components.BackdropHighlight} + * See {@link org.jabref.gui.walkthrough.effects.BackdropHighlight} */ BACKDROP_HIGHLIGHT, /** - * See {@link org.jabref.gui.walkthrough.components.PulseAnimateIndicator} + * See {@link org.jabref.gui.walkthrough.effects.PulseAnimateIndicator} */ ANIMATED_PULSE, /** - * See {@link org.jabref.gui.walkthrough.components.FullScreenDarken} + * See {@link org.jabref.gui.walkthrough.effects.FullScreenDarken} */ FULL_SCREEN_DARKEN, diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/BackdropHighlight.java similarity index 77% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/effects/BackdropHighlight.java index 9ef65e8bfcf..f60f8b95200 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/BackdropHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/BackdropHighlight.java @@ -1,4 +1,4 @@ -package org.jabref.gui.walkthrough.components; +package org.jabref.gui.walkthrough.effects; import javafx.geometry.Bounds; import javafx.scene.Node; @@ -10,17 +10,15 @@ import org.jabref.gui.walkthrough.WalkthroughUpdater; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; -/** - * A backdrop highlight effect that dims the background and highlights a specific node. - */ public class BackdropHighlight extends WalkthroughEffect { private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); - private Node node; - private Rectangle backdrop; - private Rectangle hole; - private Shape overlayShape; + private @Nullable Node node; + private final @NonNull Rectangle backdrop = new Rectangle(); + private final @NonNull Rectangle hole = new Rectangle(); + private @Nullable Shape overlayShape; public BackdropHighlight(@NonNull Pane pane) { super(pane); @@ -49,8 +47,6 @@ public void detach() { @Override protected void initializeEffect() { - this.backdrop = new Rectangle(); - this.hole = new Rectangle(); this.overlayShape = Shape.subtract(backdrop, hole); this.overlayShape.setFill(OVERLAY_COLOR); this.overlayShape.setVisible(false); @@ -64,15 +60,9 @@ protected void updateLayout() { return; } - Bounds nodeBoundsInScene; - try { - nodeBoundsInScene = node.localToScene(node.getBoundsInLocal()); - } catch (IllegalStateException e) { - hideEffect(); - return; - } + Bounds bounds = node.localToScene(node.getBoundsInLocal()); - if (nodeBoundsInScene == null || nodeBoundsInScene.getWidth() <= 0 || nodeBoundsInScene.getHeight() <= 0) { + if (bounds == null || bounds.getWidth() <= 0 || bounds.getHeight() <= 0) { hideEffect(); return; } @@ -82,7 +72,7 @@ protected void updateLayout() { backdrop.setWidth(pane.getWidth()); backdrop.setHeight(pane.getHeight()); - Bounds nodeBoundsInRootPane = pane.sceneToLocal(nodeBoundsInScene); + Bounds nodeBoundsInRootPane = pane.sceneToLocal(bounds); hole.setX(nodeBoundsInRootPane.getMinX()); hole.setY(nodeBoundsInRootPane.getMinY()); hole.setWidth(nodeBoundsInRootPane.getWidth()); @@ -103,6 +93,7 @@ protected void updateLayout() { @Override protected void hideEffect() { + assert overlayShape != null : "Overlay shape should be initialized before hiding effect"; overlayShape.setVisible(false); } } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/FullScreenDarken.java similarity index 94% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/effects/FullScreenDarken.java index fd085a3ed51..91a11091fad 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/FullScreenDarken.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/FullScreenDarken.java @@ -1,4 +1,4 @@ -package org.jabref.gui.walkthrough.components; +package org.jabref.gui.walkthrough.effects; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; @@ -29,9 +29,6 @@ protected void initializeEffect() { this.pane.getChildren().add(overlay); } - /** - * Attaches the effect to the pane - */ public void attach() { updater.cleanup(); if (overlay == null) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/PulseAnimateIndicator.java similarity index 96% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/effects/PulseAnimateIndicator.java index 32132195512..bad3b311f75 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/PulseAnimateIndicator.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/PulseAnimateIndicator.java @@ -1,4 +1,4 @@ -package org.jabref.gui.walkthrough.components; +package org.jabref.gui.walkthrough.effects; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; @@ -14,9 +14,6 @@ import org.jspecify.annotations.NonNull; -/** - * A pulsing circular indicator that can be attached to a target node. - */ public class PulseAnimateIndicator extends WalkthroughEffect { public static final int INDICATOR_OFFSET = 5; private Circle pulseIndicator; diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/WalkthroughEffect.java similarity index 68% rename from jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java rename to jabgui/src/main/java/org/jabref/gui/walkthrough/effects/WalkthroughEffect.java index 21aa0360ec4..ed894f8f9b1 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/components/WalkthroughEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/WalkthroughEffect.java @@ -1,4 +1,4 @@ -package org.jabref.gui.walkthrough.components; +package org.jabref.gui.walkthrough.effects; import javafx.scene.Node; import javafx.scene.layout.Pane; @@ -8,12 +8,17 @@ import org.jspecify.annotations.NonNull; /** - * Base class for walkthrough effects with common listener management and positioning. + * Base class for walkthrough effects BackdropHighlight, TooltipHighlight, FullScreenDarken, etc. */ public abstract class WalkthroughEffect { protected final Pane pane; protected final WalkthroughUpdater updater = new WalkthroughUpdater(); + /** + * Constructor for WalkthroughEffect. + * + * @param pane The pane where the effect will be applied. Usually obtained from window.getScene().getRoot(). + */ protected WalkthroughEffect(@NonNull Pane pane) { this.pane = pane; initializeEffect(); @@ -23,8 +28,16 @@ protected WalkthroughEffect(@NonNull Pane pane) { protected abstract void updateLayout(); + /** + * Hide the effect, e.g., by making it invisible. The effect should not be removed from the pane, + * and the scene graph is not modified. + */ protected abstract void hideEffect(); + /** + * Detach the effect, cleaning up any resources and listeners. The effect is no longer active + * and reattaching will require scene graph modifications. + */ public void detach() { updater.cleanup(); hideEffect(); diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 9f97c97d509..2ca3c0bd3b7 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2950,7 +2950,7 @@ Select\ the\ "Linked\ files"\ tab=Select the "Linked files" tab This\ section\ manages\ how\ JabRef\ handles\ your\ PDF\ files\ and\ other\ documents.=This section manages how JabRef handles your PDF files and other documents. Enable\ "Main\ file\ directory"\ option=Enable "Main file directory" option Choose\ this\ option\ to\ tell\ JabRef\ where\ your\ research\ files\ are\ stored.\ This\ makes\ it\ easy\ to\ attach\ PDFs\ and\ other\ documents\ to\ your\ bibliography\ entries.\ You\ can\ browse\ to\ select\ your\ preferred\ folder\ in\ the\ next\ step.=Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step. -Click\ "OK"\ to\ save\ changes=Click "OK" to save changes +Click\ "Save"\ to\ save\ changes=Click "Save" to save changes Congratulations!\ Your\ main\ file\ directory\ is\ now\ configured.\ JabRef\ will\ use\ this\ location\ to\ automatically\ find\ and\ organize\ your\ research\ documents.=Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents. Additional\ information\ on\ main\ file\ directory\ can\ be\ found\ in\ https\:\\\\docs.jabref.org\\v5\\finding-sorting-and-cleaning-entries\\filelinks=Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks From 4979337c31804d5017d43c376559ce63c433192c Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 17:29:37 -0400 Subject: [PATCH 36/50] Additional fix per Trag --- .../SingleWindowWalkthroughOverlay.java | 26 +++++++++++++++---- .../gui/walkthrough/WalkthroughOverlay.java | 7 ++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 93631813981..74e0f73e666 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -64,12 +64,28 @@ public SingleWindowWalkthroughOverlay(Window window) { } /** - * Displays a walkthrough step with the specified target node. + * Displays a walkthrough step with a target node. */ - public void displayStep(WalkthroughStep step, - @Nullable Node targetNode, - Runnable beforeNavigate, - Walkthrough walkthrough) { + public void displayStepWithTarget(WalkthroughStep step, + Node targetNode, + Runnable beforeNavigate, + Walkthrough walkthrough) { + displayStep(step, targetNode, beforeNavigate, walkthrough); + } + + /** + * Displays a walkthrough step without a target node. + */ + public void displayStepWithoutTarget(WalkthroughStep step, + Runnable beforeNavigate, + Walkthrough walkthrough) { + displayStep(step, null, beforeNavigate, walkthrough); + } + + private void displayStep(WalkthroughStep step, + @Nullable Node targetNode, + Runnable beforeNavigate, + Walkthrough walkthrough) { hide(); switch (step) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index d1f95ab0511..8e0fc28d630 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -81,7 +81,12 @@ private void tryRevertToPreviousResolvableStep() { } private void displayStep(WalkthroughStep step, Window window, @Nullable Node node) { - getOrCreateOverlay(window).displayStep(step, node, this::stopNodePolling, walkthrough); + SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(window); + if (node != null) { + overlay.displayStepWithTarget(step, node, this::stopNodePolling, walkthrough); + } else { + overlay.displayStepWithoutTarget(step, this::stopNodePolling, walkthrough); + } } private void startNodePolling(WalkthroughStep step, Window window, @Nullable Node node) { From 57353e433802194f815ac80b995ae06b02d79d7c Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 17:37:46 -0400 Subject: [PATCH 37/50] Fix backdrop highlight NPE --- .../jabref/gui/walkthrough/effects/BackdropHighlight.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/BackdropHighlight.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/BackdropHighlight.java index f60f8b95200..fb58f66da75 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/BackdropHighlight.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/BackdropHighlight.java @@ -16,8 +16,8 @@ public class BackdropHighlight extends WalkthroughEffect { private static final Color OVERLAY_COLOR = Color.rgb(0, 0, 0, 0.55); private @Nullable Node node; - private final @NonNull Rectangle backdrop = new Rectangle(); - private final @NonNull Rectangle hole = new Rectangle(); + private Rectangle backdrop; + private Rectangle hole; private @Nullable Shape overlayShape; public BackdropHighlight(@NonNull Pane pane) { @@ -47,6 +47,8 @@ public void detach() { @Override protected void initializeEffect() { + this.backdrop = new Rectangle(); + this.hole = new Rectangle(); this.overlayShape = Shape.subtract(backdrop, hole); this.overlayShape.setFill(OVERLAY_COLOR); this.overlayShape.setVisible(false); From da50fcd598432024a8516aeb0c1c2a0a95afe2b0 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 19:58:07 -0400 Subject: [PATCH 38/50] Fix localization issue --- .../gui/walkthrough/WalkthroughAction.java | 18 ++++++++---------- .../declarative/step/PanelStep.java | 5 +++-- .../declarative/step/TooltipStep.java | 4 ++-- .../main/resources/l10n/JabRef_en.properties | 7 +------ 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 63f1664c5d8..6c18c8b5ce6 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -41,7 +41,7 @@ public void execute() { private static Map buildRegistry() { WalkthroughStep step1 = TooltipStep - .builder("Click on \"File\" menu") + .builder(Localization.lang("Click on \"File\" menu")) .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.BOTTOM) @@ -49,7 +49,7 @@ private static Map buildRegistry() { .build(); WalkthroughStep step2 = TooltipStep - .builder("Click on \"Preferences\"") + .builder(Localization.lang("Click on \"Preferences\"")) .resolver(NodeResolver.menuItem("Preferences")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.RIGHT) @@ -61,7 +61,7 @@ private static Map buildRegistry() { .build(); WalkthroughStep step3 = TooltipStep - .builder("Select the \"Linked files\" tab") + .builder(Localization.lang("Select the \"Linked files\" tab")) .content(new TextBlock(Localization.lang("This section manages how JabRef handles your PDF files and other documents."))) .width(400) .resolver(NodeResolver.predicate(node -> @@ -77,10 +77,8 @@ private static Map buildRegistry() { .build(); WalkthroughStep step4 = TooltipStep - .builder("Enable \"Main file directory\" option") - .content(new TextBlock(""" - Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step. - """)) + .builder(Localization.lang("Enable \"Main file directory\" option")) + .content(new TextBlock(Localization.lang("Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step."))) .width(400) .resolver(NodeResolver.fxId("useMainFileDirectory")) .navigation(NavigationPredicate.onClick()) @@ -93,10 +91,10 @@ private static Map buildRegistry() { .build(); WalkthroughStep step5 = PanelStep - .builder("Click \"Save\" to save changes") + .builder(Localization.lang("Click \"Save\" to save changes")) .content( - new TextBlock("Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents."), - new InfoBlock("Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks") + new TextBlock(Localization.lang("Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents.")), + new InfoBlock(Localization.lang("Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks")) ) .height(180) .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("button") && node.toString().contains(Localization.lang("Save")))) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index 7e386e126f3..1634bc2298d 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -11,6 +11,7 @@ import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; +import org.jabref.logic.l10n.Localization; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -74,8 +75,8 @@ public Optional activeWindowResolver() { return Optional.ofNullable(activeWindowResolverValue); } - public static Builder builder(String title) { - return new Builder(title); + public static Builder builder(String key) { + return new Builder(key); } public static class Builder { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index 8287143def0..5f67f1f23cd 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -76,8 +76,8 @@ public Optional activeWindowResolver() { return Optional.ofNullable(activeWindowResolverValue); } - public static Builder builder(String key, Object... params) { - return new Builder(Localization.lang(key, params)); + public static Builder builder(String title) { + return new Builder(title); } public static class Builder { diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index b8a2a6f7c94..b1c4ef13fa1 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2951,14 +2951,9 @@ You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) c Configure\ main\ file\ directory=Configure main file directory Walkthroughs=Walkthroughs Saving\ Your\ Work=Saving Your Work -Skip\ for\ Now=Skip for Now Browse...=Browse... Skip=Skip Finish=Finish -Skip\ to\ finish=Skip to Finish -Start\ walkthrough=Start Walkthrough -Step\ %0\ of\ %1=Step %0 of %1 -Complete\ walkthrough=Complete Walkthrough Back=Back Click\ on\ "File"\ menu=Click on "File" menu Click\ on\ "Preferences"=Click on "Preferences" @@ -2968,7 +2963,7 @@ Enable\ "Main\ file\ directory"\ option=Enable "Main file directory" option Choose\ this\ option\ to\ tell\ JabRef\ where\ your\ research\ files\ are\ stored.\ This\ makes\ it\ easy\ to\ attach\ PDFs\ and\ other\ documents\ to\ your\ bibliography\ entries.\ You\ can\ browse\ to\ select\ your\ preferred\ folder\ in\ the\ next\ step.=Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step. Click\ "Save"\ to\ save\ changes=Click "Save" to save changes Congratulations!\ Your\ main\ file\ directory\ is\ now\ configured.\ JabRef\ will\ use\ this\ location\ to\ automatically\ find\ and\ organize\ your\ research\ documents.=Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents. -Additional\ information\ on\ main\ file\ directory\ can\ be\ found\ in\ https\:\\\\docs.jabref.org\\v5\\finding-sorting-and-cleaning-entries\\filelinks=Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks +Additional\ information\ on\ main\ file\ directory\ can\ be\ found\ in\ https\://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks=Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks # CommandLine Available\ export\ formats\:=Available export formats: From 7b284be9d0bdf0f3f92ce23b32c58e234b29c09c Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 22:18:22 -0400 Subject: [PATCH 39/50] Fix according to Trag bot --- .../jabref/gui/walkthrough/WalkthroughAction.java | 2 +- .../gui/walkthrough/declarative/step/PanelStep.java | 11 +++++------ .../walkthrough/declarative/step/TooltipStep.java | 13 ++++++------- jablib/src/main/resources/l10n/JabRef_en.properties | 8 ++------ 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 6c18c8b5ce6..8906a8a9092 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -93,7 +93,7 @@ private static Map buildRegistry() { WalkthroughStep step5 = PanelStep .builder(Localization.lang("Click \"Save\" to save changes")) .content( - new TextBlock(Localization.lang("Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents.")), + new TextBlock(Localization.lang("Congratulations. Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents.")), new InfoBlock(Localization.lang("Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks")) ) .height(180) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java index 1634bc2298d..e94853f2e08 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/PanelStep.java @@ -11,20 +11,19 @@ import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; -import org.jabref.logic.l10n.Localization; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; public record PanelStep( - String title, - List content, + @NonNull String title, + @NonNull List content, @Nullable NodeResolver resolverValue, @Nullable String continueButtonTextValue, @Nullable String skipButtonTextValue, @Nullable String backButtonTextValue, @Nullable NavigationPredicate navigationPredicateValue, - PanelPosition position, + @NonNull PanelPosition position, @Nullable Double widthValue, @Nullable Double heightValue, @Nullable MultiWindowHighlight highlightValue, @@ -75,8 +74,8 @@ public Optional activeWindowResolver() { return Optional.ofNullable(activeWindowResolverValue); } - public static Builder builder(String key) { - return new Builder(key); + public static Builder builder(@NonNull String title) { + return new Builder(title); } public static class Builder { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java index 5f67f1f23cd..3b29f32649d 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/step/TooltipStep.java @@ -11,20 +11,19 @@ import org.jabref.gui.walkthrough.declarative.effect.MultiWindowHighlight; import org.jabref.gui.walkthrough.declarative.effect.WindowEffect; import org.jabref.gui.walkthrough.declarative.richtext.WalkthroughRichTextBlock; -import org.jabref.logic.l10n.Localization; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; public record TooltipStep( - String title, - List content, - NodeResolver resolverValue, + @NonNull String title, + @NonNull List content, + @NonNull NodeResolver resolverValue, @Nullable String continueButtonTextValue, @Nullable String skipButtonTextValue, @Nullable String backButtonTextValue, @Nullable NavigationPredicate navigationPredicateValue, - TooltipPosition position, + @NonNull TooltipPosition position, @Nullable Double widthValue, @Nullable Double heightValue, @Nullable MultiWindowHighlight highlightValue, @@ -33,7 +32,7 @@ public record TooltipStep( @Override public Optional resolver() { - return Optional.ofNullable(resolverValue); + return Optional.of(resolverValue); } @Override @@ -76,7 +75,7 @@ public Optional activeWindowResolver() { return Optional.ofNullable(activeWindowResolverValue); } - public static Builder builder(String title) { + public static Builder builder(@NonNull String title) { return new Builder(title); } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index b1c4ef13fa1..347c730ba91 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2950,11 +2950,7 @@ You\ must\ specify\ one\ (or\ more)\ citations.=You must specify one (or more) c # Walkthrough Configure\ main\ file\ directory=Configure main file directory Walkthroughs=Walkthroughs -Saving\ Your\ Work=Saving Your Work -Browse...=Browse... -Skip=Skip -Finish=Finish -Back=Back +# Configure main file directory walkthrough Click\ on\ "File"\ menu=Click on "File" menu Click\ on\ "Preferences"=Click on "Preferences" Select\ the\ "Linked\ files"\ tab=Select the "Linked files" tab @@ -2962,7 +2958,7 @@ This\ section\ manages\ how\ JabRef\ handles\ your\ PDF\ files\ and\ other\ docu Enable\ "Main\ file\ directory"\ option=Enable "Main file directory" option Choose\ this\ option\ to\ tell\ JabRef\ where\ your\ research\ files\ are\ stored.\ This\ makes\ it\ easy\ to\ attach\ PDFs\ and\ other\ documents\ to\ your\ bibliography\ entries.\ You\ can\ browse\ to\ select\ your\ preferred\ folder\ in\ the\ next\ step.=Choose this option to tell JabRef where your research files are stored. This makes it easy to attach PDFs and other documents to your bibliography entries. You can browse to select your preferred folder in the next step. Click\ "Save"\ to\ save\ changes=Click "Save" to save changes -Congratulations!\ Your\ main\ file\ directory\ is\ now\ configured.\ JabRef\ will\ use\ this\ location\ to\ automatically\ find\ and\ organize\ your\ research\ documents.=Congratulations! Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents. +Congratulations.\ Your\ main\ file\ directory\ is\ now\ configured.\ JabRef\ will\ use\ this\ location\ to\ automatically\ find\ and\ organize\ your\ research\ documents.=Congratulations. Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents. Additional\ information\ on\ main\ file\ directory\ can\ be\ found\ in\ https\://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks=Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks # CommandLine From 66e85af06ef698718da8c82076bb76d3aba66d71 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 20 Jun 2025 22:24:41 -0400 Subject: [PATCH 40/50] Fix WalkthroughStep3's LinkedFiles issue --- .../main/java/org/jabref/gui/walkthrough/WalkthroughAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 8906a8a9092..3f1622a5a5c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -66,7 +66,7 @@ private static Map buildRegistry() { .width(400) .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("list-cell") && - node.toString().contains("Linked files"))) + node.toString().contains(Localization.lang("Linked files")))) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) .activeWindow(WindowResolver.title("JabRef preferences")) From c635e33233a1f1d0572387576005783060e4a455 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Sun, 22 Jun 2025 12:37:05 -0400 Subject: [PATCH 41/50] Fix the issue where PopOver rendered unstyled --- .../jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java | 1 + .../java/org/jabref/gui/walkthrough/WalkthroughRenderer.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 74e0f73e666..9a4049a057e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -136,6 +136,7 @@ public void detach() { private void displayTooltipStep(Node content, @Nullable Node targetNode, TooltipStep step) { PopOver popover = new PopOver(); + popover.getScene().getStylesheets().setAll(window.getScene().getStylesheets()); popover.setContentNode(content); popover.setDetachable(false); popover.setCloseButtonEnabled(false); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java index 672edea7149..bf9a7939ff6 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -34,7 +34,7 @@ public class WalkthroughRenderer { */ public Node render(TooltipStep step, Walkthrough walkthrough, Runnable beforeNavigate) { VBox tooltip = new VBox(); - tooltip.getStyleClass().add("walkthrough-tooltip-content-container"); + tooltip.getStyleClass().addAll("root", "walkthrough-tooltip-content-container"); Label titleLabel = new Label(Localization.lang(step.title())); titleLabel.getStyleClass().add("walkthrough-tooltip-title"); From 62147cf7af111e81283d0372c43cc901ed4a1abf Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Thu, 26 Jun 2025 15:37:26 -0400 Subject: [PATCH 42/50] Update Walkthrough comments --- .../java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java index 9560a947f0f..ade463972d3 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughHighlighter.java @@ -22,7 +22,7 @@ * Manages highlight effects across multiple windows for walkthrough steps. */ public class WalkthroughHighlighter { - // backdropHighlights, pulseIndicators, and fullScreenDarkens needs to be mutable + // backdropHighlights, pulseIndicators, and fullScreenDarkens represents the current state of walkthrough GUI effects private final Map backdropHighlights = new HashMap<>(); private final Map pulseIndicators = new HashMap<>(); private final Map fullScreenDarkens = new HashMap<>(); From 3f775832e856e5751ee3e2477694086f11fd2a5a Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 27 Jun 2025 15:02:43 -0400 Subject: [PATCH 43/50] Comment on the walkthrough and fix the switch expression --- .../SingleWindowWalkthroughOverlay.java | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 9a4049a057e..c88d23e4297 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -136,7 +136,7 @@ public void detach() { private void displayTooltipStep(Node content, @Nullable Node targetNode, TooltipStep step) { PopOver popover = new PopOver(); - popover.getScene().getStylesheets().setAll(window.getScene().getStylesheets()); + popover.getScene().getStylesheets().setAll(window.getScene().getStylesheets()); // FIXME: walkaround to prevent popover from not properly inheriting styles popover.setContentNode(content); popover.setDetachable(false); popover.setCloseButtonEnabled(false); @@ -201,28 +201,34 @@ private void displayPanelStep(Node content, PanelStep step) { } private void configurePanelLayout(PanelPosition position) { - RowConstraints rowConstraints = new RowConstraints(); - ColumnConstraints columnConstraints = new ColumnConstraints(); - - switch (position) { + overlayPane.getRowConstraints().add(switch (position) { case LEFT, RIGHT -> { + RowConstraints rowConstraints = new RowConstraints(); rowConstraints.setVgrow(Priority.ALWAYS); - columnConstraints.setHgrow(Priority.NEVER); + yield rowConstraints; } case TOP, BOTTOM -> { - columnConstraints.setHgrow(Priority.ALWAYS); + RowConstraints rowConstraints = new RowConstraints(); rowConstraints.setVgrow(Priority.NEVER); + yield rowConstraints; } - default -> { - rowConstraints.setVgrow(Priority.NEVER); + }); + overlayPane.getColumnConstraints().add(switch (position) { + case LEFT, + RIGHT -> { + ColumnConstraints columnConstraints = new ColumnConstraints(); columnConstraints.setHgrow(Priority.NEVER); + yield columnConstraints; } - } - - overlayPane.getRowConstraints().add(rowConstraints); - overlayPane.getColumnConstraints().add(columnConstraints); + case TOP, + BOTTOM -> { + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setHgrow(Priority.ALWAYS); + yield columnConstraints; + } + }); } private Optional mapToArrowLocation(TooltipPosition position) { From 4a1ad10626e27dcf137593c506056840fa96b408 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 27 Jun 2025 15:11:31 -0400 Subject: [PATCH 44/50] Properly refactor the displayStep code --- .../SingleWindowWalkthroughOverlay.java | 28 ++++--------------- .../gui/walkthrough/WalkthroughOverlay.java | 6 +--- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index c88d23e4297..0112435466f 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -63,29 +63,11 @@ public SingleWindowWalkthroughOverlay(Window window) { scene.setRoot(stackPane); } - /** - * Displays a walkthrough step with a target node. - */ - public void displayStepWithTarget(WalkthroughStep step, - Node targetNode, - Runnable beforeNavigate, - Walkthrough walkthrough) { - displayStep(step, targetNode, beforeNavigate, walkthrough); - } - - /** - * Displays a walkthrough step without a target node. - */ - public void displayStepWithoutTarget(WalkthroughStep step, - Runnable beforeNavigate, - Walkthrough walkthrough) { - displayStep(step, null, beforeNavigate, walkthrough); - } - - private void displayStep(WalkthroughStep step, - @Nullable Node targetNode, - Runnable beforeNavigate, - Walkthrough walkthrough) { + /// Displays a walkthrough step, with or without a target node. + public void displayStep(WalkthroughStep step, + Node targetNode, + Runnable beforeNavigate, + Walkthrough walkthrough) { hide(); switch (step) { diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java index 8e0fc28d630..88aa7b6fd09 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughOverlay.java @@ -82,11 +82,7 @@ private void tryRevertToPreviousResolvableStep() { private void displayStep(WalkthroughStep step, Window window, @Nullable Node node) { SingleWindowWalkthroughOverlay overlay = getOrCreateOverlay(window); - if (node != null) { - overlay.displayStepWithTarget(step, node, this::stopNodePolling, walkthrough); - } else { - overlay.displayStepWithoutTarget(step, this::stopNodePolling, walkthrough); - } + overlay.displayStep(step, node, this::stopNodePolling, walkthrough); } private void startNodePolling(WalkthroughStep step, Window window, @Nullable Node node) { From 325f822e36dba7c8346f29946154be3b8e3ba271 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 27 Jun 2025 15:13:25 -0400 Subject: [PATCH 45/50] Provide overload for displayStep --- .../gui/walkthrough/SingleWindowWalkthroughOverlay.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 0112435466f..62ecfcf2888 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -63,9 +63,14 @@ public SingleWindowWalkthroughOverlay(Window window) { scene.setRoot(stackPane); } + /// Display a walkthrough step without a target node. + public void displayStep(WalkthroughStep step, Runnable beforeNavigate, Walkthrough walkthrough) { + displayStep(step, null, beforeNavigate, walkthrough); + } + /// Displays a walkthrough step, with or without a target node. public void displayStep(WalkthroughStep step, - Node targetNode, + @Nullable Node targetNode, Runnable beforeNavigate, Walkthrough walkthrough) { hide(); From 001f1f8fa217337f7c62f5186aca0aa23a8f9681 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 27 Jun 2025 15:17:50 -0400 Subject: [PATCH 46/50] Lazy loading for WalkthroughAction registry --- .../jabref/gui/walkthrough/WalkthroughAction.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 3f1622a5a5c..b163539d653 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -2,6 +2,7 @@ import java.util.Map; import java.util.Objects; +import java.util.function.Supplier; import javafx.scene.control.ContextMenu; @@ -23,14 +24,15 @@ import org.jabref.logic.l10n.Localization; public class WalkthroughAction extends SimpleCommand { - private static final Map WALKTHROUGH_REGISTRY = buildRegistry(); + private static final Map> WALKTHROUGH_REGISTRY = Map.of("mainFileDirectory", WalkthroughAction::createMainFileDirectoryWalkthrough); private final Walkthrough walkthrough; private final JabRefFrame frame; public WalkthroughAction(String name, JabRefFrame frame) { - this.walkthrough = WALKTHROUGH_REGISTRY.get(name); - Objects.requireNonNull(this.walkthrough); + Supplier walkthroughSupplier = WALKTHROUGH_REGISTRY.get(name); + Objects.requireNonNull(walkthroughSupplier, "Walkthrough not found: " + name); + this.walkthrough = walkthroughSupplier.get(); this.frame = frame; } @@ -39,7 +41,7 @@ public void execute() { walkthrough.start(frame.getMainStage()); } - private static Map buildRegistry() { + private static Walkthrough createMainFileDirectoryWalkthrough() { WalkthroughStep step1 = TooltipStep .builder(Localization.lang("Click on \"File\" menu")) .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) @@ -107,7 +109,6 @@ private static Map buildRegistry() { .activeWindow(WindowResolver.title("JabRef preferences")) .build(); - Walkthrough mainFileDirectory = new Walkthrough(step1, step2, step3, step4, step5); - return Map.of("mainFileDirectory", mainFileDirectory); + return new Walkthrough(step1, step2, step3, step4, step5); } } From 39c62598a9576358d9694368b0a31e9bbccb1032 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 27 Jun 2025 15:32:35 -0400 Subject: [PATCH 47/50] Make the walkthrough more robust in the checking the null arguments --- .../java/org/jabref/gui/walkthrough/Walkthrough.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java index 770f4362c68..0334f0f1dc0 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/Walkthrough.java @@ -1,6 +1,7 @@ package org.jabref.gui.walkthrough; import java.util.List; +import java.util.Objects; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; @@ -11,6 +12,7 @@ import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,12 +31,16 @@ public class Walkthrough { private Stage currentStage; public Walkthrough(List steps) { + if (steps.isEmpty() || steps.stream().anyMatch(Objects::isNull)) { + // This throwing is acceptable, since the Walkthrough is often hardcoded and won't make the application crash + throw new IllegalArgumentException("Walkthrough must have at least one step and no null steps allowed."); + } this.currentStep = new SimpleIntegerProperty(0); this.active = new SimpleBooleanProperty(false); this.steps = steps; } - public Walkthrough(WalkthroughStep... steps) { + public Walkthrough(@NonNull WalkthroughStep... steps) { this(List.of(steps)); } @@ -136,7 +142,7 @@ public void goToStep(int stepIndex) { overlay.displayStep(step); } - public WalkthroughStep getStepAtIndex(int index) { + public @NonNull WalkthroughStep getStepAtIndex(int index) { return steps.get(index); } @@ -144,7 +150,7 @@ public void skip() { stop(); } - private WalkthroughStep getCurrentStep() { + private @NonNull WalkthroughStep getCurrentStep() { return steps.get(currentStep.get()); } } From 6dbecfc252c9bfdbef11164b3eb5b49895261be7 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 27 Jun 2025 15:38:28 -0400 Subject: [PATCH 48/50] Slightly reformatted the walkthrough updater. --- .../gui/walkthrough/WalkthroughUpdater.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java index 579ab36635b..e4b17957f0d 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughUpdater.java @@ -83,16 +83,19 @@ public void setupNodeListeners(@NonNull Node node, @NonNull Runnable updateHandl public void setupScrollContainerListeners(@NonNull Node node, @NonNull Runnable updateHandler) { EventHandler scrollHandler = _ -> handleScrollEvent(updateHandler); - Stream.iterate(node, Objects::nonNull, Node::getParent).filter(p -> p instanceof ScrollPane || p instanceof ListView).findFirst().ifPresent(parent -> { - if (parent instanceof ScrollPane scrollPane) { - ChangeListener scrollListener = (_, _, _) -> handleScrollEvent(updateHandler); - listen(scrollPane.vvalueProperty(), scrollListener); - } else if (parent instanceof ListView listView) { - listView.addEventFilter(ScrollEvent.ANY, scrollHandler); - cleanupTasks.add(() -> listView.removeEventFilter(ScrollEvent.ANY, scrollHandler)); - listen(listView.focusModelProperty(), _ -> updateHandler.run()); - } - }); + Stream.iterate(node, Objects::nonNull, Node::getParent) + .filter(p -> p instanceof ScrollPane || p instanceof ListView) + .findFirst() + .ifPresent(parent -> { + if (parent instanceof ScrollPane scrollPane) { + ChangeListener scrollListener = (_, _, _) -> handleScrollEvent(updateHandler); + listen(scrollPane.vvalueProperty(), scrollListener); + } else if (parent instanceof ListView listView) { + listView.addEventFilter(ScrollEvent.ANY, scrollHandler); + cleanupTasks.add(() -> listView.removeEventFilter(ScrollEvent.ANY, scrollHandler)); + listen(listView.focusModelProperty(), _ -> updateHandler.run()); + } + }); node.addEventFilter(ScrollEvent.ANY, scrollHandler); cleanupTasks.add(() -> node.removeEventFilter(ScrollEvent.ANY, scrollHandler)); From b845b3261d13353dd75ef92e0226d645d861a60e Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 27 Jun 2025 16:11:06 -0400 Subject: [PATCH 49/50] Introduce fix to the WindowResolver's hard coding problem and inversion of control to the JabRefFrame --- .../main/java/org/jabref/gui/JabRefGUI.java | 1 + .../org/jabref/gui/frame/JabRefFrame.java | 1 + .../java/org/jabref/gui/frame/MainMenu.java | 7 ++- .../preferences/PreferencesDialogView.java | 3 +- .../gui/walkthrough/WalkthroughAction.java | 62 +++++++++++-------- .../effects/WalkthroughEffect.java | 4 +- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index b1af3c9aa24..761fd2b0473 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -92,6 +92,7 @@ public static void setup(List uiCommands, @Override public void start(Stage stage) { this.mainStage = stage; + Injector.setModelOrService(Stage.class, mainStage); FallbackExceptionHandler.installExceptionHandler((exception, thread) -> UiTaskExecutor.runInJavaFXThread(() -> { DialogService dialogService = Injector.instantiateModelOrService(DialogService.class); diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java index 4280e9b5ff4..3fa13641fb7 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -630,6 +630,7 @@ public void openLastEditedDatabases() { getOpenDatabaseAction().openFiles(lastFiles); } + @Deprecated public Stage getMainStage() { return mainStage; } diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 3f65052271b..60c63d99f04 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -7,6 +7,7 @@ import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; +import javafx.stage.Stage; import org.jabref.gui.ClipBoardManager; import org.jabref.gui.DialogService; @@ -91,6 +92,8 @@ import org.jabref.model.entry.field.SpecialField; import org.jabref.model.util.FileUpdateMonitor; +import com.airhacks.afterburner.injection.Injector; + public class MainMenu extends MenuBar { private final JabRefFrame frame; private final FileHistoryMenu fileHistoryMenu; @@ -236,7 +239,7 @@ private void createMenu() { edit.addEventHandler(ActionEvent.ACTION, event -> { // Work around for mac only issue, where cmd+v on a dialogue triggers the paste action of menu item, resulting in addition of the pasted content in the MainTable. // If the mainscreen is not focused, the actions captured by menu are consumed. - if (OS.OS_X && !frame.getMainStage().focusedProperty().get()) { + if (OS.OS_X && !Injector.instantiateModelOrService(Stage.class).focusedProperty().get()) { event.consume(); } }); @@ -371,7 +374,7 @@ private void createMenu() { new SeparatorMenuItem(), factory.createSubMenu(StandardActions.WALKTHROUGH_MENU, - factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory", frame)) + factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory")) ), new SeparatorMenuItem(), diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java index dfebe248e69..617954e645e 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogView.java @@ -31,6 +31,7 @@ */ public class PreferencesDialogView extends BaseDialog { + public static final String DIALOG_TITLE = Localization.lang("JabRef preferences"); @FXML private CustomTextField searchBox; @FXML private ListView preferenceTabList; @FXML private ScrollPane preferencesContainer; @@ -45,7 +46,7 @@ public class PreferencesDialogView extends BaseDialog preferencesTabToSelectClass; public PreferencesDialogView(Class preferencesTabToSelectClass) { - this.setTitle(Localization.lang("JabRef preferences")); + this.setTitle(DIALOG_TITLE); this.preferencesTabToSelectClass = preferencesTabToSelectClass; ViewLoader.view(this) diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index b163539d653..99646dbf9a5 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -1,13 +1,16 @@ package org.jabref.gui.walkthrough; +import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.function.Supplier; +import java.util.Optional; +import java.util.function.Function; import javafx.scene.control.ContextMenu; +import javafx.stage.Stage; import org.jabref.gui.actions.SimpleCommand; -import org.jabref.gui.frame.JabRefFrame; +import org.jabref.gui.preferences.PreferencesDialogView; import org.jabref.gui.walkthrough.declarative.NavigationPredicate; import org.jabref.gui.walkthrough.declarative.NodeResolver; import org.jabref.gui.walkthrough.declarative.WindowResolver; @@ -23,25 +26,35 @@ import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.jabref.logic.l10n.Localization; +import com.airhacks.afterburner.injection.Injector; + public class WalkthroughAction extends SimpleCommand { - private static final Map> WALKTHROUGH_REGISTRY = Map.of("mainFileDirectory", WalkthroughAction::createMainFileDirectoryWalkthrough); + private static final Map> WALKTHROUGH_REGISTRY = Map.of("mainFileDirectory", WalkthroughAction::createMainFileDirectoryWalkthrough); + private static final Map WALKTHROUGH_CACHE = new HashMap<>(); // must be mutable to allow caching of created walkthroughs private final Walkthrough walkthrough; - private final JabRefFrame frame; + private final Stage mainStage; - public WalkthroughAction(String name, JabRefFrame frame) { - Supplier walkthroughSupplier = WALKTHROUGH_REGISTRY.get(name); - Objects.requireNonNull(walkthroughSupplier, "Walkthrough not found: " + name); - this.walkthrough = walkthroughSupplier.get(); - this.frame = frame; + public WalkthroughAction(String name) { + this.mainStage = Injector.instantiateModelOrService(Stage.class); + if (WALKTHROUGH_CACHE.containsKey(name)) { + this.walkthrough = WALKTHROUGH_CACHE.get(name); + } else { + Function walkthroughProvider = WALKTHROUGH_REGISTRY.get(name); + Objects.requireNonNull(walkthroughProvider, "Walkthrough not found: " + name); + this.walkthrough = walkthroughProvider.apply(mainStage); + WALKTHROUGH_CACHE.put(name, this.walkthrough); + } } @Override public void execute() { - walkthrough.start(frame.getMainStage()); + walkthrough.start(this.mainStage); } - private static Walkthrough createMainFileDirectoryWalkthrough() { + private static Walkthrough createMainFileDirectoryWalkthrough(Stage mainStage) { + WindowResolver mainResolver = () -> Optional.of(mainStage); + WalkthroughStep step1 = TooltipStep .builder(Localization.lang("Click on \"File\" menu")) .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) @@ -58,10 +71,14 @@ private static Walkthrough createMainFileDirectoryWalkthrough() { .activeWindow(WindowResolver.clazz(ContextMenu.class)) .highlight(new MultiWindowHighlight( new WindowEffect(HighlightEffect.ANIMATED_PULSE), - new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) + new WindowEffect(mainResolver, HighlightEffect.FULL_SCREEN_DARKEN) )) .build(); + MultiWindowHighlight preferenceHighlight = new MultiWindowHighlight( + new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(mainResolver, HighlightEffect.FULL_SCREEN_DARKEN) + ); WalkthroughStep step3 = TooltipStep .builder(Localization.lang("Select the \"Linked files\" tab")) .content(new TextBlock(Localization.lang("This section manages how JabRef handles your PDF files and other documents."))) @@ -71,11 +88,8 @@ private static Walkthrough createMainFileDirectoryWalkthrough() { node.toString().contains(Localization.lang("Linked files")))) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) - .activeWindow(WindowResolver.title("JabRef preferences")) - .highlight(new MultiWindowHighlight( - new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), - new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) - )) + .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) + .highlight(preferenceHighlight) .build(); WalkthroughStep step4 = TooltipStep @@ -85,11 +99,8 @@ private static Walkthrough createMainFileDirectoryWalkthrough() { .resolver(NodeResolver.fxId("useMainFileDirectory")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) - .highlight(new MultiWindowHighlight( - new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), - new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) - )) - .activeWindow(WindowResolver.title("JabRef preferences")) + .highlight(preferenceHighlight) + .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) .build(); WalkthroughStep step5 = PanelStep @@ -102,11 +113,8 @@ private static Walkthrough createMainFileDirectoryWalkthrough() { .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("button") && node.toString().contains(Localization.lang("Save")))) .navigation(NavigationPredicate.onClick()) .position(PanelPosition.TOP) - .highlight(new MultiWindowHighlight( - new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), - new WindowEffect(WindowResolver.title("JabRef"), HighlightEffect.FULL_SCREEN_DARKEN) - )) - .activeWindow(WindowResolver.title("JabRef preferences")) + .highlight(preferenceHighlight) + .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) .build(); return new Walkthrough(step1, step2, step3, step4, step5); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/WalkthroughEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/WalkthroughEffect.java index ed894f8f9b1..7bccddf93d1 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/WalkthroughEffect.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/effects/WalkthroughEffect.java @@ -7,9 +7,7 @@ import org.jspecify.annotations.NonNull; -/** - * Base class for walkthrough effects BackdropHighlight, TooltipHighlight, FullScreenDarken, etc. - */ +/// Base class for walkthrough effects BackdropHighlight, TooltipHighlight, FullScreenDarken, etc. public abstract class WalkthroughEffect { protected final Pane pane; protected final WalkthroughUpdater updater = new WalkthroughUpdater(); From a7e40c757b242c7962da9d4ffef93c2e74fe91b0 Mon Sep 17 00:00:00 2001 From: Yubo Cao Date: Sat, 28 Jun 2025 08:24:31 -0400 Subject: [PATCH 50/50] Update jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java Co-authored-by: Subhramit Basu --- jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 60c63d99f04..78654f6939d 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -239,7 +239,9 @@ private void createMenu() { edit.addEventHandler(ActionEvent.ACTION, event -> { // Work around for mac only issue, where cmd+v on a dialogue triggers the paste action of menu item, resulting in addition of the pasted content in the MainTable. // If the mainscreen is not focused, the actions captured by menu are consumed. - if (OS.OS_X && !Injector.instantiateModelOrService(Stage.class).focusedProperty().get()) { + boolean isStageUnfocused = !Injector.instantiateModelOrService(Stage.class).focusedProperty().get(); + + if (OS.OS_X && isStageUnfocused) { event.consume(); } });