/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.tools.scripts;
import org.jetbrains.annotations.NotNull;
import org.pepsoft.util.FileUtils;
import org.pepsoft.util.undo.UndoManager;
import org.pepsoft.worldpainter.Configuration;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.World2;
import org.pepsoft.worldpainter.WorldPainterDialog;
import org.pepsoft.worldpainter.layers.Layer;
import org.pepsoft.worldpainter.vo.EventVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.filechooser.FileFilter;
import java.awt.*;
import java.io.*;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.joining;
import static org.pepsoft.worldpainter.Constants.ATTRIBUTE_KEY_SCRIPT_FILENAME;
import static org.pepsoft.worldpainter.Constants.ATTRIBUTE_KEY_SCRIPT_NAME;
/**
*
* @author Pepijn Schmitz
*/
public class ScriptRunner extends WorldPainterDialog {
/**
* Creates new form ScriptRunner
*/
public ScriptRunner(Window parent, World2 world, Dimension dimension, Collection<UndoManager> undoManagers) {
super(parent);
this.world = world;
this.dimension = dimension;
this.undoManagers = undoManagers;
initComponents();
Configuration config = Configuration.getInstance();
recentScriptFiles = (config.getRecentScriptFiles() != null) ? new ArrayList<>(config.getRecentScriptFiles()) : new ArrayList<>();
recentScriptFiles.removeIf(file -> !file.isFile());
jComboBox1.setModel(new DefaultComboBoxModel<>(recentScriptFiles.toArray(new File[recentScriptFiles.size()])));
if ((jComboBox1.getSelectedItem() != null) && ((File) jComboBox1.getSelectedItem()).isFile()) {
setupScript((File) jComboBox1.getSelectedItem());
}
setControlStates();
getRootPane().setDefaultButton(jButton2);
setLocationRelativeTo(parent);
}
private void setControlStates() {
jButton2.setEnabled((jComboBox1.getSelectedItem() != null)
&& ((File) jComboBox1.getSelectedItem()).isFile()
&& ((scriptDescriptor == null) || scriptDescriptor.isValid()));
}
private void selectFile() {
Set<String> extensions = new HashSet<>();
scriptEngineManager.getEngineFactories().forEach(factory -> extensions.addAll(factory.getExtensions()));
File script = FileUtils.selectFileForOpen(this, "Select Script", (File) jComboBox1.getSelectedItem(), new FileFilter() {
@Override
public boolean accept(File f) {
if (f.isDirectory()) {
return true;
} else {
String name = f.getName();
int p = name.lastIndexOf('.');
if (p >= 0) {
return extensions.contains(name.substring(p + 1));
} else {
return false;
}
}
}
@Override
public String getDescription() {
StringBuilder sb = new StringBuilder();
sb.append("Script files (");
sb.append(extensions.stream().map(extension -> "*." + extension).collect(joining(", ")));
sb.append(')');
return sb.toString();
}
});
if ((script != null) && script.isFile()) {
recentScriptFiles.remove(script);
recentScriptFiles.add(0, script);
jComboBox1.setModel(new DefaultComboBoxModel<>(recentScriptFiles.toArray(new File[recentScriptFiles.size()])));
jComboBox1.setSelectedItem(script);
setupScript(script);
setControlStates();
}
}
private void setupScript(File script) {
scriptDescriptor = analyseScript(script);
// Remove any previously added fields:
while (panelDescriptor.getComponentCount() > 2) {
panelDescriptor.remove(2);
}
// If there is a descriptor, use it to add fields for the parameters:
if (scriptDescriptor != null) {
if (scriptDescriptor.name != null) {
labelName.setText(scriptDescriptor.name);
} else {
labelName.setText(script.getName());
}
if (scriptDescriptor.description != null) {
addRegular(panelDescriptor, new JLabel("Description:"));
JTextArea textArea = new JTextArea(scriptDescriptor.description);
textArea.setEditable(false);
textArea.setOpaque(false);
addlastOnLine(panelDescriptor, textArea);
}
boolean allFieldsOptional = true;
for (ParameterDescriptor paramDescriptor: scriptDescriptor.parameterDescriptors) {
boolean showAsMandatory = (! paramDescriptor.optional) && ((paramDescriptor instanceof FileParameterDescriptor) || (paramDescriptor instanceof FloatParameterDescriptor) || (paramDescriptor instanceof StringParameterDescriptor));
JLabel label = new JLabel(((paramDescriptor.displayName != null) ? paramDescriptor.displayName : paramDescriptor.name) + (showAsMandatory ? "*:" : ":"));
allFieldsOptional &= ! showAsMandatory;
JComponent editor = paramDescriptor.getEditor();
label.setLabelFor(editor);
if (paramDescriptor.description != null) {
label.setToolTipText(paramDescriptor.description);
}
addRegular(panelDescriptor, label);
if (paramDescriptor.description != null) {
editor.setToolTipText(paramDescriptor.description);
}
addlastOnLine(panelDescriptor, editor);
paramDescriptor.setChangeListener(e -> setControlStates());
}
if (! allFieldsOptional) {
addlastOnLine(panelDescriptor, new JLabel("* mandatory parameter"));
}
jLabel2.setVisible(! scriptDescriptor.hideCmdLineParams);
jLabel3.setVisible(! scriptDescriptor.hideCmdLineParams);
jScrollPane1.setVisible(! scriptDescriptor.hideCmdLineParams);
} else {
labelName.setText(script.getName());
jLabel2.setVisible(true);
jLabel3.setVisible(true);
jScrollPane1.setVisible(true);
}
pack();
}
private void addRegular(JPanel panel, JComponent component) {
GridBagConstraints constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.BASELINE_LEADING;
constraints.insets = new Insets(3, 0, 3, 3);
panel.add(component, constraints);
}
private void addlastOnLine(JPanel panel, JComponent component) {
GridBagConstraints constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.BASELINE_LEADING;
constraints.insets = new Insets(3, 3, 3, 0);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 1.0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
panel.add(component, constraints);
}
private ScriptDescriptor analyseScript(File script) {
if (! script.isFile()) {
return null;
}
Map<String, String> properties = new LinkedHashMap<>();
try (BufferedReader in = new BufferedReader(new FileReader(script))) {
String line;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) {
continue;
} else if (line.startsWith("#") || line.startsWith("//")) {
if (((line.startsWith("#")) && (line.substring(1).trim().startsWith("--")))
|| ((line.startsWith("//")) && (line.substring(2).trim().startsWith("--")))) {
// Script descriptor comment
continue;
} else {
Matcher matcher = DESCRIPTOR_PATTERN.matcher(line);
if (matcher.find()) {
properties.put(matcher.group(1), matcher.group(2));
}
}
} else {
// Stop after the first non-comment and non-empty line
break;
}
}
} catch (IOException e) {
throw new RuntimeException("I/O error reading script " + script);
}
if (properties.isEmpty()) {
return null;
} else {
ScriptDescriptor descriptor = new ScriptDescriptor();
Map<String, ParameterDescriptor> paramMap = new LinkedHashMap<>();
properties.forEach((key, value) -> {
if (key.equals("name")) {
descriptor.name = value.trim();
} else if (key.equals("description")) {
descriptor.description = value.trim().replace("\\n", "\n");
} else if (key.startsWith("param.")) {
String[] parts = key.split("\\.");
if (parts.length != 3) {
throw new IllegalArgumentException("Invalid key \"" + key + "\" in script descriptor");
}
ParameterDescriptor paramDescriptor = paramMap.get(parts[1]);
switch (parts[2]) {
case "type":
if (paramDescriptor == null) {
switch (value.toLowerCase().trim()) {
case "string":
paramDescriptor = new StringParameterDescriptor();
break;
case "integer":
paramDescriptor = new IntegerParameterDescriptor();
break;
case "percentage":
paramDescriptor = new PercentageParameterDescriptor();
break;
case "float":
paramDescriptor = new FloatParameterDescriptor();
break;
case "file":
paramDescriptor = new FileParameterDescriptor();
break;
case "boolean":
paramDescriptor = new BooleanParameterDescriptor();
break;
default:
throw new IllegalArgumentException("Invalid type \"" + value + "\" specified for parameter " + parts[1]);
}
paramDescriptor.name = parts[1];
paramMap.put(parts[1], paramDescriptor);
descriptor.parameterDescriptors.add(paramDescriptor);
} else {
throw new IllegalArgumentException("Type specified more than once for parameter " + parts[1]);
}
break;
case "description":
paramDescriptor.description = value.replace("\\n", "\n");
break;
case "optional":
paramDescriptor.optional = value.trim().isEmpty() || Boolean.parseBoolean(value.toLowerCase().trim());
break;
case "default":
paramDescriptor.defaultValue = paramDescriptor.toObject(value.trim());
break;
case "displayName":
paramDescriptor.displayName = value.trim();
break;
default:
throw new IllegalArgumentException("Invalid key \"" + key + "\" in script descriptor");
}
} else if (key.equals("hideCmdLineParams")) {
descriptor.hideCmdLineParams = "true".equalsIgnoreCase(value.trim());
} else {
throw new IllegalArgumentException("Invalid key \"" + key + "\" in script descriptor");
}
});
return descriptor;
}
}
private void run() {
jComboBox1.setEnabled(false);
jButton1.setEnabled(false);
jTextArea1.setEnabled(false);
jButton2.setEnabled(false);
jButton3.setEnabled(false);
jTextArea2.setText(null);
File scriptFile = (File) jComboBox1.getSelectedItem();
String scriptFileName = scriptFile.getName(), scriptName;
Map<String, Object> params;
if (scriptDescriptor != null) {
params = scriptDescriptor.getValues();
if (scriptDescriptor.name != null) {
scriptName = scriptDescriptor.name;
} else {
scriptName = scriptFileName;
}
} else {
params = null;
scriptName = scriptFileName;
}
new Thread(scriptFileName) {
@Override
public void run() {
try {
Configuration config = Configuration.getInstance();
int p = scriptFileName.lastIndexOf('.');
String extension = scriptFileName.substring(p + 1);
ScriptEngine scriptEngine = scriptEngineManager.getEngineByExtension(extension);
scriptEngine.put(ScriptEngine.FILENAME, scriptFileName);
config.setRecentScriptFiles(new ArrayList<>(recentScriptFiles));
// Initialise script context
ScriptingContext context = new ScriptingContext(false);
Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("wp", context);
String[] parameters = jTextArea1.getText().split("\\R");
bindings.put("argc", parameters.length + 1);
String[] argv = new String[parameters.length + 1];
argv[0] = scriptFileName;
System.arraycopy(parameters, 0, argv, 1, parameters.length);
bindings.put("argv", argv);
bindings.put("arguments", parameters);
if (params != null) {
bindings.put("params", params);
}
if (world != null) {
bindings.put("world", world);
}
if (dimension != null) {
bindings.put("dimension", dimension);
}
Map<String, Layer.DataSize> dataSizes = new HashMap<>();
for (Layer.DataSize dataSize: Layer.DataSize.values()) {
dataSizes.put(dataSize.name(), dataSize);
}
bindings.put("DataSize", dataSizes);
// Capture output
List<String> textQueue = new LinkedList<>();
boolean[] textUpdateScheduled = new boolean[] {false};
Writer writer = new Writer() {
@Override
public void write(@NotNull char[] cbuf, int off, int len) throws IOException {
synchronized (textQueue) {
textQueue.add(new String(cbuf, off, len));
if (! textUpdateScheduled[0]) {
SwingUtilities.invokeLater(() -> {
synchronized (textQueue) {
// Join the fragments first so that
// only one string need be appended
// to the text area's document
jTextArea2.append(textQueue.stream().collect(joining()));
textQueue.clear();
textUpdateScheduled[0] = false;
}
});
textUpdateScheduled[0] = true;
}
}
}
@Override public void flush() throws IOException {}
@Override public void close() throws IOException {}
};
scriptEngine.getContext().setWriter(writer);
scriptEngine.getContext().setErrorWriter(writer);
// Log the execution
config.logEvent(new EventVO("script.execute").addTimestamp()
.setAttribute(ATTRIBUTE_KEY_SCRIPT_NAME, scriptName)
.setAttribute(ATTRIBUTE_KEY_SCRIPT_FILENAME, scriptFileName));
// Execute script
if (dimension != null) {
dimension.setEventsInhibited(true);
}
try {
scriptEngine.eval(new FileReader(scriptFile));
// Check that go() was invoked on the last operation:
context.checkGoCalled(null);
} catch (RuntimeException e) {
logger.error(e.getClass().getSimpleName() + " occurred while executing " + scriptFileName, e);
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(ScriptRunner.this, e.getClass().getSimpleName() + " occurred (message: " + e.getMessage() + ")", "Error", JOptionPane.ERROR_MESSAGE));
} catch (javax.script.ScriptException e) {
logger.error("ScriptException occurred while executing " + scriptFileName, e);
StringBuilder sb = new StringBuilder();
sb.append(e.getMessage());
if (e.getLineNumber() != -1) {
sb.append(" (");
sb.append(e.getLineNumber());
if (e.getColumnNumber() != -1) {
sb.append(':');
sb.append(e.getColumnNumber());
}
sb.append(')');
}
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(ScriptRunner.this, sb.toString(), "Error", JOptionPane.ERROR_MESSAGE));
} catch (FileNotFoundException e) {
logger.error("FileNotFoundException occurred while executing " + scriptFileName, e);
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(ScriptRunner.this, "File not found while executing " + scriptFileName, "Error", JOptionPane.ERROR_MESSAGE));
} finally {
if (dimension != null) {
dimension.setEventsInhibited(false);
}
if (undoManagers != null) {
undoManagers.forEach(UndoManager::armSavePoint);
}
}
} finally {
SwingUtilities.invokeLater(() -> {
jComboBox1.setEnabled(true);
jButton1.setEnabled(true);
jTextArea1.setEnabled(true);
jButton2.setEnabled(true);
jButton3.setText("Close");
jButton3.setEnabled(true);
});
}
}
}.start();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
java.awt.GridBagConstraints gridBagConstraints;
jLabel1 = new javax.swing.JLabel();
jComboBox1 = new javax.swing.JComboBox();
jButton1 = new javax.swing.JButton();
jLabel2 = new javax.swing.JLabel();
jScrollPane1 = new javax.swing.JScrollPane();
jTextArea1 = new javax.swing.JTextArea();
jLabel3 = new javax.swing.JLabel();
jLabel4 = new javax.swing.JLabel();
jButton2 = new javax.swing.JButton();
jScrollPane2 = new javax.swing.JScrollPane();
jTextArea2 = new javax.swing.JTextArea();
jButton3 = new javax.swing.JButton();
panelDescriptor = new javax.swing.JPanel();
jLabel5 = new javax.swing.JLabel();
labelName = new javax.swing.JLabel();
setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
setTitle("Run Script");
jLabel1.setText("Script:");
jComboBox1.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" }));
jComboBox1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jComboBox1ActionPerformed(evt);
}
});
jButton1.setText("...");
jButton1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButton1ActionPerformed(evt);
}
});
jLabel2.setText("Parameters:");
jTextArea1.setColumns(20);
jTextArea1.setRows(5);
jScrollPane1.setViewportView(jTextArea1);
jLabel3.setText("(one per line)");
jLabel4.setText("Output:");
jButton2.setText("Run");
jButton2.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButton2ActionPerformed(evt);
}
});
jTextArea2.setEditable(false);
jTextArea2.setColumns(20);
jTextArea2.setLineWrap(true);
jTextArea2.setRows(5);
jTextArea2.setWrapStyleWord(true);
jScrollPane2.setViewportView(jTextArea2);
jButton3.setText("Cancel");
jButton3.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButton3ActionPerformed(evt);
}
});
panelDescriptor.setLayout(new java.awt.GridBagLayout());
jLabel5.setText("Name:");
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START;
gridBagConstraints.insets = new java.awt.Insets(0, 1, 0, 2);
panelDescriptor.add(jLabel5, gridBagConstraints);
labelName.setText("jLabel6");
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.insets = new java.awt.Insets(0, 2, 0, 0);
panelDescriptor.add(labelName, gridBagConstraints);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(jScrollPane2)
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addComponent(jLabel1)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jComboBox1, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jButton1))
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jLabel2)
.addComponent(jLabel3))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 598, Short.MAX_VALUE))
.addGroup(layout.createSequentialGroup()
.addGap(0, 0, Short.MAX_VALUE)
.addComponent(jButton2)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jButton3))
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addComponent(jLabel4)
.addGap(0, 0, Short.MAX_VALUE))
.addComponent(panelDescriptor, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(jLabel1)
.addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(jButton1))
.addGap(18, 18, 18)
.addComponent(panelDescriptor, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(18, 18, 18)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(jLabel2)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jLabel3))
.addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jLabel4)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 243, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(jButton2)
.addComponent(jButton3))
.addContainerGap())
);
pack();
}// </editor-fold>//GEN-END:initComponents
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed
selectFile();
}//GEN-LAST:event_jButton1ActionPerformed
private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton2ActionPerformed
run();
}//GEN-LAST:event_jButton2ActionPerformed
private void jComboBox1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jComboBox1ActionPerformed
setupScript((File) jComboBox1.getSelectedItem());
setControlStates();
}//GEN-LAST:event_jComboBox1ActionPerformed
private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton3ActionPerformed
cancel();
}//GEN-LAST:event_jButton3ActionPerformed
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton jButton1;
private javax.swing.JButton jButton2;
private javax.swing.JButton jButton3;
private javax.swing.JComboBox jComboBox1;
private javax.swing.JLabel jLabel1;
private javax.swing.JLabel jLabel2;
private javax.swing.JLabel jLabel3;
private javax.swing.JLabel jLabel4;
private javax.swing.JLabel jLabel5;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JScrollPane jScrollPane2;
private javax.swing.JTextArea jTextArea1;
private javax.swing.JTextArea jTextArea2;
private javax.swing.JLabel labelName;
private javax.swing.JPanel panelDescriptor;
// End of variables declaration//GEN-END:variables
private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
private final World2 world;
private final Dimension dimension;
private final ArrayList<File> recentScriptFiles;
private final Collection<UndoManager> undoManagers;
private ScriptDescriptor scriptDescriptor;
private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("script\\.([.a-zA-Z_0-9]+)=(.+)$");
private static final Logger logger = LoggerFactory.getLogger(ScriptRunner.class);
@SuppressWarnings("Convert2MethodRef") // This is shorter
static class ScriptDescriptor {
boolean isValid() {
return parameterDescriptors.stream().allMatch(p -> p.isEditorValid());
}
Map<String, Object> getValues() {
Map<String, Object> values = new HashMap<>();
parameterDescriptors.forEach(p -> {
Object value = p.getValue();
if (value != null) {
values.put(p.name, value);
}
});
return values;
}
String name, description;
List<ParameterDescriptor> parameterDescriptors = new ArrayList<>();
boolean hideCmdLineParams;
}
abstract static class ParameterDescriptor<T, E extends JComponent> {
E getEditor() {
if (editor == null) {
editor = createEditor();
}
if (defaultValue != null) {
setValue(defaultValue);
}
return editor;
}
boolean isEditorValid() {
return true;
}
abstract T getValue();
abstract void setValue(T value);
abstract T toObject(String str);
ChangeListener getChangeListener() {
return changeListener;
}
void setChangeListener(ChangeListener changeListener) {
this.changeListener = changeListener;
}
protected abstract E createEditor();
protected void notifyChangeListener() {
if (changeListener != null) {
changeListener.stateChanged(new ChangeEvent(this));
}
}
String name, description, displayName;
boolean optional;
E editor;
T defaultValue;
private ChangeListener changeListener;
}
static class StringParameterDescriptor extends ParameterDescriptor<String, JTextField> {
@Override
protected JTextField createEditor() {
JTextField field = new JTextField(defaultValue);
field.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
notifyChangeListener();
}
@Override
public void removeUpdate(DocumentEvent e) {
notifyChangeListener();
}
@Override
public void changedUpdate(DocumentEvent e) {
notifyChangeListener();
}
});
return field;
}
@Override
String toObject(String str) {
return str;
}
@Override
boolean isEditorValid() {
return optional || (! editor.getText().trim().isEmpty());
}
@Override
String getValue() {
String text = editor.getText();
return text.trim().isEmpty() ? null : text.trim();
}
@Override
void setValue(String value) {
editor.setText(value);
}
}
static class IntegerParameterDescriptor extends ParameterDescriptor<Integer, JSpinner> {
@Override
protected JSpinner createEditor() {
JSpinner spinner = new JSpinner();
spinner.addChangeListener(e -> notifyChangeListener());
return spinner;
}
@Override
Integer toObject(String str) {
return Integer.valueOf(str);
}
@Override
Integer getValue() {
return (Integer) editor.getValue();
}
@Override
void setValue(Integer value) {
editor.setValue(value);
}
}
static class PercentageParameterDescriptor extends ParameterDescriptor<Integer, JPanel> {
@Override
protected JPanel createEditor() {
JPanel panel = new JPanel(new GridBagLayout()) {
@Override
public int getBaseline(int width, int height) {
return getComponent(0).getBaseline(width, height);
}
};
GridBagConstraints constraints = new GridBagConstraints();
constraints.weightx = 1.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
SpinnerNumberModel spinnerModel = new SpinnerNumberModel(0, 0, 100, 1);
JSpinner spinner = new JSpinner(spinnerModel);
spinner.setToolTipText(description);
spinner.addChangeListener(e -> notifyChangeListener());
panel.add(spinner, constraints);
constraints.weightx = 0.0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
JLabel label = new JLabel("%");
label.setToolTipText(description);
panel.add(label, constraints);
return panel;
}
@Override
Integer toObject(String str) {
if (str.endsWith("%")) {
return Integer.valueOf(str.substring(0, str.length() - 1).trim());
} else {
return Integer.valueOf(str.trim());
}
}
@Override
Integer getValue() {
return (Integer) ((JSpinner) editor.getComponent(0)).getValue();
}
@Override
void setValue(Integer value) {
((JSpinner) editor.getComponent(0)).setValue(value);
}
}
static class FloatParameterDescriptor extends ParameterDescriptor<Float, JFormattedTextField> {
@Override
protected JFormattedTextField createEditor() {
JFormattedTextField field = new JFormattedTextField(NumberFormat.getNumberInstance());
field.setHorizontalAlignment(SwingConstants.TRAILING);
field.addPropertyChangeListener("value", e -> notifyChangeListener());
return field;
}
@Override
Float toObject(String str) {
return Float.valueOf(str);
}
@Override
boolean isEditorValid() {
try {
editor.commitEdit();
return optional || (editor.getValue() != null);
} catch (ParseException e) {
return optional;
}
}
@Override
Float getValue() {
Number nr = (Number) editor.getValue();
return (nr != null) ? nr.floatValue() : null;
}
@Override
void setValue(Float value) {
editor.setValue(value);
}
}
static class FileParameterDescriptor extends ParameterDescriptor<File, JPanel> {
@Override
protected JPanel createEditor() {
JPanel panel = new JPanel(new GridBagLayout()) {
@Override
public int getBaseline(int width, int height) {
return getComponent(0).getBaseline(width, height);
}
};
GridBagConstraints constraints = new GridBagConstraints();
constraints.weightx = 1.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
JTextField field = new JTextField();
field.setToolTipText(description);
field.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
notifyChangeListener();
}
@Override
public void removeUpdate(DocumentEvent e) {
notifyChangeListener();
}
@Override
public void changedUpdate(DocumentEvent e) {
notifyChangeListener();
}
});
panel.add(field, constraints);
constraints.weightx = 0.0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(0, 3, 0, 0);
JButton button = new JButton("...");
button.setToolTipText(description);
button.addActionListener(e -> {
JFileChooser fileChooser = new JFileChooser();
if (! field.getText().trim().isEmpty()) {
fileChooser.setSelectedFile(new File(field.getText().trim()));
}
if (fileChooser.showOpenDialog(panel) == JFileChooser.APPROVE_OPTION) {
field.setText(fileChooser.getSelectedFile().getAbsolutePath());
}
});
panel.add(button, constraints);
return panel;
}
@Override
File toObject(String str) {
return new File(str);
}
@Override
boolean isEditorValid() {
if (optional) {
return true;
} else {
String text = ((JTextField) editor.getComponent(0)).getText();
return (! text.trim().isEmpty()) && new File(text.trim()).isFile();
}
}
@Override
File getValue() {
String text = ((JTextField) editor.getComponent(0)).getText();
return text.trim().isEmpty() ? null : new File(text.trim());
}
@Override
void setValue(File value) {
((JTextField) editor.getComponent(0)).setText(value.getAbsolutePath());
}
}
static class BooleanParameterDescriptor extends ParameterDescriptor<Boolean, JCheckBox> {
@Override
protected JCheckBox createEditor() {
JCheckBox checkBox = new JCheckBox(" ");
checkBox.addChangeListener(e -> notifyChangeListener());
return checkBox;
}
@Override
Boolean toObject(String str) {
return Boolean.valueOf(str);
}
@Override
Boolean getValue() {
return editor.isSelected();
}
@Override
void setValue(Boolean value) {
editor.setSelected(value);
}
}
}