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}