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 1 commit
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
14 changes: 13 additions & 1 deletion jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
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;
import org.jabref.logic.journals.JournalAbbreviationRepository;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Runnable> cleanupTasks = new ArrayList<>();
private final ChangeListener<Object> 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<Scene> 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;
}
}
Loading