001package jmri.script.swing;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.BorderLayout;
006import java.awt.FlowLayout;
007import java.awt.Font;
008import java.awt.HeadlessException;
009import java.awt.event.ActionEvent;
010import java.io.BufferedReader;
011import java.io.BufferedWriter;
012import java.io.File;
013import java.io.FileReader;
014import java.io.FileWriter;
015import java.io.IOException;
016
017import javax.script.ScriptException;
018import javax.swing.*;
019import javax.swing.event.CaretEvent;
020import javax.swing.text.BadLocationException;
021
022import jmri.UserPreferencesManager;
023import jmri.script.JmriScriptEngineManager;
024import jmri.script.ScriptEngineSelector;
025import jmri.script.ScriptEngineSelector.Engine;
026import jmri.util.FileUtil;
027import jmri.util.JmriJFrame;
028import jmri.util.swing.JmriJOptionPane;
029
030import org.python.google.common.io.Files;
031
032/**
033 * A JFrame for sending input to the global jython
034 * interpreter
035 *
036 * @author Bob Jacobsen Copyright (C) 2004, 2021, 2022
037 */
038public class InputWindow extends JPanel {
039
040    JTextArea area;
041    JButton button;
042    JButton loadButton;
043    JButton storeButton;
044    private UserPreferencesManager pref;
045    JLabel status;
046    JCheckBox alwaysOnTopCheckBox = new JCheckBox();
047
048    private ScriptEngineSelector scriptEngineSelector = new ScriptEngineSelector();
049    private ScriptEngineSelectorSwing scriptEngineSelectorSwing;
050
051    JFileChooser userFileChooser = new ScriptFileChooser(FileUtil.getScriptsPath());
052
053    public static final String languageSelection = InputWindow.class.getName() + ".language";
054    public static final String alwaysOnTopChecked = InputWindow.class.getName() + ".alwaysOnTopChecked";
055
056    public InputWindow() {
057        pref = jmri.InstanceManager.getDefault(UserPreferencesManager.class);
058
059        //setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS));
060        setLayout(new BorderLayout());
061
062        area = new JTextArea(12, 50);
063
064        // from: http://stackoverflow.com/questions/5139995/java-column-number-and-line-number-of-cursors-current-position
065        area.addCaretListener((CaretEvent e) -> {
066            // Each time the caret is moved, it will trigger the listener and its method caretUpdate.
067            // It will then pass the event to the update method including the source of the event (which is our textarea control)
068            JTextArea editArea = (JTextArea) e.getSource();
069
070            // Lets start with some default values for the line and column.
071            int linenum = 1;
072            int columnnum = 1;
073
074            // We create a try catch to catch any exceptions. We will simply ignore such an error for our demonstration.
075            try {
076                // First we find the position of the caret. This is the number of where the caret is in relation to the start of the JTextArea
077                // in the upper left corner. We use this position to find offset values (eg what line we are on for the given position as well as
078                // what position that line starts on.
079                int caretpos = editArea.getCaretPosition();
080                linenum = editArea.getLineOfOffset(caretpos);
081
082                // We subtract the offset of where our line starts from the overall caret position.
083                // So lets say that we are on line 5 and that line starts at caret position 100, if our caret position is currently 106
084                // we know that we must be on column 6 of line 5.
085                columnnum = caretpos - editArea.getLineStartOffset(linenum);
086
087                // We have to add one here because line numbers start at 0 for getLineOfOffset and we want it to start at 1 for display.
088                linenum += 1;
089            } catch (BadLocationException ex) {
090            }
091
092            // Once we know the position of the line and the column, pass it to a helper function for updating the status bar.
093            updateStatus(linenum, columnnum);
094        });
095
096        JScrollPane js = new JScrollPane(area);
097        js.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
098        js.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
099        add(js, BorderLayout.CENTER);
100
101        // set the preferred language
102        String preferredLanguage = pref.getComboBoxLastSelection(languageSelection);
103        if (preferredLanguage != null) {
104            // Backwards compability pre 4.99.9
105            boolean updatePreferredLanguage = false;
106            if (preferredLanguage.equals(Bundle.getMessage("jython_python"))) {
107                scriptEngineSelector.setSelectedEngine(ScriptEngineSelector.JYTHON);
108                updatePreferredLanguage = true;
109            } else if (preferredLanguage.equals(Bundle.getMessage("Oracle_Nashorn_ECMAScript"))) {
110                scriptEngineSelector.setSelectedEngine(ScriptEngineSelector.ECMA_SCRIPT);
111                updatePreferredLanguage = true;
112            } else {
113                scriptEngineSelector.setSelectedEngine(preferredLanguage);
114            }
115
116            Engine engine = scriptEngineSelector.getSelectedEngine();
117            if (updatePreferredLanguage && engine != null) {
118                pref.setComboBoxLastSelection(languageSelection, engine.getLanguageName());
119            }
120        }
121
122        scriptEngineSelectorSwing = new ScriptEngineSelectorSwing(scriptEngineSelector);
123
124        JPanel p = new JPanel();
125        p.setLayout(new FlowLayout());
126        p.add(loadButton = new JButton(Bundle.getMessage("ButtonLoad_")));
127        p.add(storeButton = new JButton(Bundle.getMessage("ButtonStore_")));
128        p.add(this.scriptEngineSelectorSwing.getComboBox());
129        p.add(button = new JButton(Bundle.getMessage("ButtonExecute")));
130
131        alwaysOnTopCheckBox.setText(Bundle.getMessage("WindowAlwaysOnTop"));
132        alwaysOnTopCheckBox.setVisible(true);
133        alwaysOnTopCheckBox.setToolTipText(Bundle.getMessage("WindowAlwaysOnTopToolTip"));
134        p.add(alwaysOnTopCheckBox);
135
136        status = new JLabel("         ");   // create some space for the counters
137        p.add(status);
138        updateStatus(1, 0);
139
140        add(p, BorderLayout.SOUTH);
141
142        button.addActionListener((ActionEvent e) -> {
143            buttonPressed();
144        });
145
146        loadButton.addActionListener((ActionEvent e) -> {
147            loadButtonPressed();
148        });
149
150        storeButton.addActionListener((ActionEvent e) -> {
151            storeButtonPressed();
152        });
153
154        scriptEngineSelectorSwing.getComboBox().addItemListener((java.awt.event.ItemEvent e) -> {
155            var comboBox = scriptEngineSelectorSwing.getComboBox();
156            Engine engine = comboBox.getItemAt(comboBox.getSelectedIndex());
157            pref.setComboBoxLastSelection(languageSelection, engine.getLanguageName());
158        });
159
160        alwaysOnTopCheckBox.addActionListener((ActionEvent e) -> {
161            if (getTopLevelAncestor() != null) {
162                ((JmriJFrame) getTopLevelAncestor()).setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
163            }
164            pref.setSimplePreferenceState(alwaysOnTopChecked, alwaysOnTopCheckBox.isSelected());
165        });
166        alwaysOnTopCheckBox.setSelected(pref.getSimplePreferenceState(alwaysOnTopChecked));
167
168        // set a monospaced font
169        int size = area.getFont().getSize();
170        area.setFont(new Font("Monospaced", Font.PLAIN, size));
171
172    }
173
174    // This helper function updates the status bar with the line number and column number.
175    private void updateStatus(int linenumber, int columnnumber) {
176        status.setText("    " + linenumber + ":" + columnnumber);
177    }
178
179    /**
180     * Load a file into this input window.
181     *
182     * @param fileChooser the chooser to select the file with
183     * @return true if successful; false otherwise
184     */
185    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "Should crash if missing ScriptEngine dependencies are not present")
186    protected boolean loadFile(JFileChooser fileChooser) {
187        boolean results = false;
188        File file = getFile(fileChooser);
189        if (file != null) {
190            try {
191                try {
192                    scriptEngineSelector.setSelectedEngine(JmriScriptEngineManager.getDefault().getFactoryByExtension(Files.getFileExtension(file.getName())).getLanguageName());
193                    scriptEngineSelectorSwing.updateSetComboBoxSelection();
194                } catch (ScriptException npe) {
195                    log.error("Unable to identify script language for {}, assuming its Python.", file);
196                    scriptEngineSelector.setSelectedEngine(JmriScriptEngineManager.getDefault().getFactory(JmriScriptEngineManager.JYTHON).getLanguageName());
197                    scriptEngineSelectorSwing.updateSetComboBoxSelection();
198                }
199                StringBuilder fileData = new StringBuilder(1024);
200                try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
201                    char[] buf = new char[1024];
202                    int numRead;
203                    while ((numRead = reader.read(buf)) != -1) {
204                        String readData = String.valueOf(buf, 0, numRead);
205                        fileData.append(readData);
206                        buf = new char[1024];
207                    }
208                }
209                area.setText(fileData.toString());
210                results = true;
211
212            } catch (IOException e) {
213                log.error("Unhandled problem in loadFile", e);
214            }
215        } else {
216            results = true;   // We assume that as the file is null then the user has clicked cancel.
217        }
218        return results;
219    }
220
221    /**
222     * Save the contents of this input window to a file.
223     *
224     * @param fileChooser the chooser to select the file with
225     * @return true if successful; false otherwise
226     */
227    protected boolean storeFile(JFileChooser fileChooser) {
228        boolean results = false;
229        File file = getFile(fileChooser);
230        if (file != null) {
231            try {
232                // check for possible overwrite
233                if (file.exists()) {
234                    int selectedValue = JmriJOptionPane.showConfirmDialog(null,
235                            Bundle.getMessage("ConfirmDialogMessage", file.getName()),
236                            Bundle.getMessage("ConfirmDialogTitle"),
237                            JmriJOptionPane.OK_CANCEL_OPTION);
238                    if (selectedValue != JmriJOptionPane.OK_OPTION) {
239                        return false; // user clicked no to override
240                    }
241                }
242
243                StringBuilder fileData = new StringBuilder(area.getText());
244                try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
245                    writer.append(fileData);
246                }
247                results = true;
248
249            } catch (HeadlessException | IOException e) {
250                log.error("Unhandled problem in storeFile", e);
251            }
252        } else {
253            results = true;   // If the file is null then the user has clicked cancel.
254        }
255        return results;
256    }
257
258    static public File getFile(JFileChooser fileChooser) {
259        fileChooser.rescanCurrentDirectory();
260        int retVal = fileChooser.showDialog(null, null);
261        if (retVal != JFileChooser.APPROVE_OPTION) {
262            return null;  // give up if no file selected
263        }
264        if (log.isDebugEnabled()) {
265            log.debug("Open file: {}", fileChooser.getSelectedFile().getPath());
266        }
267        return fileChooser.getSelectedFile();
268    }
269
270    void loadButtonPressed() {
271        userFileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
272        userFileChooser.setApproveButtonText(Bundle.getMessage("MenuItemLoad"));
273        userFileChooser.setDialogTitle(Bundle.getMessage("MenuItemLoad"));
274
275        boolean results = loadFile(userFileChooser);
276        log.debug("load {}", results ? "was successful" : "failed");
277        if (!results) {
278            log.warn("Not loading file: {}", userFileChooser.getSelectedFile().getPath());
279        }
280    }
281
282    void storeButtonPressed() {
283        userFileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
284        userFileChooser.setApproveButtonText(Bundle.getMessage("MenuItemStore"));
285        userFileChooser.setDialogTitle(Bundle.getMessage("MenuItemStore"));
286
287        boolean results = storeFile(userFileChooser);
288        log.debug("store {}", results ? "was successful" : "failed");
289        if (!results) {
290            log.warn("Not storing file: {}", userFileChooser.getSelectedFile().getPath());
291        }
292    }
293
294    public void buttonPressed() {  // public for testing
295        ScriptOutput.writeScript(area.getText());
296        try {
297            ScriptEngineSelector.Engine engine = scriptEngineSelector.getSelectedEngine();
298            if (engine != null) {
299                JmriScriptEngineManager
300                    .getDefault().eval(area.getText(),engine.getScriptEngine());
301            } else {
302                throw new NullPointerException("scriptEngineSelector.getSelectedEngine() returns null");
303            }
304        } catch (ScriptException ex) {
305            log.error("Error executing script", ex);
306        }
307    }
308    // initialize logging
309    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(InputWindow.class);
310}