existingCites = getExistingEntriesFromCiteField(entryThatCitesOurExistingEntry);
- existingCites.removeIf(String::isEmpty);
- String key;
- if (generateNewKeyOnImport || entryThatCitesOurExistingEntry.getCitationKey().isEmpty()) {
- key = generator.generateKey(entryThatCitesOurExistingEntry);
- entryThatCitesOurExistingEntry.setCitationKey(key);
- } else {
- key = existingEntry.getCitationKey().get();
+ /**
+ * "cited by" is the opposite of "cites", but not stored in field `CITED_BY`, but in the `CITES` field of the citing entry.
+ *
+ * Therefore, some special handling is needed
+ */
+ private void importCitedBy(List entries, BibEntry existingEntry, ImportHandler importHandler, CitationKeyGenerator generator, boolean generateNewKeyOnImport) {
+ if (existingEntry.getCitationKey().isEmpty()) {
+ if (!generateNewKeyOnImport) {
+ dialogService.notify(Localization.lang("No citation key for %0", existingEntry.getAuthorTitleYear()));
+ return;
}
- addToKeyToList(existingCites, key);
- entryThatCitesOurExistingEntry.setField(StandardField.CITES, toCommaSeparatedString(existingCites));
+ existingEntry.setCitationKey(generator.generateKey(existingEntry));
}
+ String citationKey = existingEntry.getCitationKey().get();
- importHandler.importEntries(entries);
- }
-
- private void addToKeyToList(List list, String key) {
- if (!list.contains(key)) {
- list.add(key);
+ for (BibEntry citingEntry : entries) {
+ SequencedSet existingCites = citingEntry.getCites();
+ existingCites.add(citationKey);
+ citingEntry.setCites(existingCites);
}
- }
- private List getExistingEntriesFromCiteField(BibEntry entry) {
- return Arrays.stream(entry.getField(StandardField.CITES).orElse("").split(",")).collect(Collectors.toList());
- }
-
- private String toCommaSeparatedString(List citeentries) {
- return String.join(",", citeentries);
+ importHandler.importEntries(entries);
}
}
diff --git a/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java
index 5e98f7d737d..174595df2e6 100644
--- a/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java
+++ b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java
@@ -77,6 +77,8 @@ public FulltextSearchResultsTab(StateManager stateManager,
content.setPadding(new Insets(10));
setContent(scrollPane);
setText(Localization.lang("Search results"));
+
+ // Rebinding is necessary because of re-rendering of highlighting of matched text
searchQueryProperty.addListener((observable, oldValue, newValue) -> bindToEntry(entry));
}
@@ -90,9 +92,6 @@ protected void bindToEntry(BibEntry entry) {
if (entry == null || !shouldShow(entry)) {
return;
}
- if (documentViewerView == null) {
- documentViewerView = new DocumentViewerView();
- }
this.entry = entry;
content.getChildren().clear();
@@ -159,6 +158,9 @@ private Text createPageLink(LinkedFile linkedFile, int pageNumber) {
pageLink.setOnMouseClicked(event -> {
if (MouseButton.PRIMARY == event.getButton()) {
+ if (documentViewerView == null) {
+ documentViewerView = new DocumentViewerView();
+ }
documentViewerView.switchToFile(linkedFile);
documentViewerView.gotoPage(pageNumber);
documentViewerView.disableLiveMode();
diff --git a/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java b/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java
index 8884561f2a7..a66e407522c 100644
--- a/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java
+++ b/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java
@@ -16,6 +16,7 @@
import javafx.scene.control.Tooltip;
import javafx.scene.layout.FlowPane;
import javafx.stage.Screen;
+import javafx.stage.Stage;
import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
@@ -87,6 +88,10 @@ public EntryTypeView(LibraryTab libraryTab, DialogService dialogService, GuiPref
.load()
.setAsDialogPane(this);
+ Stage stage = (Stage) getDialogPane().getScene().getWindow();
+ stage.setMinHeight(400);
+ stage.setMinWidth(500);
+
ControlHelper.setAction(generateButton, this.getDialogPane(), event -> viewModel.runFetcherWorker());
setOnCloseRequest(e -> viewModel.cancelFetcherWorker());
diff --git a/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java b/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java
index e476883b7f5..48d321b1d81 100644
--- a/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java
+++ b/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java
@@ -25,7 +25,7 @@
import org.slf4j.LoggerFactory;
/**
- * Try to download fulltext PDF for selected entry(ies) by following URL or DOI link.
+ * Try to download fulltext PDF for selected entry(s) by following URL or DOI link.
*/
public class DownloadFullTextAction extends SimpleCommand {
diff --git a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java
index 26fde2c5ba6..a156472dcbf 100644
--- a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java
+++ b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java
@@ -4,8 +4,10 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Supplier;
import java.util.stream.Stream;
+import org.jabref.gui.StateManager;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.externalfiletype.UnknownExternalFileType;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
@@ -27,13 +29,16 @@ public class ExternalFilesEntryLinker {
private final ExternalApplicationsPreferences externalApplicationsPreferences;
private final FilePreferences filePreferences;
- private final BibDatabaseContext bibDatabaseContext;
private final NotificationService notificationService;
+ private final Supplier bibDatabaseContextSupplier;
- public ExternalFilesEntryLinker(ExternalApplicationsPreferences externalApplicationsPreferences, FilePreferences filePreferences, BibDatabaseContext bibDatabaseContext, NotificationService notificationService) {
+ /**
+ * @param stateManager required for currently active BibDatabaseContext
+ */
+ public ExternalFilesEntryLinker(ExternalApplicationsPreferences externalApplicationsPreferences, FilePreferences filePreferences, NotificationService notificationService, StateManager stateManager) {
this.externalApplicationsPreferences = externalApplicationsPreferences;
this.filePreferences = filePreferences;
- this.bibDatabaseContext = bibDatabaseContext;
+ this.bibDatabaseContextSupplier = () -> stateManager.getActiveDatabase().orElse(new BibDatabaseContext());
this.notificationService = notificationService;
}
@@ -43,7 +48,7 @@ public void linkFilesToEntry(BibEntry entry, List files) {
String typeName = FileUtil.getFileExtension(file)
.map(ext -> ExternalFileTypes.getExternalFileTypeByExt(ext, externalApplicationsPreferences).orElse(new UnknownExternalFileType(ext)).getName())
.orElse("");
- Path relativePath = FileUtil.relativize(file, bibDatabaseContext, filePreferences);
+ Path relativePath = FileUtil.relativize(file, bibDatabaseContextSupplier.get(), filePreferences);
LinkedFile linkedFile = new LinkedFile("", relativePath, typeName);
String link = linkedFile.getLink();
@@ -75,7 +80,7 @@ public void coveOrMoveFilesSteps(BibEntry entry, List files, boolean shoul
.map(ext -> ExternalFileTypes.getExternalFileTypeByExt(ext, externalApplicationsPreferences).orElse(new UnknownExternalFileType(ext)).getName())
.orElse("");
LinkedFile linkedFile = new LinkedFile("", file, typeName);
- LinkedFileHandler linkedFileHandler = new LinkedFileHandler(linkedFile, entry, bibDatabaseContext, filePreferences);
+ LinkedFileHandler linkedFileHandler = new LinkedFileHandler(linkedFile, entry, bibDatabaseContextSupplier.get(), filePreferences);
try {
linkedFileHandler.copyOrMoveToDefaultDirectory(shouldMove, true);
} catch (IOException exception) {
diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java
index 43038d286f5..808764d0231 100644
--- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java
+++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java
@@ -35,6 +35,7 @@
import org.jabref.logic.importer.ImportFormatReader;
import org.jabref.logic.importer.ImportFormatReader.UnknownFormatImport;
import org.jabref.logic.importer.ParseException;
+import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.importer.fileformat.BibtexParser;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
@@ -89,7 +90,7 @@ public ImportHandler(BibDatabaseContext database,
this.dialogService = dialogService;
this.taskExecutor = taskExecutor;
- this.fileLinker = new ExternalFilesEntryLinker(preferences.getExternalApplicationsPreferences(), preferences.getFilePreferences(), database, dialogService);
+ this.fileLinker = new ExternalFilesEntryLinker(preferences.getExternalApplicationsPreferences(), preferences.getFilePreferences(), dialogService, stateManager);
this.contentImporter = new ExternalFilesContentImporter(preferences.getImportFormatPreferences());
this.undoManager = undoManager;
}
@@ -102,6 +103,7 @@ public BackgroundTask> importFilesInBackgro
return new BackgroundTask<>() {
private int counter;
private final List results = new ArrayList<>();
+ private final List allEntriesToAdd = new ArrayList<>();
@Override
public List call() {
@@ -115,13 +117,18 @@ public List call() {
}
UiTaskExecutor.runInJavaFXThread(() -> {
- updateMessage(Localization.lang("Processing file %0", file.getFileName()));
- updateProgress(counter, files.size() - 1d);
+ setTitle(Localization.lang("Importing files into %1 | %2 of %0 file(s) processed.",
+ files.size(),
+ bibDatabaseContext.getDatabasePath().map(path -> path.getFileName().toString()).orElse(Localization.lang("untitled")),
+ counter));
+ updateMessage(Localization.lang("Processing %0", FileUtil.shortenFileName(file.getFileName().toString(), 68)));
+ updateProgress(counter, files.size());
+ showToUser(true);
});
try {
if (FileUtil.isPDFFile(file)) {
- var pdfImporterResult = contentImporter.importPDFContent(file);
+ ParserResult pdfImporterResult = contentImporter.importPDFContent(file, bibDatabaseContext, filePreferences);
List pdfEntriesInFile = pdfImporterResult.getDatabase().getEntries();
if (pdfImporterResult.hasWarnings()) {
@@ -168,10 +175,7 @@ public List call() {
UiTaskExecutor.runInJavaFXThread(() -> updateMessage(Localization.lang("Error")));
}
-
- // We need to run the actual import on the FX Thread, otherwise we will get some deadlocks with the UIThreadList
- // That method does a clone() on each entry
- UiTaskExecutor.runInJavaFXThread(() -> importEntries(entriesToAdd));
+ allEntriesToAdd.addAll(entriesToAdd);
ce.addEdit(new UndoableInsertEntries(bibDatabaseContext.getDatabase(), entriesToAdd));
ce.end();
@@ -180,6 +184,9 @@ public List call() {
counter++;
}
+ // We need to run the actual import on the FX Thread, otherwise we will get some deadlocks with the UIThreadList
+ // That method does a clone() on each entry
+ UiTaskExecutor.runInJavaFXThread(() -> importEntries(allEntriesToAdd));
return results;
}
@@ -208,7 +215,6 @@ public void importEntries(List entries) {
}
public void importCleanedEntries(List entries) {
- entries = entries.stream().map(entry -> (BibEntry) entry.clone()).toList();
bibDatabaseContext.getDatabase().insertEntries(entries);
generateKeys(entries);
setAutomaticFields(entries);
@@ -235,6 +241,8 @@ private void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext
}
importCleanedEntries(List.of(finalEntry));
downloadLinkedFiles(finalEntry);
+ BibEntry entryToFocus = finalEntry;
+ stateManager.activeTabProperty().get().ifPresent(tab -> tab.clearAndSelect(entryToFocus));
}).executeWith(taskExecutor);
}
diff --git a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java
index 643ca3179e3..4cf0deb39be 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java
@@ -6,7 +6,6 @@
import javax.swing.undo.UndoManager;
-import org.jabref.gui.DialogService;
import org.jabref.gui.autocompleter.ContentSelectorSuggestionProvider;
import org.jabref.gui.autocompleter.SuggestionProvider;
import org.jabref.gui.autocompleter.SuggestionProviders;
@@ -25,7 +24,6 @@
import org.jabref.gui.undo.UndoAction;
import org.jabref.logic.integrity.FieldCheckers;
import org.jabref.logic.journals.JournalAbbreviationRepository;
-import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldFactory;
@@ -45,8 +43,6 @@ public class FieldEditors {
private static final Logger LOGGER = LoggerFactory.getLogger(FieldEditors.class);
public static FieldEditorFX getForField(final Field field,
- final TaskExecutor taskExecutor,
- final DialogService dialogService,
final JournalAbbreviationRepository journalAbbreviationRepository,
final GuiPreferences preferences,
final BibDatabaseContext databaseContext,
diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java
index 5a95b3e42f2..96c00de9f3b 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java
@@ -41,7 +41,7 @@
import org.jabref.gui.copyfiles.CopySingleFileAction;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.icon.JabRefIconView;
-import org.jabref.gui.importer.GrobidOptInDialogHelper;
+import org.jabref.gui.importer.GrobidUseDialogHelper;
import org.jabref.gui.keyboard.KeyBinding;
import org.jabref.gui.linkedfile.DeleteFileAction;
import org.jabref.gui.linkedfile.LinkedFileEditDialog;
@@ -236,7 +236,7 @@ private Node createFileDisplay(LinkedFileViewModel linkedFile) {
parsePdfMetadata.setTooltip(new Tooltip(Localization.lang("Parse Metadata from PDF.")));
parsePdfMetadata.visibleProperty().bind(linkedFile.isOfflinePdfProperty());
parsePdfMetadata.setOnAction(event -> {
- GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(dialogService, preferences.getGrobidPreferences());
+ GrobidUseDialogHelper.showAndWaitIfUserIsUndecided(dialogService, preferences.getGrobidPreferences());
linkedFile.parsePdfMetadataAndShowMergeDialog();
});
parsePdfMetadata.getStyleClass().setAll("icon-button");
@@ -287,7 +287,7 @@ public Parent getNode() {
@FXML
private void addNewFile() {
- dialogService.showCustomDialogAndWait(new LinkedFileEditDialog()).ifPresent(newLinkedFile -> {
+ dialogService.showCustomDialogAndWait(new LinkedFileEditDialog()).filter(file -> !file.isEmpty()).ifPresent(newLinkedFile -> {
viewModel.addNewLinkedFile(newLinkedFile);
});
}
diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java
index 540e5603d36..4cd947b2287 100644
--- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java
+++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java
@@ -4,7 +4,6 @@
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -32,7 +31,6 @@
import org.jabref.gui.linkedfile.AttachFileFromURLAction;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.BindingsHelper;
-import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.logic.bibtex.FileFieldWriter;
import org.jabref.logic.importer.FulltextFetchers;
import org.jabref.logic.importer.util.FileFieldParser;
@@ -40,7 +38,6 @@
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
-import org.jabref.logic.util.io.FileNameCleaner;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
@@ -107,19 +104,6 @@ public static LinkedFile fromFile(Path file, List fileDirectories, Externa
return new LinkedFile("", relativePath, suggestedFileType.getName());
}
- public LinkedFileViewModel fromFile(Path file, ExternalApplicationsPreferences externalApplicationsPreferences) {
- List fileDirectories = databaseContext.getFileDirectories(preferences.getFilePreferences());
-
- LinkedFile linkedFile = fromFile(file, fileDirectories, externalApplicationsPreferences);
- return new LinkedFileViewModel(
- linkedFile,
- entry,
- databaseContext,
- taskExecutor,
- dialogService,
- preferences);
- }
-
private List parseToFileViewModel(String stringValue) {
return FileFieldParser.parse(stringValue).stream()
.map(linkedFile -> new LinkedFileViewModel(
@@ -140,41 +124,6 @@ public ListProperty filesProperty() {
return files;
}
- public void addNewFile() {
- Path workingDirectory = databaseContext.getFirstExistingFileDir(preferences.getFilePreferences())
- .orElse(preferences.getFilePreferences().getWorkingDirectory());
-
- FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
- .withInitialDirectory(workingDirectory)
- .build();
-
- List fileDirectories = databaseContext.getFileDirectories(preferences.getFilePreferences());
- List selectedFiles = dialogService.showFileOpenDialogAndGetMultipleFiles(fileDialogConfiguration);
-
- for (Path fileToAdd : selectedFiles) {
- if (FileUtil.detectBadFileName(fileToAdd.toString())) {
- String newFilename = FileNameCleaner.cleanFileName(fileToAdd.getFileName().toString());
-
- boolean correctButtonPressed = dialogService.showConfirmationDialogAndWait(Localization.lang("File \"%0\" cannot be added!", fileToAdd.getFileName()),
- Localization.lang("Illegal characters in the file name detected.\nFile will be renamed to \"%0\" and added.", newFilename),
- Localization.lang("Rename and add"));
-
- if (correctButtonPressed) {
- Path correctPath = fileToAdd.resolveSibling(newFilename);
- try {
- Files.move(fileToAdd, correctPath);
- addNewLinkedFile(correctPath, fileDirectories);
- } catch (IOException ex) {
- LOGGER.error("Error moving file", ex);
- dialogService.showErrorDialogAndWait(ex);
- }
- }
- } else {
- addNewLinkedFile(fileToAdd, fileDirectories);
- }
- }
- }
-
public void addNewLinkedFile(LinkedFile linkedFile) {
files.add(new LinkedFileViewModel(
linkedFile,
@@ -185,22 +134,17 @@ public void addNewLinkedFile(LinkedFile linkedFile) {
preferences));
}
- private void addNewLinkedFile(Path correctPath, List fileDirectories) {
- LinkedFile newLinkedFile = fromFile(correctPath, fileDirectories, preferences.getExternalApplicationsPreferences());
- addNewLinkedFile(newLinkedFile);
- }
-
@Override
public void bindToEntry(BibEntry entry) {
super.bindToEntry(entry);
- if ((entry != null) && preferences.getEntryEditorPreferences().autoLinkFilesEnabled()) {
+ if (preferences.getEntryEditorPreferences().autoLinkFilesEnabled()) {
LOGGER.debug("Auto-linking files for entry {}", entry);
BackgroundTask> findAssociatedNotLinkedFiles = BackgroundTask
.wrap(() -> findAssociatedNotLinkedFiles(entry))
.onSuccess(list -> {
if (!list.isEmpty()) {
- LOGGER.debug("Found non-associated files:", list);
+ LOGGER.debug("Found non-associated files: {}", list);
files.addAll(list);
}
});
diff --git a/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/src/main/java/org/jabref/gui/frame/JabRefFrame.java
index 2b7ffb38497..01420aa6b71 100644
--- a/src/main/java/org/jabref/gui/frame/JabRefFrame.java
+++ b/src/main/java/org/jabref/gui/frame/JabRefFrame.java
@@ -64,6 +64,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import static org.jabref.gui.actions.ActionHelper.needsSavedLocalDatabase;
+
/**
* Represents the inner frame of the JabRef window
*/
@@ -79,7 +81,6 @@ public class JabRefFrame extends BorderPane implements LibraryTabContainer, UiMe
private final GlobalSearchBar globalSearchBar;
private final FileHistoryMenu fileHistory;
- private final FrameDndHandler frameDndHandler;
@SuppressWarnings({"FieldCanBeLocal"}) private EasyObservableList openDatabaseList;
@@ -136,7 +137,7 @@ public JabRefFrame(Stage mainStage,
taskExecutor);
Injector.setModelOrService(UiMessageHandler.class, viewModel);
- this.frameDndHandler = new FrameDndHandler(
+ FrameDndHandler frameDndHandler = new FrameDndHandler(
tabbedPane,
mainStage::getScene,
this::getOpenDatabaseAction,
@@ -425,7 +426,7 @@ public void showLibraryTab(@NonNull LibraryTab libraryTab) {
/**
* Opens a new tab with existing data.
* Asynchronous loading is done at {@link LibraryTab#createLibraryTab}.
- * Similar method: {@link OpenDatabaseAction#openTheFile(Path)}
+ * Similar method: {@link OpenDatabaseAction#openTheFile(Path)} (Path)}
*/
public void addTab(@NonNull BibDatabaseContext databaseContext, boolean raisePanel) {
Objects.requireNonNull(databaseContext);
@@ -524,7 +525,8 @@ private OpenDatabaseAction getOpenDatabaseAction() {
* Refreshes the ui after preferences changes
*/
public void refresh() {
- getLibraryTabs().forEach(LibraryTab::setupMainPanel);
+ // Disabled, because Bindings implement automatic update. Left here as commented out code to guide if something does not work after updating the preferences.
+ // getLibraryTabs().forEach(LibraryTab::setupMainPanel);
getLibraryTabs().forEach(tab -> tab.getMainTable().getTableModel().resetFieldFormatter());
}
@@ -636,6 +638,7 @@ private class OpenDatabaseFolder extends SimpleCommand {
public OpenDatabaseFolder(Supplier databaseContext) {
this.databaseContext = databaseContext;
+ this.executable.bind(needsSavedLocalDatabase(stateManager));
}
@Override
diff --git a/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java b/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java
index dceb39f05cc..afb86d06d70 100644
--- a/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java
+++ b/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java
@@ -1,5 +1,8 @@
package org.jabref.gui.frame;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.ArrayList;
@@ -30,18 +33,22 @@
import org.jabref.logic.UiCommand;
import org.jabref.logic.ai.AiService;
import org.jabref.logic.importer.ImportCleanup;
+import org.jabref.logic.importer.OpenDatabase;
import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.l10n.Localization;
+import org.jabref.logic.os.OS;
import org.jabref.logic.shared.DatabaseNotSupportedException;
import org.jabref.logic.shared.exception.InvalidDBMSConnectionPropertiesException;
import org.jabref.logic.shared.exception.NotASharedDatabaseException;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
+import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.util.FileUpdateMonitor;
+import org.jooq.lambda.Unchecked;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -146,9 +153,12 @@ public boolean close() {
public void handleUiCommands(List uiCommands) {
LOGGER.debug("Handling UI commands {}", uiCommands);
if (uiCommands.isEmpty()) {
+ checkForBibInUpperDir();
return;
}
+ assert !uiCommands.isEmpty();
+
// Handle blank workspace
boolean blank = uiCommands.stream().anyMatch(UiCommand.BlankWorkspace.class::isInstance);
@@ -180,6 +190,85 @@ public void handleUiCommands(List uiCommands) {
});
}
+ private void checkForBibInUpperDir() {
+ // "Open last edited databases" happened before this call
+ // Moreover, there is not any CLI command (especially, not opening any new tab)
+ // Thus, we check if there are any tabs open.
+ if (tabContainer.getLibraryTabs().isEmpty()) {
+ Optional firstBibFile = firstBibFile();
+ if (firstBibFile.isPresent()) {
+ ParserResult parserResult;
+ try {
+ parserResult = OpenDatabase.loadDatabase(
+ firstBibFile.get(),
+ preferences.getImportFormatPreferences(),
+ fileUpdateMonitor);
+ } catch (IOException e) {
+ LOGGER.error("Could not open bib file {}", firstBibFile.get(), e);
+ return;
+ }
+ List librariesToOpen = new ArrayList<>(1);
+ librariesToOpen.add(parserResult);
+ openDatabases(librariesToOpen);
+ }
+ }
+ }
+
+ /// Use case: User starts `JabRef.bat` or `JabRef.exe`. JabRef should open a "close by" bib file.
+ /// By "close by" a `.bib` file in the current folder or one level up of `JabRef.exe`is meant.
+ ///
+ /// Paths:
+ /// - `...\{example-dir}\JabRef\JabRef.exe` (Windows)
+ /// - `.../{example-dir}/JabRef/bin/JabRef` (Linux)
+ /// - `...\{example-dir}\JabRef\runtime\bin\JabRef.bat` (Windows)
+ ///
+ /// In the example, `...\{example-dir}\example.bib` should be found.
+ ///
+ /// We do NOT go up another level (i.e., everything in `...` is not found)
+ private Optional firstBibFile() {
+ Path absolutePath = Path.of(".").toAbsolutePath();
+ if (OS.LINUX && absolutePath.startsWith("/usr")) {
+ return Optional.empty();
+ }
+ if (OS.OS_X && absolutePath.startsWith("/Applications")) {
+ return Optional.empty();
+ }
+ if (OS.WINDOWS && absolutePath.startsWith("C:\\Program Files")) {
+ return Optional.empty();
+ }
+
+ boolean isJabRefExe = Files.exists(Path.of("JabRef.exe"));
+ boolean isJabRefBat = Files.exists(Path.of("JabRef.bat"));
+ boolean isJabRef = Files.exists(Path.of("JabRef"));
+
+ ArrayList dirsToCheck = new ArrayList<>(2);
+ dirsToCheck.add(Path.of(""));
+ if (isJabRefExe) {
+ dirsToCheck.add(Path.of("../")); // directory above `JabRef.exe` directory
+ } else if (isJabRefBat) {
+ dirsToCheck.add(Path.of("../../../")); // directory above `runtime\bin\JabRef.bat`
+ } else if (isJabRef) {
+ dirsToCheck.add(Path.of("../..(/")); // directory above `bin/JabRef` directory
+ }
+
+ // We want to check dirsToCheck only, not all subdirs (due to unnecessary disk i/o)
+ try {
+ return dirsToCheck.stream()
+ .map(Path::toAbsolutePath)
+ .flatMap(Unchecked.function(dir -> Files.list(dir)))
+ .filter(path -> FileUtil.getFileExtension(path).equals(Optional.of("bib")))
+ .findFirst();
+ } catch (UncheckedIOException ex) {
+ // Could be access denied exception - when this is started from the application directory
+ // Therefore log level "debug"
+ LOGGER.debug("Could not check for existing bib file {}", dirsToCheck, ex);
+ return Optional.empty();
+ }
+ }
+
+ /// Opens the libraries given in `parserResults`. This list needs to be modifiable, because invalidDatabases are removed.
+ ///
+ /// @param parserResults A modifiable list of parser results
private void openDatabases(List parserResults) {
final List toOpenTab = new ArrayList<>();
@@ -220,11 +309,10 @@ private void openDatabases(List parserResults) {
undoManager,
clipBoardManager,
taskExecutor);
- } catch (
- SQLException |
- DatabaseNotSupportedException |
- InvalidDBMSConnectionPropertiesException |
- NotASharedDatabaseException e) {
+ } catch (SQLException
+ | DatabaseNotSupportedException
+ | InvalidDBMSConnectionPropertiesException
+ | NotASharedDatabaseException e) {
LOGGER.error("Connection error", e);
dialogService.showErrorDialogAndWait(
Localization.lang("Connection error"),
diff --git a/src/main/java/org/jabref/gui/frame/OpenConsoleAction.java b/src/main/java/org/jabref/gui/frame/OpenConsoleAction.java
index c29c86a5d02..c60451c37f0 100644
--- a/src/main/java/org/jabref/gui/frame/OpenConsoleAction.java
+++ b/src/main/java/org/jabref/gui/frame/OpenConsoleAction.java
@@ -35,7 +35,7 @@ public OpenConsoleAction(Supplier databaseContext, StateMana
this.preferences = preferences;
this.dialogService = dialogService;
- this.executable.bind(ActionHelper.needsDatabase(stateManager));
+ this.executable.bind(ActionHelper.needsSavedLocalDatabase(stateManager));
}
/**
diff --git a/src/main/java/org/jabref/gui/icon/IconTheme.java b/src/main/java/org/jabref/gui/icon/IconTheme.java
index aa6cff98462..3788b551b18 100644
--- a/src/main/java/org/jabref/gui/icon/IconTheme.java
+++ b/src/main/java/org/jabref/gui/icon/IconTheme.java
@@ -283,6 +283,7 @@ public enum JabRefIcons implements JabRefIcon {
APPLICATION_SUBLIMETEXT(JabRefMaterialDesignIcon.SUBLIME_TEXT),
APPLICATION_TEXSHOP(JabRefMaterialDesignIcon.TEXSHOP),
APPLICATION_TEXWORS(JabRefMaterialDesignIcon.TEXWORKS),
+ APPLICATION_VSCODE(JabRefMaterialDesignIcon.VSCODE),
KEY_BINDINGS(MaterialDesignK.KEYBOARD),
FIND_DUPLICATES(MaterialDesignC.CODE_EQUAL),
CONNECT_DB(MaterialDesignC.CLOUD_UPLOAD),
diff --git a/src/main/java/org/jabref/gui/importer/GrobidOptInDialogHelper.java b/src/main/java/org/jabref/gui/importer/GrobidUseDialogHelper.java
similarity index 63%
rename from src/main/java/org/jabref/gui/importer/GrobidOptInDialogHelper.java
rename to src/main/java/org/jabref/gui/importer/GrobidUseDialogHelper.java
index 6ef67bc17a0..352f37b7a85 100644
--- a/src/main/java/org/jabref/gui/importer/GrobidOptInDialogHelper.java
+++ b/src/main/java/org/jabref/gui/importer/GrobidUseDialogHelper.java
@@ -7,29 +7,31 @@
/**
* Metadata extraction from PDFs and plaintext works very well using Grobid, but we do not want to enable it by default
* due to data privacy concerns.
- * To make users aware of the feature, we ask each time before querying Grobid, giving the option to opt-out.
+ * To make users aware of the feature, we ask before querying for the first time Grobid, saving the users' preference
*/
-public class GrobidOptInDialogHelper {
+public class GrobidUseDialogHelper {
/**
- * If Grobid is not enabled but the user has not explicitly opted-out of Grobid, we ask for permission to send data
- * to Grobid using a dialog and giving an opt-out option.
+ * If the user has not explicitly opted-in/out of Grobid, we ask for permission to send data to Grobid by using
+ * a dialog. The users' preference is saved.
*
* @param dialogService the DialogService to use
* @return if the user enabled Grobid, either in the past or after being asked by the dialog.
*/
public static boolean showAndWaitIfUserIsUndecided(DialogService dialogService, GrobidPreferences preferences) {
+ if (preferences.isGrobidUseAsked()) {
+ return preferences.isGrobidEnabled();
+ }
if (preferences.isGrobidEnabled()) {
+ preferences.setGrobidUseAsked(true);
return true;
}
- if (preferences.isGrobidOptOut()) {
- return false;
- }
- boolean grobidEnabled = dialogService.showConfirmationDialogWithOptOutAndWait(
+ boolean grobidEnabled = dialogService.showConfirmationDialogAndWait(
Localization.lang("Remote services"),
Localization.lang("Allow sending PDF files and raw citation strings to a JabRef online service (Grobid) to determine Metadata. This produces better results."),
- Localization.lang("Do not ask again"),
- optOut -> preferences.setGrobidOptOut(optOut));
+ Localization.lang("Send to Grobid"),
+ Localization.lang("Do not send"));
+ preferences.setGrobidUseAsked(true);
preferences.setGrobidEnabled(grobidEnabled);
return grobidEnabled;
}
diff --git a/src/main/java/org/jabref/gui/importer/ImportCommand.java b/src/main/java/org/jabref/gui/importer/ImportCommand.java
index 55469924eb9..85fc638b174 100644
--- a/src/main/java/org/jabref/gui/importer/ImportCommand.java
+++ b/src/main/java/org/jabref/gui/importer/ImportCommand.java
@@ -109,7 +109,7 @@ private void importSingleFile(Path file, SortedSet importers, FileChoo
if (importMethod == ImportMethod.AS_NEW) {
task.onSuccess(parserResult -> {
tabContainer.addTab(parserResult.getDatabaseContext(), true);
- dialogService.notify(Localization.lang("Imported entries") + ": " + parserResult.getDatabase().getEntries().size());
+ dialogService.notify(Localization.lang("%0 entry(s) imported", parserResult.getDatabase().getEntries().size()));
})
.onFailure(ex -> {
LOGGER.error("Error importing", ex);
@@ -144,7 +144,7 @@ private ParserResult doImport(List files, Importer importFormat) throws IO
if (importer.isEmpty()) {
// Unknown format
UiTaskExecutor.runAndWaitInJavaFXThread(() -> {
- if (FileUtil.isPDFFile(filename) && GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(dialogService, preferences.getGrobidPreferences())) {
+ if (FileUtil.isPDFFile(filename) && GrobidUseDialogHelper.showAndWaitIfUserIsUndecided(dialogService, preferences.getGrobidPreferences())) {
importFormatReader.reset();
}
dialogService.notify(Localization.lang("Importing file %0 as unknown format", filename.getFileName().toString()));
@@ -155,7 +155,7 @@ private ParserResult doImport(List files, Importer importFormat) throws IO
UiTaskExecutor.runAndWaitInJavaFXThread(() -> {
if (((importer.get() instanceof PdfGrobidImporter)
|| (importer.get() instanceof PdfMergeMetadataImporter))
- && GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(dialogService, preferences.getGrobidPreferences())) {
+ && GrobidUseDialogHelper.showAndWaitIfUserIsUndecided(dialogService, preferences.getGrobidPreferences())) {
importFormatReader.reset();
}
dialogService.notify(Localization.lang("Importing in %0 format", importer.get().getName()) + "...");
diff --git a/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java b/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java
index 38348315456..8dc031f249a 100644
--- a/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java
+++ b/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java
@@ -149,11 +149,6 @@ public void importEntries(List entriesToImport, boolean shouldDownload
parserResult.getMetaData(),
parserResult.getPath().map(path -> path.getFileName().toString()).orElse("unknown"),
parserResult.getDatabase().getEntries());
-
- buildImportHandlerThenImportEntries(entriesToImport);
- }
-
- private void buildImportHandlerThenImportEntries(List entriesToImport) {
ImportHandler importHandler = new ImportHandler(
selectedDb.getValue(),
preferences,
diff --git a/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesViewModel.java b/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesViewModel.java
index 2e28d8021fd..93edf244bd5 100644
--- a/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesViewModel.java
+++ b/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesViewModel.java
@@ -2,7 +2,9 @@
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Optional;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
@@ -18,6 +20,7 @@
import org.jabref.gui.libraryproperties.PropertiesTabViewModel;
import org.jabref.gui.util.DirectoryDialogConfiguration;
import org.jabref.logic.l10n.Encodings;
+import org.jabref.logic.l10n.Localization;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.shared.DatabaseLocation;
import org.jabref.model.database.BibDatabaseContext;
@@ -40,16 +43,12 @@ public class GeneralPropertiesViewModel implements PropertiesTabViewModel {
private final BibDatabaseContext databaseContext;
private final MetaData metaData;
- private final DirectoryDialogConfiguration directoryDialogConfiguration;
GeneralPropertiesViewModel(BibDatabaseContext databaseContext, DialogService dialogService, CliPreferences preferences) {
this.dialogService = dialogService;
this.preferences = preferences;
this.databaseContext = databaseContext;
this.metaData = databaseContext.getMetaData();
-
- this.directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder()
- .withInitialDirectory(preferences.getFilePreferences().getWorkingDirectory()).build();
}
@Override
@@ -96,16 +95,22 @@ public void storeSettings() {
}
public void browseLibrarySpecificDir() {
+ DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder()
+ .withInitialDirectory(getBrowseDirectory(librarySpecificDirectoryProperty.getValue())).build();
dialogService.showDirectorySelectionDialog(directoryDialogConfiguration)
.ifPresent(dir -> librarySpecificDirectoryProperty.setValue(dir.toAbsolutePath().toString()));
}
public void browseUserDir() {
+ DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder()
+ .withInitialDirectory(getBrowseDirectory(userSpecificFileDirectoryProperty.getValue())).build();
dialogService.showDirectorySelectionDialog(directoryDialogConfiguration)
.ifPresent(dir -> userSpecificFileDirectoryProperty.setValue(dir.toAbsolutePath().toString()));
}
public void browseLatexDir() {
+ DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder()
+ .withInitialDirectory(getBrowseDirectory(laTexFileDirectoryProperty.getValue())).build();
dialogService.showDirectorySelectionDialog(directoryDialogConfiguration)
.ifPresent(dir -> laTexFileDirectoryProperty.setValue(dir.toAbsolutePath().toString()));
}
@@ -141,4 +146,19 @@ public StringProperty userSpecificFileDirectoryProperty() {
public StringProperty laTexFileDirectoryProperty() {
return this.laTexFileDirectoryProperty;
}
+
+ private Path getBrowseDirectory(String configuredDir) {
+ if (configuredDir.isEmpty()) {
+ return preferences.getFilePreferences().getWorkingDirectory();
+ }
+ Optional foundPath = this.databaseContext.getFileDirectories(preferences.getFilePreferences()).stream()
+ .filter(path -> path.toString().endsWith(configuredDir))
+ .filter(Files::exists).findFirst();
+
+ if (foundPath.isEmpty()) {
+ dialogService.notify(Localization.lang("Path %0 could not be resolved. Using working dir.", configuredDir));
+ return preferences.getFilePreferences().getWorkingDirectory();
+ }
+ return foundPath.get();
+ }
}
diff --git a/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialogViewModel.java b/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialogViewModel.java
index cd31283742c..82863bc6c5b 100644
--- a/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialogViewModel.java
+++ b/src/main/java/org/jabref/gui/linkedfile/LinkedFileEditDialogViewModel.java
@@ -1,7 +1,9 @@
package org.jabref.gui.linkedfile;
+import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.regex.Pattern;
@@ -22,15 +24,22 @@
import org.jabref.gui.frame.ExternalApplicationsPreferences;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.logic.FilePreferences;
+import org.jabref.logic.l10n.Localization;
+import org.jabref.logic.util.io.FileNameCleaner;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.LinkedFile;
+import com.google.common.annotations.VisibleForTesting;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.optional.ObservableOptionalValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class LinkedFileEditDialogViewModel extends AbstractViewModel {
+ private static final Logger LOGGER = LoggerFactory.getLogger(LinkedFileEditDialogViewModel.class);
+
private static final Pattern REMOTE_LINK_PATTERN = Pattern.compile("[a-z]+://.*");
private final StringProperty link = new SimpleStringProperty("");
private final StringProperty description = new SimpleStringProperty("");
@@ -86,13 +95,31 @@ public void openBrowseDialog() {
.withInitialFileName(fileName)
.build();
- dialogService.showFileOpenDialog(fileDialogConfiguration).ifPresent(path -> {
- // Store the directory for next time:
- filePreferences.setWorkingDirectory(path);
- link.set(relativize(path));
+ dialogService.showFileOpenDialog(fileDialogConfiguration).ifPresent(this::checkForBadFileNameAndAdd);
+ }
- setExternalFileTypeByExtension(link.getValueSafe());
- });
+ @VisibleForTesting
+ void checkForBadFileNameAndAdd(Path fileToAdd) {
+ if (FileUtil.detectBadFileName(fileToAdd.toString())) {
+ String newFilename = FileNameCleaner.cleanFileName(fileToAdd.getFileName().toString());
+
+ boolean correctButtonPressed = dialogService.showConfirmationDialogAndWait(Localization.lang("File \"%0\" cannot be added!", fileToAdd.getFileName()),
+ Localization.lang("Illegal characters in the file name detected.\nFile will be renamed to \"%0\" and added.", newFilename),
+ Localization.lang("Rename and add"));
+
+ if (correctButtonPressed) {
+ Path correctPath = fileToAdd.resolveSibling(newFilename);
+ try {
+ Files.move(fileToAdd, correctPath);
+ link.set(relativize(correctPath));
+ filePreferences.setWorkingDirectory(correctPath);
+ setExternalFileTypeByExtension(link.getValueSafe());
+ } catch (IOException ex) {
+ LOGGER.error("Error moving file", ex);
+ dialogService.showErrorDialogAndWait(ex);
+ }
+ }
+ }
}
public void setValues(LinkedFile linkedFile) {
diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java
index 4d66704856f..accc3879fd3 100644
--- a/src/main/java/org/jabref/gui/maintable/MainTable.java
+++ b/src/main/java/org/jabref/gui/maintable/MainTable.java
@@ -46,19 +46,16 @@
import org.jabref.gui.util.ControlHelper;
import org.jabref.gui.util.CustomLocalDragboard;
import org.jabref.gui.util.DragDrop;
-import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.gui.util.ViewModelTableRowFactory;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.citationstyle.CitationStyleOutputFormat;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
-import org.jabref.model.database.event.EntriesAddedEvent;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryTypesManager;
import com.airhacks.afterburner.injection.Injector;
-import com.google.common.eventbus.Subscribe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -246,11 +243,6 @@ private void jumpToSearchKey(TableColumn sortedColumn
});
}
- @Subscribe
- public void listen(EntriesAddedEvent event) {
- UiTaskExecutor.runInJavaFXThread(() -> clearAndSelect(event.getFirstEntry()));
- }
-
public void clearAndSelect(BibEntry bibEntry) {
// check if entries merged from citation relations tab
if (citationMergeMode) {
@@ -490,10 +482,7 @@ public List getSelectedEntries() {
}
private Optional findEntry(BibEntry entry) {
- return model.getEntriesFilteredAndSorted()
- .stream()
- .filter(viewModel -> viewModel.getEntry().equals(entry))
- .findFirst();
+ return model.getViewModelByIndex(database.getDatabase().indexOf(entry));
}
public void setCitationMergeMode(boolean citationMerge) {
diff --git a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java
index d4d9b8fb576..e0fddf29c2b 100644
--- a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java
+++ b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java
@@ -80,7 +80,7 @@ public MainTableColumnFactory(BibDatabaseContext database,
this.undoManager = undoManager;
this.stateManager = stateManager;
ThemeManager themeManager = Injector.instantiateModelOrService(ThemeManager.class);
- this.tooltip = new MainTableTooltip(database, dialogService, preferences, themeManager, taskExecutor);
+ this.tooltip = new MainTableTooltip(dialogService, preferences, themeManager, taskExecutor);
}
public TableColumn createColumn(MainTableColumnModel column) {
@@ -120,12 +120,12 @@ public MainTableColumnFactory(BibDatabaseContext database,
}
}
break;
- default:
case NORMALFIELD:
if (!column.getQualifier().isBlank()) {
returnColumn = createFieldColumn(column, tooltip);
}
break;
+ default:
}
return returnColumn;
}
diff --git a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java
index 4c4d6a11c75..e758516ade9 100644
--- a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java
+++ b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java
@@ -38,10 +38,13 @@
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.Subscription;
import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import static org.jabref.model.search.PostgreConstants.ENTRY_ID;
public class MainTableDataModel {
+ private final Logger LOGGER = LoggerFactory.getLogger(MainTableDataModel.class);
private final ObservableList entriesViewModel;
private final FilteredList entriesFiltered;
@@ -195,6 +198,14 @@ public SortedList getEntriesFilteredAndSorted() {
return entriesFilteredAndSorted;
}
+ public Optional getViewModelByIndex(int index) {
+ if (index < 0 || index >= entriesViewModel.size()) {
+ LOGGER.warn("Tried to access out of bounds index {} in entriesViewModel", index);
+ return Optional.empty();
+ }
+ return Optional.of(entriesViewModel.get(index));
+ }
+
public void resetFieldFormatter() {
this.fieldValueFormatter.setValue(new MainTableFieldValueFormatter(nameDisplayPreferences, bibDatabaseContext));
}
diff --git a/src/main/java/org/jabref/gui/maintable/MainTableTooltip.java b/src/main/java/org/jabref/gui/maintable/MainTableTooltip.java
index 941f96dcbd3..b39705744ca 100644
--- a/src/main/java/org/jabref/gui/maintable/MainTableTooltip.java
+++ b/src/main/java/org/jabref/gui/maintable/MainTableTooltip.java
@@ -20,17 +20,18 @@ public class MainTableTooltip extends Tooltip {
private final VBox tooltipContent = new VBox();
private final Label fieldValueLabel = new Label();
- public MainTableTooltip(BibDatabaseContext databaseContext, DialogService dialogService, GuiPreferences preferences, ThemeManager themeManager, TaskExecutor taskExecutor) {
+ public MainTableTooltip(DialogService dialogService, GuiPreferences preferences, ThemeManager themeManager, TaskExecutor taskExecutor) {
this.preferences = preferences;
- this.preview = new PreviewViewer(databaseContext, dialogService, preferences, themeManager, taskExecutor);
+ this.preview = new PreviewViewer(dialogService, preferences, themeManager, taskExecutor);
this.setShowDelay(Duration.seconds(1));
this.tooltipContent.getChildren().addAll(fieldValueLabel, preview);
}
- public Tooltip createTooltip(BibEntry entry, String fieldValue) {
+ public Tooltip createTooltip(BibDatabaseContext databaseContext, BibEntry entry, String fieldValue) {
fieldValueLabel.setText(fieldValue + "\n");
if (preferences.getPreviewPreferences().shouldShowPreviewEntryTableTooltip()) {
preview.setLayout(preferences.getPreviewPreferences().getSelectedPreviewLayout());
+ preview.setDatabaseContext(databaseContext);
preview.setEntry(entry);
this.setGraphic(tooltipContent);
} else {
diff --git a/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java b/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java
index 4096455c765..74a6345cc71 100644
--- a/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java
+++ b/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java
@@ -17,6 +17,7 @@
* This resize policy is almost the same as {@link TableView#CONSTRAINED_RESIZE_POLICY}
* We make sure that the width of all columns sums up to the total width of the table.
* However, in contrast to {@link TableView#CONSTRAINED_RESIZE_POLICY} we size the columns initially by their preferred width.
+ * Although {@link TableView#CONSTRAINED_RESIZE_POLICY} is deprecated, this policy maintains a similar resizing behavior.
*/
public class SmartConstrainedResizePolicy implements Callback {
diff --git a/src/main/java/org/jabref/gui/maintable/columns/FieldColumn.java b/src/main/java/org/jabref/gui/maintable/columns/FieldColumn.java
index e272467f409..512b9906951 100644
--- a/src/main/java/org/jabref/gui/maintable/columns/FieldColumn.java
+++ b/src/main/java/org/jabref/gui/maintable/columns/FieldColumn.java
@@ -67,6 +67,6 @@ private ObservableValue getFieldValue(BibEntryTableViewModel entry) {
}
private Tooltip createTooltip(BibEntryTableViewModel entry, String fieldValue) {
- return tooltip.createTooltip(entry.getEntry(), fieldValue);
+ return tooltip.createTooltip(entry.getBibDatabaseContext(), entry.getEntry(), fieldValue);
}
}
diff --git a/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java b/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java
index ce1fc758ef3..974b5fd111e 100644
--- a/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java
+++ b/src/main/java/org/jabref/gui/maintable/columns/FileColumn.java
@@ -98,8 +98,35 @@ public FileColumn(MainTableColumnModel model,
.getGraphicNode());
new ValueTableCellFactory>()
- .withGraphic((entry, linkedFiles) -> createFileIcon(entry, linkedFiles.stream().filter(linkedFile ->
- linkedFile.getFileType().equalsIgnoreCase(fileType)).collect(Collectors.toList())))
+ .withGraphic((entry, linkedFiles) -> createFileIcon(entry, linkedFiles.stream()
+ .filter(linkedFile -> linkedFile.getFileType().equalsIgnoreCase(fileType))
+ .collect(Collectors.toList())))
+ .withOnMouseClickedEvent((entry, linkedFiles) -> event -> {
+ List filteredFiles = linkedFiles.stream()
+ .filter(linkedFile -> linkedFile.getFileType().equalsIgnoreCase(fileType))
+ .collect(Collectors.toList());
+
+ if (event.getButton() == MouseButton.PRIMARY) {
+ if (filteredFiles.size() == 1) {
+ // Only one file - open directly
+ LinkedFileViewModel linkedFileViewModel = new LinkedFileViewModel(filteredFiles.getFirst(),
+ entry.getEntry(), database, taskExecutor, dialogService, preferences);
+ linkedFileViewModel.open();
+ } else if (filteredFiles.size() > 1) {
+ // Multiple files - show context menu to choose file
+ ContextMenu contextMenu = new ContextMenu();
+ for (LinkedFile linkedFile : filteredFiles) {
+ LinkedFileViewModel linkedFileViewModel = new LinkedFileViewModel(linkedFile,
+ entry.getEntry(), database, taskExecutor, dialogService, preferences);
+ MenuItem menuItem = new MenuItem(linkedFileViewModel.getTruncatedDescriptionAndLink(),
+ linkedFileViewModel.getTypeIcon().getGraphicNode());
+ menuItem.setOnAction(e -> linkedFileViewModel.open());
+ contextMenu.getItems().add(menuItem);
+ }
+ contextMenu.show(((Node) event.getSource()), event.getScreenX(), event.getScreenY());
+ }
+ }
+ })
.install(this);
}
diff --git a/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java b/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java
index e907147c684..3f68a89608c 100644
--- a/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java
+++ b/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java
@@ -215,7 +215,8 @@ private void addStyleFile() {
}
private PreviewViewer initializePreviewViewer(BibEntry entry) {
- PreviewViewer viewer = new PreviewViewer(new BibDatabaseContext(), dialogService, preferences, themeManager, taskExecutor);
+ PreviewViewer viewer = new PreviewViewer(dialogService, preferences, themeManager, taskExecutor);
+ viewer.setDatabaseContext(new BibDatabaseContext());
viewer.setEntry(entry);
return viewer;
}
diff --git a/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogViewModel.java b/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogViewModel.java
index 82befda6b05..1cca1235fd4 100644
--- a/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogViewModel.java
+++ b/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogViewModel.java
@@ -138,8 +138,7 @@ public void editStyle() {
Optional type = ExternalFileTypes.getExternalFileTypeByExt("jstyle", externalApplicationsPreferences);
try {
NativeDesktop.openExternalFileAnyFormat(new BibDatabaseContext(), externalApplicationsPreferences, filePreferences, jStyle.getPath(), type);
- } catch (
- IOException e) {
+ } catch (IOException e) {
dialogService.showErrorDialogAndWait(e);
}
}
diff --git a/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
index a72b8c66d3a..f2b21abd906 100644
--- a/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
+++ b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
@@ -9,6 +9,7 @@
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
+import javafx.stage.Stage;
import org.jabref.gui.ClipBoardManager;
import org.jabref.gui.DialogService;
@@ -53,6 +54,10 @@ public PlainCitationParserDialog() {
.setAsDialogPane(this);
this.setTitle(Localization.lang("Plain Citations Parser"));
+
+ Stage stage = (Stage) getDialogPane().getScene().getWindow();
+ stage.setMinHeight(550);
+ stage.setMinWidth(500);
}
@FXML
diff --git a/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java b/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java
index 07bb423173b..2ab75d18cb7 100644
--- a/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java
+++ b/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java
@@ -139,6 +139,7 @@ public class JabRefGuiPreferences extends JabRefCliPreferences implements GuiPre
private static final String PUSH_VIM_SERVER = "vimServer";
private static final String PUSH_VIM = "vim";
private static final String PUSH_SUBLIME_TEXT_PATH = "sublimeTextPath";
+ private static final String PUSH_VSCODE_PATH = "VScodePath";
// endregion
// region NameDisplayPreferences
@@ -360,6 +361,7 @@ private JabRefGuiPreferences() {
defaults.put(PUSH_VIM, "vim");
defaults.put(PUSH_VIM_SERVER, "vim");
defaults.put(PUSH_EMACS_ADDITIONAL_PARAMETERS, "-n -e");
+ defaults.put(PUSH_VSCODE_PATH, OS.detectProgramPath("Code", "Microsoft VS Code"));
if (OS.OS_X) {
defaults.put(PUSH_EMACS_PATH, "emacsclient");
@@ -937,6 +939,7 @@ public PushToApplicationPreferences getPushToApplicationPreferences() {
applicationCommands.put(PushToApplications.VIM, getEmptyIsDefault(PUSH_VIM));
applicationCommands.put(PushToApplications.WIN_EDT, getEmptyIsDefault(PUSH_WINEDT_PATH));
applicationCommands.put(PushToApplications.SUBLIME_TEXT, getEmptyIsDefault(PUSH_SUBLIME_TEXT_PATH));
+ applicationCommands.put(PushToApplications.VSCODE, getEmptyIsDefault(PUSH_VSCODE_PATH));
pushToApplicationPreferences = new PushToApplicationPreferences(
get(PUSH_TO_APPLICATION),
@@ -971,6 +974,8 @@ private void storePushToApplicationPath(Map commandPair) {
put(PUSH_WINEDT_PATH, value);
case PushToApplications.SUBLIME_TEXT ->
put(PUSH_SUBLIME_TEXT_PATH, value);
+ case PushToApplications.VSCODE ->
+ put(PUSH_VSCODE_PATH, value);
}
});
}
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml b/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
index b81bd96ba06..64e737747f5 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
@@ -16,6 +16,9 @@
+
+
+
-
-
@@ -235,5 +234,37 @@
glyph="REFRESH"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTab.java b/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
index efa406a1d27..1646e57420e 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
@@ -1,11 +1,13 @@
package org.jabref.gui.preferences.ai;
import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
+import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import org.jabref.gui.actions.ActionFactory;
@@ -14,13 +16,13 @@
import org.jabref.gui.preferences.AbstractPreferenceTabView;
import org.jabref.gui.preferences.PreferencesTab;
import org.jabref.gui.util.ViewModelListCellFactory;
+import org.jabref.logic.ai.templates.AiTemplate;
import org.jabref.logic.help.HelpFile;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.ai.AiProvider;
import org.jabref.model.ai.EmbeddingModel;
import com.airhacks.afterburner.views.ViewLoader;
-import com.dlsc.gemsfx.ResizableTextArea;
import com.dlsc.unitfx.IntegerInputField;
import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer;
import org.controlsfx.control.SearchableComboBox;
@@ -28,6 +30,7 @@
public class AiTab extends AbstractPreferenceTabView implements PreferencesTab {
private static final String HUGGING_FACE_CHAT_MODEL_PROMPT = "TinyLlama/TinyLlama_v1.1 (or any other model name)";
+ private static final String GPT_4_ALL_CHAT_MODEL_PROMPT = "Phi-3.1-mini (or any other local model name from GPT4All)";
@FXML private CheckBox enableAi;
@FXML private CheckBox autoGenerateEmbeddings;
@@ -41,7 +44,6 @@ public class AiTab extends AbstractPreferenceTabView implements
@FXML private TextField apiBaseUrlTextField;
@FXML private SearchableComboBox embeddingModelComboBox;
- @FXML private ResizableTextArea instructionTextArea;
@FXML private TextField temperatureTextField;
@FXML private IntegerInputField contextWindowSizeTextField;
@FXML private IntegerInputField documentSplitterChunkSizeTextField;
@@ -49,8 +51,14 @@ public class AiTab extends AbstractPreferenceTabView implements
@FXML private IntegerInputField ragMaxResultsCountTextField;
@FXML private TextField ragMinScoreTextField;
+ @FXML private TextArea systemMessageTextArea;
+ @FXML private TextArea userMessageTextArea;
+ @FXML private TextArea summarizationChunkTextArea;
+ @FXML private TextArea summarizationCombineTextArea;
+
@FXML private Button generalSettingsHelp;
@FXML private Button expertSettingsHelp;
+ @FXML private Button templatesHelp;
private final ControlsFxVisualizer visualizer = new ControlsFxVisualizer();
@@ -72,14 +80,14 @@ public void initialize() {
new ViewModelListCellFactory()
.withText(AiProvider::toString)
.install(aiProviderComboBox);
- aiProviderComboBox.setItems(viewModel.aiProvidersProperty());
+ aiProviderComboBox.itemsProperty().bind(viewModel.aiProvidersProperty());
aiProviderComboBox.valueProperty().bindBidirectional(viewModel.selectedAiProviderProperty());
aiProviderComboBox.disableProperty().bind(viewModel.disableBasicSettingsProperty());
new ViewModelListCellFactory()
.withText(text -> text)
.install(chatModelComboBox);
- chatModelComboBox.setItems(viewModel.chatModelsProperty());
+ chatModelComboBox.itemsProperty().bind(viewModel.chatModelsProperty());
chatModelComboBox.valueProperty().bindBidirectional(viewModel.selectedChatModelProperty());
chatModelComboBox.disableProperty().bind(viewModel.disableBasicSettingsProperty());
@@ -87,10 +95,19 @@ public void initialize() {
if (newValue == AiProvider.HUGGING_FACE) {
chatModelComboBox.setPromptText(HUGGING_FACE_CHAT_MODEL_PROMPT);
}
+ if (newValue == AiProvider.GPT4ALL) {
+ chatModelComboBox.setPromptText(GPT_4_ALL_CHAT_MODEL_PROMPT);
+ }
});
apiKeyTextField.textProperty().bindBidirectional(viewModel.apiKeyProperty());
- apiKeyTextField.disableProperty().bind(viewModel.disableBasicSettingsProperty());
+ // Disable if GPT4ALL is selected
+ apiKeyTextField.disableProperty().bind(
+ Bindings.or(
+ viewModel.disableBasicSettingsProperty(),
+ aiProviderComboBox.valueProperty().isEqualTo(AiProvider.GPT4ALL)
+ )
+ );
customizeExpertSettingsCheckbox.selectedProperty().bindBidirectional(viewModel.customizeExpertSettingsProperty());
customizeExpertSettingsCheckbox.disableProperty().bind(viewModel.disableBasicSettingsProperty());
@@ -112,9 +129,6 @@ public void initialize() {
apiBaseUrlTextField.setDisable(newValue || viewModel.disableExpertSettingsProperty().get())
);
- instructionTextArea.textProperty().bindBidirectional(viewModel.instructionProperty());
- instructionTextArea.disableProperty().bind(viewModel.disableExpertSettingsProperty());
-
// bindBidirectional doesn't work well with number input fields ({@link IntegerInputField}, {@link DoubleInputField}),
// so they are expanded into `addListener` calls.
@@ -169,7 +183,6 @@ public void initialize() {
visualizer.initVisualization(viewModel.getChatModelValidationStatus(), chatModelComboBox);
visualizer.initVisualization(viewModel.getApiBaseUrlValidationStatus(), apiBaseUrlTextField);
visualizer.initVisualization(viewModel.getEmbeddingModelValidationStatus(), embeddingModelComboBox);
- visualizer.initVisualization(viewModel.getSystemMessageValidationStatus(), instructionTextArea);
visualizer.initVisualization(viewModel.getTemperatureTypeValidationStatus(), temperatureTextField);
visualizer.initVisualization(viewModel.getTemperatureRangeValidationStatus(), temperatureTextField);
visualizer.initVisualization(viewModel.getMessageWindowSizeValidationStatus(), contextWindowSizeTextField);
@@ -180,9 +193,15 @@ public void initialize() {
visualizer.initVisualization(viewModel.getRagMinScoreRangeValidationStatus(), ragMinScoreTextField);
});
+ systemMessageTextArea.textProperty().bindBidirectional(viewModel.getTemplateSources().get(AiTemplate.CHATTING_SYSTEM_MESSAGE));
+ userMessageTextArea.textProperty().bindBidirectional(viewModel.getTemplateSources().get(AiTemplate.CHATTING_USER_MESSAGE));
+ summarizationChunkTextArea.textProperty().bindBidirectional(viewModel.getTemplateSources().get(AiTemplate.SUMMARIZATION_CHUNK));
+ summarizationCombineTextArea.textProperty().bindBidirectional(viewModel.getTemplateSources().get(AiTemplate.SUMMARIZATION_COMBINE));
+
ActionFactory actionFactory = new ActionFactory();
actionFactory.configureIconButton(StandardActions.HELP, new HelpAction(HelpFile.AI_GENERAL_SETTINGS, dialogService, preferences.getExternalApplicationsPreferences()), generalSettingsHelp);
actionFactory.configureIconButton(StandardActions.HELP, new HelpAction(HelpFile.AI_EXPERT_SETTINGS, dialogService, preferences.getExternalApplicationsPreferences()), expertSettingsHelp);
+ actionFactory.configureIconButton(StandardActions.HELP, new HelpAction(HelpFile.AI_TEMPLATES, dialogService, preferences.getExternalApplicationsPreferences()), templatesHelp);
}
@Override
@@ -195,6 +214,11 @@ private void onResetExpertSettingsButtonClick() {
viewModel.resetExpertSettings();
}
+ @FXML
+ private void onResetTemplatesButtonClick() {
+ viewModel.resetTemplates();
+ }
+
public ReadOnlyBooleanProperty aiEnabledProperty() {
return enableAi.selectedProperty();
}
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
index c1c4ef5f6a5..c47da76d38b 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
@@ -1,7 +1,9 @@
package org.jabref.gui.preferences.ai;
+import java.util.Arrays;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import javafx.beans.property.BooleanProperty;
@@ -20,6 +22,7 @@
import org.jabref.gui.preferences.PreferenceTabViewModel;
import org.jabref.logic.ai.AiDefaultPreferences;
import org.jabref.logic.ai.AiPreferences;
+import org.jabref.logic.ai.templates.AiTemplate;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.util.LocalizedNumbers;
@@ -54,6 +57,7 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final StringProperty mistralAiChatModel = new SimpleStringProperty();
private final StringProperty geminiChatModel = new SimpleStringProperty();
private final StringProperty huggingFaceChatModel = new SimpleStringProperty();
+ private final StringProperty gpt4AllChatModel = new SimpleStringProperty();
private final StringProperty currentApiKey = new SimpleStringProperty();
@@ -61,6 +65,7 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final StringProperty mistralAiApiKey = new SimpleStringProperty();
private final StringProperty geminiAiApiKey = new SimpleStringProperty();
private final StringProperty huggingFaceApiKey = new SimpleStringProperty();
+ private final StringProperty gpt4AllApiKey = new SimpleStringProperty();
private final BooleanProperty customizeExpertSettings = new SimpleBooleanProperty();
@@ -75,8 +80,15 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final StringProperty mistralAiApiBaseUrl = new SimpleStringProperty();
private final StringProperty geminiApiBaseUrl = new SimpleStringProperty();
private final StringProperty huggingFaceApiBaseUrl = new SimpleStringProperty();
+ private final StringProperty gpt4AllApiBaseUrl = new SimpleStringProperty();
+
+ private final Map templateSources = Map.of(
+ AiTemplate.CHATTING_SYSTEM_MESSAGE, new SimpleStringProperty(),
+ AiTemplate.CHATTING_USER_MESSAGE, new SimpleStringProperty(),
+ AiTemplate.SUMMARIZATION_CHUNK, new SimpleStringProperty(),
+ AiTemplate.SUMMARIZATION_COMBINE, new SimpleStringProperty()
+ );
- private final StringProperty instruction = new SimpleStringProperty();
private final StringProperty temperature = new SimpleStringProperty();
private final IntegerProperty contextWindowSize = new SimpleIntegerProperty();
private final IntegerProperty documentSplitterChunkSize = new SimpleIntegerProperty();
@@ -93,7 +105,6 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final Validator chatModelValidator;
private final Validator apiBaseUrlValidator;
private final Validator embeddingModelValidator;
- private final Validator instructionValidator;
private final Validator temperatureTypeValidator;
private final Validator temperatureRangeValidator;
private final Validator contextWindowSizeValidator;
@@ -114,20 +125,20 @@ public AiTabViewModel(CliPreferences preferences) {
});
this.customizeExpertSettings.addListener((observableValue, oldValue, newValue) ->
- disableExpertSettings.set(!newValue || !enableAi.get())
+ disableExpertSettings.set(!newValue || !enableAi.get())
);
this.selectedAiProvider.addListener((observable, oldValue, newValue) -> {
List models = AiDefaultPreferences.getAvailableModels(newValue);
+ disableApiBaseUrl.set(newValue == AiProvider.HUGGING_FACE || newValue == AiProvider.GEMINI);
+
// When we setAll on Hugging Face, models are empty, and currentChatModel become null.
- // It becomes null beause currentChatModel is binded to combobox, and this combobox becomes empty.
+ // It becomes null because currentChatModel is bound to combobox, and this combobox becomes empty.
// For some reason, custom edited value in the combobox will be erased, so we need to store the old value.
String oldChatModel = currentChatModel.get();
chatModelsList.setAll(models);
- disableApiBaseUrl.set(newValue == AiProvider.HUGGING_FACE || newValue == AiProvider.GEMINI);
-
if (oldValue != null) {
switch (oldValue) {
case OPEN_AI -> {
@@ -150,6 +161,11 @@ public AiTabViewModel(CliPreferences preferences) {
huggingFaceApiKey.set(currentApiKey.get());
huggingFaceApiBaseUrl.set(currentApiBaseUrl.get());
}
+ case GPT4ALL-> {
+ gpt4AllChatModel.set(oldChatModel);
+ gpt4AllApiKey.set(currentApiKey.get());
+ gpt4AllApiBaseUrl.set(currentApiBaseUrl.get());
+ }
}
}
@@ -174,15 +190,25 @@ public AiTabViewModel(CliPreferences preferences) {
currentApiKey.set(huggingFaceApiKey.get());
currentApiBaseUrl.set(huggingFaceApiBaseUrl.get());
}
+ case GPT4ALL -> {
+ currentChatModel.set(gpt4AllChatModel.get());
+ currentApiKey.set(gpt4AllApiKey.get());
+ currentApiBaseUrl.set(gpt4AllApiBaseUrl.get());
+ }
}
});
this.currentChatModel.addListener((observable, oldValue, newValue) -> {
+ if (newValue == null) {
+ return;
+ }
+
switch (selectedAiProvider.get()) {
case OPEN_AI -> openAiChatModel.set(newValue);
case MISTRAL_AI -> mistralAiChatModel.set(newValue);
case GEMINI -> geminiChatModel.set(newValue);
case HUGGING_FACE -> huggingFaceChatModel.set(newValue);
+ case GPT4ALL -> gpt4AllChatModel.set(newValue);
}
contextWindowSize.set(AiDefaultPreferences.getContextWindowSize(selectedAiProvider.get(), newValue));
@@ -194,6 +220,7 @@ public AiTabViewModel(CliPreferences preferences) {
case MISTRAL_AI -> mistralAiApiKey.set(newValue);
case GEMINI -> geminiAiApiKey.set(newValue);
case HUGGING_FACE -> huggingFaceApiKey.set(newValue);
+ case GPT4ALL -> gpt4AllApiKey.set(newValue);
}
});
@@ -203,6 +230,7 @@ public AiTabViewModel(CliPreferences preferences) {
case MISTRAL_AI -> mistralAiApiBaseUrl.set(newValue);
case GEMINI -> geminiApiBaseUrl.set(newValue);
case HUGGING_FACE -> huggingFaceApiBaseUrl.set(newValue);
+ case GPT4ALL -> gpt4AllApiBaseUrl.set(newValue);
}
});
@@ -226,11 +254,6 @@ public AiTabViewModel(CliPreferences preferences) {
Objects::nonNull,
ValidationMessage.error(Localization.lang("Embedding model has to be provided")));
- this.instructionValidator = new FunctionBasedValidator<>(
- instruction,
- message -> !StringUtil.isBlank(message),
- ValidationMessage.error(Localization.lang("The instruction has to be provided")));
-
this.temperatureTypeValidator = new FunctionBasedValidator<>(
temperature,
temp -> LocalizedNumbers.stringToDouble(temp).isPresent(),
@@ -279,16 +302,19 @@ public void setValues() {
mistralAiApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.MISTRAL_AI));
geminiAiApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.GEMINI));
huggingFaceApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.HUGGING_FACE));
+ gpt4AllApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.GPT4ALL));
openAiApiBaseUrl.setValue(aiPreferences.getOpenAiApiBaseUrl());
mistralAiApiBaseUrl.setValue(aiPreferences.getMistralAiApiBaseUrl());
geminiApiBaseUrl.setValue(aiPreferences.getGeminiApiBaseUrl());
huggingFaceApiBaseUrl.setValue(aiPreferences.getHuggingFaceApiBaseUrl());
+ gpt4AllApiBaseUrl.setValue(aiPreferences.getGpt4AllApiBaseUrl());
openAiChatModel.setValue(aiPreferences.getOpenAiChatModel());
mistralAiChatModel.setValue(aiPreferences.getMistralAiChatModel());
geminiChatModel.setValue(aiPreferences.getGeminiChatModel());
huggingFaceChatModel.setValue(aiPreferences.getHuggingFaceChatModel());
+ gpt4AllChatModel.setValue(aiPreferences.getGpt4AllChatModel());
enableAi.setValue(aiPreferences.getEnableAi());
autoGenerateSummaries.setValue(aiPreferences.getAutoGenerateSummaries());
@@ -299,7 +325,10 @@ public void setValues() {
customizeExpertSettings.setValue(aiPreferences.getCustomizeExpertSettings());
selectedEmbeddingModel.setValue(aiPreferences.getEmbeddingModel());
- instruction.setValue(aiPreferences.getInstruction());
+
+ Arrays.stream(AiTemplate.values()).forEach(template ->
+ templateSources.get(template).set(aiPreferences.getTemplate(template)));
+
temperature.setValue(LocalizedNumbers.doubleToString(aiPreferences.getTemperature()));
contextWindowSize.setValue(aiPreferences.getContextWindowSize());
documentSplitterChunkSize.setValue(aiPreferences.getDocumentSplitterChunkSize());
@@ -320,11 +349,13 @@ public void storeSettings() {
aiPreferences.setMistralAiChatModel(mistralAiChatModel.get() == null ? "" : mistralAiChatModel.get());
aiPreferences.setGeminiChatModel(geminiChatModel.get() == null ? "" : geminiChatModel.get());
aiPreferences.setHuggingFaceChatModel(huggingFaceChatModel.get() == null ? "" : huggingFaceChatModel.get());
+ aiPreferences.setGpt4AllChatModel(gpt4AllChatModel.get() == null ? "" : gpt4AllChatModel.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.OPEN_AI, openAiApiKey.get() == null ? "" : openAiApiKey.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.MISTRAL_AI, mistralAiApiKey.get() == null ? "" : mistralAiApiKey.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.GEMINI, geminiAiApiKey.get() == null ? "" : geminiAiApiKey.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.HUGGING_FACE, huggingFaceApiKey.get() == null ? "" : huggingFaceApiKey.get());
+ aiPreferences.storeAiApiKeyInKeyring(AiProvider.GPT4ALL, gpt4AllApiKey.get() == null ? "" : gpt4AllApiKey.get());
// We notify in all cases without a real check if something was changed
aiPreferences.apiKeyUpdated();
@@ -336,8 +367,11 @@ public void storeSettings() {
aiPreferences.setMistralAiApiBaseUrl(mistralAiApiBaseUrl.get() == null ? "" : mistralAiApiBaseUrl.get());
aiPreferences.setGeminiApiBaseUrl(geminiApiBaseUrl.get() == null ? "" : geminiApiBaseUrl.get());
aiPreferences.setHuggingFaceApiBaseUrl(huggingFaceApiBaseUrl.get() == null ? "" : huggingFaceApiBaseUrl.get());
+ aiPreferences.setGpt4AllApiBaseUrl(gpt4AllApiBaseUrl.get() == null ? "" : gpt4AllApiBaseUrl.get());
+
+ Arrays.stream(AiTemplate.values()).forEach(template ->
+ aiPreferences.setTemplate(template, templateSources.get(template).get()));
- aiPreferences.setInstruction(instruction.get());
// We already check the correctness of temperature and RAG minimum score in validators, so we don't need to check it here.
aiPreferences.setTemperature(LocalizedNumbers.stringToDouble(oldLocale, temperature.get()).get());
aiPreferences.setContextWindowSize(contextWindowSize.get());
@@ -351,8 +385,6 @@ public void resetExpertSettings() {
String resetApiBaseUrl = selectedAiProvider.get().getApiUrl();
currentApiBaseUrl.set(resetApiBaseUrl);
- instruction.set(AiDefaultPreferences.SYSTEM_MESSAGE);
-
contextWindowSize.set(AiDefaultPreferences.getContextWindowSize(selectedAiProvider.get(), currentChatModel.get()));
temperature.set(LocalizedNumbers.doubleToString(AiDefaultPreferences.TEMPERATURE));
@@ -362,6 +394,11 @@ public void resetExpertSettings() {
ragMinScore.set(LocalizedNumbers.doubleToString(AiDefaultPreferences.RAG_MIN_SCORE));
}
+ public void resetTemplates() {
+ Arrays.stream(AiTemplate.values()).forEach(template ->
+ templateSources.get(template).set(AiDefaultPreferences.TEMPLATES.get(template)));
+ }
+
@Override
public boolean validateSettings() {
if (enableAi.get()) {
@@ -388,7 +425,6 @@ public boolean validateExpertSettings() {
List validators = List.of(
apiBaseUrlValidator,
embeddingModelValidator,
- instructionValidator,
temperatureTypeValidator,
temperatureRangeValidator,
contextWindowSizeValidator,
@@ -462,8 +498,8 @@ public BooleanProperty disableApiBaseUrlProperty() {
return disableApiBaseUrl;
}
- public StringProperty instructionProperty() {
- return instruction;
+ public Map getTemplateSources() {
+ return templateSources;
}
public StringProperty temperatureProperty() {
@@ -514,10 +550,6 @@ public ValidationStatus getEmbeddingModelValidationStatus() {
return embeddingModelValidator.getValidationStatus();
}
- public ValidationStatus getSystemMessageValidationStatus() {
- return instructionValidator.getValidationStatus();
- }
-
public ValidationStatus getTemperatureTypeValidationStatus() {
return temperatureTypeValidator.getValidationStatus();
}
diff --git a/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java b/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
index 134f290b763..20fe9aa44ad 100644
--- a/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
+++ b/src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
@@ -177,7 +177,8 @@ public void initialize() {
sortUpButton.disableProperty().bind(viewModel.chosenSelectionModelProperty().getValue().selectedItemProperty().isNull());
sortDownButton.disableProperty().bind(viewModel.chosenSelectionModelProperty().getValue().selectedItemProperty().isNull());
- PreviewViewer previewViewer = new PreviewViewer(new BibDatabaseContext(), dialogService, preferences, themeManager, taskExecutor);
+ PreviewViewer previewViewer = new PreviewViewer(dialogService, preferences, themeManager, taskExecutor);
+ previewViewer.setDatabaseContext(new BibDatabaseContext());
previewViewer.setEntry(TestEntry.getTestEntry());
EasyBind.subscribe(viewModel.selectedLayoutProperty(), previewViewer::setLayout);
previewViewer.visibleProperty().bind(viewModel.chosenSelectionModelProperty().getValue().selectedItemProperty().isNotNull()
diff --git a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java
index 550669211d2..06907a2bcb8 100644
--- a/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java
@@ -164,7 +164,7 @@ public void storeSettings() {
filePreferences.setKeepDownloadUrl(shouldkeepDownloadUrl.getValue());
importerPreferences.setDefaultPlainCitationParser(defaultPlainCitationParser.getValue());
grobidPreferences.setGrobidEnabled(grobidEnabledProperty.getValue());
- grobidPreferences.setGrobidOptOut(grobidPreferences.isGrobidOptOut());
+ grobidPreferences.setGrobidUseAsked(grobidPreferences.isGrobidUseAsked());
grobidPreferences.setGrobidURL(grobidURLProperty.getValue());
doiPreferences.setUseCustom(useCustomDOIProperty.get());
doiPreferences.setDefaultBaseURI(useCustomDOINameProperty.getValue().trim());
diff --git a/src/main/java/org/jabref/gui/preview/PreviewPanel.java b/src/main/java/org/jabref/gui/preview/PreviewPanel.java
index ed05024c02a..a1e27d45820 100644
--- a/src/main/java/org/jabref/gui/preview/PreviewPanel.java
+++ b/src/main/java/org/jabref/gui/preview/PreviewPanel.java
@@ -11,13 +11,15 @@
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
+import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
-import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jabref.gui.DialogService;
+import org.jabref.gui.StateManager;
import org.jabref.gui.externalfiles.ExternalFilesEntryLinker;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.keyboard.KeyBinding;
@@ -33,6 +35,9 @@
import org.jabref.model.entry.BibEntry;
import org.jabref.model.search.query.SearchQuery;
+/// Displays the entry preview
+///
+/// The instance is re-used at each tab. The code ensures that the panel is moved across tabs when the user switches the tab.
public class PreviewPanel extends VBox {
private final ExternalFilesEntryLinker fileLinker;
@@ -40,59 +45,64 @@ public class PreviewPanel extends VBox {
private final PreviewViewer previewView;
private final PreviewPreferences previewPreferences;
private final DialogService dialogService;
+
private BibEntry entry;
- public PreviewPanel(BibDatabaseContext database,
- DialogService dialogService,
+ public PreviewPanel(DialogService dialogService,
KeyBindingRepository keyBindingRepository,
GuiPreferences preferences,
ThemeManager themeManager,
TaskExecutor taskExecutor,
+ StateManager stateManager,
OptionalObjectProperty searchQueryProperty) {
this.keyBindingRepository = keyBindingRepository;
this.dialogService = dialogService;
this.previewPreferences = preferences.getPreviewPreferences();
- this.fileLinker = new ExternalFilesEntryLinker(preferences.getExternalApplicationsPreferences(), preferences.getFilePreferences(), database, dialogService);
+ this.fileLinker = new ExternalFilesEntryLinker(preferences.getExternalApplicationsPreferences(), preferences.getFilePreferences(), dialogService, stateManager);
PreviewPreferences previewPreferences = preferences.getPreviewPreferences();
- previewView = new PreviewViewer(database, dialogService, preferences, themeManager, taskExecutor, searchQueryProperty);
+ previewView = new PreviewViewer(dialogService, preferences, themeManager, taskExecutor, searchQueryProperty);
previewView.setLayout(previewPreferences.getSelectedPreviewLayout());
previewView.setContextMenu(createPopupMenu());
- previewView.setOnDragDetected(event -> {
- previewView.startFullDrag();
+ previewView.setOnDragDetected(this::onDragDetected);
+ previewView.setOnDragOver(PreviewPanel::onDragOver);
+ previewView.setOnDragDropped(event -> onDragDropped(event));
- Dragboard dragboard = previewView.startDragAndDrop(TransferMode.COPY);
- ClipboardContent content = new ClipboardContent();
- content.putHtml(previewView.getSelectionHtmlContent());
- dragboard.setContent(content);
+ this.getChildren().add(previewView);
- event.consume();
- });
+ createKeyBindings();
+ previewView.setLayout(previewPreferences.getSelectedPreviewLayout());
+ }
- previewView.setOnDragOver(event -> {
- if (event.getDragboard().hasFiles()) {
- event.acceptTransferModes(TransferMode.COPY, TransferMode.MOVE, TransferMode.LINK);
- }
- event.consume();
- });
+ private void onDragDetected(MouseEvent event) {
+ previewView.startFullDrag();
- previewView.setOnDragDropped(event -> {
- boolean success = false;
- if (event.getDragboard().hasContent(DataFormat.FILES)) {
- TransferMode transferMode = event.getTransferMode();
- List files = event.getDragboard().getFiles().stream().map(File::toPath).collect(Collectors.toList());
- DragDrop.handleDropOfFiles(files, transferMode, fileLinker, entry);
- success = true;
- }
+ Dragboard dragboard = previewView.startDragAndDrop(TransferMode.COPY);
+ ClipboardContent content = new ClipboardContent();
+ content.putHtml(previewView.getSelectionHtmlContent());
+ dragboard.setContent(content);
- event.setDropCompleted(success);
- event.consume();
- });
- this.getChildren().add(previewView);
- VBox.setVgrow(previewView, Priority.ALWAYS);
+ event.consume();
+ }
- createKeyBindings();
- previewView.setLayout(previewPreferences.getSelectedPreviewLayout());
+ private static void onDragOver(DragEvent event) {
+ if (event.getDragboard().hasFiles()) {
+ event.acceptTransferModes(TransferMode.COPY, TransferMode.MOVE, TransferMode.LINK);
+ }
+ event.consume();
+ }
+
+ private void onDragDropped(DragEvent event) {
+ boolean success = false;
+ if (event.getDragboard().hasContent(DataFormat.FILES)) {
+ TransferMode transferMode = event.getTransferMode();
+ List files = event.getDragboard().getFiles().stream().map(File::toPath).collect(Collectors.toList());
+ DragDrop.handleDropOfFiles(files, transferMode, fileLinker, entry);
+ success = true;
+ }
+
+ event.setDropCompleted(success);
+ event.consume();
}
private void createKeyBindings() {
@@ -138,6 +148,10 @@ public void setEntry(BibEntry entry) {
previewView.setLayout(previewPreferences.getSelectedPreviewLayout());
}
+ public void setDatabase(BibDatabaseContext databaseContext) {
+ previewView.setDatabaseContext(databaseContext);
+ }
+
public void print() {
previewView.print();
}
diff --git a/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/src/main/java/org/jabref/gui/preview/PreviewViewer.java
index 29d6ed72ea8..306c543af77 100644
--- a/src/main/java/org/jabref/gui/preview/PreviewViewer.java
+++ b/src/main/java/org/jabref/gui/preview/PreviewViewer.java
@@ -5,7 +5,6 @@
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.util.Objects;
-import java.util.Optional;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -33,6 +32,7 @@
import org.jabref.model.search.query.SearchQuery;
import com.airhacks.afterburner.injection.Injector;
+import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@@ -74,26 +74,33 @@ function getSelectionHtml() {
private final DialogService dialogService;
private final TaskExecutor taskExecutor;
private final WebView previewView;
- private final BibDatabaseContext database;
private final OptionalObjectProperty searchQueryProperty;
- private Optional entry = Optional.empty();
+ // Used for resolving strings and pdf directories for links.
+ private @Nullable BibDatabaseContext databaseContext;
+
+ private @Nullable BibEntry entry;
+
private PreviewLayout layout;
+
private String layoutText;
- /**
- * @param database Used for resolving strings and pdf directories for links.
- */
- public PreviewViewer(BibDatabaseContext database,
- DialogService dialogService,
+ public PreviewViewer(DialogService dialogService,
+ GuiPreferences preferences,
+ ThemeManager themeManager,
+ TaskExecutor taskExecutor) {
+ this(dialogService, preferences, themeManager, taskExecutor, OptionalObjectProperty.empty());
+ }
+
+ public PreviewViewer(DialogService dialogService,
GuiPreferences preferences,
ThemeManager themeManager,
TaskExecutor taskExecutor,
OptionalObjectProperty searchQueryProperty) {
- this.database = Objects.requireNonNull(database);
this.dialogService = dialogService;
this.clipBoardManager = Injector.instantiateModelOrService(ClipBoardManager.class);
this.taskExecutor = taskExecutor;
+
this.searchQueryProperty = searchQueryProperty;
this.searchQueryProperty.addListener((queryObservable, queryOldValue, queryNewValue) -> highlightLayoutText());
@@ -135,14 +142,6 @@ public PreviewViewer(BibDatabaseContext database,
themeManager.installCss(previewView.getEngine());
}
- public PreviewViewer(BibDatabaseContext database,
- DialogService dialogService,
- GuiPreferences preferences,
- ThemeManager themeManager,
- TaskExecutor taskExecutor) {
- this(database, dialogService, preferences, themeManager, taskExecutor, OptionalObjectProperty.empty());
- }
-
public void setLayout(PreviewLayout newLayout) {
// Change listeners might set the layout to null while the update method is executing, therefore we need to prevent this here
if (newLayout == null || newLayout.equals(layout)) {
@@ -152,29 +151,43 @@ public void setLayout(PreviewLayout newLayout) {
update();
}
- public void setEntry(BibEntry newEntry) {
- if (newEntry.equals(entry.orElse(null))) {
+ public void setEntry(@Nullable BibEntry newEntry) {
+ if (Objects.equals(entry, newEntry)) {
return;
}
// Remove update listener for old entry
- entry.ifPresent(oldEntry -> {
- for (Observable observable : oldEntry.getObservables()) {
+ if (entry != null) {
+ for (Observable observable : entry.getObservables()) {
observable.removeListener(this);
}
- });
+ }
+
+ entry = newEntry;
- entry = Optional.of(newEntry);
+ if (entry != null) {
+ // Register for changes
+ for (Observable observable : newEntry.getObservables()) {
+ observable.addListener(this);
+ }
+ }
+ update();
+ }
- // Register for changes
- for (Observable observable : newEntry.getObservables()) {
- observable.addListener(this);
+ public void setDatabaseContext(BibDatabaseContext newDatabaseContext) {
+ if (Objects.equals(databaseContext, newDatabaseContext)) {
+ return;
}
+
+ setEntry(null);
+
+ databaseContext = newDatabaseContext;
update();
}
private void update() {
- if (entry.isEmpty() || (layout == null)) {
+ if (databaseContext == null || entry == null || layout == null) {
+ LOGGER.debug("databaseContext null {}, entry null {}, or layout null {}", databaseContext == null, entry == null, layout == null);
// Make sure that the preview panel is not completely white, especially with dark theme on
setPreviewText("");
return;
@@ -182,9 +195,9 @@ private void update() {
Number.serialExportNumber = 1; // Set entry number in case that is included in the preview layout.
- final BibEntry theEntry = entry.get();
+ final BibEntry theEntry = entry;
BackgroundTask
- .wrap(() -> layout.generatePreview(theEntry, database))
+ .wrap(() -> layout.generatePreview(theEntry, databaseContext))
.onSuccess(this::setPreviewText)
.onFailure(exception -> {
LOGGER.error("Error while generating citation style", exception);
@@ -228,13 +241,13 @@ private void highlightLayoutText() {
public void print() {
PrinterJob job = PrinterJob.createPrinterJob();
boolean proceed = dialogService.showPrintDialog(job);
- if (!proceed) {
+ if (!proceed && entry != null) {
return;
}
BackgroundTask
.wrap(() -> {
- job.getJobSettings().setJobName(entry.flatMap(BibEntry::getCitationKey).orElse("NO ENTRY"));
+ job.getJobSettings().setJobName(entry.getCitationKey().orElse("NO CITATION KEY"));
previewView.getEngine().print(job);
job.endJob();
})
diff --git a/src/main/java/org/jabref/gui/push/PushToApplications.java b/src/main/java/org/jabref/gui/push/PushToApplications.java
index 08b551674a7..0a277e471a2 100644
--- a/src/main/java/org/jabref/gui/push/PushToApplications.java
+++ b/src/main/java/org/jabref/gui/push/PushToApplications.java
@@ -18,6 +18,7 @@ public class PushToApplications {
public static final String WIN_EDT = "WinEdt";
public static final String SUBLIME_TEXT = "Sublime Text";
public static final String TEXSHOP = "TeXShop";
+ public static final String VSCODE = "VScode";
private static final List APPLICATIONS = new ArrayList<>();
@@ -38,7 +39,8 @@ public static List getAllApplications(DialogService dialogSer
new PushToTeXworks(dialogService, preferences),
new PushToVim(dialogService, preferences),
new PushToWinEdt(dialogService, preferences),
- new PushToTexShop(dialogService, preferences)));
+ new PushToTexShop(dialogService, preferences),
+ new PushToVScode(dialogService, preferences)));
return APPLICATIONS;
}
diff --git a/src/main/java/org/jabref/gui/push/PushToTeXworks.java b/src/main/java/org/jabref/gui/push/PushToTeXworks.java
index 6459d848c13..87c3a3b9931 100644
--- a/src/main/java/org/jabref/gui/push/PushToTeXworks.java
+++ b/src/main/java/org/jabref/gui/push/PushToTeXworks.java
@@ -38,7 +38,6 @@ protected String[] getCommandLine(String keyString) {
@Override
protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) {
- // No command known to jump to a specific line
- return new String[] {commandPath, fileName.toString()};
+ return new String[] {commandPath, "--position=\"%s\"".formatted(line), fileName.toString()};
}
}
diff --git a/src/main/java/org/jabref/gui/push/PushToVScode.java b/src/main/java/org/jabref/gui/push/PushToVScode.java
new file mode 100644
index 00000000000..6a40a1edf45
--- /dev/null
+++ b/src/main/java/org/jabref/gui/push/PushToVScode.java
@@ -0,0 +1,38 @@
+package org.jabref.gui.push;
+
+import java.nio.file.Path;
+
+import org.jabref.gui.DialogService;
+import org.jabref.gui.icon.IconTheme;
+import org.jabref.gui.icon.JabRefIcon;
+import org.jabref.gui.preferences.GuiPreferences;
+
+public class PushToVScode extends AbstractPushToApplication {
+
+ public static final String NAME = PushToApplications.VSCODE;
+
+ public PushToVScode(DialogService dialogService, GuiPreferences preferences) {
+ super(dialogService, preferences);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return NAME;
+ }
+
+ @Override
+ public JabRefIcon getApplicationIcon() {
+ return IconTheme.JabRefIcons.APPLICATION_VSCODE;
+ }
+
+ @Override
+ protected String[] getCommandLine(String keyString) {
+ // TODO - Implementing this will fix https://github.com/JabRef/jabref/issues/6775
+ return new String[] {commandPath};
+ }
+
+ @Override
+ public String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) {
+ return new String[] {commandPath, "--g", "%s:%s:%s".formatted(fileName.toString(), line, column)};
+ }
+}
diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java
index 87d72be7774..8562662cc55 100644
--- a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java
+++ b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java
@@ -65,8 +65,9 @@ private void initialize() {
searchBarContainer.getChildren().addFirst(searchBar);
HBox.setHgrow(searchBar, Priority.ALWAYS);
- PreviewViewer previewViewer = new PreviewViewer(viewModel.getSearchDatabaseContext(), dialogService, preferences, themeManager, taskExecutor, stateManager.activeSearchQuery(SearchType.GLOBAL_SEARCH));
+ PreviewViewer previewViewer = new PreviewViewer(dialogService, preferences, themeManager, taskExecutor, stateManager.activeSearchQuery(SearchType.GLOBAL_SEARCH));
previewViewer.setLayout(preferences.getPreviewPreferences().getSelectedPreviewLayout());
+ previewViewer.setDatabaseContext(viewModel.getSearchDatabaseContext());
SearchResultsTableDataModel model = new SearchResultsTableDataModel(viewModel.getSearchDatabaseContext(), preferences, stateManager, taskExecutor);
SearchResultsTable resultsTable = new SearchResultsTable(model, viewModel.getSearchDatabaseContext(), preferences, undoManager, dialogService, stateManager, taskExecutor);
diff --git a/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java b/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java
index 519b9899ed2..15dba31fdad 100644
--- a/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java
+++ b/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java
@@ -197,8 +197,7 @@ private void updateDirectoryWarning(Path directory) {
} else {
directoryWarning.setVisible(false);
}
- } catch (
- IOException e) {
+ } catch (IOException e) {
directoryWarning.setText(Localization.lang("Warning: Failed to check if the directory is empty."));
directoryWarning.setVisible(true);
}
diff --git a/src/main/java/org/jabref/gui/specialfields/SpecialFieldAction.java b/src/main/java/org/jabref/gui/specialfields/SpecialFieldAction.java
index 990c81cd3d3..9e6bcc3d95a 100644
--- a/src/main/java/org/jabref/gui/specialfields/SpecialFieldAction.java
+++ b/src/main/java/org/jabref/gui/specialfields/SpecialFieldAction.java
@@ -82,7 +82,6 @@ public void execute() {
if (ce.hasEdits()) {
undoManager.addEdit(ce);
tabSupplier.get().markBaseChanged();
- tabSupplier.get().updateEntryEditorIfShowing();
String outText;
if (nullFieldIfValueIsTheSame || value == null) {
outText = getTextDone(specialField, Integer.toString(bes.size()));
diff --git a/src/main/java/org/jabref/gui/util/IconValidationDecorator.java b/src/main/java/org/jabref/gui/util/IconValidationDecorator.java
index 2608c5c0663..6128ff040c6 100644
--- a/src/main/java/org/jabref/gui/util/IconValidationDecorator.java
+++ b/src/main/java/org/jabref/gui/util/IconValidationDecorator.java
@@ -31,19 +31,11 @@ public IconValidationDecorator(Pos position) {
this.position = position;
}
- @Override
- protected Node createErrorNode() {
- return IconTheme.JabRefIcons.ERROR.getGraphicNode();
- }
-
- @Override
- protected Node createWarningNode() {
- return IconTheme.JabRefIcons.WARNING.getGraphicNode();
- }
-
@Override
public Node createDecorationNode(ValidationMessage message) {
- Node graphic = Severity.ERROR == message.getSeverity() ? createErrorNode() : createWarningNode();
+ Node graphic = Severity.ERROR == message.getSeverity()
+ ? IconTheme.JabRefIcons.ERROR.getGraphicNode()
+ : IconTheme.JabRefIcons.WARNING.getGraphicNode();
graphic.getStyleClass().add(Severity.ERROR == message.getSeverity() ? "error-icon" : "warning-icon");
Label label = new Label();
label.setGraphic(graphic);
diff --git a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
index e83c1a084b5..8e4b50d9cb0 100644
--- a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
+++ b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
@@ -4,6 +4,7 @@
import java.util.List;
import java.util.Map;
+import org.jabref.logic.ai.templates.AiTemplate;
import org.jabref.model.ai.AiProvider;
import org.jabref.model.ai.EmbeddingModel;
@@ -23,7 +24,9 @@ public enum PredefinedChatModel {
GEMINI_1_5_PRO(AiProvider.GEMINI, "gemini-1.5-pro", 2097152),
GEMINI_1_0_PRO(AiProvider.GEMINI, "gemini-1.0-pro", 32000),
// Dummy variant for Hugging Face models.
- HUGGING_FACE(AiProvider.HUGGING_FACE, "", 0);
+ // Blank entry used for cases where the model name is not specified.
+ BLANK_HUGGING_FACE(AiProvider.HUGGING_FACE, "", 0),
+ BLANK_GPT4ALL(AiProvider.GPT4ALL, "", 0);
private final AiProvider aiProvider;
private final String name;
@@ -62,7 +65,8 @@ public String toString() {
AiProvider.OPEN_AI, PredefinedChatModel.GPT_4O_MINI,
AiProvider.MISTRAL_AI, PredefinedChatModel.OPEN_MIXTRAL_8X22B,
AiProvider.GEMINI, PredefinedChatModel.GEMINI_1_5_FLASH,
- AiProvider.HUGGING_FACE, PredefinedChatModel.HUGGING_FACE
+ AiProvider.HUGGING_FACE, PredefinedChatModel.BLANK_HUGGING_FACE,
+ AiProvider.GPT4ALL, PredefinedChatModel.BLANK_GPT4ALL
);
public static final boolean CUSTOMIZE_SETTINGS = false;
@@ -77,6 +81,46 @@ public String toString() {
public static final int FALLBACK_CONTEXT_WINDOW_SIZE = 8196;
+ public static final Map TEMPLATES = Map.of(
+ AiTemplate.CHATTING_SYSTEM_MESSAGE, """
+ You are an AI assistant that analyses research papers. You answer questions about papers.
+ You will be supplied with the necessary information. The supplied information will contain mentions of papers in form '@citationKey'.
+ Whenever you refer to a paper, use its citation key in the same form with @ symbol. Whenever you find relevant information, always use the citation key.
+
+ Here are the papers you are analyzing:
+ #foreach( $entry in $entries )
+ ${CanonicalBibEntry.getCanonicalRepresentation($entry)}
+ #end""",
+
+ AiTemplate.CHATTING_USER_MESSAGE, """
+ $message
+
+ Here is some relevant information for you:
+ #foreach( $excerpt in $excerpts )
+ ${excerpt.citationKey()}:
+ ${excerpt.text()}
+ #end""",
+
+ AiTemplate.SUMMARIZATION_CHUNK, """
+ Please provide an overview of the following text. It is a part of a scientific paper.
+ The summary should include the main objectives, methodologies used, key findings, and conclusions.
+ Mention any significant experiments, data, or discussions presented in the paper.
+
+ DOCUMENT:
+ $text
+
+ OVERVIEW:""",
+
+ AiTemplate.SUMMARIZATION_COMBINE, """
+ You have written an overview of a scientific paper. You have been collecting notes from various parts
+ of the paper. Now your task is to combine all of the notes in one structured message.
+
+ SUMMARIES:
+ $chunks
+
+ FINAL OVERVIEW:"""
+ );
+
public static List getAvailableModels(AiProvider aiProvider) {
return Arrays.stream(AiDefaultPreferences.PredefinedChatModel.values())
.filter(model -> model.getAiProvider() == aiProvider)
diff --git a/src/main/java/org/jabref/logic/ai/AiPreferences.java b/src/main/java/org/jabref/logic/ai/AiPreferences.java
index 3a07a02e70c..9b6787e2d4d 100644
--- a/src/main/java/org/jabref/logic/ai/AiPreferences.java
+++ b/src/main/java/org/jabref/logic/ai/AiPreferences.java
@@ -1,6 +1,7 @@
package org.jabref.logic.ai;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import javafx.beans.property.BooleanProperty;
@@ -15,6 +16,7 @@
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
+import org.jabref.logic.ai.templates.AiTemplate;
import org.jabref.model.ai.AiProvider;
import org.jabref.model.ai.EmbeddingModel;
import org.jabref.model.strings.StringUtil;
@@ -40,6 +42,7 @@ public class AiPreferences {
private final StringProperty mistralAiChatModel;
private final StringProperty geminiChatModel;
private final StringProperty huggingFaceChatModel;
+ private final StringProperty gpt4AllChatModel;
private final BooleanProperty customizeExpertSettings;
@@ -47,6 +50,7 @@ public class AiPreferences {
private final StringProperty mistralAiApiBaseUrl;
private final StringProperty geminiApiBaseUrl;
private final StringProperty huggingFaceApiBaseUrl;
+ private final StringProperty gpt4AllApiBaseUrl;
private final ObjectProperty embeddingModel;
private final StringProperty instruction;
@@ -57,6 +61,8 @@ public class AiPreferences {
private final IntegerProperty ragMaxResultsCount;
private final DoubleProperty ragMinScore;
+ private final Map templates;
+
private Runnable apiKeyChangeListener;
public AiPreferences(boolean enableAi,
@@ -67,11 +73,13 @@ public AiPreferences(boolean enableAi,
String mistralAiChatModel,
String geminiChatModel,
String huggingFaceChatModel,
+ String gpt4AllModel,
boolean customizeExpertSettings,
String openAiApiBaseUrl,
String mistralAiApiBaseUrl,
String geminiApiBaseUrl,
String huggingFaceApiBaseUrl,
+ String gpt4AllApiBaseUrl,
EmbeddingModel embeddingModel,
String instruction,
double temperature,
@@ -79,7 +87,8 @@ public AiPreferences(boolean enableAi,
int documentSplitterChunkSize,
int documentSplitterOverlapSize,
int ragMaxResultsCount,
- double ragMinScore
+ double ragMinScore,
+ Map templates
) {
this.enableAi = new SimpleBooleanProperty(enableAi);
this.autoGenerateEmbeddings = new SimpleBooleanProperty(autoGenerateEmbeddings);
@@ -91,6 +100,7 @@ public AiPreferences(boolean enableAi,
this.mistralAiChatModel = new SimpleStringProperty(mistralAiChatModel);
this.geminiChatModel = new SimpleStringProperty(geminiChatModel);
this.huggingFaceChatModel = new SimpleStringProperty(huggingFaceChatModel);
+ this.gpt4AllChatModel = new SimpleStringProperty(gpt4AllModel);
this.customizeExpertSettings = new SimpleBooleanProperty(customizeExpertSettings);
@@ -98,6 +108,7 @@ public AiPreferences(boolean enableAi,
this.mistralAiApiBaseUrl = new SimpleStringProperty(mistralAiApiBaseUrl);
this.geminiApiBaseUrl = new SimpleStringProperty(geminiApiBaseUrl);
this.huggingFaceApiBaseUrl = new SimpleStringProperty(huggingFaceApiBaseUrl);
+ this.gpt4AllApiBaseUrl = new SimpleStringProperty(gpt4AllApiBaseUrl);
this.embeddingModel = new SimpleObjectProperty<>(embeddingModel);
this.instruction = new SimpleStringProperty(instruction);
@@ -107,6 +118,13 @@ public AiPreferences(boolean enableAi,
this.documentSplitterOverlapSize = new SimpleIntegerProperty(documentSplitterOverlapSize);
this.ragMaxResultsCount = new SimpleIntegerProperty(ragMaxResultsCount);
this.ragMinScore = new SimpleDoubleProperty(ragMinScore);
+
+ this.templates = Map.of(
+ AiTemplate.CHATTING_SYSTEM_MESSAGE, new SimpleStringProperty(templates.get(AiTemplate.CHATTING_SYSTEM_MESSAGE)),
+ AiTemplate.CHATTING_USER_MESSAGE, new SimpleStringProperty(templates.get(AiTemplate.CHATTING_USER_MESSAGE)),
+ AiTemplate.SUMMARIZATION_CHUNK, new SimpleStringProperty(templates.get(AiTemplate.SUMMARIZATION_CHUNK)),
+ AiTemplate.SUMMARIZATION_COMBINE, new SimpleStringProperty(templates.get(AiTemplate.SUMMARIZATION_COMBINE))
+ );
}
public String getApiKeyForAiProvider(AiProvider aiProvider) {
@@ -233,6 +251,18 @@ public void setHuggingFaceChatModel(String huggingFaceChatModel) {
this.huggingFaceChatModel.set(huggingFaceChatModel);
}
+ public StringProperty gpt4AllChatModelProperty() {
+ return gpt4AllChatModel;
+ }
+
+ public String getGpt4AllChatModel() {
+ return gpt4AllChatModel.get();
+ }
+
+ public void setGpt4AllChatModel(String gpt4AllChatModel) {
+ this.gpt4AllChatModel.set(gpt4AllChatModel);
+ }
+
public BooleanProperty customizeExpertSettingsProperty() {
return customizeExpertSettings;
}
@@ -309,6 +339,18 @@ public void setHuggingFaceApiBaseUrl(String huggingFaceApiBaseUrl) {
this.huggingFaceApiBaseUrl.set(huggingFaceApiBaseUrl);
}
+ public StringProperty gpt4AllApiBaseUrlProperty() {
+ return gpt4AllApiBaseUrl;
+ }
+
+ public String getGpt4AllApiBaseUrl() {
+ return gpt4AllApiBaseUrl.get();
+ }
+
+ public void setGpt4AllApiBaseUrl(String gpt4AllApiBaseUrl) {
+ this.gpt4AllApiBaseUrl.set(gpt4AllApiBaseUrl);
+ }
+
public StringProperty instructionProperty() {
return instruction;
}
@@ -354,6 +396,7 @@ public int getContextWindowSize() {
case MISTRAL_AI -> AiDefaultPreferences.getContextWindowSize(AiProvider.MISTRAL_AI, mistralAiChatModel.get());
case HUGGING_FACE -> AiDefaultPreferences.getContextWindowSize(AiProvider.HUGGING_FACE, huggingFaceChatModel.get());
case GEMINI -> AiDefaultPreferences.getContextWindowSize(AiProvider.GEMINI, geminiChatModel.get());
+ case GPT4ALL -> AiDefaultPreferences.getContextWindowSize(AiProvider.GPT4ALL, gpt4AllChatModel.get());
};
}
}
@@ -481,6 +524,8 @@ public String getSelectedChatModel() {
huggingFaceChatModel.get();
case GEMINI ->
geminiChatModel.get();
+ case GPT4ALL ->
+ gpt4AllChatModel.get();
};
}
@@ -495,6 +540,8 @@ public String getSelectedApiBaseUrl() {
huggingFaceApiBaseUrl.get();
case GEMINI ->
geminiApiBaseUrl.get();
+ case GPT4ALL ->
+ gpt4AllApiBaseUrl.get();
};
} else {
return aiProvider.get().getApiUrl();
@@ -511,4 +558,16 @@ public void setApiKeyChangeListener(Runnable apiKeyChangeListener) {
public void apiKeyUpdated() {
apiKeyChangeListener.run();
}
+
+ public void setTemplate(AiTemplate aiTemplate, String template) {
+ templates.get(aiTemplate).set(template);
+ }
+
+ public String getTemplate(AiTemplate aiTemplate) {
+ return templates.get(aiTemplate).get();
+ }
+
+ public StringProperty templateProperty(AiTemplate aiTemplate) {
+ return templates.get(aiTemplate);
+ }
}
diff --git a/src/main/java/org/jabref/logic/ai/AiService.java b/src/main/java/org/jabref/logic/ai/AiService.java
index a068037b423..2e1b76b67b0 100644
--- a/src/main/java/org/jabref/logic/ai/AiService.java
+++ b/src/main/java/org/jabref/logic/ai/AiService.java
@@ -17,6 +17,7 @@
import org.jabref.logic.ai.ingestion.storages.MVStoreFullyIngestedDocumentsTracker;
import org.jabref.logic.ai.summarization.SummariesService;
import org.jabref.logic.ai.summarization.storages.MVStoreSummariesStorage;
+import org.jabref.logic.ai.templates.TemplatesService;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
import org.jabref.logic.util.Directories;
import org.jabref.logic.util.NotificationService;
@@ -71,11 +72,12 @@ public AiService(AiPreferences aiPreferences,
this.mvStoreFullyIngestedDocumentsTracker = new MVStoreFullyIngestedDocumentsTracker(Directories.getAiFilesDirectory().resolve(FULLY_INGESTED_FILE_NAME), notificationService);
this.mvStoreSummariesStorage = new MVStoreSummariesStorage(Directories.getAiFilesDirectory().resolve(SUMMARIES_FILE_NAME), notificationService);
+ TemplatesService templatesService = new TemplatesService(aiPreferences);
this.chatHistoryService = new ChatHistoryService(citationKeyPatternPreferences, mvStoreChatHistoryStorage);
this.jabRefChatLanguageModel = new JabRefChatLanguageModel(aiPreferences);
this.jabRefEmbeddingModel = new JabRefEmbeddingModel(aiPreferences, notificationService, taskExecutor);
- this.aiChatService = new AiChatService(aiPreferences, jabRefChatLanguageModel, jabRefEmbeddingModel, mvStoreEmbeddingStore, cachedThreadPool);
+ this.aiChatService = new AiChatService(aiPreferences, jabRefChatLanguageModel, jabRefEmbeddingModel, mvStoreEmbeddingStore, templatesService);
this.ingestionService = new IngestionService(
aiPreferences,
@@ -91,6 +93,7 @@ public AiService(AiPreferences aiPreferences,
aiPreferences,
mvStoreSummariesStorage,
jabRefChatLanguageModel,
+ templatesService,
shutdownSignal,
filePreferences,
taskExecutor
diff --git a/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java b/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java
index f079b093604..fb6f5656cdf 100644
--- a/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java
+++ b/src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java
@@ -1,8 +1,7 @@
package org.jabref.logic.ai.chatting;
import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
+import java.util.Optional;
import javafx.beans.property.StringProperty;
import javafx.collections.ListChangeListener;
@@ -10,15 +9,15 @@
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.ingestion.FileEmbeddingsManager;
+import org.jabref.logic.ai.templates.AiTemplate;
+import org.jabref.logic.ai.templates.PaperExcerpt;
+import org.jabref.logic.ai.templates.TemplatesService;
import org.jabref.logic.ai.util.ErrorMessage;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
-import org.jabref.model.entry.CanonicalBibEntry;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.util.ListUtil;
-import dev.langchain4j.chain.Chain;
-import dev.langchain4j.chain.ConversationalRetrievalChain;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
@@ -29,14 +28,12 @@
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiTokenizer;
-import dev.langchain4j.rag.DefaultRetrievalAugmentor;
-import dev.langchain4j.rag.RetrievalAugmentor;
-import dev.langchain4j.rag.content.retriever.ContentRetriever;
-import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
+import dev.langchain4j.store.embedding.EmbeddingMatch;
+import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
+import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.filter.Filter;
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder;
-import jakarta.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,7 +44,7 @@ public class AiChatLogic {
private final ChatLanguageModel chatLanguageModel;
private final EmbeddingModel embeddingModel;
private final EmbeddingStore embeddingStore;
- private final Executor cachedThreadPool;
+ private final TemplatesService templatesService;
private final ObservableList chatHistory;
private final ObservableList entries;
@@ -55,13 +52,14 @@ public class AiChatLogic {
private final BibDatabaseContext bibDatabaseContext;
private ChatMemory chatMemory;
- private Chain chain;
+
+ private Optional filter = Optional.empty();
public AiChatLogic(AiPreferences aiPreferences,
ChatLanguageModel chatLanguageModel,
EmbeddingModel embeddingModel,
EmbeddingStore embeddingStore,
- Executor cachedThreadPool,
+ TemplatesService templatesService,
StringProperty name,
ObservableList chatHistory,
ObservableList entries,
@@ -71,26 +69,30 @@ public AiChatLogic(AiPreferences aiPreferences,
this.chatLanguageModel = chatLanguageModel;
this.embeddingModel = embeddingModel;
this.embeddingStore = embeddingStore;
- this.cachedThreadPool = cachedThreadPool;
+ this.templatesService = templatesService;
this.chatHistory = chatHistory;
this.entries = entries;
this.name = name;
this.bibDatabaseContext = bibDatabaseContext;
- this.entries.addListener((ListChangeListener) change -> rebuildChain());
+ this.entries.addListener((ListChangeListener) change -> rebuildFilter());
setupListeningToPreferencesChanges();
rebuildFull(chatHistory);
}
private void setupListeningToPreferencesChanges() {
- aiPreferences.instructionProperty().addListener(obs -> setSystemMessage(aiPreferences.getInstruction()));
+ aiPreferences
+ .templateProperty(AiTemplate.CHATTING_SYSTEM_MESSAGE)
+ .addListener(obs ->
+ setSystemMessage(templatesService.makeChattingSystemMessage(entries)));
+
aiPreferences.contextWindowSizeProperty().addListener(obs -> rebuildFull(chatMemory.messages()));
}
private void rebuildFull(List chatMessages) {
rebuildChatMemory(chatMessages);
- rebuildChain();
+ rebuildFilter();
}
private void rebuildChatMemory(List chatMessages) {
@@ -101,75 +103,93 @@ private void rebuildChatMemory(List chatMessages) {
chatMessages.stream().filter(chatMessage -> !(chatMessage instanceof ErrorMessage)).forEach(chatMemory::add);
- setSystemMessage(aiPreferences.getInstruction());
+ setSystemMessage(templatesService.makeChattingSystemMessage(entries));
}
- private void rebuildChain() {
+ private void rebuildFilter() {
List linkedFiles = ListUtil.getLinkedFiles(entries).toList();
- @Nullable Filter filter;
if (linkedFiles.isEmpty()) {
- // You must not pass an empty list to langchain4j {@link IsIn} filter.
- filter = null;
+ filter = Optional.empty();
} else {
- filter = MetadataFilterBuilder
+ filter = Optional.of(MetadataFilterBuilder
.metadataKey(FileEmbeddingsManager.LINK_METADATA_KEY)
.isIn(linkedFiles
.stream()
.map(LinkedFile::getLink)
.toList()
- );
+ ));
}
+ }
+
+ private void setSystemMessage(String systemMessage) {
+ chatMemory.add(new SystemMessage(systemMessage));
+ }
+
+ public AiMessage execute(UserMessage message) {
+ // Message will be automatically added to ChatMemory through ConversationalRetrievalChain.
- ContentRetriever contentRetriever = EmbeddingStoreContentRetriever
+ chatHistory.add(message);
+
+ LOGGER.info("Sending message to AI provider ({}) for answering in {}: {}",
+ aiPreferences.getAiProvider().getApiUrl(),
+ name.get(),
+ message.singleText());
+
+ EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest
.builder()
- .embeddingStore(embeddingStore)
- .filter(filter)
- .embeddingModel(embeddingModel)
.maxResults(aiPreferences.getRagMaxResultsCount())
.minScore(aiPreferences.getRagMinScore())
+ .filter(filter.orElse(null))
+ .queryEmbedding(embeddingModel.embed(message.singleText()).content())
.build();
- RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor
- .builder()
- .contentRetriever(contentRetriever)
- .contentInjector(new JabRefContentInjector(bibDatabaseContext))
- .executor(cachedThreadPool)
- .build();
+ EmbeddingSearchResult embeddingSearchResult = embeddingStore.search(embeddingSearchRequest);
- this.chain = ConversationalRetrievalChain
+ List excerpts = embeddingSearchResult
+ .matches()
+ .stream()
+ .map(EmbeddingMatch::embedded)
+ .map(textSegment -> {
+ String link = textSegment.metadata().getString(FileEmbeddingsManager.LINK_METADATA_KEY);
+
+ if (link == null) {
+ return new PaperExcerpt("", textSegment.text());
+ } else {
+ return new PaperExcerpt(findEntryByLink(link).flatMap(BibEntry::getCitationKey).orElse(""), textSegment.text());
+ }
+ })
+ .toList();
+
+ LOGGER.debug("Found excerpts for the message: {}", excerpts);
+
+ // This is crazy, but langchain4j {@link ChatMemory} does not allow to remove single messages.
+ ChatMemory tempChatMemory = TokenWindowChatMemory
.builder()
- .chatLanguageModel(chatLanguageModel)
- .retrievalAugmentor(retrievalAugmentor)
- .chatMemory(chatMemory)
+ .maxTokens(aiPreferences.getContextWindowSize(), new OpenAiTokenizer())
.build();
- }
- private void setSystemMessage(String systemMessage) {
- chatMemory.add(new SystemMessage(augmentSystemMessage(systemMessage)));
- }
+ chatMemory.messages().forEach(tempChatMemory::add);
- private String augmentSystemMessage(String systemMessage) {
- String entriesInfo = entries.stream().map(CanonicalBibEntry::getCanonicalRepresentation).collect(Collectors.joining("\n"));
+ tempChatMemory.add(new UserMessage(templatesService.makeChattingUserMessage(entries, message.singleText(), excerpts)));
+ chatMemory.add(message);
- return systemMessage + "\n" + entriesInfo;
- }
+ AiMessage aiMessage = chatLanguageModel.generate(tempChatMemory.messages()).content();
- public AiMessage execute(UserMessage message) {
- // Message will be automatically added to ChatMemory through ConversationalRetrievalChain.
-
- LOGGER.info("Sending message to AI provider ({}) for answering in {}: {}",
- aiPreferences.getAiProvider().getApiUrl(),
- name.get(),
- message.singleText());
+ chatMemory.add(aiMessage);
+ chatHistory.add(aiMessage);
- chatHistory.add(message);
- AiMessage result = new AiMessage(chain.execute(message.singleText()));
- chatHistory.add(result);
+ LOGGER.debug("Message was answered by the AI provider for {}: {}", name.get(), aiMessage.text());
- LOGGER.debug("Message was answered by the AI provider for {}: {}", name.get(), result.text());
+ return aiMessage;
+ }
- return result;
+ private Optional findEntryByLink(String link) {
+ return bibDatabaseContext
+ .getEntries()
+ .stream()
+ .filter(entry -> entry.getFiles().stream().anyMatch(file -> file.getLink().equals(link)))
+ .findFirst();
}
public ObservableList getChatHistory() {
diff --git a/src/main/java/org/jabref/logic/ai/chatting/AiChatService.java b/src/main/java/org/jabref/logic/ai/chatting/AiChatService.java
index d12b20ec502..d0d1698c497 100644
--- a/src/main/java/org/jabref/logic/ai/chatting/AiChatService.java
+++ b/src/main/java/org/jabref/logic/ai/chatting/AiChatService.java
@@ -1,11 +1,10 @@
package org.jabref.logic.ai.chatting;
-import java.util.concurrent.Executor;
-
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import org.jabref.logic.ai.AiPreferences;
+import org.jabref.logic.ai.templates.TemplatesService;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
@@ -20,19 +19,19 @@ public class AiChatService {
private final ChatLanguageModel chatLanguageModel;
private final EmbeddingModel embeddingModel;
private final EmbeddingStore embeddingStore;
- private final Executor cachedThreadPool;
+ private final TemplatesService templatesService;
public AiChatService(AiPreferences aiPreferences,
ChatLanguageModel chatLanguageModel,
EmbeddingModel embeddingModel,
EmbeddingStore embeddingStore,
- Executor cachedThreadPool
+ TemplatesService templatesService
) {
this.aiPreferences = aiPreferences;
this.chatLanguageModel = chatLanguageModel;
this.embeddingModel = embeddingModel;
this.embeddingStore = embeddingStore;
- this.cachedThreadPool = cachedThreadPool;
+ this.templatesService = templatesService;
}
public AiChatLogic makeChat(
@@ -46,7 +45,7 @@ public AiChatLogic makeChat(
chatLanguageModel,
embeddingModel,
embeddingStore,
- cachedThreadPool,
+ templatesService,
name,
chatHistory,
entries,
diff --git a/src/main/java/org/jabref/logic/ai/chatting/model/Gpt4AllModel.java b/src/main/java/org/jabref/logic/ai/chatting/model/Gpt4AllModel.java
new file mode 100644
index 00000000000..20c26951f69
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ai/chatting/model/Gpt4AllModel.java
@@ -0,0 +1,148 @@
+package org.jabref.logic.ai.chatting.model;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.jabref.logic.ai.AiPreferences;
+
+import com.google.gson.Gson;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import dev.langchain4j.model.output.FinishReason;
+import dev.langchain4j.model.output.Response;
+import dev.langchain4j.model.output.TokenUsage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Gpt4AllModel implements ChatLanguageModel {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Gpt4AllModel.class);
+
+ private final AiPreferences aiPreferences;
+ private final HttpClient httpClient;
+ private final Gson gson = new Gson();
+
+ public Gpt4AllModel(AiPreferences aiPreferences, HttpClient httpClient) {
+ this.aiPreferences = aiPreferences;
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public Response generate(List list) {
+ LOGGER.debug("Generating response from Gpt4All model with {} messages: {}", list.size(), list);
+
+ List messages = list.stream()
+ .map(chatMessage -> switch (chatMessage) {
+ case AiMessage aiMessage -> new Message("assistant", aiMessage.text());
+ case SystemMessage systemMessage -> new Message("system", systemMessage.text());
+ case ToolExecutionResultMessage toolExecutionResultMessage -> new Message("tool", toolExecutionResultMessage.text());
+ case UserMessage userMessage -> new Message("user", userMessage.singleText());
+ default -> throw new IllegalStateException("Unknown ChatMessage type: " + chatMessage);
+ }).collect(Collectors.toList());
+
+ TextGenerationRequest request = TextGenerationRequest
+ .builder()
+ .model(aiPreferences.getSelectedChatModel())
+ .messages(messages)
+ .temperature(aiPreferences.getTemperature())
+ .max_tokens(2048)
+ .build();
+
+ try {
+ String requestBody = gson.toJson(request);
+ String baseUrl = aiPreferences.getSelectedApiBaseUrl();
+ String fullUrl = baseUrl.endsWith("/") ? baseUrl + "chat/completions" : baseUrl + "/chat/completions";
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .uri(URI.create(fullUrl))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .timeout(Duration.ofSeconds(60))
+ .build();
+
+ HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+ LOGGER.info("Gpt4All response: {}", response.body());
+
+ TextGenerationResponse textGenerationResponse = gson.fromJson(response.body(), TextGenerationResponse.class);
+ if (textGenerationResponse.choices() == null || textGenerationResponse.choices().isEmpty()) {
+ throw new IllegalArgumentException("No choices returned in the response");
+ }
+
+ String generatedText = textGenerationResponse.choices().getFirst().message().content();
+ if (generatedText == null || generatedText.isEmpty()) {
+ throw new IllegalArgumentException("Generated text is null or empty");
+ }
+
+ // Note: We do not check the token usage and finish reason here.
+ // This class is not a complete implementation of langchain4j's ChatLanguageModel.
+ // We only implemented the functionality we specifically need.
+ return new Response<>(new AiMessage(generatedText), new TokenUsage(0, 0), FinishReason.OTHER);
+ } catch (Exception e) {
+ LOGGER.error("Error generating message from Gpt4All", e);
+ throw new RuntimeException("Failed to generate AI message", e);
+ }
+ }
+
+ private static class TextGenerationRequest {
+ protected final String model;
+ protected final List messages;
+ protected final Double temperature;
+ protected final Integer max_tokens;
+
+ private TextGenerationRequest(Builder builder) {
+ this.model = builder.model;
+ this.messages = builder.messages;
+ this.temperature = builder.temperature;
+ this.max_tokens = builder.max_tokens;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String model;
+ private List messages;
+ private Double temperature;
+ private Integer max_tokens;
+
+ public Builder model(String model) {
+ this.model = model;
+ return this;
+ }
+
+ public Builder messages(List messages) {
+ this.messages = messages;
+ return this;
+ }
+
+ public Builder temperature(Double temperature) {
+ this.temperature = temperature;
+ return this;
+ }
+
+ public Builder max_tokens(Integer max_tokens) {
+ this.max_tokens = max_tokens;
+ return this;
+ }
+
+ public TextGenerationRequest build() {
+ return new TextGenerationRequest(this);
+ }
+ }
+ }
+
+ private record TextGenerationResponse(List choices) { }
+
+ private record Choice(Message message) { }
+
+ private record Message(String role, String content) { }
+}
diff --git a/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java b/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java
index 92fc11df484..8445e15347d 100644
--- a/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java
+++ b/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java
@@ -10,6 +10,7 @@
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.chatting.AiChatLogic;
import org.jabref.logic.l10n.Localization;
+import org.jabref.model.ai.AiProvider;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import dev.langchain4j.data.message.AiMessage;
@@ -54,7 +55,7 @@ public JabRefChatLanguageModel(AiPreferences aiPreferences) {
*/
private void rebuild() {
String apiKey = aiPreferences.getApiKeyForAiProvider(aiPreferences.getAiProvider());
- if (!aiPreferences.getEnableAi() || apiKey.isEmpty()) {
+ if (!aiPreferences.getEnableAi() || (apiKey.isEmpty() && aiPreferences.getAiProvider() != AiProvider.GPT4ALL)) {
langchainChatModel = Optional.empty();
return;
}
@@ -64,6 +65,10 @@ private void rebuild() {
langchainChatModel = Optional.of(new JvmOpenAiChatLanguageModel(aiPreferences, httpClient));
}
+ case GPT4ALL-> {
+ langchainChatModel = Optional.of(new Gpt4AllModel(aiPreferences, httpClient));
+ }
+
case MISTRAL_AI -> {
langchainChatModel = Optional.of(MistralAiChatModel
.builder()
@@ -129,7 +134,7 @@ public Response generate(List list) {
if (langchainChatModel.isEmpty()) {
if (!aiPreferences.getEnableAi()) {
throw new RuntimeException(Localization.lang("In order to use AI chat, you need to enable chatting with attached PDF files in JabRef preferences (AI tab)."));
- } else if (aiPreferences.getApiKeyForAiProvider(aiPreferences.getAiProvider()).isEmpty()) {
+ } else if (aiPreferences.getApiKeyForAiProvider(aiPreferences.getAiProvider()).isEmpty() && aiPreferences.getAiProvider() != AiProvider.GPT4ALL) {
throw new RuntimeException(Localization.lang("In order to use AI chat, set an API key inside JabRef preferences (AI tab)."));
} else {
rebuild();
diff --git a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java
index 4940fba8b21..849040d4de0 100644
--- a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java
+++ b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java
@@ -12,6 +12,7 @@
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.processingstatus.ProcessingInfo;
import org.jabref.logic.ai.processingstatus.ProcessingState;
+import org.jabref.logic.ai.templates.TemplatesService;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.ProgressCounter;
@@ -36,6 +37,7 @@ public class GenerateSummaryForSeveralTask extends BackgroundTask {
private final BibDatabaseContext bibDatabaseContext;
private final SummariesStorage summariesStorage;
private final ChatLanguageModel chatLanguageModel;
+ private final TemplatesService templatesService;
private final ReadOnlyBooleanProperty shutdownSignal;
private final AiPreferences aiPreferences;
private final FilePreferences filePreferences;
@@ -51,6 +53,7 @@ public GenerateSummaryForSeveralTask(
BibDatabaseContext bibDatabaseContext,
SummariesStorage summariesStorage,
ChatLanguageModel chatLanguageModel,
+ TemplatesService templatesService,
ReadOnlyBooleanProperty shutdownSignal,
AiPreferences aiPreferences,
FilePreferences filePreferences,
@@ -61,6 +64,7 @@ public GenerateSummaryForSeveralTask(
this.bibDatabaseContext = bibDatabaseContext;
this.summariesStorage = summariesStorage;
this.chatLanguageModel = chatLanguageModel;
+ this.templatesService = templatesService;
this.shutdownSignal = shutdownSignal;
this.aiPreferences = aiPreferences;
this.filePreferences = filePreferences;
@@ -95,6 +99,7 @@ public Void call() throws Exception {
bibDatabaseContext,
summariesStorage,
chatLanguageModel,
+ templatesService,
shutdownSignal,
aiPreferences,
filePreferences
diff --git a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java
index f863c8f01e0..57fa54d405f 100644
--- a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java
+++ b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java
@@ -3,7 +3,6 @@
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -14,6 +13,8 @@
import org.jabref.logic.FilePreferences;
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.ingestion.FileToDocument;
+import org.jabref.logic.ai.templates.AiTemplate;
+import org.jabref.logic.ai.templates.TemplatesService;
import org.jabref.logic.ai.util.CitationKeyCheck;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
@@ -27,8 +28,6 @@
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.chat.ChatLanguageModel;
-import dev.langchain4j.model.input.Prompt;
-import dev.langchain4j.model.input.PromptTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,32 +41,6 @@
public class GenerateSummaryTask extends BackgroundTask {
private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSummaryTask.class);
- // Be careful when constructing prompt.
- // 1. It should contain variables `bullets` and `chunk`.
- // 2. Variables should be wrapped in `{{` and `}}` and only with them. No whitespace inside.
- private static final PromptTemplate CHUNK_PROMPT_TEMPLATE = PromptTemplate.from(
- """
- Please provide an overview of the following text. It's a part of a scientific paper.
- The summary should include the main objectives, methodologies used, key findings, and conclusions.
- Mention any significant experiments, data, or discussions presented in the paper.
-
- DOCUMENT:
- {{document}}
-
- OVERVIEW:"""
- );
-
- private static final PromptTemplate COMBINE_PROMPT_TEMPLATE = PromptTemplate.from(
- """
- You have written an overview of a scientific paper. You have been collecting notes from various parts
- of the paper. Now your task is to combine all of the notes in one structured message.
-
- SUMMARIES:
- {{summaries}}
-
- FINAL OVERVIEW:"""
- );
-
private static final int MAX_OVERLAP_SIZE_IN_CHARS = 100;
private static final int CHAR_TOKEN_FACTOR = 4; // Means, every token is roughly 4 characters.
@@ -76,6 +49,7 @@ public class GenerateSummaryTask extends BackgroundTask {
private final String citationKey;
private final ChatLanguageModel chatLanguageModel;
private final SummariesStorage summariesStorage;
+ private final TemplatesService templatesService;
private final ReadOnlyBooleanProperty shutdownSignal;
private final AiPreferences aiPreferences;
private final FilePreferences filePreferences;
@@ -86,6 +60,7 @@ public GenerateSummaryTask(BibEntry entry,
BibDatabaseContext bibDatabaseContext,
SummariesStorage summariesStorage,
ChatLanguageModel chatLanguageModel,
+ TemplatesService templatesService,
ReadOnlyBooleanProperty shutdownSignal,
AiPreferences aiPreferences,
FilePreferences filePreferences
@@ -95,6 +70,7 @@ public GenerateSummaryTask(BibEntry entry,
this.citationKey = entry.getCitationKey().orElse("");
this.chatLanguageModel = chatLanguageModel;
this.summariesStorage = summariesStorage;
+ this.templatesService = templatesService;
this.shutdownSignal = shutdownSignal;
this.aiPreferences = aiPreferences;
this.filePreferences = filePreferences;
@@ -222,7 +198,7 @@ private Optional generateSummary(LinkedFile linkedFile) throws Interrupt
public String summarizeOneDocument(String filePath, String document) throws InterruptedException {
addMoreWork(1); // For the combination of summary chunks.
- DocumentSplitter documentSplitter = DocumentSplitters.recursive(aiPreferences.getContextWindowSize() - MAX_OVERLAP_SIZE_IN_CHARS * 2 - estimateTokenCount(CHUNK_PROMPT_TEMPLATE), MAX_OVERLAP_SIZE_IN_CHARS);
+ DocumentSplitter documentSplitter = DocumentSplitters.recursive(aiPreferences.getContextWindowSize() - MAX_OVERLAP_SIZE_IN_CHARS * 2 - estimateTokenCount(aiPreferences.getTemplate(AiTemplate.SUMMARIZATION_CHUNK)), MAX_OVERLAP_SIZE_IN_CHARS);
List chunkSummaries = documentSplitter.split(new Document(document)).stream().map(TextSegment::text).toList();
@@ -243,10 +219,10 @@ public String summarizeOneDocument(String filePath, String document) throws Inte
throw new InterruptedException();
}
- Prompt prompt = CHUNK_PROMPT_TEMPLATE.apply(Collections.singletonMap("document", chunkSummary));
+ String prompt = templatesService.makeSummarizationChunk(chunkSummary);
LOGGER.debug("Sending request to AI provider to summarize a chunk from file \"{}\" of entry {}", filePath, citationKey);
- String chunk = chatLanguageModel.generate(prompt.toString());
+ String chunk = chatLanguageModel.generate(prompt);
LOGGER.debug("Chunk summary for file \"{}\" of entry {} was generated successfully", filePath, citationKey);
list.add(chunk);
@@ -254,7 +230,7 @@ public String summarizeOneDocument(String filePath, String document) throws Inte
}
chunkSummaries = list;
- } while (estimateTokenCount(chunkSummaries) > aiPreferences.getContextWindowSize() - estimateTokenCount(COMBINE_PROMPT_TEMPLATE));
+ } while (estimateTokenCount(chunkSummaries) > aiPreferences.getContextWindowSize() - estimateTokenCount(aiPreferences.getTemplate(AiTemplate.SUMMARIZATION_COMBINE)));
if (chunkSummaries.size() == 1) {
doneOneWork(); // No need to call LLM for combination of summary chunks.
@@ -262,14 +238,14 @@ public String summarizeOneDocument(String filePath, String document) throws Inte
return chunkSummaries.getFirst();
}
- Prompt prompt = COMBINE_PROMPT_TEMPLATE.apply(Collections.singletonMap("summaries", String.join("\n\n", chunkSummaries)));
+ String prompt = templatesService.makeSummarizationCombine(chunkSummaries);
if (shutdownSignal.get()) {
throw new InterruptedException();
}
LOGGER.debug("Sending request to AI provider to combine summary chunk(s) for file \"{}\" of entry {}", filePath, citationKey);
- String result = chatLanguageModel.generate(prompt.toString());
+ String result = chatLanguageModel.generate(prompt);
LOGGER.debug("Summary of the file \"{}\" of entry {} was generated successfully", filePath, citationKey);
doneOneWork();
@@ -284,10 +260,6 @@ private static int estimateTokenCount(List chunkSummaries) {
return chunkSummaries.stream().mapToInt(GenerateSummaryTask::estimateTokenCount).sum();
}
- private static int estimateTokenCount(PromptTemplate promptTemplate) {
- return estimateTokenCount(promptTemplate.template());
- }
-
private static int estimateTokenCount(String string) {
return estimateTokenCount(string.length());
}
diff --git a/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java b/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java
index afac5e823f5..faf8a4dece4 100644
--- a/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java
+++ b/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java
@@ -12,6 +12,7 @@
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.processingstatus.ProcessingInfo;
import org.jabref.logic.ai.processingstatus.ProcessingState;
+import org.jabref.logic.ai.templates.TemplatesService;
import org.jabref.logic.ai.util.CitationKeyCheck;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
@@ -44,6 +45,7 @@ public class SummariesService {
private final AiPreferences aiPreferences;
private final SummariesStorage summariesStorage;
private final ChatLanguageModel chatLanguageModel;
+ private final TemplatesService templatesService;
private final BooleanProperty shutdownSignal;
private final FilePreferences filePreferences;
private final TaskExecutor taskExecutor;
@@ -51,6 +53,7 @@ public class SummariesService {
public SummariesService(AiPreferences aiPreferences,
SummariesStorage summariesStorage,
ChatLanguageModel chatLanguageModel,
+ TemplatesService templatesService,
BooleanProperty shutdownSignal,
FilePreferences filePreferences,
TaskExecutor taskExecutor
@@ -58,6 +61,7 @@ public SummariesService(AiPreferences aiPreferences,
this.aiPreferences = aiPreferences;
this.summariesStorage = summariesStorage;
this.chatLanguageModel = chatLanguageModel;
+ this.templatesService = templatesService;
this.shutdownSignal = shutdownSignal;
this.filePreferences = filePreferences;
this.taskExecutor = taskExecutor;
@@ -134,7 +138,7 @@ public List> summarize(StringProperty groupNam
private void startSummarizationTask(BibEntry entry, BibDatabaseContext bibDatabaseContext, ProcessingInfo processingInfo) {
processingInfo.setState(ProcessingState.PROCESSING);
- new GenerateSummaryTask(entry, bibDatabaseContext, summariesStorage, chatLanguageModel, shutdownSignal, aiPreferences, filePreferences)
+ new GenerateSummaryTask(entry, bibDatabaseContext, summariesStorage, chatLanguageModel, templatesService, shutdownSignal, aiPreferences, filePreferences)
.onSuccess(processingInfo::setSuccess)
.onFailure(processingInfo::setException)
.executeWith(taskExecutor);
@@ -143,7 +147,7 @@ private void startSummarizationTask(BibEntry entry, BibDatabaseContext bibDataba
private void startSummarizationTask(StringProperty groupName, List> entries, BibDatabaseContext bibDatabaseContext) {
entries.forEach(processingInfo -> processingInfo.setState(ProcessingState.PROCESSING));
- new GenerateSummaryForSeveralTask(groupName, entries, bibDatabaseContext, summariesStorage, chatLanguageModel, shutdownSignal, aiPreferences, filePreferences, taskExecutor)
+ new GenerateSummaryForSeveralTask(groupName, entries, bibDatabaseContext, summariesStorage, chatLanguageModel, templatesService, shutdownSignal, aiPreferences, filePreferences, taskExecutor)
.executeWith(taskExecutor);
}
diff --git a/src/main/java/org/jabref/logic/ai/templates/AiTemplate.java b/src/main/java/org/jabref/logic/ai/templates/AiTemplate.java
new file mode 100644
index 00000000000..50feff648d3
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ai/templates/AiTemplate.java
@@ -0,0 +1,30 @@
+package org.jabref.logic.ai.templates;
+
+import org.jabref.logic.l10n.Localization;
+
+public enum AiTemplate {
+ // System message that is applied in the AI chat.
+ CHATTING_SYSTEM_MESSAGE,
+
+ // Template of a last user message in the AI chat with embeddings.
+ CHATTING_USER_MESSAGE,
+
+ // Template that is used to summarize the chunks of text.
+ SUMMARIZATION_CHUNK,
+
+ // Template that is used to combine the summarized chunks of text.
+ SUMMARIZATION_COMBINE;
+
+ public String getLocalizedName() {
+ return switch (this) {
+ case CHATTING_SYSTEM_MESSAGE ->
+ Localization.lang("System message for chatting");
+ case CHATTING_USER_MESSAGE ->
+ Localization.lang("User message for chatting");
+ case SUMMARIZATION_CHUNK ->
+ Localization.lang("Completion text for summarization of a chunk");
+ case SUMMARIZATION_COMBINE ->
+ Localization.lang("Completion text for summarization of several chunks");
+ };
+ }
+}
diff --git a/src/main/java/org/jabref/logic/ai/templates/PaperExcerpt.java b/src/main/java/org/jabref/logic/ai/templates/PaperExcerpt.java
new file mode 100644
index 00000000000..b8fbf810650
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ai/templates/PaperExcerpt.java
@@ -0,0 +1,3 @@
+package org.jabref.logic.ai.templates;
+
+public record PaperExcerpt(String citationKey, String text) { }
diff --git a/src/main/java/org/jabref/logic/ai/templates/TemplatesService.java b/src/main/java/org/jabref/logic/ai/templates/TemplatesService.java
new file mode 100644
index 00000000000..c130670d0bd
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ai/templates/TemplatesService.java
@@ -0,0 +1,63 @@
+package org.jabref.logic.ai.templates;
+
+import java.io.StringWriter;
+import java.util.List;
+
+import org.jabref.logic.ai.AiPreferences;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.CanonicalBibEntry;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+
+public class TemplatesService {
+ private final AiPreferences aiPreferences;
+ private final VelocityEngine velocityEngine = new VelocityEngine();
+ private final VelocityContext baseContext = new VelocityContext();
+
+ public TemplatesService(AiPreferences aiPreferences) {
+ this.aiPreferences = aiPreferences;
+
+ velocityEngine.init();
+
+ baseContext.put("CanonicalBibEntry", CanonicalBibEntry.class);
+ }
+
+ public String makeChattingSystemMessage(List entries) {
+ VelocityContext context = new VelocityContext(baseContext);
+ context.put("entries", entries);
+
+ return makeTemplate(AiTemplate.CHATTING_SYSTEM_MESSAGE, context);
+ }
+
+ public String makeChattingUserMessage(List entries, String message, List excerpts) {
+ VelocityContext context = new VelocityContext(baseContext);
+ context.put("entries", entries);
+ context.put("message", message);
+ context.put("excerpts", excerpts);
+
+ return makeTemplate(AiTemplate.CHATTING_USER_MESSAGE, context);
+ }
+
+ public String makeSummarizationChunk(String text) {
+ VelocityContext context = new VelocityContext(baseContext);
+ context.put("text", text);
+
+ return makeTemplate(AiTemplate.SUMMARIZATION_CHUNK, context);
+ }
+
+ public String makeSummarizationCombine(List chunks) {
+ VelocityContext context = new VelocityContext(baseContext);
+ context.put("chunks", chunks);
+
+ return makeTemplate(AiTemplate.SUMMARIZATION_COMBINE, context);
+ }
+
+ private String makeTemplate(AiTemplate template, VelocityContext context) {
+ StringWriter writer = new StringWriter();
+
+ velocityEngine.evaluate(context, writer, template.name(), aiPreferences.getTemplate(template));
+
+ return writer.toString();
+ }
+}
diff --git a/src/main/java/org/jabref/logic/citationkeypattern/CitationKeyGenerator.java b/src/main/java/org/jabref/logic/citationkeypattern/CitationKeyGenerator.java
index 9438194a2d2..a4eedc7ac18 100644
--- a/src/main/java/org/jabref/logic/citationkeypattern/CitationKeyGenerator.java
+++ b/src/main/java/org/jabref/logic/citationkeypattern/CitationKeyGenerator.java
@@ -21,23 +21,25 @@
* This is the utility class of the LabelPattern package.
*/
public class CitationKeyGenerator extends BracketedPattern {
+
/**
* All single characters that we can use for extending a key to make it unique.
*/
public static final String APPENDIX_CHARACTERS = "abcdefghijklmnopqrstuvwxyz";
- /**
- * List of unwanted characters. These will be removed at the end.
- * Note that +
is a wanted character to indicate "et al." in authorsAlpha.
- * Example: "ABC+". See {@link org.jabref.logic.citationkeypattern.BracketedPatternTest#authorsAlpha()} for examples.
- */
- public static final String DEFAULT_UNWANTED_CHARACTERS = "-`ʹ:!;?^";
+ /// List of unwanted characters. These will be removed at the end.
+ /// Note that `+` is a wanted character to indicate "et al." in authorsAlpha.
+ /// Example: `ABC+`. See {@link org.jabref.logic.citationkeypattern.BracketedPatternTest#authorsAlpha()} for examples.
+ ///
+ /// See also #DISALLOWED_CHARACTERS
+ public static final String DEFAULT_UNWANTED_CHARACTERS = "?!;^`ʹ";
- private static final Logger LOGGER = LoggerFactory.getLogger(CitationKeyGenerator.class);
-
- // Source of disallowed characters : https://tex.stackexchange.com/a/408548/9075
+ /// Source of disallowed characters:
+ /// These characters are disallowed in BibTeX keys.
private static final List DISALLOWED_CHARACTERS = Arrays.asList('{', '}', '(', ')', ',', '=', '\\', '"', '#', '%', '~', '\'');
+ private static final Logger LOGGER = LoggerFactory.getLogger(CitationKeyGenerator.class);
+
private final AbstractCitationKeyPatterns citeKeyPattern;
private final BibDatabase database;
private final CitationKeyPatternPreferences citationKeyPatternPreferences;
diff --git a/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java b/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java
index 8f15e8a4d5a..6c8d8e1be90 100644
--- a/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java
+++ b/src/main/java/org/jabref/logic/cleanup/CleanupWorker.java
@@ -66,9 +66,9 @@ private CleanupJob toJob(CleanupPreferences.CleanupStep action) {
case MAKE_PATHS_RELATIVE ->
new RelativePathsCleanup(databaseContext, filePreferences);
case RENAME_PDF ->
- new RenamePdfCleanup(false, databaseContext, filePreferences);
+ new RenamePdfCleanup(false, () -> databaseContext, filePreferences);
case RENAME_PDF_ONLY_RELATIVE_PATHS ->
- new RenamePdfCleanup(true, databaseContext, filePreferences);
+ new RenamePdfCleanup(true, () -> databaseContext, filePreferences);
case CLEAN_UP_UPGRADE_EXTERNAL_LINKS ->
new UpgradePdfPsToFileCleanup();
case CLEAN_UP_DELETED_LINKED_FILES ->
@@ -82,7 +82,7 @@ private CleanupJob toJob(CleanupPreferences.CleanupStep action) {
case CONVERT_TIMESTAMP_TO_MODIFICATIONDATE ->
new TimeStampToModificationDate(timestampPreferences);
case MOVE_PDF ->
- new MoveFilesCleanup(databaseContext, filePreferences);
+ new MoveFilesCleanup(() -> databaseContext, filePreferences);
case FIX_FILE_LINKS ->
new FileLinksCleanup();
case CLEAN_UP_ISSN ->
diff --git a/src/main/java/org/jabref/logic/cleanup/MoveFilesCleanup.java b/src/main/java/org/jabref/logic/cleanup/MoveFilesCleanup.java
index 3f7419239c4..c1cec043d88 100644
--- a/src/main/java/org/jabref/logic/cleanup/MoveFilesCleanup.java
+++ b/src/main/java/org/jabref/logic/cleanup/MoveFilesCleanup.java
@@ -6,6 +6,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.function.Supplier;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.JabRefException;
@@ -27,11 +28,11 @@ public class MoveFilesCleanup implements CleanupJob {
private static final Logger LOGGER = LoggerFactory.getLogger(MoveFilesCleanup.class);
- private final BibDatabaseContext databaseContext;
+ private final Supplier databaseContext;
private final FilePreferences filePreferences;
private final List ioExceptions;
- public MoveFilesCleanup(BibDatabaseContext databaseContext, FilePreferences filePreferences) {
+ public MoveFilesCleanup(Supplier databaseContext, FilePreferences filePreferences) {
this.databaseContext = Objects.requireNonNull(databaseContext);
this.filePreferences = Objects.requireNonNull(filePreferences);
this.ioExceptions = new ArrayList<>();
@@ -43,7 +44,7 @@ public List cleanup(BibEntry entry) {
boolean changed = false;
for (LinkedFile file : files) {
- LinkedFileHandler fileHandler = new LinkedFileHandler(file, entry, databaseContext, filePreferences);
+ LinkedFileHandler fileHandler = new LinkedFileHandler(file, entry, databaseContext.get(), filePreferences);
try {
changed = fileHandler.moveToDefaultDirectory() || changed;
} catch (IOException exception) {
diff --git a/src/main/java/org/jabref/logic/cleanup/RenamePdfCleanup.java b/src/main/java/org/jabref/logic/cleanup/RenamePdfCleanup.java
index 5152fddee41..674596df2bd 100644
--- a/src/main/java/org/jabref/logic/cleanup/RenamePdfCleanup.java
+++ b/src/main/java/org/jabref/logic/cleanup/RenamePdfCleanup.java
@@ -6,6 +6,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.function.Supplier;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.externalfiles.LinkedFileHandler;
@@ -21,11 +22,11 @@
public class RenamePdfCleanup implements CleanupJob {
private static final Logger LOGGER = LoggerFactory.getLogger(RenamePdfCleanup.class);
- private final BibDatabaseContext databaseContext;
+ private final Supplier databaseContext;
private final boolean onlyRelativePaths;
private final FilePreferences filePreferences;
- public RenamePdfCleanup(boolean onlyRelativePaths, BibDatabaseContext databaseContext, FilePreferences filePreferences) {
+ public RenamePdfCleanup(boolean onlyRelativePaths, Supplier databaseContext, FilePreferences filePreferences) {
this.databaseContext = Objects.requireNonNull(databaseContext);
this.onlyRelativePaths = onlyRelativePaths;
this.filePreferences = filePreferences;
@@ -41,7 +42,7 @@ public List cleanup(BibEntry entry) {
continue;
}
- LinkedFileHandler fileHandler = new LinkedFileHandler(file, entry, databaseContext, filePreferences);
+ LinkedFileHandler fileHandler = new LinkedFileHandler(file, entry, databaseContext.get(), filePreferences);
try {
boolean changedFile = fileHandler.renameToSuggestedName();
if (changedFile) {
diff --git a/src/main/java/org/jabref/logic/externalfiles/ExternalFilesContentImporter.java b/src/main/java/org/jabref/logic/externalfiles/ExternalFilesContentImporter.java
index e75b1df2683..7a868577c42 100644
--- a/src/main/java/org/jabref/logic/externalfiles/ExternalFilesContentImporter.java
+++ b/src/main/java/org/jabref/logic/externalfiles/ExternalFilesContentImporter.java
@@ -3,10 +3,12 @@
import java.io.IOException;
import java.nio.file.Path;
+import org.jabref.logic.FilePreferences;
import org.jabref.logic.importer.ImportFormatPreferences;
import org.jabref.logic.importer.OpenDatabase;
import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.importer.fileformat.PdfMergeMetadataImporter;
+import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.util.FileUpdateMonitor;
public class ExternalFilesContentImporter {
@@ -17,9 +19,9 @@ public ExternalFilesContentImporter(ImportFormatPreferences importFormatPreferen
this.importFormatPreferences = importFormatPreferences;
}
- public ParserResult importPDFContent(Path file) {
+ public ParserResult importPDFContent(Path file, BibDatabaseContext context, FilePreferences filePreferences) {
try {
- return new PdfMergeMetadataImporter(importFormatPreferences).importDatabase(file);
+ return new PdfMergeMetadataImporter(importFormatPreferences).importDatabase(file, context, filePreferences);
} catch (IOException e) {
return ParserResult.fromError(e);
}
diff --git a/src/main/java/org/jabref/logic/help/HelpFile.java b/src/main/java/org/jabref/logic/help/HelpFile.java
index c4f91c54153..7ead357ec38 100644
--- a/src/main/java/org/jabref/logic/help/HelpFile.java
+++ b/src/main/java/org/jabref/logic/help/HelpFile.java
@@ -48,7 +48,8 @@ public enum HelpFile {
SQL_DATABASE_MIGRATION("collaborative-work/sqldatabase/sqldatabasemigration"),
PUSH_TO_APPLICATION("cite/pushtoapplications"),
AI_GENERAL_SETTINGS("ai/preferences"),
- AI_EXPERT_SETTINGS("ai/preferences#ai-expert-settings");
+ AI_EXPERT_SETTINGS("ai/preferences#ai-expert-settings"),
+ AI_TEMPLATES("ai/preferences#templates");
private final String pageName;
diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CitaviXmlImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CitaviXmlImporter.java
index 78b0400932c..9bed9fdeadb 100644
--- a/src/main/java/org/jabref/logic/importer/fileformat/CitaviXmlImporter.java
+++ b/src/main/java/org/jabref/logic/importer/fileformat/CitaviXmlImporter.java
@@ -462,10 +462,11 @@ private BufferedReader getReaderFromZip(Path filePath) throws IOException {
// Solution inspired by https://stackoverflow.com/a/37445972/873282
return new BufferedReader(
new InputStreamReader(
- new BOMInputStream(
- Files.newInputStream(newFile, StandardOpenOption.READ),
- false,
- ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE)));
+ new BOMInputStream.Builder()
+ .setInputStream(Files.newInputStream(newFile, StandardOpenOption.READ))
+ .setInclude(false)
+ .setByteOrderMarks(ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE)
+ .get()));
}
private String clean(String input) {
diff --git a/src/main/java/org/jabref/logic/importer/fileformat/PdfContentImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/PdfContentImporter.java
index ef4c25321e5..ca266e8c895 100644
--- a/src/main/java/org/jabref/logic/importer/fileformat/PdfContentImporter.java
+++ b/src/main/java/org/jabref/logic/importer/fileformat/PdfContentImporter.java
@@ -5,10 +5,13 @@
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -26,9 +29,13 @@
import org.jabref.model.entry.types.StandardEntryType;
import org.jabref.model.strings.StringUtil;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
+import org.apache.pdfbox.text.TextPosition;
+
+import static org.jabref.model.strings.StringUtil.isNullOrEmpty;
/**
* PdfContentImporter parses data of the first page of the PDF and creates a BibTeX entry.
@@ -196,7 +203,8 @@ public ParserResult importDatabase(Path filePath) {
List result = new ArrayList<>(1);
try (PDDocument document = new XmpUtilReader().loadWithAutomaticDecryption(filePath)) {
String firstPageContents = getFirstPageContents(document);
- Optional entry = getEntryFromPDFContent(firstPageContents, OS.NEWLINE);
+ String titleByFontSize = extractTitleFromDocument(document);
+ Optional entry = getEntryFromPDFContent(firstPageContents, OS.NEWLINE, titleByFontSize);
entry.ifPresent(result::add);
} catch (EncryptedPdfsNotSupportedException e) {
return ParserResult.fromErrorMessage(Localization.lang("Decryption not supported."));
@@ -208,17 +216,125 @@ public ParserResult importDatabase(Path filePath) {
return new ParserResult(result);
}
- // make this method package visible so we can test it
- Optional getEntryFromPDFContent(String firstpageContents, String lineSeparator) {
- // idea: split[] contains the different lines
- // blocks are separated by empty lines
- // treat each block
- // or do special treatment at authors (which are not broken)
- // therefore, we do a line-based and not a block-based splitting
- // i points to the current line
- // curString (mostly) contains the current block
- // the different lines are joined into one and thereby separated by " "
+ private static String extractTitleFromDocument(PDDocument document) throws IOException {
+ TitleExtractorByFontSize stripper = new TitleExtractorByFontSize();
+ return stripper.getTitleFromFirstPage(document);
+ }
+
+ private static class TitleExtractorByFontSize extends PDFTextStripper {
+
+ private final List textPositionsList;
+
+ public TitleExtractorByFontSize() {
+ super();
+ this.textPositionsList = new ArrayList<>();
+ }
+
+ public String getTitleFromFirstPage(PDDocument document) throws IOException {
+ this.setStartPage(1);
+ this.setEndPage(1);
+ this.writeText(document, new StringWriter());
+ return findLargestFontText(textPositionsList);
+ }
+
+ @Override
+ protected void writeString(String text, List textPositions) {
+ textPositionsList.addAll(textPositions);
+ }
+
+ private boolean isFarAway(TextPosition previous, TextPosition current) {
+ float XspaceThreshold = 3.0F;
+ float YspaceThreshold = previous.getFontSizeInPt() * 1.5F;
+ float Xgap = current.getXDirAdj() - (previous.getXDirAdj() + previous.getWidthDirAdj());
+ float Ygap = current.getYDirAdj() - (previous.getYDirAdj() - previous.getHeightDir());
+ return Xgap > XspaceThreshold && Ygap > YspaceThreshold;
+ }
+ private boolean isUnwantedText(TextPosition previousTextPosition, TextPosition textPosition) {
+ if (textPosition == null || previousTextPosition == null) {
+ return false;
+ }
+ if (StringUtil.isBlank(textPosition.getUnicode())) {
+ return true;
+ }
+ // The title usually don't in the bottom 10% of a page.
+ if ((textPosition.getPageHeight() - textPosition.getYDirAdj())
+ < (textPosition.getPageHeight() * 0.1)) {
+ return true;
+ }
+ // The title character usually stay together.
+ return isFarAway(previousTextPosition, textPosition);
+ }
+
+ private String findLargestFontText(List textPositions) {
+ Map fontSizeTextMap = new TreeMap<>(Collections.reverseOrder());
+ TextPosition previousTextPosition = null;
+ for (TextPosition textPosition : textPositions) {
+ // Exclude unwanted text based on heuristics
+ if (isUnwantedText(previousTextPosition, textPosition)) {
+ continue;
+ }
+ float fontSize = textPosition.getFontSizeInPt();
+ fontSizeTextMap.putIfAbsent(fontSize, new StringBuilder());
+ if (previousTextPosition != null && isThereSpace(previousTextPosition, textPosition)) {
+ fontSizeTextMap.get(fontSize).append(" ");
+ }
+ fontSizeTextMap.get(fontSize).append(textPosition.getUnicode());
+ previousTextPosition = textPosition;
+ }
+ for (Map.Entry entry : fontSizeTextMap.entrySet()) {
+ String candidateText = entry.getValue().toString().trim();
+ if (isLegalTitle(candidateText)) {
+ return candidateText;
+ }
+ }
+ return fontSizeTextMap.values().iterator().next().toString().trim();
+ }
+
+ private boolean isLegalTitle(String candidateText) {
+ // The minimum title length typically observed in academic research is 4 characters.
+ return candidateText.length() >= 4;
+ }
+
+ private boolean isThereSpace(TextPosition previous, TextPosition current) {
+ float XspaceThreshold = 1F;
+ float YspaceThreshold = previous.getFontSizeInPt();
+ float Xgap = current.getXDirAdj() - (previous.getXDirAdj() + previous.getWidthDirAdj());
+ float Ygap = current.getYDirAdj() - (previous.getYDirAdj() - previous.getHeightDir());
+ return Math.abs(Xgap) > XspaceThreshold || Math.abs(Ygap) > YspaceThreshold;
+ }
+ }
+
+ /**
+ * Parses the first page content of a PDF document and extracts bibliographic information such as title, author,
+ * abstract, keywords, and other relevant metadata. This method processes the content line-by-line and uses
+ * custom parsing logic to identify and assemble information blocks from academic papers.
+ *
+ * idea: split[] contains the different lines, blocks are separated by empty lines, treat each block
+ * or do special treatment at authors (which are not broken).
+ * Therefore, we do a line-based and not a block-based splitting i points to the current line
+ * curString (mostly) contains the current block,
+ * the different lines are joined into one and thereby separated by " "
+ *
+ * This method follows the structure typically found in academic paper PDFs:
+ * - First, it attempts to detect the title by font size, if available, or by text position.
+ * - Authors are then processed line-by-line until reaching the next section.
+ * - Abstract and keywords, if found, are extracted as they appear on the page.
+ * - Finally, conference details, DOI, and publication information are parsed from the lower blocks.
+ *
+ *
The parsing logic also identifies and categorizes entries based on keywords such as "Abstract" or "Keywords"
+ * and specific terms that denote sections. Additionally, this method can handle
+ * publisher-specific formats like Springer or IEEE, extracting data like series, volume, and conference titles.
+ *
+ * @param firstpageContents The raw content of the PDF's first page, which may contain metadata and main content.
+ * @param lineSeparator The line separator used to format and unify line breaks in the text content.
+ * @param titleByFontSize An optional title string determined by font size; if provided, this overrides the
+ * default title parsing.
+ * @return An {@link Optional} containing a {@link BibEntry} with the parsed bibliographic data if extraction
+ * is successful. Otherwise, an empty {@link Optional}.
+ */
+ @VisibleForTesting
+ Optional getEntryFromPDFContent(String firstpageContents, String lineSeparator, String titleByFontSize) {
String firstpageContentsUnifiedLineBreaks = StringUtil.unifyLineBreaks(firstpageContents, lineSeparator);
lines = firstpageContentsUnifiedLineBreaks.split(lineSeparator);
@@ -275,8 +391,11 @@ Optional getEntryFromPDFContent(String firstpageContents, String lineS
// start: title
fillCurStringWithNonEmptyLines();
title = streamlineTitle(curString);
- curString = "";
// i points to the next non-empty line
+ curString = "";
+ if (!isNullOrEmpty(titleByFontSize)) {
+ title = titleByFontSize;
+ }
// after title: authors
author = null;
@@ -393,13 +512,6 @@ Optional getEntryFromPDFContent(String firstpageContents, String lineS
// IEEE has the conference things at the end
publisher = "IEEE";
- // year is extracted by extractYear
- // otherwise, we could it determine as follows:
- // String yearStr = curString.substring(curString.length()-4);
- // if (isYear(yearStr)) {
- // year = yearStr;
- // }
-
if (conference == null) {
pos = curString.indexOf('$');
if (pos > 0) {
diff --git a/src/main/java/org/jabref/logic/importer/fileformat/PdfMergeMetadataImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/PdfMergeMetadataImporter.java
index da023360ad0..21b18e27f84 100644
--- a/src/main/java/org/jabref/logic/importer/fileformat/PdfMergeMetadataImporter.java
+++ b/src/main/java/org/jabref/logic/importer/fileformat/PdfMergeMetadataImporter.java
@@ -136,6 +136,25 @@ public ParserResult importDatabase(Path filePath) throws IOException {
return new ParserResult(List.of(entry));
}
+ /**
+ * A modified version of {@link PdfMergeMetadataImporter#importDatabase(Path)}, but it
+ * relativizes the {@code filePath} if there are working directories before parsing it
+ * into {@link PdfMergeMetadataImporter#importDatabase(Path)}
+ * (Otherwise no path modification happens).
+ *
+ * @param filePath The unrelativized {@code filePath}.
+ */
+ public ParserResult importDatabase(Path filePath, BibDatabaseContext context, FilePreferences filePreferences) throws IOException {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(filePreferences);
+
+ List directories = context.getFileDirectories(filePreferences);
+
+ filePath = FileUtil.relativize(filePath, directories);
+
+ return importDatabase(filePath);
+ }
+
@Override
public String getName() {
return "PDF meta data merger";
diff --git a/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java b/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java
index c58c84b4bcd..04de98b71d9 100644
--- a/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java
+++ b/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java
@@ -7,14 +7,14 @@
public class GrobidPreferences {
private final BooleanProperty grobidEnabled;
- private final BooleanProperty grobidOptOut;
+ private final BooleanProperty grobidUseAsked;
private final StringProperty grobidURL;
public GrobidPreferences(boolean grobidEnabled,
- boolean grobidOptOut,
+ boolean grobidUseAsked,
String grobidURL) {
this.grobidEnabled = new SimpleBooleanProperty(grobidEnabled);
- this.grobidOptOut = new SimpleBooleanProperty(grobidOptOut);
+ this.grobidUseAsked = new SimpleBooleanProperty(grobidUseAsked);
this.grobidURL = new SimpleStringProperty(grobidURL);
}
@@ -30,19 +30,19 @@ public void setGrobidEnabled(boolean grobidEnabled) {
this.grobidEnabled.set(grobidEnabled);
}
- // region: optout; models "Do not ask again" option
- public boolean isGrobidOptOut() {
- return grobidOptOut.get();
+ // region: GrobidUseAsked;
+ public boolean isGrobidUseAsked() {
+ return grobidUseAsked.get();
}
- public BooleanProperty grobidOptOutProperty() {
- return grobidOptOut;
+ public BooleanProperty grobidUseAskedProperty() {
+ return grobidUseAsked;
}
- public void setGrobidOptOut(boolean grobidOptOut) {
- this.grobidOptOut.set(grobidOptOut);
+ public void setGrobidUseAsked(boolean grobidUseAsked) {
+ this.grobidUseAsked.set(grobidUseAsked);
}
- // endregion: optout
+ // endregion: GrobidUseAsked
public String getGrobidURL() {
return grobidURL.get();
diff --git a/src/main/java/org/jabref/logic/openoffice/action/Update.java b/src/main/java/org/jabref/logic/openoffice/action/Update.java
index 7294842e732..8c31ee37e68 100644
--- a/src/main/java/org/jabref/logic/openoffice/action/Update.java
+++ b/src/main/java/org/jabref/logic/openoffice/action/Update.java
@@ -64,8 +64,7 @@ private static List updateDocument(XTextDocument doc,
}
return frontend.citationGroups.getUnresolvedKeys();
- } catch (
- IOException e) {
+ } catch (IOException e) {
Logger.warn("Error while updating document", e);
} finally {
if (useLockControllers && UnoScreenRefresh.hasControllersLocked(doc)) {
diff --git a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java
index 865806947f3..e9c611f02ec 100644
--- a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java
+++ b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java
@@ -69,7 +69,6 @@ public void insertCitation(XTextCursor cursor, CitationStyle selectedStyle, List
OOText ooText = OOFormat.setLocaleNone(OOText.fromString(formattedCitation));
insertReferences(cursor, entries, ooText, selectedStyle.isNumericStyle());
- cursor.collapseToEnd();
}
/**
@@ -119,7 +118,6 @@ public void insertInTextCitation(XTextCursor cursor, CitationStyle selectedStyle
}
OOText ooText = OOFormat.setLocaleNone(OOText.fromString(finalText));
insertReferences(cursor, List.of(currentEntry), ooText, selectedStyle.isNumericStyle());
- cursor.collapseToEnd();
}
}
@@ -131,9 +129,6 @@ public void insertEmpty(XTextCursor cursor, CitationStyle selectedStyle, List entries, OOText
CSLReferenceMark mark = markManager.createReferenceMark(entries);
mark.insertReferenceIntoOO(document, cursor, ooText, !preceedingSpaceExists, false);
- // Move the cursor to the end of the inserted text
- cursor.collapseToEnd();
-
markManager.setUpdateRequired(isNumericStyle);
readAndUpdateExistingMarks();
- cursor.collapseToEnd();
}
/**
diff --git a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java
index 1ac64172287..525a1803be5 100644
--- a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java
+++ b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java
@@ -72,6 +72,15 @@ public static String transformHTML(String html) {
// Clean up any remaining span tags
html = html.replaceAll("?span[^>]*>", "");
+ // Convert line breaks to paragraph breaks
+ html = html.replaceAll("[\n\r]+", "");
+
+ // Remove leading paragraph tags (preserving any whitespaces after them for indentation)
+ html = html.replaceAll("^\\s*\\s*
", "");
+
+ // Remove extra trailing paragraph tags when there are multiple (keeping one)
+ html = html.replaceAll("(?:\\s*
\\s*){2,}$", "");
+
return html;
}
@@ -113,9 +122,9 @@ public static String generateAlphanumericCitation(List entries, BibDat
/**
* Method to update citation number of a bibliographic entry (to be inserted in the list of references).
- * By default, citeproc-java ({@link org.jabref.logic.citationstyle.CitationStyleGenerator#generateBibliography(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateBibliography} always start the numbering of a list of citations with "1".
- * If a citation doesn't correspond to the first cited entry, the number should be changed to the relevant current citation number.
- * If an entries has been cited before, the colder number should be reused.
+ * By default, citeproc-java ({@link org.jabref.logic.citationstyle.CitationStyleGenerator#generateBibliography(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateBibliography} always starts the numbering of a list of citations with "1".
+ * If a citation doesn't correspond to the first cited entry, the number should be changed to the appropriate current citation number.
+ * The numbers should be globally unique. If an entry has been cited before, the older citation number corresponding to it should be reused.
* The number can be enclosed in different formats, such as "1", "1.", "1)", "(1)" or "[1]".
*
* Precondition: Use ONLY with numeric citation styles.
diff --git a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLUpdateBibliography.java b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLUpdateBibliography.java
index 107974934b4..732db3899ec 100644
--- a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLUpdateBibliography.java
+++ b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLUpdateBibliography.java
@@ -101,9 +101,7 @@ private void populateCSLBibTextSection(XTextDocument doc,
// Use CSLCitationOOAdapter to insert the bibliography
cslAdapter.insertBibliography(cursor, citationStyle, entries, bibDatabaseContext, bibEntryTypesManager);
- LOGGER.debug("Bibliography inserted using CSLCitationOOAdapter");
- cursor.collapseToEnd();
LOGGER.debug("CSL bibliography section population completed");
}
}
diff --git a/src/main/java/org/jabref/logic/openoffice/style/JStyle.java b/src/main/java/org/jabref/logic/openoffice/style/JStyle.java
index d94c71ab4df..8ab93ca939c 100644
--- a/src/main/java/org/jabref/logic/openoffice/style/JStyle.java
+++ b/src/main/java/org/jabref/logic/openoffice/style/JStyle.java
@@ -130,14 +130,14 @@ public class JStyle implements Comparable, OOStyle {
private final Map bibLayout = new HashMap<>();
private final Map properties = new HashMap<>();
private final Map citProperties = new HashMap<>();
- private final boolean fromResource;
+ private boolean fromResource;
private final String path;
private final LayoutFormatterPreferences layoutPreferences;
private final JournalAbbreviationRepository abbreviationRepository;
private String name = "";
private Layout defaultBibLayout;
private boolean valid;
- private Path styleFile;
+ private final Path styleFile;
private long styleFileModificationTime = Long.MIN_VALUE;
private String localCopy;
private boolean isDefaultLayoutPresent;
@@ -158,8 +158,15 @@ public JStyle(String resourcePath, LayoutFormatterPreferences layoutPreferences,
Objects.requireNonNull(resourcePath);
setDefaultProperties();
- initialize(JStyle.class.getResourceAsStream(resourcePath));
+ // we need to distinguish if it's a style from the local resources or a file on disk
+ InputStream stream = JStyle.class.getResourceAsStream(resourcePath);
+ styleFile = Path.of(resourcePath);
fromResource = true;
+ if (stream == null) {
+ stream = Files.newInputStream(styleFile);
+ fromResource = false;
+ }
+ initialize(stream);
path = resourcePath;
}
@@ -251,7 +258,7 @@ private void initialize(InputStream stream) throws IOException {
* If this style was initialized from a file on disk, reload the style
* if the file has been modified since it was read.
*
- * @throws IOException
+ * @throws IOException in case of errors
*/
public void ensureUpToDate() throws IOException {
if (!isUpToDate()) {
@@ -263,7 +270,7 @@ public void ensureUpToDate() throws IOException {
* If this style was initialized from a file on disk, reload the style
* information.
*
- * @throws IOException
+ * @throws IOException in case of error
*/
private void reload() throws IOException {
if (styleFile != null) {
@@ -563,9 +570,9 @@ public boolean equals(Object object) {
}
if (object instanceof JStyle otherStyle) {
return Objects.equals(path, otherStyle.path)
- && Objects.equals(name, otherStyle.name)
- && Objects.equals(citProperties, otherStyle.citProperties)
- && Objects.equals(properties, otherStyle.properties);
+ && Objects.equals(name, otherStyle.name)
+ && Objects.equals(citProperties, otherStyle.citProperties)
+ && Objects.equals(properties, otherStyle.properties);
}
return false;
}
@@ -584,22 +591,25 @@ enum BibStyleMode {
JOURNALS
}
- /** The String to represent authors that are not mentioned,
+ /**
+ * The String to represent authors that are not mentioned,
* e.g. " et al."
*/
public String getEtAlString() {
return getStringCitProperty(JStyle.ET_AL_STRING);
}
- /** The String to add between author names except the last two:
- * "[Smith{, }Jones and Brown]"
+ /**
+ * The String to add between author names except the last two:
+ * "[Smith{, }Jones and Brown]"
*/
protected String getAuthorSeparator() {
return getStringCitProperty(JStyle.AUTHOR_SEPARATOR);
}
- /** The String to put after the second to last author in case
- * of three or more authors: (A, B{,} and C)
+ /**
+ * The String to put after the second to last author in case
+ * of three or more authors: (A, B{,} and C)
*/
protected String getOxfordComma() {
return getStringCitProperty(JStyle.OXFORD_COMMA);
@@ -684,8 +694,8 @@ protected void setDefaultBibLayout(Layout layout) {
public OOText getNumCitationMarker2(List entries) {
final int minGroupingCount = this.getMinimumGroupingCount();
return JStyleGetNumCitationMarker.getNumCitationMarker2(this,
- entries,
- minGroupingCount);
+ entries,
+ minGroupingCount);
}
/**
@@ -694,8 +704,8 @@ public OOText getNumCitationMarker2(List entries) {
public OOText getNumCitationMarker2(List entries,
int minGroupingCount) {
return JStyleGetNumCitationMarker.getNumCitationMarker2(this,
- entries,
- minGroupingCount);
+ entries,
+ minGroupingCount);
}
/**
@@ -715,38 +725,33 @@ public OOText getNormalizedCitationMarker(CitationMarkerNormEntry entry) {
* citationMarkerEntries argument. If successive entries within
* the citation are uniquefied from each other, this method will
* perform a grouping of these entries.
- *
+ *
* If successive entries within the citation are uniquefied from
* each other, this method will perform a grouping of these
* entries.
*
- * @param citationMarkerEntries The list of entries providing the
- * data.
- *
- * @param inParenthesis Signals whether a parenthesized citation
- * or an in-text citation is wanted.
- *
- * @param nonUniqueCitationMarkerHandling
- *
- * THROWS : Should throw if finds that uniqueLetters
- * provided do not make the entries unique.
- *
- * FORGIVEN : is needed to allow preliminary markers
- * for freshly inserted citations without
- * going throw the uniquefication process.
- *
+ * @param citationMarkerEntries The list of entries providing the
+ * data.
+ * @param inParenthesis Signals whether a parenthesized citation
+ * or an in-text citation is wanted.
+ * @param nonUniqueCitationMarkerHandling THROWS : Should throw if finds that uniqueLetters
+ * provided do not make the entries unique.
+ *
+ * FORGIVEN : is needed to allow preliminary markers
+ * for freshly inserted citations without
+ * going throw the uniquefication process.
* @return The formatted citation. The result does not include
- * the standard wrappers:
- * OOFormat.setLocaleNone() and OOFormat.setCharStyle().
- * These are added by decorateCitationMarker()
+ * the standard wrappers:
+ * OOFormat.setLocaleNone() and OOFormat.setCharStyle().
+ * These are added by decorateCitationMarker()
*/
public OOText createCitationMarker(List citationMarkerEntries,
boolean inParenthesis,
NonUniqueCitationMarker nonUniqueCitationMarkerHandling) {
return JStyleGetCitationMarker.createCitationMarker(this,
- citationMarkerEntries,
- inParenthesis,
- nonUniqueCitationMarkerHandling);
+ citationMarkerEntries,
+ inParenthesis,
+ nonUniqueCitationMarkerHandling);
}
/**
@@ -791,7 +796,6 @@ private String getStringProperty(String propName) {
/**
* Should citation markers be italicized?
- *
*/
public String getCitationGroupMarkupBefore() {
return getStringCitProperty(CITATION_GROUP_MARKUP_BEFORE);
@@ -801,7 +805,9 @@ public String getCitationGroupMarkupAfter() {
return getStringCitProperty(CITATION_GROUP_MARKUP_AFTER);
}
- /** Author list, including " et al." */
+ /**
+ * Author list, including " et al."
+ */
public String getAuthorsPartMarkupBefore() {
return getStringCitProperty(AUTHORS_PART_MARKUP_BEFORE);
}
@@ -810,7 +816,9 @@ public String getAuthorsPartMarkupAfter() {
return getStringCitProperty(AUTHORS_PART_MARKUP_AFTER);
}
- /** Author list, excluding " et al." */
+ /**
+ * Author list, excluding " et al."
+ */
public String getAuthorNamesListMarkupBefore() {
return getStringCitProperty(AUTHOR_NAMES_LIST_MARKUP_BEFORE);
}
@@ -819,7 +827,9 @@ public String getAuthorNamesListMarkupAfter() {
return getStringCitProperty(AUTHOR_NAMES_LIST_MARKUP_AFTER);
}
- /** Author names. Excludes Author separators */
+ /**
+ * Author names. Excludes Author separators
+ */
public String getAuthorNameMarkupBefore() {
return getStringCitProperty(AUTHOR_NAME_MARKUP_BEFORE);
}
@@ -840,8 +850,8 @@ public boolean getItalicEtAl() {
}
/**
- * @return Names of fields containing authors: the first
- * non-empty field will be used.
+ * @return Names of fields containing authors: the first
+ * non-empty field will be used.
*/
protected OrFields getAuthorFieldNames() {
String authorFieldNamesString = this.getStringCitProperty(JStyle.AUTHOR_FIELD);
@@ -849,7 +859,7 @@ protected OrFields getAuthorFieldNames() {
}
/**
- * @return Field containing year, with fallback fields.
+ * @return Field containing year, with fallback fields.
*/
protected OrFields getYearFieldNames() {
String yearFieldNamesString = this.getStringCitProperty(JStyle.YEAR_FIELD);
@@ -888,8 +898,9 @@ protected String getYearSeparatorInText() {
return getStringCitProperty(JStyle.IN_TEXT_YEAR_SEPARATOR);
}
- /** The maximum number of authors to write out in full without
- * using "et al." Set to -1 to always write out all authors.
+ /**
+ * The maximum number of authors to write out in full without
+ * using "et al." Set to -1 to always write out all authors.
*/
protected int getMaxAuthors() {
return getIntCitProperty(JStyle.MAX_AUTHORS);
@@ -899,17 +910,23 @@ public int getMaxAuthorsFirst() {
return getIntCitProperty(JStyle.MAX_AUTHORS_FIRST);
}
- /** Opening parenthesis before citation (or year, for in-text) */
+ /**
+ * Opening parenthesis before citation (or year, for in-text)
+ */
protected String getBracketBefore() {
return getStringCitProperty(JStyle.BRACKET_BEFORE);
}
- /** Closing parenthesis after citation */
+ /**
+ * Closing parenthesis after citation
+ */
protected String getBracketAfter() {
return getStringCitProperty(JStyle.BRACKET_AFTER);
}
- /** Opening parenthesis before citation marker in the bibliography. */
+ /**
+ * Opening parenthesis before citation marker in the bibliography.
+ */
private String getBracketBeforeInList() {
return getStringCitProperty(JStyle.BRACKET_BEFORE_IN_LIST);
}
@@ -918,7 +935,9 @@ public String getBracketBeforeInListWithFallBack() {
return Objects.requireNonNullElse(getBracketBeforeInList(), getBracketBefore());
}
- /** Closing parenthesis after citation marker in the bibliography */
+ /**
+ * Closing parenthesis after citation marker in the bibliography
+ */
private String getBracketAfterInList() {
return getStringCitProperty(JStyle.BRACKET_AFTER_IN_LIST);
}
diff --git a/src/main/java/org/jabref/logic/os/OS.java b/src/main/java/org/jabref/logic/os/OS.java
index 3bba6e3ee33..df2f942aad8 100644
--- a/src/main/java/org/jabref/logic/os/OS.java
+++ b/src/main/java/org/jabref/logic/os/OS.java
@@ -49,8 +49,7 @@ public static String getHostName() {
if (StringUtil.isBlank(hostName)) {
try {
hostName = InetAddress.getLocalHost().getHostName();
- } catch (
- UnknownHostException e) {
+ } catch (UnknownHostException e) {
LoggerFactory.getLogger(OS.class).info("Hostname not found. Using \"localhost\" as fallback.", e);
hostName = "localhost";
}
@@ -65,12 +64,10 @@ public static boolean isKeyringAvailable() {
return false;
}
keyring.deletePassword("JabRef", "keyringTest");
- } catch (
- BackendNotSupportedException ex) {
+ } catch (BackendNotSupportedException ex) {
LoggerFactory.getLogger(OS.class).warn("Credential store not supported.");
return false;
- } catch (
- PasswordAccessException ex) {
+ } catch (PasswordAccessException ex) {
LoggerFactory.getLogger(OS.class).warn("Password storage in credential store failed.");
return false;
} catch (Exception ex) {
@@ -90,9 +87,8 @@ public static String detectProgramPath(String programName, String directoryName)
try {
ShellLink link = new ShellLink(texworksLinkPath);
return link.resolveTarget();
- } catch (
- IOException |
- ShellLinkException e) {
+ } catch (IOException
+ | ShellLinkException e) {
// Static logger instance cannot be used. See the class comment.
Logger logger = Logger.getLogger(OS.class.getName());
logger.log(Level.WARNING, "Had an error while reading .lnk file for TeXworks", e);
diff --git a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
index f3ecebe41d9..095febc0b7f 100644
--- a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
+++ b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
@@ -38,6 +38,7 @@
import org.jabref.logic.LibraryPreferences;
import org.jabref.logic.ai.AiDefaultPreferences;
import org.jabref.logic.ai.AiPreferences;
+import org.jabref.logic.ai.templates.AiTemplate;
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.citationkeypattern.CitationKeyPattern;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
@@ -222,7 +223,7 @@ public class JabRefCliPreferences implements CliPreferences {
public static final String IMPORTERS_ENABLED = "importersEnabled";
public static final String GENERATE_KEY_ON_IMPORT = "generateKeyOnImport";
public static final String GROBID_ENABLED = "grobidEnabled";
- public static final String GROBID_OPT_OUT = "grobidOptOut";
+ public static final String GROBID_PREFERENCE = "grobidPreference";
public static final String GROBID_URL = "grobidURL";
public static final String DEFAULT_CITATION_KEY_PATTERN = "defaultBibtexKeyPattern";
@@ -356,12 +357,14 @@ public class JabRefCliPreferences implements CliPreferences {
private static final String AI_MISTRAL_AI_CHAT_MODEL = "aiMistralAiChatModel";
private static final String AI_GEMINI_CHAT_MODEL = "aiGeminiChatModel";
private static final String AI_HUGGING_FACE_CHAT_MODEL = "aiHuggingFaceChatModel";
+ private static final String AI_GPT_4_ALL_MODEL = "aiGpt4AllChatModel";
private static final String AI_CUSTOMIZE_SETTINGS = "aiCustomizeSettings";
private static final String AI_EMBEDDING_MODEL = "aiEmbeddingModel";
private static final String AI_OPEN_AI_API_BASE_URL = "aiOpenAiApiBaseUrl";
private static final String AI_MISTRAL_AI_API_BASE_URL = "aiMistralAiApiBaseUrl";
private static final String AI_GEMINI_API_BASE_URL = "aiGeminiApiBaseUrl";
private static final String AI_HUGGING_FACE_API_BASE_URL = "aiHuggingFaceApiBaseUrl";
+ private static final String AI_GPT_4_ALL_API_BASE_URL = "aiGpt4AllApiBaseUrl";
private static final String AI_SYSTEM_MESSAGE = "aiSystemMessage";
private static final String AI_TEMPERATURE = "aiTemperature";
private static final String AI_CONTEXT_WINDOW_SIZE = "aiMessageWindowSize";
@@ -370,6 +373,11 @@ public class JabRefCliPreferences implements CliPreferences {
private static final String AI_RAG_MAX_RESULTS_COUNT = "aiRagMaxResultsCount";
private static final String AI_RAG_MIN_SCORE = "aiRagMinScore";
+ private static final String AI_CHATTING_SYSTEM_MESSAGE_TEMPLATE = "aiChattingSystemMessageTemplate";
+ private static final String AI_CHATTING_USER_MESSAGE_TEMPLATE = "aiChattingUserMessageTemplate";
+ private static final String AI_SUMMARIZATION_CHUNK_TEMPLATE = "aiSummarizationChunkTemplate";
+ private static final String AI_SUMMARIZATION_COMBINE_TEMPLATE = "aiSummarizationCombineTemplate";
+
private static final Logger LOGGER = LoggerFactory.getLogger(JabRefCliPreferences.class);
private static final Preferences PREFS_NODE = Preferences.userRoot().node("/org/jabref");
@@ -455,7 +463,7 @@ protected JabRefCliPreferences() {
// region: Grobid
defaults.put(GROBID_ENABLED, Boolean.FALSE);
- defaults.put(GROBID_OPT_OUT, Boolean.FALSE);
+ defaults.put(GROBID_PREFERENCE, Boolean.FALSE);
defaults.put(GROBID_URL, "http://grobid.jabref.org:8070");
// endregion
@@ -641,12 +649,14 @@ protected JabRefCliPreferences() {
defaults.put(AI_MISTRAL_AI_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.MISTRAL_AI).getName());
defaults.put(AI_GEMINI_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.GEMINI).getName());
defaults.put(AI_HUGGING_FACE_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.HUGGING_FACE).getName());
+ defaults.put(AI_GPT_4_ALL_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.GPT4ALL).getName());
defaults.put(AI_CUSTOMIZE_SETTINGS, AiDefaultPreferences.CUSTOMIZE_SETTINGS);
defaults.put(AI_EMBEDDING_MODEL, AiDefaultPreferences.EMBEDDING_MODEL.name());
defaults.put(AI_OPEN_AI_API_BASE_URL, AiProvider.OPEN_AI.getApiUrl());
defaults.put(AI_MISTRAL_AI_API_BASE_URL, AiProvider.MISTRAL_AI.getApiUrl());
defaults.put(AI_GEMINI_API_BASE_URL, AiProvider.GEMINI.getApiUrl());
defaults.put(AI_HUGGING_FACE_API_BASE_URL, AiProvider.HUGGING_FACE.getApiUrl());
+ defaults.put(AI_GPT_4_ALL_API_BASE_URL, AiProvider.GPT4ALL.getApiUrl());
defaults.put(AI_SYSTEM_MESSAGE, AiDefaultPreferences.SYSTEM_MESSAGE);
defaults.put(AI_TEMPERATURE, AiDefaultPreferences.TEMPERATURE);
defaults.put(AI_CONTEXT_WINDOW_SIZE, AiDefaultPreferences.getContextWindowSize(AiDefaultPreferences.PROVIDER, AiDefaultPreferences.CHAT_MODELS.get(AiDefaultPreferences.PROVIDER).getName()));
@@ -654,6 +664,14 @@ protected JabRefCliPreferences() {
defaults.put(AI_DOCUMENT_SPLITTER_OVERLAP_SIZE, AiDefaultPreferences.DOCUMENT_SPLITTER_OVERLAP);
defaults.put(AI_RAG_MAX_RESULTS_COUNT, AiDefaultPreferences.RAG_MAX_RESULTS_COUNT);
defaults.put(AI_RAG_MIN_SCORE, AiDefaultPreferences.RAG_MIN_SCORE);
+
+ // region:AI templates
+ defaults.put(AI_CHATTING_SYSTEM_MESSAGE_TEMPLATE, AiDefaultPreferences.TEMPLATES.get(AiTemplate.CHATTING_SYSTEM_MESSAGE));
+ defaults.put(AI_CHATTING_USER_MESSAGE_TEMPLATE, AiDefaultPreferences.TEMPLATES.get(AiTemplate.CHATTING_USER_MESSAGE));
+ defaults.put(AI_SUMMARIZATION_CHUNK_TEMPLATE, AiDefaultPreferences.TEMPLATES.get(AiTemplate.SUMMARIZATION_CHUNK));
+ defaults.put(AI_SUMMARIZATION_COMBINE_TEMPLATE, AiDefaultPreferences.TEMPLATES.get(AiTemplate.SUMMARIZATION_COMBINE));
+ // endregion
+
// endregion
}
@@ -1835,11 +1853,13 @@ public AiPreferences getAiPreferences() {
get(AI_MISTRAL_AI_CHAT_MODEL),
get(AI_GEMINI_CHAT_MODEL),
get(AI_HUGGING_FACE_CHAT_MODEL),
+ get(AI_GPT_4_ALL_MODEL),
getBoolean(AI_CUSTOMIZE_SETTINGS),
get(AI_OPEN_AI_API_BASE_URL),
get(AI_MISTRAL_AI_API_BASE_URL),
get(AI_GEMINI_API_BASE_URL),
get(AI_HUGGING_FACE_API_BASE_URL),
+ get(AI_GPT_4_ALL_API_BASE_URL),
EmbeddingModel.valueOf(get(AI_EMBEDDING_MODEL)),
get(AI_SYSTEM_MESSAGE),
getDouble(AI_TEMPERATURE),
@@ -1847,7 +1867,13 @@ public AiPreferences getAiPreferences() {
getInt(AI_DOCUMENT_SPLITTER_CHUNK_SIZE),
getInt(AI_DOCUMENT_SPLITTER_OVERLAP_SIZE),
getInt(AI_RAG_MAX_RESULTS_COUNT),
- getDouble(AI_RAG_MIN_SCORE));
+ getDouble(AI_RAG_MIN_SCORE),
+ Map.of(
+ AiTemplate.CHATTING_SYSTEM_MESSAGE, get(AI_CHATTING_SYSTEM_MESSAGE_TEMPLATE),
+ AiTemplate.CHATTING_USER_MESSAGE, get(AI_CHATTING_USER_MESSAGE_TEMPLATE),
+ AiTemplate.SUMMARIZATION_CHUNK, get(AI_SUMMARIZATION_CHUNK_TEMPLATE),
+ AiTemplate.SUMMARIZATION_COMBINE, get(AI_SUMMARIZATION_COMBINE_TEMPLATE)
+ ));
EasyBind.listen(aiPreferences.enableAiProperty(), (obs, oldValue, newValue) -> putBoolean(AI_ENABLED, newValue));
EasyBind.listen(aiPreferences.autoGenerateEmbeddingsProperty(), (obs, oldValue, newValue) -> putBoolean(AI_AUTO_GENERATE_EMBEDDINGS, newValue));
@@ -1859,6 +1885,7 @@ public AiPreferences getAiPreferences() {
EasyBind.listen(aiPreferences.mistralAiChatModelProperty(), (obs, oldValue, newValue) -> put(AI_MISTRAL_AI_CHAT_MODEL, newValue));
EasyBind.listen(aiPreferences.geminiChatModelProperty(), (obs, oldValue, newValue) -> put(AI_GEMINI_CHAT_MODEL, newValue));
EasyBind.listen(aiPreferences.huggingFaceChatModelProperty(), (obs, oldValue, newValue) -> put(AI_HUGGING_FACE_CHAT_MODEL, newValue));
+ EasyBind.listen(aiPreferences.gpt4AllChatModelProperty(), (obs, oldValue, newValue) -> put(AI_GPT_4_ALL_MODEL, newValue));
EasyBind.listen(aiPreferences.customizeExpertSettingsProperty(), (obs, oldValue, newValue) -> putBoolean(AI_CUSTOMIZE_SETTINGS, newValue));
@@ -1866,6 +1893,7 @@ public AiPreferences getAiPreferences() {
EasyBind.listen(aiPreferences.mistralAiApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_MISTRAL_AI_API_BASE_URL, newValue));
EasyBind.listen(aiPreferences.geminiApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_GEMINI_API_BASE_URL, newValue));
EasyBind.listen(aiPreferences.huggingFaceApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_HUGGING_FACE_API_BASE_URL, newValue));
+ EasyBind.listen(aiPreferences.gpt4AllApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_GPT_4_ALL_API_BASE_URL, newValue));
EasyBind.listen(aiPreferences.embeddingModelProperty(), (obs, oldValue, newValue) -> put(AI_EMBEDDING_MODEL, newValue.name()));
EasyBind.listen(aiPreferences.instructionProperty(), (obs, oldValue, newValue) -> put(AI_SYSTEM_MESSAGE, newValue));
@@ -1876,6 +1904,11 @@ public AiPreferences getAiPreferences() {
EasyBind.listen(aiPreferences.ragMaxResultsCountProperty(), (obs, oldValue, newValue) -> putInt(AI_RAG_MAX_RESULTS_COUNT, newValue));
EasyBind.listen(aiPreferences.ragMinScoreProperty(), (obs, oldValue, newValue) -> putDouble(AI_RAG_MIN_SCORE, newValue.doubleValue()));
+ EasyBind.listen(aiPreferences.templateProperty(AiTemplate.CHATTING_SYSTEM_MESSAGE), (obs, oldValue, newValue) -> put(AI_CHATTING_SYSTEM_MESSAGE_TEMPLATE, newValue));
+ EasyBind.listen(aiPreferences.templateProperty(AiTemplate.CHATTING_USER_MESSAGE), (obs, oldValue, newValue) -> put(AI_CHATTING_USER_MESSAGE_TEMPLATE, newValue));
+ EasyBind.listen(aiPreferences.templateProperty(AiTemplate.SUMMARIZATION_CHUNK), (obs, oldValue, newValue) -> put(AI_SUMMARIZATION_CHUNK_TEMPLATE, newValue));
+ EasyBind.listen(aiPreferences.templateProperty(AiTemplate.SUMMARIZATION_COMBINE), (obs, oldValue, newValue) -> put(AI_SUMMARIZATION_COMBINE_TEMPLATE, newValue));
+
return aiPreferences;
}
@@ -2181,11 +2214,11 @@ public GrobidPreferences getGrobidPreferences() {
grobidPreferences = new GrobidPreferences(
getBoolean(GROBID_ENABLED),
- getBoolean(GROBID_OPT_OUT),
+ getBoolean(GROBID_PREFERENCE),
get(GROBID_URL));
EasyBind.listen(grobidPreferences.grobidEnabledProperty(), (obs, oldValue, newValue) -> putBoolean(GROBID_ENABLED, newValue));
- EasyBind.listen(grobidPreferences.grobidOptOutProperty(), (obs, oldValue, newValue) -> putBoolean(GROBID_OPT_OUT, newValue));
+ EasyBind.listen(grobidPreferences.grobidUseAskedProperty(), (obs, oldValue, newValue) -> putBoolean(GROBID_PREFERENCE, newValue));
EasyBind.listen(grobidPreferences.grobidURLProperty(), (obs, oldValue, newValue) -> put(GROBID_URL, newValue));
return grobidPreferences;
diff --git a/src/main/java/org/jabref/logic/search/PostgreServer.java b/src/main/java/org/jabref/logic/search/PostgreServer.java
index 13a113c441a..012f92f011d 100644
--- a/src/main/java/org/jabref/logic/search/PostgreServer.java
+++ b/src/main/java/org/jabref/logic/search/PostgreServer.java
@@ -23,6 +23,7 @@ public PostgreServer() {
EmbeddedPostgres embeddedPostgres;
try {
embeddedPostgres = EmbeddedPostgres.builder()
+ .setOutputRedirector(ProcessBuilder.Redirect.DISCARD)
.start();
LOGGER.info("Postgres server started, connection port: {}", embeddedPostgres.getPort());
} catch (IOException e) {
diff --git a/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java b/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java
index 6312ba54ffb..6ace9d64537 100644
--- a/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java
+++ b/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java
@@ -23,6 +23,7 @@
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.search.PostgreConstants;
+import io.github.thibaultmeyer.cuid.CUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,7 +54,7 @@ public BibFieldsIndexer(BibEntryPreferences bibEntryPreferences, BibDatabaseCont
this.keywordSeparator = bibEntryPreferences.getKeywordSeparator();
this.libraryName = databaseContext.getDatabasePath().map(path -> path.getFileName().toString()).orElse("unsaved");
- this.mainTable = databaseContext.getUniqueName();
+ this.mainTable = CUID.randomCUID2(12).toString();
this.splitValuesTable = mainTable + SPLIT_TABLE_SUFFIX;
this.schemaMainTableReference = PostgreConstants.getMainTableSchemaReference(mainTable);
diff --git a/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java b/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java
index 50683c8d8f6..bf7386ddef3 100644
--- a/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java
+++ b/src/main/java/org/jabref/logic/search/indexing/DefaultLinkedFilesIndexer.java
@@ -23,6 +23,7 @@
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.HeadlessExecutorService;
import org.jabref.logic.util.StandardFileType;
+import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;
@@ -133,8 +134,6 @@ private void addToIndex(Map> linkedFiles, BackgroundTas
return;
}
- task.setTitle(Localization.lang("Indexing PDF files for %0", libraryName));
- task.showToUser(true);
LOGGER.debug("Adding {} files to index", linkedFiles.size());
int i = 1;
for (Map.Entry> entry : linkedFiles.entrySet()) {
@@ -143,8 +142,10 @@ private void addToIndex(Map> linkedFiles, BackgroundTas
return;
}
addToIndex(entry.getKey(), entry.getValue().getKey(), entry.getValue().getValue());
+ task.setTitle(Localization.lang("Indexing files for %1 | %2 of %0 file(s) indexed.", linkedFiles.size(), libraryName, i));
task.updateProgress(i, linkedFiles.size());
- task.updateMessage(Localization.lang("Indexing %0. %1 of %2 files added to the index.", entry.getValue().getValue().getFileName(), i, linkedFiles.size()));
+ task.updateMessage(Localization.lang("Indexing %0", FileUtil.shortenFileName(entry.getValue().getValue().getFileName().toString(), 68)));
+ task.showToUser(true);
i++;
}
LOGGER.debug("Added {} files to index", linkedFiles.size());
diff --git a/src/main/java/org/jabref/logic/search/query/SearchQueryConversion.java b/src/main/java/org/jabref/logic/search/query/SearchQueryConversion.java
index 9c2b8ea0ff2..e782f31d1dc 100644
--- a/src/main/java/org/jabref/logic/search/query/SearchQueryConversion.java
+++ b/src/main/java/org/jabref/logic/search/query/SearchQueryConversion.java
@@ -7,7 +7,6 @@
import org.jabref.model.search.query.SqlQueryNode;
import org.jabref.search.SearchParser;
-import org.apache.lucene.search.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,9 +23,9 @@ public static String flagsToSearchExpression(SearchQuery searchQuery) {
return new SearchFlagsToExpressionVisitor(searchQuery.getSearchFlags()).visit(searchQuery.getContext());
}
- public static Query searchToLucene(SearchQuery searchQuery) {
+ public static String searchToLucene(SearchQuery searchQuery) {
LOGGER.debug("Converting search expression to Lucene: {}", searchQuery.getSearchExpression());
- return new SearchToLuceneVisitor().visit(searchQuery.getContext());
+ return new SearchToLuceneVisitor(searchQuery.getSearchFlags()).visit(searchQuery.getContext());
}
public static List extractSearchTerms(SearchQuery searchQuery) {
diff --git a/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java b/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java
index 4f38a0ed1e2..c25a162045c 100644
--- a/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java
+++ b/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java
@@ -1,152 +1,131 @@
package org.jabref.logic.search.query;
+import java.util.EnumSet;
import java.util.List;
+import java.util.Locale;
import org.jabref.model.search.LinkedFilesConstants;
+import org.jabref.model.search.SearchFlags;
import org.jabref.search.SearchBaseVisitor;
import org.jabref.search.SearchParser;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.MatchNoDocsQuery;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.RegexpQuery;
-import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.util.QueryBuilder;
+import org.apache.lucene.queryparser.classic.QueryParser;
/**
* Tests are located in {@link org.jabref.logic.search.query.SearchQueryLuceneConversionTest}.
*/
-public class SearchToLuceneVisitor extends SearchBaseVisitor {
+public class SearchToLuceneVisitor extends SearchBaseVisitor {
+ private final EnumSet searchFlags;
- private static final List SEARCH_FIELDS = LinkedFilesConstants.PDF_FIELDS;
-
- private final QueryBuilder queryBuilder;
-
- public SearchToLuceneVisitor() {
- this.queryBuilder = new QueryBuilder(LinkedFilesConstants.LINKED_FILES_ANALYZER);
+ public SearchToLuceneVisitor(EnumSet searchFlags) {
+ this.searchFlags = searchFlags;
}
@Override
- public Query visitStart(SearchParser.StartContext ctx) {
+ public String visitStart(SearchParser.StartContext ctx) {
return visit(ctx.andExpression());
}
@Override
- public Query visitImplicitAndExpression(SearchParser.ImplicitAndExpressionContext ctx) {
- List children = ctx.expression().stream().map(this::visit).toList();
- if (children.size() == 1) {
- return children.getFirst();
- }
- BooleanQuery.Builder builder = new BooleanQuery.Builder();
- for (Query child : children) {
- builder.add(child, BooleanClause.Occur.MUST);
- }
- return builder.build();
+ public String visitImplicitAndExpression(SearchParser.ImplicitAndExpressionContext ctx) {
+ List children = ctx.expression().stream().map(this::visit).toList();
+ return children.size() == 1 ? children.getFirst() : String.join(" ", children);
}
@Override
- public Query visitParenExpression(SearchParser.ParenExpressionContext ctx) {
- return visit(ctx.andExpression());
+ public String visitParenExpression(SearchParser.ParenExpressionContext ctx) {
+ String expr = visit(ctx.andExpression());
+ return expr.isEmpty() ? "" : "(" + expr + ")";
}
@Override
- public Query visitNegatedExpression(SearchParser.NegatedExpressionContext ctx) {
- Query innerQuery = visit(ctx.expression());
- if (innerQuery instanceof MatchNoDocsQuery) {
- return innerQuery;
- }
- BooleanQuery.Builder builder = new BooleanQuery.Builder();
- builder.add(innerQuery, BooleanClause.Occur.MUST_NOT);
- return builder.build();
+ public String visitNegatedExpression(SearchParser.NegatedExpressionContext ctx) {
+ return "NOT (" + visit(ctx.expression()) + ")";
}
@Override
- public Query visitBinaryExpression(SearchParser.BinaryExpressionContext ctx) {
- Query left = visit(ctx.left);
- Query right = visit(ctx.right);
+ public String visitBinaryExpression(SearchParser.BinaryExpressionContext ctx) {
+ String left = visit(ctx.left);
+ String right = visit(ctx.right);
- if (left instanceof MatchNoDocsQuery) {
+ if (left.isEmpty() && right.isEmpty()) {
+ return "";
+ }
+ if (left.isEmpty()) {
return right;
}
- if (right instanceof MatchNoDocsQuery) {
+ if (right.isEmpty()) {
return left;
}
- BooleanQuery.Builder builder = new BooleanQuery.Builder();
+ String operator = ctx.bin_op.getType() == SearchParser.AND ? " AND " : " OR ";
+ return left + operator + right;
+ }
+
+ @Override
+ public String visitComparison(SearchParser.ComparisonContext ctx) {
+ String term = SearchQueryConversion.unescapeSearchValue(ctx.searchValue());
+ boolean isQuoted = ctx.searchValue().getStart().getType() == SearchParser.STRING_LITERAL;
+
+ // unfielded expression
+ if (ctx.FIELD() == null) {
+ if (searchFlags.contains(SearchFlags.REGULAR_EXPRESSION)) {
+ return "/" + term + "/";
+ }
+ return isQuoted ? "\"" + escapeQuotes(term) + "\"" : QueryParser.escape(term);
+ }
- if (ctx.bin_op.getType() == SearchParser.AND) {
- builder.add(left, BooleanClause.Occur.MUST);
- builder.add(right, BooleanClause.Occur.MUST);
- } else if (ctx.bin_op.getType() == SearchParser.OR) {
- builder.add(left, BooleanClause.Occur.SHOULD);
- builder.add(right, BooleanClause.Occur.SHOULD);
+ String field = ctx.FIELD().getText().toLowerCase(Locale.ROOT);
+ if (!isValidField(field)) {
+ return "";
}
- return builder.build();
+ field = "any".equals(field) || "anyfield".equals(field) ? "" : field + ":";
+ int operator = ctx.operator().getStart().getType();
+ return buildFieldExpression(field, term, operator, isQuoted);
}
- @Override
- public Query visitComparisonExpression(SearchParser.ComparisonExpressionContext ctx) {
- return visit(ctx.comparison());
+ private boolean isValidField(String field) {
+ return "any".equals(field) || "anyfield".equals(field) || LinkedFilesConstants.PDF_FIELDS.contains(field);
}
- @Override
- public Query visitComparison(SearchParser.ComparisonContext ctx) {
- String field = ctx.FIELD() == null ? null : ctx.FIELD().getText();
- String term = SearchQueryConversion.unescapeSearchValue(ctx.searchValue());
+ private String buildFieldExpression(String field, String term, int operator, boolean isQuoted) {
+ boolean isRegexOp = isRegexOperator(operator);
+ boolean isNegationOp = isNegationOperator(operator);
- // unfielded expression
- if (field == null || "anyfield".equals(field) || "any".equals(field)) {
- return createMultiFieldQuery(term, ctx.operator());
- } else if (SEARCH_FIELDS.contains(field)) {
- return createFieldQuery(field, term, ctx.operator());
+ if (isRegexOp) {
+ String expression = field + "/" + term + "/";
+ return isNegationOp ? "NOT " + expression : expression;
} else {
- return new MatchNoDocsQuery();
+ term = isQuoted ? "\"" + escapeQuotes(term) + "\"" : QueryParser.escape(term);
+ String expression = field + term;
+ return isNegationOp ? "NOT " + expression : expression;
}
}
- private Query createMultiFieldQuery(String value, SearchParser.OperatorContext operator) {
- BooleanQuery.Builder builder = new BooleanQuery.Builder();
- for (String field : SEARCH_FIELDS) {
- builder.add(createFieldQuery(field, value, operator), BooleanClause.Occur.SHOULD);
- }
- return builder.build();
+ private static String escapeQuotes(String term) {
+ return term.replace("\"", "\\\"");
}
- private Query createFieldQuery(String field, String value, SearchParser.OperatorContext operator) {
- if (operator == null) {
- return createTermOrPhraseQuery(field, value);
- }
-
- return switch (operator.getStart().getType()) {
- case SearchParser.REQUAL,
- SearchParser.CREEQUAL ->
- new RegexpQuery(new Term(field, value));
+ private static boolean isNegationOperator(int operator) {
+ return switch (operator) {
case SearchParser.NEQUAL,
SearchParser.NCEQUAL,
SearchParser.NEEQUAL,
- SearchParser.NCEEQUAL ->
- createNegatedQuery(createTermOrPhraseQuery(field, value));
- case SearchParser.NREQUAL,
- SearchParser.NCREEQUAL ->
- createNegatedQuery(new RegexpQuery(new Term(field, value)));
- default ->
- createTermOrPhraseQuery(field, value);
+ SearchParser.NCEEQUAL,
+ SearchParser.NREQUAL,
+ SearchParser.NCREEQUAL -> true;
+ default -> false;
};
}
- private Query createNegatedQuery(Query query) {
- BooleanQuery.Builder negatedQuery = new BooleanQuery.Builder();
- negatedQuery.add(query, BooleanClause.Occur.MUST_NOT);
- return negatedQuery.build();
- }
-
- private Query createTermOrPhraseQuery(String field, String value) {
- if (value.contains("*") || value.contains("?")) {
- return new TermQuery(new Term(field, value));
- }
- return queryBuilder.createPhraseQuery(field, value);
+ private static boolean isRegexOperator(int operator) {
+ return switch (operator) {
+ case SearchParser.REQUAL,
+ SearchParser.CREEQUAL,
+ SearchParser.NREQUAL,
+ SearchParser.NCREEQUAL -> true;
+ default -> false;
+ };
}
}
diff --git a/src/main/java/org/jabref/logic/search/retrieval/LinkedFilesSearcher.java b/src/main/java/org/jabref/logic/search/retrieval/LinkedFilesSearcher.java
index 5b2a93047fe..1e3e47a11b4 100644
--- a/src/main/java/org/jabref/logic/search/retrieval/LinkedFilesSearcher.java
+++ b/src/main/java/org/jabref/logic/search/retrieval/LinkedFilesSearcher.java
@@ -22,6 +22,9 @@
import org.apache.lucene.document.Document;
import org.apache.lucene.index.StoredFields;
+import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
+import org.apache.lucene.queryparser.classic.ParseException;
+import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
@@ -39,11 +42,14 @@ public final class LinkedFilesSearcher {
private final FilePreferences filePreferences;
private final BibDatabaseContext databaseContext;
private final SearcherManager searcherManager;
+ private final MultiFieldQueryParser parser;
public LinkedFilesSearcher(BibDatabaseContext databaseContext, LuceneIndexer linkedFilesIndexer, FilePreferences filePreferences) {
this.searcherManager = linkedFilesIndexer.getSearcherManager();
this.databaseContext = databaseContext;
this.filePreferences = filePreferences;
+ this.parser = new MultiFieldQueryParser(LinkedFilesConstants.PDF_FIELDS.toArray(new String[0]), LinkedFilesConstants.LINKED_FILES_ANALYZER);
+ parser.setDefaultOperator(QueryParser.Operator.AND);
}
public SearchResults search(SearchQuery searchQuery) {
@@ -51,17 +57,21 @@ public SearchResults search(SearchQuery searchQuery) {
return new SearchResults();
}
- Query luceneQuery = SearchQueryConversion.searchToLucene(searchQuery);
+ Optional luceneQuery = getLuceneQuery(searchQuery);
+ if (luceneQuery.isEmpty()) {
+ return new SearchResults();
+ }
+
EnumSet searchFlags = searchQuery.getSearchFlags();
boolean shouldSearchInLinkedFiles = searchFlags.contains(SearchFlags.FULLTEXT) && filePreferences.shouldFulltextIndexLinkedFiles();
if (!shouldSearchInLinkedFiles) {
return new SearchResults();
}
- LOGGER.debug("Searching in linked files with query: {}", luceneQuery);
+ LOGGER.debug("Searching in linked files with query: {}", luceneQuery.get());
try {
IndexSearcher linkedFilesIndexSearcher = acquireIndexSearcher(searcherManager);
- SearchResults searchResults = search(linkedFilesIndexSearcher, luceneQuery);
+ SearchResults searchResults = search(linkedFilesIndexSearcher, luceneQuery.get());
releaseIndexSearcher(searcherManager, linkedFilesIndexSearcher);
return searchResults;
} catch (IOException | IndexSearcher.TooManyClauses e) {
@@ -70,6 +80,16 @@ public SearchResults search(SearchQuery searchQuery) {
return new SearchResults();
}
+ private Optional getLuceneQuery(SearchQuery searchQuery) {
+ String query = SearchQueryConversion.searchToLucene(searchQuery);
+ try {
+ return Optional.of(parser.parse(query));
+ } catch (ParseException e) {
+ LOGGER.error("Error during query parsing", e);
+ return Optional.empty();
+ }
+ }
+
private SearchResults search(IndexSearcher indexSearcher, Query searchQuery) throws IOException {
TopDocs topDocs = indexSearcher.search(searchQuery, Integer.MAX_VALUE);
StoredFields storedFields = indexSearcher.storedFields();
diff --git a/src/main/java/org/jabref/logic/util/io/FileUtil.java b/src/main/java/org/jabref/logic/util/io/FileUtil.java
index 749b7d56cd4..5ddb734d8ce 100644
--- a/src/main/java/org/jabref/logic/util/io/FileUtil.java
+++ b/src/main/java/org/jabref/logic/util/io/FileUtil.java
@@ -45,6 +45,8 @@ public class FileUtil {
public static final int MAXIMUM_FILE_NAME_LENGTH = 255;
private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class);
+ private static final String ELLIPSIS = "...";
+ private static final int ELLIPSIS_LENGTH = ELLIPSIS.length();
/**
* MUST ALWAYS BE A SORTED ARRAY because it is used in a binary search
@@ -523,6 +525,63 @@ public static boolean detectBadFileName(String fileName) {
return false;
}
+ /**
+ * Shorten a given file name in the middle of the name using ellipsis. Example: verylongfilenameisthis.pdf
+ * with maxLength = 20 is shortened into verylo...isthis.pdf
+ *
+ * @param fileName the given file name to be shortened
+ * @param maxLength the maximum number of characters in the string after shortening (including the extension)
+ * @return the original fileName if fileName.length() <= maxLength. Otherwise, a shortened fileName
+ */
+ public static String shortenFileName(String fileName, Integer maxLength) {
+ if (fileName == null || maxLength == null || maxLength < ELLIPSIS_LENGTH) {
+ return "";
+ }
+
+ if (fileName.length() <= maxLength) {
+ return fileName;
+ }
+
+ String name;
+ String extension;
+
+ extension = FileUtil.getFileExtension(fileName).map(fileExtension -> '.' + fileExtension).orElse("");
+ if (extension.isEmpty()) {
+ name = fileName;
+ } else {
+ name = fileName.substring(0, fileName.length() - extension.length());
+ }
+
+ int totalNeededLength = ELLIPSIS_LENGTH + extension.length();
+ if (maxLength <= totalNeededLength) {
+ return fileName.substring(0, maxLength - ELLIPSIS_LENGTH) + ELLIPSIS;
+ }
+
+ int charsForName = maxLength - totalNeededLength;
+ if (charsForName <= 0) {
+ return ELLIPSIS + extension;
+ }
+
+ int numCharsBeforeEllipsis;
+ int numCharsAfterEllipsis;
+ if (charsForName == 1) {
+ numCharsBeforeEllipsis = 1;
+ numCharsAfterEllipsis = 0;
+ } else {
+ // Allow the front part to have the extra in odd cases
+ numCharsBeforeEllipsis = (charsForName + 1) / 2;
+ numCharsAfterEllipsis = charsForName / 2;
+ }
+
+ numCharsBeforeEllipsis = Math.min(numCharsBeforeEllipsis, name.length());
+ numCharsAfterEllipsis = Math.min(numCharsAfterEllipsis, name.length() - numCharsBeforeEllipsis);
+
+ return name.substring(0, numCharsBeforeEllipsis) +
+ ELLIPSIS +
+ name.substring(name.length() - numCharsAfterEllipsis) +
+ extension;
+ }
+
public static boolean isCharLegal(char c) {
return Arrays.binarySearch(ILLEGAL_CHARS, c) < 0;
}
diff --git a/src/main/java/org/jabref/model/ai/AiProvider.java b/src/main/java/org/jabref/model/ai/AiProvider.java
index 00382e98d36..053406fdc5f 100644
--- a/src/main/java/org/jabref/model/ai/AiProvider.java
+++ b/src/main/java/org/jabref/model/ai/AiProvider.java
@@ -3,10 +3,11 @@
import java.io.Serializable;
public enum AiProvider implements Serializable {
- OPEN_AI("OpenAI", "https://openai.com/policies/privacy-policy/", "https://openai.com/policies/privacy-policy/"),
+ OPEN_AI("OpenAI", "https://api.openai.com/v1", "https://openai.com/policies/privacy-policy/"),
MISTRAL_AI("Mistral AI", "https://mistral.ai/terms/#privacy-policy", "https://mistral.ai/terms/#privacy-policy"),
GEMINI("Gemini", "https://huggingface.co/privacy", "https://ai.google.dev/gemini-api/terms"),
- HUGGING_FACE("Hugging Face", "https://huggingface.co/api", "https://huggingface.co/privacy");
+ HUGGING_FACE("Hugging Face", "https://huggingface.co/api", "https://huggingface.co/privacy"),
+ GPT4ALL("GPT4All", "http://localhost:4891/v1", "https://www.nomic.ai/gpt4all/legal/privacy-policy");
private final String label;
private final String apiUrl;
diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java
index 56f650e8893..911b5bed2a0 100644
--- a/src/main/java/org/jabref/model/database/BibDatabase.java
+++ b/src/main/java/org/jabref/model/database/BibDatabase.java
@@ -637,7 +637,11 @@ public String getNewLineSeparator() {
*/
public int indexOf(BibEntry bibEntry) {
int index = Collections.binarySearch(entries, bibEntry, Comparator.comparing(BibEntry::getId));
- return index >= 0 ? index : -1;
+ if (index >= 0) {
+ return index;
+ }
+ LOGGER.warn("Could not find entry with ID {} in the database", bibEntry.getId());
+ return -1;
}
public BibEntry getEntryById(String id) {
diff --git a/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/src/main/java/org/jabref/model/database/BibDatabaseContext.java
index c09138a978d..f90a7c64fbd 100644
--- a/src/main/java/org/jabref/model/database/BibDatabaseContext.java
+++ b/src/main/java/org/jabref/model/database/BibDatabaseContext.java
@@ -40,7 +40,6 @@
public class BibDatabaseContext {
private static final Logger LOGGER = LoggerFactory.getLogger(BibDatabaseContext.class);
- private static int NUMBER_OF_UNSAVED_LIBRARIES = 0;
private final BibDatabase database;
private MetaData metaData;
@@ -163,10 +162,10 @@ public List getFileDirectories(FilePreferences preferences) {
// Paths are a) ordered and b) should be contained only once in the result
LinkedHashSet fileDirs = new LinkedHashSet<>(3);
- Optional userFileDirectory = metaData.getUserFileDirectory(preferences.getUserAndHost()).map(dir -> getFileDirectoryPath(dir));
+ Optional userFileDirectory = metaData.getUserFileDirectory(preferences.getUserAndHost()).map(this::getFileDirectoryPath);
userFileDirectory.ifPresent(fileDirs::add);
- Optional librarySpecificFileDirectory = metaData.getLibrarySpecificFileDirectory().map(dir -> getFileDirectoryPath(dir));
+ Optional librarySpecificFileDirectory = metaData.getLibrarySpecificFileDirectory().map(this::getFileDirectoryPath);
librarySpecificFileDirectory.ifPresent(fileDirs::add);
// fileDirs.isEmpty() is true after these two if there are no directories set in the BIB file itself:
@@ -275,14 +274,6 @@ public Path getFulltextIndexPath() {
return indexPath;
}
- public String getUniqueName() {
- if (getDatabasePath().isPresent()) {
- Path databasePath = getDatabasePath().get();
- return BackupFileUtil.getUniqueFilePrefix(databasePath) + "--" + databasePath.getFileName();
- }
- return "unsaved" + NUMBER_OF_UNSAVED_LIBRARIES++;
- }
-
@Override
public String toString() {
return "BibDatabaseContext{" +
diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java
index e6f3d064ac3..6f2a38aca7c 100644
--- a/src/main/java/org/jabref/model/entry/BibEntry.java
+++ b/src/main/java/org/jabref/model/entry/BibEntry.java
@@ -1,6 +1,7 @@
package org.jabref.model.entry;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -18,6 +19,7 @@
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
@@ -1124,6 +1126,26 @@ public Optional addFiles(List filesToAdd) {
}
// endregion
+ /**
+ * Checks {@link StandardField#CITES} for a list of citation keys and returns them.
+ *
+ * Empty citation keys are not returned. There is no consistency check made.
+ *
+ * @return List of citation keys; empty list if field is empty or not available.
+ */
+ public SequencedSet getCites() {
+ return this.getField(StandardField.CITES)
+ .map(content -> Arrays.stream(content.split(",")))
+ .orElseGet(Stream::empty)
+ .map(String::trim)
+ .filter(key -> !key.isEmpty())
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ }
+
+ public Optional setCites(SequencedSet keys) {
+ return this.setField(StandardField.CITES, keys.stream().collect(Collectors.joining(",")));
+ }
+
public void setDate(Date date) {
date.getYear().ifPresent(year -> setField(StandardField.YEAR, year.toString()));
date.getMonth().ifPresent(this::setMonth);
diff --git a/src/main/java/org/jabref/model/entry/identifier/DOI.java b/src/main/java/org/jabref/model/entry/identifier/DOI.java
index fb457522809..58e5c59c03b 100644
--- a/src/main/java/org/jabref/model/entry/identifier/DOI.java
+++ b/src/main/java/org/jabref/model/entry/identifier/DOI.java
@@ -201,6 +201,7 @@ public static boolean isValid(String doi) {
*/
public static Optional findInText(String text) {
Optional result = Optional.empty();
+ text = text.replaceAll("[�]", "");
Matcher matcher = FIND_DOI_PATT.matcher(text);
if (matcher.find()) {
diff --git a/src/main/java/org/jabref/model/groups/GroupTreeNode.java b/src/main/java/org/jabref/model/groups/GroupTreeNode.java
index 6e5b3284a68..6fa9e354823 100644
--- a/src/main/java/org/jabref/model/groups/GroupTreeNode.java
+++ b/src/main/java/org/jabref/model/groups/GroupTreeNode.java
@@ -58,9 +58,7 @@ public ObjectProperty getGroupProperty() {
* Associates the specified group with this node.
*
* @param newGroup the new group (has to be non-null)
- * @deprecated use {@link #setGroup(AbstractGroup, boolean, boolean, List)}} instead
*/
- @Deprecated
public void setGroup(AbstractGroup newGroup) {
this.groupProperty.set(Objects.requireNonNull(newGroup));
}
diff --git a/src/main/java/org/jabref/model/search/PostgreConstants.java b/src/main/java/org/jabref/model/search/PostgreConstants.java
index 811d3e7d9af..e2cf9d07904 100644
--- a/src/main/java/org/jabref/model/search/PostgreConstants.java
+++ b/src/main/java/org/jabref/model/search/PostgreConstants.java
@@ -5,7 +5,7 @@
public enum PostgreConstants {
BIB_FIELDS_SCHEME("bib_fields"),
SPLIT_TABLE_SUFFIX("_split_values"),
- ENTRY_ID("entry_id"),
+ ENTRY_ID("entryid"),
FIELD_NAME("field_name"),
FIELD_VALUE_LITERAL("field_value_literal"), // contains the value as-is
FIELD_VALUE_TRANSFORMED("field_value_transformed"); // contains the value transformed for better querying
diff --git a/src/main/resources/csl-locales b/src/main/resources/csl-locales
index 4753e3a9aca..96d704de2fc 160000
--- a/src/main/resources/csl-locales
+++ b/src/main/resources/csl-locales
@@ -1 +1 @@
-Subproject commit 4753e3a9aca4b806ac0e3036ed727d47bf8f678e
+Subproject commit 96d704de2fc7b930ae4a0ec4686a7143bb4a0d33
diff --git a/src/main/resources/csl-styles b/src/main/resources/csl-styles
index 5cfc7ae46c1..568b63625d7 160000
--- a/src/main/resources/csl-styles
+++ b/src/main/resources/csl-styles
@@ -1 +1 @@
-Subproject commit 5cfc7ae46c1d596351d75aaa11ab06e98e0aeeaf
+Subproject commit 568b63625d72c25297b498cf3f658e5691655a41
diff --git a/src/main/resources/l10n/JabRef_ar.properties b/src/main/resources/l10n/JabRef_ar.properties
index 231318377dd..111db309d93 100644
--- a/src/main/resources/l10n/JabRef_ar.properties
+++ b/src/main/resources/l10n/JabRef_ar.properties
@@ -243,7 +243,6 @@ Import\ preferences\ from\ file=استيراد الإعدادات من ملف
-
JabRef\ preferences=إعدادات JabRef
@@ -444,6 +443,7 @@ Error\ opening\ file=حدث خطأ أثناء فتح الملف
+
Rename\ field=إعادة تسمية الحقل
@@ -721,5 +721,6 @@ File\ not\ found=لم يتم العثور على الملف
+
diff --git a/src/main/resources/l10n/JabRef_da.properties b/src/main/resources/l10n/JabRef_da.properties
index 527dcc58e28..c06c35711b1 100644
--- a/src/main/resources/l10n/JabRef_da.properties
+++ b/src/main/resources/l10n/JabRef_da.properties
@@ -305,8 +305,6 @@ Import\ preferences=Importer indstillinger
Import\ preferences\ from\ file=Importer indstillinger fra fil
-Imported\ entries=Importerede poster
-
Importer\ class=Importer-klasse
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Inkluder undergrupper\: Vis poster indeholdt i denne gruppe eller en undergruppe
@@ -645,7 +643,7 @@ Could\ not\ move\ file\ '%0'.=Kunne ikke flytte fil '%0'.
Could\ not\ find\ file\ '%0'.=Kunne ikke finde filen '%0'.
Error\ opening\ file=Fejl ved åbning af fil
-Number\ of\ entries\ successfully\ imported=Antal poster korrekt importeret
+
Error\ while\ fetching\ from\ %0=Fejl under hentning fra %0
Unable\ to\ open\ link.=Kan ikke åbne link.
@@ -1013,4 +1011,5 @@ Path\ to\ %0=Sti til %0
+
diff --git a/src/main/resources/l10n/JabRef_de.properties b/src/main/resources/l10n/JabRef_de.properties
index 93a60774ef6..6c35cb304a1 100644
--- a/src/main/resources/l10n/JabRef_de.properties
+++ b/src/main/resources/l10n/JabRef_de.properties
@@ -103,8 +103,11 @@ Available\ import\ formats=Verfügbare Importformate
Show\ BibTeX\ source=BibTeX-Quellcode anzeigen
Show/edit\ %0\ source=%0-Quelltext anzeigen/editieren
+Background\ tasks=Hintergrund Job
+Background\ tasks\ are\ running=Hintergrundprozesse laufen
+Background\ tasks\ are\ finished=Hintergrundprozesse beendet
Browse=Durchsuchen
@@ -122,6 +125,7 @@ Case\ sensitive=Groß-/Kleinschreibung
change\ assignment\ of\ entries=Änderung der zugewiesenen Einträge
+Catalogues\ used\ for\ 'Search\ pre-configured'=Kataloge für 'Vorkonfigurierte Suche' verwendet
Change\ case=Groß- und Kleinschreibung
@@ -269,6 +273,8 @@ Search\ groups\ migration\ of\ %0=Suchgruppenmigration von %0
The\ search\ groups\ syntax\ is\ outdated.\ Do\ you\ want\ to\ migrate\ to\ the\ new\ syntax?=Die Suchgruppen-Syntax ist veraltet. Möchten Sie zur neuen Syntax migrieren?
Migrate=Migrieren
Keep\ as\ is=Unverändert belassen
+Search\ group\ migration\ failed=Suchgruppenmigration fehlgeschlagen
+The\ search\ group\ '%0'\ could\ not\ be\ migrated.\ Please\ enter\ the\ new\ search\ expression.=Die Suchgruppe '%0' konnte nicht migriert werden. Bitte geben Sie den neuen Suchbegriff ein.
Edit=Bearbeiten
Edit\ file\ type=Dateityp bearbeiten
@@ -313,7 +319,8 @@ Extract\ References\ (online)=Referenzen extrahieren (online)
Processing...=In Bearbeitung...
Processing\ "%0"...=Verarbeite "%0"...
Processing\ PDF(s)=Verarbeite PDF(s)
-Processing\ file\ %0=Datei %0 wird verarbeitet
+Processing\ %0=Verarbeitung %0
+Importing\ files\ into\ %1\ |\ %2\ of\ %0\ file(s)\ processed.=Importiere Datei(en) in %1 | %2 von %0 Datei(en) bearbeitet.
Processing\ a\ large\ number\ of\ files=Verarbeitung einer großen Anzahl an Dateien
You\ are\ about\ to\ process\ %0\ files.\ Continue?=Sie sind dabei %0 Dateien zu verarbeiten. Fortfahren?
@@ -474,8 +481,6 @@ Import\ preferences=Einstellungen importieren
Import\ preferences\ from\ file=Einstellungen aus Datei importieren
-Imported\ entries=Einträge importiert
-
Importer\ class=Importer Klasse
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Untergruppen berücksichtigen\: Einträge dieser Gruppe und ihrer Untergruppen anzeigen
@@ -484,10 +489,10 @@ Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Unabh
I\ Agree=Ich stimme zu
Indexing\ bib\ fields\ for\ %0=Indizierung von Bib-Feldern für %0
-Indexing\ PDF\ files\ for\ %0=Indizierung von PDF-Dateien für %0
+Indexing\ %0=Indiziere %0
+Indexing\ files\ for\ %1\ |\ %2\ of\ %0\ file(s)\ indexed.=Indiziere Datei(en) für %1 | %2 von %0 Datei(en) indiziert.
%0\ of\ %1\ entries\ added\ to\ the\ index.=%0 von %1 Einträgen zum Index hinzugefügt.
%0\ of\ %1\ entries\ removed\ from\ the\ index.=%0 von %1 Einträgen aus dem Index entfernt.
-Indexing\ %0.\ %1\ of\ %2\ files\ added\ to\ the\ index.=Indizierung von %0. %1 von %2 Dateien zum Index hinzugefügt.
Removing\ entries\ from\ index\ for\ %0=Entferne Einträge aus dem Index für %0
Invalid\ URL=Ungültige URL
@@ -802,15 +807,19 @@ Character\ encoding\ '%0'\ is\ not\ supported.=Die Zeichenkodierung '%0' wird ni
Filter\ search\ results=Suchergebnisse filtern
Filter\ by\ groups=Nach Gruppe filtern
Invert\ groups=Gruppen umkehren
-Scroll\ to\ previous\ match\ category=Zur vorherigen Übereinstimmungskategorie scrollen
-Scroll\ to\ next\ match\ category=Zur nächsten Übereinstimmungskategorie scrollen
+Scroll\ to\ previous\ match\ category=Vorherige Trefferkategorie anzeigen
+Scroll\ to\ next\ match\ category=Nächste Trefferkategorie anzeigen
Search=Suchen
Search...=Suchen...
Searching...=Suche läuft...
Finished\ Searching=Suche beendet
Search\ expression=Suchausdruck
+This\ only\ affects\ unfielded\ terms.\ For\ using\ RegEx\ in\ a\ fielded\ term,\ use\ \=~\ operator.=Dies betrifft nur Felder ohne Feldnamen. Um RegEx in einem Feld mit Feldnamen zu verwenden, benutzen Sie den \=~ Operator.
+This\ only\ affects\ unfielded\ terms.\ For\ using\ case-sensitive\ in\ a\ fielded\ term,\ use\ \=\!\ operator.=Dies betrifft nur Felder ohne Feldnamen. Für die Groß-/Kleinschreibung in einem Feld mit Feldnamen verwenden Sie den \=\! Operator.
Fulltext\ search=Volltextsuche
+Enable\ indexing=Indizierung aktivieren
+Fulltext\ search\ requires\ the\ setting\ 'Automatically\ index\ all\ linked\ files\ for\ fulltext\ search'\ to\ be\ enabled.\ Do\ you\ want\ to\ enable\ indexing\ now?=Für die Volltextsuche muss die Einstellung 'Alle verknüpften Dateien automatisch für die Volltextsuche indizieren' aktiviert sein. Möchten Sie die Indizierung jetzt aktivieren?
Help\ on\ regular\ expression\ search=Hilfe zur Suche mit regulärem Ausdruck
Searching\ for\ duplicates...=Suche nach doppelten Einträgen...
@@ -818,6 +827,7 @@ Searching\ for\ files=Suche nach Dateien
Use\ regular\ expression\ search=Suche mit regulärem Ausdruck benutzen
search\ expression=Suchausdruck
Free\ search\ expression=Freier Suchausdruck
+Illegal\ search\ expression=Ungültiger Suchbegriff
No\ search\ matches.=Keine Übereinstimmungen gefunden.
Web\ search=Internetrecherche
Search\ results=Suchergebnisse
@@ -832,6 +842,7 @@ Clear\ search=Suche zurücksetzen
Search\ document\ identifier\ online=Suche online nach Dokumentenbezeichner
Search\ for\ unlinked\ local\ files=Suche nach nicht verlinkten Dateien
Search\ full\ text\ documents\ online=Suche Volltextdokumente online
+Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\=Smith\ AND\ title\=electrical=Tipp\:\n\nUm alle Felder nach Smith zu durchsuchen, geben Sie ein\:\nsmith\n\nUm das Feld author nach Smith und das Feld title nach electrical zu durchsuchen, geben Sie ein\:\nauthor\: Smith AND title\: electrical
Search\ term\ is\ empty.=Suchbegriff ist leer.
Invalid\ regular\ expression.=Ungültiger regulärer Ausdruck.
Searching\ for\ a\ keyword=Suche nach einem Schlüsselwort
@@ -992,8 +1003,10 @@ Could\ not\ move\ file\ '%0'.=Datei '%0' konnte nicht verschoben werden.
Could\ not\ find\ file\ '%0'.=Datei '%0' nicht gefunden.
Error\ opening\ file=Fehler beim Öffnen der Datei
Error\ opening\ file\ '%0'=Fehler beim Öffnen der Datei '%0'
+File\ '%0'\ already\ linked=Datei '%0' bereits verknüpft
+
+%0\ entry(s)\ imported=Keine Einträge importiert
-Number\ of\ entries\ successfully\ imported=Zahl der erfolgreich importierten Einträge
Error\ while\ fetching\ from\ %0=Fehler beim Abrufen von %0
Unable\ to\ open\ link.=Öffnen des Links nicht möglich
@@ -1043,6 +1056,7 @@ Waiting\ for\ background\ tasks\ to\ finish.\ Quit\ anyway?=Warte auf das Beende
Find\ and\ remove\ duplicate\ citation\ keys=Doppelte Zitationsschlüssel suchen und entfernen
Expected\ syntax\ for\ --fetch\='\:'=Erwartete Syntax für --fetch\='\:'
+Library-specific\ file\ directory=Bibliothekseigener Dateipfad
User-specific\ file\ directory=Benutzerdefiniertes Dateiverzeichnis
LaTeX\ file\ directory=LaTeX-Dateiverzeichnis
@@ -1630,7 +1644,7 @@ Found\ overlapping\ ranges=Überlappende Bereiche gefunden
Found\ touching\ ranges=Sich berührende Bereiche gefunden
Note\:\ Use\ the\ placeholder\ %DIR%\ for\ the\ location\ of\ the\ opened\ library\ file.=Hinweis\: %DIR% als Platzhalter für den Speicherort der Bibliothek benutzen.
-Error\ occurred\ while\ executing\ the\ command\ "%0".=Während der Ausführung des Befehls "%0" ist ein Fehler aufgetreten.
+Error\ occurred\ while\ executing\ the\ command\ "%0".=Während der Ausführung des Befehls \"%0\" ist ein Fehler aufgetreten.
Reformat\ ISSN=Formatiere ISSN
Computer\ science=Informatik
@@ -1746,6 +1760,8 @@ Remote\ services=Remote-Dienste
Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Port %0 konnte nicht für externen Zugriff genutzt werden; er wird möglicherweise von einer anderen Anwendung benutzt. Versuchen Sie einen anderen Port.
Grobid\ URL=Grobid URL
Allow\ sending\ PDF\ files\ and\ raw\ citation\ strings\ to\ a\ JabRef\ online\ service\ (Grobid)\ to\ determine\ Metadata.\ This\ produces\ better\ results.=Erlaube das Senden von PDF-Dateien und Rohzitaten an einen JabRef Online-Dienst (Grobid) um Metadaten zu ermitteln. Dies führt zu besseren Ergebnissen.
+Send\ to\ Grobid=An Grobid senden
+Do\ not\ send=Nicht senden
Proxy\ requires\ password=Proxy benötigt ein Passwort
Proxy\ configuration=Proxy Einstellungen
@@ -1787,6 +1803,7 @@ See\ what\ has\ been\ changed\ in\ the\ JabRef\ versions=Beschreibt was in den v
Referenced\ citation\ key\ '%0'\ does\ not\ exist=Referenzierter Zitationsschlüssel '%0' existiert nicht
Full\ text\ document\ for\ entry\ %0\ already\ linked.=Volltextdokument für Eintrag %0 bereits verknüpft.
Download\ full\ text\ documents=Volltext-Dokumente herunterladen
+You\ are\ attempting\ to\ download\ full\ text\ documents\ for\ %0\ entries.\nJabRef\ will\ send\ at\ least\ one\ request\ per\ entry\ to\ a\ publisher.=Sie versuchen, Volltextdokumente für %0 Einträge herunterzuladen.\nJabRef sendet mindestens eine Anfrage pro Eintrag an einen Verlag.
last\ four\ nonpunctuation\ characters\ should\ be\ numerals=Die letzten vier Nichtinterpunktionszeichen sollten Ziffern sein
Author=Autor
@@ -2117,7 +2134,18 @@ This\ operation\ requires\ an\ open\ library.=Dieser Vorgang erfordert eine offe
Add\ to\ current\ library=Zur aktuellen Bibliothek hinzufügen
%0\ entries\ were\ parsed\ from\ your\ query.=%0 Einträge wurden aus Ihrer Anfrage extrahiert.
Your\ text\ is\ being\ parsed...=Ihr Text wird analysiert...
+LLM=LLM
+Please\ verify\ any\ information\ provided.=Bitte prüfen Sie die angegebenen Informationen.
+Warning\:\ plain\ citation\ parsing\ may\ generate\ inaccurate\ or\ inappropriate\ responses.=Warnung\: Ungenaue Ergebnisse bei einfacher Zitatverarbeitung möglich.
New\ entry\ from\ plain\ text\ (online)=Neuer Eintrag aus reinem Text (online)
+Parser\ choice=Parser-Auswahl
+Plain\ Citations\ Parser=Parser für einfache Zitate
+Please\ enter\ the\ plain\ citations\ to\ parse\ from\ separated\ by\ double\ empty\ lines.=Einfache Zitate bitte mit zwei Leerzeilen trennen.
+Rule-based=Regelbasiert
+Starts\ the\ parsing\ and\ adds\ the\ resulting\ entries\ to\ the\ currently\ opened\ database=Startet die Auswertung und fügt die Einträge zur geöffneten Datenbank hinzu
+Unable\ to\ parse\ plain\ citations.\ Detailed\ information\:\ %0=Einfache Zitate konnten nicht verarbeitet werden. Details\: %0
+Default\ plain\ citation\ parser=Standard-Parser für einfache Zitate
+Grobid=Grobid
Citation\ key\ filters=Zitationsschlüssel-Filter
Field\ filters=Feld-Filter
@@ -2536,7 +2564,6 @@ Are\ you\ sure\ you\ want\ to\ clear\ the\ chat\ history\ of\ this\ entry?=Sind
Context\ window\ size=Größe des Kontextfensters
Context\ window\ size\ must\ be\ greater\ than\ 0=Kontext-Fenstergröße muss größer als 0 sein
Instruction\ for\ AI\ (also\ known\ as\ prompt\ or\ system\ message)=Anweisungen für KI (auch als Prompt oder Systemnachricht bekannt)
-The\ instruction\ has\ to\ be\ provided=Die Anweisung muss bereitgestellt werden
An\ I/O\ error\ occurred\ while\ opening\ the\ embedding\ model\ by\ URL\ %0=Beim Öffnen des Einbettungsmodells von URL %0 ist ein E/A-Fehler aufgetreten
Got\ error\ while\ processing\ the\ file\:=Fehler beim Verarbeiten der Datei\:
The\ model\ by\ URL\ %0\ is\ malformed=Das Modell unter der URL %0 ist fehlerhaft
@@ -2613,6 +2640,20 @@ RAG\ minimum\ score\ must\ be\ a\ number=RAG Mindestpunktzahl muss numerisch sei
RAG\ minimum\ score\ must\ be\ greater\ than\ 0\ and\ less\ than\ 1=RAG-Mindestpunktzahl muss größer als 0 und kleiner als 1 sein
Temperature\ must\ be\ a\ number=Temperatur muss numerisch sein
If\ you\ have\ chosen\ %0\ as\ an\ AI\ provider,\ the\ privacy\ policy\ of\ %0\ applies.\ You\ find\ it\ at\ %1.=Wenn Sie %0 als KI-Anbieter gewählt haben, gilt die Datenschutzrichtlinie von %0. Sie finden sie unter %1.
+Automatically\ generate\ embeddings\ for\ new\ entries=Einbettungen für neue Einträge automatisch erstellen
+Automatically\ generate\ summaries\ for\ new\ entries=Zusammenfassungen für neue Einträge automatisch erstellen
+Connection=Verbindung
+Generate\ embeddings\ for\ linked\ files\ in\ the\ group=Einbettungen für verknüpfte Dateien in der Gruppe erstellen
+Generate\ summaries\ for\ entries\ in\ the\ group=Zusammenfassungen für Einträge in der Gruppe erstellen
+Generating\ summaries\ for\ %0=Generiere Zusammenfassungen für %0
+Ingestion\ started\ for\ group\ "%0".=Einlesen für Gruppe '%0' gestartet.
+Summarization\ started\ for\ group\ "%0".=Zusammenfassung für Gruppe '%0' gestartet.
+Reset\ templates\ to\ default=Vorlagen zurücksetzen
+Templates=Vorlagen
+System\ message\ for\ chatting=Systemnachricht (Chat)
+User\ message\ for\ chatting=Benutzernachricht
+Completion\ text\ for\ summarization\ of\ a\ chunk=Ergänzung zur Zusammenfassung eines Abschnitts
+Completion\ text\ for\ summarization\ of\ several\ chunks=Ergänzung zur Zusammenfassung mehrerer Abschnitte
Link=Link
Source\ URL=Quellen-URL
@@ -2728,9 +2769,9 @@ Pushing\ citations\ to\ TeXShop\ is\ only\ possible\ on\ macOS\!=Zitate in TeXSh
Single\ instance=Einzelne Instanz
-Copied\ %0\ entry(ies)=Kopierte %0 Einträge
-Cut\ %0\ entry(ies)=%0 Eintrage ausgeschnitten
-Deleted\ %0\ entry(ies)=%0 Einträge gelöscht
+Copied\ %0\ entry(s)=%0 Eintrag/Einträge kopiert
+Cut\ %0\ entry(s)=%0 Eintrag(e) ausgeschnitten
+Deleted\ %0\ entry(s)=%0 Eintrag/Einträge gelöscht
Enable\ Journal\ Information\ Fetching?=Journalinformationen abrufen?
Would\ you\ like\ to\ enable\ fetching\ of\ journal\ information?\ This\ can\ be\ changed\ later\ in\ %0\ >\ %1.=Möchten Sie das Abrufen von Journal-Informationen aktivieren? Dies kann später in %0 > %1 geändert werden.
@@ -2763,3 +2804,9 @@ Currently\ selected\ JStyle\:\ '%0' = Derzeit ausgewählter JStyle\: '%0'
Currently\ selected\ CSL\ Style\:\ '%0' = Derzeit ausgewählter CSL-Stil\: '%0'
Store\ url\ for\ downloaded\ file=Url für heruntergeladene Datei speichern
+Compare\ with\ existing\ entry=Mit vorhandenem Eintrag vergleichen
+Library\ Entry=Bibliothekseintrag
+Citation\ Entry=Zitationseintrag
+
+File\ Move\ Errors=Fehler beim Verschieben von Dateien
+Could\ not\ move\ file\ %0.\ Please\ close\ this\ file\ and\ retry.=Datei %0 konnte nicht verschoben werden. Bitte schließen Sie diese Datei und versuchen Sie es erneut.
diff --git a/src/main/resources/l10n/JabRef_el.properties b/src/main/resources/l10n/JabRef_el.properties
index ed247e38e89..8e758879050 100644
--- a/src/main/resources/l10n/JabRef_el.properties
+++ b/src/main/resources/l10n/JabRef_el.properties
@@ -393,8 +393,6 @@ Import\ preferences=Εισαγωγή προτιμήσεων
Import\ preferences\ from\ file=Εισαγωγή προτιμήσεων από αρχείο
-Imported\ entries=Καταχωρήσεις που έχουν εισαχθεί
-
Importer\ class=Κλάση εισαγωγέα
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Να περιλαμβάνονται υπο-ομάδες\: Όταν επιλεχθεί αυτό, προβάλλονται οι καταχωρήσεις που περιλαμβάνονται σε αυτή την ομάδα ή τις υπο-ομάδες της
@@ -852,7 +850,7 @@ Could\ not\ move\ file\ '%0'.=Αδυναμία μετακίνησης αρχεί
Could\ not\ find\ file\ '%0'.=Αδυναμία εύρεσης αρχείου '%0'.
Error\ opening\ file=Σφάλμα κατά το άνοιγμα αρχείου
-Number\ of\ entries\ successfully\ imported=Έγινε εισαγωγή του αριθμού των καταχωρήσεων με επιτυχία
+
Error\ while\ fetching\ from\ %0=Σφάλμα κατά την ανάκτηση από %0
Unable\ to\ open\ link.=Αδυναμία ανοίγματος συνδέσμου.
@@ -1792,3 +1790,4 @@ Related\ articles=Σχετικά άρθρα
+
diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties
index 7505818e8fe..cdd2cad328a 100644
--- a/src/main/resources/l10n/JabRef_en.properties
+++ b/src/main/resources/l10n/JabRef_en.properties
@@ -319,7 +319,8 @@ Extract\ References\ (online)=Extract References (online)
Processing...=Processing...
Processing\ "%0"...=Processing "%0"...
Processing\ PDF(s)=Processing PDF(s)
-Processing\ file\ %0=Processing file %0
+Processing\ %0=Processing %0
+Importing\ files\ into\ %1\ |\ %2\ of\ %0\ file(s)\ processed.=Importing files into %1 | %2 of %0 file(s) processed.
Processing\ a\ large\ number\ of\ files=Processing a large number of files
You\ are\ about\ to\ process\ %0\ files.\ Continue?=You are about to process %0 files. Continue?
@@ -480,8 +481,6 @@ Import\ preferences=Import preferences
Import\ preferences\ from\ file=Import preferences from file
-Imported\ entries=Imported entries
-
Importer\ class=Importer class
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Include subgroups: When selected, view entries contained in this group or its subgroups
@@ -490,10 +489,10 @@ Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Indepe
I\ Agree=I Agree
Indexing\ bib\ fields\ for\ %0=Indexing bib fields for %0
-Indexing\ PDF\ files\ for\ %0=Indexing PDF files for %0
+Indexing\ %0=Indexing %0
+Indexing\ files\ for\ %1\ |\ %2\ of\ %0\ file(s)\ indexed.=Indexing files for %1 | %2 of %0 file(s) indexed.
%0\ of\ %1\ entries\ added\ to\ the\ index.=%0 of %1 entries added to the index.
%0\ of\ %1\ entries\ removed\ from\ the\ index.=%0 of %1 entries removed from the index.
-Indexing\ %0.\ %1\ of\ %2\ files\ added\ to\ the\ index.=Indexing %0. %1 of %2 files added to the index.
Removing\ entries\ from\ index\ for\ %0=Removing entries from index for %0
Invalid\ URL=Invalid URL
@@ -1006,7 +1005,8 @@ Error\ opening\ file=Error opening file
Error\ opening\ file\ '%0'=Error opening file '%0'
File\ '%0'\ already\ linked=File '%0' already linked
-Number\ of\ entries\ successfully\ imported=Number of entries successfully imported
+%0\ entry(s)\ imported=%0 entry(s) imported
+
Error\ while\ fetching\ from\ %0=Error while fetching from %0
Unable\ to\ open\ link.=Unable to open link.
@@ -1059,6 +1059,8 @@ Expected\ syntax\ for\ --fetch\='\:'=Expected syntax f
Library-specific\ file\ directory=Library-specific file directory
User-specific\ file\ directory=User-specific file directory
LaTeX\ file\ directory=LaTeX file directory
+Path\ %0\ could\ not\ be\ resolved.\ Using\ working\ dir.=Path %0 could not be resolved. Using working dir.
+
You\ must\ enter\ an\ integer\ value\ in\ the\ interval\ 1025-65535=You must enter an integer value in the interval 1025-65535
Autocomplete\ names\ in\ 'Firstname\ Lastname'\ format\ only=Autocomplete names in 'Firstname Lastname' format only
@@ -1760,6 +1762,8 @@ Remote\ services=Remote services
Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Cannot use port %0 for remote operation; another application may be using it. Try specifying another port.
Grobid\ URL=Grobid URL
Allow\ sending\ PDF\ files\ and\ raw\ citation\ strings\ to\ a\ JabRef\ online\ service\ (Grobid)\ to\ determine\ Metadata.\ This\ produces\ better\ results.=Allow sending PDF files and raw citation strings to a JabRef online service (Grobid) to determine Metadata. This produces better results.
+Send\ to\ Grobid=Send to Grobid
+Do\ not\ send=Do not send
Proxy\ requires\ password=Proxy requires password
Proxy\ configuration=Proxy configuration
@@ -2562,7 +2566,6 @@ Are\ you\ sure\ you\ want\ to\ clear\ the\ chat\ history\ of\ this\ entry?=Are y
Context\ window\ size=Context window size
Context\ window\ size\ must\ be\ greater\ than\ 0=Context window size must be greater than 0
Instruction\ for\ AI\ (also\ known\ as\ prompt\ or\ system\ message)=Instruction for AI (also known as prompt or system message)
-The\ instruction\ has\ to\ be\ provided=The instruction has to be provided
An\ I/O\ error\ occurred\ while\ opening\ the\ embedding\ model\ by\ URL\ %0=An I/O error occurred while opening the embedding model by URL %0
Got\ error\ while\ processing\ the\ file\:=Got error while processing the file:
The\ model\ by\ URL\ %0\ is\ malformed=The model by URL %0 is malformed
@@ -2647,6 +2650,12 @@ Generate\ summaries\ for\ entries\ in\ the\ group=Generate summaries for entries
Generating\ summaries\ for\ %0=Generating summaries for %0
Ingestion\ started\ for\ group\ "%0".=Ingestion started for group "%0".
Summarization\ started\ for\ group\ "%0".=Summarization started for group "%0".
+Reset\ templates\ to\ default=Reset templates to default
+Templates=Templates
+System\ message\ for\ chatting=System message for chatting
+User\ message\ for\ chatting=User message for chatting
+Completion\ text\ for\ summarization\ of\ a\ chunk=Completion text for summarization of a chunk
+Completion\ text\ for\ summarization\ of\ several\ chunks=Completion text for summarization of several chunks
Link=Link
Source\ URL=Source URL
@@ -2762,9 +2771,9 @@ Pushing\ citations\ to\ TeXShop\ is\ only\ possible\ on\ macOS\!=Pushing citatio
Single\ instance=Single instance
-Copied\ %0\ entry(ies)=Copied %0 entry(ies)
-Cut\ %0\ entry(ies)=Cut %0 entry(ies)
-Deleted\ %0\ entry(ies)=Deleted %0 entry(ies)
+Copied\ %0\ entry(s)=Copied %0 entry(s)
+Cut\ %0\ entry(s)=Cut %0 entry(s)
+Deleted\ %0\ entry(s)=Deleted %0 entry(s)
Enable\ Journal\ Information\ Fetching?=Enable Journal Information Fetching?
Would\ you\ like\ to\ enable\ fetching\ of\ journal\ information?\ This\ can\ be\ changed\ later\ in\ %0\ >\ %1.=Would you like to enable fetching of journal information? This can be changed later in %0 > %1.
diff --git a/src/main/resources/l10n/JabRef_es.properties b/src/main/resources/l10n/JabRef_es.properties
index a21e480ccdf..3b7fbba315a 100644
--- a/src/main/resources/l10n/JabRef_es.properties
+++ b/src/main/resources/l10n/JabRef_es.properties
@@ -302,7 +302,6 @@ Export\ to\ text\ file.=Exportar a un archivo de texto.
Processing...=Procesando...
-Processing\ file\ %0=Procesando archivo %0
You\ are\ about\ to\ process\ %0\ files.\ Continue?=Está por procesar %0 archivos. ¿Quiere continuar?
@@ -428,8 +427,6 @@ Import\ preferences=Importar preferencias
Import\ preferences\ from\ file=Importar preferencias desde archivo
-Imported\ entries=Importar entradas
-
Importer\ class=Importar formato de clase
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Incluir subgrupos\: Ver entradas contenidas es este grupo o sus subgrupos cuando estén seleccionadas.
@@ -919,7 +916,7 @@ Could\ not\ find\ file\ '%0'.=No se encuentra el archivo '%0'.
Error\ opening\ file=Error al abrir el archivo
Error\ opening\ file\ '%0'=Error al abrir el archivo «%0»
-Number\ of\ entries\ successfully\ imported=Número de entradas importadas con éxito
+
Error\ while\ fetching\ from\ %0=Error al recuperar desde %0
Unable\ to\ open\ link.=No es posible abrir el enlace.
@@ -2459,9 +2456,6 @@ Related\ articles=Artículos relacionados
-Copied\ %0\ entry(ies)=Se copiaron %0 entradas
-Cut\ %0\ entry(ies)=Se cortaron %0 entradas
-Deleted\ %0\ entry(ies)=Se eliminaron %0 entradas
Enable=Activar
Keep\ disabled=Mantener desactivado
@@ -2485,3 +2479,4 @@ Warning\:\ Failed\ to\ check\ if\ the\ directory\ is\ empty.=Atención\: no se h
Warning\:\ The\ selected\ directory\ is\ not\ a\ valid\ directory.=Atención\: el directorio seleccionado no es válido.
+
diff --git a/src/main/resources/l10n/JabRef_fa.properties b/src/main/resources/l10n/JabRef_fa.properties
index 60c593eccb0..cfec353d983 100644
--- a/src/main/resources/l10n/JabRef_fa.properties
+++ b/src/main/resources/l10n/JabRef_fa.properties
@@ -254,7 +254,6 @@ Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=فراد
-
Open\ file=بازکردن پرونده
@@ -390,6 +389,7 @@ Search\ results\ from\ open\ libraries=جستجوی نتایج از کتابخا
+
Check\ integrity=بررسی بینقصی
@@ -657,5 +657,6 @@ Auto\ complete\ enabled.=تکمیل خودکار غیرفعال شد.
+
diff --git a/src/main/resources/l10n/JabRef_fi.properties b/src/main/resources/l10n/JabRef_fi.properties
index 748d3d59c95..d79921567de 100644
--- a/src/main/resources/l10n/JabRef_fi.properties
+++ b/src/main/resources/l10n/JabRef_fi.properties
@@ -601,5 +601,6 @@ Proxy\ requires\ password=Välityspalvelin vaatii salasanan
+
diff --git a/src/main/resources/l10n/JabRef_fr.properties b/src/main/resources/l10n/JabRef_fr.properties
index 987502da283..44205cf0320 100644
--- a/src/main/resources/l10n/JabRef_fr.properties
+++ b/src/main/resources/l10n/JabRef_fr.properties
@@ -319,7 +319,8 @@ Extract\ References\ (online)=Extraire les références (en ligne)
Processing...=Traitement en cours...
Processing\ "%0"...=Traitement de « %0 »...
Processing\ PDF(s)=Traitement des PDF(s) en cours
-Processing\ file\ %0=Traitement du fichier %0
+Processing\ %0=Traitement de %0
+Importing\ files\ into\ %1\ |\ %2\ of\ %0\ file(s)\ processed.=Indexation des fichiers pour %1 | %2 de %0 fichier(s) indexé(s).
Processing\ a\ large\ number\ of\ files=Traitement d'un grand nombre de fichiers
You\ are\ about\ to\ process\ %0\ files.\ Continue?=Vous êtes sur le point de traiter %0 fichiers. Continuer ?
@@ -480,8 +481,6 @@ Import\ preferences=Importer les préférences
Import\ preferences\ from\ file=Importer les préférences depuis un fichier
-Imported\ entries=Entrées importées
-
Importer\ class=Classe d'importateur
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Inclut les sous-groupes \: quand sélectionné, affiche les entrées contenues dans ce groupe ou ses sous-groupes
@@ -490,10 +489,10 @@ Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Groupe
I\ Agree=J’accepte
Indexing\ bib\ fields\ for\ %0=Indexation des champs bib à %0
-Indexing\ PDF\ files\ for\ %0=Indexation des fichiers PDF à %0
+Indexing\ %0=Indexation de %0
+Indexing\ files\ for\ %1\ |\ %2\ of\ %0\ file(s)\ indexed.=Indexation des fichiers pour %1 | %2 de %0 fichier(s) indexé(s).
%0\ of\ %1\ entries\ added\ to\ the\ index.=%0 sur %1 entrées ajoutées à l'index.
%0\ of\ %1\ entries\ removed\ from\ the\ index.=%0 of %1 entrées supprimées de l'index.
-Indexing\ %0.\ %1\ of\ %2\ files\ added\ to\ the\ index.=Indexation de %0. %1 sur %2 fichiers ajoutés à l'index.
Removing\ entries\ from\ index\ for\ %0=Suppression des entrées de l'index à %0
Invalid\ URL=URL invalide
@@ -815,6 +814,8 @@ Search...=Recherche...
Searching...=Recherche...
Finished\ Searching=Recherche terminée
Search\ expression=Expression à rechercher
+This\ only\ affects\ unfielded\ terms.\ For\ using\ RegEx\ in\ a\ fielded\ term,\ use\ \=~\ operator.=Cela ne concerne que les champs sans nom de champ. Pour utiliser RegEx dans un terme en champ, utilisez l'opérateur \=~ .
+This\ only\ affects\ unfielded\ terms.\ For\ using\ case-sensitive\ in\ a\ fielded\ term,\ use\ \=\!\ operator.=Cela ne concerne que les termes sans nom de champ. Pour la sensibilité à la casse dans un champ avec nom de champ, utilisez l'opérateur \=\! .
Fulltext\ search=Recherche dans les documents
Enable\ indexing=Activer l'indexation
@@ -1004,7 +1005,8 @@ Error\ opening\ file=Erreur lors de l'ouverture du fichier
Error\ opening\ file\ '%0'=Erreur lors de l'ouverture du fichier '%0'
File\ '%0'\ already\ linked=Le fichier '%0' est déjà lié
-Number\ of\ entries\ successfully\ imported=Nombre d'entrées importées avec succès
+%0\ entry(s)\ imported=%0 entrée(s) importée(s)
+
Error\ while\ fetching\ from\ %0=Erreur au cours de la collecte %0
Unable\ to\ open\ link.=Impossible d'ouvrir le lien.
@@ -1642,7 +1644,7 @@ Found\ overlapping\ ranges=Plages se chevauchant trouvées
Found\ touching\ ranges=Plages contiguës trouvées
Note\:\ Use\ the\ placeholder\ %DIR%\ for\ the\ location\ of\ the\ opened\ library\ file.=Note \: Utiliser le paramètre %DIR% pour le répertoire du fichier ouvert.
-Error\ occurred\ while\ executing\ the\ command\ "%0".=Une erreur est survenue lors de l'exécution de la commande "%0".
+Error\ occurred\ while\ executing\ the\ command\ "%0".=Une erreur est survenue lors de l'exécution de la commande \"%0\".
Reformat\ ISSN=Reformater l'ISSN
Computer\ science=Informatique (en anglais)
@@ -1758,6 +1760,8 @@ Remote\ services=Services distants
Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Le port %0 ne peut pas être utilisé pour une opération à distance ; un autre logiciel pourrait être en train de l'utiliser. Essayer de spécifier un autre port.
Grobid\ URL=URL de Grobid
Allow\ sending\ PDF\ files\ and\ raw\ citation\ strings\ to\ a\ JabRef\ online\ service\ (Grobid)\ to\ determine\ Metadata.\ This\ produces\ better\ results.=Autoriser l'envoi de fichiers PDF et de chaînes de citation brutes à un service en ligne de JabRef (Grobid) pour déterminer les métadonnées. Cela donne de meilleurs résultats.
+Send\ to\ Grobid=Envoyer à Grobid
+Do\ not\ send=Ne pas envoyer
Proxy\ requires\ password=Le proxy nécessite un mot de passe
Proxy\ configuration=Configuration du Proxy
@@ -2560,7 +2564,6 @@ Are\ you\ sure\ you\ want\ to\ clear\ the\ chat\ history\ of\ this\ entry?=Êtes
Context\ window\ size=Taille de la fenêtre de contexte
Context\ window\ size\ must\ be\ greater\ than\ 0=La taille de la fenêtre de contexte doit être supérieure à 0
Instruction\ for\ AI\ (also\ known\ as\ prompt\ or\ system\ message)=Instructions pour l'IA (également connue sous le nom de message d'invite ou de système)
-The\ instruction\ has\ to\ be\ provided=Les instructions doivent être fournies
An\ I/O\ error\ occurred\ while\ opening\ the\ embedding\ model\ by\ URL\ %0=Une erreur d'E/S s'est produite lors de l'ouverture du modèle d'intégration par l'URL %0
Got\ error\ while\ processing\ the\ file\:=Erreur lors du traitement du fichier \:
The\ model\ by\ URL\ %0\ is\ malformed=Le modèle par URL %0 est mal formé
@@ -2645,6 +2648,12 @@ Generate\ summaries\ for\ entries\ in\ the\ group=Générer des résumés pour l
Generating\ summaries\ for\ %0=Génération de résumés pour %0
Ingestion\ started\ for\ group\ "%0".=Le traitement a commencé pour le groupe « %0 ».
Summarization\ started\ for\ group\ "%0".=Résumé démarré pour le groupe « %0 ».
+Reset\ templates\ to\ default=Réinitialiser les modèles par défaut
+Templates=Modèles
+System\ message\ for\ chatting=Message système pour le tchat
+User\ message\ for\ chatting=Message de l'utilisateur pour le tchat
+Completion\ text\ for\ summarization\ of\ a\ chunk=Texte de complétion pour la synthèse d'un fragment
+Completion\ text\ for\ summarization\ of\ several\ chunks=Texte de complétion pour la synthèse de plusieurs fragments
Link=Lien
Source\ URL=URL de la source
@@ -2760,9 +2769,9 @@ Pushing\ citations\ to\ TeXShop\ is\ only\ possible\ on\ macOS\!=L'envoi des cit
Single\ instance=Instance unique
-Copied\ %0\ entry(ies)=%0 entrée(s) copié(es)
-Cut\ %0\ entry(ies)=%0 entrée(s) coupé(es)
-Deleted\ %0\ entry(ies)=%0 entrée(s) supprimée(s)
+Copied\ %0\ entry(s)=%0 entrée(s) copiée(s)
+Cut\ %0\ entry(s)=%0 entrée(s) coupée(s)
+Deleted\ %0\ entry(s)=%0 entrée(s) supprimée(s)
Enable\ Journal\ Information\ Fetching?=Activer la récupération des informations des journaux ?
Would\ you\ like\ to\ enable\ fetching\ of\ journal\ information?\ This\ can\ be\ changed\ later\ in\ %0\ >\ %1.=Voulez-vous activer la récupération des informations des journaux ? Cela peut être modifié plus tard dans %0 > %1.
@@ -2798,3 +2807,6 @@ Store\ url\ for\ downloaded\ file=Enregistrer l'URL du fichier téléchargé
Compare\ with\ existing\ entry=Comparer avec une entrée existante
Library\ Entry=Entrée du fichier
Citation\ Entry=Entrée de citation
+
+File\ Move\ Errors=Erreurs de déplacement de fichier
+Could\ not\ move\ file\ %0.\ Please\ close\ this\ file\ and\ retry.=Impossible de déplacer le fichier %0. Veuillez fermer ce fichier et réessayer.
diff --git a/src/main/resources/l10n/JabRef_id.properties b/src/main/resources/l10n/JabRef_id.properties
index d81d3917b6e..3808f0f5c8e 100644
--- a/src/main/resources/l10n/JabRef_id.properties
+++ b/src/main/resources/l10n/JabRef_id.properties
@@ -345,8 +345,6 @@ Import\ preferences=Preferensi Impor
Import\ preferences\ from\ file=Impor preferensi dari berkas
-Imported\ entries=entri diimpor
-
Importer\ class=kelas Importer
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Termasuk sub-grup\: Ketika dipilih, lihat entri yang ada di grup atau sub-grup ini.
@@ -715,7 +713,7 @@ Could\ not\ move\ file\ '%0'.=Tidak bisa meindah berkas '%0'.
Could\ not\ find\ file\ '%0'.=Tidak bisa menemukan berkas '%0'.
Error\ opening\ file=Kesalahan ketika membuka berkas
-Number\ of\ entries\ successfully\ imported=Jumlah entri yang berhasil diimpor
+
Error\ while\ fetching\ from\ %0=Kesalahan ketika mengambil dari %0
Unable\ to\ open\ link.=Tidak bisa membuka tautan.
@@ -1556,3 +1554,4 @@ Related\ articles=Artikel terkait
+
diff --git a/src/main/resources/l10n/JabRef_it.properties b/src/main/resources/l10n/JabRef_it.properties
index 36265d27f3c..9151455d45e 100644
--- a/src/main/resources/l10n/JabRef_it.properties
+++ b/src/main/resources/l10n/JabRef_it.properties
@@ -319,7 +319,8 @@ Extract\ References\ (online)=Estrai Riferimenti (online)
Processing...=Elaborazione...
Processing\ "%0"...=Elaborazione di %0"...
Processing\ PDF(s)=Elaborazione PDF
-Processing\ file\ %0=Elaborazione file %0
+Processing\ %0=Elaborazione di %0
+Importing\ files\ into\ %1\ |\ %2\ of\ %0\ file(s)\ processed.=Importazione di file in %1 | %2 di %0 file elaborati.
Processing\ a\ large\ number\ of\ files=Elaborazione di un gran numero di file
You\ are\ about\ to\ process\ %0\ files.\ Continue?=Stai per elaborare %0 file. Continuare?
@@ -480,8 +481,6 @@ Import\ preferences=Importa preferenze
Import\ preferences\ from\ file=Importa preferenze da un file
-Imported\ entries=Voci importate
-
Importer\ class=Classe Importer
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Includi i sottogruppi\: Quando selezionato, mostra le voci contenute in questo gruppo e nei suoi sottogruppi
@@ -490,10 +489,10 @@ Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Gruppo
I\ Agree=Accetto
Indexing\ bib\ fields\ for\ %0=Indicizzazione dei campi bib per %0
-Indexing\ PDF\ files\ for\ %0=Indicizzazione dei file PDF per %0
+Indexing\ %0=Indicizzazione di %0
+Indexing\ files\ for\ %1\ |\ %2\ of\ %0\ file(s)\ indexed.=Indicizzazione di file per %1 | %2 di %0 file indicizzati.
%0\ of\ %1\ entries\ added\ to\ the\ index.=%0 di %1 voci aggiunte all'indice.
%0\ of\ %1\ entries\ removed\ from\ the\ index.=%0 di %1 voci rimosse dall'indice.
-Indexing\ %0.\ %1\ of\ %2\ files\ added\ to\ the\ index.=Indicizzazione %0. %1 di %2 file aggiunti all'indice.
Removing\ entries\ from\ index\ for\ %0=Rimozione delle voci dall'indice per %0
Invalid\ URL=URL non valido
@@ -1006,7 +1005,8 @@ Error\ opening\ file=Errore all'apertura del file
Error\ opening\ file\ '%0'=Errore nell'aprire il file '%0'
File\ '%0'\ already\ linked=File '%0' già collegato
-Number\ of\ entries\ successfully\ imported=Numero di voci importate con successo
+%0\ entry(s)\ imported=%0 voci importate
+
Error\ while\ fetching\ from\ %0=Errore durante la ricerca %0
Unable\ to\ open\ link.=Impossibile aprire il collegamento.
@@ -1760,6 +1760,8 @@ Remote\ services=Servizi remoti
Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Impossibile utilizzare la porta %0 per operazioni remote; la porta potrebbe essere in uso da parte di un'altra applicazione. Provare a specificare una porta diversa.
Grobid\ URL=Url Grobid
Allow\ sending\ PDF\ files\ and\ raw\ citation\ strings\ to\ a\ JabRef\ online\ service\ (Grobid)\ to\ determine\ Metadata.\ This\ produces\ better\ results.=Consenti l'invio di file PDF e stringhe di citazione grezze a un servizio online JabRef (Grobid) per determinare i metadati. Questo produce risultati migliori.
+Send\ to\ Grobid=Invia a Grobid
+Do\ not\ send=Non inviare
Proxy\ requires\ password=Il proxy richiede password
Proxy\ configuration=Configurazione proxy
@@ -2562,7 +2564,6 @@ Are\ you\ sure\ you\ want\ to\ clear\ the\ chat\ history\ of\ this\ entry?=Sei s
Context\ window\ size=Dimensione finestra contestuale
Context\ window\ size\ must\ be\ greater\ than\ 0=La dimensione della finestra contestuale deve essere maggiore di 0
Instruction\ for\ AI\ (also\ known\ as\ prompt\ or\ system\ message)=Istruzione per l'IA (nota anche come prompt o messaggio di sistema)
-The\ instruction\ has\ to\ be\ provided=L'istruzione deve essere fornita
An\ I/O\ error\ occurred\ while\ opening\ the\ embedding\ model\ by\ URL\ %0=Si è verificato un errore I/O durante l'apertura del modello incorporato dall'URL %0
Got\ error\ while\ processing\ the\ file\:=Si è verificato un errore durante l'elaborazione del file\:
The\ model\ by\ URL\ %0\ is\ malformed=Il modello di URL %0 è malformato
@@ -2616,6 +2617,12 @@ Generate\ summaries\ for\ entries\ in\ the\ group=Genera sommari per le voci nel
Generating\ summaries\ for\ %0=Generazione di riepiloghi per %0
Ingestion\ started\ for\ group\ "%0".=Ingestione iniziata per il gruppo "%0".
Summarization\ started\ for\ group\ "%0".=Riepilogo avviato per il gruppo "%0".
+Reset\ templates\ to\ default=Reimposta i modelli a quelli predefiniti
+Templates=Modelli
+System\ message\ for\ chatting=Messaggio di sistema per la chat
+User\ message\ for\ chatting=Messaggio utente per la chat
+Completion\ text\ for\ summarization\ of\ a\ chunk=Testo di completamento per la sintesi di un pezzo
+Completion\ text\ for\ summarization\ of\ several\ chunks=Testo di completamento per riepilogo di diversi pezzi
Link=Collegamento
Source\ URL=URL di origine
@@ -2731,9 +2738,9 @@ Pushing\ citations\ to\ TeXShop\ is\ only\ possible\ on\ macOS\!=Inviare le cita
Single\ instance=Singola istanza
-Copied\ %0\ entry(ies)=Copiate %0 voci
-Cut\ %0\ entry(ies)=Taglia %0 voci
-Deleted\ %0\ entry(ies)=Cancellate %0 voci
+Copied\ %0\ entry(s)=Copiate %0 voci
+Cut\ %0\ entry(s)=Taglia %0 voci
+Deleted\ %0\ entry(s)=Cancellate %0 voci
Enable\ Journal\ Information\ Fetching?=Abilitare il recupero delle informazioni della rivista?
Would\ you\ like\ to\ enable\ fetching\ of\ journal\ information?\ This\ can\ be\ changed\ later\ in\ %0\ >\ %1.=Vuoi abilitare il recupero delle informazioni della rivista? Questo può essere modificato in seguito in %0 > %1.
@@ -2769,3 +2776,6 @@ Store\ url\ for\ downloaded\ file=Memorizza url per il file scaricato
Compare\ with\ existing\ entry=Confronta con la voce esistente
Library\ Entry=Voce della Libreria
Citation\ Entry=Voce della Citazione
+
+File\ Move\ Errors=Errori di spostamente dei file
+Could\ not\ move\ file\ %0.\ Please\ close\ this\ file\ and\ retry.=Impossibile spostare il file %0. Chiudere questo file e riprovare.
diff --git a/src/main/resources/l10n/JabRef_ja.properties b/src/main/resources/l10n/JabRef_ja.properties
index 8dd4a2b101f..7071b2d9662 100644
--- a/src/main/resources/l10n/JabRef_ja.properties
+++ b/src/main/resources/l10n/JabRef_ja.properties
@@ -288,7 +288,6 @@ Export\ to\ text\ file.=テキストファイルに書き出します.
Processing...=処理中...
-Processing\ file\ %0=ファイル %0 を処理しています
Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=選択した項目からリンクされたPDFにメタデータを書き込みます.
@@ -406,8 +405,6 @@ Import\ preferences=設定を読み込む
Import\ preferences\ from\ file=ファイルから設定を読み込み
-Imported\ entries=項目を読み込みました
-
Importer\ class=Importerクラス
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=下層グループの取り込み:このグループやその配下の下層グループに含まれている項目を表示
@@ -867,7 +864,7 @@ Could\ not\ move\ file\ '%0'.=ファイルを%0移動できませんでした
Could\ not\ find\ file\ '%0'.=ファイル「%0」を見つけられませんでした.
Error\ opening\ file=ファイルを開く際にエラー発生
-Number\ of\ entries\ successfully\ imported=読み込みに成功した項目数
+
Error\ while\ fetching\ from\ %0=%0からの取得中にエラー発生
Unable\ to\ open\ link.=リンクを開くことができませんでした.
@@ -2359,3 +2356,4 @@ Related\ articles=関連文献
+
diff --git a/src/main/resources/l10n/JabRef_ko.properties b/src/main/resources/l10n/JabRef_ko.properties
index 2d216f69340..8e395560bf8 100644
--- a/src/main/resources/l10n/JabRef_ko.properties
+++ b/src/main/resources/l10n/JabRef_ko.properties
@@ -273,7 +273,6 @@ Export\ to\ clipboard=클립보드로 내보내기
Export\ to\ text\ file.=텍스트 파일로 내보내기
-Processing\ file\ %0=파일 %0 처리중
@@ -386,8 +385,6 @@ Import\ preferences=환경 설정 가져오기
Import\ preferences\ from\ file=파일에서 설정 가져오기
-Imported\ entries=가져온 목록
-
Importer\ class=클래스 가져오기
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=하위 그룹 포함\: 선택하면 이 그룹 또는 하위 그룹에 포함된 항목을 봅니다.
@@ -819,7 +816,7 @@ Could\ not\ move\ file\ '%0'.=파일 '%0' 을 이동할 수 없습니다.
Could\ not\ find\ file\ '%0'.=파일 "%0" 을 찾을 수 없습니다
Error\ opening\ file=파일을 여는 동안 오류
-Number\ of\ entries\ successfully\ imported=항목들을 성공적으로 가져왔습니다
+
Error\ while\ fetching\ from\ %0=%0에서 가져오는 중 오류 발생
Unable\ to\ open\ link.=링크를 열 수 없습니다.
@@ -2193,3 +2190,4 @@ Related\ articles=관련 글
+
diff --git a/src/main/resources/l10n/JabRef_nl.properties b/src/main/resources/l10n/JabRef_nl.properties
index 03c73e912c5..0cf1de57a77 100644
--- a/src/main/resources/l10n/JabRef_nl.properties
+++ b/src/main/resources/l10n/JabRef_nl.properties
@@ -293,7 +293,6 @@ Export\ to\ text\ file.=Exporteer naar tekstbestand.
Processing...=Verwerken...
-Processing\ file\ %0=Bestand %0 wordt verwerkt
Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=Zal metadata schrijven naar de PDF's gelinkt van geselecteerde invoer.
@@ -413,8 +412,6 @@ Import\ preferences=Instellingen importeren
Import\ preferences\ from\ file=Importeer instellingen van bestand
-Imported\ entries=Geïmporteerde invoergegevens
-
Importer\ class=Importer Klasse
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Subgroepen insluiten\: Wanneer geselecteerd, toon invoergegevens in deze groep of in zijn subgroepen
@@ -899,7 +896,7 @@ Could\ not\ move\ file\ '%0'.=Kon bestand '%0' niet verplaatsen.
Could\ not\ find\ file\ '%0'.=Kon bestand '%0' niet vinden.
Error\ opening\ file=Foutmelding bij het openen van het bestand
-Number\ of\ entries\ successfully\ imported=Aantal invoergegevens succesvol geïmporteerd
+
Error\ while\ fetching\ from\ %0=Fout tijdens het ophalen van %0
Unable\ to\ open\ link.=Link openen niet mogelijk.
@@ -2458,3 +2455,4 @@ Related\ articles=Gerelateerde artikelen
+
diff --git a/src/main/resources/l10n/JabRef_no.properties b/src/main/resources/l10n/JabRef_no.properties
index 9f037dbae81..d7d2f0aea26 100644
--- a/src/main/resources/l10n/JabRef_no.properties
+++ b/src/main/resources/l10n/JabRef_no.properties
@@ -320,8 +320,6 @@ Import\ preferences=Importer innstillinger
Import\ preferences\ from\ file=Importer innstillinger fra fil
-Imported\ entries=Importerte enheter
-
Importer\ class=Importer-klasse
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Inkluder undergrupper\: Vis enheter inneholdt i denne gruppen eller en undergruppe
@@ -684,7 +682,7 @@ Could\ not\ move\ file\ '%0'.=Kunne ikke flytte filen '%0'.
Could\ not\ find\ file\ '%0'.=Kunne ikke finne filen '%0'.
Error\ opening\ file=Feil ved åpning av fil
-Number\ of\ entries\ successfully\ imported=Antall oppføringer som ble korrekt importert
+
Error\ while\ fetching\ from\ %0=Feil ved henting fra %0
Unable\ to\ open\ link.=Kan ikke åpne link.
@@ -1124,4 +1122,5 @@ Path\ to\ %0=Sti til %0
+
diff --git a/src/main/resources/l10n/JabRef_pl.properties b/src/main/resources/l10n/JabRef_pl.properties
index e2b1b356b0c..11da1bb4b10 100644
--- a/src/main/resources/l10n/JabRef_pl.properties
+++ b/src/main/resources/l10n/JabRef_pl.properties
@@ -300,7 +300,8 @@ Extract\ References\ (online)=Wyodrębnij referencje (online)
Processing...=Przetwarzanie...
Processing\ "%0"...=Przetwarzanie "%0"...
Processing\ PDF(s)=Przetwarzanie PDF(ów)
-Processing\ file\ %0=Przetwarzanie pliku %0
+Processing\ %0=Przetwarzanie %0
+Importing\ files\ into\ %1\ |\ %2\ of\ %0\ file(s)\ processed.=Importowanie plików do %1 | %2 z %0 plików przetworzonych.
Processing\ a\ large\ number\ of\ files=Przetwarzanie dużej liczby plików
You\ are\ about\ to\ process\ %0\ files.\ Continue?=Zamierzasz przetworzyć %0 plików. Czy chcesz kontynuować?
@@ -438,13 +439,13 @@ Import\ preferences=Ustawienia importu
Import\ preferences\ from\ file=Importuj ustawienia z pliku
-Imported\ entries=Zaimportowane wpisy
-
Importer\ class=Klasa importera
I\ Agree=Wyrażam zgodę
+Indexing\ %0=Indeksowanie %0
+Indexing\ files\ for\ %1\ |\ %2\ of\ %0\ file(s)\ indexed.=Indeksowanie plików do %1 | %2 z %0 plików przetworzonych.
Invalid\ URL=Nieprawidłowy URL
Online\ help=Pomoc online
@@ -940,7 +941,8 @@ Error\ opening\ file=Błąd podczas otwierania pliku
Error\ opening\ file\ '%0'=Błąd podczas otwierania pliku '%0'
File\ '%0'\ already\ linked=Plik '%0' jest już połączony
-Number\ of\ entries\ successfully\ imported=Liczba wpisów pomyślnie zaimportowanych
+%0\ entry(s)\ imported=Zaimportowano %0 wpisów
+
Error\ while\ fetching\ from\ %0=Błąd podczas pobierania z %0
Unable\ to\ open\ link.=Nie można otworzyć linku.
@@ -1469,6 +1471,8 @@ Remember\ Password=Zapamiętaj hasło
Remote\ operation=Zdalna operacja
Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Nie można użyć portu %0 do zdalnej operacji; inna aplikacja może go używać. Spróbuj określić inny port.
+Send\ to\ Grobid=Wyślij do Grobid
+Do\ not\ send=Nie wysyłaj
Proxy\ requires\ password=Proxy wymaga hasła
Proxy\ configuration=Konfiguracja proxy
@@ -1796,6 +1800,10 @@ Generate\ summaries\ for\ entries\ in\ the\ group=Generuj podsumowania dla rekor
Generating\ summaries\ for\ %0=Generowanie podsumowań dla %0
Ingestion\ started\ for\ group\ "%0".=Rozpoczęto przetwarzanie w grupie "%0".
Summarization\ started\ for\ group\ "%0".=Rozpoczęto streszczanie w grupie "%0".
+Reset\ templates\ to\ default=Przywróć domyślne szablony
+Templates=Szablony
+System\ message\ for\ chatting=Wiadomość systemowa dla czatu
+User\ message\ for\ chatting=Wiadomość użytkownika dla czatu
Link=Odnośnik
Source\ URL=Źródłowy adres URL
@@ -1867,7 +1875,9 @@ Show\ tab\ 'Related\ articles'=Pokaż zakładkę 'Powiązane artykuły'
-Copied\ %0\ entry(ies)=Skopiowano %0 wpis(ów)
+Copied\ %0\ entry(s)=Skopiowano %0 wpisów
+Cut\ %0\ entry(s)=Wycięto %0 wpisów
+Deleted\ %0\ entry(s)=Skasowano %0 wpisów
@@ -1889,3 +1899,6 @@ Store\ url\ for\ downloaded\ file=Przechowuj adres URL pobranego pliku
Compare\ with\ existing\ entry=Porównaj z istniejącym wpisem
Library\ Entry=Wpis biblioteki
Citation\ Entry=Wpis cytatowania
+
+File\ Move\ Errors=Błąd przenoszenia pliku
+Could\ not\ move\ file\ %0.\ Please\ close\ this\ file\ and\ retry.=Nie można przenieść pliku %0. Zamknij ten plik i spróbuj ponownie.
diff --git a/src/main/resources/l10n/JabRef_pt.properties b/src/main/resources/l10n/JabRef_pt.properties
index 32abbbce0ea..9971e8a6a3a 100644
--- a/src/main/resources/l10n/JabRef_pt.properties
+++ b/src/main/resources/l10n/JabRef_pt.properties
@@ -392,8 +392,6 @@ Import\ preferences=Importar preferências
Import\ preferences\ from\ file=Importar preferências a partir de um arquivo
-Imported\ entries=Referências importadas
-
Importer\ class=Classe Importer
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Incluir subgrupos\: Quando selecionado, visualizar referências contidas neste grupo ou em seus subgrupos
@@ -818,7 +816,7 @@ Could\ not\ move\ file\ '%0'.=Não foi possível mover o arquivo '%0'.
Could\ not\ find\ file\ '%0'.=Não foi possível encontrar o arquivo '%0'.
Error\ opening\ file=Erro ao abrir o arquivo
-Number\ of\ entries\ successfully\ imported=Número de referências importadas com sucesso
+
Error\ while\ fetching\ from\ %0=Erro ao recuperar do %0
Unable\ to\ open\ link.=Não foi possível abrir link.
@@ -1373,3 +1371,4 @@ Related\ articles=Artigos relacionados
+
diff --git a/src/main/resources/l10n/JabRef_pt_BR.properties b/src/main/resources/l10n/JabRef_pt_BR.properties
index 8ff637d5d3f..be1385e4085 100644
--- a/src/main/resources/l10n/JabRef_pt_BR.properties
+++ b/src/main/resources/l10n/JabRef_pt_BR.properties
@@ -103,8 +103,11 @@ Available\ import\ formats=Formatos de importação disponíveis
Show\ BibTeX\ source=Mostrar fonte BibTeX
Show/edit\ %0\ source=Exibir/editar fonte %0
+Background\ tasks=Tarefas em segundo plano
+Background\ tasks\ are\ running=Tarefas em segundo plano estão sendo executadas
+Background\ tasks\ are\ finished=Tarefas em segundo plano finalizadas
Browse=Explorar
@@ -316,7 +319,8 @@ Extract\ References\ (online)=Extrair Referências (online)
Processing...=Processando...
Processing\ "%0"...=Processando "%0"...
Processing\ PDF(s)=Processando PDF(s)
-Processing\ file\ %0=Processando arquivo %0
+Processing\ %0=Processando %0
+Importing\ files\ into\ %1\ |\ %2\ of\ %0\ file(s)\ processed.=Importando arquivos para %1 | %2 arquivo(s) de %0 processado(s).
Processing\ a\ large\ number\ of\ files=Processando um grande número de arquivos
You\ are\ about\ to\ process\ %0\ files.\ Continue?=Você está prestes a processar %0 arquivos. Continuar?
@@ -476,8 +480,6 @@ Import\ preferences=Importar preferências
Import\ preferences\ from\ file=Importar preferências a partir de um arquivo
-Imported\ entries=Referências importadas
-
Importer\ class=Classe Importer
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Incluir subgrupos\: Quando selecionado, visualizar referências contidas neste grupo ou em seus subgrupos
@@ -486,10 +488,10 @@ Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Grupo
I\ Agree=Concordo
Indexing\ bib\ fields\ for\ %0=Indexando campos bib para %0
-Indexing\ PDF\ files\ for\ %0=Indexando arquivos PDF para %0
+Indexing\ %0=Indexando %0
+Indexing\ files\ for\ %1\ |\ %2\ of\ %0\ file(s)\ indexed.=Indexando arquivos para %1 | %2 arquivo(s) de %0 indexado.
%0\ of\ %1\ entries\ added\ to\ the\ index.=%0 de %1 entradas adicionadas ao índice.
%0\ of\ %1\ entries\ removed\ from\ the\ index.=%0 dos %1 registros removidos do índice.
-Indexing\ %0.\ %1\ of\ %2\ files\ added\ to\ the\ index.=Indexando %0. %1 dos arquivos %2 adicionados ao índice.
Removing\ entries\ from\ index\ for\ %0=Removendo entradas do índice %0
Invalid\ URL=URL inválida
@@ -822,6 +824,7 @@ Searching\ for\ files=Procurando por arquivos...
Use\ regular\ expression\ search=Utilizar pesquisa por expressão regular
search\ expression=expressão de pesquisa
Free\ search\ expression=Expressão de busca livre
+Illegal\ search\ expression=Expressão de busca ilegal
No\ search\ matches.=Sem correspondências.
Web\ search=Pesquisa na Web
Search\ results=Resultados da pesquisa
@@ -836,6 +839,7 @@ Clear\ search=Limpar busca
Search\ document\ identifier\ online=Pesquisar identificador de documento na web
Search\ for\ unlinked\ local\ files=Procurar por arquivos locais desvinculados
Search\ full\ text\ documents\ online=Pesquisar textos completos on-line
+Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\=Smith\ AND\ title\=electrical=Dica\:\n\nPara pesquisar em todos os campos por Smith, entre\:\nSmith\n\nPara pesquisar no campo autor por Smith e no campo título porelétrico, Enter\:\nautor\=Smith AND título\=elétrico
Search\ term\ is\ empty.=O termo de pesquisa está vazio.
Invalid\ regular\ expression.=Expressão regular inválida.
Searching\ for\ a\ keyword=Procurando palavras-chave
@@ -996,8 +1000,10 @@ Could\ not\ move\ file\ '%0'.=Não foi possível mover o arquivo '%0'.
Could\ not\ find\ file\ '%0'.=Não foi possível encontrar o arquivo '%0'.
Error\ opening\ file=Erro ao abrir o arquivo
Error\ opening\ file\ '%0'=Erro ao abrir arquivo '%0'
+File\ '%0'\ already\ linked=Arquivo '%0' já vinculado
+
+%0\ entry(s)\ imported=%0 entrada(s) importadas
-Number\ of\ entries\ successfully\ imported=Número de referências importadas com sucesso
Error\ while\ fetching\ from\ %0=Erro ao recuperar do %0
Unable\ to\ open\ link.=Não foi possível abrir link.
@@ -1047,6 +1053,7 @@ Waiting\ for\ background\ tasks\ to\ finish.\ Quit\ anyway?=Aguardando a finaliz
Find\ and\ remove\ duplicate\ citation\ keys=Localizar e remover chaves de citação duplicadas
Expected\ syntax\ for\ --fetch\='\:'=Sintaxe esperada para --fetch\='\:'
+Library-specific\ file\ directory=Diretório de arquivos específicos da biblioteca
User-specific\ file\ directory=Diretório de arquivo específico do usuário
LaTeX\ file\ directory=Diretório de arquivos LaTeX
@@ -1634,7 +1641,7 @@ Found\ overlapping\ ranges=Intervalos sobrepostos encontrados
Found\ touching\ ranges=Intervalos que se tocam foram encontrados
Note\:\ Use\ the\ placeholder\ %DIR%\ for\ the\ location\ of\ the\ opened\ library\ file.=Nota\: Use o %DIR% para definir a localização do arquivo da biblioteca aberta.
-Error\ occurred\ while\ executing\ the\ command\ "%0".=Ocorreu um erro ao executar o comando "%0".
+Error\ occurred\ while\ executing\ the\ command\ "%0".=Ocorreu um erro ao executar o comando \"%0\".
Reformat\ ISSN=Reformatar ISSN
Computer\ science=Ciência da computação
@@ -1750,6 +1757,8 @@ Remote\ services=Serviços remotos
Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Não é possível utilizar a porta %0 para operação remota; outra aplicação pode estar usando-a. Tente utilizar uma outra porta.
Grobid\ URL=URL Grobid
Allow\ sending\ PDF\ files\ and\ raw\ citation\ strings\ to\ a\ JabRef\ online\ service\ (Grobid)\ to\ determine\ Metadata.\ This\ produces\ better\ results.=Permite enviar arquivos PDF e strings de citação bruta para um serviço JabRef on-line (Grobid) para determinar Metadados. Isto produz melhores resultados.
+Send\ to\ Grobid=Enviar para Grobid
+Do\ not\ send=Não enviar
Proxy\ requires\ password=Proxy requisita senha
Proxy\ configuration=Configuração de proxy
@@ -2552,7 +2561,6 @@ Are\ you\ sure\ you\ want\ to\ clear\ the\ chat\ history\ of\ this\ entry?=Você
Context\ window\ size=Tamanho da janela de contexto
Context\ window\ size\ must\ be\ greater\ than\ 0=Tamanho da janela de contexto deve ser maior que 0
Instruction\ for\ AI\ (also\ known\ as\ prompt\ or\ system\ message)=Instrução para IA (também conhecido como prompt ou mensagem do sistema)
-The\ instruction\ has\ to\ be\ provided=A instrução tem de ser fornecida
An\ I/O\ error\ occurred\ while\ opening\ the\ embedding\ model\ by\ URL\ %0=Um erro de I/O ocorreu ao abrir o modelo de incorporação pela URL %0
Got\ error\ while\ processing\ the\ file\:=Ocorreu um erro ao processar o arquivo\:
The\ model\ by\ URL\ %0\ is\ malformed=O modelo pela URL %0 está malformado
@@ -2637,6 +2645,12 @@ Generate\ summaries\ for\ entries\ in\ the\ group=Gerar resumos para referência
Generating\ summaries\ for\ %0=Gerando resumos para %0
Ingestion\ started\ for\ group\ "%0".=Assimilação iniciada para o grupo "%0".
Summarization\ started\ for\ group\ "%0".=Resumo iniciado para o grupo "%0".
+Reset\ templates\ to\ default=Redefinir modelos para padrão
+Templates=Modelos
+System\ message\ for\ chatting=Mensagem de sistema para bate-papo
+User\ message\ for\ chatting=Mensagem de usuário para bate-papo
+Completion\ text\ for\ summarization\ of\ a\ chunk=Texto de conclusão para o resumo de um pedaço
+Completion\ text\ for\ summarization\ of\ several\ chunks=Texto de conclusão para o resumo de vários pedaços
Link=Linkar
Source\ URL=URL de origem
@@ -2752,9 +2766,9 @@ Pushing\ citations\ to\ TeXShop\ is\ only\ possible\ on\ macOS\!=Só é possíve
Single\ instance=Instância única
-Copied\ %0\ entry(ies)=Copiou %0 entrada(s)
-Cut\ %0\ entry(ies)=Cortar %0 entrada(s)
-Deleted\ %0\ entry(ies)=%0 entrada(s) excluída(s)
+Copied\ %0\ entry(s)=%0 entrada(s) copiada(s)
+Cut\ %0\ entry(s)=Cortar %0 entrada(s)
+Deleted\ %0\ entry(s)=%0 entrada(s) excluída(s)
Enable\ Journal\ Information\ Fetching?=Ativar busca de informações do periódico?
Would\ you\ like\ to\ enable\ fetching\ of\ journal\ information?\ This\ can\ be\ changed\ later\ in\ %0\ >\ %1.=Você gostaria de habilitar a busca de informações de periódicos? Isto pode ser alterado posteriormente em %0 > %1.
@@ -2790,3 +2804,6 @@ Store\ url\ for\ downloaded\ file=Salvar url do arquivo baixado
Compare\ with\ existing\ entry=Comparar com entrada existente
Library\ Entry=Registro de Biblioteca
Citation\ Entry=Chave de citação
+
+File\ Move\ Errors=Erro ao Mover Arquivo
+Could\ not\ move\ file\ %0.\ Please\ close\ this\ file\ and\ retry.=Não foi possível mover o arquivo %0. Por favor, feche este arquivo e tente novamente.
diff --git a/src/main/resources/l10n/JabRef_ru.properties b/src/main/resources/l10n/JabRef_ru.properties
index 8170dc01861..c0b44f18daa 100644
--- a/src/main/resources/l10n/JabRef_ru.properties
+++ b/src/main/resources/l10n/JabRef_ru.properties
@@ -294,7 +294,6 @@ Export\ to\ text\ file.=Экспортировать в текстовый фа
Processing\ PDF(s)=Обработка PDF(ов)
-Processing\ file\ %0=Обработка файла %0
Processing\ a\ large\ number\ of\ files=Обработка большого количества файлов
@@ -419,8 +418,6 @@ Import\ preferences=Импорт пользовательских настрое
Import\ preferences\ from\ file=Импорт пользовательских настроек из файла
-Imported\ entries=Импортированные записи
-
Importer\ class=Класс Importer
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Включить подгруппы\: Просмотр записей, содержащихся в этой группе или ее подгруппах (если выбрано)
@@ -885,7 +882,7 @@ Could\ not\ move\ file\ '%0'.=Не удалось переместить фай
Could\ not\ find\ file\ '%0'.=Не удалось найти файл '%0'.
Error\ opening\ file=Ошибка при открытии файла
-Number\ of\ entries\ successfully\ imported=Число успешно импортированных записей
+
Error\ while\ fetching\ from\ %0=Ошибка выполнения выборки %0
Unable\ to\ open\ link.=Не удалось перейти по ссылке.
@@ -2327,3 +2324,4 @@ Related\ articles=Связанные статьи
+
diff --git a/src/main/resources/l10n/JabRef_sv.properties b/src/main/resources/l10n/JabRef_sv.properties
index 9898920800f..bddb5ad12ce 100644
--- a/src/main/resources/l10n/JabRef_sv.properties
+++ b/src/main/resources/l10n/JabRef_sv.properties
@@ -1,12 +1,12 @@
-Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ restart.\ You\ may\ encounter\ errors\ if\ you\ continue\ with\ this\ session.=Kan inte övervaka filändringar. Stäng filer och processer och starta om. Du kan stöta på fel om du fortsätter med denna session.
+Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ restart.\ You\ may\ encounter\ errors\ if\ you\ continue\ with\ this\ session.=Det går inte att övervaka filändringar. Stäng filer och processer och starta om. Du kan stöta på fel om du fortsätter med denna session.
%0/%1\ entries=%0/%1 poster
-Export\ operation\ finished\ successfully.=Exportoperationen slutfördes framgångsrikt.
+Export\ operation\ finished\ successfully.=Exportåtgärden slutfördes.
-Reveal\ in\ File\ Explorer=Visa i Filutforskaren
+Reveal\ in\ File\ Explorer=Visa i Utforskaren
-Abbreviate\ journal\ names\ of\ the\ selected\ entries\ (DEFAULT\ abbreviation)=Förkorta tidskriftsnamnen för valda poster (STANDARD förkortning)
+Abbreviate\ journal\ names\ of\ the\ selected\ entries\ (DEFAULT\ abbreviation)=Förkorta tidskriftsnamnen för markerade poster (STANDARD-förkortning)
Abbreviate\ names=Förkorta namn
Abbreviated\ %0\ journal\ names.=Förkortade %0 tidskriftsnamn.
@@ -370,8 +370,6 @@ Import\ preferences=Importera inställningar
Import\ preferences\ from\ file=Importera inställningar från fil
-Imported\ entries=Importerade poster
-
Importer\ class=Importer-klass
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Inkludera undergrupper\: När detta är valts visas poster som finns i denna grupp och dess undergrupper
@@ -738,7 +736,7 @@ Could\ not\ find\ file\ '%0'.=Kunde inte hitta filen '%0'.
Error\ opening\ file=Fel vid öppning av fil
Error\ opening\ file\ '%0'=Fel vid öppning av filen '%0'
-Number\ of\ entries\ successfully\ imported=Antal poster som lyckades importeras
+
Error\ while\ fetching\ from\ %0=Fel vid hämtning från %0
Unable\ to\ open\ link.=Kan inte öppna länk.
@@ -1520,3 +1518,4 @@ Warning\:\ Failed\ to\ check\ if\ the\ directory\ is\ empty.=Varning\: Det gick
Warning\:\ The\ selected\ directory\ is\ not\ a\ valid\ directory.=Varning\: Den valda mappen är inte en giltig mapp.
+
diff --git a/src/main/resources/l10n/JabRef_tl.properties b/src/main/resources/l10n/JabRef_tl.properties
index 1be3c7ae6c0..03eb0060e63 100644
--- a/src/main/resources/l10n/JabRef_tl.properties
+++ b/src/main/resources/l10n/JabRef_tl.properties
@@ -319,8 +319,6 @@ Import\ preferences=I-import ang mga preferences
Import\ preferences\ from\ file=I-import ang mga preferences mula sa file
-Imported\ entries=Mga imported na entries
-
Importer\ class=Ang importer na klase
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Isama ang mga mababang grupo\: Kapag napili, tingnan ang mga entries na nilalaman ng grupo o sa mababang grupo
@@ -681,7 +679,7 @@ Could\ not\ move\ file\ '%0'.=Hindi mailipat ang file '%0'.
Could\ not\ find\ file\ '%0'.=Hindi mahanap ang file na '%0'.
Error\ opening\ file=May mali sa pag bukas ng file
-Number\ of\ entries\ successfully\ imported=Ang bilang ng mga entry ay matagumpay na na-import
+
Error\ while\ fetching\ from\ %0=Error habang kinukuha mula sa %0
Unable\ to\ open\ link.=Hindi mabuksan ang link.
@@ -1253,3 +1251,4 @@ Related\ articles=Kaugnay na mga artikulo
+
diff --git a/src/main/resources/l10n/JabRef_tr.properties b/src/main/resources/l10n/JabRef_tr.properties
index 9ae2b65f140..f6241c68cb3 100644
--- a/src/main/resources/l10n/JabRef_tr.properties
+++ b/src/main/resources/l10n/JabRef_tr.properties
@@ -50,6 +50,7 @@ Added\ group\ "%0".="%0" grubu eklendi.
Added\ string\:\ '%0'=Dizge eklendi\: '%0'
Added\ string=Dizge eklendi
Edit\ strings=Dizgeleri düzenle
+Duplicate\ string\ name\:\ '%0'='%0' adlı metin zaten mevcut
Modified\ string=Değiştirilmiş dizge
Modified\ string\:\ '%0' =Değiştirilmiş dizge\: '%0'
New\ string=Yeni dizge
@@ -68,6 +69,10 @@ Add\ new\ String=Yeni Dizge Ekle
String\ constants=Dizge sabitleri
Must\ not\ be\ empty\!=Boş olmamalı\!
A\ string\ with\ the\ label\ '%0'\ already\ exists.='%0' etiketine sahip bir dizge zaten mevcut.
+String\ constant\ "%0"\ was\ not\ imported\ because\ it\ is\ not\ a\ valid\ string\ constant='%0' metin değeri geçerli bir metin değeri olmadığı için içe aktarılamadı
+String\ constant\ %0\ was\ not\ imported\ because\ it\ already\ exists\ in\ this\ library=Bu kitaplıkta %0 metin sabiti zaten mevcut. İçe aktarma işlemi başarısız oldu
+Could\ not\ import\ the\ following\ string\ constants\:\n\ %0=İçe aktarılamayan metin sabitleri\:\n%0
+Importing\ String\ constants=Metin sabitleri içe aktarılıyor
All\ entries=Tüm girdiler
@@ -95,9 +100,12 @@ Available\ export\ formats=Mevcut dışa aktarım biçemleri
Available\ import\ formats=Mevcut içe aktarım biçemleri
%0\ source=%0 kaynağı
+Show\ BibTeX\ source=BibTeX kaynağını görüntüle
Show/edit\ %0\ source=%0 kaynağın göster/düzenle
+Background\ tasks=Arkaplan işlemleri
+Background\ tasks\ are\ running=Arkaplan işlemleri devam ediyor
Browse=Göz at
@@ -294,7 +302,6 @@ Export\ to\ text\ file.=Metin dosyasına aktar.
Processing...=İşleniyor...
-Processing\ file\ %0=Dosya işleniyor %0
Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=Seçili girdilerle bağlantılı PDF'lere metaverisi yazılacak.
@@ -420,8 +427,6 @@ Import\ preferences=İçe aktarma tercihleri
Import\ preferences\ from\ file=Dosyadan içe aktarma tercihleri
-Imported\ entries=İçe aktarılmış girdiler
-
Importer\ class=Importer sınıfı
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Altgrupları içer\: Seçildiğinde, bu grup ya da altgruplarındaki girdileri göster
@@ -913,7 +918,7 @@ Could\ not\ find\ file\ '%0'.='%0' dosyası bulunamadı.
Error\ opening\ file=Dosya açmada hata
Error\ opening\ file\ '%0'=Dosya açmada hata '%0'
-Number\ of\ entries\ successfully\ imported=Girdi sayısı başarıyla içe aktarıldı
+
Error\ while\ fetching\ from\ %0=%0'dan getirme sırasında hata
Unable\ to\ open\ link.=Bağlantı açılamadı.
@@ -1535,7 +1540,7 @@ Found\ overlapping\ ranges=Örtüşen aralıklar bulundu
Found\ touching\ ranges=Birbirine komşu aralıklar bulundu
Note\:\ Use\ the\ placeholder\ %DIR%\ for\ the\ location\ of\ the\ opened\ library\ file.=Not\: Açık kütüphane dosyasının konumu için %DIR% yer tutucusunu kullan.
-Error\ occurred\ while\ executing\ the\ command\ "%0".="%0" komutu çalıştırılırken hata oluştu.
+Error\ occurred\ while\ executing\ the\ command\ "%0".=\"%0\" komutu çalıştırılırken hata oluştu.
Reformat\ ISSN=ISSN'i yeniden biçimlendir
Computer\ science=Bilgisayar bilimi
@@ -2512,3 +2517,4 @@ More\ options...=Daha fazla seçenekler...
+
diff --git a/src/main/resources/l10n/JabRef_uk.properties b/src/main/resources/l10n/JabRef_uk.properties
index 5aecfd04e31..d06e34795db 100644
--- a/src/main/resources/l10n/JabRef_uk.properties
+++ b/src/main/resources/l10n/JabRef_uk.properties
@@ -668,5 +668,6 @@ Proxy\ requires\ password=Потрібен пароль проксі-серве
+
diff --git a/src/main/resources/l10n/JabRef_vi.properties b/src/main/resources/l10n/JabRef_vi.properties
index 00a641ab526..e2dfb886985 100644
--- a/src/main/resources/l10n/JabRef_vi.properties
+++ b/src/main/resources/l10n/JabRef_vi.properties
@@ -328,8 +328,6 @@ Import\ preferences=Nhập các tùy thích
Import\ preferences\ from\ file=Nhập các tùy thích từ tập tin
-Imported\ entries=Các mục được nhập
-
Importer\ class=Lớp ĐịnhdạngNhập
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=Đưa vào các nhóm con\: Khi được chọn, xem các mục chứa trong nhóm này hoặc các nhóm phụ của nó
@@ -679,7 +677,7 @@ Could\ not\ move\ file\ '%0'.=Không thể chuyển tập tin '%0'.
Could\ not\ find\ file\ '%0'.=Không tìm thấy tập tin '%0'.
Error\ opening\ file=Lỗi khi đang mở tập tin
-Number\ of\ entries\ successfully\ imported=Số mục được nhập vào thành công
+
Error\ while\ fetching\ from\ %0=Lỗi khi lấy về từ %0
Unable\ to\ open\ link.=Không thể mở liên kết.
@@ -1058,4 +1056,5 @@ Path\ to\ %0=Đường dẫn đến %0
+
diff --git a/src/main/resources/l10n/JabRef_zh_CN.properties b/src/main/resources/l10n/JabRef_zh_CN.properties
index 06466b53a4c..d9803cbca90 100644
--- a/src/main/resources/l10n/JabRef_zh_CN.properties
+++ b/src/main/resources/l10n/JabRef_zh_CN.properties
@@ -293,7 +293,6 @@ Export\ to\ text\ file.=导出到文本文件。
Processing...=处理中...
-Processing\ file\ %0=正在处理文件 %0
Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=将为选中条目链接的PDF文件写入XMP元数据。
@@ -413,8 +412,6 @@ Import\ preferences=导入首选项设置
Import\ preferences\ from\ file=从文件中导入首选项设置
-Imported\ entries=已导入记录
-
Importer\ class=Importer 类
Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group\ or\ its\ subgroups=包含子分组:当分组被选中时,显示所有它和它的子分组中的记录
@@ -890,7 +887,7 @@ Could\ not\ move\ file\ '%0'.=无法移动文件 '%0'
Could\ not\ find\ file\ '%0'.=无法找到文件 '%0'。
Error\ opening\ file=打开文件错误
-Number\ of\ entries\ successfully\ imported=成功导入的记录数
+
Error\ while\ fetching\ from\ %0=从 %0 抓取发生错误
Unable\ to\ open\ link.=无法打开链接。
@@ -2445,3 +2442,4 @@ Related\ articles=相关文章
+
diff --git a/src/main/resources/l10n/JabRef_zh_TW.properties b/src/main/resources/l10n/JabRef_zh_TW.properties
index 9e502d9027c..706f0e1f4b0 100644
--- a/src/main/resources/l10n/JabRef_zh_TW.properties
+++ b/src/main/resources/l10n/JabRef_zh_TW.properties
@@ -322,8 +322,6 @@ Import\ preferences=匯入偏好設定
Import\ preferences\ from\ file=從檔案匯入偏好設定
-Imported\ entries=已匯入條目
-
I\ Agree=我同意
@@ -616,6 +614,7 @@ Error\ opening\ file=開啟檔案錯誤
+
Rename\ field=重新命名欄位
@@ -1046,3 +1045,4 @@ Related\ articles=相關文章
+
diff --git a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java
index ad0af7443bc..14f10f08cfe 100644
--- a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java
+++ b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java
@@ -9,10 +9,9 @@
import org.jabref.gui.DialogService;
import org.jabref.gui.autocompleter.SuggestionProviders;
import org.jabref.gui.preferences.GuiPreferences;
-import org.jabref.gui.theme.ThemeManager;
+import org.jabref.gui.preview.PreviewPanel;
import org.jabref.gui.undo.RedoAction;
import org.jabref.gui.undo.UndoAction;
-import org.jabref.gui.util.OptionalObjectProperty;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.preferences.OwnerPreferences;
import org.jabref.logic.util.TaskExecutor;
@@ -63,13 +62,13 @@ class CommentsTabTest {
@Mock
private GuiPreferences preferences;
@Mock
- private ThemeManager themeManager;
- @Mock
private TaskExecutor taskExecutor;
@Mock
private JournalAbbreviationRepository journalAbbreviationRepository;
@Mock
private OwnerPreferences ownerPreferences;
+ @Mock
+ private PreviewPanel previewPanel;
@Mock
private EntryEditorPreferences entryEditorPreferences;
@@ -93,11 +92,8 @@ void setUp() {
undoManager,
mock(UndoAction.class),
mock(RedoAction.class),
- dialogService,
- themeManager,
- taskExecutor,
journalAbbreviationRepository,
- OptionalObjectProperty.empty()
+ previewPanel
);
}
diff --git a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java
index 0421103ddc3..de416b2ecd6 100644
--- a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java
+++ b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java
@@ -24,6 +24,7 @@
import org.jabref.logic.util.CurrentThreadTaskExecutor;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.database.BibDatabaseMode;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.StandardEntryType;
@@ -79,6 +80,7 @@ void setUp() {
when(preferences.getCitationKeyPatternPreferences()).thenReturn(citationKeyPatternPreferences);
bibDatabaseContext = new BibDatabaseContext(new BibDatabase());
+ bibDatabaseContext.setMode(BibDatabaseMode.BIBTEX);
when(duplicateCheck.isDuplicate(any(), any(), any())).thenReturn(false);
StateManager stateManager = mock(StateManager.class, Answers.RETURNS_DEEP_STUBS);
@@ -109,31 +111,42 @@ void setUp() {
@Test
void existingEntryCitesOtherPaperWithCitationKeys() {
- var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false),
+ var citationItems = List.of(
+ new CitationRelationItem(firstEntryToImport, false),
new CitationRelationItem(secondEntryToImport, false));
viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITES, existingEntry);
+
assertEquals(Optional.of("FirstAuthorCitationKey2022,SecondAuthorCitationKey20221"), existingEntry.getField(StandardField.CITES));
assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries());
}
@Test
void importedEntriesWithExistingCitationKeysCiteExistingEntry() {
- var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false),
+ var citationItems = List.of(
+ new CitationRelationItem(firstEntryToImport, false),
new CitationRelationItem(secondEntryToImport, false));
viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITED_BY, existingEntry);
- assertEquals(Optional.of("Test2023"), firstEntryToImport.getField(StandardField.CITES));
- assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries());
+
+ // The entries are cloned during the import. Thus, we need to get the actual entries from the database.
+ // In the test, the citation key is not changed during the import, thus we can just look up the entries by their citation key.
+ BibEntry firstEntryInLibrary = bibDatabaseContext.getDatabase().getEntryByCitationKey(firstEntryToImport.getCitationKey().get()).get();
+ BibEntry secondEntryInLibrary = bibDatabaseContext.getDatabase().getEntryByCitationKey(secondEntryToImport.getCitationKey().get()).get();
+
+ assertEquals(Optional.of("Test2023"), firstEntryInLibrary.getField(StandardField.CITES));
+ assertEquals(List.of(existingEntry, firstEntryInLibrary, secondEntryInLibrary), bibDatabaseContext.getEntries());
}
@Test
void existingEntryCitesOtherPaperWithCitationKeysAndExistingCiteField() {
existingEntry.setField(StandardField.CITES, "Asdf1222");
- var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false),
+ var citationItems = List.of(
+ new CitationRelationItem(firstEntryToImport, false),
new CitationRelationItem(secondEntryToImport, false));
viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITES, existingEntry);
+
assertEquals(Optional.of("Asdf1222,FirstAuthorCitationKey2022,SecondAuthorCitationKey20221"), existingEntry.getField(StandardField.CITES));
assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries());
}
diff --git a/src/test/java/org/jabref/gui/linkedfile/LinkedFileEditDialogViewModelTest.java b/src/test/java/org/jabref/gui/linkedfile/LinkedFileEditDialogViewModelTest.java
new file mode 100644
index 00000000000..d19c5d9cbb0
--- /dev/null
+++ b/src/test/java/org/jabref/gui/linkedfile/LinkedFileEditDialogViewModelTest.java
@@ -0,0 +1,55 @@
+package org.jabref.gui.linkedfile;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.TreeSet;
+
+import javafx.collections.FXCollections;
+
+import org.jabref.gui.DialogService;
+import org.jabref.gui.externalfiletype.ExternalFileTypes;
+import org.jabref.gui.frame.ExternalApplicationsPreferences;
+import org.jabref.gui.preferences.GuiPreferences;
+import org.jabref.logic.FilePreferences;
+import org.jabref.model.database.BibDatabaseContext;
+import org.jabref.model.entry.LinkedFile;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Answers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class LinkedFileEditDialogViewModelTest {
+ private final GuiPreferences preferences = mock(GuiPreferences.class, Answers.RETURNS_DEEP_STUBS);
+ private final FilePreferences filePreferences = mock(FilePreferences.class, Answers.RETURNS_DEEP_STUBS);
+ private final BibDatabaseContext bibDatabaseContext = mock(BibDatabaseContext.class);
+ private final DialogService dialogService = mock(DialogService.class);
+ private final ExternalApplicationsPreferences externalApplicationsPreferences = mock(ExternalApplicationsPreferences.class);
+
+ @BeforeEach
+ void setup() {
+ when(externalApplicationsPreferences.getExternalFileTypes()).thenReturn(FXCollections.observableSet(new TreeSet<>(ExternalFileTypes.getDefaultExternalFileTypes())));
+ when(preferences.getExternalApplicationsPreferences()).thenReturn(externalApplicationsPreferences);
+ when(preferences.getFilePreferences()).thenReturn(filePreferences);
+ }
+
+ @Test
+ void badFilenameCharWillBeReplacedByUnderscore(@TempDir Path tempDir) throws Exception {
+
+ Path invalidFile = tempDir.resolve("?invalid.pdf");
+ Files.createFile(invalidFile);
+ when(dialogService.showConfirmationDialogAndWait(any(), any(), any())).thenReturn(true);
+
+ LinkedFileEditDialogViewModel viewModel = new LinkedFileEditDialogViewModel(new LinkedFile("", "", ""), bibDatabaseContext, dialogService, externalApplicationsPreferences, filePreferences);
+
+ viewModel.checkForBadFileNameAndAdd(invalidFile);
+
+ LinkedFile expectedFile = new LinkedFile("", tempDir.resolve("_invalid.pdf").toString(), "PDF");
+ assertEquals(expectedFile, viewModel.getNewLinkedFile());
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citationkeypattern/CitationKeyGeneratorTest.java b/src/test/java/org/jabref/logic/citationkeypattern/CitationKeyGeneratorTest.java
index c84f92c3019..913896f8c1e 100644
--- a/src/test/java/org/jabref/logic/citationkeypattern/CitationKeyGeneratorTest.java
+++ b/src/test/java/org/jabref/logic/citationkeypattern/CitationKeyGeneratorTest.java
@@ -66,7 +66,6 @@ class CitationKeyGeneratorTest {
private static final String AUTHNOFMTH = "[auth%d_%d]";
private static final String AUTHFOREINI = "[authForeIni]";
private static final String AUTHFIRSTFULL = "[authFirstFull]";
- private static final String AUTHORS = "[authors]";
private static final String AUTHORSALPHA = "[authorsAlpha]";
private static final String AUTHORLAST = "[authorLast]";
private static final String AUTHORLASTFOREINI = "[authorLastForeIni]";
@@ -192,14 +191,15 @@ void specialLatexCharacterInAuthorName() throws ParseException {
"@ARTICLE{kohn, author={Andreas Ùöning}, year={2000}}", "Uoe",
# Special cases
- "@ARTICLE{kohn, author={Oraib Al-Ketan}, year={2000}}", "AlK",
+ # We keep "-" in citation keys, thus Al-Ketan with three letters is "Al-"
+ "@ARTICLE{kohn, author={Oraib Al-Ketan}, year={2000}}", "Al-",
"@ARTICLE{kohn, author={Andrés D'Alessandro}, year={2000}}", "DAl",
"@ARTICLE{kohn, author={Andrés Aʹrnold}, year={2000}}", "Arn"
"""
)
void makeLabelAndCheckLegalKeys(String bibtexString, String expectedResult) throws ParseException {
- Optional bibEntry = BibtexParser.singleFromString(bibtexString, importFormatPreferences);
- String citationKey = generateKey(bibEntry.orElse(null), "[auth3]", new BibDatabase());
+ BibEntry bibEntry = BibtexParser.singleFromString(bibtexString, importFormatPreferences).get();
+ String citationKey = generateKey(bibEntry, "[auth3]", new BibDatabase());
String cleanedKey = CitationKeyGenerator.cleanKey(citationKey, DEFAULT_UNWANTED_CHARACTERS);
@@ -493,14 +493,19 @@ void firstAuthorVonAndLastNoVonInName() {
assertEquals("Newton", generateKey(AUTHOR_FIRSTNAME_FULL_LASTNAME_FULL_COUNT_2, AUTHFIRSTFULL));
}
- /**
- * Tests [authors]
- */
- @Test
- void allAuthors() {
- assertEquals("Newton", generateKey(AUTHOR_FIRSTNAME_INITIAL_LASTNAME_FULL_COUNT_1, AUTHORS));
- assertEquals("NewtonMaxwell", generateKey(AUTHOR_FIRSTNAME_INITIAL_LASTNAME_FULL_COUNT_2, AUTHORS));
- assertEquals("NewtonMaxwellEinstein", generateKey(AUTHOR_FIRSTNAME_INITIAL_LASTNAME_FULL_COUNT_3, AUTHORS));
+ @ParameterizedTest
+ @MethodSource
+ void authors(String expectedKey, BibEntry entry, String pattern) {
+ assertEquals(expectedKey, generateKey(entry, pattern));
+ }
+
+ static Stream authors() {
+ return Stream.of(
+ Arguments.of("Newton", AUTHOR_FIRSTNAME_INITIAL_LASTNAME_FULL_COUNT_1, "[authors]"),
+ Arguments.of("NewtonMaxwell", AUTHOR_FIRSTNAME_INITIAL_LASTNAME_FULL_COUNT_2, "[authors]"),
+ Arguments.of("NewtonMaxwellEinstein", AUTHOR_FIRSTNAME_INITIAL_LASTNAME_FULL_COUNT_3, "[authors]"),
+ Arguments.of("Newton-Maxwell-Einstein", AUTHOR_FIRSTNAME_INITIAL_LASTNAME_FULL_COUNT_3, "[authors:regex(\"(.)([A-Z])\",\"$1-$2\")]")
+ );
}
static Stream