Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
400c6b0
Add cleanup dialog tabs with individual tab preferences
alexsukhin Sep 10, 2025
dd2f74c
Fixed indentation and added commenting
alexsukhin Sep 10, 2025
033201c
Fix Trag-bot review issues
alexsukhin Sep 11, 2025
14fde53
Fix Trag-bot review issues
alexsukhin Sep 11, 2025
9c44aad
Merge branch 'main' into 13819-add-dialog-tabs
alexsukhin Sep 11, 2025
0c5939e
Fix Trag-bot review issues
alexsukhin Sep 11, 2025
757514e
Avoid nested Optionals, returning Optional<CleanupPreferences> directly
alexsukhin Sep 11, 2025
a60f696
Refactor CleanupPreferences by keeping one assertion per test
alexsukhin Sep 11, 2025
43c981b
Converted tests to assertEquals
alexsukhin Sep 11, 2025
56b6164
Maintain consistent naming conventions
alexsukhin Sep 11, 2025
4f2ed12
Returns CleanupPreferences directly since value is always present
alexsukhin Sep 11, 2025
1d2f991
Initial review refactor draft
alexsukhin Sep 14, 2025
c8c3d42
Merge branch 'main' into 13819-add-dialog-tabs
alexsukhin Sep 14, 2025
7276eb0
fix import error!
alexsukhin Sep 14, 2025
9ed570f
Reformat codebase (more carefully) (#13885)
subhramit Sep 13, 2025
55c02c2
fix import error & merge
alexsukhin Sep 14, 2025
b0c4f6a
Apply OpenRewrite Cleanup
alexsukhin Sep 14, 2025
651f5ef
Refactor Cleanup Tabs
alexsukhin Sep 16, 2025
d280c21
Merge remote-tracking branch 'upstream/main' into 13819-add-dialog-tabs
alexsukhin Sep 16, 2025
161e36f
Fix getDisplayName method
alexsukhin Sep 16, 2025
2dfb7a7
Fix formatting
alexsukhin Sep 16, 2025
84d7716
Trag-bot review and fix en properties
alexsukhin Sep 16, 2025
442a623
fix indentation plssss
alexsukhin Sep 17, 2025
6606e59
format properly and change to observablelist
alexsukhin Sep 17, 2025
f90923a
fix formatting entriestoprocess (please)
alexsukhin Sep 17, 2025
9073361
Updated names and changed optional dependencies back to nullable
alexsukhin Sep 19, 2025
a8d371e
Merge branch 'main' into 13819-add-dialog-tabs
alexsukhin Sep 19, 2025
074242e
Merge branch 'main' into 13819-add-dialog-tabs
Siedlerchr Sep 23, 2025
8714141
Refactored panels to use separate ViewModels
alexsukhin Sep 25, 2025
f35319f
Moved ALL_JOBS to respective ViewModels, small naming changes
alexsukhin Sep 25, 2025
0ee7d35
Merge upstream/main, fix submodules
alexsukhin Sep 25, 2025
b24af6b
Replaced requireNotNull to @NotNull following https://github.com/JabR…
alexsukhin Sep 25, 2025
b7f4594
Merge branch 'main' into 13819-add-dialog-tabs
alexsukhin Oct 1, 2025
9c9f202
Merge branch 'main' into 13819-add-dialog-tabs
alexsukhin Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We improved the event viewer for debugging [#13783](https://github.com/JabRef/jabref/pull/13783).
- We improved "REDACTED" replacement of API key value in web fetcher search URL [#13796](https://github.com/JabRef/jabref/issues/13796)
- When the pin "Keep dialog always on top" in the global search dialog is selected, the search window stays open when double-clicking on an entry. [#13840](https://github.com/JabRef/jabref/issues/13840)
- We separated the "Clean up entries" dialog into three tabs for clarity [#13819](https://github.com/JabRef/jabref/issues/13819)
- We improved the way we check for matching curly braces in BibTeX fields and made error messages easier to understand. [#12605](https://github.com/JabRef/jabref/issues/12605)

### Fixed
Expand Down
138 changes: 7 additions & 131 deletions jabgui/src/main/java/org/jabref/gui/cleanup/CleanupAction.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
package org.jabref.gui.cleanup;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.swing.undo.UndoManager;

import javafx.application.Platform;

import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
import org.jabref.gui.StateManager;
import org.jabref.gui.actions.ActionHelper;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.undo.NamedCompoundEdit;
import org.jabref.gui.undo.UndoableFieldChange;
import org.jabref.logic.JabRefException;
import org.jabref.logic.cleanup.CleanupPreferences;
import org.jabref.logic.cleanup.CleanupWorker;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.FieldChange;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;

public class CleanupAction extends SimpleCommand {

Expand All @@ -36,10 +20,6 @@ public class CleanupAction extends SimpleCommand {
private final StateManager stateManager;
private final TaskExecutor taskExecutor;
private final UndoManager undoManager;
private final List<JabRefException> failures;

private boolean isCanceled;
private int modifiedEntriesCount;

public CleanupAction(Supplier<LibraryTab> tabSupplier,
CliPreferences preferences,
Expand All @@ -53,7 +33,6 @@ public CleanupAction(Supplier<LibraryTab> tabSupplier,
this.stateManager = stateManager;
this.taskExecutor = taskExecutor;
this.undoManager = undoManager;
this.failures = new ArrayList<>();

this.executable.bind(ActionHelper.needsEntriesSelected(stateManager));
}
Expand All @@ -64,119 +43,16 @@ public void execute() {
return;
}

if (stateManager.getSelectedEntries().isEmpty()) { // None selected. Inform the user to select entries first.
dialogService.showInformationDialogAndWait(Localization.lang("Cleanup entry"), Localization.lang("First select entries to clean up."));
return;
}

isCanceled = false;
modifiedEntriesCount = 0;

CleanupDialog cleanupDialog = new CleanupDialog(
stateManager.getActiveDatabase().get(),
preferences.getCleanupPreferences(),
preferences.getFilePreferences()
);

Optional<CleanupPreferences> chosenPreset = dialogService.showCustomDialogAndWait(cleanupDialog);

chosenPreset.ifPresent(preset -> {
if (preset.isActive(CleanupPreferences.CleanupStep.RENAME_PDF) && preferences.getAutoLinkPreferences().shouldAskAutoNamingPdfs()) {
boolean confirmed = dialogService.showConfirmationDialogWithOptOutAndWait(Localization.lang("Autogenerate PDF Names"),
Localization.lang("Auto-generating PDF-Names does not support undo. Continue?"),
Localization.lang("Autogenerate PDF Names"),
Localization.lang("Cancel"),
Localization.lang("Do not ask again"),
optOut -> preferences.getAutoLinkPreferences().setAskAutoNamingPdfs(!optOut));
if (!confirmed) {
isCanceled = true;
return;
}
}

preferences.getCleanupPreferences().setActiveJobs(preset.getActiveJobs());
preferences.getCleanupPreferences().setFieldFormatterCleanups(preset.getFieldFormatterCleanups());

BackgroundTask.wrap(() -> cleanup(stateManager.getActiveDatabase().get(), preset))
.onSuccess(result -> showResults())
.onFailure(dialogService::showErrorDialogAndWait)
.executeWith(taskExecutor);
});
}

/**
* Runs the cleanup on the entry and records the change.
*
* @return true iff entry was modified
*/
private boolean doCleanup(BibDatabaseContext databaseContext, CleanupPreferences preset, BibEntry entry, NamedCompoundEdit compoundEdit) {
// Create and run cleaner
CleanupWorker cleaner = new CleanupWorker(
databaseContext,
preferences.getFilePreferences(),
preferences.getTimestampPreferences()
preferences,
dialogService,
stateManager,
undoManager,
tabSupplier,
taskExecutor
);

List<FieldChange> changes = cleaner.cleanup(preset, entry);

// Register undo action
for (FieldChange change : changes) {
compoundEdit.addEdit(new UndoableFieldChange(change));
}

failures.addAll(cleaner.getFailures());

return !changes.isEmpty();
}

private void showResults() {
if (isCanceled) {
return;
}

if (modifiedEntriesCount > 0) {
tabSupplier.get().markBaseChanged();
}

if (modifiedEntriesCount == 0) {
dialogService.notify(Localization.lang("No entry needed a clean up"));
} else if (modifiedEntriesCount == 1) {
dialogService.notify(Localization.lang("One entry needed a clean up"));
} else {
dialogService.notify(Localization.lang("%0 entries needed a clean up", Integer.toString(modifiedEntriesCount)));
}
}

private void cleanup(BibDatabaseContext databaseContext, CleanupPreferences cleanupPreferences) {
this.failures.clear();

// undo granularity is on set of all entries
NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Clean up entries"));

for (BibEntry entry : List.copyOf(stateManager.getSelectedEntries())) {
if (doCleanup(databaseContext, cleanupPreferences, entry, compoundEdit)) {
modifiedEntriesCount++;
}
}

compoundEdit.end();

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

if (!failures.isEmpty()) {
showFailures(failures);
}
}

private void showFailures(List<JabRefException> failures) {
String message = failures.stream()
.map(exception -> "- " + exception.getLocalizedMessage())
.collect(Collectors.joining("\n"));

Platform.runLater(() ->
dialogService.showErrorDialogAndWait(Localization.lang("File Move Errors"), message)
);
dialogService.showCustomDialogAndWait(cleanupDialog);
}
}
98 changes: 75 additions & 23 deletions jabgui/src/main/java/org/jabref/gui/cleanup/CleanupDialog.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,87 @@
package org.jabref.gui.cleanup;

import javafx.scene.control.ButtonType;
import javafx.scene.control.ScrollPane;
import java.util.List;
import java.util.function.Supplier;

import javax.swing.undo.UndoManager;

import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;

import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
import org.jabref.gui.StateManager;
import org.jabref.gui.util.BaseDialog;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.cleanup.CleanupPreferences;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;

import com.airhacks.afterburner.views.ViewLoader;

public class CleanupDialog extends BaseDialog<Void> {

@FXML private TabPane tabPane;

private final CleanupDialogViewModel viewModel;

// Constructor for multiple-entry cleanup
public CleanupDialog(BibDatabaseContext databaseContext,
CliPreferences preferences,
DialogService dialogService,
StateManager stateManager,
UndoManager undoManager,
Supplier<LibraryTab> tabSupplier,
TaskExecutor taskExecutor) {

this.viewModel = new CleanupDialogViewModel(
databaseContext, preferences, dialogService,
stateManager, undoManager, tabSupplier, taskExecutor
);

init(databaseContext, preferences);
}

public class CleanupDialog extends BaseDialog<CleanupPreferences> {
public CleanupDialog(BibDatabaseContext databaseContext, CleanupPreferences initialPreset, FilePreferences filePreferences) {
// Constructor for single-entry cleanup
public CleanupDialog(BibEntry targetEntry,
BibDatabaseContext databaseContext,
CliPreferences preferences,
DialogService dialogService,
StateManager stateManager,
UndoManager undoManager) {

this.viewModel = new CleanupDialogViewModel(
databaseContext, preferences, dialogService,
stateManager, undoManager, null, null
);

viewModel.setTargetEntries(List.of(targetEntry));

init(databaseContext, preferences);
}

private void init(BibDatabaseContext databaseContext, CliPreferences preferences) {
setTitle(Localization.lang("Clean up entries"));
getDialogPane().setPrefSize(600, 650);
getDialogPane().getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);

CleanupPresetPanel presetPanel = new CleanupPresetPanel(databaseContext, initialPreset, filePreferences);

// placing the content of the presetPanel in a scroll pane
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
scrollPane.setContent(presetPanel);

getDialogPane().setContent(scrollPane);
setResultConverter(button -> {
if (button == ButtonType.OK) {
return presetPanel.getCleanupPreset();
} else {
return null;
}
});

ViewLoader.view(this)
.load()
.setAsDialogPane(this);

CleanupPreferences initialPreset = preferences.getCleanupPreferences();
FilePreferences filePreferences = preferences.getFilePreferences();

CleanupSingleFieldPanel singleFieldPanel = new CleanupSingleFieldPanel(initialPreset, viewModel);
CleanupFileRelatedPanel fileRelatedPanel = new CleanupFileRelatedPanel(databaseContext, initialPreset, filePreferences, viewModel);
CleanupMultiFieldPanel multiFieldPanel = new CleanupMultiFieldPanel(initialPreset, viewModel);

tabPane.getTabs().setAll(
new Tab(Localization.lang("Single field"), singleFieldPanel),
new Tab(Localization.lang("File-related"), fileRelatedPanel),
new Tab(Localization.lang("Multi-field"), multiFieldPanel)
);
}
}
Loading