From 06ec2cbc9cf677561c858bfcd09b52e5fac440d3 Mon Sep 17 00:00:00 2001 From: Yubo-Cao Date: Fri, 8 Aug 2025 15:12:03 -0400 Subject: [PATCH 01/22] Add "Add Group" walkthrough, tweak quick settings, and tweak welcome tab --- .../gui/walkthrough/WalkthroughAction.java | 151 +++++++++++++++++- .../walkthrough/declarative/NodeResolver.java | 22 ++- .../gui/walkthrough/declarative/Trigger.java | 61 +++++++ .../org/jabref/gui/welcome/WelcomeTab.java | 24 ++- .../main/resources/org/jabref/gui/Base.css | 2 +- .../gui/welcome/components/QuickSettings.fxml | 2 +- .../main/resources/l10n/JabRef_en.properties | 44 +++++ 7 files changed, 287 insertions(+), 19 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 5af61d663c6..beaeb480972 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -6,7 +6,6 @@ import javafx.scene.control.ButtonType; import javafx.scene.control.ContextMenu; -import javafx.scene.control.DialogPane; import javafx.stage.Stage; import javafx.util.Duration; @@ -36,6 +35,7 @@ public class WalkthroughAction extends SimpleCommand { public static final String PDF_LINK_WALKTHROUGH_NAME = "pdfLink"; public static final String MAIN_FILE_DIRECTORY_WALKTHROUGH_NAME = "mainFileDirectory"; public static final String CUSTOMIZE_ENTRY_TABLE_WALKTHROUGH_NAME = "customizeEntryTable"; + public static final String GROUP_WALKTHROUGH_NAME = "group"; private static final Map WALKTHROUGH_CACHE = new ConcurrentHashMap<>(); @@ -59,10 +59,14 @@ public void execute() { private Walkthrough getWalkthrough(String name) { return WALKTHROUGH_CACHE.computeIfAbsent(name, _ -> switch (name) { - case MAIN_FILE_DIRECTORY_WALKTHROUGH_NAME -> createMainFileDirectoryWalkthrough(); + case MAIN_FILE_DIRECTORY_WALKTHROUGH_NAME -> + createMainFileDirectoryWalkthrough(); case PDF_LINK_WALKTHROUGH_NAME -> createPdfLinkWalkthrough(); - case CUSTOMIZE_ENTRY_TABLE_WALKTHROUGH_NAME -> createCustomizeEntryTableWalkthrough(); - default -> throw new IllegalArgumentException("Unknown walkthrough: " + name); + case CUSTOMIZE_ENTRY_TABLE_WALKTHROUGH_NAME -> + createCustomizeEntryTableWalkthrough(); + case GROUP_WALKTHROUGH_NAME -> createGroupWalkthrough(); + default -> + throw new IllegalArgumentException("Unknown walkthrough: " + name); } ); } @@ -274,9 +278,7 @@ private Walkthrough createPdfLinkWalkthrough() { .addStep(WalkthroughStep .tooltip(Localization.lang("Confirm URL download")) .content(new TextBlock(Localization.lang("Click \"OK\" to start downloading the PDF from the entered URL."))) - .resolver(scene -> NodeResolver.predicate(DialogPane.class::isInstance) - .resolve(scene) - .map(node -> node instanceof DialogPane pane ? pane.lookupButton(ButtonType.OK) : null)) + .resolver(NodeResolver.buttonType(ButtonType.OK)) .trigger(Trigger.onClick()) .activeWindow(WindowResolver.not(stage)) .highlight(pdfDialogEffect) @@ -296,6 +298,141 @@ private Walkthrough createPdfLinkWalkthrough() { .build(); } + private Walkthrough createGroupWalkthrough() { + WindowResolver mainResolver = () -> Optional.of(stage); + WalkthroughEffect groupHighlight = new WalkthroughEffect( + new WindowEffect(HighlightEffect.PING), + new WindowEffect(mainResolver, HighlightEffect.FULL_SCREEN_DARKEN) + ); + String groupName = Localization.lang("Research Papers"); + String addGroup = Localization.lang("Add group"); + String addSelectedEntries = Localization.lang("Add selected entries to this group"); + + return Walkthrough + .create(stateManager) + // Step 1: Open example library + .addStep(WalkthroughStep.sideEffect(Localization.lang("Open Example Library")) + .sideEffect(new OpenLibrarySideEffect(frame))) + // Step 2: Highlight groups sidepane + .addStep(WalkthroughStep + .panel(Localization.lang("Welcome to groups walkthrough")) + .content( + new TextBlock(Localization.lang("This walkthrough will guide you through creating and managing groups in JabRef. Groups help you organize your bibliography entries into collections. We've opened an example library so you can practice with real entries.")), + new InfoBlock(Localization.lang("The groups panel on the left side shows all your groups in a tree structure. You can create groups, add entries to them, and organize them hierarchically.")) + ) + .resolver(NodeResolver.predicate(node -> node.getClass().getName().contains("GroupsSidePaneComponent"))) + .continueButton(Localization.lang("Continue")) + .position(PanelPosition.RIGHT) + .highlight(HighlightEffect.SPOT_LIGHT)) + // Step 3: Click "Add group" button + .addStep(WalkthroughStep + .tooltip(Localization.lang("Click on \"%0\" button", addGroup)) + .content(new TextBlock(Localization.lang("Let's create your first group. Click the \"%0\" button at the bottom of the groups panel to open the group creation dialog.", addGroup))) + .resolver(NodeResolver.selectorWithText(".button", addGroup::equals)) + .trigger(Trigger.create().withWindowChangeListener().onClick()) + .position(TooltipPosition.TOP) + .highlight(HighlightEffect.SPOT_LIGHT)) + // Step 4: Fill group creation dialog + .addStep(WalkthroughStep + .tooltip(Localization.lang("Enter group name")) + .content(new TextBlock(Localization.lang("Type \"%0\" as the name for your group.", groupName))) + .resolver(NodeResolver.fxId("nameField")) + .trigger(Trigger.onTextEquals(groupName)) + .position(TooltipPosition.RIGHT) + .activeWindow(WindowResolver.title(Localization.lang("Add group"))) + .showQuitButton(false) + .highlight(groupHighlight)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Add a description (optional)")) + .content(new TextBlock(Localization.lang("You can add a description to help remember what this group is for. For example, type \"Important research papers for my project\"."))) + .resolver(NodeResolver.fxId("descriptionField")) + .trigger(Trigger.onTextInput()) + .position(TooltipPosition.RIGHT) + .activeWindow(WindowResolver.title(Localization.lang("Add group"))) + .showQuitButton(false) + .highlight(groupHighlight)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Select \"Explicit selection\"")) + .content(new TextBlock(Localization.lang("This allows you to manually choose which entries belong to this group."))) + .resolver(NodeResolver.fxId("explicitRadioButton")) + .trigger(Trigger.onClick()) + .position(TooltipPosition.RIGHT) + .activeWindow(WindowResolver.title(Localization.lang("Add group"))) + .showQuitButton(false) + .highlight(groupHighlight)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Click \"OK\" to create the group")) + .content(new TextBlock(Localization.lang("Now click \"OK\" to create your new group."))) + .resolver(NodeResolver.buttonType(ButtonType.OK)) + .trigger(Trigger.onClick()) + .activeWindow(WindowResolver.title(Localization.lang("Add group"))) + .showQuitButton(false) + .highlight(groupHighlight)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Click on \"All entries\" group")) + .content(new TextBlock(Localization.lang("Select \"All entries\" to show all entries in the main table before dragging items to your new group."))) + .resolver(NodeResolver.selectorWithText( + ".tree-table-row-cell", + text -> Localization.lang("All entries").equals(text) + || (text != null && text.contains(Localization.lang("All entries"))))) + .trigger(Trigger.onClick()) + .position(TooltipPosition.RIGHT) + .highlight(HighlightEffect.SPOT_LIGHT)) + // Step 5: Drag and drop entry to group + .addStep(WalkthroughStep + .panel(Localization.lang("Add entries to your group")) + .content( + new TextBlock(Localization.lang("Your \"%0\" group has been created. Now let's add some entries to it.", groupName)), + new InfoBlock(Localization.lang("You can add entries to a group by dragging and dropping them. Try selecting the \"Ding_2006\" entry from the main table and drag it to your new group.")) + ) + .resolver(NodeResolver.selectorWithText(".table-row-cell", + text -> "Ding_2006".equals(text) + || "Ding et al.".equals(text) + || "Chocolate and Prevention of Cardiovascular Disease: A Systematic Review".equals(text))) + .continueButton(Localization.lang("Continue")) + .position(PanelPosition.RIGHT) + .highlight(HighlightEffect.PING)) + // Step 6: Right-click to add entry + .addStep(WalkthroughStep + .tooltip(Localization.lang("Select the Corti_2009 entry")) + .content(new TextBlock(Localization.lang("You can also add entries using the context menu. First, select the \"Corti_2009\" entry (Cocoa and Cardiovascular Health by Corti et al.) from the main table."))) + .resolver(NodeResolver.selectorWithText(".table-row-cell", + text -> "Corti_2009".equals(text) + || "Corti et al.".equals(text) + || "Cocoa and Cardiovascular Health".equals(text))) + .trigger(Trigger.onClick()) + .position(TooltipPosition.RIGHT) + .highlight(HighlightEffect.SPOT_LIGHT)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Right-click on your group")) + .content(new TextBlock(Localization.lang("Now right-click on your \"%0\" group to open the context menu.", groupName))) + .resolver(NodeResolver.selectorWithText(".tree-table-row-cell", + text -> text != null && text.contains(groupName))) + .trigger(Trigger.create().withWindowChangeListener().onRightClick()) + .position(TooltipPosition.RIGHT) + .highlight(HighlightEffect.SPOT_LIGHT)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Click \"%0\"", addSelectedEntries)) + .content(new TextBlock(Localization.lang("Click on \"%0\" to add the Corti_2009 entry to your group.", addSelectedEntries))) + .resolver(NodeResolver.menuItem(addSelectedEntries)) + .trigger(Trigger.onClick()) + .activeWindow(WindowResolver.clazz(ContextMenu.class)) + .showQuitButton(false) + .highlight(groupHighlight)) + // Completion + .addStep(WalkthroughStep + .panel(Localization.lang("Groups walkthrough completed")) + .content( + new TextBlock(Localization.lang("You've learned how to create groups and add entries to them. Groups are a powerful way to organize your bibliography and can be nested to create hierarchical structures.")), + new InfoBlock(Localization.lang("For more information about groups: [Groups documentation](https://docs.jabref.org/finding-sorting-and-cleaning-entries/groups)")) + ) + .resolver(NodeResolver.predicate(node -> node.getClass().getName().contains("GroupsSidePaneComponent"))) + .continueButton(Localization.lang("Finish")) + .position(PanelPosition.RIGHT) + .highlight(HighlightEffect.SPOT_LIGHT)) + .build(); + } + private Walkthrough createMainFileDirectoryWalkthrough() { WindowResolver mainResolver = () -> Optional.of(stage); WalkthroughEffect preferenceHighlight = new WalkthroughEffect( 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 60be51a7933..1d82ce74584 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 @@ -1,15 +1,16 @@ 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.ButtonType; import javafx.scene.control.ContextMenu; +import javafx.scene.control.DialogPane; +import javafx.scene.control.MenuItem; import org.jabref.gui.actions.StandardActions; import org.jabref.gui.icon.IconTheme; @@ -74,6 +75,17 @@ static NodeResolver action(@NonNull StandardActions action) { return scene -> Optional.ofNullable(findNodeByAction(scene, action)); } + /// Creates a resolver that finds a button by its button type, assuming the node + /// resolved is a [javafx.scene.control.DialogPane]. + /// + /// @param buttonType the button type to find + /// @return a resolver that finds the button by button type + static NodeResolver buttonType(@NonNull ButtonType buttonType) { + return scene -> predicate(DialogPane.class::isInstance) + .resolve(scene) + .map(node -> node instanceof DialogPane pane ? pane.lookupButton(buttonType) : null); + } + /// Creates a resolver that finds a node by selector first, then predicate. /// /// @param selector the style class to match @@ -110,11 +122,7 @@ static NodeResolver menuItem(@NonNull String key) { .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(); + .map(MenuItem::getStyleableNode).findFirst(); }; } diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/Trigger.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/Trigger.java index bd9a36a06d1..2bddc55fde8 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/Trigger.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/Trigger.java @@ -5,6 +5,7 @@ import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; +import java.util.regex.Pattern; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; @@ -67,6 +68,14 @@ static Trigger onFetchFulltextCompleted() { return create().onFetchFulltextCompleted().build(); } + static Trigger onTextEquals(String expected) { + return create().onTextEquals(expected).build(); + } + + static Trigger onTextMatchesRegex(String regex) { + return create().onTextMatchesRegex(regex).build(); + } + static Builder create() { return new Builder(); } @@ -177,6 +186,41 @@ public Builder onTextInput() { return this; } + public Builder onTextEquals(String expected) { + Objects.requireNonNull(expected, "expected must not be null"); + setGenerator((node, onNavigate) -> { + if (!(node instanceof TextInputControl textInput)) { + throw new IllegalArgumentException("onTextEquals can only be used with TextInputControl"); + } + ChangeListener listener = (_, _, newText) -> { + if (expected.equals(newText)) { + onNavigate.apply(NOTHING); + } + }; + textInput.textProperty().addListener(listener); + return () -> textInput.textProperty().removeListener(listener); + }); + return this; + } + + public Builder onTextMatchesRegex(String regex) { + Objects.requireNonNull(regex, "regex must not be null"); + final Pattern compiled = Pattern.compile(regex); + setGenerator((node, onNavigate) -> { + if (!(node instanceof TextInputControl textInput)) { + throw new IllegalArgumentException("onTextMatchesRegex can only be used with TextInputControl"); + } + ChangeListener listener = (_, _, newText) -> { + if (newText != null && compiled.matcher(newText).matches()) { + onNavigate.apply(NOTHING); + } + }; + textInput.textProperty().addListener(listener); + return () -> textInput.textProperty().removeListener(listener); + }); + return this; + } + public Builder onDoubleClick() { setGenerator((node, onNavigate) -> { EventHandler handler = event -> { @@ -190,6 +234,23 @@ public Builder onDoubleClick() { return this; } + public Builder onRightClick() { + setGenerator((node, onNavigate) -> { + final EventDispatcher originalDispatcher = node.getEventDispatcher(); + final EventDispatcher newDispatcher = (event, tail) -> { + if (event.getEventType() == MouseEvent.MOUSE_PRESSED && ((MouseEvent) event).getButton() == javafx.scene.input.MouseButton.SECONDARY) { + node.setEventDispatcher(originalDispatcher); + Supplier originalAction = () -> originalDispatcher.dispatchEvent(event, tail); + return (Event) onNavigate.apply(originalAction); + } + return originalDispatcher.dispatchEvent(event, tail); + }; + node.setEventDispatcher(newDispatcher); + return () -> node.setEventDispatcher(originalDispatcher); + }); + return this; + } + public Builder onFileAddedToListView() { setGenerator((node, onNavigate) -> { ListView listView = (ListView) node.lookup("#listView"); diff --git a/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java b/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java index c58443010ba..fe509968d7d 100644 --- a/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java +++ b/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java @@ -11,6 +11,7 @@ import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; +import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; @@ -177,13 +178,30 @@ private VBox createWalkthroughBox() { Button linkPdfWalkthroughButton = createWalkthroughButton( Localization.lang("Link PDF to entries"), - IconTheme.JabRefIcons.TOGGLE_GROUPS, + IconTheme.JabRefIcons.PDF_FILE, WalkthroughAction.PDF_LINK_WALKTHROUGH_NAME ); - walkthroughsContainer.getChildren().addAll(mainFileDirWalkthroughButton, entryTableWalkthroughButton, linkPdfWalkthroughButton); + Button groupButton = createWalkthroughButton( + Localization.lang("Add group"), + IconTheme.JabRefIcons.NEW_GROUP, + WalkthroughAction.GROUP_WALKTHROUGH_NAME + ); + + walkthroughsContainer.getChildren().addAll( + mainFileDirWalkthroughButton, + entryTableWalkthroughButton, + linkPdfWalkthroughButton, + groupButton + ); + + ScrollPane scrollPane = new ScrollPane(walkthroughsContainer); + scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.getStyleClass().add("walkthroughs-scroll-pane"); - return createVBoxContainer(header, walkthroughsContainer); + return createVBoxContainer(header, scrollPane); } private VBox createCommunityBox() { diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index 1774fff976c..4c0d017610a 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -2584,13 +2584,13 @@ journalInfo .grid-cell-b { /* Quick Settings */ - .quick-settings-container, .walkthroughs-container { -fx-spacing: 0.6667em; -fx-alignment: top-left; } +.walkthroughs-scroll-pane, .quick-settings-scroll-pane { -fx-max-height: 13.333em; /* item-size * 4 + gap * 3 = (0.0833 * 2 + 0.833 * 2 + 1) * 4 + 0.667 * 3 */ -fx-min-height: 13.333em; diff --git a/jabgui/src/main/resources/org/jabref/gui/welcome/components/QuickSettings.fxml b/jabgui/src/main/resources/org/jabref/gui/welcome/components/QuickSettings.fxml index 380853c3451..b40eca9007e 100644 --- a/jabgui/src/main/resources/org/jabref/gui/welcome/components/QuickSettings.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/welcome/components/QuickSettings.fxml @@ -8,7 +8,7 @@ + styleClass="welcome-section">