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}