diff --git a/CHANGELOG.md b/CHANGELOG.md index ee187579b5d..576b6b29c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We changed the validation error dialog for overriding the default file directories to a confirmation dialog for saving other preferences under the library properties. [#13488](https://github.com/JabRef/jabref/pull/13488) - We improved file exists warning dialog with clearer options and tooltips [#12565](https://github.com/JabRef/jabref/issues/12565) - We introduced walkthrough functionality [#12664](https://github.com/JabRef/jabref/issues/12664) +- The Welcome tab now has a responsive layout. [#12664](https://github.com/JabRef/jabref/issues/12664) +- We introduced a donation prompt in the Welcome tab. [#12664](https://github.com/JabRef/jabref/issues/12664) ### Fixed 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 815b92e8268..dfae60afdaf 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -48,6 +48,7 @@ import org.jabref.gui.search.SearchType; import org.jabref.gui.sidepane.SidePane; import org.jabref.gui.sidepane.SidePaneType; +import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.undo.RedoAction; import org.jabref.gui.undo.UndoAction; @@ -447,7 +448,8 @@ private void initBindings() { } private void updateTabBarVisible() { - if (preferences.getWorkspacePreferences().shouldHideTabBar() && stateManager.getOpenDatabases().size() <= 1) { + // When WelcomeTab is open, the tabbar should be visible + if (preferences.getWorkspacePreferences().shouldHideTabBar() && tabbedPane.getTabs().size() <= 1) { if (!tabbedPane.getStyleClass().contains("hide-tab-bar")) { tabbedPane.getStyleClass().add("hide-tab-bar"); } @@ -507,7 +509,9 @@ public void showWelcomeTab() { clipBoardManager, taskExecutor, fileHistory, - Injector.instantiateModelOrService(BuildInfo.class) + Injector.instantiateModelOrService(BuildInfo.class), + preferences.getWorkspacePreferences(), + Injector.instantiateModelOrService(ThemeManager.class) ); tabbedPane.getTabs().add(welcomeTab); tabbedPane.getSelectionModel().select(welcomeTab); diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/DonationPreferences.java b/jabgui/src/main/java/org/jabref/gui/preferences/DonationPreferences.java new file mode 100644 index 00000000000..b4526219b2d --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/preferences/DonationPreferences.java @@ -0,0 +1,40 @@ +package org.jabref.gui.preferences; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; + +public class DonationPreferences { + private final BooleanProperty neverShowAgain = new SimpleBooleanProperty(); + private final IntegerProperty lastShownEpochDay = new SimpleIntegerProperty(); + + public DonationPreferences(boolean neverShowAgain, int lastShownEpochDay) { + this.neverShowAgain.set(neverShowAgain); + this.lastShownEpochDay.set(lastShownEpochDay); + } + + public boolean isNeverShowAgain() { + return neverShowAgain.get(); + } + + public void setNeverShowAgain(boolean value) { + this.neverShowAgain.set(value); + } + + public BooleanProperty neverShowAgainProperty() { + return neverShowAgain; + } + + public int getLastShownEpochDay() { + return lastShownEpochDay.get(); + } + + public void setLastShownEpochDay(int value) { + this.lastShownEpochDay.set(value); + } + + public IntegerProperty lastShownEpochDayProperty() { + return lastShownEpochDay; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java b/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java index 6e1faaded08..dd134684e73 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java @@ -58,4 +58,6 @@ public interface GuiPreferences extends CliPreferences { KeyBindingRepository getKeyBindingRepository(); NewEntryPreferences getNewEntryPreferences(); + + DonationPreferences getDonationPreferences(); } diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java b/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java index c82ee3c79d3..e91f4d6bd62 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java @@ -204,6 +204,11 @@ public class JabRefGuiPreferences extends JabRefCliPreferences implements GuiPre private static final String INCLUDE_CROSS_REFERENCES = "includeCrossReferences"; private static final String ASK_FOR_INCLUDING_CROSS_REFERENCES = "askForIncludingCrossReferences"; + // region Donation preferences + private static final String DONATION_NEVER_SHOW = "donationNeverShow"; + private static final String DONATION_LAST_SHOWN_EPOCH_DAY = "donationLastShownEpochDay"; + // endregion + // region NewEntryPreferences private static final String CREATE_ENTRY_APPROACH = "latestApproach"; private static final String CREATE_ENTRY_EXPAND_RECOMMENDED = "typesRecommendedExpanded"; @@ -236,6 +241,7 @@ public class JabRefGuiPreferences extends JabRefCliPreferences implements GuiPre private KeyBindingRepository keyBindingRepository; private CopyToPreferences copyToPreferences; private NewEntryPreferences newEntryPreferences; + private DonationPreferences donationPreferences; private JabRefGuiPreferences() { super(); @@ -378,6 +384,11 @@ private JabRefGuiPreferences() { defaults.put(ASK_FOR_INCLUDING_CROSS_REFERENCES, Boolean.TRUE); defaults.put(INCLUDE_CROSS_REFERENCES, Boolean.FALSE); + // region donation defaults + defaults.put(DONATION_NEVER_SHOW, Boolean.FALSE); + defaults.put(DONATION_LAST_SHOWN_EPOCH_DAY, -1); + // endregion + // region NewEntryUnifierPreferences defaults.put(CREATE_ENTRY_APPROACH, List.of(NewEntryDialogTab.values()).indexOf(NewEntryDialogTab.CHOOSE_ENTRY_TYPE)); defaults.put(CREATE_ENTRY_EXPAND_RECOMMENDED, true); @@ -1192,6 +1203,17 @@ public NewEntryPreferences getNewEntryPreferences() { return newEntryPreferences; } + public DonationPreferences getDonationPreferences() { + if (donationPreferences != null) { + return donationPreferences; + } + + donationPreferences = new DonationPreferences(getBoolean(DONATION_NEVER_SHOW), getInt(DONATION_LAST_SHOWN_EPOCH_DAY)); + EasyBind.listen(donationPreferences.neverShowAgainProperty(), (_, _, newValue) -> putBoolean(DONATION_NEVER_SHOW, newValue)); + EasyBind.listen(donationPreferences.lastShownEpochDayProperty(), (_, _, newValue) -> putInt(DONATION_LAST_SHOWN_EPOCH_DAY, newValue.intValue())); + return donationPreferences; + } + /** * In GUI mode, we can lookup the directory better */ diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java index 28a224ba2ca..6a63aa72282 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java @@ -49,6 +49,7 @@ public class GeneralTab extends AbstractPreferenceTabView i @FXML private CheckBox confirmDelete; @FXML private CheckBox shouldAskForIncludingCrossReferences; @FXML private CheckBox confirmHideTabBar; + @FXML private CheckBox donationNeverShow; @FXML private ComboBox biblatexMode; @FXML private CheckBox alwaysReformatBib; @FXML private CheckBox autosaveLocalLibraries; @@ -125,6 +126,7 @@ public void initialize() { confirmDelete.selectedProperty().bindBidirectional(viewModel.confirmDeleteProperty()); shouldAskForIncludingCrossReferences.selectedProperty().bindBidirectional(viewModel.shouldAskForIncludingCrossReferences()); confirmHideTabBar.selectedProperty().bindBidirectional(viewModel.confirmHideTabBarProperty()); + donationNeverShow.selectedProperty().bindBidirectional(viewModel.donationNeverShowProperty()); new ViewModelListCellFactory() .withText(BibDatabaseMode::getFormattedName) diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java index fb753d849c8..beb2b7be4e2 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java @@ -84,6 +84,7 @@ public class GeneralTabViewModel implements PreferenceTabViewModel { private final BooleanProperty confirmDeleteProperty = new SimpleBooleanProperty(); private final BooleanProperty shouldAskForIncludingCrossReferencesProperty = new SimpleBooleanProperty(); private final BooleanProperty hideTabBarProperty = new SimpleBooleanProperty(); + private final BooleanProperty donationNeverShowProperty = new SimpleBooleanProperty(); private final ListProperty bibliographyModeListProperty = new SimpleListProperty<>(); private final ObjectProperty selectedBiblatexModeProperty = new SimpleObjectProperty<>(); @@ -210,6 +211,7 @@ public void setValues() { confirmDeleteProperty.setValue(workspacePreferences.shouldConfirmDelete()); shouldAskForIncludingCrossReferencesProperty.setValue(preferences.getCopyToPreferences().getShouldAskForIncludingCrossReferences()); hideTabBarProperty.setValue(workspacePreferences.shouldHideTabBar()); + donationNeverShowProperty.setValue(preferences.getDonationPreferences().isNeverShowAgain()); bibliographyModeListProperty.setValue(FXCollections.observableArrayList(BibDatabaseMode.values())); selectedBiblatexModeProperty.setValue(libraryPreferences.getDefaultBibDatabaseMode()); @@ -256,6 +258,7 @@ public void storeSettings() { workspacePreferences.setConfirmDelete(confirmDeleteProperty.getValue()); preferences.getCopyToPreferences().setShouldAskForIncludingCrossReferences(shouldAskForIncludingCrossReferencesProperty.getValue()); workspacePreferences.setHideTabBar(confirmHideTabBarProperty().getValue()); + preferences.getDonationPreferences().setNeverShowAgain(donationNeverShowProperty.getValue()); libraryPreferences.setDefaultBibDatabaseMode(selectedBiblatexModeProperty.getValue()); @@ -423,6 +426,10 @@ public BooleanProperty confirmHideTabBarProperty() { return this.hideTabBarProperty; } + public BooleanProperty donationNeverShowProperty() { + return this.donationNeverShowProperty; + } + public ListProperty biblatexModeListProperty() { return this.bibliographyModeListProperty; } diff --git a/jabgui/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/jabgui/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index 028c66f9558..e53d00a8735 100644 --- a/jabgui/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/jabgui/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -278,7 +278,14 @@ private void initSearchModifierButtons() { regexButton.setSelected(searchPreferences.isRegularExpression()); regexButton.setTooltip(new Tooltip(Localization.lang("Regular expression") + "\n" + Localization.lang("This only affects unfielded terms. For using RegEx in a fielded term, use =~ operator."))); initSearchModifierButton(regexButton); - regexButton.setOnAction(event -> { + searchPreferences.getObservableSearchFlags().addListener((SetChangeListener) change -> { + if (change.wasAdded() && change.getElementAdded() == SearchFlags.REGULAR_EXPRESSION) { + regexButton.setSelected(true); + } else if (change.wasRemoved() && change.getElementRemoved() == SearchFlags.REGULAR_EXPRESSION) { + regexButton.setSelected(false); + } + }); + regexButton.setOnAction(_ -> { searchPreferences.setSearchFlag(SearchFlags.REGULAR_EXPRESSION, regexButton.isSelected()); searchField.requestFocus(); updateSearchQuery(); @@ -287,7 +294,14 @@ private void initSearchModifierButtons() { caseSensitiveButton.setSelected(searchPreferences.isCaseSensitive()); caseSensitiveButton.setTooltip(new Tooltip(Localization.lang("Case sensitive") + "\n" + Localization.lang("This only affects unfielded terms. For using case-sensitive in a fielded term, use =! operator."))); initSearchModifierButton(caseSensitiveButton); - caseSensitiveButton.setOnAction(event -> { + searchPreferences.getObservableSearchFlags().addListener((SetChangeListener) change -> { + if (change.wasAdded() && change.getElementAdded() == SearchFlags.CASE_SENSITIVE) { + caseSensitiveButton.setSelected(true); + } else if (change.wasRemoved() && change.getElementRemoved() == SearchFlags.CASE_SENSITIVE) { + caseSensitiveButton.setSelected(false); + } + }); + caseSensitiveButton.setOnAction(_ -> { searchPreferences.setSearchFlag(SearchFlags.CASE_SENSITIVE, caseSensitiveButton.isSelected()); searchField.requestFocus(); updateSearchQuery(); @@ -296,7 +310,8 @@ private void initSearchModifierButtons() { keepSearchString.setSelected(searchPreferences.shouldKeepSearchString()); keepSearchString.setTooltip(new Tooltip(Localization.lang("Keep search string across libraries"))); initSearchModifierButton(keepSearchString); - keepSearchString.selectedProperty().addListener((obs, oldVal, newVal) -> { + searchPreferences.keepSearchStringProperty().addListener((_, _, newVal) -> keepSearchString.setSelected(newVal)); + keepSearchString.selectedProperty().addListener((_, _, newVal) -> { searchPreferences.setKeepSearchString(newVal); searchField.requestFocus(); }); @@ -304,7 +319,8 @@ private void initSearchModifierButtons() { filterModeButton.setSelected(searchPreferences.getSearchDisplayMode() == SearchDisplayMode.FILTER); filterModeButton.setTooltip(new Tooltip(Localization.lang("Filter search results"))); initSearchModifierButton(filterModeButton); - filterModeButton.setOnAction(event -> { + searchPreferences.searchDisplayModeProperty().addListener((_, _, newVal) -> filterModeButton.setSelected(newVal == SearchDisplayMode.FILTER)); + filterModeButton.setOnAction(_ -> { searchPreferences.setSearchDisplayMode(filterModeButton.isSelected() ? SearchDisplayMode.FILTER : SearchDisplayMode.FLOAT); searchField.requestFocus(); }); diff --git a/jabgui/src/main/java/org/jabref/gui/util/URLs.java b/jabgui/src/main/java/org/jabref/gui/util/URLs.java index e00cc65e4c8..bd6069fc465 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/URLs.java +++ b/jabgui/src/main/java/org/jabref/gui/util/URLs.java @@ -23,6 +23,14 @@ public class URLs { public static final String GROBID_DOC = "https://docs.jabref.org/collect/newentryfromplaintext#grobid"; public static final String ENTRY_TABLE_COLUMNS_DOC = "https://docs.jabref.org/advanced/main-window"; + // Walkthroughs URLs + public static final String GROUPS_DOC = "https://docs.jabref.org/finding-sorting-and-cleaning-entries/groups"; + public static final String SEARCH_WITH_IN_LIBRARY_DOC = "https://docs.jabref.org/finding-sorting-and-cleaning-entries/search"; + public static final String MANAGE_ASSOCIATED_FILES_DOC = "https://docs.jabref.org/finding-sorting-and-cleaning-entries/filelinks"; + public static final String LINK_EXTERNAL_FILE_WALKTHROUGH_EXAMPLE_PDF = "https://nutritionandmetabolism.biomedcentral.com/counter/pdf/10.1186/1743-7075-3-2.pdf"; + public static final String ADD_PDF_DOC = "https://docs.jabref.org/collect/add-pdfs-to-an-entry"; + public static final String FIND_UNLINKED_FILES_DOC = "https://docs.jabref.org/collect/findunlinkedfiles"; + // AboutDialogViewModel URLs public static final String HOMEPAGE_URL = "https://www.jabref.org"; public static final String DONATION_URL = "https://donations.jabref.org"; 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..23f66ed579a 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; @@ -16,7 +15,10 @@ import org.jabref.gui.fieldeditors.LinkedFilesEditor; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.maintable.MainTable; +import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.preferences.PreferencesDialogView; +import org.jabref.gui.search.GlobalSearchBar; +import org.jabref.gui.util.URLs; import org.jabref.gui.walkthrough.declarative.NodeResolver; import org.jabref.gui.walkthrough.declarative.Trigger; import org.jabref.gui.walkthrough.declarative.WindowResolver; @@ -25,6 +27,7 @@ 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.sideeffect.EnsureSearchSettingsSideEffect; import org.jabref.gui.walkthrough.declarative.sideeffect.OpenLibrarySideEffect; import org.jabref.gui.walkthrough.declarative.step.PanelPosition; import org.jabref.gui.walkthrough.declarative.step.QuitButtonPosition; @@ -32,10 +35,14 @@ import org.jabref.gui.walkthrough.declarative.step.WalkthroughStep; import org.jabref.logic.l10n.Localization; +import org.controlsfx.control.textfield.CustomTextField; + 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"; + public static final String SEARCH_WALKTHROUGH_NAME = "search"; private static final Map WALKTHROUGH_CACHE = new ConcurrentHashMap<>(); @@ -43,11 +50,13 @@ public class WalkthroughAction extends SimpleCommand { private final LibraryTabContainer frame; private final StateManager stateManager; private final Stage stage; + private final GuiPreferences preferences; - public WalkthroughAction(Stage stage, LibraryTabContainer frame, StateManager stateManager, String name) { + public WalkthroughAction(Stage stage, LibraryTabContainer frame, StateManager stateManager, GuiPreferences preferences, String name) { this.stage = stage; this.frame = frame; this.stateManager = stateManager; + this.preferences = preferences; this.walkthrough = getWalkthrough(name); } @@ -59,10 +68,15 @@ 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(); + case SEARCH_WALKTHROUGH_NAME -> createSearchWalkthrough(); + default -> + throw new IllegalArgumentException("Unknown walkthrough: " + name); } ); } @@ -125,7 +139,7 @@ private Walkthrough createCustomizeEntryTableWalkthrough() { .panel(Localization.lang("Click \"Save\" to save changes")) .content( new TextBlock(Localization.lang("Your entry table columns are now configured. These settings will be applied to all your libraries in JabRef.")), - new InfoBlock(Localization.lang("You can find more information about customizing JabRef at [documentation](https://docs.jabref.org/advanced/main-window)")) + new InfoBlock(Localization.lang("You can find more information about customizing JabRef at [documentation](%0)", URLs.ENTRY_TABLE_COLUMNS_DOC)) ) .resolver(NodeResolver.selectorWithText(".button", text -> Localization.lang("Save").equals(text))) .trigger(Trigger.onClick()) @@ -265,7 +279,7 @@ private Walkthrough createPdfLinkWalkthrough() { .highlight(HighlightEffect.SPOT_LIGHT)) .addStep(WalkthroughStep .tooltip(Localization.lang("Enter URL for download")) - .content(new TextBlock(Localization.lang("Enter the URL of the PDF file you want to download. You can try this example URL: https://nutritionandmetabolism.biomedcentral.com/counter/pdf/10.1186/1743-7075-3-2.pdf"))) + .content(new TextBlock(Localization.lang("Enter the URL of the PDF file you want to download. You can try this example URL: %0", URLs.LINK_EXTERNAL_FILE_WALKTHROUGH_EXAMPLE_PDF))) .resolver(NodeResolver.selector(".text-input")) .trigger(Trigger.onTextInput()) .activeWindow(WindowResolver.not(stage)) @@ -274,9 +288,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) @@ -286,7 +298,7 @@ private Walkthrough createPdfLinkWalkthrough() { .panel(Localization.lang("PDF file linked successfully")) .content( new TextBlock(Localization.lang("Congratulations. You have successfully linked a PDF file to a bibliography entry. This makes it easy to access your research documents directly from JabRef. You can repeat this process for all your entries.")), - new InfoBlock(Localization.lang("For detailed information: [Adding PDFs](https://docs.jabref.org/collect/add-pdfs-to-an-entry), [Managing files](https://docs.jabref.org/finding-sorting-and-cleaning-entries/filelinks), [Finding unlinked files](https://docs.jabref.org/collect/findunlinkedfiles).")) + new InfoBlock(Localization.lang("For detailed information: [Adding PDFs](%0), [Managing files](%1), [Finding unlinked files](%2).", URLs.ADD_PDF_DOC, URLs.MANAGE_ASSOCIATED_FILES_DOC, URLs.FIND_UNLINKED_FILES_DOC)) ) .resolver(NodeResolver.predicate(LinkedFilesEditor.class::isInstance)) .continueButton(Localization.lang("Finish")) @@ -296,6 +308,298 @@ 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) + .continueButton(Localization.lang("Continue")) + .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](%0)", URLs.GROUPS_DOC)) + ) + .resolver(NodeResolver.predicate(node -> node.getClass().getName().contains("GroupsSidePaneComponent"))) + .continueButton(Localization.lang("Finish")) + .position(PanelPosition.RIGHT) + .highlight(HighlightEffect.SPOT_LIGHT)) + .build(); + } + + private Walkthrough createSearchWalkthrough() { + NodeResolver searchFieldResolver = scene -> NodeResolver + .predicate(GlobalSearchBar.class::isInstance) + .resolve(scene) + .flatMap(node -> node instanceof GlobalSearchBar bar ? + bar.getChildren().stream().filter(CustomTextField.class::isInstance).findAny() : + Optional.empty()); + + return Walkthrough + .create(stateManager) + // Step 1: Open example library + .addStep(WalkthroughStep.sideEffect(Localization.lang("Open Example Library")) + .sideEffect(new OpenLibrarySideEffect(frame, "SearchExamples.bib"))) + // Step 1b: Ensure initial search settings + .addStep(WalkthroughStep.sideEffect(Localization.lang("Prepare search settings")) + .sideEffect(new EnsureSearchSettingsSideEffect(preferences.getSearchPreferences()))) + // Step 2: Introduction + .addStep(WalkthroughStep + .panel(Localization.lang("Welcome to the search walkthrough")) + .content( + new TextBlock(Localization.lang("This walkthrough will guide you through JabRef's search capabilities. We've loaded a sample library to demonstrate various search techniques.")) + ) + .resolver(NodeResolver.predicate(node -> node.getClass().getName().contains("MainTable"))) + .continueButton(Localization.lang("Continue")) + .position(PanelPosition.BOTTOM) + .quitButtonPosition(QuitButtonPosition.BOTTOM_LEFT) + .highlight(HighlightEffect.SPOT_LIGHT)) + // Step 3: Focus on GlobalSearchBar + .addStep(WalkthroughStep + .tooltip(Localization.lang("Click in the search field to begin")) + .content(new InfoBlock(Localization.lang("You can also use `Ctrl+F` or `Cmd+F` to focus the search field."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onClick()) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.SPOT_LIGHT)) + // Step 4-7: Simple text search + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `machine learning`")) + .content(new TextBlock(Localization.lang("As you type, JabRef instantly dims all non-matching entries. By default, searches are case-insensitive and look through all fields like title, author, abstract, and keywords."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextEquals("machine learning")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.SPOT_LIGHT)) + .addStep(WalkthroughStep + .panel(Localization.lang("Understanding search results")) + .content( + new TextBlock(Localization.lang("Notice how entries not containing \"machine learning\" are dimmed.")), + new InfoBlock(Localization.lang("This found entries with at least a field in their metadata (*e.g.,* title, author, abstract, *etc.*) containing \"machine learning\".")) + ) + .resolver(NodeResolver.predicate(node -> node.getClass().getName().contains("MainTable"))) + .continueButton(Localization.lang("Continue")) + .position(PanelPosition.RIGHT) + .quitButtonPosition(QuitButtonPosition.BOTTOM_LEFT) + .highlight(HighlightEffect.NONE)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Toggle filter mode to hide non-matching entries")) + .content(new TextBlock(Localization.lang("Filter mode shows only matching entries, hiding everything else."))) + .resolver(NodeResolver.buttonWithGraphic(IconTheme.JabRefIcons.FILTER)) + .trigger(Trigger.onClick()) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.SPOT_LIGHT)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Clear the search to see all entries again")) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextEquals("")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 8: Field-specific search + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `author = Son`")) + .content( + new TextBlock(Localization.lang("The equals sign (`=`) means \"contains\" and is case-insensitive by default.")), + new InfoBlock(Localization.lang("This query finds all papers where any author's name contains \"Son,\" including \"Maddison,\" \"Watson, J.,\" or \"Matson, Robert P.\". It won't match \"Son\" appearing in titles or abstracts.")) + ) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextMatchesRegex("(?i)\\s*author\\s*=\\s*son\\s*")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 9: Exact field matching + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `date == 2023`")) + .content(new TextBlock(Localization.lang("The double equals (`==`) means exact match, not just contains. This query finds only papers with field \"date\" exactly in 2023.")), + new InfoBlock(Localization.lang("With single equals (*e.g.,* `date = 23`), you'd also match dates like 1923 or 2230."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextMatchesRegex("(?i)\\s*date\\s*==\\s*2023\\s*")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 10: Combining search terms with AND + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `shortauthor = WHO AND date == 2023`")) + .content(new TextBlock(Localization.lang("This query finds papers by WHO from 2023. `AND` requires both conditions to be true."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextMatchesRegex("(?i)shortauthor\\s*=\\s*WHO\\s+AND\\s+date\\s*==\\s*2023|date\\s*==\\s*2023\\s+AND\\s+shortauthor\\s*=\\s*WHO")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 11: Using OR for alternatives + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `title = covid OR title = pandemic`")) + .content(new TextBlock(Localization.lang("This query finds paper with title containing either COVID or pandemics in general.")), + new InfoBlock(Localization.lang("`OR` can be satisified with either of the conditions being true."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextMatchesRegex("(?i)title\\s*=\\s*covid\\s+OR\\s+title\\s*=\\s*pandemic|title\\s*=\\s*pandemic\\s+OR\\s+title\\s*=\\s*covid")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 12: Negation with NOT + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `deep learning NOT title = survey`")) + .content(new TextBlock(Localization.lang("This query finds entries with \"deep learning\" in its metadata, but not \"survey\" in the title.")), + new InfoBlock(Localization.lang("NOT helps you filter out unwanted results."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextMatchesRegex("(?i)deep\\s+learning\\s+NOT\\s+title\\s*=\\s*survey")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 13: Regex + .addStep(WalkthroughStep + .tooltip(Localization.lang("Enable regular expressions for pattern matching")) + .content(new TextBlock(Localization.lang("Regular expressions allow sophisticated pattern matching. Click this button to enable regex mode for advanced searches."))) + .resolver(NodeResolver.buttonWithGraphic(IconTheme.JabRefIcons.REG_EX)) + .trigger(Trigger.onClick()) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.SPOT_LIGHT)) + // Step 14: Using regex patterns + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `title =~ \\(deep|machine\\) learning`")) + .content(new TextBlock(Localization.lang("The `=~` operator explicitly uses regex. The pipe operator `|` allows `(deep|machine)` matches either word (deep or machine). This finds \"deep learning\" and \"machine learning\" but not \"learning\" alone.")), + new InfoBlock(Localization.lang("Click on the regex button on previous step enables regex mode, allowing you to type `\\(deep|machine\\) learning` directly to search for any entry with metadata matching the regex. If you use `=~`, regardless of whether regex mode is enabled, it will always treat the search as a regex."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextMatchesRegex("(?i)title\\s*=~\\s*\\\\\\(deep\\|machine\\\\\\)\\s*learning")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 15-16: Case sensitivity + .addStep(WalkthroughStep + .tooltip(Localization.lang("Enable case-sensitive searching")) + .content(new TextBlock(Localization.lang("This is useful when case matters, like distinguishing \"WHO\" (World Health Organization) from \"who\"."))) + .resolver(NodeResolver.buttonWithGraphic(IconTheme.JabRefIcons.CASE_SENSITIVE)) + .trigger(Trigger.onClick()) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.SPOT_LIGHT)) + .addStep(WalkthroughStep + .tooltip(Localization.lang("Type `WHO`")) + .content(new TextBlock(Localization.lang("This query finds entries with capital WHO only. With case sensitivity on, this won't match \"who\" or \"Who\"."))) + .resolver(searchFieldResolver) + .trigger(Trigger.onTextEquals("WHO")) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.PING)) + // Step 17: Completion + .addStep(WalkthroughStep + .panel(Localization.lang("Search walkthrough completed")) + .content(new TextBlock(Localization.lang("**Quick reference:**\n- Press **Ctrl+F** to jump to search\n- Use **field = value** for field searches\n- Combine with **AND**, **OR**, **NOT**\n- Enable **regex** for pattern matching")), + new InfoBlock(Localization.lang("For complete search documentation and more examples, visit [Search documentation](%0)", URLs.SEARCH_WITH_IN_LIBRARY_DOC))) + .continueButton(Localization.lang("Finish")) + .position(PanelPosition.RIGHT) + .quitButtonPosition(QuitButtonPosition.BOTTOM_LEFT)) + .build(); + } + private Walkthrough createMainFileDirectoryWalkthrough() { WindowResolver mainResolver = () -> Optional.of(stage); WalkthroughEffect preferenceHighlight = new WalkthroughEffect( @@ -360,7 +664,7 @@ private Walkthrough createMainFileDirectoryWalkthrough() { .panel(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 InfoBlock(Localization.lang("Additional information on main file directory can be found in [help](https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks)")) + new InfoBlock(Localization.lang("Additional information on main file directory can be found in [Manage Associated Files Documentation](%0)", URLs.MANAGE_ASSOCIATED_FILES_DOC)) ) .resolver(NodeResolver.selectorWithText(".button", text -> Localization.lang("Save").equals(text))) .trigger(Trigger.onClick()) 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 43679021794..90b5e9d641c 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -37,7 +37,7 @@ public Node render(TooltipStep step, Walkthrough walkthrough, Runnable beforeNav titleContainer.getStyleClass().add("walkthrough-title-container"); MarkdownTextFlow titleFlow = new MarkdownTextFlow(titleContainer); titleFlow.getStyleClass().add("walkthrough-tooltip-title"); - titleFlow.setMarkdown("## " + Localization.lang(step.title())); + titleFlow.setMarkdown(step.title()); titleContainer.getChildren().add(titleFlow); VBox contentContainer = createContent(step, walkthrough, beforeNavigate); @@ -68,7 +68,7 @@ public Node render(PanelStep step, Walkthrough walkthrough, Runnable beforeNavig titleContainer.getStyleClass().add("walkthrough-title-container"); MarkdownTextFlow titleFlow = new MarkdownTextFlow(titleContainer); titleFlow.getStyleClass().add("walkthrough-title"); - titleFlow.setMarkdown("## " + Localization.lang(step.title())); + titleFlow.setMarkdown(step.title()); titleContainer.getChildren().add(titleFlow); VBox contentContainer = createContent(step, walkthrough, beforeNavigate); @@ -105,7 +105,7 @@ private Node render(TextBlock textBlock) { MarkdownTextFlow textFlow = new MarkdownTextFlow(container); textFlow.getStyleClass().add("walkthrough-text-content"); - textFlow.setMarkdown(Localization.lang(textBlock.text())); + textFlow.setMarkdown(textBlock.text()); container.getChildren().add(textFlow); return container; @@ -120,7 +120,7 @@ private Node render(InfoBlock infoBlock) { StackPane textContainer = new StackPane(); MarkdownTextFlow infoFlow = new MarkdownTextFlow(textContainer); infoFlow.getStyleClass().add("walkthrough-info-text"); - infoFlow.setMarkdown(Localization.lang(infoBlock.text())); + infoFlow.setMarkdown(infoBlock.text()); textContainer.getChildren().add(infoFlow); HBox.setHgrow(textContainer, Priority.ALWAYS); 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..afbb454c2c0 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,24 +1,28 @@ 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.ButtonBase; +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; import org.jabref.gui.icon.JabRefIconView; import org.jabref.logic.l10n.Localization; +import com.google.common.collect.Streams; import com.sun.javafx.scene.control.LabeledText; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.kordamp.ikonli.javafx.FontIcon; /// Resolves nodes from a Scene @FunctionalInterface @@ -50,12 +54,18 @@ static NodeResolver fxId(@NonNull String fxId) { /// @param glyph the graphic of the button /// @return a resolver that finds the button by graphic static NodeResolver buttonWithGraphic(IconTheme.JabRefIcons glyph) { - return scene -> scene.getRoot().lookupAll(".button").stream() - .filter(node -> node instanceof Button button - && Optional.ofNullable(findNode(button.getGraphic(), JabRefIconView.class::isInstance)) - .map(JabRefIconView.class::cast).filter(icon -> icon.getGlyph() == glyph) - .isPresent()) - .findFirst(); + // .icon-button, .button selector is not used, because lookupAll doesn't support multiple selectors + return scene -> Streams.concat(scene.getRoot().lookupAll(".button").stream(), + scene.getRoot().lookupAll(".icon-button").stream()) + .filter(node -> { + if (!(node instanceof ButtonBase button)) { + return false; + } + Node graphic = button.getGraphic(); + return (graphic instanceof JabRefIconView jabRefIconView) && jabRefIconView.getGlyph() == glyph || + (graphic instanceof FontIcon fontIcon) && fontIcon.getIconCode() == glyph.getIkon(); + }) + .findFirst(); } /// Creates a resolver that finds a node by a predicate. @@ -74,6 +84,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 +131,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/walkthrough/declarative/sideeffect/EnsureSearchSettingsSideEffect.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/sideeffect/EnsureSearchSettingsSideEffect.java new file mode 100644 index 00000000000..fdbe287bbb8 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/sideeffect/EnsureSearchSettingsSideEffect.java @@ -0,0 +1,51 @@ +package org.jabref.gui.walkthrough.declarative.sideeffect; + +import org.jabref.gui.walkthrough.Walkthrough; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.search.SearchPreferences; +import org.jabref.model.search.SearchDisplayMode; +import org.jabref.model.search.SearchFlags; + +import org.jspecify.annotations.NonNull; + +/// Ensures the search bar starts with unfiltered mode and regex disabled, then restores +/// previous settings on exit. +public class EnsureSearchSettingsSideEffect implements WalkthroughSideEffect { + private final boolean previousRegex; + private final boolean previousCaseSensitive; + private final SearchDisplayMode previousDisplayMode; + private final SearchPreferences searchPreferences; + + public EnsureSearchSettingsSideEffect(SearchPreferences searchPreferences) { + this.searchPreferences = searchPreferences; + this.previousRegex = searchPreferences.isRegularExpression(); + this.previousCaseSensitive = searchPreferences.isCaseSensitive(); + this.previousDisplayMode = searchPreferences.getSearchDisplayMode(); + } + + @Override + public @NonNull ExpectedCondition expectedCondition() { + return ExpectedCondition.ALWAYS_TRUE; + } + + @Override + public boolean forward(@NonNull Walkthrough walkthrough) { + searchPreferences.setSearchFlag(SearchFlags.REGULAR_EXPRESSION, false); + searchPreferences.setSearchFlag(SearchFlags.CASE_SENSITIVE, false); + searchPreferences.setSearchDisplayMode(SearchDisplayMode.FLOAT); + return true; + } + + @Override + public boolean backward(@NonNull Walkthrough walkthrough) { + searchPreferences.setSearchFlag(SearchFlags.REGULAR_EXPRESSION, previousRegex); + searchPreferences.setSearchFlag(SearchFlags.CASE_SENSITIVE, previousCaseSensitive); + searchPreferences.setSearchDisplayMode(previousDisplayMode); + return true; + } + + @Override + public @NonNull String description() { + return Localization.lang("Prepare search settings"); + } +} 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..51bcd11c32b 100644 --- a/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java +++ b/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java @@ -7,14 +7,17 @@ import javafx.collections.ListChangeListener; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Button; 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.ColumnConstraints; import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.Stage; @@ -23,6 +26,7 @@ import org.jabref.gui.LibraryTab; import org.jabref.gui.LibraryTabContainer; import org.jabref.gui.StateManager; +import org.jabref.gui.WorkspacePreferences; import org.jabref.gui.actions.StandardActions; import org.jabref.gui.edit.OpenBrowserAction; import org.jabref.gui.frame.FileHistoryMenu; @@ -30,10 +34,12 @@ import org.jabref.gui.importer.NewDatabaseAction; import org.jabref.gui.importer.actions.OpenDatabaseAction; import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.util.URLs; -import org.jabref.gui.walkthrough.WalkthroughAction; +import org.jabref.gui.welcome.components.DonationProvider; import org.jabref.gui.welcome.components.QuickSettings; +import org.jabref.gui.welcome.components.Walkthroughs; import org.jabref.logic.ai.AiService; import org.jabref.logic.importer.Importer; import org.jabref.logic.importer.ParserResult; @@ -45,6 +51,7 @@ import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.util.FileUpdateMonitor; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +72,12 @@ public class WelcomeTab extends Tab { private final FileHistoryMenu fileHistoryMenu; private final BuildInfo buildInfo; private final Stage stage; + private final WorkspacePreferences workspacePreferences; + private final ThemeManager themeManager; + private Walkthroughs walkthroughs; + + private QuickSettings quickSettings; + private final DonationProvider donationProvider; public WelcomeTab(Stage stage, LibraryTabContainer tabContainer, @@ -78,7 +91,9 @@ public WelcomeTab(Stage stage, ClipBoardManager clipBoardManager, TaskExecutor taskExecutor, FileHistoryMenu fileHistoryMenu, - BuildInfo buildInfo) { + BuildInfo buildInfo, + WorkspacePreferences workspacePreferences, + ThemeManager themeManager) { super(Localization.lang("Welcome")); setClosable(true); this.tabContainer = tabContainer; @@ -94,27 +109,29 @@ public WelcomeTab(Stage stage, this.fileHistoryMenu = fileHistoryMenu; this.buildInfo = buildInfo; this.stage = stage; + this.workspacePreferences = workspacePreferences; + this.themeManager = themeManager; this.recentLibrariesBox = new VBox(); recentLibrariesBox.getStyleClass().add("welcome-recent-libraries"); - VBox mainContainer = new VBox(createTopTitles(), createColumnsContainer(), createCommunityBox()); - mainContainer.getStyleClass().add("welcome-main-container"); + Node titles = createTopTitles(); + Node communityBox = createCommunityBox(); + ScrollPane columnsScroll = createColumnsContainerScrollable(); - VBox container = new VBox(); - container.getChildren().add(mainContainer); - VBox.setVgrow(mainContainer, Priority.ALWAYS); + VBox mainStack = new VBox(titles, columnsScroll, communityBox); + mainStack.getStyleClass().add("welcome-main-container"); + VBox.setVgrow(columnsScroll, Priority.ALWAYS); + + VBox container = new VBox(mainStack); container.setAlignment(Pos.CENTER); - setContent(container); - } + StackPane rootPane = new StackPane(container); + setContent(rootPane); - private Button createWalkthroughButton(String text, IconTheme.JabRefIcons icon, String walkthroughId) { - Button button = new Button(text); - button.setGraphic(icon.getGraphicNode()); - button.getStyleClass().add("quick-settings-button"); - button.setMaxWidth(Double.MAX_VALUE); - button.setOnAction(_ -> new WalkthroughAction(stage, tabContainer, stateManager, walkthroughId).execute()); - return button; + donationProvider = new DonationProvider(rootPane, preferences, dialogService); + donationProvider.showIfNeeded(); + + setOnClosed(_ -> donationProvider.cleanUp()); } private VBox createTopTitles() { @@ -122,23 +139,32 @@ private VBox createTopTitles() { welcomeLabel.getStyleClass().add("welcome-label"); Label descriptionLabel = new Label(Localization.lang("Stay on top of your literature")); descriptionLabel.getStyleClass().add("welcome-description-label"); - VBox topTitles = new VBox(); + VBox topTitles = new VBox(welcomeLabel, descriptionLabel); topTitles.getStyleClass().add("welcome-top-titles"); - topTitles.getChildren().addAll(welcomeLabel, descriptionLabel); return topTitles; } - private HBox createColumnsContainer() { + private ScrollPane createColumnsContainerScrollable() { + GridPane grid = new GridPane(); + grid.getStyleClass().add("welcome-columns-container"); + VBox leftColumn = createLeftColumn(); + GridPane.setHgrow(leftColumn, Priority.ALWAYS); + VBox rightColumn = createRightColumn(); - HBox columnsContainer = new HBox(); - columnsContainer.getStyleClass().add("welcome-columns-container"); - leftColumn.getStyleClass().add("welcome-left-column"); - rightColumn.getStyleClass().add("welcome-right-column"); - HBox.setHgrow(leftColumn, Priority.ALWAYS); - HBox.setHgrow(rightColumn, Priority.ALWAYS); - columnsContainer.getChildren().addAll(leftColumn, rightColumn); - return columnsContainer; + GridPane.setHgrow(rightColumn, Priority.ALWAYS); + + Runnable applyLayout = () -> updateColumnsLayout(grid, leftColumn, rightColumn, grid.getWidth()); + grid.widthProperty().addListener((_, _, _) -> applyLayout.run()); + applyLayout.run(); + + ScrollPane scrollPane = new ScrollPane(grid); + scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.getStyleClass().add("welcome-columns-scroll"); + scrollPane.setStyle("-fx-background-color: transparent;"); // Using class selector is insufficient to prevent background from turning white on click. + return scrollPane; } private VBox createLeftColumn() { @@ -151,51 +177,42 @@ private VBox createLeftColumn() { } private VBox createRightColumn() { - VBox rightColumn = new VBox(new QuickSettings(), createWalkthroughBox()); + this.quickSettings = new QuickSettings(preferences, dialogService, taskExecutor, themeManager); + this.walkthroughs = new Walkthroughs(stage, tabContainer, stateManager, preferences); + VBox rightColumn = new VBox(quickSettings, walkthroughs); rightColumn.getStyleClass().add("welcome-content-column"); + VBox.setVgrow(quickSettings, Priority.ALWAYS); + VBox.setVgrow(walkthroughs, Priority.ALWAYS); return rightColumn; } - private VBox createWalkthroughBox() { - Label header = new Label(Localization.lang("Walkthroughs")); - header.getStyleClass().add("welcome-header-label"); - - VBox walkthroughsContainer = new VBox(); - walkthroughsContainer.getStyleClass().add("walkthroughs-container"); - - Button mainFileDirWalkthroughButton = createWalkthroughButton( - Localization.lang("Set main file directory"), - IconTheme.JabRefIcons.FOLDER, - WalkthroughAction.MAIN_FILE_DIRECTORY_WALKTHROUGH_NAME - ); - - Button entryTableWalkthroughButton = createWalkthroughButton( - Localization.lang("Customize entry table"), - IconTheme.JabRefIcons.TOGGLE_GROUPS, - WalkthroughAction.CUSTOMIZE_ENTRY_TABLE_WALKTHROUGH_NAME - ); - - Button linkPdfWalkthroughButton = createWalkthroughButton( - Localization.lang("Link PDF to entries"), - IconTheme.JabRefIcons.TOGGLE_GROUPS, - WalkthroughAction.PDF_LINK_WALKTHROUGH_NAME - ); - - walkthroughsContainer.getChildren().addAll(mainFileDirWalkthroughButton, entryTableWalkthroughButton, linkPdfWalkthroughButton); - - return createVBoxContainer(header, walkthroughsContainer); - } - - private VBox createCommunityBox() { - Label header = new Label(Localization.lang("Community")); - header.getStyleClass().add("welcome-header-label"); - FlowPane iconLinksContainer = createIconLinksContainer(); - HBox textLinksContainer = createTextLinksContainer(); - HBox versionContainer = createVersionContainer(); - VBox container = new VBox(); - container.getStyleClass().add("welcome-community-content"); - container.getChildren().addAll(iconLinksContainer, textLinksContainer, versionContainer); - return createVBoxContainer(header, container); + private void updateColumnsLayout(@NonNull GridPane grid, + @NonNull VBox leftColumn, + @NonNull VBox rightColumn, + double availableWidth) { + grid.getChildren().clear(); + grid.getColumnConstraints().clear(); + + double threshold = 48 * workspacePreferences.getMainFontSize(); + if (availableWidth < threshold) { + grid.getStyleClass().add("compact"); + ColumnConstraints single = new ColumnConstraints(); + single.setPercentWidth(100); + grid.getColumnConstraints().add(single); + grid.add(leftColumn, 0, 0); + grid.add(rightColumn, 0, 1); + quickSettings.disableScroll(); + walkthroughs.disableScroll(); + } else { + grid.getStyleClass().remove("compact"); + ColumnConstraints half = new ColumnConstraints(); + half.setPercentWidth(50); + grid.getColumnConstraints().addAll(half, half); + grid.add(leftColumn, 0, 0); + grid.add(rightColumn, 1, 0); + quickSettings.enableScroll(); + walkthroughs.enableScroll(); + } } private VBox createWelcomeStartBox() { @@ -281,11 +298,16 @@ private void displayNoRecentLibrariesMessage() { fileHistoryMenu.setDisable(true); } - private VBox createVBoxContainer(Node... nodes) { - VBox box = new VBox(); - box.getStyleClass().add("welcome-section"); - box.getChildren().addAll(nodes); - return box; + private VBox createCommunityBox() { + Label header = new Label(Localization.lang("Community")); + header.getStyleClass().add("welcome-header-label"); + FlowPane iconLinksContainer = createIconLinksContainer(); + HBox textLinksContainer = createTextLinksContainer(); + HBox versionContainer = createVersionContainer(); + VBox container = new VBox(); + container.getStyleClass().add("welcome-community-content"); + container.getChildren().addAll(iconLinksContainer, textLinksContainer, versionContainer); + return createVBoxContainer(header, container); } private FlowPane createIconLinksContainer() { @@ -345,4 +367,11 @@ private HBox createVersionContainer() { container.getChildren().add(versionLabel); return container; } + + private VBox createVBoxContainer(Node... nodes) { + VBox box = new VBox(); + box.getStyleClass().add("welcome-section"); + box.getChildren().addAll(nodes); + return box; + } } diff --git a/jabgui/src/main/java/org/jabref/gui/welcome/components/DonationProvider.java b/jabgui/src/main/java/org/jabref/gui/welcome/components/DonationProvider.java new file mode 100644 index 00000000000..f5c75b90ea2 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/welcome/components/DonationProvider.java @@ -0,0 +1,176 @@ +package org.jabref.gui.welcome.components; + +import java.time.LocalDate; + +import javafx.animation.TranslateTransition; +import javafx.geometry.Insets; +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.StackPane; +import javafx.scene.layout.VBox; +import javafx.util.Duration; + +import org.jabref.gui.DialogService; +import org.jabref.gui.edit.OpenBrowserAction; +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.util.DelayedExecution; +import org.jabref.gui.util.URLs; +import org.jabref.logic.l10n.Localization; + +public class DonationProvider { + private static final int DONATION_INTERVAL_DAYS = 365; + private static final double DONATION_POPUP_ANIM_MS = 200; + private static final double DONATION_POPUP_TIMEOUT_SECONDS = 15; + private static final String DONATION_URL = URLs.DONATE_URL; + + private final StackPane rootPane; + private final GuiPreferences preferences; + private final DialogService dialogService; + private HBox donationToast; + private DelayedExecution scheduledShow; + private DelayedExecution autoHide; + + public DonationProvider(StackPane rootPane, GuiPreferences preferences, DialogService dialogService) { + this.rootPane = rootPane; + this.preferences = preferences; + this.dialogService = dialogService; + } + + public void showIfNeeded() { + if (preferences.getDonationPreferences().isNeverShowAgain()) { + return; + } + int lastShown = preferences.getDonationPreferences().getLastShownEpochDay(); + scheduleAfterDays(calculateDaysUntilNextPopup(lastShown)); + } + + public int calculateDaysUntilNextPopup(int lastShownEpochDay) { + int today = (int) LocalDate.now().toEpochDay(); + if (lastShownEpochDay < 0) { + return 7; // 7 days after first-launch, show the donation popup + } + return Math.max(0, DONATION_INTERVAL_DAYS - (today - lastShownEpochDay)); + } + + public void showToast() { + if (donationToast != null && rootPane.getChildren().contains(donationToast)) { + return; + } + + preferences.getDonationPreferences().setLastShownEpochDay((int) LocalDate.now().toEpochDay()); + + Label title = new Label(Localization.lang("Support JabRef")); + title.getStyleClass().add("donation-toast-title"); + Label subtitle = new Label(Localization.lang("Help us improve JabRef by donating.")); + subtitle.getStyleClass().add("donation-toast-desc"); + VBox textBox = new VBox(title, subtitle); + textBox.getStyleClass().add("donation-toast-text"); + + Node iconNode = IconTheme.JabRefIcons.DONATE.getGraphicNode(); + HBox leftContent = new HBox(10, iconNode, textBox); + leftContent.setAlignment(Pos.CENTER_LEFT); + + Button neverButton = new Button(Localization.lang("Never show again")); + neverButton.getStyleClass().add("donation-btn-ghost"); + neverButton.setOnAction(_ -> { + preferences.getDonationPreferences().setNeverShowAgain(true); + hideToast(); + }); + + Button cancelButton = new Button(Localization.lang("Cancel")); + cancelButton.getStyleClass().add("donation-btn-secondary"); + cancelButton.setOnAction(_ -> hideToast()); + + Button donateButton = new Button(Localization.lang("Donate")); + donateButton.getStyleClass().add("donation-btn-primary"); + donateButton.setDefaultButton(true); + donateButton.setOnAction(_ -> { + new OpenBrowserAction(DONATION_URL, dialogService, preferences.getExternalApplicationsPreferences()).execute(); + hideToast(); + }); + + HBox rightButtons = new HBox(8, neverButton, cancelButton, donateButton); + rightButtons.setAlignment(Pos.CENTER_RIGHT); + + Region textSpacer = new Region(); + HBox.setHgrow(textSpacer, Priority.ALWAYS); + + donationToast = new HBox(leftContent, textSpacer, rightButtons); + donationToast.getStyleClass().add("donation-toast"); + donationToast.setMaxWidth(Region.USE_PREF_SIZE); + donationToast.setMinWidth(Region.USE_PREF_SIZE); + donationToast.setTranslateY(-40); + + StackPane.setAlignment(donationToast, Pos.TOP_CENTER); + StackPane.setMargin(donationToast, new Insets(16)); + rootPane.getChildren().add(donationToast); + + TranslateTransition slideDown = new TranslateTransition(Duration.millis(DONATION_POPUP_ANIM_MS), donationToast); + slideDown.setFromY(-40); + slideDown.setToY(0); + slideDown.play(); + + if (autoHide != null) { + autoHide.cancel(); + } + autoHide = new DelayedExecution(Duration.seconds(DONATION_POPUP_TIMEOUT_SECONDS), this::hideToast); + autoHide.start(); + } + + private void hideToast() { + if (donationToast == null) { + return; + } + TranslateTransition slideUp = new TranslateTransition(Duration.millis(DONATION_POPUP_ANIM_MS), donationToast); + slideUp.setFromY(donationToast.getTranslateY()); + slideUp.setToY(-40); + slideUp.setOnFinished(_ -> { + rootPane.getChildren().remove(donationToast); + donationToast = null; + if (autoHide != null) { + autoHide.cancel(); + autoHide = null; + } + }); + slideUp.play(); + } + + /// Schedules the next donation popup after a specified number of days to take care + /// of situation where the user may leave the application open for an extended + /// period. + private void scheduleAfterDays(int days) { + cancelScheduled(); + if (days <= 0) { + showToast(); + scheduledShow = new DelayedExecution(Duration.millis(DONATION_INTERVAL_DAYS), this::showIfNeeded); + } else { + scheduledShow = new DelayedExecution(Duration.hours(days * 24), this::showIfNeeded); + } + scheduledShow.start(); + } + + private void cancelScheduled() { + if (scheduledShow != null) { + scheduledShow.cancel(); + scheduledShow = null; + } + } + + public void cleanUp() { + cancelScheduled(); + if (autoHide != null) { + autoHide.cancel(); + autoHide = null; + } + if (donationToast != null) { + rootPane.getChildren().remove(donationToast); + donationToast = null; + } + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/welcome/components/QuickSettings.java b/jabgui/src/main/java/org/jabref/gui/welcome/components/QuickSettings.java index f496a101dc4..f853151d6fe 100644 --- a/jabgui/src/main/java/org/jabref/gui/welcome/components/QuickSettings.java +++ b/jabgui/src/main/java/org/jabref/gui/welcome/components/QuickSettings.java @@ -1,9 +1,12 @@ package org.jabref.gui.welcome.components; -import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; import org.jabref.gui.DialogService; +import org.jabref.gui.icon.IconTheme; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.welcome.quicksettings.EntryTableConfigurationDialog; @@ -12,49 +15,128 @@ import org.jabref.gui.welcome.quicksettings.OnlineServicesDialog; import org.jabref.gui.welcome.quicksettings.PushApplicationDialog; import org.jabref.gui.welcome.quicksettings.ThemeDialog; +import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.TaskExecutor; -import com.airhacks.afterburner.views.ViewLoader; -import jakarta.inject.Inject; - public class QuickSettings extends VBox { - @Inject private GuiPreferences preferences; - @Inject private DialogService dialogService; - @Inject private TaskExecutor taskExecutor; - @Inject private ThemeManager themeManager; + private final GuiPreferences preferences; + private final DialogService dialogService; + private final TaskExecutor taskExecutor; + + private final ThemeManager themeManager; + private final Label header; + private boolean isScrollEnabled = true; + + public QuickSettings(GuiPreferences preferences, DialogService dialogService, TaskExecutor taskExecutor, ThemeManager themeManager) { + this.preferences = preferences; + this.dialogService = dialogService; + this.taskExecutor = taskExecutor; + this.themeManager = themeManager; + + getStyleClass().add("welcome-section"); + + header = new Label(Localization.lang("Quick settings")); + header.getStyleClass().add("welcome-header-label"); + enableScroll(); + } + + public void enableScroll() { + if (isScrollEnabled) { + return; + } + isScrollEnabled = true; + getChildren().clear(); + getChildren().addAll(header, createScrollPane(createContent())); + } + + public void disableScroll() { + if (!isScrollEnabled) { + return; + } + isScrollEnabled = false; + getChildren().clear(); + getChildren().addAll(header, createContent()); + } + + private VBox createContent() { + Button mainFileDirButton = createButton( + Localization.lang("Set main file directory"), + IconTheme.JabRefIcons.FOLDER, + this::showMainFileDirectoryDialog); + + Button themeButton = createButton( + Localization.lang("Change visual theme"), + IconTheme.JabRefIcons.PREFERENCES, + this::showThemeDialog); + + Button largeLibraryButton = createButton( + Localization.lang("Optimize for large libraries"), + IconTheme.JabRefIcons.SELECTORS, + this::showLargeLibraryOptimizationDialog); + + Button pushApplicationButton = createButton( + Localization.lang("Configure push to applications"), + IconTheme.JabRefIcons.APPLICATION_GENERIC, + this::showPushApplicationConfigurationDialog); + + Button onlineServicesButton = createButton( + Localization.lang("Configure web search services"), + IconTheme.JabRefIcons.WWW, + this::showOnlineServicesConfigurationDialog); + + Button entryTableButton = createButton( + Localization.lang("Customize entry table"), + IconTheme.JabRefIcons.TOGGLE_GROUPS, + this::showEntryTableConfigurationDialog); + + VBox newContent = new VBox(mainFileDirButton, + themeButton, + largeLibraryButton, + entryTableButton, + pushApplicationButton, + onlineServicesButton); + newContent.getStyleClass().add("quick-settings-container"); + return newContent; + } + + private ScrollPane createScrollPane(VBox contentPane) { + ScrollPane newScrollPane = new ScrollPane(contentPane); + newScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + newScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + newScrollPane.setFitToWidth(true); + newScrollPane.getStyleClass().add("quick-settings-scroll-pane"); + return newScrollPane; + } - public QuickSettings() { - ViewLoader.view(this) - .root(this) - .load(); + private Button createButton(String text, IconTheme.JabRefIcons icon, Runnable action) { + Button button = new Button(text); + button.setGraphic(icon.getGraphicNode()); + button.getStyleClass().add("quick-settings-button"); + button.setMaxWidth(Double.MAX_VALUE); + button.setOnAction(event -> action.run()); + return button; } - @FXML private void showMainFileDirectoryDialog() { dialogService.showCustomDialogAndWait(new MainFileDirectoryDialog(preferences, dialogService, themeManager)); } - @FXML private void showThemeDialog() { dialogService.showCustomDialogAndWait(new ThemeDialog(preferences, dialogService, themeManager)); } - @FXML private void showLargeLibraryOptimizationDialog() { dialogService.showCustomDialogAndWait(new LargeLibraryOptimizationDialog(preferences, themeManager)); } - @FXML private void showPushApplicationConfigurationDialog() { dialogService.showCustomDialogAndWait(new PushApplicationDialog(preferences, dialogService, taskExecutor, themeManager)); } - @FXML private void showOnlineServicesConfigurationDialog() { dialogService.showCustomDialogAndWait(new OnlineServicesDialog(preferences, themeManager)); } - @FXML private void showEntryTableConfigurationDialog() { dialogService.showCustomDialogAndWait(new EntryTableConfigurationDialog(preferences, themeManager)); } diff --git a/jabgui/src/main/java/org/jabref/gui/welcome/components/Walkthroughs.java b/jabgui/src/main/java/org/jabref/gui/welcome/components/Walkthroughs.java new file mode 100644 index 00000000000..76067aec797 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/welcome/components/Walkthroughs.java @@ -0,0 +1,107 @@ +package org.jabref.gui.welcome.components; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import org.jabref.gui.LibraryTabContainer; +import org.jabref.gui.StateManager; +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.walkthrough.WalkthroughAction; +import org.jabref.logic.l10n.Localization; + +public class Walkthroughs extends VBox { + private final Stage stage; + private final LibraryTabContainer tabContainer; + private final StateManager stateManager; + private final GuiPreferences preferences; + + private final Label header; + private boolean isScrollEnabled = true; + + public Walkthroughs(Stage stage, LibraryTabContainer tabContainer, StateManager stateManager, GuiPreferences preferences) { + this.stage = stage; + this.tabContainer = tabContainer; + this.stateManager = stateManager; + this.preferences = preferences; + + getStyleClass().add("welcome-section"); + + header = new Label(Localization.lang("Walkthroughs")); + header.getStyleClass().add("welcome-header-label"); + enableScroll(); + } + + public void enableScroll() { + if (isScrollEnabled) { + return; + } + isScrollEnabled = true; + getChildren().clear(); + getChildren().addAll(header, createScrollPane(createWalkthroughContent())); + } + + public void disableScroll() { + if (!isScrollEnabled) { + return; + } + isScrollEnabled = false; + getChildren().clear(); + getChildren().addAll(header, createWalkthroughContent()); + } + + private VBox createWalkthroughContent() { + VBox content = new VBox(); + content.getStyleClass().add("walkthroughs-container"); + + Button mainFileDirWalkthroughButton = createWalkthroughButton( + Localization.lang("Set main file directory"), + IconTheme.JabRefIcons.FOLDER, + WalkthroughAction.MAIN_FILE_DIRECTORY_WALKTHROUGH_NAME); + Button entryTableWalkthroughButton = createWalkthroughButton( + Localization.lang("Customize entry table"), + IconTheme.JabRefIcons.TOGGLE_GROUPS, + WalkthroughAction.CUSTOMIZE_ENTRY_TABLE_WALKTHROUGH_NAME); + Button linkPdfWalkthroughButton = createWalkthroughButton( + Localization.lang("Link PDF to entries"), + IconTheme.JabRefIcons.PDF_FILE, + WalkthroughAction.PDF_LINK_WALKTHROUGH_NAME); + Button groupButton = createWalkthroughButton( + Localization.lang("Add group"), + IconTheme.JabRefIcons.NEW_GROUP, + WalkthroughAction.GROUP_WALKTHROUGH_NAME); + Button searchButton = createWalkthroughButton( + Localization.lang("Search your library"), + IconTheme.JabRefIcons.SEARCH, + WalkthroughAction.SEARCH_WALKTHROUGH_NAME); + + content.getChildren().addAll( + mainFileDirWalkthroughButton, + entryTableWalkthroughButton, + linkPdfWalkthroughButton, + groupButton, + searchButton); + return content; + } + + private ScrollPane createScrollPane(VBox content) { + ScrollPane newScrollPane = new ScrollPane(content); + newScrollPane.setFitToWidth(true); + newScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + newScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + newScrollPane.getStyleClass().add("walkthroughs-scroll-pane"); + return newScrollPane; + } + + private Button createWalkthroughButton(String text, IconTheme.JabRefIcons icon, String walkthroughId) { + Button button = new Button(text); + button.setGraphic(icon.getGraphicNode()); + button.getStyleClass().add("quick-settings-button"); + button.setMaxWidth(Double.MAX_VALUE); + button.setOnAction(_ -> new WalkthroughAction(stage, tabContainer, stateManager, preferences, walkthroughId).execute()); + return button; + } +} diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index e2785f6a27b..4e5245c0c3f 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -1752,6 +1752,7 @@ We want to have a look that matches our icons in the tool-bar */ -fx-font-size: 1.5em; -fx-font-weight: bold; -fx-font-family: "Arial"; + -fx-text-fill: -jr-gray-3; } .welcome-no-recent-label { @@ -1778,6 +1779,13 @@ We want to have a look that matches our icons in the tool-bar */ -fx-padding: 2.667em 2em; } +.welcome-columns-container, +.welcome-columns-container:focus, +.welcome-columns-scroll, +.welcome-columns-scroll:focus { + -fx-background-color: transparent; +} + .welcome-top-titles { -fx-spacing: 0.833em; -fx-alignment: top-left; @@ -1785,7 +1793,8 @@ We want to have a look that matches our icons in the tool-bar */ } .welcome-columns-container { - -fx-spacing: 3.333em; + -fx-hgap: 2.083em; + -fx-vgap: 2.083em; -fx-alignment: top-center; } @@ -1851,6 +1860,64 @@ We want to have a look that matches our icons in the tool-bar */ -fx-font-family: "Arial"; } +/* donation toast */ +.donation-toast { + -fx-background-color: -jr-background-alt; + -fx-background-radius: 0.8333333333em; + -fx-padding: 0.8333em 1em; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.075), 20, 0.25, 0, 6); + -fx-alignment: center-left; + -fx-border-color: derive(-jr-base, -10%); + -fx-border-radius: 0.8333333333em; + -fx-max-height: 4em; + -fx-spacing: 1.16667em; +} + +.donation-toast .label { + -fx-text-fill: -fx-text-base-color; +} + +.donation-toast-title { + -fx-font-size: 1.0em; + -fx-font-weight: bold; +} + +.donation-toast-desc { + -fx-font-size: 0.92em; +} + +.donation-toast-text { + -fx-spacing: 2; +} + +.donation-btn-primary, +.donation-btn-secondary, +.donation-btn-ghost { + -fx-padding: 0.5em 1em; + -fx-background-radius: 0.5em; + -fx-border-radius: 0.5em; +} + +.donation-btn-primary { + -fx-background-color: -jr-primary; +} + +.donation-btn-secondary { + -fx-background-color: derive(-fx-control-inner-background, -6%); +} + +.donation-btn-ghost { + -fx-background-color: transparent; + -fx-text-fill: -jr-gray-1; + -fx-stroke-width: 0; + -fx-border-width: 0 +} + +.donation-btn-ghost:hover { + -fx-background-color: transparent; + -fx-text-fill: -jr-gray-2; +} + /* AboutDialog */ #aboutDialog .about-heading { -fx-font-size: 2.40em; @@ -2584,17 +2651,16 @@ journalInfo .grid-cell-b { /* Quick Settings */ - .quick-settings-container, .walkthroughs-container { - -fx-spacing: 0.6667em; + -fx-spacing: 0.3333em; -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; - -fx-pref-height: 13.333em; + -fx-max-height: 12em; + -fx-pref-height: 12em; } .quick-settings-button { @@ -2659,7 +2725,7 @@ journalInfo .grid-cell-b { -fx-border-width: 0.0625em; -fx-border-radius: 0.25em; -fx-background-radius: 0.25em; - -fx-effect: dropshadow(three-pass-box , rgba(0,0,0,0.1) , 0.125em, 0.0 , 0 , 0.0625em ); + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.1), 0.125em, 0.0, 0, 0.0625em); -fx-border-color: -jr-wf-border-color; } @@ -2836,12 +2902,10 @@ journalInfo .grid-cell-b { -fx-max-width: infinity; } -.walkthrough-tooltip-title, -.walkthrough-title, -.walkthrough-tooltip-title .markdown-h2, -.walkthrough-title .markdown-h2 { - -fx-text-fill: -jr-theme; - -fx-fill: -jr-theme; +.walkthrough-tooltip-title > *, +.walkthrough-title > * { + -fx-text-fill: -jr-theme!important; + -fx-fill: -jr-theme!important; -fx-font-weight: bold; -fx-font-size: 1.75em; -fx-line-spacing: -0.25em; @@ -2876,7 +2940,7 @@ journalInfo .grid-cell-b { } .walkthrough-text-content { - -fx-text-fill: -jr-gray-3; + -fx-text-fill: -fx-text-base-color; -fx-font-size: 1.2em; -fx-line-spacing: 0.5em; } diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/general/GeneralTab.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/general/GeneralTab.fxml index cab859b9ac9..d3b67bea682 100644 --- a/jabgui/src/main/resources/org/jabref/gui/preferences/general/GeneralTab.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/general/GeneralTab.fxml @@ -74,6 +74,7 @@ +