package org.jabref.gui.customentrytypes; import java.awt.BorderLayout; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.ListSelectionModel; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import org.jabref.Globals; import org.jabref.gui.BasePanel; import org.jabref.gui.JabRefDialog; import org.jabref.gui.JabRefFrame; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.logic.l10n.Localization; import org.jabref.model.EntryTypes; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.CustomEntryType; import org.jabref.model.entry.EntryType; import org.jabref.model.entry.InternalBibtexFields; import org.jabref.model.strings.StringUtil; import com.jgoodies.forms.builder.ButtonBarBuilder; public class EntryCustomizationDialog extends JabRefDialog implements ListSelectionListener { protected GridBagLayout gbl = new GridBagLayout(); protected GridBagConstraints con = new GridBagConstraints(); protected JButton helpButton; protected JButton delete; private final JabRefFrame frame; private FieldSetComponent reqComp; private FieldSetComponent optComp; private FieldSetComponent optComp2; private EntryTypeList typeComp; private JButton ok; private JButton cancel; private JButton apply; private final List<String> preset = InternalBibtexFields.getAllPublicFieldNames(); private String lastSelected; private final Map<String, List<String>> reqLists = new HashMap<>(); private final Map<String, List<String>> optLists = new HashMap<>(); private final Map<String, List<String>> opt2Lists = new HashMap<>(); private final Set<String> defaulted = new HashSet<>(); private final Set<String> changed = new HashSet<>(); private boolean biblatexMode; private BibDatabaseMode bibDatabaseMode; /** * Creates a new instance of EntryCustomizationDialog */ public EntryCustomizationDialog(JabRefFrame frame) { super(frame, Localization.lang("Customize entry types"), false, EntryCustomizationDialog.class); this.frame = frame; initGui(); } private void initGui() { Container pane = getContentPane(); pane.setLayout(new BorderLayout()); if (frame.getCurrentBasePanel() == null) { bibDatabaseMode = Globals.prefs.getDefaultBibDatabaseMode(); } else { bibDatabaseMode = frame.getCurrentBasePanel().getBibDatabaseContext().getMode(); } biblatexMode = BibDatabaseMode.BIBLATEX.equals(bibDatabaseMode); JPanel main = new JPanel(); JPanel buttons = new JPanel(); JPanel right = new JPanel(); main.setLayout(new BorderLayout()); right.setLayout(new GridLayout(biblatexMode ? 2 : 1, 2)); List<String> entryTypes = new ArrayList<>(); for (String s : EntryTypes.getAllTypes(bibDatabaseMode)) { entryTypes.add(s); } typeComp = new EntryTypeList(entryTypes, bibDatabaseMode); typeComp.addListSelectionListener(this); typeComp.addAdditionActionListener(e -> typeComp.selectField(e.getActionCommand())); typeComp.addDefaultActionListener(new DefaultListener()); typeComp.setListSelectionMode(ListSelectionModel.SINGLE_SELECTION); //typeComp.setEnabled(false); reqComp = new FieldSetComponent(Localization.lang("Required fields"), new ArrayList<>(), preset, true, true); reqComp.setEnabled(false); reqComp.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); ListDataListener dataListener = new DataListener(); reqComp.addListDataListener(dataListener); optComp = new FieldSetComponent(Localization.lang("Optional fields"), new ArrayList<>(), preset, true, true); optComp.setEnabled(false); optComp.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); optComp.addListDataListener(dataListener); right.add(reqComp); right.add(optComp); if (biblatexMode) { optComp2 = new FieldSetComponent(Localization.lang("Optional fields") + " 2", new ArrayList<>(), preset, true, true); optComp2.setEnabled(false); optComp2.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); optComp2.addListDataListener(dataListener); right.add(new JPanel()); right.add(optComp2); } //right.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Globals.lang("Fields"))); right.setBorder(BorderFactory.createEtchedBorder()); ok = new JButton(Localization.lang("OK")); cancel = new JButton(Localization.lang("Cancel")); apply = new JButton(Localization.lang("Apply")); ok.addActionListener(e -> { applyChanges(); dispose(); }); apply.addActionListener(e -> applyChanges()); cancel.addActionListener(e -> dispose()); ButtonBarBuilder bb = new ButtonBarBuilder(buttons); buttons.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); bb.addGlue(); bb.addButton(ok); bb.addButton(apply); bb.addButton(cancel); bb.addGlue(); AbstractAction closeAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { dispose(); } }; ActionMap am = main.getActionMap(); InputMap im = main.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close"); am.put("close", closeAction); //con.fill = GridBagConstraints.BOTH; //con.weightx = 0.3; //con.weighty = 1; //gbl.setConstraints(typeComp, con); main.add(typeComp, BorderLayout.WEST); main.add(right, BorderLayout.CENTER); main.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); pane.add(main, BorderLayout.CENTER); pane.add(buttons, BorderLayout.SOUTH); pack(); } @Override public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } if (lastSelected != null) { // The entry type lastSelected is now unselected, so we store the current settings // for that type in our two maps. reqLists.put(lastSelected, reqComp.getFields()); optLists.put(lastSelected, optComp.getFields()); if (biblatexMode) { opt2Lists.put(lastSelected, optComp2.getFields()); } } String selectedTypeName = typeComp.getFirstSelected(); if (selectedTypeName == null) { return; } List<String> requiredFieldsSelectedType = reqLists.get(selectedTypeName); if (requiredFieldsSelectedType == null) { Optional<EntryType> type = EntryTypes.getType(selectedTypeName, bibDatabaseMode); if (type.isPresent()) { List<String> req = type.get().getRequiredFields(); List<String> opt; if (biblatexMode) { opt = type.get().getPrimaryOptionalFields(); List<String> opt2 = type.get().getSecondaryOptionalFields(); optComp2.setFields(opt2); optComp2.setEnabled(true); } else { opt = type.get().getOptionalFields(); } reqComp.setFields(req); reqComp.setEnabled(true); optComp.setFields(opt); optComp.setEnabled(true); } else { // New entry reqComp.setFields(new ArrayList<>()); reqComp.setEnabled(true); optComp.setFields(new ArrayList<>()); optComp.setEnabled(true); if (biblatexMode) { optComp2.setFields(new ArrayList<>()); optComp2.setEnabled(true); } reqComp.requestFocus(); } } else { reqComp.setFields(requiredFieldsSelectedType); optComp.setFields(optLists.get(selectedTypeName)); if (biblatexMode) { optComp2.setFields(opt2Lists.get(selectedTypeName)); } } lastSelected = selectedTypeName; typeComp.enable(selectedTypeName, changed.contains(lastSelected) && !defaulted.contains(lastSelected)); } private void applyChanges() { valueChanged(new ListSelectionEvent(new JList<>(), 0, 0, false)); List<String> actuallyChangedTypes = new ArrayList<>(); // Iterate over our map of required fields, and list those types if necessary: List<String> types = typeComp.getFields(); for (Map.Entry<String, List<String>> stringListEntry : reqLists.entrySet()) { if (!types.contains(stringListEntry.getKey())) { continue; } List<String> requiredFieldsList = stringListEntry.getValue(); List<String> optionalFieldsList = optLists.get(stringListEntry.getKey()); List<String> secondaryOptionalFieldsLists = opt2Lists.get(stringListEntry.getKey()); if (secondaryOptionalFieldsLists == null) { secondaryOptionalFieldsLists = new ArrayList<>(0); } // If this type is already existing, check if any changes have // been made boolean changesMade = true; if (defaulted.contains(stringListEntry.getKey())) { // This type should be reverted to its default setup. EntryTypes.removeType(stringListEntry.getKey(), bibDatabaseMode); actuallyChangedTypes.add(stringListEntry.getKey().toLowerCase(Locale.ENGLISH)); defaulted.remove(stringListEntry.getKey()); continue; } Optional<EntryType> oldType = EntryTypes.getType(stringListEntry.getKey(), bibDatabaseMode); if (oldType.isPresent()) { List<String> oldRequiredFieldsList = oldType.get().getRequiredFieldsFlat(); List<String> oldOptionalFieldsList = oldType.get().getOptionalFields(); if (biblatexMode) { List<String> oldPrimaryOptionalFieldsLists = oldType.get().getPrimaryOptionalFields(); List<String> oldSecondaryOptionalFieldsList = oldType.get().getSecondaryOptionalFields(); if (equalLists(oldRequiredFieldsList, requiredFieldsList) && equalLists(oldPrimaryOptionalFieldsLists, optionalFieldsList) && equalLists(oldSecondaryOptionalFieldsList, secondaryOptionalFieldsLists)) { changesMade = false; } } else if (equalLists(oldRequiredFieldsList, requiredFieldsList) && equalLists(oldOptionalFieldsList, optionalFieldsList)) { changesMade = false; } } if (changesMade) { CustomEntryType customType = biblatexMode ? new CustomEntryType(StringUtil.capitalizeFirst(stringListEntry.getKey()), requiredFieldsList, optionalFieldsList, secondaryOptionalFieldsLists) : new CustomEntryType(StringUtil.capitalizeFirst(stringListEntry.getKey()), requiredFieldsList, optionalFieldsList); EntryTypes.addOrModifyCustomEntryType(customType, bibDatabaseMode); actuallyChangedTypes.add(customType.getName().toLowerCase(Locale.ENGLISH)); } } // update all affected entries if something has been changed if (!actuallyChangedTypes.isEmpty()) { updateEntriesForChangedTypes(actuallyChangedTypes); } Set<String> typesToRemove = new HashSet<>(); for (String existingType : EntryTypes.getAllTypes(bibDatabaseMode)) { if (!types.contains(existingType)) { typesToRemove.add(existingType); } } // Remove those that should be removed: if (!typesToRemove.isEmpty()) { for (String typeToRemove : typesToRemove) { deleteType(typeToRemove); } } updateTables(); CustomEntryTypesManager.saveCustomEntryTypes(Globals.prefs); } private void deleteType(String name) { Optional<EntryType> type = EntryTypes.getType(name, bibDatabaseMode); if (type.isPresent() && (type.get() instanceof CustomEntryType)) { if (!EntryTypes.getStandardType(name, bibDatabaseMode).isPresent()) { int reply = JOptionPane.showConfirmDialog (frame, Localization.lang("All entries of this " + "type will be declared " + "typeless. Continue?"), Localization.lang("Delete custom format") + " '" + StringUtil.capitalizeFirst(name) + '\'', JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (reply != JOptionPane.YES_OPTION) { return; } } EntryTypes.removeType(name, bibDatabaseMode); updateEntriesForChangedTypes(Arrays.asList(name.toLowerCase(Locale.ENGLISH))); changed.remove(name); reqLists.remove(name); optLists.remove(name); if (biblatexMode) { opt2Lists.remove(name); } } } private static boolean equalLists(List<String> one, List<String> two) { if ((one == null) && (two == null)) { return true; // Both null. } if ((one == null) || (two == null)) { return false; // One of them null, the other not. } if (one.size() != two.size()) { return false; // Different length. } // If we get here, we know that both are non-null, and that they have the same length. for (int i = 0; i < one.size(); i++) { if (!one.get(i).equals(two.get(i))) { return false; } } // If we get here, all entries have matched. return true; } private void updateEntriesForChangedTypes(List<String> actuallyChangedTypes) { for (BasePanel bp : frame.getBasePanelList()) { // get all affected entries List<BibEntry> filtered = bp.getDatabase().getEntries().stream() .filter(entry -> actuallyChangedTypes.contains(entry.getType().toLowerCase(Locale.ENGLISH))).collect(Collectors.toList()); // update all affected entries with new type filtered.forEach(entry -> EntryTypes.getType(entry.getType(), bibDatabaseMode).ifPresent(entry::setType)); } } private void updateTables() { for (BasePanel basePanel : frame.getBasePanelList()) { ((AbstractTableModel) basePanel.getMainTable().getModel()).fireTableDataChanged(); } } // DEFAULT button pressed. Remember that this entry should be reset to default, // unless changes are made later. private class DefaultListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { if (lastSelected == null) { return; } defaulted.add(lastSelected); Optional<EntryType> type = EntryTypes.getStandardType(lastSelected, bibDatabaseMode); if (type.isPresent()) { List<String> of = type.get().getOptionalFields(); List<String> req = type.get().getRequiredFields(); List<String> opt1 = new ArrayList<>(); List<String> opt2 = new ArrayList<>(); if (!(of.isEmpty())) { if (biblatexMode) { opt1 = type.get().getPrimaryOptionalFields(); opt2 = type.get().getSecondaryOptionalFields(); } else { opt1 = of; } } reqComp.setFields(req); reqComp.setEnabled(true); optComp.setFields(opt1); if (biblatexMode) { optComp2.setFields(opt2); } } } } class DataListener implements ListDataListener { @Override public void intervalAdded(ListDataEvent e) { record(); } @Override public void intervalRemoved(ListDataEvent e) { record(); } @Override public void contentsChanged(ListDataEvent e) { record(); } private void record() { if (lastSelected == null) { return; } defaulted.remove(lastSelected); changed.add(lastSelected); typeComp.enable(lastSelected, true); } } }