package org.jabref.gui.actions; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JOptionPane; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.undo.UndoableEdit; import org.jabref.Globals; import org.jabref.gui.BasePanel; import org.jabref.gui.JabRefFrame; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.undo.NamedCompound; import org.jabref.gui.undo.UndoableFieldChange; import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; import com.jgoodies.forms.builder.ButtonBarBuilder; import com.jgoodies.forms.builder.FormBuilder; import com.jgoodies.forms.layout.FormLayout; /** * An Action for launching mass field. * * Functionality: * * Defaults to selected entries, or all entries if none are selected. * * Input field name * * Either set field, or clear field. */ public class MassSetFieldAction extends MnemonicAwareAction { private final JabRefFrame frame; private JDialog diag; private JRadioButton all; private JRadioButton selected; private JRadioButton clear; private JRadioButton set; private JRadioButton rename; private JComboBox<String> field; private JTextField text; private JTextField renameTo; private boolean canceled = true; private JCheckBox overwrite; public MassSetFieldAction(JabRefFrame frame) { putValue(Action.NAME, Localization.menuTitle("Set/clear/rename fields") + "..."); this.frame = frame; } private void createDialog() { diag = new JDialog(frame, Localization.lang("Set/clear/rename fields"), true); field = new JComboBox<>(); field.setEditable(true); text = new JTextField(); text.setEnabled(false); renameTo = new JTextField(); renameTo.setEnabled(false); JButton ok = new JButton(Localization.lang("OK")); JButton cancel = new JButton(Localization.lang("Cancel")); all = new JRadioButton(Localization.lang("All entries")); selected = new JRadioButton(Localization.lang("Selected entries")); clear = new JRadioButton(Localization.lang("Clear fields")); set = new JRadioButton(Localization.lang("Set fields")); rename = new JRadioButton(Localization.lang("Rename field to") + ":"); rename.setToolTipText(Localization.lang("Move contents of a field into a field with a different name")); Set<String> allFields = frame.getCurrentBasePanel().getDatabase().getAllVisibleFields(); for (String f : allFields) { field.addItem(f); } set.addChangeListener(e -> // Entering a text is only relevant if we are setting, not clearing: text.setEnabled(set.isSelected())); clear.addChangeListener(e -> // Overwrite protection makes no sense if we are clearing the field: overwrite.setEnabled(!clear.isSelected())); rename.addChangeListener(e -> // Entering a text is only relevant if we are renaming renameTo.setEnabled(rename.isSelected())); overwrite = new JCheckBox(Localization.lang("Overwrite existing field values"), true); ButtonGroup bg = new ButtonGroup(); bg.add(all); bg.add(selected); bg = new ButtonGroup(); bg.add(clear); bg.add(set); bg.add(rename); FormBuilder builder = FormBuilder.create().layout(new FormLayout( "left:pref, 4dlu, fill:100dlu:grow", "pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref")); builder.addSeparator(Localization.lang("Field name")).xyw(1, 1, 3); builder.add(Localization.lang("Field name")).xy(1, 3); builder.add(field).xy(3, 3); builder.addSeparator(Localization.lang("Include entries")).xyw(1, 5, 3); builder.add(all).xyw(1, 7, 3); builder.add(selected).xyw(1, 9, 3); builder.addSeparator(Localization.lang("New field value")).xyw(1, 11, 3); builder.add(set).xy(1, 13); builder.add(text).xy(3, 13); builder.add(clear).xyw(1, 15, 3); builder.add(rename).xy(1, 17); builder.add(renameTo).xy(3, 17); builder.add(overwrite).xyw(1, 19, 3); ButtonBarBuilder bb = new ButtonBarBuilder(); bb.addGlue(); bb.addButton(ok); bb.addButton(cancel); bb.addGlue(); builder.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); bb.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); diag.getContentPane().add(builder.getPanel(), BorderLayout.CENTER); diag.getContentPane().add(bb.getPanel(), BorderLayout.SOUTH); diag.pack(); ok.addActionListener(e -> { // Check that any field name is set String fieldText = (String) field.getSelectedItem(); if ((fieldText == null) || fieldText.trim().isEmpty()) { JOptionPane.showMessageDialog(diag, Localization.lang("You must enter at least one field name"), "", JOptionPane.ERROR_MESSAGE); return; // Do not close the dialog. } // Check if the user tries to rename multiple fields: if (rename.isSelected()) { String[] fields = getFieldNames(fieldText); if (fields.length > 1) { JOptionPane.showMessageDialog(diag, Localization.lang("You can only rename one field at a time"), "", JOptionPane.ERROR_MESSAGE); return; // Do not close the dialog. } } canceled = false; diag.dispose(); }); Action cancelAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { canceled = true; diag.dispose(); } }; cancel.addActionListener(cancelAction); // Key bindings: ActionMap am = builder.getPanel().getActionMap(); InputMap im = builder.getPanel().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close"); am.put("close", cancelAction); } private void prepareDialog(boolean selection) { selected.setEnabled(selection); if (selection) { selected.setSelected(true); } else { all.setSelected(true); } // Make sure one of the following ones is selected: if (!set.isSelected() && !clear.isSelected() && !rename.isSelected()) { set.setSelected(true); } } @Override public void actionPerformed(ActionEvent e) { BasePanel bp = frame.getCurrentBasePanel(); if (bp == null) { return; } List<BibEntry> entries = bp.getSelectedEntries(); // Lazy creation of the dialog: if (diag == null) { createDialog(); } canceled = true; prepareDialog(!entries.isEmpty()); if (diag != null) { diag.setLocationRelativeTo(frame); diag.setVisible(true); } if (canceled) { return; } Collection<BibEntry> entryList; // If all entries should be treated, change the entries array: if (all.isSelected()) { entryList = bp.getDatabase().getEntries(); } else { entryList = entries; } String toSet = text.getText(); if (toSet.isEmpty()) { toSet = null; } String[] fields = getFieldNames(((String) field.getSelectedItem()).trim().toLowerCase(Locale.ROOT)); NamedCompound ce = new NamedCompound(Localization.lang("Set field")); if (rename.isSelected()) { if (fields.length > 1) { JOptionPane.showMessageDialog(diag, Localization.lang("You can only rename one field at a time"), "", JOptionPane.ERROR_MESSAGE); return; // Do not close the dialog. } else { ce.addEdit(MassSetFieldAction.massRenameField(entryList, fields[0], renameTo.getText(), overwrite.isSelected())); } } else { for (String field1 : fields) { ce.addEdit(MassSetFieldAction.massSetField(entryList, field1, set.isSelected() ? toSet : null, overwrite.isSelected())); } } ce.end(); bp.getUndoManager().addEdit(ce); bp.markBaseChanged(); } /** * 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 text 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<BibEntry> entries, String field, String text, boolean overwriteValues) { NamedCompound ce = new NamedCompound(Localization.lang("Set field")); for (BibEntry entry : entries) { Optional<String> oldVal = entry.getField(field); // If we are not allowed to overwrite values, check if there is a // nonempty // value already for this entry: if (!overwriteValues && (oldVal.isPresent()) && !oldVal.get().isEmpty()) { continue; } if (text == null) { entry.clearField(field); } else { entry.setField(field, text); } ce.addEdit(new UndoableFieldChange(entry, field, oldVal.orElse(null), text)); } ce.end(); return ce; } /** * 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<BibEntry> entries, String field, String newField, boolean overwriteValues) { NamedCompound ce = new NamedCompound(Localization.lang("Rename field")); for (BibEntry entry : entries) { Optional<String> 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<String> valInNewField = entry.getField(newField); if (!overwriteValues && (valInNewField.isPresent()) && !valInNewField.get().isEmpty()) { continue; } entry.setField(newField, valToMove.get()); ce.addEdit(new UndoableFieldChange(entry, newField, valInNewField.orElse(null), valToMove.get())); entry.clearField(field); ce.addEdit(new UndoableFieldChange(entry, field, valToMove.get(), null)); } ce.end(); return ce; } private static String[] getFieldNames(String s) { return s.split("[\\s;,]"); } }