package org.jabref.gui.openoffice;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.table.TableColumnModel;
import org.jabref.Globals;
import org.jabref.gui.DialogService;
import org.jabref.gui.FXDialogService;
import org.jabref.gui.IconTheme;
import org.jabref.gui.JabRefDialog;
import org.jabref.gui.JabRefFrame;
import org.jabref.gui.PreviewPanel;
import org.jabref.gui.desktop.JabRefDesktop;
import org.jabref.gui.externalfiletype.ExternalFileType;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.externalfiletype.UnknownExternalFileType;
import org.jabref.gui.keyboard.KeyBinding;
import org.jabref.gui.util.DefaultTaskExecutor;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.gui.util.WindowLocation;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.openoffice.OOBibStyle;
import org.jabref.logic.openoffice.OpenOfficePreferences;
import org.jabref.logic.openoffice.StyleLoader;
import org.jabref.logic.util.FileExtensions;
import org.jabref.logic.util.TestEntry;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.preferences.JabRefPreferences;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.swing.DefaultEventSelectionModel;
import ca.odell.glazedlists.swing.DefaultEventTableModel;
import ca.odell.glazedlists.swing.GlazedListsSwing;
import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.FormBuilder;
import com.jgoodies.forms.layout.FormLayout;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* This class produces a dialog box for choosing a style file.
*/
class StyleSelectDialog {
private static final Log LOGGER = LogFactory.getLog(StyleSelectDialog.class);
private final JabRefFrame frame;
private EventList<OOBibStyle> styles;
private JDialog diag;
private JTable table;
private DefaultEventTableModel<OOBibStyle> tableModel;
private DefaultEventSelectionModel<OOBibStyle> selectionModel;
private final JPopupMenu popup = new JPopupMenu();
private final JMenuItem edit = new JMenuItem(Localization.lang("Edit"));
private final JMenuItem show = new JMenuItem(Localization.lang("View"));
private final JMenuItem remove = new JMenuItem(Localization.lang("Remove"));
private final JMenuItem reload = new JMenuItem(Localization.lang("Reload"));
private final JButton addButton = new JButton(IconTheme.JabRefIcon.ADD_NOBOX.getIcon());
private final JButton removeButton = new JButton(IconTheme.JabRefIcon.REMOVE_NOBOX.getIcon());
private PreviewPanel preview;
private ActionListener removeAction;
private final Rectangle toRect = new Rectangle(0, 0, 1, 1);
private final JButton ok = new JButton(Localization.lang("OK"));
private final JButton cancel = new JButton(Localization.lang("Cancel"));
private final BibEntry prevEntry;
private boolean okPressed;
private final StyleLoader loader;
private final OpenOfficePreferences preferences;
public StyleSelectDialog(JabRefFrame frame, OpenOfficePreferences preferences, StyleLoader loader) {
this.frame = Objects.requireNonNull(frame);
this.preferences = Objects.requireNonNull(preferences);
this.loader = Objects.requireNonNull(loader);
prevEntry = TestEntry.getTestEntry();
init();
}
private void init() {
setupPopupMenu();
addButton.addActionListener(actionEvent -> {
AddFileDialog addDialog = new AddFileDialog();
addDialog.setDirectoryPath(preferences.getCurrentStyle());
addDialog.setVisible(true);
addDialog.getFileName().ifPresent(fileName -> {
if (loader.addStyleIfValid(fileName)) {
preferences.setCurrentStyle(fileName);
}
});
updateStyles();
});
addButton.setToolTipText(Localization.lang("Add style file"));
removeButton.addActionListener(removeAction);
removeButton.setToolTipText(Localization.lang("Remove style"));
// Create a preview panel for previewing styles
// Must be done before creating the table to avoid NPEs
preview = new PreviewPanel(null, null);
// Use the test entry from the Preview settings tab in Preferences:
preview.setEntry(prevEntry);
setupTable();
updateStyles();
// Build dialog
diag = new JDialog(frame, Localization.lang("Select style"), true);
FormBuilder builder = FormBuilder.create();
builder.layout(new FormLayout("fill:pref:grow, 4dlu, left:pref, 4dlu, left:pref",
"pref, 4dlu, 100dlu:grow, 4dlu, pref, 4dlu, fill:100dlu"));
builder.add(Localization.lang("Select one of the available styles or add a style file from disk.")).xyw(1, 1,
5);
builder.add(new JScrollPane(table)).xyw(1, 3, 5);
builder.add(addButton).xy(3, 5);
builder.add(removeButton).xy(5, 5);
builder.add(preview).xyw(1, 7, 5);
builder.padding("5dlu, 5dlu, 5dlu, 5dlu");
diag.add(builder.getPanel(), BorderLayout.CENTER);
AbstractAction okListener = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent event) {
if ((table.getRowCount() == 0) || (table.getSelectedRowCount() == 0)) {
JOptionPane.showMessageDialog(diag, Localization.lang("You must select a valid style file."),
Localization.lang("Style selection"), JOptionPane.ERROR_MESSAGE);
return;
}
okPressed = true;
storeSettings();
diag.dispose();
}
};
ok.addActionListener(okListener);
Action cancelListener = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent event) {
diag.dispose();
}
};
cancel.addActionListener(cancelListener);
ButtonBarBuilder bb = new ButtonBarBuilder();
bb.addGlue();
bb.addButton(ok);
bb.addButton(cancel);
bb.addGlue();
bb.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
diag.add(bb.getPanel(), BorderLayout.SOUTH);
ActionMap am = bb.getPanel().getActionMap();
InputMap im = bb.getPanel().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close");
am.put("close", cancelListener);
im.put(KeyStroke.getKeyStroke("ENTER"), "enterOk");
am.put("enterOk", okListener);
diag.pack();
WindowLocation pw = new WindowLocation(diag, JabRefPreferences.STYLES_POS_X, JabRefPreferences.STYLES_POS_Y,
JabRefPreferences.STYLES_SIZE_X, JabRefPreferences.STYLES_SIZE_Y);
pw.displayWindowAtStoredLocation();
}
private void setupTable() {
styles = new BasicEventList<>();
EventList<OOBibStyle> sortedStyles = new SortedList<>(styles);
tableModel = (DefaultEventTableModel<OOBibStyle>) GlazedListsSwing
.eventTableModelWithThreadProxyList(sortedStyles, new StyleTableFormat());
table = new JTable(tableModel);
TableColumnModel cm = table.getColumnModel();
cm.getColumn(0).setPreferredWidth(100);
cm.getColumn(1).setPreferredWidth(200);
cm.getColumn(2).setPreferredWidth(80);
selectionModel = (DefaultEventSelectionModel<OOBibStyle>) GlazedListsSwing
.eventSelectionModelWithThreadProxyList(sortedStyles);
table.setSelectionModel(selectionModel);
table.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent mouseEvent) {
if (mouseEvent.isPopupTrigger()) {
tablePopup(mouseEvent);
}
}
@Override
public void mouseReleased(MouseEvent mouseEvent) {
if (mouseEvent.isPopupTrigger()) {
tablePopup(mouseEvent);
}
}
});
selectionModel.getSelected().addListEventListener(new EntrySelectionListener());
}
private void setupPopupMenu() {
popup.add(edit);
popup.add(show);
popup.add(remove);
popup.add(reload);
// Add action listener to "Edit" menu item, which is supposed to open the style file in an external editor:
edit.addActionListener(actionEvent -> getSelectedStyle().ifPresent(style -> {
Optional<ExternalFileType> type = ExternalFileTypes.getInstance().getExternalFileTypeByExt("jstyle");
String link = style.getPath();
try {
if (type.isPresent()) {
JabRefDesktop.openExternalFileAnyFormat(new BibDatabaseContext(), link, type);
} else {
JabRefDesktop.openExternalFileUnknown(frame, new BibEntry(), new BibDatabaseContext(), link,
new UnknownExternalFileType("jstyle"));
}
} catch (IOException e) {
LOGGER.warn("Problem open style file editor", e);
}
}));
// Add action listener to "Show" menu item, which is supposed to open the style file in a dialog:
show.addActionListener(actionEvent -> getSelectedStyle().ifPresent(this::displayStyle));
// Create action listener for removing a style, also used for the remove button
removeAction = actionEvent -> getSelectedStyle().ifPresent(style -> {
if (!style.isFromResource() && (JOptionPane.showConfirmDialog(diag,
Localization.lang("Are you sure you want to remove the style?"), Localization.lang("Remove style"),
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)) {
if (!loader.removeStyle(style)) {
LOGGER.info("Problem removing style");
}
updateStyles();
}
});
// Add it to the remove menu item
remove.addActionListener(removeAction);
// Add action listener to the "Reload" menu item, which is supposed to reload an external style file
reload.addActionListener(actionEvent -> getSelectedStyle().ifPresent(style -> {
try {
style.ensureUpToDate();
} catch (IOException e) {
LOGGER.warn("Problem with style file '" + style.getPath() + "'", e);
}
}));
}
public void setVisible(boolean visible) {
okPressed = false;
diag.setVisible(visible);
}
/**
* Read all style files or directories of style files indicated by the current
* settings, and add the styles to the list of styles.
*/
private void updateStyles() {
table.clearSelection();
styles.getReadWriteLock().writeLock().lock();
styles.clear();
styles.addAll(loader.getStyles());
styles.getReadWriteLock().writeLock().unlock();
selectLastUsed();
}
/**
* This method scans the current list of styles, and looks for the styles
* that was last used. If found, that style is selected. If not found,
* the first style is selected provided there are >0 styles.
*/
private void selectLastUsed() {
String usedStyleFile = preferences.getCurrentStyle();
// Set the initial selection of the table:
if (usedStyleFile == null) {
if (table.getRowCount() > 0) {
table.setRowSelectionInterval(0, 0);
}
} else {
boolean found = false;
for (int i = 0; i < table.getRowCount(); i++) {
if (usedStyleFile.equals(tableModel.getElementAt(i).getPath())) {
table.setRowSelectionInterval(i, i);
found = true;
break;
}
}
if (!found && (table.getRowCount() > 0)) {
table.setRowSelectionInterval(0, 0);
}
}
}
private void storeSettings() {
getSelectedStyle().ifPresent(style -> preferences.setCurrentStyle(style.getPath()));
}
public Optional<OOBibStyle> getStyle() {
if (okPressed) {
return getSelectedStyle();
}
return Optional.empty();
}
/**
* Get the currently selected style.
* @return the selected style, or empty if no style is selected.
*/
private Optional<OOBibStyle> getSelectedStyle() {
if (!selectionModel.getSelected().isEmpty()) {
return Optional.of(selectionModel.getSelected().get(0));
}
return Optional.empty();
}
static class StyleTableFormat implements TableFormat<OOBibStyle> {
@Override
public int getColumnCount() {
return 3;
}
@Override
public String getColumnName(int i) {
switch (i) {
case 0:
return Localization.lang("Name");
case 1:
return Localization.lang("Journals");
case 2:
return Localization.lang("File");
default:
return "";
}
}
@Override
public Object getColumnValue(OOBibStyle style, int i) {
switch (i) {
case 0:
return style.getName();
case 1:
return String.join(", ", style.getJournals());
case 2:
return style.isFromResource() ? Localization.lang("Internal style") : style.getFile().getName();
default:
return "";
}
}
}
public boolean isOkPressed() {
return okPressed;
}
private void tablePopup(MouseEvent e) {
popup.show(e.getComponent(), e.getX(), e.getY());
}
private void displayStyle(OOBibStyle style) {
// Make a dialog box to display the contents:
final JDialog dd = new JDialog(diag, style.getName(), true);
JTextArea ta = new JTextArea(style.getLocalCopy());
ta.setEditable(false);
JScrollPane sp = new JScrollPane(ta);
sp.setPreferredSize(new Dimension(700, 500));
dd.getContentPane().add(sp, BorderLayout.CENTER);
JButton okButton = new JButton(Localization.lang("OK"));
ButtonBarBuilder bb = new ButtonBarBuilder();
bb.addGlue();
bb.addButton(okButton);
bb.addGlue();
bb.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
dd.getContentPane().add(bb.getPanel(), BorderLayout.SOUTH);
okButton.addActionListener(actionEvent -> dd.dispose());
dd.pack();
dd.setLocationRelativeTo(diag);
dd.setVisible(true);
}
/**
* The listener for the Glazed list monitoring the current selection.
* When selection changes, we need to update the preview panel.
*/
private class EntrySelectionListener implements ListEventListener<OOBibStyle> {
@Override
public void listChanged(ListEvent<OOBibStyle> listEvent) {
if (listEvent.getSourceList().size() == 1) {
OOBibStyle style = listEvent.getSourceList().get(0);
// Enable/disable popup menu items and buttons
if (style.isFromResource()) {
remove.setEnabled(false);
edit.setEnabled(false);
reload.setEnabled(false);
removeButton.setEnabled(false);
} else {
remove.setEnabled(true);
edit.setEnabled(true);
reload.setEnabled(true);
removeButton.setEnabled(true);
}
// Set new preview layout
preview.setLayout(style.getReferenceFormat("default"));
// Update the preview's entry:
SwingUtilities.invokeLater(() -> {
preview.update();
preview.scrollRectToVisible(toRect);
});
}
}
}
private class AddFileDialog extends JabRefDialog {
private final JTextField newFile = new JTextField();
private boolean addOKPressed;
public AddFileDialog() {
super(diag, Localization.lang("Add style file"), true, AddFileDialog.class);
JButton browse = new JButton(Localization.lang("Browse"));
FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
.addExtensionFilter(FileExtensions.JSTYLE)
.withDefaultExtension(FileExtensions.JSTYLE)
.withInitialDirectory(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)).build();
DialogService ds = new FXDialogService();
browse.addActionListener(e -> {
Optional<Path> file = DefaultTaskExecutor
.runInJavaFXThread(() -> ds.showFileOpenDialog(fileDialogConfiguration));
file.ifPresent(f -> newFile.setText(f.toAbsolutePath().toString()));
});
// Build content panel
FormBuilder builder = FormBuilder.create();
builder.layout(new FormLayout("left:pref, 4dlu, fill:100dlu:grow, 4dlu, pref", "p"));
builder.add(Localization.lang("File")).xy(1, 1);
builder.add(newFile).xy(3, 1);
builder.add(browse).xy(5, 1);
builder.padding("10dlu, 10dlu, 10dlu, 10dlu");
getContentPane().add(builder.build(), BorderLayout.CENTER);
// Buttons
ButtonBarBuilder bb = new ButtonBarBuilder();
JButton addOKButton = new JButton(Localization.lang("OK"));
JButton addCancelButton = new JButton(Localization.lang("Cancel"));
bb.addGlue();
bb.addButton(addOKButton);
bb.addButton(addCancelButton);
bb.addGlue();
bb.getPanel().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
getContentPane().add(bb.getPanel(), BorderLayout.SOUTH);
addOKButton.addActionListener(e -> {
addOKPressed = true;
dispose();
});
Action cancelAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
addOKPressed = false;
dispose();
}
};
addCancelButton.addActionListener(cancelAction);
// Key bindings:
bb.getPanel().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close");
bb.getPanel().getActionMap().put("close", cancelAction);
pack();
setLocationRelativeTo(diag);
}
public Optional<String> getFileName() {
if (addOKPressed && (newFile.getText() != null) && !newFile.getText().isEmpty()) {
return Optional.of(newFile.getText());
}
return Optional.empty();
}
public void setDirectoryPath(String path) {
this.newFile.setText(path);
}
}
}