net.sf.jabref.EntryEditor.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jabref.EntryEditor.java

Source

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

import java.awt.AWTKeyStroke;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.VetoableChangeListener;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.JTextComponent;

import org.apache.commons.lang.StringUtils;

import com.ironiacorp.string.StringUtil;

import net.sf.jabref.autocompleter.AbstractAutoCompleter;
import net.sf.jabref.export.LatexFieldFormatter;
import net.sf.jabref.gui.VerticalLabelUI;
import net.sf.jabref.imports.BibtexParser;
import net.sf.jabref.undo.NamedCompound;
import net.sf.jabref.undo.UndoableChangeType;
import net.sf.jabref.undo.UndoableFieldChange;
import net.sf.jabref.undo.UndoableKeyChange;
import net.sf.jabref.undo.UndoableRemoveEntry;

/**
 * GUI component that allows editing of the fields of a BibtexEntry (i.e. the
 * one that shows up, when you double click on an entry in the table)
 * 
 * It hosts the tabs (required, general, optional) and the buttons to the left.
 * 
 * EntryEditor also registers itself as a VetoableChangeListener, receiving
 * events whenever a field of the entry changes, enabling the text fields to
 * update themselves if the change is made from somewhere else.
 */
public class EntryEditor extends JPanel implements VetoableChangeListener, EntryContainer {

    /**
     * Reference to the entry this editor works on.
     */
    private BibtexEntry entry; // TODO: this field should be final, but for now there violations of this rule

    /**
     * Type of entry when created the editor. If the type of the current entry has been
     * changed, a new EntryEditor should be created.
     */
    private final BibtexEntryType type;

    // The action concerned with copying the BibTeX key to the clipboard.
    AbstractAction nextEntryAction = new NextEntryAction();

    // Actions for switching to next/previous entry.
    AbstractAction prevEntryAction = new PrevEntryAction();

    // The action concerned with storing a field value.
    public StoreFieldAction storeFieldAction = new StoreFieldAction();

    // The actions concerned with switching the panels.
    SwitchLeftAction switchLeftAction = new SwitchLeftAction();

    SwitchRightAction switchRightAction = new SwitchRightAction();

    SaveDatabaseAction saveDatabaseAction = new SaveDatabaseAction();

    protected HelpAction helpAction;

    private JPanel srcPanel;

    private JTextArea source;

    private JTabbedPane tabbed;

    JabRefFrame frame;

    BasePanel panel;

    EntryEditor ths = this;

    HashSet<FieldContentSelector> contentSelectors = new HashSet<FieldContentSelector>();

    private boolean shouldUpdateSourcePanel = true; // This can be set to false to stop the source

    // Indicates that we are about to go to the next or previous entry
    // text area from gettin updated. This is used in cases where the source
    // couldn't be parsed, and the user is given the option to edit it.
    private boolean movingToDifferentEntry = false;

    // This indicates whether the last
    // attempt at parsing the source was successful. It is used to determine whether the
    // dialog should close; it should stay open if the user received an error
    // message about the source, whatever he or she chose to do about it.
    private boolean lastSourceAccepted = true;

    private String lastAcceptedSourceString = null; // This is used to prevent double fields

    private JabRefPreferences prefs;

    private TabListener tabListener = new TabListener();

    public EntryEditor(JabRefFrame frame, BasePanel panel, BibtexEntry entry) {
        this.frame = frame;
        this.panel = panel;
        this.entry = entry;
        prefs = Globals.prefs;
        type = entry.getType();
        entry.addPropertyChangeListener(this);
        helpAction = new HelpAction(frame.helpDiag, GUIGlobals.entryEditorHelp, "Help");

        tabbed = new JTabbedPane();
        BorderLayout bl = new BorderLayout();
        setLayout(bl);
        setupToolBar();
        setupFieldPanels();
        setupSourcePanel();
        add(tabbed, BorderLayout.CENTER);
        tabbed.addChangeListener(tabListener);
        updateAllFields();
    }

    private void setupFieldPanels() {
        EntryEditorTab tab;
        EntryEditorTabList tabList;
        String[] fields = entry.getRequiredFields();
        List<String> fieldList = null;
        if (fields != null) {
            fieldList = Arrays.asList(fields);
        }
        tabbed.removeAll();

        tab = new EntryEditorTab(frame, panel, fieldList, this, true, Globals.lang("Required fields"));
        tabbed.addTab(Globals.lang("Required fields"), GUIGlobals.getImage("required"), tab,
                Globals.lang("Show required fields"));

        if (entry.getOptionalFields() != null && entry.getOptionalFields().length > 0) {
            tab = new EntryEditorTab(frame, panel, Arrays.asList(entry.getOptionalFields()), this, false,
                    Globals.lang("Optional fields"));
            tabbed.addTab(Globals.lang("Optional fields"), GUIGlobals.getImage("optional"), tab,
                    Globals.lang("Show optional fields"));
        }

        tabList = Globals.prefs.getEntryEditorTabList();
        for (int i = 0; i < tabList.getTabCount(); i++) {
            tab = new EntryEditorTab(frame, panel, tabList.getTabFields(i), this, false, tabList.getTabName(i));
            tabbed.addTab(tabList.getTabName(i), GUIGlobals.getImage("general"), tab);
        }
    }

