diff --git a/CHANGELOG.md b/CHANGELOG.md index 22db2799aeb..efee6747df4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added the field `monthfiled` to the default list of fields to resolve BibTeX-Strings for [#13375](https://github.com/JabRef/jabref/issues/13375) - We added a new ID based fetcher for [EuropePMC](https://europepmc.org/). [#13389](https://github.com/JabRef/jabref/pull/13389) - We added an initial [cite as you write](https://retorque.re/zotero-better-bibtex/citing/cayw/) endpoint. [#13187](https://github.com/JabRef/jabref/issues/13187) +- We added quick settings for welcome tab. [#12664](https://github.com/JabRef/jabref/issues/12664) ### Changed diff --git a/jabgui/src/main/java/module-info.java b/jabgui/src/main/java/module-info.java index 7eafc8547f3..5b62ad2acc4 100644 --- a/jabgui/src/main/java/module-info.java +++ b/jabgui/src/main/java/module-info.java @@ -59,7 +59,7 @@ // endregion provides org.tinylog.writers.Writer - with org.jabref.gui.logging.GuiWriter; + with org.jabref.gui.logging.GuiWriter; // Preferences and XML requires java.prefs; diff --git a/jabgui/src/main/java/org/jabref/gui/WelcomeTab.java b/jabgui/src/main/java/org/jabref/gui/WelcomeTab.java deleted file mode 100644 index a48c7990598..00000000000 --- a/jabgui/src/main/java/org/jabref/gui/WelcomeTab.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.jabref.gui; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; - -import javafx.collections.ListChangeListener; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Hyperlink; -import javafx.scene.control.Label; -import javafx.scene.control.MenuItem; -import javafx.scene.control.Tab; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; - -import org.jabref.gui.actions.StandardActions; -import org.jabref.gui.edit.OpenBrowserAction; -import org.jabref.gui.frame.FileHistoryMenu; -import org.jabref.gui.icon.IconTheme; -import org.jabref.gui.importer.NewDatabaseAction; -import org.jabref.gui.importer.actions.OpenDatabaseAction; -import org.jabref.gui.preferences.GuiPreferences; -import org.jabref.gui.undo.CountingUndoManager; -import org.jabref.gui.util.URLs; -import org.jabref.logic.ai.AiService; -import org.jabref.logic.importer.Importer; -import org.jabref.logic.importer.ParserResult; -import org.jabref.logic.importer.fileformat.BibtexParser; -import org.jabref.logic.l10n.Localization; -import org.jabref.logic.util.BuildInfo; -import org.jabref.logic.util.TaskExecutor; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntryTypesManager; -import org.jabref.model.util.FileUpdateMonitor; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class WelcomeTab extends Tab { - - private static final Logger LOGGER = LoggerFactory.getLogger(WelcomeTab.class); - - private final VBox recentLibrariesBox; - private final LibraryTabContainer tabContainer; - private final GuiPreferences preferences; - private final AiService aiService; - private final DialogService dialogService; - private final StateManager stateManager; - private final FileUpdateMonitor fileUpdateMonitor; - private final BibEntryTypesManager entryTypesManager; - private final CountingUndoManager undoManager; - private final ClipBoardManager clipBoardManager; - private final TaskExecutor taskExecutor; - private final FileHistoryMenu fileHistoryMenu; - private final BuildInfo buildInfo; - - public WelcomeTab(LibraryTabContainer tabContainer, - GuiPreferences preferences, - AiService aiService, - DialogService dialogService, - StateManager stateManager, - FileUpdateMonitor fileUpdateMonitor, - BibEntryTypesManager entryTypesManager, - CountingUndoManager undoManager, - ClipBoardManager clipBoardManager, - TaskExecutor taskExecutor, - FileHistoryMenu fileHistoryMenu, - BuildInfo buildInfo) { - - super(Localization.lang("Welcome")); - setClosable(true); - - this.tabContainer = tabContainer; - this.preferences = preferences; - this.aiService = aiService; - this.dialogService = dialogService; - this.stateManager = stateManager; - this.fileUpdateMonitor = fileUpdateMonitor; - this.entryTypesManager = entryTypesManager; - this.undoManager = undoManager; - this.clipBoardManager = clipBoardManager; - this.taskExecutor = taskExecutor; - this.fileHistoryMenu = fileHistoryMenu; - this.buildInfo = buildInfo; - - this.recentLibrariesBox = new VBox(10); - - VBox welcomeBox = createWelcomeBox(); - VBox startBox = createWelcomeStartBox(); - VBox recentBox = createWelcomeRecentBox(); - - VBox welcomePageContainer = new VBox(10); - welcomePageContainer.setAlignment(Pos.CENTER); - welcomePageContainer.getChildren().addAll(welcomeBox, startBox, recentBox); - - HBox welcomeMainContainer = new HBox(10); - welcomeMainContainer.setAlignment(Pos.CENTER); - welcomeMainContainer.setPadding(new Insets(10, 10, 10, 50)); - - welcomeMainContainer.getChildren().add(welcomePageContainer); - - BorderPane rootLayout = new BorderPane(); - rootLayout.setCenter(welcomeMainContainer); - rootLayout.setBottom(createFooter()); - - VBox container = new VBox(); - container.getChildren().add(rootLayout); - VBox.setVgrow(rootLayout, Priority.ALWAYS); - setContent(container); - } - - private VBox createWelcomeBox() { - Label welcomeLabel = new Label(Localization.lang("Welcome to JabRef")); - welcomeLabel.getStyleClass().add("welcome-label"); - - Label descriptionLabel = new Label(Localization.lang("Stay on top of your literature")); - descriptionLabel.getStyleClass().add("welcome-description-label"); - - return createVBoxContainer(welcomeLabel, descriptionLabel); - } - - private VBox createWelcomeStartBox() { - Label startLabel = new Label(Localization.lang("Start")); - startLabel.getStyleClass().add("welcome-header-label"); - - Hyperlink newLibraryLink = new Hyperlink(Localization.lang("New empty library")); - newLibraryLink.getStyleClass().add("welcome-hyperlink"); - newLibraryLink.setOnAction(e -> new NewDatabaseAction(tabContainer, preferences).execute()); - - Hyperlink openLibraryLink = new Hyperlink(Localization.lang("Open library")); - openLibraryLink.getStyleClass().add("welcome-hyperlink"); - openLibraryLink.setOnAction(e -> new OpenDatabaseAction(tabContainer, preferences, aiService, dialogService, - stateManager, fileUpdateMonitor, entryTypesManager, undoManager, clipBoardManager, - taskExecutor).execute()); - - Hyperlink openExampleLibraryLink = new Hyperlink(Localization.lang("New example library")); - openExampleLibraryLink.getStyleClass().add("welcome-hyperlink"); - openExampleLibraryLink.setOnAction(e -> { - try (InputStream in = WelcomeTab.class.getClassLoader().getResourceAsStream("Chocolate.bib")) { - if (in == null) { - LOGGER.warn("Example library file not found."); - return; - } - Reader reader = Importer.getReader(in); - BibtexParser bibtexParser = new BibtexParser(preferences.getImportFormatPreferences(), fileUpdateMonitor); - ParserResult result = bibtexParser.parse(reader); - BibDatabaseContext databaseContext = result.getDatabaseContext(); - LibraryTab libraryTab = LibraryTab.createLibraryTab(databaseContext, tabContainer, dialogService, aiService, - preferences, stateManager, fileUpdateMonitor, entryTypesManager, undoManager, clipBoardManager, taskExecutor); - tabContainer.addTab(libraryTab, true); - } catch (IOException ex) { - LOGGER.error("Failed to load example library", ex); - } - }); - - return createVBoxContainer(startLabel, newLibraryLink, openExampleLibraryLink, openLibraryLink); - } - - private VBox createWelcomeRecentBox() { - Label recentLabel = new Label(Localization.lang("Recent")); - recentLabel.getStyleClass().add("welcome-header-label"); - - recentLibrariesBox.setAlignment(Pos.TOP_LEFT); - updateWelcomeRecentLibraries(); - - fileHistoryMenu.getItems().addListener((ListChangeListener) _ -> updateWelcomeRecentLibraries()); - - return createVBoxContainer(recentLabel, recentLibrariesBox); - } - - private void updateWelcomeRecentLibraries() { - if (fileHistoryMenu.getItems().isEmpty()) { - displayNoRecentLibrariesMessage(); - return; - } - - recentLibrariesBox.getChildren().clear(); - fileHistoryMenu.disableProperty().unbind(); - fileHistoryMenu.setDisable(false); - - for (MenuItem item : fileHistoryMenu.getItems()) { - Hyperlink recentLibraryLink = new Hyperlink(item.getText()); - recentLibraryLink.getStyleClass().add("welcome-hyperlink"); - recentLibraryLink.setOnAction(item.getOnAction()); - recentLibrariesBox.getChildren().add(recentLibraryLink); - } - } - - private void displayNoRecentLibrariesMessage() { - recentLibrariesBox.getChildren().clear(); - Label noRecentLibrariesLabel = new Label(Localization.lang("No recent libraries")); - noRecentLibrariesLabel.getStyleClass().add("welcome-no-recent-label"); - recentLibrariesBox.getChildren().add(noRecentLibrariesLabel); - - fileHistoryMenu.disableProperty().unbind(); - fileHistoryMenu.setDisable(true); - } - - private VBox createVBoxContainer(Node... nodes) { - VBox box = new VBox(10); - box.setAlignment(Pos.TOP_LEFT); - box.getChildren().addAll(nodes); - return box; - } - - private VBox createFooter() { - // Heading for the footer area - Label communityLabel = createFooterLabel(Localization.lang("Community")); - - HBox iconLinksContainer = createIconLinksContainer(); - HBox textLinksContainer = createTextLinksContainer(); - HBox versionContainer = createVersionContainer(); - - VBox footerBox = new VBox(10); - footerBox.setAlignment(Pos.CENTER); - footerBox.getChildren().addAll(communityLabel, iconLinksContainer, textLinksContainer, versionContainer); - footerBox.setPadding(new Insets(10, 0, 10, 0)); - footerBox.getStyleClass().add("welcome-footer-container"); - - return footerBox; - } - - private Label createFooterLabel(String text) { - Label label = new Label(text); - label.getStyleClass().add("welcome-footer-label"); - return label; - } - - private HBox createIconLinksContainer() { - HBox container = new HBox(10); - container.setAlignment(Pos.CENTER); - - Hyperlink onlineHelpLink = createFooterLink(Localization.lang("Online help"), StandardActions.HELP, IconTheme.JabRefIcons.HELP); - Hyperlink forumLink = createFooterLink(Localization.lang("Community forum"), StandardActions.OPEN_FORUM, IconTheme.JabRefIcons.FORUM); - Hyperlink mastodonLink = createFooterLink(Localization.lang("Mastodon"), StandardActions.OPEN_MASTODON, IconTheme.JabRefIcons.MASTODON); - Hyperlink linkedInLink = createFooterLink(Localization.lang("LinkedIn"), StandardActions.OPEN_LINKEDIN, IconTheme.JabRefIcons.LINKEDIN); - Hyperlink donationLink = createFooterLink(Localization.lang("Donation"), StandardActions.DONATE, IconTheme.JabRefIcons.DONATE); - - container.getChildren().addAll(onlineHelpLink, forumLink, mastodonLink, linkedInLink, donationLink); - return container; - } - - private HBox createTextLinksContainer() { - HBox container = new HBox(10); - container.setAlignment(Pos.CENTER); - - Hyperlink devVersionLink = createFooterLink(Localization.lang("Download development version"), StandardActions.OPEN_DEV_VERSION_LINK, null); - Hyperlink changelogLink = createFooterLink(Localization.lang("CHANGELOG"), StandardActions.OPEN_CHANGELOG, null); - - container.getChildren().addAll(devVersionLink, changelogLink); - return container; - } - - private Hyperlink createFooterLink(String text, StandardActions action, IconTheme.JabRefIcons icon) { - Hyperlink link = new Hyperlink(text); - link.getStyleClass().add("welcome-footer-link"); - - String url = switch (action) { - case HELP -> URLs.HELP_URL; - case OPEN_FORUM -> URLs.FORUM_URL; - case OPEN_MASTODON -> URLs.MASTODON_URL; - case OPEN_LINKEDIN -> URLs.LINKEDIN_URL; - case DONATE -> URLs.DONATE_URL; - case OPEN_DEV_VERSION_LINK -> URLs.DEV_VERSION_LINK_URL; - case OPEN_CHANGELOG -> URLs.CHANGELOG_URL; - default -> null; - }; - - if (url != null) { - link.setOnAction(e -> new OpenBrowserAction(url, dialogService, preferences.getExternalApplicationsPreferences()).execute()); - } - - if (icon != null) { - link.setGraphic(icon.getGraphicNode()); - } - - return link; - } - - private HBox createVersionContainer() { - HBox container = new HBox(10); - container.setAlignment(Pos.CENTER); - - Label versionLabel = new Label(Localization.lang("Current JabRef version: %0", buildInfo.version)); - versionLabel.getStyleClass().add("welcome-footer-version"); - - container.getChildren().add(versionLabel); - return container; - } -} diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index f21144c1d0a..f9b0cdfa87f 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -181,12 +181,14 @@ public enum StandardActions implements Action { OPEN_FACEBOOK("Facebook", Localization.lang("Opens JabRef's Facebook page"), IconTheme.JabRefIcons.FACEBOOK), OPEN_LINKEDIN("LinkedIn", Localization.lang("Opens JabRef's LinkedIn page"), IconTheme.JabRefIcons.LINKEDIN), OPEN_MASTODON("Mastodon", Localization.lang("Opens JabRef's Mastodon page"), IconTheme.JabRefIcons.MASTODON), + OPEN_PRIVACY_POLICY("Privacy policy", Localization.lang("Opens JabRef's privacy policy"), IconTheme.JabRefIcons.BOOK), OPEN_BLOG(Localization.lang("Blog"), Localization.lang("Opens JabRef's blog"), IconTheme.JabRefIcons.BLOG), OPEN_DEV_VERSION_LINK(Localization.lang("Development version"), Localization.lang("Opens a link where the current development version can be downloaded")), OPEN_CHANGELOG(Localization.lang("View change log"), Localization.lang("See what has been changed in the JabRef versions")), OPEN_GITHUB("GitHub", Localization.lang("Opens JabRef's GitHub page"), IconTheme.JabRefIcons.GITHUB), WALKTHROUGH_MENU(Localization.lang("Walkthroughs"), IconTheme.JabRefIcons.BOOK), MAIN_FILE_DIRECTORY_WALKTHROUGH(Localization.lang("Configure main file directory"), IconTheme.JabRefIcons.LATEX_FILE_DIRECTORY), + CUSTOMIZE_ENTRY_TABLE_WALKTHROUGH(Localization.lang("Customize entry table"), IconTheme.JabRefIcons.TOGGLE_GROUPS), DONATE(Localization.lang("Donate to JabRef"), Localization.lang("Donate to JabRef"), IconTheme.JabRefIcons.DONATE), OPEN_FORUM(Localization.lang("Community forum"), Localization.lang("Community forum"), IconTheme.JabRefIcons.FORUM), ERROR_CONSOLE(Localization.lang("View event log"), Localization.lang("Display all error messages")), 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 3f70d2f42f0..ec49ab38026 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -32,7 +32,6 @@ import org.jabref.gui.LibraryTab; import org.jabref.gui.LibraryTabContainer; import org.jabref.gui.StateManager; -import org.jabref.gui.WelcomeTab; import org.jabref.gui.actions.ActionFactory; import org.jabref.gui.actions.ActionHelper; import org.jabref.gui.actions.SimpleCommand; @@ -53,6 +52,7 @@ import org.jabref.gui.undo.RedoAction; import org.jabref.gui.undo.UndoAction; import org.jabref.gui.util.BindingsHelper; +import org.jabref.gui.welcome.WelcomeTab; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; import org.jabref.logic.journals.JournalAbbreviationRepository; diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index a3134de9ae4..ed7d587a821 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -240,7 +240,7 @@ private void createMenu() { // Work around for mac only issue, where cmd+v on a dialogue triggers the paste action of menu item, resulting in addition of the pasted content in the MainTable. // If the mainscreen is not focused, the actions captured by menu are consumed. boolean isStageUnfocused = !Injector.instantiateModelOrService(Stage.class).focusedProperty().get(); - + if (OS.OS_X && isStageUnfocused) { event.consume(); } @@ -376,7 +376,8 @@ private void createMenu() { new SeparatorMenuItem(), factory.createSubMenu(StandardActions.WALKTHROUGH_MENU, - factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory")) + factory.createMenuItem(StandardActions.MAIN_FILE_DIRECTORY_WALKTHROUGH, new WalkthroughAction("mainFileDirectory")), + factory.createMenuItem(StandardActions.CUSTOMIZE_ENTRY_TABLE_WALKTHROUGH, new WalkthroughAction("customizeEntryTable")) ), new SeparatorMenuItem(), @@ -389,6 +390,7 @@ private void createMenu() { factory.createMenuItem(StandardActions.SEARCH_FOR_UPDATES, new SearchForUpdateAction(preferences, dialogService, taskExecutor)), factory.createSubMenu(StandardActions.WEB_MENU, factory.createMenuItem(StandardActions.OPEN_WEBPAGE, new OpenBrowserAction(URLs.WEBPAGE_URL, dialogService, preferences.getExternalApplicationsPreferences())), + factory.createMenuItem(StandardActions.OPEN_PRIVACY_POLICY, new OpenBrowserAction(URLs.PRIVACY_POLICY_URL, dialogService, preferences.getExternalApplicationsPreferences())), factory.createMenuItem(StandardActions.OPEN_BLOG, new OpenBrowserAction(URLs.BLOG_URL, dialogService, preferences.getExternalApplicationsPreferences())), factory.createMenuItem(StandardActions.OPEN_LINKEDIN, new OpenBrowserAction(URLs.LINKEDIN_URL, dialogService, preferences.getExternalApplicationsPreferences())), factory.createMenuItem(StandardActions.OPEN_FACEBOOK, new OpenBrowserAction(URLs.FACEBOOK_URL, dialogService, preferences.getExternalApplicationsPreferences())), diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java index 62ecfcf2888..de20d36874a 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/SingleWindowWalkthroughOverlay.java @@ -4,19 +4,14 @@ import javafx.beans.value.ChangeListener; import javafx.geometry.Bounds; -import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.GridPane; +import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.RowConstraints; import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; import javafx.stage.Window; -import org.jabref.gui.walkthrough.declarative.step.PanelPosition; import org.jabref.gui.walkthrough.declarative.step.PanelStep; import org.jabref.gui.walkthrough.declarative.step.TooltipPosition; import org.jabref.gui.walkthrough.declarative.step.TooltipStep; @@ -34,7 +29,7 @@ public class SingleWindowWalkthroughOverlay { private static final Logger LOGGER = LoggerFactory.getLogger(SingleWindowWalkthroughOverlay.class); private final Window window; - private final GridPane overlayPane; + private final BorderPane overlayPane; private final Pane originalRoot; private final StackPane stackPane; private final WalkthroughRenderer renderer; @@ -44,22 +39,23 @@ public SingleWindowWalkthroughOverlay(Window window) { this.window = window; this.renderer = new WalkthroughRenderer(); - overlayPane = new GridPane(); + overlayPane = new BorderPane(); overlayPane.getStyleClass().add("walkthrough-overlay"); overlayPane.setPickOnBounds(false); overlayPane.setMaxWidth(Double.MAX_VALUE); overlayPane.setMaxHeight(Double.MAX_VALUE); + overlayPane.prefWidthProperty().bind(window.widthProperty()); + overlayPane.prefHeightProperty().bind(window.heightProperty()); + overlayPane.minWidthProperty().bind(window.widthProperty()); + overlayPane.minHeightProperty().bind(window.heightProperty()); + Scene scene = window.getScene(); // This basically never happens, so only a development time check is needed assert scene != null; originalRoot = (Pane) scene.getRoot(); - stackPane = new StackPane(); - - stackPane.getChildren().add(originalRoot); - stackPane.getChildren().add(overlayPane); - + stackPane = new StackPane(originalRoot, overlayPane); scene.setRoot(stackPane); } @@ -84,7 +80,7 @@ public void displayStep(WalkthroughStep step, case PanelStep panelStep -> { Node content = renderer.render(panelStep, walkthrough, beforeNavigate); displayPanelStep(content, panelStep); - setupClipping(content); + setupClipping(content, panelStep); overlayPane.toFront(); } } @@ -101,7 +97,12 @@ public void displayStep(WalkthroughStep step, * Hide the overlay and clean up any resources. */ public void hide() { - overlayPane.getChildren().clear(); + overlayPane.setTop(null); + overlayPane.setBottom(null); + overlayPane.setLeft(null); + overlayPane.setRight(null); + overlayPane.setCenter(null); + overlayPane.setClip(null); overlayPane.setVisible(true); updater.cleanup(); @@ -149,87 +150,25 @@ private void displayTooltipStep(Node content, @Nullable Node targetNode, Tooltip } private void displayPanelStep(Node content, PanelStep step) { - overlayPane.getChildren().clear(); - overlayPane.getRowConstraints().clear(); - overlayPane.getColumnConstraints().clear(); - - configurePanelLayout(step.position()); - - overlayPane.getChildren().add(content); - GridPane.setHgrow(content, Priority.NEVER); - GridPane.setVgrow(content, Priority.NEVER); - switch (step.position()) { - case LEFT -> { - overlayPane.setAlignment(Pos.CENTER_LEFT); - GridPane.setVgrow(content, Priority.ALWAYS); - GridPane.setFillHeight(content, true); - } - case RIGHT -> { - overlayPane.setAlignment(Pos.CENTER_RIGHT); - GridPane.setVgrow(content, Priority.ALWAYS); - GridPane.setFillHeight(content, true); - } - case TOP -> { - overlayPane.setAlignment(Pos.TOP_CENTER); - GridPane.setHgrow(content, Priority.ALWAYS); - GridPane.setFillWidth(content, true); - } - case BOTTOM -> { - overlayPane.setAlignment(Pos.BOTTOM_CENTER); - GridPane.setHgrow(content, Priority.ALWAYS); - GridPane.setFillWidth(content, true); - } + case LEFT -> overlayPane.setLeft(content); + case RIGHT -> overlayPane.setRight(content); + case TOP -> overlayPane.setTop(content); + case BOTTOM -> overlayPane.setBottom(content); default -> { LOGGER.warn("Unsupported position for panel step: {}", step.position()); - overlayPane.setAlignment(Pos.CENTER); + overlayPane.setCenter(content); } } } - private void configurePanelLayout(PanelPosition position) { - overlayPane.getRowConstraints().add(switch (position) { - case LEFT, - RIGHT -> { - RowConstraints rowConstraints = new RowConstraints(); - rowConstraints.setVgrow(Priority.ALWAYS); - yield rowConstraints; - } - case TOP, - BOTTOM -> { - RowConstraints rowConstraints = new RowConstraints(); - rowConstraints.setVgrow(Priority.NEVER); - yield rowConstraints; - } - }); - overlayPane.getColumnConstraints().add(switch (position) { - case LEFT, - RIGHT -> { - ColumnConstraints columnConstraints = new ColumnConstraints(); - columnConstraints.setHgrow(Priority.NEVER); - yield columnConstraints; - } - case TOP, - BOTTOM -> { - ColumnConstraints columnConstraints = new ColumnConstraints(); - columnConstraints.setHgrow(Priority.ALWAYS); - yield columnConstraints; - } - }); - } - private Optional mapToArrowLocation(TooltipPosition position) { return Optional.ofNullable(switch (position) { - case TOP -> - PopOver.ArrowLocation.BOTTOM_CENTER; - case BOTTOM -> - PopOver.ArrowLocation.TOP_CENTER; - case LEFT -> - PopOver.ArrowLocation.RIGHT_CENTER; - case RIGHT -> - PopOver.ArrowLocation.LEFT_CENTER; - case AUTO -> - null; + case TOP -> PopOver.ArrowLocation.BOTTOM_CENTER; + case BOTTOM -> PopOver.ArrowLocation.TOP_CENTER; + case LEFT -> PopOver.ArrowLocation.RIGHT_CENTER; + case RIGHT -> PopOver.ArrowLocation.LEFT_CENTER; + case AUTO -> null; }); } @@ -238,15 +177,34 @@ private void hideOverlayPane() { updater.addCleanupTask(() -> overlayPane.setVisible(true)); } - private void setupClipping(Node node) { - ChangeListener listener = (_, _, bounds) -> { - if (bounds != null && bounds.getWidth() > 0 && bounds.getHeight() > 0) { - Rectangle clip = new Rectangle(bounds.getMinX(), bounds.getMinY(), - bounds.getWidth(), bounds.getHeight()); - overlayPane.setClip(clip); + private void setupClipping(Node node, PanelStep step) { + ChangeListener listener = (_, _, _) -> { + Bounds windowBounds = window.getScene().getRoot().getBoundsInLocal(); + Bounds nodeBounds = node.getBoundsInParent(); + + if (windowBounds.getWidth() <= 0 || windowBounds.getHeight() <= 0) { + return; } + + Rectangle clip = switch (step.position()) { + case LEFT -> + new Rectangle(0, 0, nodeBounds.getWidth(), windowBounds.getHeight()); + case RIGHT -> + new Rectangle(Math.max(0, windowBounds.getWidth() - nodeBounds.getWidth()), 0, + nodeBounds.getWidth(), windowBounds.getHeight()); + case TOP -> + new Rectangle(0, 0, windowBounds.getWidth(), nodeBounds.getHeight()); + case BOTTOM -> + new Rectangle(0, Math.max(0, windowBounds.getHeight() - nodeBounds.getHeight()), + windowBounds.getWidth(), nodeBounds.getHeight()); + }; + + overlayPane.setClip(clip); }; - updater.listen(node.boundsInLocalProperty(), listener); + + updater.listen(node.boundsInParentProperty(), listener); + updater.listen(overlayPane.boundsInLocalProperty(), listener); + listener.changed(null, null, node.getBoundsInParent()); } } 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 99646dbf9a5..c49916f6ac6 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -29,8 +29,11 @@ import com.airhacks.afterburner.injection.Injector; public class WalkthroughAction extends SimpleCommand { - private static final Map> WALKTHROUGH_REGISTRY = Map.of("mainFileDirectory", WalkthroughAction::createMainFileDirectoryWalkthrough); - private static final Map WALKTHROUGH_CACHE = new HashMap<>(); // must be mutable to allow caching of created walkthroughs + private static final Map> WALKTHROUGH_REGISTRY = Map.of( + "mainFileDirectory", WalkthroughAction::createMainFileDirectoryWalkthrough, + "customizeEntryTable", WalkthroughAction::createEntryTableWalkthrough + ); + private static final Map WALKTHROUGH_CACHE = new HashMap<>(); private final Walkthrough walkthrough; private final Stage mainStage; @@ -52,45 +55,49 @@ public void execute() { walkthrough.start(this.mainStage); } - private static Walkthrough createMainFileDirectoryWalkthrough(Stage mainStage) { - WindowResolver mainResolver = () -> Optional.of(mainStage); + private static Walkthrough createEntryTableWalkthrough(Stage stage) { + WalkthroughBuilder builder = new WalkthroughBuilder(stage); - WalkthroughStep step1 = TooltipStep - .builder(Localization.lang("Click on \"File\" menu")) - .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) - .navigation(NavigationPredicate.onClick()) - .position(TooltipPosition.BOTTOM) - .highlight(HighlightEffect.BACKDROP_HIGHLIGHT) - .build(); + WalkthroughStep step1 = builder.createFileMenuStep(); + WalkthroughStep step2 = builder.createPreferencesStep(); + WalkthroughStep step3 = builder.createTabSelectionStep( + "Entry table", + Localization.lang("This section allows you to customize the columns displayed in the entry table when viewing your bibliography.") + ); - WalkthroughStep step2 = TooltipStep - .builder(Localization.lang("Click on \"Preferences\"")) - .resolver(NodeResolver.menuItem("Preferences")) - .navigation(NavigationPredicate.onClick()) - .position(TooltipPosition.RIGHT) - .activeWindow(WindowResolver.clazz(ContextMenu.class)) - .highlight(new MultiWindowHighlight( - new WindowEffect(HighlightEffect.ANIMATED_PULSE), - new WindowEffect(mainResolver, HighlightEffect.FULL_SCREEN_DARKEN) - )) + WalkthroughStep step4 = PanelStep + .builder(Localization.lang("Customize your entry table columns")) + .content( + new TextBlock(Localization.lang("Here you can customize which columns appear in your entry table. You can add, remove, or reorder columns such as citation key, title, author, year, and journal. This helps you see the most relevant information for your research at a glance.")), + new InfoBlock(Localization.lang("The columns you configure here will be displayed whenever you open a library in JabRef. You can always return to this settings page to modify your column preferences.")) + ) + .continueButton(Localization.lang("Next")) + .backButton(Localization.lang("Back")) + .resolver(NodeResolver.fxId("columnsList")) + .navigation(NavigationPredicate.manual()) + .position(PanelPosition.RIGHT) + .highlight(builder.getPreferenceHighlight()) + .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) .build(); - MultiWindowHighlight preferenceHighlight = new MultiWindowHighlight( - new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), - new WindowEffect(mainResolver, HighlightEffect.FULL_SCREEN_DARKEN) + WalkthroughStep step5 = builder.createSaveStep( + Localization.lang("Your entry table columns are now configured. These settings will be applied to all your libraries in JabRef."), + Localization.lang("You can find more information about customizing JabRef at https://docs.jabref.org/"), + 150 + ); + + return new Walkthrough(step1, step2, step3, step4, step5); + } + + private static Walkthrough createMainFileDirectoryWalkthrough(Stage stage) { + WalkthroughBuilder builder = new WalkthroughBuilder(stage); + + WalkthroughStep step1 = builder.createFileMenuStep(); + WalkthroughStep step2 = builder.createPreferencesStep(); + WalkthroughStep step3 = builder.createTabSelectionStep( + "Linked files", + Localization.lang("This section manages how JabRef handles your PDF files and other documents.") ); - WalkthroughStep step3 = TooltipStep - .builder(Localization.lang("Select the \"Linked files\" tab")) - .content(new TextBlock(Localization.lang("This section manages how JabRef handles your PDF files and other documents."))) - .width(400) - .resolver(NodeResolver.predicate(node -> - node.getStyleClass().contains("list-cell") && - node.toString().contains(Localization.lang("Linked files")))) - .navigation(NavigationPredicate.onClick()) - .position(TooltipPosition.AUTO) - .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) - .highlight(preferenceHighlight) - .build(); WalkthroughStep step4 = TooltipStep .builder(Localization.lang("Enable \"Main file directory\" option")) @@ -99,24 +106,90 @@ private static Walkthrough createMainFileDirectoryWalkthrough(Stage mainStage) { .resolver(NodeResolver.fxId("useMainFileDirectory")) .navigation(NavigationPredicate.onClick()) .position(TooltipPosition.AUTO) - .highlight(preferenceHighlight) + .highlight(builder.getPreferenceHighlight()) .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) .build(); - WalkthroughStep step5 = PanelStep - .builder(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 https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks")) - ) - .height(180) - .resolver(NodeResolver.predicate(node -> node.getStyleClass().contains("button") && node.toString().contains(Localization.lang("Save")))) - .navigation(NavigationPredicate.onClick()) - .position(PanelPosition.TOP) - .highlight(preferenceHighlight) - .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) - .build(); + WalkthroughStep step5 = builder.createSaveStep( + Localization.lang("Congratulations. Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents."), + Localization.lang("Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks"), + 180 + ); return new Walkthrough(step1, step2, step3, step4, step5); } + + private static class WalkthroughBuilder { + private final WindowResolver mainResolver; + private final MultiWindowHighlight preferenceHighlight; + + public WalkthroughBuilder(Stage stage) { + this.mainResolver = () -> Optional.of(stage); + this.preferenceHighlight = new MultiWindowHighlight( + new WindowEffect(HighlightEffect.BACKDROP_HIGHLIGHT), + new WindowEffect(mainResolver, HighlightEffect.FULL_SCREEN_DARKEN) + ); + } + + public WalkthroughStep createFileMenuStep() { + return TooltipStep + .builder(Localization.lang("Click on \"File\" menu")) + .resolver(NodeResolver.selector(".menu-bar .menu-button:first-child")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.BOTTOM) + .highlight(HighlightEffect.BACKDROP_HIGHLIGHT) + .build(); + } + + public WalkthroughStep createPreferencesStep() { + return TooltipStep + .builder(Localization.lang("Click on \"Preferences\"")) + .resolver(NodeResolver.menuItem("Preferences")) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.RIGHT) + .activeWindow(WindowResolver.clazz(ContextMenu.class)) + .highlight(new MultiWindowHighlight( + new WindowEffect(HighlightEffect.ANIMATED_PULSE), + new WindowEffect(mainResolver, HighlightEffect.FULL_SCREEN_DARKEN) + )) + .build(); + } + + public WalkthroughStep createTabSelectionStep(String tabName, String description) { + return TooltipStep + .builder(Localization.lang("Select the \"" + tabName + "\" tab")) + .content(new TextBlock(description)) + .width(400) + .resolver(NodeResolver.predicate(node -> + node.getStyleClass().contains("list-cell") && + node.toString().contains(Localization.lang(tabName)))) + .navigation(NavigationPredicate.onClick()) + .position(TooltipPosition.AUTO) + .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) + .highlight(preferenceHighlight) + .build(); + } + + public WalkthroughStep createSaveStep(String completionMessage, String infoMessage, int height) { + return PanelStep + .builder(Localization.lang("Click \"Save\" to save changes")) + .content( + new TextBlock(completionMessage), + new InfoBlock(infoMessage) + ) + .height(height) + .resolver(NodeResolver.predicate(node -> + node.getStyleClass().contains("button") && + node.toString().contains(Localization.lang("Save")))) + .navigation(NavigationPredicate.onClick()) + .position(PanelPosition.TOP) + .highlight(preferenceHighlight) + .activeWindow(WindowResolver.title(PreferencesDialogView.DIALOG_TITLE)) + .build(); + } + + public MultiWindowHighlight getPreferenceHighlight() { + return preferenceHighlight; + } + } } 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 bf9a7939ff6..b18da25b17e 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughRenderer.java @@ -85,11 +85,10 @@ public Node render(PanelStep step, Walkthrough walkthrough, Runnable beforeNavig private void configurePanelSize(VBox panel, PanelStep step) { boolean isVertical = step.position() == PanelPosition.LEFT || step.position() == PanelPosition.RIGHT; + VBox.setVgrow(panel, Priority.ALWAYS); if (isVertical) { panel.getStyleClass().add("walkthrough-side-panel-vertical"); - VBox.setVgrow(panel, Priority.ALWAYS); - panel.setMaxHeight(Double.MAX_VALUE); step.width().ifPresent(width -> { panel.setPrefWidth(width); panel.setMaxWidth(width); @@ -97,8 +96,6 @@ private void configurePanelSize(VBox panel, PanelStep step) { }); } else if (step.position() == PanelPosition.TOP || step.position() == PanelPosition.BOTTOM) { panel.getStyleClass().add("walkthrough-side-panel-horizontal"); - HBox.setHgrow(panel, Priority.ALWAYS); - panel.setMaxWidth(Double.MAX_VALUE); step.height().ifPresent(height -> { panel.setPrefHeight(height); panel.setMaxHeight(height); @@ -114,6 +111,7 @@ private Node render(ArbitraryJFXBlock block, Walkthrough walkthrough, Runnable b private Node render(TextBlock textBlock) { Label textLabel = new Label(Localization.lang(textBlock.text())); textLabel.getStyleClass().add("walkthrough-text-content"); + textLabel.setWrapText(true); return textLabel; } @@ -122,6 +120,7 @@ private Node render(InfoBlock infoBlock) { infoContainer.getStyleClass().add("walkthrough-info-container"); JabRefIconView icon = new JabRefIconView(IconTheme.JabRefIcons.INTEGRITY_INFO); Label infoLabel = new Label(Localization.lang(infoBlock.text())); + infoLabel.setWrapText(true); HBox.setHgrow(infoLabel, Priority.ALWAYS); infoContainer.getChildren().addAll(icon, infoLabel); return infoContainer; diff --git a/jabgui/src/main/java/org/jabref/gui/welcome/ThemeWireFrameComponent.java b/jabgui/src/main/java/org/jabref/gui/welcome/ThemeWireFrameComponent.java new file mode 100644 index 00000000000..fb72b28c0e8 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/welcome/ThemeWireFrameComponent.java @@ -0,0 +1,55 @@ +package org.jabref.gui.welcome; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.layout.VBox; + +import com.airhacks.afterburner.views.ViewLoader; + +public class ThemeWireFrameComponent extends VBox { + + private final StringProperty themeType = new SimpleStringProperty(); + + public ThemeWireFrameComponent() { + ViewLoader.view(this) + .root(this) + .load(); + + themeType.addListener((_, _, newValue) -> { + if (newValue != null) { + updateTheme(); + } + }); + } + + public ThemeWireFrameComponent(String themeType) { + this(); + setThemeType(themeType); + } + + public void setThemeType(String themeType) { + this.themeType.set(themeType); + } + + @FXML + private void initialize() { + if (themeType.get() != null) { + updateTheme(); + } + } + + private void updateTheme() { + String theme = themeType.get(); + if (theme == null) { + return; + } + + getStyleClass().removeIf(styleClass -> + styleClass.startsWith("wireframe-light") || + styleClass.startsWith("wireframe-dark") || + styleClass.startsWith("wireframe-custom")); + + getStyleClass().add("wireframe-" + theme); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java b/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java new file mode 100644 index 00000000000..fe0518b029b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java @@ -0,0 +1,1196 @@ +package org.jabref.gui.welcome; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javafx.application.Platform; +import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.MenuItem; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TextField; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import org.jabref.gui.ClipBoardManager; +import org.jabref.gui.DialogService; +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; +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.importer.NewDatabaseAction; +import org.jabref.gui.importer.actions.OpenDatabaseAction; +import org.jabref.gui.maintable.ColumnPreferences; +import org.jabref.gui.maintable.MainTableColumnModel; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.push.GuiPushToApplication; +import org.jabref.gui.push.GuiPushToApplications; +import org.jabref.gui.slr.StudyCatalogItem; +import org.jabref.gui.theme.Theme; +import org.jabref.gui.theme.ThemeTypes; +import org.jabref.gui.undo.CountingUndoManager; +import org.jabref.gui.util.DirectoryDialogConfiguration; +import org.jabref.gui.util.FileDialogConfiguration; +import org.jabref.gui.util.URLs; +import org.jabref.gui.walkthrough.WalkthroughAction; +import org.jabref.logic.FilePreferences; +import org.jabref.logic.ai.AiService; +import org.jabref.logic.importer.Importer; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.SearchBasedFetcher; +import org.jabref.logic.importer.WebFetchers; +import org.jabref.logic.importer.fetcher.CompositeSearchBasedFetcher; +import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.os.OS; +import org.jabref.logic.push.PushToApplication; +import org.jabref.logic.push.PushToApplicationPreferences; +import org.jabref.logic.util.BuildInfo; +import org.jabref.logic.util.StandardFileType; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.field.InternalField; +import org.jabref.model.util.FileUpdateMonitor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WelcomeTab extends Tab { + + private static final Logger LOGGER = LoggerFactory.getLogger(WelcomeTab.class); + + private final VBox recentLibrariesBox; + private final LibraryTabContainer tabContainer; + private final GuiPreferences preferences; + private final AiService aiService; + private final DialogService dialogService; + private final StateManager stateManager; + private final FileUpdateMonitor fileUpdateMonitor; + private final BibEntryTypesManager entryTypesManager; + private final CountingUndoManager undoManager; + private final ClipBoardManager clipBoardManager; + private final TaskExecutor taskExecutor; + private final FileHistoryMenu fileHistoryMenu; + private final BuildInfo buildInfo; + + public WelcomeTab(LibraryTabContainer tabContainer, + GuiPreferences preferences, + AiService aiService, + DialogService dialogService, + StateManager stateManager, + FileUpdateMonitor fileUpdateMonitor, + BibEntryTypesManager entryTypesManager, + CountingUndoManager undoManager, + ClipBoardManager clipBoardManager, + TaskExecutor taskExecutor, + FileHistoryMenu fileHistoryMenu, + BuildInfo buildInfo) { + super(Localization.lang("Welcome")); + setClosable(true); + + this.tabContainer = tabContainer; + this.preferences = preferences; + this.aiService = aiService; + this.dialogService = dialogService; + this.stateManager = stateManager; + this.fileUpdateMonitor = fileUpdateMonitor; + this.entryTypesManager = entryTypesManager; + this.undoManager = undoManager; + this.clipBoardManager = clipBoardManager; + this.taskExecutor = taskExecutor; + this.fileHistoryMenu = fileHistoryMenu; + this.buildInfo = buildInfo; + + this.recentLibrariesBox = new VBox(); + recentLibrariesBox.getStyleClass().add("welcome-recent-libraries"); + + VBox mainContainer = new VBox(createColumnsContainer()); + mainContainer.getStyleClass().add("welcome-main-container"); + + VBox container = new VBox(); + container.getChildren().add(mainContainer); + VBox.setVgrow(mainContainer, Priority.ALWAYS); + container.setAlignment(Pos.CENTER); + setContent(container); + } + + private Optional createQuickSettingsDialog(String titleKey, String headerKey, + BooleanSupplier validationSupplier, List> deps, Node... children) { + Dialog dialog = new Dialog<>(); + dialog.setTitle(Localization.lang(titleKey)); + dialog.setHeaderText(Localization.lang(headerKey)); + + VBox content = new VBox(); + content.getStyleClass().add("quick-settings-dialog-container"); + content.getChildren().addAll(children); + + dialog.getDialogPane().setContent(content); + dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + Button okButton = (Button) dialog.getDialogPane().lookupButton(ButtonType.OK); + okButton.setDisable(!validationSupplier.getAsBoolean()); + deps.forEach(obs -> obs.addListener((_, _, _) -> okButton.setDisable(!validationSupplier.getAsBoolean()))); + + return dialogService.showCustomDialogAndWait(dialog); + } + + private Optional createQuickSettingsDialog(String titleKey, String headerKey, Node... children) { + return createQuickSettingsDialog(titleKey, headerKey, () -> true, List.of(), children); + } + + private boolean validateDialogSubmission(ListView applicationsList, + PathSelectionField pathSelector) { + PushToApplication selectedApp = applicationsList.getSelectionModel().getSelectedItem(); + if (selectedApp == null) { + return false; + } + + String pathText = pathSelector.getText().trim(); + Path path = Path.of(pathText); + return !pathText.isEmpty() && path.isAbsolute() && path.toFile().exists(); + } + + private Button createQuickSettingsButton(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(_ -> action.run()); + return button; + } + + private Button createWalkthroughButton(String text, IconTheme.JabRefIcons icon, String walkthroughId) { + return createQuickSettingsButton( + Localization.lang("Walkthrough") + ": " + Localization.lang(text), + icon, + () -> new WalkthroughAction(walkthroughId).execute() + ); + } + + private Button createHelpButton(String url) { + Button helpButton = new Button(); + helpButton.setGraphic(IconTheme.JabRefIcons.HELP.getGraphicNode()); + helpButton.getStyleClass().add("icon-button"); + helpButton.setOnAction(_ -> new OpenBrowserAction(url, dialogService, preferences.getExternalApplicationsPreferences()).execute()); + return helpButton; + } + + private static class PathSelectionField extends HBox { + private final TextField pathField; + private final Button browseButton; + + public PathSelectionField(String promptText) { + pathField = new TextField(); + pathField.setPromptText(promptText); + HBox.setHgrow(pathField, Priority.ALWAYS); + + browseButton = new Button(); + browseButton.setGraphic(IconTheme.JabRefIcons.OPEN.getGraphicNode()); + browseButton.getStyleClass().addAll("icon-button"); + + setSpacing(4); + getChildren().addAll(pathField, browseButton); + } + + public void setOnBrowseAction(Runnable action) { + browseButton.setOnAction(_ -> action.run()); + } + + public String getText() { + return pathField.getText(); + } + + public void setText(String text) { + pathField.setText(text); + } + + public TextField getTextField() { + return pathField; + } + } + + private VBox createTopTitles() { + Label welcomeLabel = new Label(Localization.lang("Welcome to JabRef")); + 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(); + topTitles.getStyleClass().add("welcome-top-titles"); + topTitles.getChildren().addAll(welcomeLabel, descriptionLabel); + + return topTitles; + } + + private HBox createColumnsContainer() { + VBox leftColumn = createLeftColumn(); + 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; + } + + private VBox createLeftColumn() { + VBox leftColumn = new VBox( + createTopTitles(), + createWelcomeStartBox(), + createWelcomeRecentBox() + ); + leftColumn.getStyleClass().add("welcome-content-column"); + return leftColumn; + } + + private VBox createRightColumn() { + VBox rightColumn = new VBox(createQuickSettingsBox(), createCommunityBox()); + rightColumn.getStyleClass().add("welcome-content-column"); + return rightColumn; + } + + private VBox createQuickSettingsBox() { + Label header = new Label(Localization.lang("Quick Settings")); + header.getStyleClass().add("welcome-header-label"); + + VBox actions = new VBox(); + actions.getStyleClass().add("quick-settings-content"); + + Button mainFileDirButton = createQuickSettingsButton( + Localization.lang("Set main file directory"), + IconTheme.JabRefIcons.FOLDER, + this::showMainFileDirectoryDialog + ); + + Button walkthroughMainFileDirButton = createWalkthroughButton( + "Set main file directory", + IconTheme.JabRefIcons.FOLDER, + "mainFileDirectory" + ); + + Button themeButton = createQuickSettingsButton( + Localization.lang("Change visual theme"), + IconTheme.JabRefIcons.PREFERENCES, + this::showThemeDialog + ); + + Button largeLibraryButton = createQuickSettingsButton( + Localization.lang("Optimize for large libraries"), + IconTheme.JabRefIcons.SELECTORS, + this::showLargeLibraryOptimizationDialog + ); + + Button pushApplicationButton = createQuickSettingsButton( + Localization.lang("Configure push to applications"), + IconTheme.JabRefIcons.APPLICATION_GENERIC, + this::showPushApplicationConfigurationDialog + ); + + Button onlineServicesButton = createQuickSettingsButton( + Localization.lang("Configure web search services"), + IconTheme.JabRefIcons.WWW, + this::showOnlineServicesConfigurationDialog + ); + + Button entryTableButton = createQuickSettingsButton( + Localization.lang("Customize entry table"), + IconTheme.JabRefIcons.TOGGLE_GROUPS, + this::showEntryTableConfigurationDialog + ); + + Button walkthroughEntryTableButton = createWalkthroughButton( + "Customize entry table", + IconTheme.JabRefIcons.TOGGLE_GROUPS, + "customizeEntryTable" + ); + + actions.getChildren().addAll( + mainFileDirButton, + walkthroughMainFileDirButton, + themeButton, + largeLibraryButton, + pushApplicationButton, + onlineServicesButton, + entryTableButton, + walkthroughEntryTableButton + ); + + ScrollPane scrollPane = new ScrollPane(actions); + scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setMaxHeight(172); // Scroll pane show exactly 4 times, item-height * 4 + gap * 3 = 35 * 4 + 8 * 3 + + return createVBoxContainer(header, scrollPane); + } + + 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 showMainFileDirectoryDialog() { + FilePreferences filePreferences = preferences.getFilePreferences(); + + PathSelectionField pathSelector = new PathSelectionField(Localization.lang("Main file directory path")); + pathSelector.setText(filePreferences.getMainFileDirectory() + .map(Path::toString) + .orElse("")); + + pathSelector.setOnBrowseAction(() -> { + DirectoryDialogConfiguration dirConfig = new DirectoryDialogConfiguration.Builder() + .withInitialDirectory(filePreferences.getWorkingDirectory()) + .build(); + dialogService.showDirectorySelectionDialog(dirConfig) + .ifPresent(selectedDir -> pathSelector.setText(selectedDir.toString())); + }); + + Optional result = createQuickSettingsDialog( + "Set main file directory", + "Choose the default directory for storing attached files", + pathSelector + ); + + if (result.isPresent() && result.get() == ButtonType.OK) { + filePreferences.setMainFileDirectory(pathSelector.getText()); + filePreferences.setStoreFilesRelativeToBibFile(false); + } + } + + private void showThemeDialog() { + ToggleGroup themeGroup = new ToggleGroup(); + HBox radioContainer = new HBox(); + + WorkspacePreferences workspacePreferences = preferences.getWorkspacePreferences(); + Theme currentTheme = workspacePreferences.getTheme(); + + RadioButton lightRadio = new RadioButton(ThemeTypes.LIGHT.getDisplayName()); + lightRadio.setToggleGroup(themeGroup); + lightRadio.setUserData(ThemeTypes.LIGHT); + VBox lightBox = createThemeOption(lightRadio, new ThemeWireFrameComponent("light")); + radioContainer.getChildren().add(lightBox); + + RadioButton darkRadio = new RadioButton(ThemeTypes.DARK.getDisplayName()); + darkRadio.setToggleGroup(themeGroup); + darkRadio.setUserData(ThemeTypes.DARK); + VBox darkBox = createThemeOption(darkRadio, new ThemeWireFrameComponent("dark")); + radioContainer.getChildren().add(darkBox); + + RadioButton customRadio = new RadioButton(ThemeTypes.CUSTOM.getDisplayName()); + customRadio.setToggleGroup(themeGroup); + customRadio.setUserData(ThemeTypes.CUSTOM); + VBox customBox = createThemeOption(customRadio, new ThemeWireFrameComponent("custom")); + radioContainer.getChildren().add(customBox); + + switch (currentTheme.getType()) { + case DEFAULT -> + lightRadio.setSelected(true); + case EMBEDDED -> + darkRadio.setSelected(true); + case CUSTOM -> + customRadio.setSelected(true); + } + + PathSelectionField customThemePath = new PathSelectionField(Localization.lang("Custom theme file path")); + customThemePath.setText(currentTheme.getType() == Theme.Type.CUSTOM ? currentTheme.getName() : ""); + + customThemePath.setOnBrowseAction(() -> { + String fileDir = customThemePath.getText().isEmpty() ? + preferences.getInternalPreferences().getLastPreferencesExportPath().toString() : + customThemePath.getText(); + + FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() + .addExtensionFilter(StandardFileType.CSS) + .withDefaultExtension(StandardFileType.CSS) + .withInitialDirectory(fileDir) + .build(); + + dialogService.showFileOpenDialog(fileDialogConfiguration).ifPresent(file -> + customThemePath.setText(file.toAbsolutePath().toString())); + }); + customThemePath.setAlignment(Pos.CENTER_LEFT); + + themeGroup.selectedToggleProperty().addListener((_, _, newValue) -> { + boolean isCustom = newValue != null && newValue.getUserData() == ThemeTypes.CUSTOM; + customThemePath.setManaged(isCustom); + customThemePath.setVisible(isCustom); + customThemePath.getScene().getWindow().sizeToScene(); + }); + + if (currentTheme.getType() != Theme.Type.CUSTOM) { + customThemePath.setVisible(false); + customThemePath.setManaged(false); + } + + Optional result = createQuickSettingsDialog( + "Change visual theme", + "Select your preferred theme for the application", + () -> Optional + .ofNullable(themeGroup.getSelectedToggle()) + .map(toggle -> toggle.getUserData() != ThemeTypes.CUSTOM || Path.of(customThemePath.getText()).toFile().exists()) + .orElse(false), + List.of(customThemePath.pathField.textProperty(), themeGroup.selectedToggleProperty()), + radioContainer, + customThemePath + ); + + if (result.isEmpty() || result.get() != ButtonType.OK) { + return; + } + + Toggle selectedToggle = themeGroup.getSelectedToggle(); + if (selectedToggle != null) { + ThemeTypes selectedTheme = (ThemeTypes) selectedToggle.getUserData(); + Theme newTheme = switch (selectedTheme) { + case LIGHT -> + Theme.light(); + case DARK -> + Theme.dark(); + case CUSTOM -> + Theme.custom(customThemePath.getText().trim()); + }; + workspacePreferences.setTheme(newTheme); + } + } + + private void showLargeLibraryOptimizationDialog() { + Label performanceOptimizationLabel = new Label(Localization.lang("Select performance optimizations to apply")); + performanceOptimizationLabel.setWrapText(true); + performanceOptimizationLabel.setMaxWidth(400); + + HBox performanceOptimizationHeader = new HBox(performanceOptimizationLabel, createHelpButton("https://docs.jabref.org/faq#q-i-have-a-huge-library.-what-can-i-do-to-mitigate-performance-issues")); + + CheckBox disableFulltextIndexing = new CheckBox(Localization.lang("Disable fulltext indexing")); + disableFulltextIndexing.setSelected(true); + + CheckBox disableCreationDate = new CheckBox(Localization.lang("Disable creation date timestamps")); + disableCreationDate.setSelected(true); + + CheckBox disableModificationDate = new CheckBox(Localization.lang("Disable modification date timestamps")); + disableModificationDate.setSelected(true); + + CheckBox disableAutosave = new CheckBox(Localization.lang("Disable automatic saving")); + disableAutosave.setSelected(true); + + CheckBox disableGroupCount = new CheckBox(Localization.lang("Disable group entry counts")); + disableGroupCount.setSelected(true); + + Optional result = createQuickSettingsDialog( + "Optimize for large libraries", + "Improve performance when working with libraries containing many entries", + performanceOptimizationHeader, + disableFulltextIndexing, + disableCreationDate, + disableModificationDate, + disableAutosave, + disableGroupCount + ); + + if (result.isEmpty() || result.get() != ButtonType.OK) { + return; + } + if (disableFulltextIndexing.isSelected()) { + preferences.getFilePreferences().setFulltextIndexLinkedFiles(false); + } + if (disableCreationDate.isSelected()) { + preferences.getTimestampPreferences().setAddCreationDate(false); + } + if (disableModificationDate.isSelected()) { + preferences.getTimestampPreferences().setAddModificationDate(false); + } + if (disableAutosave.isSelected()) { + preferences.getLibraryPreferences().setAutoSave(false); + } + if (disableGroupCount.isSelected()) { + preferences.getGroupsPreferences().setDisplayGroupCount(false); + } + } + + private void showPushApplicationConfigurationDialog() { + Label explanationLabel = new Label(Localization.lang("Detected applications are highlighted. Application that are not detected can be set manually by specifying the path to the executable.")); + explanationLabel.setWrapText(true); + explanationLabel.setMaxWidth(400); + + ListView applicationsList = new ListView<>(); + applicationsList.getStyleClass().add("applications-list"); + + PushToApplicationPreferences pushToApplicationPreferences = preferences.getPushToApplicationPreferences(); + List allApplications = GuiPushToApplications.getAllGUIApplications(dialogService, pushToApplicationPreferences); + + applicationsList.getItems().addAll(allApplications); + applicationsList.setCellFactory(_ -> new PushApplicationListCell(Collections.emptySet())); + + if (!pushToApplicationPreferences.getActiveApplicationName().isEmpty()) { + allApplications.stream() + .filter(app -> app.getDisplayName().equals(pushToApplicationPreferences.getActiveApplicationName())) + .findFirst() + .ifPresent(applicationsList.getSelectionModel()::select); + } + + PathSelectionField pathSelector = new PathSelectionField(Localization.lang("Path to application executable")); + pathSelector.setOnBrowseAction(() -> { + FileDialogConfiguration fileConfig = new FileDialogConfiguration.Builder() + .withInitialDirectory(preferences.getFilePreferences().getWorkingDirectory()) + .build(); + dialogService.showFileOpenDialog(fileConfig) + .ifPresent(selectedFile -> pathSelector.setText(selectedFile.toString())); + }); + + Map detectedApplicationPaths = new ConcurrentHashMap<>(); + + TextField pathField = pathSelector.getTextField(); + applicationsList.getSelectionModel().selectedItemProperty().addListener((_, _, selectedApp) -> { + if (selectedApp == null) { + pathSelector.setText(""); + pathField.setPromptText(Localization.lang("Path to application executable")); + return; + } + + String existingPath = pushToApplicationPreferences.getCommandPaths().get(selectedApp.getDisplayName()); + pathSelector.setText(isValidAbsolutePath(existingPath) ? + existingPath : + Objects.requireNonNullElse(detectedApplicationPaths.get(selectedApp), "")); + }); + + pathField.textProperty().addListener((_, _, newText) -> { + if (newText == null || newText.trim().isEmpty()) { + pathField.getStyleClass().removeAll("invalid-path"); + return; + } + + if (isValidAbsolutePath(newText)) { + pathField.getStyleClass().removeAll("invalid-path"); + } else { + if (!pathField.getStyleClass().contains("invalid-path")) { + pathField.getStyleClass().add("invalid-path"); + } + } + }); + + CompletableFuture> detectionFuture = + detectApplicationPathsAsync(allApplications); + + detectionFuture.thenAccept(detectedPaths -> Platform.runLater(() -> { + detectedApplicationPaths.putAll(detectedPaths); + applicationsList.setCellFactory(_ -> new PushApplicationListCell(detectedPaths.keySet())); + + List sortedApplications = new ArrayList<>(detectedPaths.keySet()); + allApplications.stream() + .filter(app -> !detectedPaths.containsKey(app)) + .forEach(sortedApplications::add); + + applicationsList.getItems().clear(); + applicationsList.getItems().addAll(sortedApplications); + + if (!pushToApplicationPreferences.getActiveApplicationName().isEmpty()) { + sortedApplications.stream() + .filter(app -> app.getDisplayName().equals(pushToApplicationPreferences.getActiveApplicationName())) + .findFirst() + .ifPresent(applicationsList.getSelectionModel()::select); + } + + LOGGER.info("Application detection completed. Found {} applications", detectedPaths.size()); + })).exceptionally(throwable -> { + LOGGER.warn("Application detection failed", throwable); + return null; + }); + + Optional result = createQuickSettingsDialog( + "Configure push to applications", + "Select your text editor or LaTeX application for pushing citations", + () -> validateDialogSubmission(applicationsList, pathSelector), + List.of(pathSelector.pathField.textProperty()), + explanationLabel, + applicationsList, + pathSelector + ); + + if (result.isEmpty() || result.get() == ButtonType.CANCEL) { + detectionFuture.cancel(true); + return; + } + + PushToApplication selectedApp = applicationsList.getSelectionModel().getSelectedItem(); + pushToApplicationPreferences.setActiveApplicationName(selectedApp.getDisplayName()); + + Map commandPaths = new HashMap<>(pushToApplicationPreferences.getCommandPaths()); + commandPaths.put(selectedApp.getDisplayName(), pathSelector.getText().trim()); + pushToApplicationPreferences.setCommandPaths(commandPaths); + } + + private CompletableFuture> detectApplicationPathsAsync(List allApplications) { + return CompletableFuture.supplyAsync(() -> + allApplications + .parallelStream() + .map(application -> { + Optional path = findApplicationPath(application); + if (path.isPresent()) { + LOGGER.debug("Detected application {}: {}", application.getDisplayName(), path.get()); + return Optional.of(Map.entry(application, path.get())); + } + return Optional.>empty(); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ); + } + + private Optional findApplicationPath(GuiPushToApplication application) { + String appName = application.getDisplayName(); + String[] possibleNames = getPossibleExecutableNames(appName); + + for (String executable : possibleNames) { + Optional pathInPath = findExecutableInPath(executable); + if (pathInPath.isPresent()) { + return pathInPath; + } + } + + return findExecutableInCommonPaths(possibleNames); + } + + private Optional findExecutableInCommonPaths(String[] executableNames) { + List commonPaths = getCommonPaths(); + + for (Path basePath : commonPaths) { + try { + if (basePath.toFile().exists()) { + Optional result = findExecutableInDirectory(basePath, executableNames); + if (result.isPresent()) { + return result; + } + } + } catch (Exception e) { + LOGGER.trace("Error checking path {}: {}", basePath, e.getMessage()); + } + } + + return Optional.empty(); + } + + private List getCommonPaths() { + List paths = new ArrayList<>(); + + if (OS.WINDOWS) { + paths.addAll(List.of( + Path.of("C:/Program Files"), + Path.of("C:/Program Files (x86)"), + Path.of(System.getProperty("user.home"), "AppData/Local"), + Path.of(System.getProperty("user.home"), "AppData/Roaming") + )); + } else if (OS.OS_X) { + paths.addAll(List.of( + Path.of("/Applications"), + Path.of("/usr/local/bin"), + Path.of("/opt/homebrew/bin"), + Path.of(System.getProperty("user.home"), "Applications"), + Path.of("/usr/local/texlive"), + Path.of("/Library/TeX/texbin") + )); + } else if (OS.LINUX) { + paths.addAll(List.of( + Path.of("/usr/bin"), + Path.of("/usr/local/bin"), + Path.of("/opt"), + Path.of("/snap/bin"), + Path.of(System.getProperty("user.home"), ".local/bin"), + Path.of("/usr/local/texlive") + )); + } + + return paths; + } + + private Optional findExecutableInDirectory(Path directory, String[] executableNames) { + try (Stream pathStream = Files.walk(directory, 3)) { + return pathStream + .filter(path -> isValidExecutable(path, executableNames)) + .map(Path::toAbsolutePath) + .map(Path::toString) + .findFirst(); + } catch (IOException e) { + LOGGER.trace("Error searching directory {}: {}", directory, e.getMessage()); + return Optional.empty(); + } + } + + private boolean isValidExecutable(Path path, String[] executableNames) { + if (!path.toFile().exists()) { + return false; + } + + String fileName = path.getFileName().toString().toLowerCase(); + + for (String execName : executableNames) { + if (isExecutableNameMatch(fileName, execName)) { + return true; + } + } + + return false; + } + + private boolean isExecutableNameMatch(String fileName, String executableName) { + if (OS.WINDOWS) { + return fileName.equals(executableName + ".exe") || + fileName.equals(executableName + ".bat") || + fileName.equals(executableName + ".cmd"); + } else { // OSX or Linux + return fileName.equals(executableName) || + fileName.equals(executableName + ".sh") || + fileName.equals(executableName + ".app"); + } + } + + private boolean isValidAbsolutePath(String pathStr) { + if (pathStr == null || pathStr.trim().isEmpty()) { + return false; + } + Path path = Path.of(pathStr); + return path.isAbsolute() && path.toFile().exists(); + } + + private String[] getPossibleExecutableNames(String appName) { + return switch (appName) { + case "Emacs" -> + new String[] {"emacs", "emacsclient"}; + case "LyX/Kile" -> + new String[] {"lyx", "kile"}; + case "Texmaker" -> + new String[] {"texmaker"}; + case "TeXstudio" -> + new String[] {"texstudio"}; + case "TeXworks" -> + new String[] {"texworks"}; + case "Vim" -> + new String[] {"vim", "nvim", "gvim"}; + case "WinEdt" -> + new String[] {"winedt"}; + case "Sublime Text" -> + new String[] {"subl", "sublime_text"}; + case "TeXShop" -> + new String[] {"texshop"}; + case "VScode" -> + new String[] {"code", "code-insiders"}; + default -> + new String[] {appName.replace(" ", "").toLowerCase()}; + }; + } + + private Optional findExecutableInPath(String executable) { + Optional result = trySystemCommand("which", executable); + System.out.println("which " + executable + " result: " + result.orElse("Not found")); + if (result.isPresent()) { + return result; + } + + return trySystemCommand("where", executable) + .map(output -> { + String[] lines = output.split("\n"); + return lines.length > 0 && !lines[0].trim().isEmpty() ? lines[0].trim() : null; + }) + .filter(Objects::nonNull); + } + + private Optional trySystemCommand(String command, String argument) { + try { + ProcessBuilder pb = new ProcessBuilder(command, argument); + Process process = pb.start(); + if (process.waitFor() == 0) { + String result = new String(process.getInputStream().readAllBytes()).trim(); + if (!result.isEmpty()) { + return Optional.of(result); + } + } + } catch (IOException | InterruptedException e) { + LOGGER.trace("Failed to execute '{}' command: {}", command, e.getMessage()); + } + return Optional.empty(); + } + + private static class PushApplicationListCell extends ListCell { + private final Set detectedApplications; + + public PushApplicationListCell(Set detectedApplications) { + this.detectedApplications = detectedApplications; + this.getStyleClass().add("application-item"); + } + + @Override + protected void updateItem(GuiPushToApplication application, boolean empty) { + super.updateItem(application, empty); + + if (empty || application == null) { + setText(null); + setGraphic(null); + getStyleClass().removeAll("detected-application"); + return; + } + + setText(application.getDisplayName()); + setGraphic(application.getApplicationIcon().getGraphicNode()); + + if (detectedApplications.contains(application)) { + if (!getStyleClass().contains("detected-application")) { + getStyleClass().add("detected-application"); + } + } else { + getStyleClass().removeAll("detected-application"); + } + } + } + + private void showOnlineServicesConfigurationDialog() { + CheckBox versionCheckBox = new CheckBox(Localization.lang("Check for updates at startup")); + versionCheckBox.setSelected(preferences.getInternalPreferences().isVersionCheckEnabled()); + + CheckBox webSearchBox = new CheckBox(Localization.lang("Enable web search")); + webSearchBox.setSelected(preferences.getImporterPreferences().areImporterEnabled()); + + CheckBox grobidCheckBox = new CheckBox(Localization.lang("Enable Grobid (metadata extraction service)")); + grobidCheckBox.setSelected(preferences.getGrobidPreferences().isGrobidEnabled()); + + HBox grobidUrl = new HBox(); + Label grobidUrlLabel = new Label(Localization.lang("Service URL")); + TextField grobidUrlField = new TextField(preferences.getGrobidPreferences().getGrobidURL()); + HBox.setHgrow(grobidUrlField, Priority.ALWAYS); + grobidUrl.getChildren().addAll( + grobidUrlLabel, + grobidUrlField, + createHelpButton("https://docs.jabref.org/collect/newentryfromplaintext#grobid") + ); + + grobidUrl.visibleProperty().bind(grobidCheckBox.selectedProperty()); + grobidUrl.managedProperty().bind(grobidCheckBox.selectedProperty()); + + Label fetchersLabel = new Label(Localization.lang("Web search databases")); + HBox fetchersHeader = new HBox(); + fetchersHeader.getChildren().addAll( + fetchersLabel, + createHelpButton("https://docs.jabref.org/collect/import-using-online-bibliographic-database") + ); + + List availableFetchers = WebFetchers + .getSearchBasedFetchers(preferences.getImportFormatPreferences(), preferences.getImporterPreferences()) + .stream() + .map(SearchBasedFetcher::getName) + .filter(name -> !CompositeSearchBasedFetcher.FETCHER_NAME.equals(name)) + .map(name -> { + boolean enabled = preferences.getImporterPreferences().getCatalogs().contains(name); + return new StudyCatalogItem(name, enabled); + }) + .toList(); + + VBox fetchersContainer = new VBox(); + fetchersContainer.getStyleClass().add("fetchers-container"); + List fetcherCheckBoxes = new ArrayList<>(); + + for (StudyCatalogItem fetcher : availableFetchers) { + CheckBox fetcherCheckBox = new CheckBox(fetcher.getName()); + fetcherCheckBox.setSelected(fetcher.isEnabled()); + fetcherCheckBoxes.add(fetcherCheckBox); + fetchersContainer.getChildren().add(fetcherCheckBox); + } + + ScrollPane fetchersScrollPane = new ScrollPane(fetchersContainer); + fetchersScrollPane.setFitToWidth(true); + fetchersScrollPane.setMaxHeight(288); + fetchersScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + fetchersScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + + Optional result = createQuickSettingsDialog( + "Configure web search services", + "Enable and configure online databases and services for importing entries", + versionCheckBox, + webSearchBox, + grobidCheckBox, + grobidUrl, + fetchersHeader, + fetchersScrollPane + ); + + if (result.isEmpty() || result.get() != ButtonType.OK) { + return; + } + + preferences.getInternalPreferences().setVersionCheckEnabled(versionCheckBox.isSelected()); + preferences.getImporterPreferences().setImporterEnabled(webSearchBox.isSelected()); + preferences.getGrobidPreferences().setGrobidEnabled(grobidCheckBox.isSelected()); + preferences.getGrobidPreferences().setGrobidURL(grobidUrlField.getText()); + + List enabledFetchers = new ArrayList<>(); + for (int i = 0; i < fetcherCheckBoxes.size(); i++) { + if (fetcherCheckBoxes.get(i).isSelected()) { + enabledFetchers.add(availableFetchers.get(i).getName()); + } + } + preferences.getImporterPreferences().setCatalogs(enabledFetchers); + } + + private void showEntryTableConfigurationDialog() { + CheckBox showCitationKeyBox = new CheckBox(Localization.lang("Show citation key column")); + + ColumnPreferences columnPreferences = preferences.getMainTablePreferences() + .getColumnPreferences(); + boolean isCitationKeyVisible = columnPreferences + .getColumns() + .stream() + .anyMatch(column -> column.getType() == MainTableColumnModel.Type.NORMALFIELD + && InternalField.KEY_FIELD.getName().equals(column.getQualifier())); + + showCitationKeyBox.setSelected(isCitationKeyVisible); + + Optional result = createQuickSettingsDialog( + "Customize entry table", + "Configure which columns are displayed in the entry table", + showCitationKeyBox + ); + + if (result.isEmpty() || result.get() != ButtonType.OK) { + return; + } + + boolean shouldShow = showCitationKeyBox.isSelected(); + + if (shouldShow && !isCitationKeyVisible) { + MainTableColumnModel citationKeyColumn = new MainTableColumnModel( + MainTableColumnModel.Type.NORMALFIELD, + InternalField.KEY_FIELD.getName() + ); + columnPreferences.getColumns().addFirst(citationKeyColumn); + } else if (!shouldShow && isCitationKeyVisible) { + columnPreferences.getColumns().removeIf(column -> + column.getType() == MainTableColumnModel.Type.NORMALFIELD + && InternalField.KEY_FIELD.getName().equals(column.getQualifier())); + } + } + + private VBox createThemeOption(RadioButton radio, Node wireframe) { + VBox container = new VBox(); + container.getStyleClass().add("theme-option"); + container.getChildren().addAll(radio, wireframe); + container.setOnMouseClicked(_ -> radio.fire()); + return container; + } + + private VBox createWelcomeStartBox() { + Label header = new Label(Localization.lang("Start")); + header.getStyleClass().add("welcome-header-label"); + + Hyperlink newLibraryLink = createActionLink(Localization.lang("New empty library"), + () -> new NewDatabaseAction(tabContainer, preferences).execute()); + + Hyperlink openLibraryLink = createActionLink(Localization.lang("Open library"), + () -> new OpenDatabaseAction(tabContainer, preferences, aiService, dialogService, + stateManager, fileUpdateMonitor, entryTypesManager, undoManager, clipBoardManager, + taskExecutor).execute()); + + Hyperlink openExampleLibraryLink = createActionLink(Localization.lang("New example library"), + this::openExampleLibrary); + + VBox container = new VBox(); + container.getStyleClass().add("welcome-links-content"); + container.getChildren().addAll(newLibraryLink, openExampleLibraryLink, openLibraryLink); + + return createVBoxContainer(header, container); + } + + private VBox createWelcomeRecentBox() { + Label header = new Label(Localization.lang("Recent")); + header.getStyleClass().add("welcome-header-label"); + + updateWelcomeRecentLibraries(); + fileHistoryMenu.getItems().addListener((ListChangeListener) _ -> updateWelcomeRecentLibraries()); + + return createVBoxContainer(header, recentLibrariesBox); + } + + private Hyperlink createActionLink(String text, Runnable action) { + Hyperlink link = new Hyperlink(text); + link.getStyleClass().add("welcome-hyperlink"); + link.setOnAction(_ -> action.run()); + return link; + } + + private void openExampleLibrary() { + try (InputStream in = WelcomeTab.class.getClassLoader().getResourceAsStream("Chocolate.bib")) { + if (in == null) { + LOGGER.warn("Example library file not found."); + return; + } + Reader reader = Importer.getReader(in); + BibtexParser bibtexParser = new BibtexParser(preferences.getImportFormatPreferences(), fileUpdateMonitor); + ParserResult result = bibtexParser.parse(reader); + BibDatabaseContext databaseContext = result.getDatabaseContext(); + LibraryTab libraryTab = LibraryTab.createLibraryTab(databaseContext, tabContainer, dialogService, aiService, + preferences, stateManager, fileUpdateMonitor, entryTypesManager, undoManager, clipBoardManager, taskExecutor); + tabContainer.addTab(libraryTab, true); + } catch (IOException ex) { + LOGGER.error("Failed to load example library", ex); + } + } + + private void updateWelcomeRecentLibraries() { + if (fileHistoryMenu.getItems().isEmpty()) { + displayNoRecentLibrariesMessage(); + return; + } + + recentLibrariesBox.getChildren().clear(); + recentLibrariesBox.getStyleClass().add("welcome-links-content"); + fileHistoryMenu.disableProperty().unbind(); + fileHistoryMenu.setDisable(false); + + for (MenuItem item : fileHistoryMenu.getItems()) { + Hyperlink recentLibraryLink = new Hyperlink(item.getText()); + recentLibraryLink.getStyleClass().add("welcome-hyperlink"); + recentLibraryLink.setOnAction(item.getOnAction()); + recentLibrariesBox.getChildren().add(recentLibraryLink); + } + } + + private void displayNoRecentLibrariesMessage() { + recentLibrariesBox.getChildren().clear(); + Label noRecentLibrariesLabel = new Label(Localization.lang("No recent libraries")); + noRecentLibrariesLabel.getStyleClass().add("welcome-no-recent-label"); + recentLibrariesBox.getChildren().add(noRecentLibrariesLabel); + + fileHistoryMenu.disableProperty().unbind(); + fileHistoryMenu.setDisable(true); + } + + private VBox createVBoxContainer(Node... nodes) { + VBox box = new VBox(); + box.getStyleClass().add("welcome-section"); + box.getChildren().addAll(nodes); + return box; + } + + private FlowPane createIconLinksContainer() { + FlowPane container = new FlowPane(); + container.getStyleClass().add("welcome-community-icons"); + + Hyperlink onlineHelpLink = createFooterLink(Localization.lang("Online help"), StandardActions.HELP, IconTheme.JabRefIcons.HELP); + Hyperlink privacyPolicyLink = createFooterLink(Localization.lang("Privacy policy"), StandardActions.OPEN_PRIVACY_POLICY, IconTheme.JabRefIcons.BOOK); + Hyperlink forumLink = createFooterLink(Localization.lang("Community forum"), StandardActions.OPEN_FORUM, IconTheme.JabRefIcons.FORUM); + Hyperlink mastodonLink = createFooterLink(Localization.lang("Mastodon"), StandardActions.OPEN_MASTODON, IconTheme.JabRefIcons.MASTODON); + Hyperlink linkedInLink = createFooterLink(Localization.lang("LinkedIn"), StandardActions.OPEN_LINKEDIN, IconTheme.JabRefIcons.LINKEDIN); + Hyperlink donationLink = createFooterLink(Localization.lang("Donation"), StandardActions.DONATE, IconTheme.JabRefIcons.DONATE); + + container.getChildren().addAll(onlineHelpLink, privacyPolicyLink, forumLink, mastodonLink, linkedInLink, donationLink); + return container; + } + + private HBox createTextLinksContainer() { + HBox container = new HBox(); + container.getStyleClass().add("welcome-community-links"); + + Hyperlink devVersionLink = createFooterLink(Localization.lang("Download development version"), StandardActions.OPEN_DEV_VERSION_LINK, null); + Hyperlink changelogLink = createFooterLink(Localization.lang("CHANGELOG"), StandardActions.OPEN_CHANGELOG, null); + + container.getChildren().addAll(devVersionLink, changelogLink); + return container; + } + + private Hyperlink createFooterLink(String text, StandardActions action, IconTheme.JabRefIcons icon) { + Hyperlink link = new Hyperlink(text); + link.getStyleClass().add("welcome-community-link"); + + String url = switch (action) { + case HELP -> + URLs.HELP_URL; + case OPEN_FORUM -> + URLs.FORUM_URL; + case OPEN_MASTODON -> + URLs.MASTODON_URL; + case OPEN_LINKEDIN -> + URLs.LINKEDIN_URL; + case DONATE -> + URLs.DONATE_URL; + case OPEN_DEV_VERSION_LINK -> + URLs.DEV_VERSION_LINK_URL; + case OPEN_CHANGELOG -> + URLs.CHANGELOG_URL; + case OPEN_PRIVACY_POLICY -> + URLs.PRIVACY_POLICY_URL; + default -> + null; + }; + + if (url != null) { + link.setOnAction(_ -> new OpenBrowserAction(url, dialogService, preferences.getExternalApplicationsPreferences()).execute()); + } + + if (icon != null) { + link.setGraphic(icon.getGraphicNode()); + } + + return link; + } + + private HBox createVersionContainer() { + HBox container = new HBox(); + container.getStyleClass().add("welcome-community-version"); + + Label versionLabel = new Label(Localization.lang("Current JabRef version: %0", buildInfo.version)); + versionLabel.getStyleClass().add("welcome-community-version-text"); + + container.getChildren().add(versionLabel); + return container; + } +} diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index a9a31b1fb18..c8a71f92b02 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -1768,32 +1768,86 @@ We want to have a look that matches our icons in the tool-bar */ -fx-text-fill: -jr-theme; } -.welcome-footer-container { - -fx-padding: 15px; + +/* Welcome Tab Layout Styles */ +.welcome-main-container { + -fx-max-width: 768px; + -fx-spacing: 28px; -fx-alignment: center; - -fx-spacing: 15px; + -fx-padding: 32px 24px; } -.welcome-footer-label { - -fx-font-size: 2.25em; - -fx-text-fill: -jr-theme-text; - -fx-font-family: "Arial"; +.welcome-top-titles { + -fx-spacing: 10px; + -fx-alignment: left; + -fx-padding: 0 0 20px 0; } -.welcome-footer-link { - -fx-font-size: 1.2em; +.welcome-columns-container { + -fx-spacing: 40px; + -fx-alignment: top-center; +} + +.welcome-left-column, +.welcome-right-column { + -fx-min-width: 0; + -fx-pref-width: 50%; + -fx-max-width: 600px; +} + +.welcome-content-column { + -fx-spacing: 25px; + -fx-alignment: top-left; +} + +.welcome-section { + -fx-spacing: 12px; + -fx-alignment: top-left; +} + +.welcome-links-content { + -fx-spacing: 8px; + -fx-alignment: top-left; +} + + +/* Welcome Community */ + +.welcome-community-content { + -fx-spacing: 12px; + -fx-alignment: top-left; +} + +.welcome-community-icons { + -fx-hgap: 12px; + -fx-vgap: 8px; + -fx-alignment: center-left; +} + +.welcome-community-links { + -fx-spacing: 15px; + -fx-alignment: center-left; +} + +.welcome-community-link { + -fx-font-size: 1.0em; -fx-text-fill: -jr-theme; -fx-font-family: "Arial"; } -.welcome-footer-link:hover { +.welcome-community-link:hover { -fx-underline: true; -fx-text-fill: derive(-jr-theme, -20%); } -.welcome-footer-version { - -fx-font-size: 1.2em; - -fx-text-fill: -jr-theme; +.welcome-community-version { + -fx-alignment: center-left; + -fx-padding: 5px 0 0 0; +} + +.welcome-community-version-text { + -fx-font-size: 0.9em; + -fx-text-fill: -jr-gray-2; -fx-font-family: "Arial"; } @@ -1984,7 +2038,7 @@ We want to have a look that matches our icons in the tool-bar */ #entryEditor #bibtexSourceCodeArea .search { -rtfx-background-color: #ffff00; - -fx-fill: #7800A9 ; + -fx-fill: #7800A9; -fx-font-size: 1.2em; -fx-font-weight: bolder; } @@ -2454,7 +2508,7 @@ journalInfo .grid-cell-b { -fx-border-width: 2.5; } -.three-way-merge .styled-text-area .text{ +.three-way-merge .styled-text-area .text { -fx-fill: -fx-text-background-color; } @@ -2474,7 +2528,7 @@ journalInfo .grid-cell-b { -fx-background-color: -jr-menu-background; } -.three-way-merge .merge-header-cell .label{ +.three-way-merge .merge-header-cell .label { -fx-font-weight: bold; -fx-padding: 1, 0, 1, 0; } @@ -2482,7 +2536,7 @@ journalInfo .grid-cell-b { .three-way-merge .field-name .glyph-icon, .three-way-merge .field-name .ikonli-font-icon { -fx-icon-size: 17; - -fx-icon-color: -jr-theme-text; + -fx-icon-color: -jr-theme-text; } /* Miscellaneous */ @@ -2496,7 +2550,9 @@ journalInfo .grid-cell-b { } #styleSelectDialog .currentStyleNameLabel { - -fx-font-size: 1em; -fx-font-weight: bold; -fx-text-fill: -jr-theme; + -fx-font-size: 1em; + -fx-font-weight: bold; + -fx-text-fill: -jr-theme; } .exampleQuestionStyle { @@ -2519,13 +2575,222 @@ journalInfo .grid-cell-b { -fx-background-color: transparent; } -.text-button-blue{ +.text-button-blue { -fx-background-color: transparent; -fx-text-fill: -jr-theme; -fx-font-size: 1.25em; -fx-border-color: transparent; } + +/* Quick Settings */ +.quick-settings-content { + -fx-spacing: 8px; + -fx-alignment: top-center; +} + +.quick-settings-button { + -fx-background-color: derive(-jr-base, 10%); + -fx-border-color: -jr-gray-1; + -fx-border-width: 1px; + -fx-border-radius: 4px; + -fx-background-radius: 4px; + -fx-padding: 8px 12px; + -fx-alignment: center-left; + -fx-graphic-text-gap: 8px; + -fx-font-size: 1.0em; + -fx-text-fill: -fx-text-base-color; + -fx-cursor: hand; +} + +.quick-settings-button:hover { + -fx-background-color: -jr-hover; + -fx-border-color: -jr-accent; +} + +.quick-settings-button .glyph-icon, +.quick-settings-button .ikonli-font-icon { + -fx-icon-color: -jr-theme-text; + -fx-fill: -jr-theme-text; + -fx-font-size: 1.1em; +} + +.quick-settings-dialog-container { + -fx-padding: 16px; + -fx-spacing: 16px; + -fx-pref-width: 512px; +} + +.quick-settings-dialog-container > HBox { + -fx-alignment: center-left; + -fx-spacing: 8px; +} + +.quick-settings-dialog-container > ScrollPane { + -fx-border-width: 1px; + -fx-border-color: derive(-fx-base, -20%); + -fx-border-radius: 4px; +} + +/* Quick Settings: Theme Selection */ +.theme-option { + -fx-padding: 8px; + -fx-border-radius: 4px; + -fx-background-radius: 4px; + -fx-spacing: 12px; + -fx-alignment: center-left; +} + +.theme-option:hover { + -fx-background-color: rgba(0, 0, 0, 0.05); +} + +/* Wireframe base styles */ +.wireframe-container { + -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-border-color: -jr-wf-border-color; +} + +.wireframe-menubar { + -fx-padding: 0.1875em 0.0625em; + -fx-background-color: -jr-wf-menubar-bg; +} + +.wireframe-menu-item { + -fx-border-radius: 0.0625em; + -fx-background-radius: 0.0625em; + -fx-background-color: -jr-wf-menu-item-bg; +} + +.wireframe-toolbar { + -fx-padding: 0.125em; + -fx-spacing: 0.375em; + -fx-background-color: -jr-wf-toolbar-bg; +} + +.wireframe-tool-item { + -fx-border-radius: 0.0625em; + -fx-background-radius: 0.0625em; + -fx-background-color: -jr-wf-tool-item-bg; +} + +.wireframe-search { + -fx-border-width: 0.03125em; + -fx-border-radius: 0.125em; + -fx-background-radius: 0.125em; + -fx-background-color: -jr-wf-search-bg; + -fx-border-color: -jr-wf-search-border; +} + +.wireframe-search-field { + -fx-border-radius: 0.0625em; + -fx-background-radius: 0.0625em; + -fx-background-color: -jr-wf-search-field-bg; +} + +.wireframe-sidebar { + -fx-padding: 0.25em 0.125em; + -fx-background-color: -jr-wf-sidebar-bg; +} + +.wireframe-sidebar-item { + -fx-border-radius: 0.0625em; + -fx-background-radius: 0.0625em; + -fx-background-color: -jr-wf-sidebar-item-bg; +} + +.wireframe-welcome-tab-area { + -fx-padding: 0.25em; + -fx-alignment: center; + -fx-background-color: -jr-wf-welcome-area-bg; +} + +.wireframe-welcome-tab-item { + -fx-border-radius: 0.0625em; + -fx-background-radius: 0.0625em; + -fx-background-color: -jr-wf-welcome-item-bg; +} + +/* Other color scheme */ +.wireframe-light { + -jr-wf-menubar-bg: #f9f9f9; + -jr-wf-menu-item-bg: -jr-gray-1; + -jr-wf-toolbar-bg: #f9f9f9; + -jr-wf-tool-item-bg: -jr-theme; + -jr-wf-search-bg: -jr-white; + -jr-wf-search-border: #dddddd; + -jr-wf-search-field-bg: #f8f8f8; + -jr-wf-sidebar-bg: -jr-gray-1; + -jr-wf-sidebar-item-bg: -jr-theme; + -jr-wf-welcome-area-bg: #f3f3f3; + -jr-wf-welcome-item-bg: #dddddd; +} + +.wireframe-dark { + -jr-wf-menubar-bg: #141824; + -jr-wf-menu-item-bg: #424758; + -jr-wf-toolbar-bg: #141824; + -jr-wf-tool-item-bg: #2c9490; + -jr-wf-search-bg: #2c2e3b; + -jr-wf-search-border: #424758; + -jr-wf-search-field-bg: #424758; + -jr-wf-sidebar-bg: #212330; + -jr-wf-sidebar-item-bg: #2c9490; + -jr-wf-welcome-area-bg: #272b38; + -jr-wf-welcome-item-bg: #7d8591; +} + +.wireframe-custom { + -jr-wf-menubar-bg: #f5ffe5; + -jr-wf-menu-item-bg: #346963; + -jr-wf-toolbar-bg: #f5ffe5; + -jr-wf-tool-item-bg: #2E838C; + -jr-wf-search-bg: -jr-white; + -jr-wf-search-border: #2E838C; + -jr-wf-search-field-bg: #f0f3ff; + -jr-wf-sidebar-bg: #e1ebd1; + -jr-wf-sidebar-item-bg: #2E838C; + -jr-wf-welcome-area-bg: #E8F2D8; + -jr-wf-welcome-item-bg: #2E838C; +} + +/* Quick Settings: Push Application Configuration */ +.applications-list { + -fx-pref-height: 200px; + -fx-pref-width: 400px; +} + +.application-item { + -fx-padding: 8px; + -fx-border-radius: 4px; + -fx-background-color: transparent; +} + +.detected-application { + -fx-background-color: derive(-jr-accent, 80%); + -fx-font-weight: bold; +} + +.application-item:filled:selected { + -fx-background-color: -jr-accent; + -fx-text-fill: white; +} + +.detected-application .glyph-icon, +.detected-application .ikonli-font-icon { + -fx-icon-color: -jr-theme-text; + -fx-fill: -jr-theme-text; +} + +/* Quick Settings: Online Fetchers */ +.fetchers-container { + -fx-padding: 12px; + -fx-spacing: 12px; +} + /* Walkthrough Styles */ .walkthrough-overlay { -fx-background-color: transparent; @@ -2663,12 +2928,6 @@ journalInfo .grid-cell-b { -fx-padding: 0.25em 0.5em; } -.walkthrough-fullscreen-content { - -fx-padding: 0.1875em; - -fx-spacing: 1em; - -fx-max-width: 48em; - -fx-fill-width: true; -} /* Markdown styles */ .markdown-textflow { @@ -2756,3 +3015,9 @@ journalInfo .grid-cell-b { .markdown-link:hover { -fx-text-fill: #004499; } + +.invalid-path { + -fx-background-color: rgba(238, 82, 83, 0.2); + -fx-border-color: -jr-error; + -fx-border-width: 1px; +} diff --git a/jabgui/src/main/resources/org/jabref/gui/welcome/ThemeWireFrameComponent.fxml b/jabgui/src/main/resources/org/jabref/gui/welcome/ThemeWireFrameComponent.fxml new file mode 100644 index 00000000000..78630d1954c --- /dev/null +++ b/jabgui/src/main/resources/org/jabref/gui/welcome/ThemeWireFrameComponent.fxml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 740154de3e2..2dc39e50068 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -1342,9 +1342,11 @@ Opens\ JabRef's\ LinkedIn\ page=Opens JabRef's LinkedIn page LinkedIn=LinkedIn Opens\ JabRef's\ Mastodon\ page=Opens JabRef's Mastodon page Mastodon=Mastodon +Privacy\ policy=Privacy policy Opens\ JabRef's\ Facebook\ page=Opens JabRef's Facebook page Opens\ JabRef's\ blog=Opens JabRef's blog Opens\ JabRef's\ website=Opens JabRef's website +Opens\ JabRef's\ privacy\ policy=Opens JabRef's privacy policy Could\ not\ open\ browser.=Could not open browser. Please\ open\ %0\ manually.=Please open %0 manually. @@ -2970,6 +2972,14 @@ Choose\ this\ option\ to\ tell\ JabRef\ where\ your\ research\ files\ are\ store Click\ "Save"\ to\ save\ changes=Click "Save" to save changes Congratulations.\ Your\ main\ file\ directory\ is\ now\ configured.\ JabRef\ will\ use\ this\ location\ to\ automatically\ find\ and\ organize\ your\ research\ documents.=Congratulations. Your main file directory is now configured. JabRef will use this location to automatically find and organize your research documents. Additional\ information\ on\ main\ file\ directory\ can\ be\ found\ in\ https\://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks=Additional information on main file directory can be found in https://docs.jabref.org/v5/finding-sorting-and-cleaning-entries/filelinks +# Entry table walkthrough +Select\ the\ "Entry\ table"\ tab=Select the "Entry table" tab +This\ section\ allows\ you\ to\ customize\ the\ columns\ displayed\ in\ the\ entry\ table\ when\ viewing\ your\ bibliography.=This section allows you to customize the columns displayed in the entry table when viewing your bibliography. +Customize\ your\ entry\ table\ columns=Customize your entry table columns +Here\ you\ can\ customize\ which\ columns\ appear\ in\ your\ entry\ table.\ You\ can\ add,\ remove,\ or\ reorder\ columns\ such\ as\ citation\ key,\ title,\ author,\ year,\ and\ journal.\ This\ helps\ you\ see\ the\ most\ relevant\ information\ for\ your\ research\ at\ a\ glance.=Here you can customize which columns appear in your entry table. You can add, remove, or reorder columns such as citation key, title, author, year, and journal. This helps you see the most relevant information for your research at a glance. +The\ columns\ you\ configure\ here\ will\ be\ displayed\ whenever\ you\ open\ a\ library\ in\ JabRef.\ You\ can\ always\ return\ to\ this\ settings\ page\ to\ modify\ your\ column\ preferences.=The columns you configure here will be displayed whenever you open a library in JabRef. You can always return to this settings page to modify your column preferences. +Your\ entry\ table\ columns\ are\ now\ configured.\ These\ settings\ will\ be\ applied\ to\ all\ your\ libraries\ in\ JabRef.=Your entry table columns are now configured. These settings will be applied to all your libraries in JabRef. +You\ can\ find\ more\ information\ about\ customizing\ JabRef\ at\ https\://docs.jabref.org/=You can find more information about customizing JabRef at https://docs.jabref.org/ # CommandLine Available\ export\ formats\:=Available export formats: @@ -3001,3 +3011,49 @@ File\ '%0'\ already\ exists.\ Use\ -f\ or\ --force\ to\ overwrite.=File '%0' alr Pseudonymizing\ library\ '%0'...=Pseudonymizing library '%0'... Invalid\ output\ file\ type\ provided.=Invalid output file type provided. Saved\ %0.=Saved %0. + +# Quick Settings +Quick\ Settings=Quick Settings + +# Quick Settings Buttons +Set\ main\ file\ directory=Set main file directory +Change\ visual\ theme=Change visual theme +Optimize\ for\ large\ libraries=Optimize for large libraries +Configure\ push\ to\ applications=Configure push to applications +Configure\ web\ search\ services=Configure web search services +Customize\ entry\ table=Customize entry table +Walkthrough=Walkthrough + +# Main File Directory Dialog +Main\ file\ directory\ path=Main file directory path +Choose\ the\ default\ directory\ for\ storing\ attached\ files=Choose the default directory for storing attached files + +# Visual Theme Dialog +Select\ your\ preferred\ theme\ for\ the\ application=Select your preferred theme for the application +Custom\ theme\ file\ path=Custom theme file path + +# Large Library Optimization Dialog +Improve\ performance\ when\ working\ with\ libraries\ containing\ many\ entries=Improve performance when working with libraries containing many entries +Select\ performance\ optimizations\ to\ apply=Select performance optimizations to apply +Disable\ fulltext\ indexing=Disable fulltext indexing +Disable\ creation\ date\ timestamps=Disable creation date timestamps +Disable\ modification\ date\ timestamps=Disable modification date timestamps +Disable\ automatic\ saving=Disable automatic saving +Disable\ group\ entry\ counts=Disable group entry counts + +# Push to Applications Dialog +Select\ your\ text\ editor\ or\ LaTeX\ application\ for\ pushing\ citations=Select your text editor or LaTeX application for pushing citations +Detected\ applications\ are\ highlighted.\ Application\ that\ are\ not\ detected\ can\ be\ set\ manually\ by\ specifying\ the\ path\ to\ the\ executable.=Detected applications are highlighted. Application that are not detected can be set manually by specifying the path to the executable. +Application\ path=Application path +Path\ to\ application\ executable=Path to application executable + +# Online Services Dialog +Enable\ and\ configure\ online\ databases\ and\ services\ for\ importing\ entries=Enable and configure online databases and services for importing entries +Service\ URL=Service URL +Web\ search\ databases=Web search databases +Check\ for\ updates\ at\ startup=Check for updates at startup +Enable\ Grobid\ (metadata\ extraction\ service)=Enable Grobid (metadata extraction service) + +# Entry Table Customization Dialog +Configure\ which\ columns\ are\ displayed\ in\ the\ entry\ table=Configure which columns are displayed in the entry table +Show\ citation\ key\ column=Show citation key column