Skip to content

Implementation of walkthrough infrastructure #13182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 57 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
5babec2
feat: add initial implementation of walkthrough
Yubo-Cao May 29, 2025
fdd6f13
Merge branch 'JabRef:main' into walkthrough
Yubo-Cao May 30, 2025
1230126
Fix issues mentioned in the PR
Jun 2, 2025
f30c5be
Merge branch 'main' into walkthrough
koppor Jun 3, 2025
7dec5e5
Merge remote-tracking branch 'upstream/main' into walkthrough
Jun 4, 2025
dafac44
Merge branch 'walkthrough' of https://github.com/Yubo-Cao/jabref into…
Jun 4, 2025
1b2af57
Tweak Base.css
Jun 6, 2025
6e21170
Implement backdrop highlight effect.
Jun 6, 2025
bf87a52
Use record classes
Jun 6, 2025
2039dfc
Fix backdrop highlight showing on invisible node
Jun 6, 2025
264c3c4
Builder refactor and adding paper directory setup
Jun 6, 2025
6f80afe
Resolve PR comments
Jun 6, 2025
f2c4add
Update JabRef_en keys
Jun 6, 2025
da595c4
Update JabRef_en keys and remove title case
Jun 6, 2025
b43936f
Fix paper directory picker
Jun 6, 2025
cc8a553
Fix modernizer
Jun 6, 2025
197f6d8
Add edit entry step (and bugs on IndexOutOfBound
Jun 6, 2025
d944011
Add edit entry step
Jun 6, 2025
50dedc2
Merge branch 'walkthrough' of https://github.com/Yubo-Cao/jabref into…
Yubo-Cao Jun 6, 2025
61b5963
Remove change to localization
Yubo-Cao Jun 6, 2025
878d22b
Preliminary implementation of main file directory walkthrough
Jun 7, 2025
9aab167
Implement fix according to subhramit
Yubo-Cao Jun 13, 2025
405cf0e
Resolve warnings from Intellij
Yubo-Cao Jun 13, 2025
ec2a036
Remove whitespace change to JabRefGUI
Yubo-Cao Jun 16, 2025
eb8d4fb
Remove whitespace changes to JabRefFrame
Yubo-Cao Jun 16, 2025
547c819
Remove whitespace change to PreferencesDialogView
Yubo-Cao Jun 16, 2025
770258b
Rename the Manager to WalkthroughHighlighter
Yubo-Cao Jun 16, 2025
d32c14b
Use PopOver class, rather than custom arrow position algorithm
Yubo-Cao Jun 16, 2025
ca33ea6
Change excess usage of Optional to Nullable
Yubo-Cao Jun 18, 2025
570bfab
Refactor highlight effect
Yubo-Cao Jun 19, 2025
d5eb6fb
Add before navigate hook to prevent unwanted revert
Yubo-Cao Jun 20, 2025
b5a10c1
Fix Checkstyle and Intellij
Yubo-Cao Jun 20, 2025
d589f9d
Update the width and height
Yubo-Cao Jun 20, 2025
487cc60
Internationalize WalkthroughAction
Yubo-Cao Jun 20, 2025
323b353
Fix according to Trag bot.
Yubo-Cao Jun 20, 2025
40a22ff
Fix according to Trag bot.
Yubo-Cao Jun 20, 2025
da7e8a5
Fix according to Trag bot.
Yubo-Cao Jun 20, 2025
a184af6
Fix improper PopOver display
Yubo-Cao Jun 20, 2025
cb1d9a1
Fix bug for scrolling and window effect
Yubo-Cao Jun 20, 2025
bfe9be7
Fix according to Trag
Yubo-Cao Jun 20, 2025
4979337
Additional fix per Trag
Yubo-Cao Jun 20, 2025
57353e4
Fix backdrop highlight NPE
Yubo-Cao Jun 20, 2025
dd41500
Merge remote-tracking branch 'upstream/main' into walkthrough
Yubo-Cao Jun 20, 2025
da50fcd
Fix localization issue
Yubo-Cao Jun 20, 2025
7b284be
Fix according to Trag bot
Yubo-Cao Jun 21, 2025
66e85af
Fix WalkthroughStep3's LinkedFiles issue
Yubo-Cao Jun 21, 2025
b02ef51
Merge branch 'main' into walkthrough
Yubo-Cao Jun 21, 2025
c635e33
Fix the issue where PopOver rendered unstyled
Yubo-Cao Jun 22, 2025
62147cf
Update Walkthrough comments
Yubo-Cao Jun 26, 2025
3f77583
Comment on the walkthrough and fix the switch expression
Yubo-Cao Jun 27, 2025
4a1ad10
Properly refactor the displayStep code
Yubo-Cao Jun 27, 2025
325f822
Provide overload for displayStep
Yubo-Cao Jun 27, 2025
001f1f8
Lazy loading for WalkthroughAction registry
Yubo-Cao Jun 27, 2025
39c6259
Make the walkthrough more robust in the checking the null arguments
Yubo-Cao Jun 27, 2025
6dbecfc
Slightly reformatted the walkthrough updater.
Yubo-Cao Jun 27, 2025
b845b32
Introduce fix to the WindowResolver's hard coding problem and inversi…
Yubo-Cao Jun 27, 2025
a7e40c7
Update jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java
Yubo-Cao Jun 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public static void setup(List<UiCommand> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,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"), IconTheme.JabRefIcons.BOOK),
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")),
Expand Down
1 change: 1 addition & 0 deletions jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ public void openLastEditedDatabases() {
getOpenDatabaseAction().openFiles(lastFiles);
}

@Deprecated
public Stage getMainStage() {
return mainStage;
}
Expand Down
14 changes: 13 additions & 1 deletion jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -77,6 +78,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;
Expand All @@ -90,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;
Expand Down Expand Up @@ -235,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 && !frame.getMainStage().focusedProperty().get()) {
boolean isStageUnfocused = !Injector.instantiateModelOrService(Stage.class).focusedProperty().get();

if (OS.OS_X && isStageUnfocused) {
event.consume();
}
});
Expand Down Expand Up @@ -369,6 +375,12 @@ private void createMenu() {

new SeparatorMenuItem(),

factory.createSubMenu(StandardActions.WALKTHROUGH_MENU,
factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory"))
),

new SeparatorMenuItem(),

factory.createMenuItem(StandardActions.ERROR_CONSOLE, new ErrorConsoleAction()),

new SeparatorMenuItem(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
*/
public class PreferencesDialogView extends BaseDialog<PreferencesDialogViewModel> {

public static final String DIALOG_TITLE = Localization.lang("JabRef preferences");
@FXML private CustomTextField searchBox;
@FXML private ListView<PreferencesTab> preferenceTabList;
@FXML private ScrollPane preferencesContainer;
Expand All @@ -45,7 +46,7 @@ public class PreferencesDialogView extends BaseDialog<PreferencesDialogViewModel
private final Class<? extends PreferencesTab> preferencesTabToSelectClass;

public PreferencesDialogView(Class<? extends PreferencesTab> preferencesTabToSelectClass) {
this.setTitle(Localization.lang("JabRef preferences"));
this.setTitle(DIALOG_TITLE);
this.preferencesTabToSelectClass = preferencesTabToSelectClass;

ViewLoader.view(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package org.jabref.gui.walkthrough;

import java.util.Optional;

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.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.WalkthroughStep;

import org.controlsfx.control.PopOver;
import org.jspecify.annotations.Nullable;
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 WalkthroughUpdater updater = new WalkthroughUpdater();

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();
// This basically never happens, so only a development time check is needed
assert scene != null;

originalRoot = (Pane) scene.getRoot();
stackPane = new StackPane();

stackPane.getChildren().add(originalRoot);
stackPane.getChildren().add(overlayPane);

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,
@Nullable Node targetNode,
Runnable beforeNavigate,
Walkthrough walkthrough) {
hide();

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)));
}

/**
* Hide the overlay and clean up any resources.
*/
public void hide() {
overlayPane.getChildren().clear();
overlayPane.setClip(null);
overlayPane.setVisible(true);
updater.cleanup();
}

/**
* 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 displayTooltipStep(Node content, @Nullable Node targetNode, TooltipStep step) {
PopOver popover = new PopOver();
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);
popover.setHeaderAlwaysVisible(false);
mapToArrowLocation(step.position()).ifPresent(popover::setArrowLocation);
popover.setAutoHide(false);
popover.setAutoFix(true);

if (targetNode == null) {
popover.show(window);
return;
}

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) {
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 LEFT -> {
overlayPane.setAlignment(Pos.CENTER_LEFT);
GridPane.setVgrow(content, Priority.ALWAYS);
GridPane.setFillHeight(content, true);
}
case RIGHT -> {
overlayPane.setAlignment(Pos.CENTER_RIGHT);
GridPane.setVgrow(content, Priority.ALWAYS);
GridPane.setFillHeight(content, true);
}
case TOP -> {
overlayPane.setAlignment(Pos.TOP_CENTER);
GridPane.setHgrow(content, Priority.ALWAYS);
GridPane.setFillWidth(content, true);
}
case BOTTOM -> {
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(PanelPosition position) {
overlayPane.getRowConstraints().add(switch (position) {
case LEFT,
RIGHT -> {
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setVgrow(Priority.ALWAYS);
yield rowConstraints;
}
case TOP,
BOTTOM -> {
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setVgrow(Priority.NEVER);
yield rowConstraints;
}
});
overlayPane.getColumnConstraints().add(switch (position) {
case LEFT,
RIGHT -> {
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setHgrow(Priority.NEVER);
yield columnConstraints;
}
case TOP,
BOTTOM -> {
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setHgrow(Priority.ALWAYS);
yield columnConstraints;
}
});
}

private Optional<PopOver.ArrowLocation> mapToArrowLocation(TooltipPosition position) {
return Optional.ofNullable(switch (position) {
case TOP ->
PopOver.ArrowLocation.BOTTOM_CENTER;
case BOTTOM ->
PopOver.ArrowLocation.TOP_CENTER;
case LEFT ->
PopOver.ArrowLocation.RIGHT_CENTER;
case RIGHT ->
PopOver.ArrowLocation.LEFT_CENTER;
case AUTO ->
null;
});
}

private void hideOverlayPane() {
overlayPane.setVisible(false);
updater.addCleanupTask(() -> overlayPane.setVisible(true));
}

private void setupClipping(Node node) {
ChangeListener<Bounds> 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);
}
};
updater.listen(node.boundsInLocalProperty(), listener);
listener.changed(null, null, node.getBoundsInParent());
}
}
Loading