    public BibtexEntryType getType() {
        return type;
    }

    public BibtexEntry getEntry() {
        return entry;
    }

    public BibtexDatabase getDatabase() {
        return panel.getDatabase();
    }

    /**
     * Create toolbar for entry editor.
     */
    private void setupToolBar() {
        JToolBar tlb = new JToolBar(JToolBar.VERTICAL);
        CloseAction closeAction = new CloseAction();
        ;
        StoreFieldAction storeFieldAction = new StoreFieldAction();
        DeleteAction deleteAction = new DeleteAction();
        UndoAction undoAction = new UndoAction();
        RedoAction redoAction = new RedoAction();

        tlb.setBorder(null);
        tlb.setRollover(true);
        tlb.setMargin(new Insets(0, 0, 0, 2));
        tlb.setFloatable(false);
        tlb.addSeparator();
        tlb.add(deleteAction);
        tlb.addSeparator();
        tlb.add(prevEntryAction);
        tlb.add(nextEntryAction);
        tlb.addSeparator();
        tlb.add(helpAction);
        for (Component comp : tlb.getComponents()) {
            ((JComponent) comp).setOpaque(false);
        }

        // The toolbar carries all the key bindings that are valid for the whole window.
        ActionMap am = tlb.getActionMap();
        InputMap im = tlb.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        im.put(prefs.getKey("Close entry editor"), "close");
        am.put("close", closeAction);
        im.put(prefs.getKey("Entry editor, store field"), "store");
        am.put("store", storeFieldAction);
        im.put(prefs.getKey("Entry editor, previous entry"), "prev");
        am.put("prev", prevEntryAction);
        im.put(prefs.getKey("Entry editor, next entry"), "next");
        am.put("next", nextEntryAction);
        im.put(prefs.getKey("Undo"), "undo");
        am.put("undo", undoAction);
        im.put(prefs.getKey("Redo"), "redo");
        am.put("redo", redoAction);
        im.put(prefs.getKey("Help"), "help");
        am.put("help", helpAction);

        // Add actions (and thus buttons)
        JButton closeBut = new JButton(closeAction);
        closeBut.setText(null);
        closeBut.setBorder(null);

        // Create type-label
        TypeLabel typeLabel = new TypeLabel(entry.getType().getName());

        JPanel leftPan = new JPanel();
        leftPan.setLayout(new BorderLayout());
        leftPan.add(closeBut, BorderLayout.NORTH);
        leftPan.add(typeLabel, BorderLayout.CENTER);
        leftPan.add(tlb, BorderLayout.SOUTH);

        add(leftPan, BorderLayout.WEST);
    }

    /**
     * Rebuild the field tabs. This is called e.g. when a new content selector
     * has been added.
     */
    protected void rebuildPanels() {
        // Remove change listener, because the rebuilding causes meaningless events and trouble
        tabbed.removeChangeListener(tabListener);

        setupFieldPanels();
        // Add the change listener again:
        tabbed.addChangeListener(tabListener);
        revalidate();
        repaint();
    }

