diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile new file mode 100644 index 00000000000..007108a0f49 --- /dev/null +++ b/.gitpod.Dockerfile @@ -0,0 +1,8 @@ +# See https://www.gitpod.io/docs/java-in-gitpod/ for a full documentation of Java in GitPod + +FROM gitpod/workspace-full + +# All available versions can be listed using sdk ls java +# More information about SDKMAN available at https://github.com/sdkman/sdkman-cli#sdkman-cli +RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \ + && sdk install java 18.0.1.1-open" diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000000..a0aa262ea90 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,16 @@ +image: + file: .gitpod.Dockerfile +tasks: + - init: ./gradlew assemble +jetbrains: + intellij: + plugins: + - CheckStyle-IDEA + - zielu.gittoolbox +vscode: + extensions: + - redhat.java + - richardwillis.vscode-gradle + - vscjava.vscode-java-debug + - kiteco.kite + - DavidAnson.vscode-markdownlint diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fb2cba24c..4bad6edb909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - The Medline/Pubmed search now also supports the [default fields and operators for searching](https://docs.jabref.org/collect/import-using-online-bibliographic-database#search-syntax). [forum#3554](https://discourse.jabref.org/t/native-pubmed-search/3354) - We improved group expansion arrow that prevent it from activating group when expanding or collapsing. [#7982](https://github.com/JabRef/jabref/issues/7982), [#3176](https://github.com/JabRef/jabref/issues/3176) - When configured SSL certificates changed, JabRef warns the user to restart to apply the configuration. +- We improved the appearances and logic of the "Manage field names & content" dialog, and renamed it to "Automatic field editor". [#6536](https://github.com/JabRef/jabref/issues/6536) - We fixed an issue that caused JabRef to sometimes open multiple instances when "Remote Operation" is enabled. [#8653](https://github.com/JabRef/jabref/issues/8653) ### Fixed diff --git a/README.md b/README.md index 78a90d2785d..08b68588cfb 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ An explanation of donation possibilities and usage of donations is available at [![OpenHub](https://www.openhub.net/p/jabref/widgets/project_thin_badge.gif)](https://www.openhub.net/p/jabref) [![Deployment Status](https://github.com/JabRef/jabref/workflows/Deployment/badge.svg)](https://github.com/JabRef/jabref/actions?query=workflow%3ADeployment) [![Test Status](https://github.com/JabRef/jabref/workflows/Tests/badge.svg)](https://github.com/JabRef/jabref/actions?query=workflow%3ATests) -[![codecov.io](https://codecov.io/github/JabRef/jabref/coverage.svg?branch=master)](https://codecov.io/github/JabRef/jabref?branch=master) +[![codecov.io](https://codecov.io/github/JabRef/jabref/coverage.svg?branch=master)](https://codecov.io/github/JabRef/jabref?branch=main) Want to be part of a free and open-source project that tens of thousands of scientists use every day? Check out the ways you can contribute, below: diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index efd992dd688..2bbbf8f4412 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -67,9 +67,9 @@ import org.jabref.gui.edit.CopyMoreAction; import org.jabref.gui.edit.EditAction; import org.jabref.gui.edit.ManageKeywordsAction; -import org.jabref.gui.edit.MassSetFieldsAction; import org.jabref.gui.edit.OpenBrowserAction; import org.jabref.gui.edit.ReplaceStringAction; +import org.jabref.gui.edit.automaticfiededitor.AutomaticFieldEditorAction; import org.jabref.gui.entryeditor.OpenEntryEditorAction; import org.jabref.gui.entryeditor.PreviewSwitchAction; import org.jabref.gui.exporter.ExportCommand; @@ -750,8 +750,7 @@ private MenuBar createMenu() { new SeparatorMenuItem(), factory.createMenuItem(StandardActions.MANAGE_KEYWORDS, new ManageKeywordsAction(stateManager)), - factory.createMenuItem(StandardActions.MASS_SET_FIELDS, new MassSetFieldsAction(stateManager, dialogService, undoManager))); - + factory.createMenuItem(StandardActions.AUTOMATIC_FIELD_EDITOR, new AutomaticFieldEditorAction(stateManager, dialogService))); SeparatorMenuItem specialFieldsSeparator = new SeparatorMenuItem(); specialFieldsSeparator.visibleProperty().bind(prefs.getSpecialFieldsPreferences().specialFieldsEnabledProperty()); diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index aec2323eb26..b673a4e516b 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -78,6 +78,8 @@ public enum StandardActions implements Action { REPLACE_ALL(Localization.lang("Find and replace"), KeyBinding.REPLACE_STRING), MANAGE_KEYWORDS(Localization.lang("Manage keywords")), MASS_SET_FIELDS(Localization.lang("Manage field names & content")), + + AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")), TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE), TOOGLE_OO(Localization.lang("OpenOffice/LibreOffice"), IconTheme.JabRefIcons.FILE_OPENOFFICE, KeyBinding.OPEN_OPEN_OFFICE_LIBRE_OFFICE_CONNECTION), TOGGLE_WEB_SEARCH(Localization.lang("Web search"), Localization.lang("Toggle web search interface"), IconTheme.JabRefIcons.WWW, KeyBinding.WEB_SEARCH), diff --git a/src/main/java/org/jabref/gui/edit/MassSetFieldsAction.java b/src/main/java/org/jabref/gui/edit/MassSetFieldsAction.java deleted file mode 100644 index 392f90536fc..00000000000 --- a/src/main/java/org/jabref/gui/edit/MassSetFieldsAction.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.jabref.gui.edit; - -import javax.swing.undo.UndoManager; - -import org.jabref.gui.DialogService; -import org.jabref.gui.StateManager; -import org.jabref.gui.actions.SimpleCommand; -import org.jabref.model.database.BibDatabaseContext; - -import static org.jabref.gui.actions.ActionHelper.needsDatabase; -import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected; - -/** - * An Action for launching mass field. - *

