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