001package jmri.util; 002 003import static jmri.util.swing.JmriJOptionPane.OK_CANCEL_OPTION; 004 005import java.awt.Component; 006import java.awt.event.ActionEvent; 007import java.awt.event.ActionListener; 008import java.util.function.Function; 009import java.util.function.Predicate; 010import javax.annotation.CheckForNull; 011import javax.annotation.Nonnull; 012import javax.swing.JButton; 013import javax.swing.JDialog; 014import javax.swing.JOptionPane; 015import javax.swing.SwingConstants; 016 017import jmri.util.swing.JmriJOptionPane; 018 019/** 020 * A collection of utilities related to prompting for values 021 * 022 * @author George Warner Copyright: (c) 2017 023 */ 024public class QuickPromptUtil { 025 026 /** 027 * Utility function to prompt for new string value. 028 * 029 * @param parentComponent the parent component 030 * @param message the prompt message 031 * @param title the dialog title 032 * @param oldValue the original string value 033 * @return the new string value 034 */ 035 public static String promptForString(Component parentComponent, String message, String title, String oldValue) { 036 String result = oldValue; 037 String newValue = (String) JmriJOptionPane.showInputDialog(parentComponent, 038 message, title, JmriJOptionPane.PLAIN_MESSAGE, 039 null, null, oldValue); 040 if (newValue != null) { 041 result = newValue; 042 } 043 return result; 044 } 045 046 /** 047 * Utility function to prompt for new integer value. 048 * 049 * @param parentComponent the parent component 050 * @param message the prompt message 051 * @param title the dialog title 052 * @param oldValue the original integer value 053 * @return the new integer value 054 */ 055 public static int promptForInt(Component parentComponent, String message, String title, int oldValue) { 056 int result = oldValue; 057 String newValue = promptForString(parentComponent, message, title, Integer.toString(oldValue)); 058 if (newValue != null) { 059 try { 060 result = Integer.parseInt(newValue); 061 } catch (NumberFormatException e) { 062 result = oldValue; 063 } 064 } 065 return result; 066 } 067 068 /** 069 * Utility function to prompt for new integer value. Allows to constrain 070 * values using a Predicate (validator). 071 * <p> 072 * The validator may throw an {@link IllegalArgumentException} whose 073 * {@link IllegalArgumentException#getLocalizedMessage()} will be displayed. 074 * The Predicate may also simply return {@code false}, which causes just 075 * general message (the value is invalid) to be printed. If the Predicate 076 * rejects the input, the OK button is disabled and the user is unable to 077 * confirm the dialog. 078 * <p> 079 * The function returns the original value if the dialog was cancelled or 080 * the entered value was empty or invalid. Otherwise, it returns the new 081 * value entered by the user. 082 * 083 * @param parentComponent the parent component 084 * @param message the prompt message. 085 * @param title title for the dialog 086 * @param oldValue the original value 087 * @param validator the validator instance. May be {@code null} 088 * @return the updated value, or the original one. 089 */ 090 public static Integer promptForInteger(Component parentComponent, @Nonnull String message, @Nonnull String title, Integer oldValue, @CheckForNull Predicate<Integer> validator) { 091 Integer result = oldValue; 092 Integer newValue = promptForData(parentComponent, message, title, oldValue, validator, (val) -> { 093 try { 094 return Integer.valueOf(Integer.parseInt(val)); 095 } catch (NumberFormatException ex) { 096 // original exception ignored; wrong message. 097 throw new NumberFormatException(Bundle.getMessage("InputDialogNotNumber")); 098 } 099 }); 100 if (newValue != null) { 101 result = newValue; 102 } 103 return result; 104 } 105 106 private static <T> T promptForData(Component parentComponent, 107 @Nonnull String message, @Nonnull String title, T oldValue, 108 @CheckForNull Predicate<T> validator, 109 @CheckForNull Function<String, T> converter) { 110 String result = oldValue == null ? "" : oldValue.toString(); // NOI18N 111 JButton okOption = new JButton(Bundle.getMessage("ButtonOK")); // NOI18N 112 JButton cancelOption = new JButton(Bundle.getMessage("ButtonCancel")); // NOI18N 113 okOption.setDefaultCapable(true); 114 115 ValidatingInputPane<T> validating = new ValidatingInputPane<T>(converter) 116 .message(message) 117 .validator(validator) 118 .attachConfirmUI(okOption); 119 validating.setText(result); 120 JOptionPane pane = new JOptionPane(validating, JOptionPane.PLAIN_MESSAGE, 121 OK_CANCEL_OPTION, null, new Object[]{okOption, cancelOption}); 122 123 pane.putClientProperty("OptionPane.buttonOrientation", SwingConstants.RIGHT); // NOI18N 124 JDialog dialog = pane.createDialog(parentComponent, title); 125 dialog.getRootPane().setDefaultButton(okOption); 126 dialog.setResizable(true); 127 128 class AL implements ActionListener { 129 130 boolean confirmed; 131 132 @Override 133 public void actionPerformed(ActionEvent e) { 134 Object s = e.getSource(); 135 if (s == okOption) { 136 confirmed = true; 137 dialog.setVisible(false); 138 } 139 if (s == cancelOption) { 140 dialog.setVisible(false); 141 } 142 } 143 } 144 145 AL al = new AL(); 146 okOption.addActionListener(al); 147 cancelOption.addActionListener(al); 148 149 dialog.setVisible(true); 150 dialog.dispose(); 151 152 if (al.confirmed) { 153 T res = validating.getValue(); 154 if (res != null) { 155 return res; 156 } 157 } 158 return oldValue; 159 } 160 161 /** 162 * Utility function to prompt for new float value. 163 * 164 * @param parentComponent the parent component. 165 * @param message the prompt message 166 * @param title the dialog title 167 * @param oldValue the original float value 168 * @return the new float value 169 */ 170 public static float promptForFloat(Component parentComponent, String message, String title, float oldValue) { 171 float result = oldValue; 172 String newValue = promptForString(parentComponent, message, title, Float.toString(oldValue)); 173 if (newValue != null) { 174 try { 175 result = Float.parseFloat(newValue); 176 } catch (NumberFormatException e) { 177 result = oldValue; 178 } 179 } 180 return result; 181 } 182 183 /** 184 * Utility function to prompt for new double value. 185 * 186 * @param parentComponent the parent component 187 * @param message the prompt message 188 * @param title the dialog title 189 * @param oldValue the original double value 190 * @return the new double value 191 */ 192 public static double promptForDouble(Component parentComponent, String message, String title, double oldValue) { 193 double result = oldValue; 194 String newValue = promptForString(parentComponent, message, title, Double.toString(oldValue)); 195 if (newValue != null) { 196 try { 197 result = Double.parseDouble(newValue); 198 } catch (NumberFormatException e) { 199 result = oldValue; 200 } 201 } 202 return result; 203 } 204 205 /** 206 * Creates a min/max predicate which will check the bounds. Suitable for 207 * {@link #promptForInteger(java.awt.Component, java.lang.String, java.lang.String, Integer, java.util.function.Predicate)}. 208 * 209 * @param min minimum value. Use {@link Integer#MIN_VALUE} to disable 210 * check. 211 * @param max maximum value, inclusive. Use {@link Integer#MAX_VALUE} 212 * to disable check. 213 * @param valueLabel label to be included in the message. Must be already 214 * I18Ned. 215 * @return predicate instance 216 */ 217 public static Predicate<Integer> checkIntRange(Integer min, Integer max, String valueLabel) { 218 return new IntRangePredicate(min, max, valueLabel); 219 } 220 221 /** 222 * Base for range predicates (int, float). Checks for min/max - if 223 * configured, produces an exception with an appropriate message if check 224 * fails. 225 * 226 * @param <T> the data type 227 */ 228 private static abstract class NumberRangePredicate<T extends Number> implements Predicate<T> { 229 230 protected final T min; 231 protected final T max; 232 protected final String label; 233 234 public NumberRangePredicate(T min, T max, String label) { 235 this.min = min; 236 this.max = max; 237 this.label = label; 238 } 239 240 protected abstract boolean acceptLow(T val, T bound); 241 242 protected abstract boolean acceptHigh(T val, T bound); 243 244 @Override 245 public boolean test(T t) { 246 boolean ok = true; 247 248 if (min != null && !acceptLow(t, min)) { 249 ok = false; 250 } else if (max != null && !acceptHigh(t, max)) { 251 ok = false; 252 } 253 if (ok) { 254 return true; 255 } 256 final String msgKey; 257 if (label != null) { 258 if (min == null) { 259 msgKey = "NumberCheckOutOfRangeMax"; // NOI18N 260 } else if (max == null) { 261 msgKey = "NumberCheckOutOfRangeMin"; // NOI18N 262 } else { 263 msgKey = "NumberCheckOutOfRangeBoth"; // NOI18N 264 } 265 } else { 266 if (min == null) { 267 msgKey = "NumberCheckOutOfRangeMax2"; // NOI18N 268 } else if (max == null) { 269 msgKey = "NumberCheckOutOfRangeMin2"; // NOI18N 270 } else { 271 msgKey = "NumberCheckOutOfRangeBoth2"; // NOI18N 272 } 273 } 274 throw new IllegalArgumentException( 275 Bundle.getMessage(msgKey, label, min, max) 276 ); 277 } 278 } 279 280 // This is currently unused, ready for converting the 281 // promptForFloat 282 static final class FloatRangePredicate extends NumberRangePredicate<Float> { 283 284 public FloatRangePredicate(Float min, Float max, String label) { 285 super(min, max, label); 286 } 287 288 @Override 289 protected boolean acceptLow(Float val, Float bound) { 290 return val >= bound; 291 } 292 293 @Override 294 protected boolean acceptHigh(Float val, Float bound) { 295 return val <= bound; 296 } 297 } 298 299 static final class IntRangePredicate extends NumberRangePredicate<Integer> { 300 301 public IntRangePredicate(Integer min, Integer max, String label) { 302 super(min, max, label); 303 } 304 305 @Override 306 protected boolean acceptLow(Integer val, Integer bound) { 307 return val >= bound; 308 } 309 310 @Override 311 protected boolean acceptHigh(Integer val, Integer bound) { 312 return val <= bound; 313 } 314 } 315 316 // initialize logging 317 // private final static Logger log = LoggerFactory.getLogger(QuickPromptUtil.class); 318}