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
}
}
}