001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.awt.Dialog.ModalityType; 008import java.awt.GraphicsEnvironment; 009import java.awt.event.ActionEvent; 010import java.awt.event.KeyEvent; 011import java.awt.event.WindowAdapter; 012import java.awt.event.WindowEvent; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.HashSet; 016import java.util.List; 017 018import javax.swing.AbstractAction; 019import javax.swing.Action; 020import javax.swing.Icon; 021import javax.swing.JButton; 022import javax.swing.JComponent; 023import javax.swing.JDialog; 024import javax.swing.JOptionPane; 025import javax.swing.KeyStroke; 026import javax.swing.event.ChangeEvent; 027import javax.swing.event.ChangeListener; 028 029import org.openstreetmap.josm.Main; 030import org.openstreetmap.josm.gui.help.HelpBrowser; 031import org.openstreetmap.josm.gui.help.HelpUtil; 032import org.openstreetmap.josm.gui.util.GuiHelper; 033import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 034import org.openstreetmap.josm.tools.ImageProvider; 035import org.openstreetmap.josm.tools.InputMapUtils; 036import org.openstreetmap.josm.tools.WindowGeometry; 037 038public final class HelpAwareOptionPane { 039 040 private HelpAwareOptionPane() { 041 // Hide default constructor for utils classes 042 } 043 044 public static class ButtonSpec { 045 public final String text; 046 public final Icon icon; 047 public final String tooltipText; 048 public final String helpTopic; 049 private boolean enabled; 050 051 private final Collection<ChangeListener> listeners = new HashSet<>(); 052 053 /** 054 * Constructs a new {@code ButtonSpec}. 055 * @param text the button text 056 * @param icon the icon to display. Can be null 057 * @param tooltipText the tooltip text. Can be null. 058 * @param helpTopic the help topic. Can be null. 059 */ 060 public ButtonSpec(String text, Icon icon, String tooltipText, String helpTopic) { 061 this(text, icon, tooltipText, helpTopic, true); 062 } 063 064 /** 065 * Constructs a new {@code ButtonSpec}. 066 * @param text the button text 067 * @param icon the icon to display. Can be null 068 * @param tooltipText the tooltip text. Can be null. 069 * @param helpTopic the help topic. Can be null. 070 * @param enabled the enabled status 071 * @since 5951 072 */ 073 public ButtonSpec(String text, Icon icon, String tooltipText, String helpTopic, boolean enabled) { 074 this.text = text; 075 this.icon = icon; 076 this.tooltipText = tooltipText; 077 this.helpTopic = helpTopic; 078 setEnabled(enabled); 079 } 080 081 /** 082 * Determines if this button spec is enabled 083 * @return {@code true} if this button spec is enabled, {@code false} otherwise 084 * @since 6051 085 */ 086 public final boolean isEnabled() { 087 return enabled; 088 } 089 090 /** 091 * Enables or disables this button spec, depending on the value of the parameter {@code b}. 092 * @param enabled if {@code true}, this button spec is enabled; otherwise this button spec is disabled 093 * @since 6051 094 */ 095 public final void setEnabled(boolean enabled) { 096 if (this.enabled != enabled) { 097 this.enabled = enabled; 098 ChangeEvent event = new ChangeEvent(this); 099 for (ChangeListener listener : listeners) { 100 listener.stateChanged(event); 101 } 102 } 103 } 104 105 private boolean addChangeListener(ChangeListener listener) { 106 return listener != null && listeners.add(listener); 107 } 108 } 109 110 private static class DefaultAction extends AbstractAction { 111 private final JDialog dialog; 112 private final JOptionPane pane; 113 private final int value; 114 115 DefaultAction(JDialog dialog, JOptionPane pane, int value) { 116 this.dialog = dialog; 117 this.pane = pane; 118 this.value = value; 119 } 120 121 @Override 122 public void actionPerformed(ActionEvent e) { 123 pane.setValue(value); 124 dialog.setVisible(false); 125 } 126 } 127 128 /** 129 * Creates the list buttons to be displayed in the option pane dialog. 130 * 131 * @param options the option. If null, just creates an OK button and a help button 132 * @param helpTopic the help topic. The context sensitive help of all buttons is equal 133 * to the context sensitive help of the whole dialog 134 * @return the list of buttons 135 */ 136 private static List<JButton> createOptionButtons(ButtonSpec[] options, String helpTopic) { 137 List<JButton> buttons = new ArrayList<>(); 138 if (options == null) { 139 JButton b = new JButton(tr("OK")); 140 b.setIcon(ImageProvider.get("ok")); 141 b.setToolTipText(tr("Click to close the dialog")); 142 b.setFocusable(true); 143 buttons.add(b); 144 } else { 145 for (final ButtonSpec spec: options) { 146 final JButton b = new JButton(spec.text); 147 b.setIcon(spec.icon); 148 b.setToolTipText(spec.tooltipText == null ? "" : spec.tooltipText); 149 if (helpTopic != null) { 150 HelpUtil.setHelpContext(b, helpTopic); 151 } 152 b.setFocusable(true); 153 b.setEnabled(spec.isEnabled()); 154 spec.addChangeListener(new ChangeListener() { 155 @Override public void stateChanged(ChangeEvent e) { 156 b.setEnabled(spec.isEnabled()); 157 } 158 }); 159 buttons.add(b); 160 } 161 } 162 return buttons; 163 } 164 165 /** 166 * Creates the help button 167 * 168 * @param helpTopic the help topic 169 * @return the help button 170 */ 171 private static JButton createHelpButton(final String helpTopic) { 172 JButton b = new JButton(tr("Help")); 173 b.setIcon(ImageProvider.get("help")); 174 b.setToolTipText(tr("Show help information")); 175 HelpUtil.setHelpContext(b, helpTopic); 176 Action a = new AbstractAction() { 177 @Override 178 public void actionPerformed(ActionEvent e) { 179 HelpBrowser.setUrlForHelpTopic(helpTopic); 180 } 181 }; 182 b.addActionListener(a); 183 InputMapUtils.enableEnter(b); 184 return b; 185 } 186 187 /** 188 * Displays an option dialog which is aware of a help context. If <code>helpTopic</code> isn't null, 189 * the dialog includes a "Help" button and launches the help browser if the user presses F1. If the 190 * user clicks on the "Help" button the option dialog remains open and JOSM launches the help 191 * browser. 192 * 193 * <code>helpTopic</code> is the trailing part of a JOSM online help URL, i.e. the part after the leading 194 * <code>https://josm.openstreetmap.de/wiki/Help</code>. It should start with a leading '/' and it 195 * may include an anchor after a '#'. 196 * 197 * <strong>Examples</strong> 198 * <ul> 199 * <li>/Dialogs/RelationEditor</li> 200 * <li>/Dialogs/RelationEditor#ConflictInData</li> 201 * </ul> 202 * 203 * In addition, the option buttons display JOSM icons, similar to ExtendedDialog. 204 * 205 * @param parentComponent the parent component 206 * @param msg the message 207 * @param title the title 208 * @param messageType the message type (see {@link JOptionPane}) 209 * @param icon the icon to display. Can be null. 210 * @param options the list of options to display. Can be null. 211 * @param defaultOption the default option. Can be null. 212 * @param helpTopic the help topic. Can be null. 213 * @return the index of the selected option or {@link JOptionPane#CLOSED_OPTION} 214 */ 215 public static int showOptionDialog(Component parentComponent, Object msg, String title, int messageType, 216 Icon icon, final ButtonSpec[] options, final ButtonSpec defaultOption, final String helpTopic) { 217 final List<JButton> buttons = createOptionButtons(options, helpTopic); 218 if (helpTopic != null) { 219 buttons.add(createHelpButton(helpTopic)); 220 } 221 222 JButton defaultButton = null; 223 if (options != null && defaultOption != null) { 224 for (int i = 0; i < options.length; i++) { 225 if (options[i] == defaultOption) { 226 defaultButton = buttons.get(i); 227 break; 228 } 229 } 230 } 231 232 final JOptionPane pane = new JOptionPane( 233 msg instanceof String ? new JMultilineLabel((String) msg, true) : msg, 234 messageType, 235 JOptionPane.DEFAULT_OPTION, 236 icon, 237 buttons.toArray(), 238 defaultButton 239 ); 240 241 // Log message. Useful for bug reports and unit tests 242 switch (messageType) { 243 case JOptionPane.ERROR_MESSAGE: 244 Main.error(title + " - " + msg); 245 break; 246 case JOptionPane.WARNING_MESSAGE: 247 Main.warn(title + " - " + msg); 248 break; 249 default: 250 Main.info(title + " - " + msg); 251 } 252 253 if (!GraphicsEnvironment.isHeadless()) { 254 doShowOptionDialog(parentComponent, title, options, defaultOption, helpTopic, buttons, pane); 255 } 256 return pane.getValue() instanceof Integer ? (Integer) pane.getValue() : JOptionPane.OK_OPTION; 257 } 258 259 private static void doShowOptionDialog(Component parentComponent, String title, final ButtonSpec[] options, 260 final ButtonSpec defaultOption, final String helpTopic, final List<JButton> buttons, 261 final JOptionPane pane) { 262 final JDialog dialog = new JDialog( 263 GuiHelper.getFrameForComponent(parentComponent), 264 title, 265 ModalityType.DOCUMENT_MODAL 266 ); 267 dialog.setContentPane(pane); 268 dialog.addWindowListener(new WindowAdapter() { 269 @Override 270 public void windowClosing(WindowEvent e) { 271 pane.setValue(JOptionPane.CLOSED_OPTION); 272 super.windowClosed(e); 273 } 274 275 @Override 276 public void windowOpened(WindowEvent e) { 277 if (defaultOption != null && options != null && options.length > 0) { 278 int i; 279 for (i = 0; i < options.length; i++) { 280 if (options[i] == defaultOption) { 281 break; 282 } 283 } 284 if (i >= options.length) { 285 buttons.get(0).requestFocusInWindow(); 286 } 287 buttons.get(i).requestFocusInWindow(); 288 } else { 289 buttons.get(0).requestFocusInWindow(); 290 } 291 } 292 }); 293 dialog.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( 294 KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close"); 295 dialog.getRootPane().getActionMap().put("close", new AbstractAction() { 296 @Override 297 public void actionPerformed(ActionEvent e) { 298 pane.setValue(JOptionPane.CLOSED_OPTION); 299 dialog.setVisible(false); 300 } 301 }); 302 303 if (options != null) { 304 for (int i = 0; i < options.length; i++) { 305 final DefaultAction action = new DefaultAction(dialog, pane, i); 306 buttons.get(i).addActionListener(action); 307 buttons.get(i).getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter"); 308 buttons.get(i).getActionMap().put("enter", action); 309 } 310 } else { 311 final DefaultAction action = new DefaultAction(dialog, pane, 0); 312 buttons.get(0).addActionListener(action); 313 buttons.get(0).getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter"); 314 buttons.get(0).getActionMap().put("enter", action); 315 } 316 317 dialog.pack(); 318 WindowGeometry.centerOnScreen(dialog.getSize()).applySafe(dialog); 319 if (helpTopic != null) { 320 HelpUtil.setHelpContext(dialog.getRootPane(), helpTopic); 321 } 322 dialog.setVisible(true); 323 } 324 325 /** 326 * Displays an option dialog which is aware of a help context. 327 * 328 * @param parentComponent the parent component 329 * @param msg the message 330 * @param title the title 331 * @param messageType the message type (see {@link JOptionPane}) 332 * @param helpTopic the help topic. Can be null. 333 * @return the index of the selected option or {@link JOptionPane#CLOSED_OPTION} 334 * @see #showOptionDialog(Component, Object, String, int, Icon, ButtonSpec[], ButtonSpec, String) 335 */ 336 public static int showOptionDialog(Component parentComponent, Object msg, String title, int messageType, String helpTopic) { 337 return showOptionDialog(parentComponent, msg, title, messageType, null, null, null, helpTopic); 338 } 339 340 /** 341 * Run it in Event Dispatch Thread. 342 * This version does not return anything, so it is more like {@code showMessageDialog}. 343 * 344 * It can be used, when you need to show a message dialog from a worker thread, 345 * e.g. from {@code PleaseWaitRunnable}. 346 * 347 * @param parentComponent the parent component 348 * @param msg the message 349 * @param title the title 350 * @param messageType the message type (see {@link JOptionPane}) 351 * @param helpTopic the help topic. Can be null. 352 */ 353 public static void showMessageDialogInEDT(final Component parentComponent, final Object msg, final String title, 354 final int messageType, final String helpTopic) { 355 GuiHelper.runInEDT(new Runnable() { 356 @Override 357 public void run() { 358 showOptionDialog(parentComponent, msg, title, messageType, null, null, null, helpTopic); 359 } 360 }); 361 } 362}