package gui.actions;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Queue;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.DefaultTableModel;
import gui.utils.GUIErrorHandler;
import gui.views.OptionsDialog;
import gui.views.components.ReplacementsTable;
import hexcapture.HexOptions;
import hexcapture.HexPipeCompleter;
import hextostring.replacement.Replacement;
import hextostring.replacement.ReplacementType;
import hextostring.replacement.Replacements;
import main.MainOptions;
import main.options.Options;
import main.utils.ReflectionUtils;
/**
* Contains all the actions for the view OptionsDialog.
*
* @author Maxime PIA
*/
public class OptionsDialogActions {
// Equivalent of runnable that can throw exceptions.
private interface Command {
void execute() throws Exception;
}
// Undoable command pattern.
private interface ModificationAction {
void apply() throws Exception;
void cancel() throws Exception;
}
private static final Queue<ModificationAction> optionModifications =
new LinkedList<>();
private static final HexPipeCompleter ccw =
new HexPipeCompleter();
private static boolean hexOptionsModified = false;
private OptionsDialog optionsDialog;
public OptionsDialogActions(OptionsDialog optionsDialog) {
this.optionsDialog = optionsDialog;
}
/**
* Sets the close action for the dialog.
*/
public void setCloseAction() {
optionsDialog.setDefaultCloseOperation(
WindowConstants.DO_NOTHING_ON_CLOSE
);
optionsDialog.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
if (optionModifications.size() == 0 ||
JOptionPane.showConfirmDialog(
optionsDialog,
"Do you wish to discard the changes?",
"Closing options dialog",
JOptionPane.YES_NO_OPTION
) == JOptionPane.YES_OPTION) {
closeWindow();
}
}
});
}
/**
* Sets the action for a checkbox representing the state of a flag.
*
* @param flagCheckBox
* The checkbox.
* @param flagField
* The field containing the flags.
* @param valueField
* The field representing the flag.
* @param opts
* The option object containing the field containing the flags.
*/
public void setFlagCheckboxAction(final JCheckBox flagCheckBox,
final Field flagField, final Field valueField, final Options opts) {
final Method flagSetter =
ReflectionUtils.getFlagSetter(opts.getClass(), flagField);
final Method flagGetter =
ReflectionUtils.getGetter(opts.getClass(), flagField);
OptionChangeListener flagCheckboxListener = new OptionChangeListener(
flagCheckBox, opts, new Command() {
@Override
public void execute() throws Exception {
flagSetter.invoke(
opts,
valueField.getLong(null),
flagCheckBox.isSelected()
);
}
}, new Command() {
@Override
public void execute() throws Exception {
flagCheckBox.setSelected(
((long) flagGetter.invoke(opts)
& valueField.getLong(null)) > 0
);
}
}
);
flagCheckBox.addActionListener(flagCheckboxListener);
}
/**
* Sets the action for a component representing an option.
*
* @param elt
* The component.
* @param optionField
* The field representing an option.
* @param opts
* The option object containing the field representing an option.
*/
@SuppressWarnings("rawtypes")
public void setOptionComponentAction(JComponent elt, Field optionField,
Options opts) {
OptionChangeListener genericListener =
new OptionChangeListener(elt, opts, optionField);
if (elt instanceof JCheckBox) {
((JCheckBox) elt).addActionListener(genericListener);
} else if (elt instanceof JComboBox) {
((JComboBox) elt).addActionListener(genericListener);
} else if (elt instanceof JSpinner) {
((JSpinner) elt).addChangeListener(genericListener);
} else {
throw new IllegalArgumentException(
"Invalid option modifier component: " + elt
);
}
}
/**
* Sets the action for the "apply" button.
*
* @param applyButton
* The button.
* @param opts
* The option object affected by the modifications.
*/
public void setApplyButtonAction(JButton applyButton,
final MainOptions opts) {
applyButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent ev) {
applyAllOptionModifications(opts);
}
});
}
/**
* Sets the action for the "ok" button.
*
* @param okButton
* The button.
* @param opts
* The option object affected by the modifications.
*/
public void setOKButtonAction(JButton okButton, final MainOptions opts) {
okButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent ev) {
applyAllOptionModifications(opts);
optionsDialog.setVisible(false);
}
});
}
/**
* Sets the action for the "cancel" button.
*
* @param cancelButton
* The button.
* @param opts
* The option object affected by the modifications.
*/
public void setCancelButtonAction(JButton cancelButton) {
cancelButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent ev) {
closeWindow();
}
});
}
/**
* Sets the action for the "add replacement" button.
*
* @param addReplacementButton
* The button.
* @param tableModel
* The model of the table containing the replacements.
*/
public void setAddReplacementButtonAction(JButton addReplacementButton,
final DefaultTableModel tableModel) {
addReplacementButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
tableModel.addRow(
new Object[]{new Replacement(ReplacementType.STR2STR)}
);
}
});
}
/**
* Sets the action for the "delete selection" button.
*
* @param addReplacementButton
* The button.
* @param tableModel
* The model of the table containing the replacements.
*/
public void setDeleteSelectionButtonAction(JButton deleteSelectionButton,
final JTable table) {
deleteSelectionButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
int[] selectedInterval = table.getSelectedRows();
int firstRowIndex = selectedInterval[0];
int intervalSize = selectedInterval.length;
DefaultTableModel model = (DefaultTableModel) table.getModel();
for (int i = 0; i < intervalSize; ++i) {
model.removeRow(firstRowIndex);
}
}
});
}
private static Object[] replacementToArray(Replacement r) {
return new Object[]{
r.getSequence(),
r.getReplacement(),
r.isEscapeCharacters(),
r.isInterpretAsPattern(),
r.getType()
};
}
/**
* Sets the actions associated to the model of the replacement table.
*
* @param table
* The table containing the replacements.
* @param columnNames
* The titles of the columns.
* @param replacements
* The replacements managed by the table.
*/
@SuppressWarnings("serial")
public void setReplacementTableModelActions(final ReplacementsTable table,
String[] columnNames, final Replacements replacements) {
table.setModel(new DefaultTableModel(columnNames, 0) {
private List<Object[]> data = new ArrayList<>();
private int columnCount =
replacementToArray(new Replacement(null)).length;
public DefaultTableModel init() {
for (Replacement replacement : replacements.getAll()) {
Object[] newRow = replacementToArray(replacement);
data.add(newRow);
visualAddRow(newRow);
}
return this;
}
@Override
public void insertRow(final int index, final Object[] rowData) {
final Replacement r = (Replacement) rowData[0];
Object[] newRow = replacementToArray(r);
data.add(index, newRow);
visualInsertRow(index, newRow);
optionModifications.add(new ModificationAction() {
@Override
public void cancel() throws Exception {
data.remove(index);
visualRemoveRow(index);
}
@Override
public void apply() throws Exception {
replacements.add(r);
}
});
}
@Override
public void addRow(Object[] rowData) {
insertRow(getRowCount(), rowData);
}
@Override
public void removeRow(final int row) {
final Object[] deletedReplacement = data.remove(row);
super.removeRow(row);
optionModifications.add(new ModificationAction() {
@Override
public void cancel() throws Exception {
data.add(row, deletedReplacement);
visualInsertRow(row, deletedReplacement);
}
@Override
public void apply() throws Exception {
replacements.remove(row);
}
});
}
@Override
public void setValueAt(final Object val, final int row,
final int column) {
if (column >= columnCount) return;
final Object previousVal = data.get(row)[column];
data.get(row)[column] = val;
visualSetValueAt(val, row, column);
optionModifications.add(new ModificationAction() {
@Override
public void cancel() throws Exception {
data.get(row)[column] = previousVal;
visualSetValueAt(previousVal, row, column);
}
@Override
public void apply() throws Exception {
switch (column) {
case 0 : replacements.get(row)
.setSequence((String) val);
break;
case 1 : replacements.get(row)
.setReplacement((String) val);
break;
case 2 : replacements.get(row)
.setEscapeCharacters((Boolean) val);
break;
case 3 : replacements.get(row)
.setInterpretAsPattern((Boolean) val);
break;
case 4 : replacements.setType(
replacements.get(row),
(ReplacementType) val
);
break;
}
}
});
}
@Override
public Object getValueAt(int row, int col) {
return col < columnCount ? data.get(row)[col] : null;
}
@Override
public Class<?> getColumnClass(int col) {
if (col < 2) {
return String.class;
} else if (col == 4) {
return ReplacementType.class;
}
return Boolean.class;
}
@Override
public boolean isCellEditable(int row, int col) {
return col < columnCount;
}
public void visualAddRow(Object[] content) {
super.addRow(content);
}
public void visualInsertRow(int row, Object[] content) {
super.insertRow(row, content);
}
public void visualRemoveRow(int row) {
super.removeRow(row);
}
public void visualSetValueAt(Object aValue, int row, int column) {
super.setValueAt(aValue, row, column);
}
}.init());
}
/**
* Sets the action for the upwards reordering button of the replacement
* table.
*
* @param table
* The table containing the replacements.
* @param columnIndex
* The index of the column containing the button.
*/
public void addReplacementTableUpCellAction(final ReplacementsTable table,
final int columnIndex) {
addReplacementTableReorderCellAction(table, columnIndex, true);
}
/**
* Sets the action for the downwards reordering button of the replacement
* table.
*
* @param table
* The table containing the replacements.
* @param columnIndex
* The index of the column containing the button.
*/
public void addReplacementTableDownCellAction(final ReplacementsTable table,
final int columnIndex) {
addReplacementTableReorderCellAction(table, columnIndex, false);
}
private void addReplacementTableReorderCellAction(
final ReplacementsTable table, final int columnIndex,
final boolean up) {
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
int row = table.rowAtPoint(evt.getPoint());
if ((up && row == 0)
|| (!up && row == table.getRowCount() - 1)) {
return;
}
int neighbor = row + (up ? -1 : 1);
int col = table.columnAtPoint(evt.getPoint());
if (col == columnIndex) {
DefaultTableModel model =
(DefaultTableModel) table.getModel();
for (int i = 0; i < model.getColumnCount(); ++i) {
Object value = model.getValueAt(neighbor, i);
model.setValueAt(model.getValueAt(row, i), neighbor, i);
model.setValueAt(value, row, i);
}
}
}
});
}
private void applyAllOptionModifications(MainOptions opts) {
try {
for (ModificationAction modif : optionModifications) {
modif.apply();
}
if (hexOptionsModified) {
ccw.updateConfig(opts.getHexOptions());
}
optionModifications.clear();
hexOptionsModified = false;
} catch (Exception e) {
new GUIErrorHandler(e);
}
}
private void cancelAllModifications() {
try {
ListIterator<ModificationAction> iom =
new ArrayList<ModificationAction>(optionModifications)
.listIterator(optionModifications.size());
while (iom.hasPrevious()) {
iom.previous().cancel();
}
optionModifications.clear();
} catch (Exception e) {
new GUIErrorHandler(e);
}
}
private void closeWindow() {
cancelAllModifications();
optionsDialog.setVisible(false);
}
// Generic listener for components associated an option.
private class OptionChangeListener implements ActionListener,
ChangeListener {
private boolean acceptEvent = true;
private JComponent elt;
private Options opts;
private ModificationAction optionModification;
private OptionChangeListener(final JComponent elt,
final Options opts, final Field optionField,
final Command applyCommand,
final Command cancelCommand) {
this.elt = elt;
this.opts = opts;
this.optionModification = new ModificationAction() {
@Override
public void cancel() throws Exception {
(cancelCommand != null
? cancelCommand
: getDefaultCancelCommand(
ReflectionUtils.getGUIComponentValueSetter(elt),
ReflectionUtils.getGetter(
opts.getClass(),
optionField
)
)
).execute();
}
@Override
public void apply() throws Exception {
(applyCommand != null
? applyCommand
: getDefaultApplyCommand(
ReflectionUtils.getGUIComponentValueGetter(elt),
ReflectionUtils.getSetter(
opts.getClass(),
optionField
)
)
).execute();
}
};
}
public OptionChangeListener(final JComponent elt,
final Options opts, final Field optionField) {
this(elt, opts, optionField, null, null);
}
public OptionChangeListener(final JComponent elt,
final Options opt, final Command applyCommand,
final Command cancelCommand) {
this(elt, opt, null, applyCommand, cancelCommand);
}
@Override
public void stateChanged(ChangeEvent e) {
actionPerformed(null);
}
@Override
public void actionPerformed(ActionEvent e) {
if (!acceptEvent) return;
optionModifications.add(this.optionModification);
}
private Command getDefaultApplyCommand(
final Method valGetter, final Method optSetter) {
return new Command() {
@Override
public void execute() throws IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
optSetter.invoke(opts, valGetter.invoke(elt));
if (opts instanceof HexOptions) {
hexOptionsModified = true;
}
}
};
}
private Command getDefaultCancelCommand(
final Method valSetter, final Method optGetter) {
return new Command() {
@Override
public void execute() throws IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
acceptEvent = false;
valSetter.invoke(elt, optGetter.invoke(opts));
acceptEvent = true;
}
};
}
}
}