- * Functionality: - *

- */ -public class MassSetFieldsAction extends SimpleCommand { - - private final StateManager stateManager; - private DialogService dialogService; - private UndoManager undoManager; - - public MassSetFieldsAction(StateManager stateManager, DialogService dialogService, UndoManager undoManager) { - this.stateManager = stateManager; - this.dialogService = dialogService; - this.undoManager = undoManager; - - this.executable.bind(needsDatabase(stateManager).and(needsEntriesSelected(stateManager))); - } - - @Override - public void execute() { - BibDatabaseContext database = stateManager.getActiveDatabase().orElseThrow(() -> new NullPointerException("Database null")); - dialogService.showCustomDialogAndWait(new MassSetFieldsDialog(stateManager.getSelectedEntries(), database, dialogService, undoManager)); - } -} diff --git a/src/main/java/org/jabref/gui/edit/MassSetFieldsDialog.java b/src/main/java/org/jabref/gui/edit/MassSetFieldsDialog.java deleted file mode 100644 index 20d5aa1f538..00000000000 --- a/src/main/java/org/jabref/gui/edit/MassSetFieldsDialog.java +++ /dev/null @@ -1,245 +0,0 @@ -package org.jabref.gui.edit; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import javax.swing.undo.UndoManager; -import javax.swing.undo.UndoableEdit; - -import javafx.application.Platform; -import javafx.geometry.Insets; -import javafx.scene.control.ButtonType; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.RadioButton; -import javafx.scene.control.TextField; -import javafx.scene.control.ToggleGroup; -import javafx.scene.control.Tooltip; -import javafx.scene.layout.GridPane; - -import org.jabref.gui.DialogService; -import org.jabref.gui.undo.NamedCompound; -import org.jabref.gui.undo.UndoableFieldChange; -import org.jabref.gui.util.BaseDialog; -import org.jabref.gui.util.IconValidationDecorator; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldFactory; -import org.jabref.model.strings.StringUtil; - -import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator; -import de.saxsys.mvvmfx.utils.validation.Severity; -import de.saxsys.mvvmfx.utils.validation.ValidationMessage; -import de.saxsys.mvvmfx.utils.validation.Validator; -import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer; - -public class MassSetFieldsDialog extends BaseDialog { - - private final List entries; - private final BibDatabaseContext database; - private final DialogService dialogService; - - private RadioButton clearRadioButton; - private RadioButton setRadioButton; - private RadioButton appendRadioButton; - private RadioButton renameRadioButton; - private ComboBox fieldComboBox; - private TextField setTextField; - private TextField appendTextField; - private TextField renameTextField; - private CheckBox overwriteCheckBox; - private UndoManager undoManager; - - MassSetFieldsDialog(List entries, BibDatabaseContext database, DialogService dialogService, UndoManager undoManager) { - this.entries = entries; - this.database = database; - this.dialogService = dialogService; - this.undoManager = undoManager; - - init(); - this.setTitle(Localization.lang("Manage field names & content")); - this.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - this.setResultConverter(button -> { - if (button == ButtonType.OK) { - performEdits(); - } - return null; - }); - } - - /** - * Append a given value to a given field for all entries in a Collection. This method DOES NOT update any UndoManager, - * but returns a relevant CompoundEdit that should be registered by the caller. - * - * @param entries The entries to process the operation for. - * @param field The name of the field to append to. - * @param textToAppend The value to set. A null in this case will simply preserve the current field state. - * @return A CompoundEdit for the entire operation. - */ - private static UndoableEdit massAppendField(Collection entries, Field field, String textToAppend) { - String newValue = ""; - - if (textToAppend != null) { - newValue = textToAppend; - } - - NamedCompound compoundEdit = new NamedCompound(Localization.lang("Append field")); - for (BibEntry entry : entries) { - Optional oldValue = entry.getField(field); - entry.setField(field, oldValue.orElse("") + newValue); - compoundEdit.addEdit(new UndoableFieldChange(entry, field, oldValue.orElse(null), newValue)); - } - compoundEdit.end(); - return compoundEdit; - } - - /** - * Move contents from one field to another for a Collection of entries. - * - * @param entries The entries to do this operation for. - * @param field The field to move contents from. - * @param newField The field to move contents into. - * @param overwriteValues If true, overwrites any existing values in the new field. If false, makes no change for - * entries with existing value in the new field. - * @return A CompoundEdit for the entire operation. - */ - private static UndoableEdit massRenameField(Collection entries, Field field, Field newField, - boolean overwriteValues) { - NamedCompound compoundEdit = new NamedCompound(Localization.lang("Rename field")); - for (BibEntry entry : entries) { - Optional valToMove = entry.getField(field); - // If there is no value, do nothing: - if ((!valToMove.isPresent()) || valToMove.get().isEmpty()) { - continue; - } - // If we are not allowed to overwrite values, check if there is a - // non-empty value already for this entry for the new field: - Optional valInNewField = entry.getField(newField); - if (!overwriteValues && (valInNewField.isPresent()) && !valInNewField.get().isEmpty()) { - continue; - } - - entry.setField(newField, valToMove.get()); - compoundEdit.addEdit(new UndoableFieldChange(entry, newField, valInNewField.orElse(null), valToMove.get())); - entry.clearField(field); - compoundEdit.addEdit(new UndoableFieldChange(entry, field, valToMove.get(), null)); - } - compoundEdit.end(); - return compoundEdit; - } - - /** - * Set a given field to a given value for all entries in a Collection. This method DOES NOT update any UndoManager, - * but returns a relevant CompoundEdit that should be registered by the caller. - * - * @param entries The entries to set the field for. - * @param field The name of the field to set. - * @param textToSet The value to set. This value can be null, indicating that the field should be cleared. - * @param overwriteValues Indicate whether the value should be set even if an entry already has the field set. - * @return A CompoundEdit for the entire operation. - */ - private static UndoableEdit massSetField(Collection entries, Field field, String textToSet, - boolean overwriteValues) { - NamedCompound compoundEdit = new NamedCompound(Localization.lang("Set field")); - for (BibEntry entry : entries) { - Optional oldValue = entry.getField(field); - // If we are not allowed to overwrite values, check if there is a - // nonempty - // value already for this entry: - if (!overwriteValues && (oldValue.isPresent()) && !oldValue.get().isEmpty()) { - continue; - } - if (textToSet == null) { - entry.clearField(field); - } else { - entry.setField(field, textToSet); - } - compoundEdit.addEdit(new UndoableFieldChange(entry, field, oldValue.orElse(null), textToSet)); - } - compoundEdit.end(); - return compoundEdit; - } - - private void init() { - fieldComboBox = new ComboBox<>(); - fieldComboBox.setEditable(true); - fieldComboBox.getItems().addAll(database.getDatabase().getAllVisibleFields().stream().map(Field::getName).collect(Collectors.toSet())); - - ToggleGroup toggleGroup = new ToggleGroup(); - clearRadioButton = new RadioButton(Localization.lang("Clear fields")); - clearRadioButton.setToggleGroup(toggleGroup); - renameRadioButton = new RadioButton(Localization.lang("Rename field to") + ":"); - renameRadioButton.setTooltip(new Tooltip(Localization.lang("Move contents of a field into a field with a different name"))); - renameRadioButton.setToggleGroup(toggleGroup); - setRadioButton = new RadioButton(Localization.lang("Set fields") + ":"); - setRadioButton.setToggleGroup(toggleGroup); - appendRadioButton = new RadioButton(Localization.lang("Append to fields") + ":"); - appendRadioButton.setToggleGroup(toggleGroup); - - setTextField = new TextField(); - setTextField.disableProperty().bind(setRadioButton.selectedProperty().not()); - appendTextField = new TextField(); - appendTextField.disableProperty().bind(appendRadioButton.selectedProperty().not()); - renameTextField = new TextField(); - renameTextField.disableProperty().bind(renameRadioButton.selectedProperty().not()); - - overwriteCheckBox = new CheckBox(Localization.lang("Overwrite existing field values")); - - GridPane main = new GridPane(); - main.add(new Label(Localization.lang("Field name")), 0, 0); - main.add(fieldComboBox, 1, 0); - main.add(setRadioButton, 0, 2); - main.add(setTextField, 1, 2); - main.add(appendRadioButton, 0, 3); - main.add(appendTextField, 1, 3); - main.add(renameRadioButton, 0, 4); - main.add(renameTextField, 1, 4); - main.add(clearRadioButton, 0, 5); - main.add(overwriteCheckBox, 0, 7); - - main.setPadding(new Insets(15, 15, 0, 15)); - main.setGridLinesVisible(false); - main.setVgap(4); - main.setHgap(10); - getDialogPane().setContent(main); - - Validator fieldNameValidator = new FunctionBasedValidator<>( - fieldComboBox.valueProperty(), - StringUtil::isNotBlank, - new ValidationMessage(Severity.ERROR, Localization.lang("You must enter at least one field name")) - ); - Platform.runLater(() -> { - // Need to run this async, otherwise the dialog does not work - ControlsFxVisualizer visualizer = new ControlsFxVisualizer(); - visualizer.setDecoration(new IconValidationDecorator()); - visualizer.initVisualization(fieldNameValidator.getValidationStatus(), fieldComboBox, true); - }); - } - - private void performEdits() { - String toSet = setTextField.getText(); - if (toSet.isEmpty()) { - toSet = null; - } - - Field field = FieldFactory.parseField(fieldComboBox.getValue()); - - NamedCompound compoundEdit = new NamedCompound(Localization.lang("Set field")); - if (renameRadioButton.isSelected()) { - compoundEdit.addEdit(massRenameField(entries, field, FieldFactory.parseField(renameTextField.getText()), overwriteCheckBox.isSelected())); - } else if (appendRadioButton.isSelected()) { - compoundEdit.addEdit(massAppendField(entries, field, appendTextField.getText())); - } else { - compoundEdit.addEdit(massSetField(entries, field, - setRadioButton.isSelected() ? toSet : null, - overwriteCheckBox.isSelected())); - } - compoundEdit.end(); - undoManager.addEdit(compoundEdit); - } -} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AbstractAutomaticFieldEditorTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AbstractAutomaticFieldEditorTabView.java new file mode 100644 index 00000000000..2954b013f68 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AbstractAutomaticFieldEditorTabView.java @@ -0,0 +1,11 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import javafx.scene.layout.AnchorPane; + +public abstract class AbstractAutomaticFieldEditorTabView extends AnchorPane implements AutomaticFieldEditorTab { + + @Override + public AnchorPane getContent() { + return this; + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorAction.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorAction.java new file mode 100644 index 00000000000..a56af9dfa78 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorAction.java @@ -0,0 +1,26 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; + +import static org.jabref.gui.actions.ActionHelper.needsDatabase; +import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected; + +public class AutomaticFieldEditorAction extends SimpleCommand { + private final StateManager stateManager; + private final DialogService dialogService; + + public AutomaticFieldEditorAction(StateManager stateManager, DialogService dialogService) { + this.stateManager = stateManager; + this.dialogService = dialogService; + + this.executable.bind(needsDatabase(stateManager).and(needsEntriesSelected(stateManager))); + } + + @Override + public void execute() { + dialogService.showCustomDialogAndWait(new AutomaticFieldEditorDialog(stateManager.getSelectedEntries(), + stateManager.getActiveDatabase().orElseThrow())); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.fxml new file mode 100644 index 00000000000..f79462369a1 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.fxml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.java new file mode 100644 index 00000000000..5ecbf06b275 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.java @@ -0,0 +1,73 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import java.util.List; + +import javax.swing.undo.UndoManager; + +import javafx.fxml.FXML; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; + +import org.jabref.gui.Globals; +import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.ControlHelper; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +import com.airhacks.afterburner.views.ViewLoader; + +public class AutomaticFieldEditorDialog extends BaseDialog { + @FXML public ButtonType saveButton; + @FXML public ButtonType cancelButton; + @FXML + private TabPane tabPane; + + private final UndoManager undoManager; + + private final BibDatabaseContext databaseContext; + private final List selectedEntries; + private AutomaticFieldEditorViewModel viewModel; + + public AutomaticFieldEditorDialog(List selectedEntries, BibDatabaseContext databaseContext) { + this.selectedEntries = selectedEntries; + this.databaseContext = databaseContext; + this.undoManager = Globals.undoManager; + + this.setTitle(Localization.lang("Automatic field editor")); + + ViewLoader.view(this) + .load() + .setAsDialogPane(this); + + ControlHelper.setAction(saveButton, getDialogPane(), event -> saveChangesAndCloseDialog()); + ControlHelper.setAction(cancelButton, getDialogPane(), event -> cancelChangesAndCloseDialog()); + + // This will prevent all dialog buttons from having the same size + // Read more: https://stackoverflow.com/questions/45866249/javafx-8-alert-different-button-sizes + getDialogPane().getButtonTypes().stream() + .map(getDialogPane()::lookupButton) + .forEach(btn-> ButtonBar.setButtonUniformSize(btn, false)); + } + + @FXML + public void initialize() { + viewModel = new AutomaticFieldEditorViewModel(selectedEntries, databaseContext, undoManager); + + for (AutomaticFieldEditorTab tabModel : viewModel.getFieldEditorTabs()) { + tabPane.getTabs().add(new Tab(tabModel.getTabName(), tabModel.getContent())); + } + } + + private void saveChangesAndCloseDialog() { + viewModel.saveChanges(); + close(); + } + + private void cancelChangesAndCloseDialog() { + viewModel.cancelChanges(); + close(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorTab.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorTab.java new file mode 100644 index 00000000000..7863422deff --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorTab.java @@ -0,0 +1,9 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import javafx.scene.layout.Pane; + +public interface AutomaticFieldEditorTab { + Pane getContent(); + + String getTabName(); +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorViewModel.java new file mode 100644 index 00000000000..0c334ddf6fa --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorViewModel.java @@ -0,0 +1,49 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import java.util.List; + +import javax.swing.undo.UndoManager; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.edit.automaticfiededitor.editfieldvalue.EditFieldValueTabView; +import org.jabref.gui.edit.automaticfiededitor.renamefield.RenameFieldTabView; +import org.jabref.gui.edit.automaticfiededitor.twofields.TwoFieldsTabView; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +public class AutomaticFieldEditorViewModel extends AbstractViewModel { + public static final String NAMED_COMPOUND_EDITS = "EDIT_FIELDS"; + private final ObservableList fieldEditorTabs = FXCollections.observableArrayList(); + private final NamedCompound dialogEdits = new NamedCompound(NAMED_COMPOUND_EDITS); + + private final BibDatabaseContext databaseContext; + private final UndoManager undoManager; + + public AutomaticFieldEditorViewModel(List selectedEntries, BibDatabaseContext databaseContext, UndoManager undoManager) { + fieldEditorTabs.addAll( + new EditFieldValueTabView(selectedEntries, databaseContext, dialogEdits), + new TwoFieldsTabView(selectedEntries, databaseContext, dialogEdits), + new RenameFieldTabView(selectedEntries, databaseContext, dialogEdits) + ); + this.databaseContext = databaseContext; + this.undoManager = undoManager; + } + + public ObservableList getFieldEditorTabs() { + return fieldEditorTabs; + } + + public void saveChanges() { + dialogEdits.end(); + undoManager.addEdit(dialogEdits); + } + + public void cancelChanges() { + dialogEdits.end(); + dialogEdits.undo(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/MoveFieldValueAction.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/MoveFieldValueAction.java new file mode 100644 index 00000000000..8863d17de78 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/MoveFieldValueAction.java @@ -0,0 +1,42 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import java.util.List; + +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.gui.undo.UndoableFieldChange; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.strings.StringUtil; + +public class MoveFieldValueAction extends SimpleCommand { + private final Field fromField; + private final Field toField; + private final List entries; + + private final NamedCompound edits; + + public MoveFieldValueAction(Field fromField, Field toField, List entries, NamedCompound edits) { + this.fromField = fromField; + this.toField = toField; + this.entries = entries; + this.edits = edits; + } + + @Override + public void execute() { + for (BibEntry entry : entries) { + String fromFieldValue = entry.getField(fromField).orElse(""); + String toFieldValue = entry.getField(toField).orElse(""); + + if (StringUtil.isNotBlank(fromFieldValue)) { + entry.setField(toField, fromFieldValue); + entry.setField(fromField, ""); + + edits.addEdit(new UndoableFieldChange(entry, fromField, fromFieldValue, null)); + edits.addEdit(new UndoableFieldChange(entry, toField, toFieldValue, fromFieldValue)); + } + } + edits.end(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTab.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTab.fxml new file mode 100644 index 00000000000..4cf8a44016e --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTab.fxml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTabView.java new file mode 100644 index 00000000000..3bd9ea57a09 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTabView.java @@ -0,0 +1,95 @@ +package org.jabref.gui.edit.automaticfiededitor.editfieldvalue; + +import java.util.Comparator; +import java.util.List; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.util.StringConverter; + +import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabView; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; + +import com.airhacks.afterburner.views.ViewLoader; + +public class EditFieldValueTabView extends AbstractAutomaticFieldEditorTabView { + public Button appendValueButton; + @FXML + private ComboBox fieldComboBox; + + @FXML + private TextField fieldValueTextField; + + @FXML + private CheckBox overwriteNonEmptyFieldsCheckBox; + + private final List selectedEntries; + private final BibDatabaseContext databaseContext; + + private EditFieldValueViewModel viewModel; + + private final NamedCompound dialogEdits; + + public EditFieldValueTabView(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { + this.selectedEntries = selectedEntries; + this.databaseContext = databaseContext; + this.dialogEdits = dialogEdits; + + ViewLoader.view(this) + .root(this) + .load(); + } + + @FXML + public void initialize() { + viewModel = new EditFieldValueViewModel(databaseContext, selectedEntries, dialogEdits); + fieldComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Field field) { + return field.getName(); + } + + @Override + public Field fromString(String name) { + return FieldFactory.parseField(name); + } + }); + fieldComboBox.getItems().addAll(viewModel.getAllFields().sorted(Comparator.comparing(Field::getName))); + fieldComboBox.getSelectionModel().selectFirst(); + viewModel.selectedFieldProperty().bindBidirectional(fieldComboBox.valueProperty()); + + viewModel.fieldValueProperty().bindBidirectional(fieldValueTextField.textProperty()); + + viewModel.overwriteNonEmptyFieldsProperty().bindBidirectional(overwriteNonEmptyFieldsCheckBox.selectedProperty()); + + appendValueButton.disableProperty().bind(overwriteNonEmptyFieldsCheckBox.selectedProperty().not()); + } + + @Override + public String getTabName() { + return Localization.lang("Edit field value"); + } + + @FXML + void appendToFieldValue() { + viewModel.appendToFieldValue(); + } + + @FXML + void clearField() { + viewModel.clearSelectedField(); + } + + @FXML + void setFieldValue() { + viewModel.setFieldValue(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueViewModel.java new file mode 100644 index 00000000000..d11928ebc53 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueViewModel.java @@ -0,0 +1,135 @@ +package org.jabref.gui.edit.automaticfiededitor.editfieldvalue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.gui.undo.UndoableFieldChange; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; + +public class EditFieldValueViewModel extends AbstractViewModel { + private final BibDatabaseContext databaseContext; + private final List selectedEntries; + + private final StringProperty fieldValue = new SimpleStringProperty(); + + private final ObjectProperty selectedField = new SimpleObjectProperty<>(); + + private final BooleanProperty overwriteNonEmptyFields = new SimpleBooleanProperty(); + + private final ObservableList allFields = FXCollections.observableArrayList(); + + private final NamedCompound dialogEdits; + + public EditFieldValueViewModel(BibDatabaseContext databaseContext, List selectedEntries, NamedCompound dialogEdits) { + this.databaseContext = databaseContext; + this.selectedEntries = new ArrayList<>(selectedEntries); + this.dialogEdits = dialogEdits; + + allFields.addAll(databaseContext.getDatabase().getAllVisibleFields().stream().toList()); + } + + public void clearSelectedField() { + NamedCompound clearFieldEdit = new NamedCompound("CLEAR_SELECTED_FIELD"); + + for (BibEntry entry : selectedEntries) { + Optional oldFieldValue = entry.getField(selectedField.get()); + if (oldFieldValue.isPresent()) { + entry.setField(selectedField.get(), ""); + + clearFieldEdit.addEdit(new UndoableFieldChange(entry, + selectedField.get(), + oldFieldValue.orElse(null), + fieldValue.get())); + } + } + + if (clearFieldEdit.hasEdits()) { + clearFieldEdit.end(); + dialogEdits.addEdit(clearFieldEdit); + } + } + + public void setFieldValue() { + NamedCompound setFieldEdit = new NamedCompound("CHANGE_SELECTED_FIELD"); + String toSetFieldValue = fieldValue.getValue(); + + for (BibEntry entry : selectedEntries) { + Optional oldFieldValue = entry.getField(selectedField.get()); + if (oldFieldValue.isEmpty() || overwriteNonEmptyFields.get()) { + entry.setField(selectedField.get(), toSetFieldValue); + + setFieldEdit.addEdit(new UndoableFieldChange(entry, + selectedField.get(), + oldFieldValue.orElse(null), + toSetFieldValue)); + fieldValue.set(""); + } + } + + if (setFieldEdit.hasEdits()) { + setFieldEdit.end(); + dialogEdits.addEdit(setFieldEdit); + } + } + + public void appendToFieldValue() { + NamedCompound appendToFieldEdit = new NamedCompound("APPEND_TO_SELECTED_FIELD"); + String toAppendFieldValue = fieldValue.getValue(); + + for (BibEntry entry : selectedEntries) { + Optional oldFieldValue = entry.getField(selectedField.get()); + // Append button should be disabled if 'overwriteNonEmptyFields' is false + if (overwriteNonEmptyFields.get()) { + String newFieldValue = oldFieldValue.orElse("").concat(toAppendFieldValue); + + entry.setField(selectedField.get(), newFieldValue); + appendToFieldEdit.addEdit(new UndoableFieldChange(entry, + selectedField.get(), + oldFieldValue.orElse(null), + newFieldValue) + ); + + fieldValue.set(""); + } + } + + if (appendToFieldEdit.hasEdits()) { + appendToFieldEdit.end(); + dialogEdits.addEdit(appendToFieldEdit); + } + } + + public ObservableList getAllFields() { + return allFields; + } + + public ObjectProperty selectedFieldProperty() { + return selectedField; + } + + public String getFieldValue() { + return fieldValue.get(); + } + + public StringProperty fieldValueProperty() { + return fieldValue; + } + + public BooleanProperty overwriteNonEmptyFieldsProperty() { + return overwriteNonEmptyFields; + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTab.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTab.fxml new file mode 100644 index 00000000000..388f1f567c0 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTab.fxml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTabView.java new file mode 100644 index 00000000000..8029cfc7827 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTabView.java @@ -0,0 +1,74 @@ +package org.jabref.gui.edit.automaticfiededitor.renamefield; + +import java.util.Comparator; +import java.util.List; + +import javafx.fxml.FXML; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.util.StringConverter; + +import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabView; +import org.jabref.gui.edit.automaticfiededitor.AutomaticFieldEditorTab; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; + +import com.airhacks.afterburner.views.ViewLoader; + +public class RenameFieldTabView extends AbstractAutomaticFieldEditorTabView implements AutomaticFieldEditorTab { + @FXML + private ComboBox fieldComboBox; + @FXML + private TextField newFieldNameTextField; + private final List selectedEntries; + private final BibDatabaseContext databaseContext; + private final NamedCompound dialogEdits; + private RenameFieldViewModel viewModel; + + public RenameFieldTabView(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { + this.selectedEntries = selectedEntries; + this.databaseContext = databaseContext; + this.dialogEdits = dialogEdits; + + ViewLoader.view(this) + .root(this) + .load(); + } + + @FXML + public void initialize() { + viewModel = new RenameFieldViewModel(selectedEntries, databaseContext, dialogEdits); + + fieldComboBox.getItems().addAll(viewModel.getAllFields().sorted(Comparator.comparing(Field::getName))); + fieldComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Field field) { + return field.getName(); + } + + @Override + public Field fromString(String name) { + return FieldFactory.parseField(name); + } + }); + + fieldComboBox.getSelectionModel().selectFirst(); + viewModel.selectedFieldProperty().bindBidirectional(fieldComboBox.valueProperty()); + + viewModel.newFieldNameProperty().bindBidirectional(newFieldNameTextField.textProperty()); + } + + @Override + public String getTabName() { + return Localization.lang("Rename field"); + } + + @FXML + void renameField() { + viewModel.renameField(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldViewModel.java new file mode 100644 index 00000000000..f8e430ccc51 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldViewModel.java @@ -0,0 +1,71 @@ +package org.jabref.gui.edit.automaticfiededitor.renamefield; + +import java.util.List; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.edit.automaticfiededitor.MoveFieldValueAction; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; + +public class RenameFieldViewModel extends AbstractViewModel { + + private final StringProperty newFieldName = new SimpleStringProperty(); + private final ObjectProperty selectedField = new SimpleObjectProperty<>(); + + private final ObservableList allFields = FXCollections.observableArrayList(); + private final List selectedEntries; + private final BibDatabaseContext databaseContext; + private final NamedCompound dialogEdits; + + public RenameFieldViewModel(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { + this.selectedEntries = selectedEntries; + this.databaseContext = databaseContext; + this.dialogEdits = dialogEdits; + + allFields.addAll(databaseContext.getDatabase().getAllVisibleFields()); + } + + public String getNewFieldName() { + return newFieldName.get(); + } + + public StringProperty newFieldNameProperty() { + return newFieldName; + } + + public Field getSelectedField() { + return selectedField.get(); + } + + public ObjectProperty selectedFieldProperty() { + return selectedField; + } + + public ObservableList getAllFields() { + return allFields; + } + + public void renameField() { + NamedCompound renameEdit = new NamedCompound("RENAME_EDIT"); + + new MoveFieldValueAction(selectedField.get(), + FieldFactory.parseField(newFieldName.get()), + selectedEntries, + renameEdit).execute(); + + if (renameEdit.hasEdits()) { + renameEdit.end(); + dialogEdits.addEdit(renameEdit); + } + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTab.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTab.fxml new file mode 100644 index 00000000000..7ac475e6cd7 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTab.fxml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +