package org.jabref.gui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.DefaultCellEditor;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.LayoutFocusTraversalPolicy;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumnModel;
import javax.swing.undo.CompoundEdit;
import org.jabref.Globals;
import org.jabref.gui.actions.Actions;
import org.jabref.gui.help.HelpAction;
import org.jabref.gui.keyboard.KeyBinding;
import org.jabref.gui.undo.UndoableInsertString;
import org.jabref.gui.undo.UndoableRemoveString;
import org.jabref.gui.undo.UndoableStringChange;
import org.jabref.gui.util.WindowLocation;
import org.jabref.logic.bibtex.InvalidFieldValueException;
import org.jabref.logic.bibtex.LatexFieldFormatter;
import org.jabref.logic.bibtex.comparator.BibtexStringComparator;
import org.jabref.logic.help.HelpFile;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.KeyCollisionException;
import org.jabref.model.entry.BibtexString;
import org.jabref.preferences.JabRefPreferences;
class StringDialog extends JabRefDialog {
private static final String STRINGS_TITLE = Localization.lang("Strings for library");
// A reference to the entry this object works on.
private final BibDatabase base;
private final BasePanel panel;
private final StringTable table;
private final HelpAction helpAction;
private final SaveDatabaseAction saveAction = new SaveDatabaseAction(this);
// The action concerned with closing the window.
private final CloseAction closeAction = new CloseAction();
private List<BibtexString> strings;
public StringDialog(JabRefFrame frame, BasePanel panel, BibDatabase base) {
super(frame, StringDialog.class);
this.panel = panel;
this.base = base;
sortStrings();
helpAction = new HelpAction(Localization.lang("Help"), HelpFile.STRING_EDITOR);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
closeAction.actionPerformed(null);
}
});
// We replace the default FocusTraversalPolicy with a subclass
// that only allows the StringTable to gain keyboard focus.
setFocusTraversalPolicy(new LayoutFocusTraversalPolicy() {
@Override
protected boolean accept(Component c) {
return super.accept(c) && (c instanceof StringTable);
}
});
JPanel pan = new JPanel();
GridBagLayout gbl = new GridBagLayout();
pan.setLayout(gbl);
GridBagConstraints con = new GridBagConstraints();
con.fill = GridBagConstraints.BOTH;
con.weighty = 1;
con.weightx = 1;
StringTableModel stm = new StringTableModel(this, base);
table = new StringTable(stm);
if (!base.hasNoStrings()) {
table.setRowSelectionInterval(0, 0);
}
gbl.setConstraints(table.getPane(), con);
pan.add(table.getPane());
JToolBar tlb = new OSXCompatibleToolbar();
InputMap im = tlb.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap am = tlb.getActionMap();
im.put(Globals.getKeyPrefs().getKey(KeyBinding.STRING_DIALOG_ADD_STRING), "add");
NewStringAction newStringAction = new NewStringAction(this);
am.put("add", newStringAction);
im.put(Globals.getKeyPrefs().getKey(KeyBinding.STRING_DIALOG_REMOVE_STRING), "remove");
RemoveStringAction removeStringAction = new RemoveStringAction(this);
am.put("remove", removeStringAction);
im.put(Globals.getKeyPrefs().getKey(KeyBinding.SAVE_DATABASE), "save");
am.put("save", saveAction);
im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close");
am.put("close", closeAction);
im.put(Globals.getKeyPrefs().getKey(KeyBinding.HELP), "help");
am.put("help", helpAction);
im.put(Globals.getKeyPrefs().getKey(KeyBinding.UNDO), "undo");
UndoAction undoAction = new UndoAction();
am.put("undo", undoAction);
im.put(Globals.getKeyPrefs().getKey(KeyBinding.REDO), "redo");
RedoAction redoAction = new RedoAction();
am.put("redo", redoAction);
tlb.add(newStringAction);
tlb.add(removeStringAction);
tlb.addSeparator();
tlb.add(helpAction);
Container conPane = getContentPane();
conPane.add(tlb, BorderLayout.NORTH);
conPane.add(pan, BorderLayout.CENTER);
setTitle(STRINGS_TITLE + ": "
+ panel.getBibDatabaseContext().getDatabaseFile().map(File::getName).orElse(GUIGlobals.UNTITLED_TITLE));
WindowLocation pw = new WindowLocation(this, JabRefPreferences.STRINGS_POS_X, JabRefPreferences.STRINGS_POS_Y,
JabRefPreferences.STRINGS_SIZE_X, JabRefPreferences.STRINGS_SIZE_Y);
pw.displayWindowAtStoredLocation();
}
private static boolean isNumber(String name) {
// A pure integer number cannot be used as a string label,
// since Bibtex will read it as a number.
try {
Integer.parseInt(name);
return true;
} catch (NumberFormatException ex) {
return false;
}
}
private void sortStrings() {
// Rebuild our sorted set of strings:
strings = new ArrayList<>();
for (String s : base.getStringKeySet()) {
strings.add(base.getString(s));
}
Collections.sort(strings, new BibtexStringComparator(false));
}
public void refreshTable() {
sortStrings();
table.revalidate();
table.clearSelection();
table.repaint();
}
public void saveDatabase() {
panel.runCommand(Actions.SAVE);
}
public void assureNotEditing() {
if (table.isEditing()) {
int col = table.getEditingColumn();
int row = table.getEditingRow();
table.getCellEditor(row, col).stopCellEditing();
}
}
static class SaveDatabaseAction extends AbstractAction {
private final StringDialog parent;
public SaveDatabaseAction(StringDialog parent) {
super("Save library", IconTheme.JabRefIcon.SAVE.getIcon());
putValue(Action.SHORT_DESCRIPTION, Localization.lang("Save library"));
this.parent = parent;
}
@Override
public void actionPerformed(ActionEvent e) {
parent.saveDatabase();
}
}
class StringTable extends JTable {
private final JScrollPane sp = new JScrollPane(this);
public StringTable(StringTableModel stm) {
super(stm);
setShowVerticalLines(true);
setShowHorizontalLines(true);
setColumnSelectionAllowed(true);
DefaultCellEditor dce = new DefaultCellEditor(new JTextField());
dce.setClickCountToStart(2);
setDefaultEditor(String.class, dce);
TableColumnModel cm = getColumnModel();
cm.getColumn(0).setPreferredWidth(800);
cm.getColumn(1).setPreferredWidth(2000);
sp.getViewport().setBackground(Globals.prefs.getColor(JabRefPreferences.TABLE_BACKGROUND));
getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close");
getActionMap().put("close", closeAction);
getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.HELP), "help");
getActionMap().put("help", helpAction);
}
public JComponent getPane() {
return sp;
}
}
class StringTableModel extends AbstractTableModel {
private final BibDatabase tbase;
private final StringDialog parent;
public StringTableModel(StringDialog parent, BibDatabase base) {
this.parent = parent;
this.tbase = base;
}
@Override
public Object getValueAt(int row, int col) {
return col == 0 ? strings.get(row).getName() : strings.get(row).getContent();
}
@Override
public void setValueAt(Object value, int row, int col) {
if (col == 0) {
// Change name of string.
if (!value.equals(strings.get(row).getName())) {
if (tbase.hasStringLabel((String) value)) {
JOptionPane.showMessageDialog(parent, Localization.lang("A string with that label already exists"),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
} else if (((String) value).contains(" ")) {
JOptionPane.showMessageDialog(parent, Localization.lang("The label of the string cannot contain spaces."),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
} else if (((String) value).contains("#")) {
JOptionPane.showMessageDialog(parent, Localization.lang("The label of the string cannot contain the '#' character."),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
} else if (isNumber((String) value)) {
JOptionPane.showMessageDialog(parent, Localization.lang("The label of the string cannot be a number."),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
} else {
// Store undo information.
BibtexString subject = strings.get(row);
panel.getUndoManager().addEdit(
new UndoableStringChange(panel, subject, true, subject.getName(), (String) value));
subject.setName((String) value);
panel.markBaseChanged();
refreshTable();
}
}
} else {
// Change content of string.
BibtexString subject = strings.get(row);
if (!value.equals(subject.getContent())) {
try {
new LatexFieldFormatter(Globals.prefs.getLatexFieldFormatterPreferences())
.format((String) value, "__dummy");
} catch (InvalidFieldValueException ex) {
return;
}
// Store undo information.
panel.getUndoManager().addEdit(
new UndoableStringChange(panel, subject, false, subject.getContent(), (String) value));
subject.setContent((String) value);
panel.markBaseChanged();
}
}
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public int getRowCount() {
return strings.size();
}
@Override
public String getColumnName(int col) {
return col == 0 ? Localization.lang("Label") :
Localization.lang("Content");
}
@Override
public boolean isCellEditable(int row, int col) {
return true;
}
}
class CloseAction extends AbstractAction {
public CloseAction() {
super("Close window");
putValue(Action.SHORT_DESCRIPTION, Localization.lang("Close dialog"));
}
@Override
public void actionPerformed(ActionEvent e) {
panel.stringsClosing();
dispose();
}
}
class NewStringAction extends AbstractAction {
private final StringDialog parent;
public NewStringAction(StringDialog parent) {
super("New string", IconTheme.JabRefIcon.ADD.getIcon());
putValue(Action.SHORT_DESCRIPTION, Localization.lang("New string"));
this.parent = parent;
}
@Override
public void actionPerformed(ActionEvent e) {
String name = JOptionPane.showInputDialog(parent, Localization.lang("Please enter the string's label"));
if (name == null) {
return;
}
if (isNumber(name)) {
JOptionPane.showMessageDialog(parent, Localization.lang("The label of the string cannot be a number."),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
return;
}
if (name.contains("#")) {
JOptionPane.showMessageDialog(parent, Localization.lang("The label of the string cannot contain the '#' character."),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
return;
}
if (name.contains(" ")) {
JOptionPane.showMessageDialog(parent, Localization.lang("The label of the string cannot contain spaces."),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
return;
}
try {
BibtexString bs = new BibtexString(name, "");
// Store undo information:
panel.getUndoManager().addEdit(new UndoableInsertString(panel, panel.getDatabase(), bs));
base.addString(bs);
refreshTable();
panel.markBaseChanged();
} catch (KeyCollisionException ex) {
JOptionPane.showMessageDialog(parent,
Localization.lang("A string with that label already exists"),
Localization.lang("Label"), JOptionPane.ERROR_MESSAGE);
}
}
}
class RemoveStringAction extends AbstractAction {
private final StringDialog parent;
public RemoveStringAction(StringDialog parent) {
super("Remove selected strings", IconTheme.JabRefIcon.REMOVE.getIcon());
putValue(Action.SHORT_DESCRIPTION, Localization.lang("Remove selected strings"));
this.parent = parent;
}
@Override
public void actionPerformed(ActionEvent e) {
int[] sel = table.getSelectedRows();
if (sel.length > 0) {
// Make sure no cell is being edited, as caused by the
// keystroke. This makes the content hang on the screen.
assureNotEditing();
String msg = (sel.length > 1 ? Localization.lang("Really delete the %0 selected entries?",
Integer.toString(sel.length)) : Localization.lang("Really delete the selected entry?"));
int answer = JOptionPane.showConfirmDialog(parent, msg, Localization.lang("Delete strings"),
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if (answer == JOptionPane.YES_OPTION) {
CompoundEdit ce = new CompoundEdit();
for (int i = sel.length - 1; i >= 0; i--) {
// Delete the strings backwards to avoid moving indexes.
BibtexString subject = strings.get(sel[i]);
// Store undo information:
ce.addEdit(new UndoableRemoveString(panel, base, subject));
base.removeString(subject.getId());
}
ce.end();
panel.getUndoManager().addEdit(ce);
refreshTable();
if (!base.hasNoStrings()) {
table.setRowSelectionInterval(0, 0);
}
}
}
}
}
class UndoAction extends AbstractAction {
public UndoAction() {
super("Undo", IconTheme.JabRefIcon.UNDO.getIcon());
putValue(Action.SHORT_DESCRIPTION, Localization.lang("Undo"));
}
@Override
public void actionPerformed(ActionEvent e) {
panel.runCommand(Actions.UNDO);
}
}
class RedoAction extends AbstractAction {
public RedoAction() {
super("Redo", IconTheme.JabRefIcon.REDO.getIcon());
putValue(Action.SHORT_DESCRIPTION, Localization.lang("Redo"));
}
@Override
public void actionPerformed(ActionEvent e) {
panel.runCommand(Actions.REDO);
}
}
}