Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added "All" option to the citation fetcher combo box, which queries all providers (CrossRef, OpenAlex, OpenCitations, SemanticScholar) and merges the results into a single deduplicated list.
- We added a quick setting toggle to enable cover images download. [#15322](https://github.com/JabRef/jabref/pull/15322)
- We now support refreshing existing CSL citations with respect to their in-text nature in the LibreOffice integration. [#15369](https://github.com/JabRef/jabref/pull/15369)
- We added a "Merge" action in the File menu to compare the current library with a selected BibTeX file and review changes. [#15401](https://github.com/JabRef/jabref/issues/15401)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public enum StandardActions implements Action {
RELEVANT(Localization.lang("Toggle relevance"), IconTheme.JabRefIcons.RELEVANCE),
NEW_LIBRARY(Localization.lang("New empty library"), IconTheme.JabRefIcons.NEW),
OPEN_LIBRARY(Localization.lang("Open library..."), IconTheme.JabRefIcons.OPEN, KeyBinding.OPEN_LIBRARY),
MERGE_LIBRARY(Localization.lang("Merge..."), IconTheme.JabRefIcons.MERGE_ENTRIES),
IMPORT(Localization.lang("Import"), IconTheme.JabRefIcons.IMPORT),
EXPORT(Localization.lang("Export"), IconTheme.JabRefIcons.EXPORT, KeyBinding.EXPORT),
SAVE_LIBRARY(Localization.lang("Save library"), IconTheme.JabRefIcons.SAVE, KeyBinding.SAVE_LIBRARY),
Expand Down
21 changes: 14 additions & 7 deletions jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jabref.gui.collab;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

import org.jabref.gui.DialogService;
Expand Down Expand Up @@ -38,16 +39,22 @@ public List<DatabaseChange> scanForChanges() {
}

try {
// Parse the modified file
// Important: apply all post-load actions
ImportFormatPreferences importFormatPreferences = preferences.getImportFormatPreferences();
ParserResult result = OpenDatabase.loadDatabase(database.getDatabasePath().get(), importFormatPreferences, new DummyFileUpdateMonitor());
BibDatabaseContext databaseOnDisk = result.getDatabaseContext();

return DatabaseChangeList.compareAndGetChanges(database, databaseOnDisk, databaseChangeResolverFactory);
return getDatabaseChanges(database.getDatabasePath().get());
} catch (IOException e) {
LOGGER.warn("Error while parsing changed file.", e);
return List.of();
}
}

public List<DatabaseChange> getDatabaseChanges(Path fileToCompare) throws IOException {
ImportFormatPreferences importFormatPreferences = preferences.getImportFormatPreferences();
ParserResult result = OpenDatabase.loadDatabase(fileToCompare, importFormatPreferences, new DummyFileUpdateMonitor());

if (result.isInvalid() || result.isEmpty()) {
return List.of();
}

BibDatabaseContext databaseOnDisk = result.getDatabaseContext();
return DatabaseChangeList.compareAndGetChanges(database, databaseOnDisk, databaseChangeResolverFactory);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public void fileUpdated() {
synchronized (database) {
// File on disk has changed, thus look for notable changes and notify listeners in case there are such changes
ChangeScanner scanner = new ChangeScanner(database, dialogService, preferences, stateManager);
BackgroundTask.wrap(scanner::scanForChanges)
BackgroundTask.wrap(() -> scanner.scanForChanges())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed?

.onSuccess(changes -> {
if (!changes.isEmpty()) {
listeners.forEach(listener -> listener.databaseChanged(changes));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ public class ExternalChangesResolverViewModel extends AbstractViewModel {
private final BooleanBinding canAskUserToResolveChange;

public ExternalChangesResolverViewModel(@NonNull List<DatabaseChange> externalChanges) {
assert !externalChanges.isEmpty();

this.visibleChanges.addAll(externalChanges);
this.changes.addAll(externalChanges);

areAllChangesResolved = Bindings.createBooleanBinding(visibleChanges::isEmpty, visibleChanges);
areAllChangesAccepted = Bindings.createBooleanBinding(() -> changes.stream().allMatch(DatabaseChange::isAccepted));
areAllChangesDenied = Bindings.createBooleanBinding(() -> changes.stream().noneMatch(DatabaseChange::isAccepted));
if (externalChanges.isEmpty()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there are no external changes it does not really make sense to show the dialog...

areAllChangesResolved = Bindings.createBooleanBinding(() -> false);
areAllChangesAccepted = Bindings.createBooleanBinding(() -> false);
areAllChangesDenied = Bindings.createBooleanBinding(() -> false);
} else {
areAllChangesResolved = Bindings.createBooleanBinding(visibleChanges::isEmpty, visibleChanges);
areAllChangesAccepted = Bindings.createBooleanBinding(() -> changes.stream().allMatch(DatabaseChange::isAccepted));
areAllChangesDenied = Bindings.createBooleanBinding(() -> changes.stream().noneMatch(DatabaseChange::isAccepted));
}
canAskUserToResolveChange = Bindings.createBooleanBinding(() -> selectedChange.isNotNull().get() && selectedChange.get().getExternalChangeResolver().isPresent(), selectedChange);
}

Expand Down
110 changes: 110 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/collab/MergeLibraryAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.jabref.gui.collab;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
import org.jabref.gui.LibraryTabContainer;
import org.jabref.gui.StateManager;
import org.jabref.gui.actions.ActionHelper;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.undo.CountingUndoManager;
import org.jabref.gui.undo.NamedCompoundEdit;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.gui.util.FileFilterConverter;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.StandardFileType;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MergeLibraryAction extends SimpleCommand {
private static final Logger LOGGER = LoggerFactory.getLogger(MergeLibraryAction.class);

private final DialogService dialogService;
private final StateManager stateManager;
private final GuiPreferences preferences;
private final TaskExecutor taskExecutor;
private final CountingUndoManager undoManager;
private final LibraryTabContainer libraryTabContainer;

public MergeLibraryAction(DialogService dialogService,
StateManager stateManager,
GuiPreferences preferences,
TaskExecutor taskExecutor,
CountingUndoManager undoManager,
LibraryTabContainer libraryTabContainer) {
this.dialogService = dialogService;
this.stateManager = stateManager;
this.preferences = preferences;
this.taskExecutor = taskExecutor;
this.undoManager = undoManager;
this.libraryTabContainer = libraryTabContainer;

this.executable.bind(ActionHelper.needsDatabase(stateManager));
}

@Override
public void execute() {
stateManager.getActiveDatabase().ifPresent(activeDatabase -> {
Path initialDirectory = activeDatabase.getDatabasePath()
.map(Path::getParent)
.orElse(preferences.getImporterPreferences().getImportWorkingDirectory());

FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
.addExtensionFilter(FileFilterConverter.toExtensionFilter(Localization.lang("BibTeX"), StandardFileType.BIBTEX_DB))
.withDefaultExtension(StandardFileType.BIBTEX_DB)
.withInitialDirectory(initialDirectory)
.build();

dialogService.showFileOpenDialog(fileDialogConfiguration).ifPresent(mergeFile -> {
if (!Files.exists(mergeFile)) {
dialogService.showErrorDialogAndWait(
Localization.lang("Merge"),
Localization.lang("File %0 not found.", mergeFile.getFileName().toString()));
return;
}

ChangeScanner changeScanner = new ChangeScanner(activeDatabase, dialogService, preferences, stateManager);
BackgroundTask.wrap(() -> changeScanner.getDatabaseChanges(mergeFile))
.onSuccess(changes -> showMergeDialog(activeDatabase, changes))
.onFailure(exception -> {
LOGGER.warn("Error while reading merge file {}", mergeFile, exception);
dialogService.showErrorDialogAndWait(
Localization.lang("Merge"),
Localization.lang("Could not read merge file."));
})
.executeWith(taskExecutor);
});
});
}

private void showMergeDialog(BibDatabaseContext activeDatabase, List<DatabaseChange> changes) {
DatabaseChangesResolverDialog databaseChangesResolverDialog = new DatabaseChangesResolverDialog(
changes,
activeDatabase,
Localization.lang("External Changes Resolver"));
dialogService.showCustomDialogAndWait(databaseChangesResolverDialog);

NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merged external changes"));
changes.stream()
.filter(DatabaseChange::isAccepted)
.forEach(change -> change.applyChange(compoundEdit));
compoundEdit.end();

if (compoundEdit.hasEdits()) {
undoManager.addEdit(compoundEdit);

libraryTabContainer.getLibraryTabs().stream()
.filter(tab -> tab.getBibDatabaseContext().equals(activeDatabase))
.findFirst()
.ifPresent(LibraryTab::markBaseChanged);
}
}
}
3 changes: 3 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.jabref.gui.citationkeypattern.GenerateCitationKeyAction;
import org.jabref.gui.cleanup.CleanupAction;
import org.jabref.gui.clipboard.ClipBoardManager;
import org.jabref.gui.collab.MergeLibraryAction;
import org.jabref.gui.consistency.ConsistencyCheckAction;
import org.jabref.gui.copyfiles.CopyFilesAction;
import org.jabref.gui.documentviewer.ShowDocumentViewerAction;
Expand Down Expand Up @@ -179,6 +180,8 @@ private void createMenu() {

new SeparatorMenuItem(),

factory.createMenuItem(StandardActions.MERGE_LIBRARY, new MergeLibraryAction(dialogService, stateManager, preferences, taskExecutor, undoManager, frame)),

factory.createSubMenu(StandardActions.IMPORT,
factory.createMenuItem(StandardActions.IMPORT_INTO_CURRENT_LIBRARY, new ImportCommand(frame, ImportCommand.ImportMethod.TO_EXISTING, preferences, stateManager, fileUpdateMonitor, taskExecutor, dialogService)),
factory.createMenuItem(StandardActions.IMPORT_INTO_NEW_LIBRARY, new ImportCommand(frame, ImportCommand.ImportMethod.AS_NEW, preferences, stateManager, fileUpdateMonitor, taskExecutor, dialogService))),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.jabref.gui.collab;

import java.util.List;

import org.jabref.gui.collab.entryadd.EntryAdd;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;

class ExternalChangesResolverViewModelTest {

@Test
void emptyChangesShouldNotBeResolved() {
ExternalChangesResolverViewModel viewModel = new ExternalChangesResolverViewModel(List.of());

assertFalse(viewModel.areAllChangesResolved());
assertFalse(viewModel.areAllChangesAccepted());
assertFalse(viewModel.areAllChangesDenied());
}

@Test
void nonEmptyChangesShouldBeUnresolvedInitially() {
BibEntry entry = new BibEntry().withCitationKey("Key");
DatabaseChange change = new EntryAdd(entry, new BibDatabaseContext(), null);
ExternalChangesResolverViewModel viewModel = new ExternalChangesResolverViewModel(List.of(change));

assertFalse(viewModel.areAllChangesResolved());
}
}
6 changes: 6 additions & 0 deletions jablib/src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,12 @@ Please\ enter\ a\ valid\ file\ path.=Please enter a valid file path.
Overwrite\ file=Overwrite file
Unable\ to\ write\ to\ %0.=Unable to write to %0.

Merge...=Merge...
Merge=Merge
Could\ not\ read\ merge\ file.=Could not read merge file.
BibTeX=BibTeX
File\ %0\ not\ found.=File %0 not found.

Move\ file=Move file
Move\ file\ to\ file\ directory\ and\ rename\ file=Move file to file directory and rename file
Rename\ file=Rename file
Expand Down
Loading