001package jmri.util; 002 003import java.awt.Component; 004import java.util.function.Function; 005import java.util.function.Predicate; 006 007import javax.swing.*; 008import javax.swing.event.DocumentEvent; 009import javax.swing.event.DocumentListener; 010 011/** 012 * A helper Panel for input-validating input boxes. It converts and validates the 013 * text input, disabling {@link #confirmUI} component (usually a button) when 014 * the input is not valid. 015 * 016 * @author Svata Dedic Copyright (c) 2019 017 */ 018@SuppressWarnings("javadoc") 019final class ValidatingInputPane<T> extends javax.swing.JPanel { 020 private final Function<String, T> convertor; 021 private final DocumentListener l = new DocumentListener() { 022 @Override 023 public void insertUpdate(DocumentEvent e) { 024 validateInput(); 025 } 026 027 @Override 028 public void removeUpdate(DocumentEvent e) { 029 validateInput(); 030 } 031 032 @Override 033 public void changedUpdate(DocumentEvent e) { 034 } 035 }; 036 037 /** 038 * Callback that validates the input after conversion. 039 */ 040 private Predicate<T> validator; 041 042 /** 043 * The confirmation component. The component is disabled when the 044 * input is rejected by converter or validator 045 */ 046 private JComponent confirmUI; 047 048 /** 049 * Holds the last seen error. {@code null} for no error - valid input 050 */ 051 private String lastError; 052 053 /** 054 * Last custom exception. {@code null}, if no error or if 055 * the validator just rejected with no message. 056 */ 057 private IllegalArgumentException customException; 058 059 /** 060 * Creates new form ValidatingInputPane. 061 * @param convertor converts String to the desired data type. 062 */ 063 public ValidatingInputPane(Function<String, T> convertor) { 064 initComponents(); 065 errorMessage.setVisible(false); 066 this.convertor = convertor; 067 } 068 069 /** 070 * Attaches a component used to confirm/proceed. The component will 071 * be disabled if the input is erroneous. The first validation will happen 072 * after this component appears on the screen. Typically, the OK button 073 * should be passed here. 074 * 075 * @param confirm the "confirm" control. 076 * @return this instance. 077 */ 078 ValidatingInputPane<T> attachConfirmUI(JComponent confirm) { 079 this.confirmUI = confirm; 080 return this; 081 } 082 083 @Override 084 public void addNotify() { 085 super.addNotify(); 086 inputText.getDocument().addDocumentListener(l); 087 SwingUtilities.invokeLater(this::validateInput); 088 } 089 090 /** 091 * Configures a prompt message for the panel. The prompt message 092 * appears above the input line. 093 * @param msg message text 094 * @return this instance. 095 */ 096 ValidatingInputPane<T> message(String msg) { 097 promtptMessage.setText(msg); 098 return this; 099 } 100 101 /** 102 * Returns the exception from the most recent validation. Only exceptions 103 * from unsuccessful conversion or thrown by validator are returned. To check 104 * whether the input is valid, call {@link #hasError()}. If the validator 105 * just rejects the input with no exception, this method returns {@code null} 106 * @return exception thrown by converter or validator. 107 */ 108 IllegalArgumentException getException() { 109 return customException; 110 } 111 112 /** 113 * Configures the validator. Validator is called to check the value after 114 * the String input is converted to the target type. The validator can either 115 * just return {@code false} to reject the value with a generic message, or 116 * throw a {@link IllegalArgumentException} subclass with a custom message. 117 * The message will be then displayed below the input line. 118 * 119 * @param val validator instance, {@code null} to disable. 120 * @return this instance 121 */ 122 ValidatingInputPane<T> validator(Predicate<T> val) { 123 this.validator = val; 124 return this; 125 } 126 127 /** 128 * Determines if the input is erroneous. 129 * @return error status 130 */ 131 boolean hasError() { 132 return lastError != null; 133 } 134 135 /** 136 * Sets the input value, as text. 137 * @param text input text 138 */ 139 void setText(String text) { 140 inputText.setText(text); 141 } 142 143 /** 144 * Gets the input value, as text. 145 * @return the input text 146 */ 147 String getText() { 148 return inputText.getText().trim(); 149 } 150 151 /** 152 * Gets the input value after conversion. May throw {@link IllegalArgumentException} 153 * if the conversion fails (text input cannot be converted to the target type). 154 * Returns {@code null} for empty (all whitespace) input. 155 * @return the entered value or {@code null} for empty input. 156 */ 157 T getValue() { 158 String s = getText(); 159 return s.isEmpty() ? null : convertor.apply(s); 160 } 161 162 /** 163 * Gets the error message. Either a custom message from an exception 164 * thrown by converter or validator, or the default message for failed validation. 165 * Returns {@code null} for valid input. 166 * @return if input is invalid, returns the error message. If the input is valid, returns {@code null}. 167 */ 168 String getErrorMessage() { 169 return lastError; 170 } 171 172 private void validateInput() { 173 if (isVisible()) { 174 validateText(getText()); 175 } 176 } 177 178 private void clearErrors() { 179 if (confirmUI != null) { 180 confirmUI.setEnabled(true); 181 } 182 errorMessage.setText(""); 183 errorMessage.setVisible(false); 184 customException = null; 185 lastError = null; 186 } 187 188 /** 189 * Should be called from tests only 190 * @param text String to check for validation 191 */ 192 void validateText(String text) { 193 String msg; 194 if (text.isEmpty()) { 195 clearErrors(); 196 return; 197 } 198 try { 199 T value = convertor.apply(text); 200 if (validator == null || 201 validator.test(value)) { 202 clearErrors(); 203 return; 204 } 205 msg = Bundle.getMessage("InputDialogError"); 206 } catch (IllegalArgumentException ex) { 207 msg = ex.getLocalizedMessage(); 208 customException = ex; 209 } 210 lastError = msg; 211 errorMessage.setText(msg); 212 errorMessage.setVisible(true); 213 if (confirmUI != null) { 214 confirmUI.setEnabled(false); 215 } 216 Component c = SwingUtilities.getRoot(this); 217 if (c != null) { 218 c.invalidate(); 219 if (c instanceof JDialog) { 220 ((JDialog)c).pack(); 221 } 222 } 223 } 224 225 // only for testing 226 JTextField getTextField() { 227 return inputText; 228 } 229 230 /** 231 * This method is called from within the constructor to initialize the form. 232 * WARNING: Do NOT modify this code. The content of this method is always 233 * regenerated by the Form Editor. 234 */ 235 // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents 236 private void initComponents() { 237 238 jScrollPane2 = new javax.swing.JScrollPane(); 239 jTextArea2 = new javax.swing.JTextArea(); 240 promtptMessage = new javax.swing.JLabel(); 241 inputText = new javax.swing.JTextField(); 242 errorMessage = new javax.swing.JTextArea(); 243 244 jTextArea2.setColumns(20); 245 jTextArea2.setRows(5); 246 jScrollPane2.setViewportView(jTextArea2); 247 248 promtptMessage.setText(" "); 249 250 errorMessage.setEditable(false); 251 errorMessage.setBackground(getBackground()); 252 errorMessage.setColumns(20); 253 errorMessage.setForeground(java.awt.Color.red); 254 errorMessage.setRows(2); 255 errorMessage.setToolTipText(""); 256 errorMessage.setAutoscrolls(false); 257 errorMessage.setBorder(null); 258 errorMessage.setFocusable(false); 259 errorMessage.setRequestFocusEnabled(false); 260 errorMessage.setVerifyInputWhenFocusTarget(false); 261 262 javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); 263 this.setLayout(layout); 264 layout.setHorizontalGroup( 265 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 266 .addGroup(layout.createSequentialGroup() 267 .addContainerGap() 268 .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 269 .addComponent(promtptMessage, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) 270 .addComponent(errorMessage, javax.swing.GroupLayout.DEFAULT_SIZE, 253, Short.MAX_VALUE) 271 .addComponent(inputText)) 272 .addContainerGap()) 273 ); 274 layout.setVerticalGroup( 275 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 276 .addGroup(layout.createSequentialGroup() 277 .addContainerGap() 278 .addComponent(promtptMessage) 279 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) 280 .addComponent(inputText, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) 281 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) 282 .addComponent(errorMessage, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE) 283 .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) 284 ); 285 }// </editor-fold>//GEN-END:initComponents 286 287 288 // Variables declaration - do not modify//GEN-BEGIN:variables 289 private javax.swing.JTextArea errorMessage; 290 private javax.swing.JTextField inputText; 291 private javax.swing.JScrollPane jScrollPane2; 292 private javax.swing.JTextArea jTextArea2; 293 private javax.swing.JLabel promtptMessage; 294 // End of variables declaration//GEN-END:variables 295}