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 23 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
3 changes: 1 addition & 2 deletions jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ public class JabRefGUI extends Application {
private static ClipBoardManager clipBoardManager;
private static DialogService dialogService;
private static JabRefFrame mainFrame;

private static RemoteListenerServerManager remoteListenerServerManager;

private Stage mainStage;
Expand Down Expand Up @@ -191,7 +190,7 @@ public void initialize() {

private void setupProxy() {
if (!preferences.getProxyPreferences().shouldUseProxy()
|| !preferences.getProxyPreferences().shouldUseAuthentication()) {
|| !preferences.getProxyPreferences().shouldUseAuthentication()) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
9 changes: 5 additions & 4 deletions jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -681,7 +681,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);
Expand Down
5 changes: 5 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.jabref.gui.preferences;
package org.jabref.gui.preferences;

import java.util.Locale;
import java.util.Optional;
Expand Down
131 changes: 131 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/walkthrough/HighlightManager.java
Original file line number Diff line number Diff line change
@@ -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<Window, BackdropHighlight> backdropHighlights = new HashMap<>();
private final Map<Window, PulseAnimateIndicator> pulseIndicators = new HashMap<>();
private final Map<Window, FullScreenDarken> 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<MultiWindowHighlight> highlightConfig,
Optional<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<Node> 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<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 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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<Window, SingleWindowOverlay> 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<Node> 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);
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<Node> 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 @NonNull WalkthroughNode getStepAtIndex(int index) {
return walkthrough.getSteps().get(index);
}

private SingleWindowOverlay getOrCreateOverlay(Window window) {
return overlays.computeIfAbsent(window, SingleWindowOverlay::new);
}
}
Loading
Loading