package org.jabref.gui.customentrytypes; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import javax.swing.Box; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.JViewport; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.event.ListDataListener; import javax.swing.event.ListSelectionListener; import org.jabref.Globals; import org.jabref.gui.IconTheme; import org.jabref.logic.bibtexkeypattern.BibtexKeyPatternUtil; import org.jabref.logic.l10n.Localization; import org.jabref.preferences.JabRefPreferences; /** * @author alver */ class FieldSetComponent extends JPanel { protected final JList<String> list; protected DefaultListModel<String> listModel; protected final JButton remove; protected final GridBagLayout gbl = new GridBagLayout(); protected final GridBagConstraints con = new GridBagConstraints(); protected final boolean forceLowerCase; protected boolean changesMade; private final Set<ActionListener> additionListeners = new HashSet<>(); private final JScrollPane sp; private JComboBox<String> sel; private JTextField input; private final JButton add; private JButton up; private JButton down; private final Set<ListDataListener> modelListeners = new HashSet<>(); /** * Creates a new instance of FieldSetComponent, with preset selection * values. These are put into a JComboBox. */ public FieldSetComponent(String title, List<String> fields, List<String> preset, boolean arrows, boolean forceLowerCase) { this(title, fields, preset, Localization.lang("Add"), Localization.lang("Remove"), arrows, forceLowerCase); } /** * Creates a new instance of FieldSetComponent without preset selection * values. Replaces the JComboBox with a JTextField. */ FieldSetComponent(String title, List<String> fields, boolean arrows, boolean forceLowerCase) { this(title, fields, null, Localization.lang("Add"), Localization.lang("Remove"), arrows, forceLowerCase); } private FieldSetComponent(String title, List<String> fields, List<String> preset, String addText, String removeText, boolean arrows, boolean forceLowerCase) { this.forceLowerCase = forceLowerCase; add = new JButton(addText); remove = new JButton(removeText); listModel = new DefaultListModel<>(); JLabel title1 = null; if (title != null) { title1 = new JLabel(title); } for (String field : fields) { listModel.addElement(field); } list = new JList<>(listModel); list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); // Set up GUI: add.addActionListener(e -> { // Selection has been made, or add button pressed: if ((sel != null) && (sel.getSelectedItem() != null)) { String s = sel.getSelectedItem().toString(); addField(s); } else if ((input != null) && !"".equals(input.getText())) { addField(input.getText()); } }); remove.addActionListener(e -> removeSelected()); // Remove button pressed setLayout(gbl); con.insets = new Insets(1, 1, 1, 1); con.fill = GridBagConstraints.BOTH; con.weightx = 1; con.gridwidth = GridBagConstraints.REMAINDER; if (title1 != null) { gbl.setConstraints(title1, con); add(title1); } con.weighty = 1; sp = new JScrollPane(list, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); gbl.setConstraints(sp, con); add(sp); con.weighty = 0; con.gridwidth = 1; if (arrows) { con.weightx = 0; up = new JButton(IconTheme.JabRefIcon.UP.getSmallIcon()); down = new JButton(IconTheme.JabRefIcon.DOWN.getSmallIcon()); up.addActionListener(e -> move(-1)); down.addActionListener(e -> move(1)); up.setToolTipText(Localization.lang("Move up")); down.setToolTipText(Localization.lang("Move down")); gbl.setConstraints(up, con); add(up); gbl.setConstraints(down, con); add(down); con.weightx = 0; } Component strut = Box.createHorizontalStrut(5); gbl.setConstraints(strut, con); add(strut); con.weightx = 1; con.gridwidth = GridBagConstraints.REMAINDER; //Component b = Box.createHorizontalGlue(); //gbl.setConstraints(b, con); //add(b); //if (!arrows) con.gridwidth = GridBagConstraints.REMAINDER; gbl.setConstraints(remove, con); add(remove); con.gridwidth = 3; con.weightx = 1; if (preset == null) { input = new JTextField(20); input.addActionListener(e -> addField(input.getText())); gbl.setConstraints(input, con); add(input); } else { sel = new JComboBox<>(preset.toArray(new String[preset.size()])); sel.setEditable(true); gbl.setConstraints(sel, con); add(sel); } con.gridwidth = GridBagConstraints.REMAINDER; con.weighty = 0; con.weightx = 0.5; con.gridwidth = 1; gbl.setConstraints(add, con); add(add); FieldListFocusListener<String> fieldListFocusListener = new FieldListFocusListener<>(list); list.addFocusListener(fieldListFocusListener); } public void setListSelectionMode(int mode) { list.setSelectionMode(mode); } public void selectField(String fieldName) { int idx = listModel.indexOf(fieldName); if (idx >= 0) { list.setSelectedIndex(idx); } // Make sure it is visible: JViewport viewport = sp.getViewport(); Rectangle rectangle = list.getCellBounds(idx, idx); if (rectangle != null) { viewport.scrollRectToVisible(rectangle); } } public String getFirstSelected() { return list.getSelectedValue(); } @Override public void setEnabled(boolean en) { if (input != null) { input.setEnabled(en); } if (sel != null) { sel.setEnabled(en); } if (up != null) { up.setEnabled(en); down.setEnabled(en); } add.setEnabled(en); remove.setEnabled(en); } public void setFields(List<String> fields) { DefaultListModel<String> newListModel = new DefaultListModel<>(); for (String field : fields) { newListModel.addElement(field); } this.listModel = newListModel; for (ListDataListener modelListener : modelListeners) { newListModel.addListDataListener(modelListener); } list.setModel(newListModel); } /** * This method is called when a new field should be added to the list. Performs validation of the * field. */ protected void addField(String str) { String s = str.trim(); if (forceLowerCase) { s = s.toLowerCase(Locale.ROOT); } if ("".equals(s) || listModel.contains(s)) { return; } String testString = BibtexKeyPatternUtil.checkLegalKey(s, Globals.prefs.getBoolean(JabRefPreferences.ENFORCE_LEGAL_BIBTEX_KEY)); if (!testString.equals(s) || (s.indexOf('&') >= 0)) { // Report error and exit. JOptionPane.showMessageDialog(this, Localization.lang("Field names are not allowed to contain white space or the following " + "characters") + ": # { } ~ , ^ &", Localization.lang("Error"), JOptionPane.ERROR_MESSAGE); return; } addFieldUncritically(s); } /** * This method adds a new field to the list, without any regard to validation. This method can be * useful for classes that overrides addField(s) to provide different validation. */ protected void addFieldUncritically(String s) { listModel.addElement(s); changesMade = true; for (ActionListener additionListener : additionListeners) { additionListener.actionPerformed(new ActionEvent(this, 0, s)); } } protected void removeSelected() { int[] selected = list.getSelectedIndices(); if (selected.length > 0) { changesMade = true; } for (int i = 0; i < selected.length; i++) { listModel.removeElementAt(selected[selected.length - 1 - i]); } } /** * Return the current list. */ public List<String> getFields() { List<String> res = new ArrayList<>(listModel.getSize()); Enumeration<String> elements = listModel.elements(); while (elements.hasMoreElements()) { res.add(elements.nextElement()); } return res; } /** * Add a ListSelectionListener to the JList component displayed as part of this component. */ public void addListSelectionListener(ListSelectionListener l) { list.addListSelectionListener(l); } /** * Adds an ActionListener that will receive events each time a field is added. The ActionEvent * will specify this component as source, and the added field as action command. */ public void addAdditionActionListener(ActionListener l) { additionListeners.add(l); } public void addListDataListener(ListDataListener l) { listModel.addListDataListener(l); modelListeners.add(l); } /** * If a field is selected in the list, move it dy positions. */ private void move(int dy) { int oldIdx = list.getSelectedIndex(); if (oldIdx < 0) { return; } String o = listModel.get(oldIdx); // Compute the new index: int newInd = Math.max(0, Math.min(listModel.size() - 1, oldIdx + dy)); listModel.remove(oldIdx); listModel.add(newInd, o); list.setSelectedIndex(newInd); } /** * FocusListener to select the first entry in the list of fields when they are focused */ protected class FieldListFocusListener<T> implements FocusListener { private final JList<T> list; public FieldListFocusListener(JList<T> list) { this.list = list; } @Override public void focusGained(FocusEvent e) { if (list.getSelectedValue() == null) { list.setSelectedIndex(0); } } @Override public void focusLost(FocusEvent e) { //focus should remain at the same position so nothing to do here } } }