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}