    private void setupSourcePanel() {
        JScrollPane sp;

        source = new JTextAreaWithHighlighting();
        source.setLineWrap(true);
        source.setTabSize(GUIGlobals.INDENT);
        source.setFont(new Font("Monospaced", Font.PLAIN, Globals.prefs.getInt("fontSize")));
        setupJTextFieldForSourceArea(source);
        updateSource(); // Must call once to setup the initial text of the source panel
        source.addFocusListener(new FieldEditorFocusListener());
        source.addFocusListener(Globals.focusListener); // Add the global focus listener, so a menu item can see 
        // if this field was focused when an action was called.
        frame.getSearchManager().addSearchListener((SearchTextListener) source);

        sp = new JScrollPane(source, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        srcPanel = new JPanel();
        srcPanel.setLayout(new BorderLayout());
        srcPanel.setName(Globals.lang("BibTeX source"));
        srcPanel.setFocusCycleRoot(true);
        srcPanel.add(sp, BorderLayout.CENTER);

        tabbed.addTab(Globals.lang("BibTeX source"), GUIGlobals.getImage("source"), srcPanel,
                Globals.lang("Show/edit BibTeX source"));
    }

    /**
     * If the entry has been just loaded or it has been changed, update the content of the BibTeX
     * panel with the new source.
     */
    public void updateSource() {
        if (shouldUpdateSourcePanel) {
            StringWriter sw = new StringWriter(200);
            try {
                LatexFieldFormatter formatter = new LatexFieldFormatter();
                formatter.setNeverFailOnHashes(true);
                entry.write(sw, formatter, false);

                String srcString = sw.getBuffer().toString();
                source.setText(srcString);
                lastAcceptedSourceString = srcString;

                //////////////////////////////////////////////////////////
                // Set the current Entry to be selected.
                // Fixes the bug of losing selection after, e.g.
                // an autogeneration of a BibTeX key.
                // - ILC (16/02/2010) -
                //////////////////////////////////////////////////////////
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        final int row = panel.mainTable.findEntry(entry);
                        if (row >= 0) {
                            if (panel.mainTable.getSelectedRowCount() == 0) {
                                panel.mainTable.setRowSelectionInterval(row, row);
                            }
                            panel.mainTable.ensureVisible(row);
                        }
                    }
                });

            } catch (IOException ex) {
                source.setText(ex.getMessage() + "\n\n"
                        + Globals.lang("Correct the entry, and reopen editor to display/edit source."));
                source.setEditable(false);
            }
        }
    }

    protected void setupSwingComponentKeyBindings(JComponent component) {
        // Set up key bindings and focus listener for the FieldEditor.
        InputMap im = component.getInputMap(JComponent.WHEN_FOCUSED);
        ActionMap am = component.getActionMap();

        im.put(prefs.getKey("Entry editor, store field"), "store");
        am.put("store", storeFieldAction);

        im.put(prefs.getKey("Entry editor, next panel"), "right");
        im.put(prefs.getKey("Entry editor, next panel 2"), "right");
        am.put("right", switchRightAction);

        im.put(prefs.getKey("Entry editor, previous panel"), "left");
        im.put(prefs.getKey("Entry editor, previous panel 2"), "left");
        am.put("left", switchLeftAction);

        im.put(prefs.getKey("Help"), "help");
        am.put("help", helpAction);

        im.put(prefs.getKey("Save database"), "save");
        am.put("save", saveDatabaseAction);

        im.put(Globals.prefs.getKey("Next tab"), "nexttab");
        am.put("nexttab", frame.nextTab);

        im.put(Globals.prefs.getKey("Previous tab"), "prevtab");
        am.put("prevtab", frame.prevTab);
    }

    /**
     * NOTE: This method is only used for the source panel, not for the
     * other tabs. Look at EntryEditorTab for the setup of text components
     * in the other tabs.
     */
    private void setupJTextFieldForSourceArea(JTextComponent ta) {
        setupSwingComponentKeyBindings(ta);

        // HashSet<AWTKeyStroke> keys = new HashSet<AWTKeyStroke>(ta.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));          
        HashSet<AWTKeyStroke> keys = new HashSet<AWTKeyStroke>();
        keys.add(AWTKeyStroke.getAWTKeyStroke("pressed TAB"));
        ta.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, keys);

        // keys = new HashSet<AWTKeyStroke>(ta.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS));
        keys = new HashSet<AWTKeyStroke>();
        keys.add(KeyStroke.getKeyStroke("shift pressed TAB"));
        ta.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, keys);

        ta.addFocusListener(new FieldListener());
    }

    public void requestFocus() {
        activateVisible();
    }

    private void activateVisible() {
        Object activeTab = tabbed.getComponentAt(tabbed.getSelectedIndex());
        if (activeTab instanceof EntryEditorTab) {
            ((EntryEditorTab) activeTab).activate();
        } else {
            new FocusRequester(source);
        }
    }

    /**
     * Reports the enabled status of the editor, as set by setEnabled()
     */
    public boolean isEnabled() {
        return source.isEnabled();
    }

    /**
     * Sets the enabled status of all text fields of the entry editor.
     */
    public void setEnabled(boolean enabled) {
        for (int i = 0; i < tabbed.getTabCount(); i++) {
            Object o = tabbed.getComponentAt(i);
            if (o instanceof EntryEditorTab) {
                ((EntryEditorTab) o).setEnabled(enabled);
            }
        }
        source.setEnabled(enabled);
    }

    /**
     * Centers the given row, and highlights it.
     * 
     * @param row
     *            an <code>int</code> value
     */
    private void scrollTo(int row) {
        movingToDifferentEntry = true;
        panel.mainTable.setRowSelectionInterval(row, row);
        panel.mainTable.ensureVisible(row);
    }

    /**
     * Makes sure the current edit is stored.
     */
    public void storeCurrentEdit() {
        Component comp = Globals.focusListener.getFocused();
        if (comp == source || (comp instanceof FieldEditor && this.isAncestorOf(comp))) {
            if (comp instanceof FieldEditor) {
                ((FieldEditor) comp).clearAutoCompleteSuggestion();
            }
            storeFieldAction.actionPerformed(new ActionEvent(comp, 0, ""));
        }
    }

    /**
     * Returns the index of the active (visible) panel.
     * 
     * @return an <code>int</code> value
     */
    public int getVisiblePanel() {
        return tabbed.getSelectedIndex();
    }

    /** Returns the name of the currently selected component. */
    public String getVisiblePanelName() {
        return tabbed.getSelectedComponent().getName();
    }

    /**
     * Sets the panel with the given index visible.
     * 
     * @param i
     *            an <code>int</code> value
     */
    public void setVisiblePanel(int i) {
        tabbed.setSelectedIndex(Math.min(i, tabbed.getTabCount() - 1));
    }

    public void setVisiblePanel(String name) {
        for (int i = 0; i < tabbed.getTabCount(); ++i) {
            if (name.equals(tabbed.getComponent(i).getName())) {
                tabbed.setSelectedIndex(i);
                return;
            }
        }
        if (tabbed.getTabCount() > 0)
            tabbed.setSelectedIndex(0);
    }

    /**
     * Updates this editor to show the given entry, regardless of type
     * correspondence.
     * 
     * @param be
     *            a <code>BibtexEntry</code> value
     */
    public synchronized void switchTo(BibtexEntry be) {
        if (entry == be) {
            /**
             * Even if the editor is already showing the same entry, update
             * the source panel. I'm not sure if this is the correct place to
             * do this, but in some cases the source panel will otherwise not
             * be up-to-date when an entry is changed while the entry editor
             * is existing, set to the same entry, but not visible.
             */
            // updateSource();
            return;
        }

        storeCurrentEdit();

        // Remove this instance as property listener for the entry, and register new entry as listener:
        entry.removePropertyChangeListener(this);
        be.addPropertyChangeListener(this);
        entry = be;

        updateAllFields();
        validateAllFields();
        shouldUpdateSourcePanel = true;
        updateSource();
        panel.newEntryShowing(be);

    }

    /**
     * Returns false if the contents of the source panel has not been validated,
     * true othervise.
     */
    public boolean lastSourceAccepted() {
        if (tabbed.getSelectedComponent() == srcPanel) {
            storeSource(false);
        }

        return lastSourceAccepted;
    }

    public boolean storeSource(boolean showError) {
        // Store edited bibtex code.
        BibtexParser bp = new BibtexParser(new StringReader(source.getText()));
        try {
            BibtexDatabase db = bp.parse().getDatabase();
            if (db.getEntryCount() > 1) {
                throw new RuntimeException("More than one entry found.");
            }
            if (db.getEntryCount() < 1) {
                throw new RuntimeException("No entries found.");
            }

            BibtexEntry newEntry = db.getEntryById(db.getKeySet().iterator().next());
            int id = entry.getId();
            String newKey = newEntry.getCiteKey();
            boolean hasChangesBetweenCurrentAndNew = false;
            boolean changedType = false;
            boolean duplicateWarning = false;
            boolean emptyWarning = StringUtils.isEmpty(newKey);

            if (panel.database.setCiteKeyForEntry(id, newKey)) {
                duplicateWarning = true;
            }

            NamedCompound compound = new NamedCompound(Globals.lang("source edit"));

            // First, remove fields that the user have removed (and add undo information to revert it if necessary)
            for (String fieldName : entry.getAllFields()) {
                String oldValue = entry.getField(fieldName);
                String newValue = newEntry.getField(fieldName);
                if (newValue == null) {
                    compound.addEdit(new UndoableFieldChange(entry, fieldName, oldValue, null));
                    entry.clearField(fieldName);
                    hasChangesBetweenCurrentAndNew = true;
                }
            }

            // Then set all fields that have been set by the user.
            for (String fieldName : newEntry.getAllFields()) {
                String oldValue = entry.getField(fieldName);
                String newValue = newEntry.getField(fieldName);
                if (newValue != null && !newValue.equals(oldValue)) {
                    LatexFieldFormatter lff = new LatexFieldFormatter();
                    lff.format(newValue, fieldName);
                    compound.addEdit(new UndoableFieldChange(entry, fieldName, oldValue, newValue));
                    entry.setField(fieldName, newValue);
                    hasChangesBetweenCurrentAndNew = true;
                }
            }

            // See if the user has changed the entry type:
            if (newEntry.getType() != entry.getType()) {
                compound.addEdit(new UndoableChangeType(entry, entry.getType(), newEntry.getType()));
                entry.setType(newEntry.getType());
                hasChangesBetweenCurrentAndNew = true;
                changedType = true;
            }
            compound.end();

            if (!hasChangesBetweenCurrentAndNew) {
                return true;
            }

            panel.undoManager.addEdit(compound);

            if (duplicateWarning) {
                warnDuplicateBibtexkey();
            } else if (emptyWarning && showError) {
                warnEmptyBibtexkey();
            } else {
                panel.output(Globals.lang("Stored entry") + ".");
            }

            lastAcceptedSourceString = source.getText();
            if (!changedType) {
                updateAllFields();
                lastSourceAccepted = true;
                shouldUpdateSourcePanel = true;
            } else {
                panel.updateEntryEditorIfShowing(); // We will throw away the current EntryEditor, so we do not have to update it 
            }

            // TODO: does updating work properly after source stored?
            // panel.tableModel.remap();
            // panel.entryTable.repaint();
            // panel.refreshTable();
            panel.markBaseChanged();
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    final int row = panel.mainTable.findEntry(entry);
                    if (row >= 0) {
                        //if (panel.mainTable.getSelectedRowCount() == 0)
                        //    panel.mainTable.setRowSelectionInterval(row, row);
                        //scrollTo(row);
                        panel.mainTable.ensureVisible(row);
                    }
                }
            });

            return true;
        } catch (Throwable ex) {
            ex.printStackTrace();
            // The source couldn't be parsed, so the user is given an
            // error message, and the choice to keep or revert the contents
            // of the source text field.
            shouldUpdateSourcePanel = false;
            lastSourceAccepted = false;
            tabbed.setSelectedComponent(srcPanel);

            if (showError) {
                Object[] options = { Globals.lang("Edit"), Globals.lang("Revert to original source") };

                int answer = JOptionPane.showOptionDialog(frame, Globals.lang("Error") + ": " + ex.getMessage(),
                        Globals.lang("Problem with parsing entry"), JOptionPane.YES_NO_OPTION,
                        JOptionPane.ERROR_MESSAGE, null, options, options[0]);

                if (answer != 0) {
                    shouldUpdateSourcePanel = true;
                    updateSource();
                }
            }

            return false;
        }
    }

    public void setField(String fieldName, String newFieldData) {
        for (int i = 0; i < tabbed.getTabCount(); i++) {
            Object o = tabbed.getComponentAt(i);
            if (o instanceof EntryEditorTab) {
                ((EntryEditorTab) o).updateField(fieldName, newFieldData);
            }
        }

    }

    /**
     * Sets all the text areas according to the shown entry.
     */
    public void updateAllFields() {
        for (int i = 0; i < tabbed.getTabCount(); i++) {
            Object o = tabbed.getComponentAt(i);
            if (o instanceof EntryEditorTab) {
                ((EntryEditorTab) o).setEntry(entry);
            }
        }
    }

    /**
     * Removes the "invalid field" color from all text areas.
     */
    public void validateAllFields() {
        for (int i = 0; i < tabbed.getTabCount(); i++) {
            Object o = tabbed.getComponentAt(i);
            if (o instanceof EntryEditorTab) {
                ((EntryEditorTab) o).validateAllFields();
            }
        }
    }

    public void updateAllContentSelectors() {
        if (contentSelectors.size() > 0) {
            for (Iterator<FieldContentSelector> i = contentSelectors.iterator(); i.hasNext();)
                i.next().rebuildComboBox();
        }
    }

    /**
     * Update the JTextArea when a field has changed.
     * 
     * @see java.beans.VetoableChangeListener#vetoableChange(java.beans.PropertyChangeEvent)
     */
    public void vetoableChange(PropertyChangeEvent e) {
        String newValue = ((e.getNewValue() != null) ? e.getNewValue().toString() : "");
        setField(e.getPropertyName(), newValue);
    }

    public void updateField(final Object source) {
        storeFieldAction.actionPerformed(new ActionEvent(source, 0, ""));
    }

    public void setMovingToDifferentEntry() {
        movingToDifferentEntry = true;
    }

    private class TypeLabel extends JLabel {
        public TypeLabel(String type) {
            super(type + " ");
            setUI(new VerticalLabelUI(false));
            setForeground(GUIGlobals.entryEditorLabelColor);
            setHorizontalAlignment(RIGHT);
            setFont(GUIGlobals.typeNameFont);
            addMouseListener(new MouseAdapter() {
                public void mouseClicked(MouseEvent e) {
                    boolean ctrlClick = prefs.getBoolean("ctrlClick");

                    if ((e.getButton() == MouseEvent.BUTTON3)
                            || (ctrlClick && (e.getButton() == MouseEvent.BUTTON1) && e.isControlDown())) {
                        JPopupMenu typeMenu = new JPopupMenu();

                        // typeMenu.addSeparator();
                        for (String s : BibtexEntryType.ALL_TYPES.keySet())
                            typeMenu.add(new ChangeTypeAction(BibtexEntryType.getType(s), panel));

                        typeMenu.show(ths, e.getX(), e.getY());
                    }
                }
            });
        }

        public void paintComponent(Graphics g) {
            Graphics2D g2 = (Graphics2D) g;
            //g2.setColor(GUIGlobals.entryEditorLabelColor);
            //g2.setFont(GUIGlobals.typeNameFont);
            //FontMetrics fm = g2.getFontMetrics();
            //int width = fm.stringWidth(label);
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            super.paintComponent(g2);
            //g2.rotate(-Math.PI / 2, 0, 0);
            //g2.drawString(label, -width - 7, 28);
        }
    }

    /*
     * Focus listener that fires the storeFieldAction when a FieldTextArea
     * loses focus.
     */
    class FieldListener extends FocusAdapter {
        public void focusGained(FocusEvent e) {
        }

        public void focusLost(FocusEvent e) {
            if (!e.isTemporary()) {
                updateField(e.getSource());
            } else {
                Util.pr("Lost focus temporarily at " + e.getSource().toString().substring(0, 30));
            }

        }
    }

    class TabListener implements ChangeListener {
        public void stateChanged(ChangeEvent e) {

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    activateVisible();
                }
            });

            // After the initial event train has finished, we tell the editor
            // tab to update all
            // its fields. This makes sure they are updated even if the tab we
            // just left contained one
            // or more of the same fields as this one:
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    Object activeTab = tabbed.getComponentAt(tabbed.getSelectedIndex());
                    if (activeTab instanceof EntryEditorTab)
                        ((EntryEditorTab) activeTab).updateAll();
                }
            });

        }
    }

    /*
     * Action that deletes the current entry, and closes the editor.
     */
    class DeleteAction extends AbstractAction {
        public DeleteAction() {
            super(Globals.lang("Delete"), GUIGlobals.getImage("delete"));
            putValue(SHORT_DESCRIPTION, Globals.lang("Delete entry"));
        }

        public void actionPerformed(ActionEvent e) {
            // Show confirmation dialog if not disabled:
            boolean goOn = panel.showDeleteConfirmationDialog(1);

            if (!goOn)
                return;

            panel.entryEditorClosing(EntryEditor.this);
            panel.database.removeEntry(entry.getId());
            panel.markBaseChanged();
            panel.undoManager.addEdit(new UndoableRemoveEntry(panel.database, entry, panel));
            panel.output(Globals.lang("Deleted") + " " + Globals.lang("entry"));
        }
    }

    class CloseAction extends AbstractAction {
        public CloseAction() {
            super(Globals.lang("Close window"), GUIGlobals.getImage("close"));
            putValue(SHORT_DESCRIPTION, Globals.lang("Close window"));
        }

        public void actionPerformed(ActionEvent e) {
            if (tabbed.getSelectedComponent() == srcPanel) {
                updateField(source);
                if (lastSourceAccepted)
                    panel.entryEditorClosing(EntryEditor.this);
            } else
                panel.entryEditorClosing(EntryEditor.this);
        }
    }

    public class StoreFieldAction extends AbstractAction {
        public StoreFieldAction() {
            super("Store field value");
            putValue(SHORT_DESCRIPTION, "Store field value");
        }

        public void actionPerformed(ActionEvent e) {
            boolean movingAway = movingToDifferentEntry;
            movingToDifferentEntry = false;

            if (e.getSource() instanceof FieldTextField) { // Storage of bibtex key field
                FieldTextField fe = (FieldTextField) e.getSource();
                String oldValue = entry.getCiteKey();
                String newValue = fe.getText();

                if (StringUtil.isEmpty(newValue)) {
                    newValue = null;
                }

                if (((oldValue == null) && (newValue == null))
                        || ((oldValue != null) && (newValue != null) && oldValue.equals(newValue))) {
                    return; // No change
                }

                // Make sure the key is legal:
                String cleaned = Util.checkLegalKey(newValue);
                if (cleaned != null && !cleaned.equals(newValue)) {
                    JOptionPane.showMessageDialog(frame, Globals.lang("Invalid BibTeX key"),
                            Globals.lang("Error setting field"), JOptionPane.ERROR_MESSAGE);
                    fe.setInvalidBackgroundColor();
                    return;
                } else {
                    fe.setValidBackgroundColor();
                }

                if (newValue == null) {
                    warnEmptyBibtexkey();
                }

                if (newValue != null) {
                    // Check key
                    boolean isDuplicate = panel.database.setCiteKeyForEntry(entry.getId(), newValue);
                    if (isDuplicate) {
                        warnDuplicateBibtexkey();
                    } else {
                        panel.output(Globals.lang("BibTeX key is unique."));
                    }
                    fe.setValidBackgroundColor();
                }

                // Add an UndoableKeyChange to the baseframe's undoManager.
                panel.undoManager.addEdit(new UndoableKeyChange(panel.database, entry.getId(), oldValue, newValue));

                if (fe.getTextComponent().hasFocus()) {
                    fe.setActiveBackgroundColor();
                }
                updateSource();
                panel.markBaseChanged();
            } else if (e.getSource() instanceof FieldEditor) { // Storage of any bibtex field (but not the key)
                FieldEditor fe = (FieldEditor) e.getSource();
                String newValue = fe.getText();
                String oldValue = entry.getField(fe.getFieldName());
                if (StringUtil.isEmpty(newValue)) {
                    newValue = null;
                } else {
                    newValue = newValue.trim();
                }
                boolean fieldHasChanged;

                // We check if the field has changed, since we don't want to
                // mark the base as changed unless we have a real change.
                if (newValue == null) {
                    if (entry.getField(fe.getFieldName()) == null) {
                        fieldHasChanged = false;
                    } else {
                        fieldHasChanged = true;
                    }
                } else {
                    if (entry.getField(fe.getFieldName()) != null && newValue.equals(oldValue)) {
                        fieldHasChanged = false;
                    } else {
                        fieldHasChanged = true;
                    }
                }

                if (fieldHasChanged) {
                    try {
                        // The following statement attempts to write the new contents into a 
                        // StringWriter, and this will cause an IOException if the field is not
                        // properly formatted. If that happens, the field is not stored and
                        // the textarea turns red.
                        if (newValue != null) {
                            (new LatexFieldFormatter()).format(newValue, fe.getFieldName());
                        }

                        if (newValue != null) {
                            entry.setField(fe.getFieldName(), newValue);
                        } else {
                            entry.clearField(fe.getFieldName());
                        }
                        fe.setValidBackgroundColor();

                        // See if we need to update an AutoCompleter instance:
                        AbstractAutoCompleter aComp = panel.getAutoCompleter(fe.getFieldName());
                        if (aComp != null) {
                            aComp.addBibtexEntry(entry);
                        }

                        // Add an UndoableFieldChange to the baseframe's undoManager.
                        panel.undoManager
                                .addEdit(new UndoableFieldChange(entry, fe.getFieldName(), oldValue, newValue));
                        updateSource();
                        panel.markBaseChanged();
                    } catch (IllegalArgumentException ex) {
                        JOptionPane.showMessageDialog(frame, Globals.lang("Error") + ": " + ex.getMessage(),
                                Globals.lang("Error setting field"), JOptionPane.ERROR_MESSAGE);
                        fe.setInvalidBackgroundColor();
                    }
                } else {
                    fe.setValidBackgroundColor();
                }
                if (fe.getTextComponent().hasFocus()) {
                    fe.setBackground(GUIGlobals.activeEditor);
                }
            } else if (source.isEditable() && (!source.getText().equals(lastAcceptedSourceString))) {
                boolean accepted = storeSource(true);
                if (!accepted) {
                    System.out.println("Error using updated data from BibTeX source");
                }
            }

            ////////////////////////////////////
            // Make sure we scroll to the entry if it moved in the table.
            // Should only be done if this editor is currently showing.
            if (!movingAway && isShowing()) {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        final int row = panel.mainTable.findEntry(entry);
                        if (row >= 0) {
                            panel.mainTable.ensureVisible(row);
                        }
                    }
                });
            }
        }
    }

    class SwitchLeftAction extends AbstractAction {
        public SwitchLeftAction() {
            super("Switch to the panel to the left");
        }

        public void actionPerformed(ActionEvent e) {
            // System.out.println("switch left");
            int i = tabbed.getSelectedIndex();
            tabbed.setSelectedIndex(((i > 0) ? (i - 1) : (tabbed.getTabCount() - 1)));

            activateVisible();
        }
    }

    class SwitchRightAction extends AbstractAction {
        public SwitchRightAction() {
            super("Switch to the panel to the right");
        }

        public void actionPerformed(ActionEvent e) {
            // System.out.println("switch right");
            int i = tabbed.getSelectedIndex();
            tabbed.setSelectedIndex((i < (tabbed.getTabCount() - 1)) ? (i + 1) : 0);
            activateVisible();

        }
    }

    class NextEntryAction extends AbstractAction {
        public NextEntryAction() {
            super(Globals.lang("Next entry"), GUIGlobals.getImage("down"));

            putValue(SHORT_DESCRIPTION, Globals.lang("Next entry"));
        }

        public void actionPerformed(ActionEvent e) {

            int thisRow = panel.mainTable.findEntry(entry);
            int newRow = -1;

            if ((thisRow + 1) < panel.database.getEntryCount())
                newRow = thisRow + 1;
            else if (thisRow > 0)
                newRow = 0;
            else
                return; // newRow is still -1, so we can assume the database has
                        // only one entry.

            scrollTo(newRow);
            panel.mainTable.setRowSelectionInterval(newRow, newRow);

        }
    }

    class PrevEntryAction extends AbstractAction {
        public PrevEntryAction() {
            super(Globals.lang("Previous entry"), GUIGlobals.getImage("up"));

            putValue(SHORT_DESCRIPTION, Globals.lang("Previous entry"));
        }

        public void actionPerformed(ActionEvent e) {
            int thisRow = panel.mainTable.findEntry(entry);
            int newRow = -1;

            if ((thisRow - 1) >= 0)
                newRow = thisRow - 1;
            else if (thisRow != (panel.database.getEntryCount() - 1))
                newRow = panel.database.getEntryCount() - 1;
            else
                return; // newRow is still -1, so we can assume the database has
                        // only one entry.
                        // id = panel.tableModel.getIdForRow(newRow);
                        // switchTo(id);

            scrollTo(newRow);
            panel.mainTable.setRowSelectionInterval(newRow, newRow);

        }
    }

    class UndoAction extends AbstractAction {
        public UndoAction() {
            super("Undo", GUIGlobals.getImage("undo"));
            putValue(SHORT_DESCRIPTION, "Undo");
        }

        public void actionPerformed(ActionEvent e) {
            try {
                panel.runCommand("undo");
            } catch (Throwable ex) {
            }
        }
    }

    class RedoAction extends AbstractAction {
        public RedoAction() {
            super("Undo", GUIGlobals.getImage("redo"));
            putValue(SHORT_DESCRIPTION, "Redo");
        }

        public void actionPerformed(ActionEvent e) {
            try {
                panel.runCommand("redo");
            } catch (Throwable ex) {
            }
        }
    }

    class SaveDatabaseAction extends AbstractAction {
        public SaveDatabaseAction() {
            super("Save database");
        }

        public void actionPerformed(ActionEvent e) {
            Object activeTab = tabbed.getComponentAt(tabbed.getSelectedIndex());
            if (activeTab instanceof EntryEditorTab) {
                // Normal panel.
                EntryEditorTab fp = (EntryEditorTab) activeTab;
                FieldEditor fe = fp.getActive();
                fe.clearAutoCompleteSuggestion();
                updateField(fe);
            } else {
                // Source panel.
                updateField(activeTab);
            }

            try {
                panel.runCommand("save");
            } catch (Throwable ex) {
                System.err.println("Error saving file.");
            }
        }
    }

    class ExternalViewerListener extends MouseAdapter {
        public void mouseClicked(MouseEvent evt) {
            if (evt.getClickCount() == 2) {
                FieldTextArea tf = (FieldTextArea) evt.getSource();

                if (tf.getText().equals(""))
                    return;

                tf.selectAll();

                String link = tf.getText(); // get selected ? String

                // getSelectedText()
                try {
                    Util.openExternalViewer(panel.metaData(), link, tf.getFieldName());
                } catch (IOException ex) {
                    System.err.println("Error opening file.");
                }
            }
        }
    }

    class ChangeTypeAction extends AbstractAction {
        BibtexEntryType type;

        BasePanel panel;

        public ChangeTypeAction(BibtexEntryType type, BasePanel bp) {
            super(type.getName());
            this.type = type;
            panel = bp;
        }

        public void actionPerformed(ActionEvent evt) {
            panel.changeType(entry, type);
        }
    }

    private void warnDuplicateBibtexkey() {
        panel.output(Globals.lang("Duplicate BibTeX key. Grouping may not work for this entry."));
    }

    private void warnEmptyBibtexkey() {
        panel.output(Globals.lang("Empty BibTeX key. Grouping may not work for this entry."));
    }
}