001package jmri.util.swing;
002
003import java.awt.*;
004import java.util.Locale;
005
006import javax.annotation.CheckForNull;
007import javax.annotation.Nonnull;
008import javax.swing.*;
009
010/**
011 * JmriJOptionPane provides a set of static methods to display Dialogs and retrieve user input.
012 * These can directly replace the javax.swing.JOptionPane static methods.
013 * <p>
014 * If the parentComponent is null, all Dialogs created will be Modal.
015 * These will block the whole JVM UI until they are closed.
016 * These may appear behind Window frames with Always On Top enabled and may not be accessible.
017 * These Dialogs are positioned in the centre of the screen.
018 * <p>
019 * If a parentComponent is provided, the Dialogs will be created Modal to
020 * ( will block ) the parent Window Frame, other Frames are not blocked.
021 * These Dialogs will appear in the centre of the parent Frame.
022 *
023 * @since 5.5.4
024 * @author Steve Young Copyright (C) 2023
025 */
026public class JmriJOptionPane {
027
028    public static final int CANCEL_OPTION = JOptionPane.CANCEL_OPTION;
029    public static final int OK_OPTION = JOptionPane.OK_OPTION;
030    public static final int OK_CANCEL_OPTION = JOptionPane.OK_CANCEL_OPTION;
031    public static final int YES_OPTION = JOptionPane.YES_OPTION;
032    public static final int YES_NO_OPTION = JOptionPane.YES_NO_OPTION;
033    public static final int YES_NO_CANCEL_OPTION = JOptionPane.YES_NO_CANCEL_OPTION;
034    public static final int NO_OPTION = JOptionPane.NO_OPTION;
035
036    public static final int CLOSED_OPTION = JOptionPane.CLOSED_OPTION;
037    public static final int DEFAULT_OPTION = JOptionPane.DEFAULT_OPTION;
038    public static final Object UNINITIALIZED_VALUE = JOptionPane.UNINITIALIZED_VALUE;
039
040    public static final int ERROR_MESSAGE = JOptionPane.ERROR_MESSAGE;
041    public static final int INFORMATION_MESSAGE = JOptionPane.INFORMATION_MESSAGE;
042    public static final int PLAIN_MESSAGE = JOptionPane.PLAIN_MESSAGE;
043    public static final int QUESTION_MESSAGE = JOptionPane.QUESTION_MESSAGE;
044    public static final int WARNING_MESSAGE = JOptionPane.WARNING_MESSAGE;
045
046    public static final String YES_STRING = UIManager.getString("OptionPane.yesButtonText", Locale.getDefault());
047    public static final String NO_STRING = UIManager.getString("OptionPane.noButtonText", Locale.getDefault());
048
049    // class only supplies static methods
050    protected JmriJOptionPane(){}
051
052    /**
053     * Displays an informational message dialog with an OK button.
054     * @param parentComponent The parent component relative to which the dialog is displayed.
055     * @param message         The message to be displayed in the dialog.
056     * @throws HeadlessException if the current environment is headless (no GUI available).
057     */
058    public static void showMessageDialog(@CheckForNull Component parentComponent,
059        Object message) throws HeadlessException {
060        showMessageDialog(parentComponent, message, 
061            UIManager.getString("OptionPane.messageDialogTitle", Locale.getDefault()),
062            INFORMATION_MESSAGE);
063    }
064
065    /**
066     * Displays a message dialog with an OK button.
067     * @param parentComponent The parent component relative to which the dialog is displayed.
068     * @param message         The message to be displayed in the dialog.
069     * @param title           The title of the dialog.
070     * @param messageType     The type of message to be displayed (e.g., {@link #WARNING_MESSAGE}).
071     * @throws HeadlessException if the current environment is headless (no GUI available).
072     */
073    public static void showMessageDialog(@CheckForNull Component parentComponent,
074        Object message, String title, int messageType) {
075        showOptionDialog(parentComponent, message, title, DEFAULT_OPTION,
076            messageType, null, null, null);
077    }
078
079    /**
080     * Displays a Non-Modal message dialog with an OK button.
081     * @param parentComponent The parent component relative to which the dialog is displayed.
082     * @param message         The message to be displayed in the dialog.
083     * @param title           The title of the dialog.
084     * @param messageType     The type of message to be displayed (e.g., {@link #WARNING_MESSAGE}).
085     * @param callback        Code to run when the Dialog is closed. Can be null.
086     * @throws HeadlessException if the current environment is headless (no GUI available).
087     */
088    public static void showMessageDialogNonModal(@CheckForNull Component parentComponent,
089        Object message, String title, int messageType, @CheckForNull final Runnable callback ) {
090
091        JOptionPane pane = new JOptionPane(message, messageType);
092        JDialog dialog = pane.createDialog(parentComponent, title);
093        Window w = findWindowForComponent(parentComponent);
094        if ( w != null ) {
095            JDialogListener pcl = new JDialogListener(dialog);
096            w.addPropertyChangeListener(pcl);
097            pane.addPropertyChangeListener(JOptionPane.VALUE_PROPERTY, unused ->
098                w.removePropertyChangeListener(pcl));
099        }
100        if ( callback !=null ) {
101            pane.addPropertyChangeListener(JOptionPane.VALUE_PROPERTY, unused -> callback.run());
102        }
103        setDialogLocation(parentComponent, dialog);
104        dialog.setModal(false);
105        dialog.setAlwaysOnTop(true);
106        dialog.toFront();
107        dialog.setVisible(true);
108    }
109
110    /**
111     * Displays a confirmation dialog with a message and title.
112     * The dialog includes options for the user to confirm or cancel an action.
113     *
114     * @param parentComponent The parent component relative to which the dialog is displayed.
115     * @param message         The message to be displayed in the dialog.
116     * @param title           The title of the dialog.
117     * @param optionType      The type of options to be displayed (e.g., {@link #YES_NO_OPTION}, {@link #OK_CANCEL_OPTION}).
118     * @return An integer representing the user's choice: {@link #YES_OPTION}, {@link #NO_OPTION}, {@link #CANCEL_OPTION}, or {@link #CLOSED_OPTION}.
119     * @throws HeadlessException if the current environment is headless (no GUI available).
120     */
121    public static int showConfirmDialog(@CheckForNull Component parentComponent,
122        Object message, String title, int optionType)
123        throws HeadlessException {
124        return showOptionDialog(parentComponent, message, title, optionType,
125            QUESTION_MESSAGE, null, null, null);
126    }
127
128    /**
129     * Displays a confirmation dialog with a message and title.The dialog includes options for the user to confirm or cancel an action.
130     *
131     * @param parentComponent The parent component relative to which the dialog is displayed.
132     * @param message         The message to be displayed in the dialog.
133     * @param title           The title of the dialog.
134     * @param optionType      The type of options to be displayed (e.g., {@link #YES_NO_OPTION}, {@link #OK_CANCEL_OPTION}).
135     * @param messageType     The type of message to be displayed (e.g., {@link #ERROR_MESSAGE}).
136     * @return An integer representing the user's choice: {@link #YES_OPTION}, {@link #NO_OPTION}, {@link #CANCEL_OPTION}, or {@link #CLOSED_OPTION}.
137     * @throws HeadlessException if the current environment is headless (no GUI available).
138     */
139    public static int showConfirmDialog(@CheckForNull Component parentComponent,
140        Object message, String title, int optionType, int messageType)
141        throws HeadlessException {
142        return showOptionDialog(parentComponent, message, title, optionType,
143            messageType, null, null, null);
144    }
145
146    /**
147     * Displays a custom option dialog.
148     * @param parentComponent The parent component relative to which the dialog is displayed.
149     * @param message         The message to be displayed in the dialog.
150     * @param title           The title of the dialog.
151     * @param optionType      The type of options to be displayed (e.g., {@link #YES_NO_OPTION}, {@link #OK_CANCEL_OPTION}).
152     * @param messageType     The type of message to be displayed (e.g., {@link #INFORMATION_MESSAGE}, {@link #WARNING_MESSAGE}).
153     * @param icon            The icon to be displayed in the dialog.
154     * @param options         An array of objects representing the options available to the user.
155     * @param initialValue    The initial value selected in the dialog.
156     * @return An integer representing the index of the selected option, or {@link #CLOSED_OPTION} if the dialog is closed.
157     * @throws HeadlessException If the current environment is headless (no GUI available).
158     */
159    public static int showOptionDialog(@CheckForNull Component parentComponent,
160        Object message, String title, int optionType, int messageType,
161        Icon icon, Object[] options, Object initialValue)
162        throws HeadlessException {
163        log.debug("showOptionDialog comp {} ", parentComponent);
164
165        JOptionPane pane = new JOptionPane(message, messageType,
166            optionType, icon, options, initialValue);
167        pane.setInitialValue(initialValue);
168        displayDialog(pane, parentComponent, title);
169
170        Object selectedValue = pane.getValue();
171        if ( selectedValue == null ) {
172            return CLOSED_OPTION;
173        }
174        if ( options == null ) {
175            if ( selectedValue instanceof Integer ) {
176                return ((Integer)selectedValue);
177            }
178            return CLOSED_OPTION;
179        }
180        for(int counter = 0, maxCounter = options.length; counter < maxCounter; counter++ ) {
181            if ( options[counter].equals(selectedValue)) {
182                return counter;
183            }
184        }
185        return CLOSED_OPTION;
186    }
187
188    /**
189     * Displays a String input dialog.
190     * @param parentComponent       The parent component relative to which the dialog is displayed.
191     * @param message               The message to be displayed in the dialog.
192     * @param initialSelectionValue The initial value pre-selected in the input dialog.
193     * @return The user's String input value, or {@code null} if the dialog is closed or the input value is uninitialized.
194     * @throws HeadlessException   if the current environment is headless (no GUI available).
195     */
196    @CheckForNull
197    public static String showInputDialog(@CheckForNull Component parentComponent,
198        String message, String initialSelectionValue ){
199        return (String)showInputDialog(parentComponent, message,
200            UIManager.getString("OptionPane.inputDialogTitle",
201            Locale.getDefault()), QUESTION_MESSAGE, null, null,
202            initialSelectionValue);
203    }
204
205    /**
206     * Displays a String input dialog.
207     * @param parentComponent       The parent component relative to which the dialog is displayed.
208     * @param message               The message to be displayed in the dialog.
209     * @param title                 The dialog Title.
210     * @param messageType           The type of message to be displayed (e.g., {@link #QUESTION_MESSAGE} ).
211     * @return The user's String input value, or {@code null} if the dialog is closed or the input value is uninitialized.
212     * @throws HeadlessException   if the current environment is headless (no GUI available).
213     */
214    @CheckForNull
215    public static String showInputDialog(@CheckForNull Component parentComponent,
216        String message, String title, int messageType ){
217        return (String)showInputDialog(parentComponent, message,
218            title, messageType, null, null,
219            "");
220    }
221
222    /**
223     * Displays an Object input dialog.
224     * @param parentComponent       The parent component relative to which the dialog is displayed.
225     * @param message               The message to be displayed in the dialog.
226     * @param initialSelectionValue The initial value pre-selected in the input dialog.
227     * @return The user's input value, or {@code null} if the dialog is closed or the input value is uninitialized.
228     * @throws HeadlessException   if the current environment is headless (no GUI available).
229     */
230    @CheckForNull
231    public static Object showInputDialog(@CheckForNull Component parentComponent,
232        String message, Object initialSelectionValue ){
233        return showInputDialog(parentComponent, message,
234            UIManager.getString("OptionPane.inputDialogTitle",
235            Locale.getDefault()), QUESTION_MESSAGE, null, null,
236            initialSelectionValue);
237    }
238
239    /**
240     * Displays an input dialog.
241     * @param parentComponent      The parent component relative to which the dialog is displayed.
242     * @param message              The message to be displayed in the dialog.
243     * @param title                The title of the dialog.
244     * @param messageType          The type of message to be displayed (e.g., {@link #INFORMATION_MESSAGE}, {@link #WARNING_MESSAGE}).
245     * @param icon                 The icon to be displayed in the dialog.
246     * @param selectionValues      An array of objects representing the input selection values.
247     * @param initialSelectionValue The initial value pre-selected in the input dialog.
248     * @return The user's input value, or {@code null} if the dialog is closed or the input value is uninitialized.
249     * @throws HeadlessException   if the current environment is headless (no GUI available).
250     */
251    @CheckForNull
252    public static Object showInputDialog(@CheckForNull Component parentComponent,
253        Object message, String title, int messageType, Icon icon,
254        Object[] selectionValues, Object initialSelectionValue)
255        throws HeadlessException {
256        JOptionPane pane = new JOptionPane(message, messageType,
257            OK_CANCEL_OPTION, icon, null, initialSelectionValue);
258
259        pane.setWantsInput(true);
260        pane.setSelectionValues(selectionValues);
261        pane.setInitialSelectionValue(initialSelectionValue);
262        pane.selectInitialValue();
263        displayDialog(pane, parentComponent, title);
264
265        Object value = pane.getInputValue();
266        if (value == UNINITIALIZED_VALUE) {
267            return null;
268        }
269        return value;
270    }
271
272    private static void displayDialog(JOptionPane pane, Component parentComponent, String title){
273        pane.setComponentOrientation(JOptionPane.getRootFrame().getComponentOrientation());
274        Window w = findWindowForComponent(parentComponent);
275        JDialog dialog = pane.createDialog(parentComponent, title);
276        JDialogListener pcl = new JDialogListener(dialog);
277        if ( w != null ) {
278            dialog.setModalityType(Dialog.ModalityType.DOCUMENT_MODAL);
279            w.addPropertyChangeListener(pcl);
280        }
281        setDialogLocation(parentComponent, dialog);
282        dialog.setAlwaysOnTop(true);
283        dialog.toFront();
284        dialog.setVisible(true); // and waits for input
285        dialog.dispose();
286        if ( w != null ) {
287            w.removePropertyChangeListener(pcl);
288        }
289    }
290
291    /**
292     * Sets the position of a dialog relative to a parent component.
293     * This method positions the dialog at the centre of
294     * the parent component or its parent window.
295     *
296     * @param parentComponent The parent component relative to which the dialog should be positioned.
297     * @param dialog           The dialog whose position is being set.
298     */
299    private static void setDialogLocation( @CheckForNull Component parentComponent, @Nonnull Dialog dialog) {
300        log.debug("set dialog position for comp {} dialog {}", parentComponent, dialog.getTitle());
301        int centreWidth;
302        int centreHeight;
303        Window w = findWindowForComponent(parentComponent);
304        if ( w == null || !w.isVisible() ) {
305            centreWidth = Toolkit.getDefaultToolkit().getScreenSize().width / 2;
306            centreHeight = Toolkit.getDefaultToolkit().getScreenSize().height / 2;
307        } else {
308            Point topLeft = w.getLocationOnScreen();
309            Dimension size = w.getSize();
310            centreWidth = topLeft.x + ( size.width / 2 );
311            centreHeight = topLeft.y + ( size.height / 2 );
312        }
313        int centerX = centreWidth - ( dialog.getWidth() / 2 );
314        int centerY = centreHeight - ( dialog.getHeight() / 2 );
315        // set top left of Dialog at least 0px into the screen.
316        dialog.setLocation( new Point(Math.max(0, centerX), Math.max(0, centerY)));
317    }
318
319    @CheckForNull
320    private static Window findWindowForComponent(@CheckForNull Component component){
321        if (component == null) {
322            return null;
323        }
324        if (component instanceof JPopupMenu ) {
325            return findWindowForComponent(((JPopupMenu)component).getInvoker());
326        }
327        if (component instanceof JFrame ) {
328            return (JFrame)component;
329        }
330        if (component instanceof Window) {
331            return (Window) component;
332        }
333        return findWindowForComponent(component.getParent());
334    }
335
336    /**
337     * Find the parent Window, normally from a java.awt.Component .
338     * <p>
339     * If the component is within a JPopupMenu,
340     * the parent Window of the Popup Menu will be returned, not the Frame of
341     * the Popup Menu itself ( which may no longer be visible ).
342     * @param object a child component of the Window.
343     * @return the parent Window, or null if none found.
344     */
345    @CheckForNull
346    public static Window findWindowForObject( @CheckForNull Object object ){
347        if ( object instanceof Component ) {
348            return JmriJOptionPane.findWindowForComponent((Component)object);
349        }
350        return null;
351    }
352
353    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriJOptionPane.class);
354
355}