/* Copyright (C) 2003-2011 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 2 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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package net.sf.jabref.groups;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.Vector;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.*;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.undo.AbstractUndoableEdit;
import net.sf.jabref.BasePanel;
import net.sf.jabref.BibtexEntry;
import net.sf.jabref.FieldContentSelector;
import net.sf.jabref.FieldTextField;
import net.sf.jabref.Globals;
import net.sf.jabref.JabRefFrame;
import net.sf.jabref.Util;
import net.sf.jabref.search.SearchExpressionParser;
import antlr.collections.AST;
import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;
/**
* Dialog for creating or modifying groups. Operates directly on the Vector
* containing group information.
*/
class GroupDialog extends JDialog {
private static final int INDEX_EXPLICITGROUP = 0;
private static final int INDEX_KEYWORDGROUP = 1;
private static final int INDEX_SEARCHGROUP = 2;
private static final int TEXTFIELD_LENGTH = 30;
// for all types
private JTextField m_name = new JTextField(TEXTFIELD_LENGTH);
private JRadioButton m_explicitRadioButton = new JRadioButton(Globals
.lang("Statically group entries by manual assignment"));
private JRadioButton m_keywordsRadioButton = new JRadioButton(
Globals.lang("Dynamically group entries by searching a field for a keyword"));
private JRadioButton m_searchRadioButton = new JRadioButton(Globals
.lang("Dynamically group entries by a free-form search expression"));
private JRadioButton m_independentButton = new JRadioButton( // JZTODO lyrics
Globals.lang("Independent group: When selected, view only this group's entries"));
private JRadioButton m_intersectionButton = new JRadioButton( // JZTODO lyrics
Globals.lang("Refine supergroup: When selected, view entries contained in both this group and its supergroup"));
private JRadioButton m_unionButton = new JRadioButton( // JZTODO lyrics
Globals.lang("Include subgroups: When selected, view entries contained in this group or its subgroups"));
// for KeywordGroup
private JTextField m_kgSearchField = new JTextField(TEXTFIELD_LENGTH);
private FieldTextField m_kgSearchTerm = new FieldTextField("keywords", "",
false);
private JCheckBox m_kgCaseSensitive = new JCheckBox(Globals
.lang("Case sensitive"));
private JCheckBox m_kgRegExp = new JCheckBox(Globals
.lang("Regular Expression"));
// for SearchGroup
private JTextField m_sgSearchExpression = new JTextField(TEXTFIELD_LENGTH);
private JCheckBox m_sgCaseSensitive = new JCheckBox(Globals
.lang("Case sensitive"));
private JCheckBox m_sgRegExp = new JCheckBox(Globals
.lang("Regular Expression"));
// for all types
private JButton m_ok = new JButton(Globals.lang("Ok"));
private JButton m_cancel = new JButton(Globals.lang("Cancel"));
private JPanel m_optionsPanel = new JPanel();
private JLabel m_description = new JLabel() {
public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
// width must be smaller than width of enclosing JScrollPane
// to prevent a horizontal scroll bar
d.width = 1;
return d;
}
};
private boolean m_okPressed = false;
private final JabRefFrame m_parent;
private final BasePanel m_basePanel;
private AbstractGroup m_resultingGroup;
private AbstractUndoableEdit m_undoAddPreviousEntires = null;
private final AbstractGroup m_editedGroup;
private CardLayout m_optionsLayout = new CardLayout();
/**
* Shows a group add/edit dialog.
*
* @param jabrefFrame
* The parent frame.
* @param defaultField
* The default grouping field.
* @param editedGroup
* The group being edited, or null if a new group is to be
* created.
*/
public GroupDialog(JabRefFrame jabrefFrame, BasePanel basePanel,
AbstractGroup editedGroup) {
super(jabrefFrame, Globals.lang("Edit group"), true);
m_basePanel = basePanel;
m_parent = jabrefFrame;
m_editedGroup = editedGroup;
// set default values (overwritten if editedGroup != null)
m_kgSearchField.setText(jabrefFrame.prefs().get("groupsDefaultField"));
// configure elements
ButtonGroup groupType = new ButtonGroup();
groupType.add(m_explicitRadioButton);
groupType.add(m_keywordsRadioButton);
groupType.add(m_searchRadioButton);
ButtonGroup groupHierarchy = new ButtonGroup();
groupHierarchy.add(m_independentButton);
groupHierarchy.add(m_intersectionButton);
groupHierarchy.add(m_unionButton);
m_description.setVerticalAlignment(JLabel.TOP);
getRootPane().setDefaultButton(m_ok);
// build individual layout cards for each group
m_optionsPanel.setLayout(m_optionsLayout);
// ... for explicit group
m_optionsPanel.add(new JPanel(), "" + INDEX_EXPLICITGROUP);
// ... for keyword group
FormLayout layoutKG = new FormLayout(
"right:pref, 4dlu, fill:1dlu:grow, 2dlu, left:pref");
DefaultFormBuilder builderKG = new DefaultFormBuilder(layoutKG);
builderKG.append(Globals.lang("Field"));
builderKG.append(m_kgSearchField, 3);
builderKG.nextLine();
builderKG.append(Globals.lang("Keyword"));
builderKG.append(m_kgSearchTerm);
builderKG.append(new FieldContentSelector(m_parent, m_basePanel, this,
m_kgSearchTerm, m_basePanel.metaData(), null, true, ", "));
builderKG.nextLine();
builderKG.append(m_kgCaseSensitive, 3);
builderKG.nextLine();
builderKG.append(m_kgRegExp, 3);
m_optionsPanel.add(builderKG.getPanel(), "" + INDEX_KEYWORDGROUP);
// ... for search group
FormLayout layoutSG = new FormLayout("right:pref, 4dlu, fill:1dlu:grow");
DefaultFormBuilder builderSG = new DefaultFormBuilder(layoutSG);
builderSG.append(Globals.lang("Search expression"));
builderSG.append(m_sgSearchExpression);
builderSG.nextLine();
builderSG.append(m_sgCaseSensitive, 3);
builderSG.nextLine();
builderSG.append(m_sgRegExp, 3);
m_optionsPanel.add(builderSG.getPanel(), "" + INDEX_SEARCHGROUP);
// ... for buttons panel
FormLayout layoutBP = new FormLayout("pref, 4dlu, pref", "p");
layoutBP.setColumnGroups(new int[][] { { 1, 3 } });
ButtonBarBuilder builderBP = new ButtonBarBuilder();
builderBP.addGlue();
builderBP.addButton(m_ok);
builderBP.addButton(m_cancel);
builderBP.addGlue();
builderBP.getPanel().setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
// create layout
FormLayout layoutAll = new FormLayout(
"right:pref, 4dlu, fill:600px, 4dlu, fill:pref",
"p, 3dlu, p, 3dlu, p, 0dlu, p, 0dlu, p, 3dlu, p, 3dlu, p, "
+ "0dlu, p, 0dlu, p, 3dlu, p, 3dlu, "
+ "p, 3dlu, p, 3dlu, top:80dlu, 9dlu, p, 9dlu, p");
DefaultFormBuilder builderAll = new DefaultFormBuilder(layoutAll);
builderAll.setDefaultDialogBorder();
builderAll.appendSeparator(Globals.lang("General"));
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(Globals.lang("Name"));
builderAll.append(m_name);
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(m_explicitRadioButton, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(m_keywordsRadioButton, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(m_searchRadioButton, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.appendSeparator(Globals.lang("Hierarchical context")); // JZTODO lyrics
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(m_independentButton, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(m_intersectionButton, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(m_unionButton, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.appendSeparator(Globals.lang("Options"));
builderAll.nextLine();
builderAll.nextLine();
builderAll.append(m_optionsPanel, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.appendSeparator(Globals.lang("Description"));
builderAll.nextLine();
builderAll.nextLine();
JScrollPane sp = new JScrollPane(m_description,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED) {
public Dimension getPreferredSize() {
return getMaximumSize();
}
};
builderAll.append(sp, 5);
builderAll.nextLine();
builderAll.nextLine();
builderAll.appendSeparator();
builderAll.nextLine();
builderAll.nextLine();
//CellConstraints cc = new CellConstraints();
//builderAll.add(builderBP.getPanel(), cc.xyw(builderAll.getColumn(),
// builderAll.getRow(), 5, "center, fill"));
Container cp = getContentPane();
//cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
cp.add(builderAll.getPanel(), BorderLayout.CENTER);
cp.add(builderBP.getPanel(), BorderLayout.SOUTH);
pack();
setResizable(false);
updateComponents();
setLayoutForSelectedGroup();
Util.placeDialog(this, m_parent);
// add listeners
ItemListener radioButtonItemListener = new ItemListener() {
public void itemStateChanged(ItemEvent e) {
setLayoutForSelectedGroup();
updateComponents();
}
};
m_explicitRadioButton.addItemListener(radioButtonItemListener);
m_keywordsRadioButton.addItemListener(radioButtonItemListener);
m_searchRadioButton.addItemListener(radioButtonItemListener);
Action cancelAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
dispose();
}
};
m_cancel.addActionListener(cancelAction);
builderAll.getPanel().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(Globals.prefs.getKey("Close dialog"), "close");
builderAll.getPanel().getActionMap().put("close", cancelAction);
m_ok.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
m_okPressed = true;
if (m_explicitRadioButton.isSelected()) {
if (m_editedGroup instanceof ExplicitGroup) {
// keep assignments from possible previous ExplicitGroup
m_resultingGroup = m_editedGroup.deepCopy();
m_resultingGroup.setName(m_name.getText().trim());
m_resultingGroup.setHierarchicalContext(getContext());
} else {
m_resultingGroup = new ExplicitGroup(m_name.getText()
.trim(), getContext());
if (m_editedGroup != null)
addPreviousEntries();
}
} else if (m_keywordsRadioButton.isSelected()) {
// regex is correct, otherwise OK would have been disabled
// therefore I don't catch anything here
m_resultingGroup = new KeywordGroup(
m_name.getText().trim(), m_kgSearchField.getText()
.trim(), m_kgSearchTerm.getText().trim(),
m_kgCaseSensitive.isSelected(), m_kgRegExp
.isSelected(), getContext());
if ((m_editedGroup instanceof ExplicitGroup || m_editedGroup instanceof SearchGroup)
&& m_resultingGroup.supportsAdd()) {
addPreviousEntries();
}
} else if (m_searchRadioButton.isSelected()) {
try {
// regex is correct, otherwise OK would have been
// disabled
// therefore I don't catch anything here
m_resultingGroup = new SearchGroup(m_name.getText()
.trim(), m_sgSearchExpression.getText().trim(),
m_sgCaseSensitive.isSelected(), m_sgRegExp
.isSelected(), getContext());
} catch (Exception e1) {
// should never happen
}
}
dispose();
}
});
CaretListener caretListener = new CaretListener() {
public void caretUpdate(CaretEvent e) {
updateComponents();
}
};
ItemListener itemListener = new ItemListener() {
public void itemStateChanged(ItemEvent e) {
updateComponents();
}
};
m_name.addCaretListener(caretListener);
m_kgSearchField.addCaretListener(caretListener);
m_kgSearchTerm.addCaretListener(caretListener);
m_kgCaseSensitive.addItemListener(itemListener);
m_kgRegExp.addItemListener(itemListener);
m_sgSearchExpression.addCaretListener(caretListener);
m_sgRegExp.addItemListener(itemListener);
m_sgCaseSensitive.addItemListener(itemListener);
// configure for current type
if (editedGroup instanceof KeywordGroup) {
KeywordGroup group = (KeywordGroup) editedGroup;
m_name.setText(group.getName());
m_kgSearchField.setText(group.getSearchField());
m_kgSearchTerm.setText(group.getSearchExpression());
m_kgCaseSensitive.setSelected(group.isCaseSensitive());
m_kgRegExp.setSelected(group.isRegExp());
m_keywordsRadioButton.setSelected(true);
setContext(editedGroup.getHierarchicalContext());
} else if (editedGroup instanceof SearchGroup) {
SearchGroup group = (SearchGroup) editedGroup;
m_name.setText(group.getName());
m_sgSearchExpression.setText(group.getSearchExpression());
m_sgCaseSensitive.setSelected(group.isCaseSensitive());
m_sgRegExp.setSelected(group.isRegExp());
m_searchRadioButton.setSelected(true);
setContext(editedGroup.getHierarchicalContext());
} else if (editedGroup instanceof ExplicitGroup) {
m_name.setText(editedGroup.getName());
m_explicitRadioButton.setSelected(true);
setContext(editedGroup.getHierarchicalContext());
} else { // creating new group -> defaults!
m_explicitRadioButton.setSelected(true);
setContext(AbstractGroup.INDEPENDENT);
}
}
public boolean okPressed() {
return m_okPressed;
}
public AbstractGroup getResultingGroup() {
return m_resultingGroup;
}
private void setLayoutForSelectedGroup() {
if (m_explicitRadioButton.isSelected())
m_optionsLayout.show(m_optionsPanel, String
.valueOf(INDEX_EXPLICITGROUP));
else if (m_keywordsRadioButton.isSelected())
m_optionsLayout.show(m_optionsPanel, String
.valueOf(INDEX_KEYWORDGROUP));
else if (m_searchRadioButton.isSelected())
m_optionsLayout.show(m_optionsPanel, String
.valueOf(INDEX_SEARCHGROUP));
}
private void updateComponents() {
// all groups need a name
boolean okEnabled = m_name.getText().trim().length() > 0;
if (!okEnabled) {
setDescription(Globals.lang("Please enter a name for the group."));
m_ok.setEnabled(false);
return;
}
String s1, s2;
if (m_keywordsRadioButton.isSelected()) {
s1 = m_kgSearchField.getText().trim();
okEnabled = okEnabled && s1.matches("\\w+");
s2 = m_kgSearchTerm.getText().trim();
okEnabled = okEnabled && s2.length() > 0;
if (!okEnabled) {
setDescription(Globals
.lang("Please enter the field to search (e.g. <b>keywords</b>) and the keyword to search it for (e.g. <b>electrical</b>)."));
} else {
if (m_kgRegExp.isSelected()) {
try {
Pattern.compile(s2);
setDescription(KeywordGroup.getDescriptionForPreview(s1, s2,
m_kgCaseSensitive.isSelected(), m_kgRegExp
.isSelected()));
} catch (Exception e) {
okEnabled = false;
setDescription(formatRegExException(s2, e));
}
} else {
setDescription(KeywordGroup.getDescriptionForPreview(s1, s2,
m_kgCaseSensitive.isSelected(), m_kgRegExp
.isSelected()));
}
}
setNameFontItalic(true);
} else if (m_searchRadioButton.isSelected()) {
s1 = m_sgSearchExpression.getText().trim();
okEnabled = okEnabled & s1.length() > 0;
if (!okEnabled) {
setDescription(Globals
.lang("Please enter a search term. For example, to search all fields for <b>Smith</b>, enter%c<p>"
+ "<tt>smith</tt><p>"
+ "To search the field <b>Author</b> for <b>Smith</b> and the field <b>Title</b> for <b>electrical</b>, enter%c<p>"
+ "<tt>author%esmith and title%eelectrical</tt>"));
} else {
AST ast = SearchExpressionParser
.checkSyntax(s1, m_sgCaseSensitive.isSelected(),
m_sgRegExp.isSelected());
setDescription(SearchGroup.getDescriptionForPreview(s1, ast,
m_sgCaseSensitive.isSelected(), m_sgRegExp.isSelected()));
if (m_sgRegExp.isSelected()) {
try {
Pattern.compile(s1);
} catch (Exception e) {
okEnabled = false;
setDescription(formatRegExException(s1, e));
}
}
}
setNameFontItalic(true);
} else if (m_explicitRadioButton.isSelected()) {
setDescription(ExplicitGroup.getDescriptionForPreview());
setNameFontItalic(false);
}
m_ok.setEnabled(okEnabled);
}
/**
* This is used when a group is converted and the new group supports
* explicit adding of entries: All entries that match the previous group are
* added to the new group.
*/
private void addPreviousEntries() {
// JZTODO lyrics...
int i = JOptionPane.showConfirmDialog(m_basePanel.frame(), Globals
.lang("Assign the original group's entries to this group?"),
Globals.lang("Change of Grouping Method"),
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if (i == JOptionPane.NO_OPTION)
return;
Vector<BibtexEntry> vec = new Vector<BibtexEntry>();
for (BibtexEntry entry : m_basePanel.database().getEntries()){
if (m_editedGroup.contains(entry))
vec.add(entry);
}
if (vec.size() > 0) {
BibtexEntry[] entries = new BibtexEntry[vec.size()];
vec.toArray(entries);
if (!Util.warnAssignmentSideEffects(new AbstractGroup[]{m_resultingGroup},
entries, m_basePanel.getDatabase(), this))
return;
// the undo information for a conversion to an ExplicitGroup is
// contained completely in the UndoableModifyGroup object.
if (!(m_resultingGroup instanceof ExplicitGroup))
m_undoAddPreviousEntires = m_resultingGroup.add(entries);
}
}
protected void setDescription(String description) {
m_description.setText("<html>" + description + "</html>");
}
protected String formatRegExException(String regExp, Exception e) {
String[] sa = e.getMessage().split("\\n");
StringBuffer sb = new StringBuffer();
for (int i = 0; i < sa.length; ++i) {
if (i > 0)
sb.append("<br>");
sb.append(Util.quoteForHTML(sa[i]));
}
String s = Globals.lang(
"The regular expression <b>%0</b> is invalid%c",
Util.quoteForHTML(regExp))
+ "<p><tt>"
+ sb.toString()
+ "</tt>";
if (!(e instanceof PatternSyntaxException))
return s;
int lastNewline = s.lastIndexOf("<br>");
int hat = s.lastIndexOf("^");
if (lastNewline >= 0 && hat >= 0 && hat > lastNewline)
return s.substring(0, lastNewline + 4)
+ s.substring(lastNewline + 4).replaceAll(" ", " ");
return s;
}
/**
* Returns an undo object for adding the edited group's entries to the new
* group, or null if this did not occur.
*/
public AbstractUndoableEdit getUndoForAddPreviousEntries() {
return m_undoAddPreviousEntires;
}
/** Sets the font of the name entry field. */
protected void setNameFontItalic(boolean italic) {
Font f = m_name.getFont();
if (f.isItalic() != italic) {
f = f.deriveFont(italic ? Font.ITALIC : Font.PLAIN);
m_name.setFont(f);
}
}
/**
* Returns the int representing the selected hierarchical group context.
*/
protected int getContext() {
if (m_independentButton.isSelected())
return AbstractGroup.INDEPENDENT;
if (m_intersectionButton.isSelected())
return AbstractGroup.REFINING;
if (m_unionButton.isSelected())
return AbstractGroup.INCLUDING;
return AbstractGroup.INDEPENDENT; // default
}
protected void setContext(int context) {
switch (context) {
case AbstractGroup.REFINING:
m_intersectionButton.setSelected(true);
return;
case AbstractGroup.INCLUDING:
m_unionButton.setSelected(true);
return;
case AbstractGroup.INDEPENDENT:
default:
m_independentButton.setSelected(true);
return;
}
}
}