/* Copyright (C) 2003-2012 JabRef contributors.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.jabref.util;
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.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.TreeSet;
import javax.swing.AbstractAction;
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 javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.sf.jabref.BasePanel;
import net.sf.jabref.BibtexEntry;
import net.sf.jabref.Globals;
import net.sf.jabref.JabRef;
import net.sf.jabref.JabRefFrame;
import net.sf.jabref.MnemonicAwareAction;
import net.sf.jabref.Util;
import net.sf.jabref.autocompleter.AbstractAutoCompleter;
import net.sf.jabref.gui.AutoCompleteListener;
import net.sf.jabref.specialfields.Priority;
import net.sf.jabref.specialfields.Quality;
import net.sf.jabref.specialfields.Rank;
import net.sf.jabref.specialfields.Relevance;
import net.sf.jabref.specialfields.SpecialFieldsUtils;
import net.sf.jabref.undo.NamedCompound;
import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;
import com.sun.star.bridge.oleautomation.Date;
/**
* 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 ManageKeywordsAction extends MnemonicAwareAction {
private JabRefFrame frame;
private JDialog diag = null;
// keyword to add
private JTextField keyword;
private DefaultListModel keywordListModel;
private JList keywordList;
private JScrollPane kPane;
private JRadioButton intersectKeywords, mergeKeywords;
private JButton ok, cancel, add, remove;
private boolean cancelled;
private TreeSet<String> sortedKeywordsOfAllEntriesBeforeUpdateByUser = new TreeSet<String>();
public ManageKeywordsAction(JabRefFrame frame) {
putValue(NAME, "Manage keywords");
this.frame = frame;
}
private void createDialog() {
keyword = new JTextField();
keywordListModel = new DefaultListModel();
keywordList = new JList(keywordListModel);
keywordList.setVisibleRowCount(8);
kPane = new JScrollPane(keywordList);
diag = new JDialog(frame, Globals.lang("Manage keywords"), true);
ok = new JButton(Globals.lang("Ok"));
cancel = new JButton(Globals.lang("Cancel"));
add = new JButton(Globals.lang("Add"));
remove = new JButton(Globals.lang("Remove"));
keywordList.setVisibleRowCount(10);
intersectKeywords = new JRadioButton("Display keywords appearing in ALL entries");
mergeKeywords = new JRadioButton("Display keywords appearing in ANY entry");
ButtonGroup group = new ButtonGroup();
group.add(intersectKeywords);
group.add(mergeKeywords);
ActionListener stateChanged = new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
fillKeyWordList();
}
};
intersectKeywords.addActionListener(stateChanged);
mergeKeywords.addActionListener(stateChanged);
intersectKeywords.setSelected(true);
DefaultFormBuilder builder = new DefaultFormBuilder(
new FormLayout("fill:200dlu, 4dlu, left:pref, 4dlu, left:pref", ""));
builder.appendSeparator(Globals.lang("Keywords of selected entries"));
builder.append(intersectKeywords, 5);
builder.nextLine();
builder.append(mergeKeywords, 5);
builder.nextLine();
builder.append(kPane, 3);
builder.add(remove);
builder.nextLine();
builder.append(keyword, 3);
builder.append(add);
builder.nextLine();
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(new ActionListener() {
public void actionPerformed(ActionEvent e) {
cancelled = false;
diag.dispose();
}
});
AbstractAction cancelAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
cancelled = true;
diag.dispose();
}
};
cancel.addActionListener(cancelAction);
final ActionListener addActionListener = new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
String text = keyword.getText().trim();
if (text.equals("")) {
// no text to add, do nothing
return;
}
if (keywordListModel.isEmpty()) {
keywordListModel.addElement(text);
} else {
int idx = 0;
String element = (String)keywordListModel.getElementAt(idx);
while ((idx < keywordListModel.size()) &&
(element.compareTo(text) < 0)) {
idx++;
}
if (idx == keywordListModel.size()) {
// list is empty or word is greater than last word in list
keywordListModel.addElement(text);
} else if (element.compareTo(text) == 0) {
// nothing to do, word already in table
} else {
keywordListModel.add(idx, text);
}
}
keyword.setText(null);
keyword.requestFocusInWindow();
}
};
add.addActionListener(addActionListener);
final ActionListener removeActionListenter = new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
// keywordList.getSelectedIndices(); does not work, therefore we operate on the values
Object[] values = keywordList.getSelectedValues();
List<String> selectedValuesList = new ArrayList<String>();
for (int i=0; i<values.length; i++)
selectedValuesList.add((String)values[i]);
for (String val: selectedValuesList) {
keywordListModel.removeElement(val);
}
}
};
remove.addActionListener(removeActionListenter);
keywordList.addKeyListener(new KeyListener() {
public void keyTyped(KeyEvent arg0) {}
public void keyReleased(KeyEvent arg0) {}
public void keyPressed(KeyEvent arg0) {
if (arg0.getKeyCode() == KeyEvent.VK_DELETE) {
removeActionListenter.actionPerformed(null);
}
}
});
AbstractAutoCompleter autoComp = JabRef.jrf.basePanel().getAutoCompleter("keywords");
AutoCompleteListener acl = new AutoCompleteListener(autoComp);
keyword.addKeyListener(acl);
keyword.addFocusListener(acl);
keyword.addKeyListener(new KeyListener() {
public void keyTyped(KeyEvent e) {}
public void keyReleased(KeyEvent e) {}
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.prefs.getKey("Close dialog"), "close");
am.put("close", cancelAction);
diag.getContentPane().add(builder.getPanel(), BorderLayout.CENTER);
diag.getContentPane().add(bb.getPanel(), BorderLayout.SOUTH);
//diag.pack();
}
public void actionPerformed(ActionEvent e) {
BasePanel bp = frame.basePanel();
if (bp == null)
return;
if (bp.getSelectedEntries().length == 0) {
// no entries selected, silently ignore action
return;
}
// Lazy creation of the dialog:
if (diag == null) {
createDialog();
}
cancelled = true;
fillKeyWordList();
diag.pack();
Util.placeDialog(diag, frame);
diag.setVisible(true);
if (cancelled)
return;
HashSet<String> keywordsToAdd = new HashSet<String>();
HashSet<String> userSelectedKeywords = new HashSet<String>();
// build keywordsToAdd and userSelectedKeywords in parallel
for (Enumeration keywords = keywordListModel.elements(); keywords.hasMoreElements(); ) {
String keyword = (String)keywords.nextElement();
userSelectedKeywords.add(keyword);
if (!sortedKeywordsOfAllEntriesBeforeUpdateByUser.contains(keyword)) {
keywordsToAdd.add(keyword);
}
}
HashSet<String> keywordsToRemove = new HashSet<String>();
for (String keyword: sortedKeywordsOfAllEntriesBeforeUpdateByUser) {
if (!userSelectedKeywords.contains(keyword)) {
keywordsToRemove.add(keyword);
}
}
if (keywordsToAdd.isEmpty() && keywordsToRemove.isEmpty()) {
// nothing to be done if nothing is new and nothing is obsolete
return;
}
if (SpecialFieldsUtils.keywordSyncEnabled()) {
if (!keywordsToAdd.isEmpty()) {
// we need to check whether a special field is added
// for each field:
// check if something is added
// if yes, add all keywords of that special fields to the keywords to be removed
HashSet<String> clone;
// Priority
clone = (HashSet<String>) keywordsToAdd.clone();
clone.retainAll(Priority.getInstance().getKeyWords());
if (!clone.isEmpty()) {
keywordsToRemove.addAll(Priority.getInstance().getKeyWords());
}
// Quality
clone = (HashSet<String>) keywordsToAdd.clone();
clone.retainAll(Quality.getInstance().getKeyWords());
if (!clone.isEmpty()) {
keywordsToRemove.addAll(Quality.getInstance().getKeyWords());
}
// Rank
clone = (HashSet<String>) keywordsToAdd.clone();
clone.retainAll(Rank.getInstance().getKeyWords());
if (!clone.isEmpty()) {
keywordsToRemove.addAll(Rank.getInstance().getKeyWords());
}
// Relevance
clone = (HashSet<String>) keywordsToAdd.clone();
clone.retainAll(Relevance.getInstance().getKeyWords());
if (!clone.isEmpty()) {
keywordsToRemove.addAll(Relevance.getInstance().getKeyWords());
}
}
}
BibtexEntry[] entries = bp.getSelectedEntries();
NamedCompound ce = new NamedCompound(Globals.lang("Update keywords"));
for (BibtexEntry entry: entries) {
ArrayList<String> separatedKeywords = Util.getSeparatedKeywords(entry);
// we "intercept" with a treeset
// pro: no duplicates
// possible con: alphabetical sorting of the keywords
TreeSet<String> keywords = new TreeSet<String>();
keywords.addAll(separatedKeywords);
// update keywords
keywords.removeAll(keywordsToRemove);
keywords.addAll(keywordsToAdd);
// put keywords back
separatedKeywords.clear();
separatedKeywords.addAll(keywords);
Util.putKeywords(entry, separatedKeywords, ce);
if (SpecialFieldsUtils.keywordSyncEnabled()) {
SpecialFieldsUtils.syncSpecialFieldsFromKeywords(entry, ce);
}
}
ce.end();
bp.undoManager.addEdit(ce);
bp.markBaseChanged();
}
private void fillKeyWordList() {
BasePanel bp = frame.basePanel();
BibtexEntry[] entries = bp.getSelectedEntries();
// fill dialog with values
keywordListModel.clear();
sortedKeywordsOfAllEntriesBeforeUpdateByUser.clear();
if (mergeKeywords.isSelected()) {
for (BibtexEntry entry : entries) {
ArrayList<String> separatedKeywords = Util.getSeparatedKeywords(entry);
sortedKeywordsOfAllEntriesBeforeUpdateByUser.addAll(separatedKeywords);
}
} else {
assert(intersectKeywords.isSelected());
// all keywords from first entry have to be added
BibtexEntry firstEntry = entries[0];
ArrayList<String> separatedKeywords = Util.getSeparatedKeywords(firstEntry);
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.length; i++) {
BibtexEntry entry = entries[i];
separatedKeywords = Util.getSeparatedKeywords(entry);
sortedKeywordsOfAllEntriesBeforeUpdateByUser.retainAll(separatedKeywords);
}
}
for (String s : sortedKeywordsOfAllEntriesBeforeUpdateByUser) {
keywordListModel.addElement(s);
}
}
}