/* 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; } } }