package org.jabref.gui.actions;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListModel;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JList;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import org.jabref.Globals;
import org.jabref.JabRefGUI;
import org.jabref.gui.BasePanel;
import org.jabref.gui.JabRefFrame;
import org.jabref.gui.autocompleter.AutoCompleteListener;
import org.jabref.gui.keyboard.KeyBinding;
import org.jabref.gui.undo.NamedCompound;
import org.jabref.gui.undo.UndoableFieldChange;
import org.jabref.logic.autocompleter.AutoCompleter;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.specialfields.SpecialFieldsUtils;
import org.jabref.model.FieldChange;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.FieldName;
import org.jabref.model.entry.Keyword;
import org.jabref.model.entry.KeywordList;
import org.jabref.model.strings.StringUtil;
import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.FormBuilder;
import com.jgoodies.forms.layout.FormLayout;
/**
* An Action for launching keyword managing dialog
*
*/
public class ManageKeywordsAction extends MnemonicAwareAction {
private final JabRefFrame frame;
private JDialog diag;
private DefaultListModel<Keyword> keywordListModel;
private JRadioButton intersectKeywords;
private JRadioButton mergeKeywords;
private boolean canceled;
private final KeywordList sortedKeywordsOfAllEntriesBeforeUpdateByUser = new KeywordList();
public ManageKeywordsAction(JabRefFrame frame) {
putValue(Action.NAME, Localization.menuTitle("Manage keywords"));
this.frame = frame;
}
private void createDialog() {
if (diag != null) {
return;
}
// keyword to add
JTextField keyword = new JTextField();
keywordListModel = new DefaultListModel<>();
JList<Keyword> keywordList = new JList<>(keywordListModel);
keywordList.setVisibleRowCount(8);
JScrollPane kPane = new JScrollPane(keywordList);
diag = new JDialog(frame, Localization.lang("Manage keywords"), true);
JButton ok = new JButton(Localization.lang("OK"));
JButton cancel = new JButton(Localization.lang("Cancel"));
JButton add = new JButton(Localization.lang("Add"));
JButton remove = new JButton(Localization.lang("Remove"));
keywordList.setVisibleRowCount(10);
intersectKeywords = new JRadioButton(Localization.lang("Display keywords appearing in ALL entries"));
mergeKeywords = new JRadioButton(Localization.lang("Display keywords appearing in ANY entry"));
ButtonGroup group = new ButtonGroup();
group.add(intersectKeywords);
group.add(mergeKeywords);
ActionListener stateChanged = e -> fillKeyWordList();
intersectKeywords.addActionListener(stateChanged);
mergeKeywords.addActionListener(stateChanged);
intersectKeywords.setSelected(true);
FormBuilder builder = FormBuilder.create().layout(new FormLayout("fill:200dlu:grow, 4dlu, fill:pref",
"pref, 2dlu, pref, 1dlu, pref, 2dlu, fill:100dlu:grow, 4dlu, pref, 4dlu, pref, "));
builder.addSeparator(Localization.lang("Keywords of selected entries")).xyw(1, 1, 3);
builder.add(intersectKeywords).xyw(1, 3, 3);
builder.add(mergeKeywords).xyw(1, 5, 3);
builder.add(kPane).xywh(1, 7, 1, 3);
builder.add(remove).xy(3, 9);
builder.add(keyword).xy(1, 11);
builder.add(add).xy(3, 11);
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));
ok.addActionListener(e -> {
canceled = false;
diag.dispose();
});
Action cancelAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
canceled = true;
diag.dispose();
}
};
cancel.addActionListener(cancelAction);
final ActionListener addActionListener = arg0 -> addButtonActionListener(keyword);
add.addActionListener(addActionListener);
final ActionListener removeActionListenter = arg0 -> {
// keywordList.getSelectedIndices(); does not work, therefore we operate on the values
List<Keyword> values = keywordList.getSelectedValuesList();
for (Keyword val : values) {
keywordListModel.removeElement(val);
}
};
remove.addActionListener(removeActionListenter);
keywordList.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent arg0) {
// Do nothing
}
@Override
public void keyReleased(KeyEvent arg0) {
// Do nothing
}
@Override
public void keyPressed(KeyEvent arg0) {
if (arg0.getKeyCode() == KeyEvent.VK_DELETE) {
removeActionListenter.actionPerformed(null);
}
}
});
AutoCompleter<String> autoComp = JabRefGUI.getMainFrame().getCurrentBasePanel().getAutoCompleters()
.get(FieldName.KEYWORDS);
AutoCompleteListener acl = new AutoCompleteListener(autoComp);
keyword.addKeyListener(acl);
keyword.addFocusListener(acl);
keyword.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// Do nothing
}
@Override
public void keyReleased(KeyEvent e) {
// Do nothing
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
addActionListener.actionPerformed(null);
}
}
});
// 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);
diag.getContentPane().add(builder.getPanel(), BorderLayout.CENTER);
diag.getContentPane().add(bb.getPanel(), BorderLayout.SOUTH);
}
private void addButtonActionListener(JTextField keywordTextField) {
if (StringUtil.isBlank(keywordTextField.getText())) {
return; // nothing to add
}
Keyword newKeyword = new Keyword(keywordTextField.getText().trim());
if (keywordListModel.isEmpty()) {
keywordListModel.addElement(newKeyword);
} else {
int idx = 0;
Keyword element = keywordListModel.getElementAt(idx);
while ((idx < keywordListModel.size()) && (element.compareTo(newKeyword) < 0)) {
idx++;
}
if (idx == keywordListModel.size()) {
// list is empty or word is greater than last word in list
keywordListModel.addElement(newKeyword);
} else if (element.compareTo(newKeyword) == 0) {
// nothing to do, word already in table
} else {
keywordListModel.add(idx, newKeyword);
}
}
keywordTextField.setText(null);
keywordTextField.requestFocusInWindow();
}
@Override
public void actionPerformed(ActionEvent e) {
BasePanel bp = frame.getCurrentBasePanel();
if (bp == null) {
return;
}
if (bp.getSelectedEntries().isEmpty()) {
bp.output(Localization.lang("Select at least one entry to manage keywords."));
return;
}
// Lazy creation of the dialog:
createDialog();
canceled = true;
fillKeyWordList();
diag.pack();
diag.setLocationRelativeTo(frame);
diag.setVisible(true);
if (canceled) {
return;
}
KeywordList keywordsToAdd = new KeywordList();
KeywordList userSelectedKeywords = new KeywordList();
// build keywordsToAdd and userSelectedKeywords in parallel
for (Enumeration<Keyword> keywords = keywordListModel.elements(); keywords.hasMoreElements();) {
Keyword keyword = keywords.nextElement();
userSelectedKeywords.add(keyword);
if (!sortedKeywordsOfAllEntriesBeforeUpdateByUser.contains(keyword)) {
keywordsToAdd.add(keyword);
}
}
KeywordList keywordsToRemove = new KeywordList();
for (Keyword kword : sortedKeywordsOfAllEntriesBeforeUpdateByUser) {
if (!userSelectedKeywords.contains(kword)) {
keywordsToRemove.add(kword);
}
}
if (keywordsToAdd.isEmpty() && keywordsToRemove.isEmpty()) {
// nothing to be done if nothing is new and nothing is obsolete
return;
}
if (Globals.prefs.isKeywordSyncEnabled() && !keywordsToAdd.isEmpty()) {
SpecialFieldsUtils.synchronizeSpecialFields(keywordsToAdd, keywordsToRemove);
}
NamedCompound ce = updateKeywords(bp.getSelectedEntries(), keywordsToAdd, keywordsToRemove);
bp.getUndoManager().addEdit(ce);
bp.markBaseChanged();
}
private NamedCompound updateKeywords(List<BibEntry> entries, KeywordList keywordsToAdd,
KeywordList keywordsToRemove) {
NamedCompound ce = new NamedCompound(Localization.lang("Update keywords"));
for (BibEntry entry : entries) {
KeywordList keywords = entry.getKeywords(Globals.prefs.getKeywordDelimiter());
// update keywords
keywords.removeAll(keywordsToRemove);
keywords.addAll(keywordsToAdd);
// put keywords back
Optional<FieldChange> change = entry.putKeywords(keywords, Globals.prefs.getKeywordDelimiter());
if (change.isPresent()) {
ce.addEdit(new UndoableFieldChange(change.get()));
}
if (Globals.prefs.isKeywordSyncEnabled()) {
SpecialFieldsUtils.syncSpecialFieldsFromKeywords(entry, Globals.prefs.getKeywordDelimiter());
}
}
ce.end();
return ce;
}
private void fillKeyWordList() {
BasePanel bp = frame.getCurrentBasePanel();
List<BibEntry> entries = bp.getSelectedEntries();
// fill dialog with values
keywordListModel.clear();
sortedKeywordsOfAllEntriesBeforeUpdateByUser.clear();
if (mergeKeywords.isSelected()) {
for (BibEntry entry : entries) {
KeywordList separatedKeywords = entry.getKeywords(Globals.prefs.getKeywordDelimiter());
sortedKeywordsOfAllEntriesBeforeUpdateByUser.addAll(separatedKeywords);
}
} else {
assert intersectKeywords.isSelected();
// all keywords from first entry have to be added
BibEntry firstEntry = entries.get(0);
KeywordList separatedKeywords = firstEntry.getKeywords(Globals.prefs.getKeywordDelimiter());
sortedKeywordsOfAllEntriesBeforeUpdateByUser.addAll(separatedKeywords);
// for the remaining entries, intersection has to be used
// this approach ensures that one empty keyword list leads to an empty set of common keywords
for (int i = 1; i < entries.size(); i++) {
BibEntry entry = entries.get(i);
separatedKeywords = entry.getKeywords(Globals.prefs.getKeywordDelimiter());
sortedKeywordsOfAllEntriesBeforeUpdateByUser.retainAll(separatedKeywords);
}
}
for (Keyword keyword : sortedKeywordsOfAllEntriesBeforeUpdateByUser) {
keywordListModel.addElement(keyword);
}
}
}