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}