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.Dimension; 008import java.awt.Frame; 009import java.awt.GridBagConstraints; 010import java.awt.GridBagLayout; 011import java.awt.Insets; 012import java.awt.event.ActionEvent; 013import java.awt.event.KeyEvent; 014import java.util.ArrayList; 015import java.util.Arrays; 016import java.util.Collections; 017import java.util.HashSet; 018import java.util.List; 019import java.util.Set; 020 021import javax.swing.AbstractAction; 022import javax.swing.Action; 023import javax.swing.Icon; 024import javax.swing.JButton; 025import javax.swing.JDialog; 026import javax.swing.JLabel; 027import javax.swing.JOptionPane; 028import javax.swing.JPanel; 029import javax.swing.JScrollBar; 030import javax.swing.JScrollPane; 031import javax.swing.KeyStroke; 032import javax.swing.UIManager; 033 034import org.openstreetmap.josm.Main; 035import org.openstreetmap.josm.gui.help.HelpBrowser; 036import org.openstreetmap.josm.gui.help.HelpUtil; 037import org.openstreetmap.josm.gui.util.GuiHelper; 038import org.openstreetmap.josm.gui.util.WindowGeometry; 039import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 040import org.openstreetmap.josm.io.OnlineResource; 041import org.openstreetmap.josm.tools.GBC; 042import org.openstreetmap.josm.tools.ImageProvider; 043import org.openstreetmap.josm.tools.InputMapUtils; 044import org.openstreetmap.josm.tools.Logging; 045import org.openstreetmap.josm.tools.Utils; 046 047/** 048 * General configurable dialog window. 049 * 050 * If dialog is modal, you can use {@link #getValue()} to retrieve the 051 * button index. Note that the user can close the dialog 052 * by other means. This is usually equivalent to cancel action. 053 * 054 * For non-modal dialogs, {@link #buttonAction(int, ActionEvent)} can be overridden. 055 * 056 * There are various options, see below. 057 * 058 * Note: The button indices are counted from 1 and upwards. 059 * So for {@link #getValue()}, {@link #setDefaultButton(int)} and 060 * {@link #setCancelButton} the first button has index 1. 061 * 062 * Simple example: 063 * <pre> 064 * ExtendedDialog ed = new ExtendedDialog( 065 * Main.parent, tr("Dialog Title"), 066 * new String[] {tr("Ok"), tr("Cancel")}); 067 * ed.setButtonIcons(new String[] {"ok", "cancel"}); // optional 068 * ed.setIcon(JOptionPane.WARNING_MESSAGE); // optional 069 * ed.setContent(tr("Really proceed? Interesting things may happen...")); 070 * ed.showDialog(); 071 * if (ed.getValue() == 1) { // user clicked first button "Ok" 072 * // proceed... 073 * } 074 * </pre> 075 */ 076public class ExtendedDialog extends JDialog implements IExtendedDialog { 077 private final boolean disposeOnClose; 078 private volatile int result; 079 public static final int DialogClosedOtherwise = 0; 080 private boolean toggleable; 081 private String rememberSizePref = ""; 082 private transient WindowGeometry defaultWindowGeometry; 083 private String togglePref = ""; 084 private int toggleValue = -1; 085 private ConditionalOptionPaneUtil.MessagePanel togglePanel; 086 private Component parent; 087 private Component content; 088 private final String[] bTexts; 089 private String[] bToolTipTexts; 090 private transient Icon[] bIcons; 091 private Set<Integer> cancelButtonIdx = Collections.emptySet(); 092 private int defaultButtonIdx = 1; 093 protected JButton defaultButton; 094 private transient Icon icon; 095 private boolean modal; 096 private boolean focusOnDefaultButton; 097 098 /** true, if the dialog should include a help button */ 099 private boolean showHelpButton; 100 /** the help topic */ 101 private String helpTopic; 102 103 /** 104 * set to true if the content of the extended dialog should 105 * be placed in a {@link JScrollPane} 106 */ 107 private boolean placeContentInScrollPane; 108 109 // For easy access when inherited 110 protected transient Insets contentInsets = new Insets(10, 5, 0, 5); 111 protected transient List<JButton> buttons = new ArrayList<>(); 112 113 /** 114 * This method sets up the most basic options for the dialog. Add more 115 * advanced features with dedicated methods. 116 * Possible features: 117 * <ul> 118 * <li><code>setButtonIcons</code></li> 119 * <li><code>setContent</code></li> 120 * <li><code>toggleEnable</code></li> 121 * <li><code>toggleDisable</code></li> 122 * <li><code>setToggleCheckboxText</code></li> 123 * <li><code>setRememberWindowGeometry</code></li> 124 * </ul> 125 * 126 * When done, call <code>showDialog</code> to display it. You can receive 127 * the user's choice using <code>getValue</code>. Have a look at this function 128 * for possible return values. 129 * 130 * @param parent The parent element that will be used for position and maximum size 131 * @param title The text that will be shown in the window titlebar 132 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 133 */ 134 public ExtendedDialog(Component parent, String title, String... buttonTexts) { 135 this(parent, title, buttonTexts, true, true); 136 } 137 138 /** 139 * Same as above but lets you define if the dialog should be modal. 140 * @param parent The parent element that will be used for position and maximum size 141 * @param title The text that will be shown in the window titlebar 142 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 143 * @param modal Set it to {@code true} if you want the dialog to be modal 144 */ 145 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal) { 146 this(parent, title, buttonTexts, modal, true); 147 } 148 149 /** 150 * Same as above but lets you define if the dialog should be disposed on close. 151 * @param parent The parent element that will be used for position and maximum size 152 * @param title The text that will be shown in the window titlebar 153 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 154 * @param modal Set it to {@code true} if you want the dialog to be modal 155 * @param disposeOnClose whether to call {@link #dispose} when closing the dialog 156 */ 157 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal, boolean disposeOnClose) { 158 super(searchRealParent(parent), title, modal ? ModalityType.DOCUMENT_MODAL : ModalityType.MODELESS); 159 this.parent = parent; 160 this.modal = modal; 161 bTexts = Utils.copyArray(buttonTexts); 162 if (disposeOnClose) { 163 setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 164 } 165 this.disposeOnClose = disposeOnClose; 166 } 167 168 private static Frame searchRealParent(Component parent) { 169 if (parent == null) { 170 return null; 171 } else { 172 return GuiHelper.getFrameForComponent(parent); 173 } 174 } 175 176 @Override 177 public ExtendedDialog setButtonIcons(Icon... buttonIcons) { 178 this.bIcons = Utils.copyArray(buttonIcons); 179 return this; 180 } 181 182 @Override 183 public ExtendedDialog setButtonIcons(String... buttonIcons) { 184 bIcons = new Icon[buttonIcons.length]; 185 for (int i = 0; i < buttonIcons.length; ++i) { 186 bIcons[i] = ImageProvider.get(buttonIcons[i]); 187 } 188 return this; 189 } 190 191 @Override 192 public ExtendedDialog setToolTipTexts(String... toolTipTexts) { 193 this.bToolTipTexts = Utils.copyArray(toolTipTexts); 194 return this; 195 } 196 197 @Override 198 public ExtendedDialog setContent(Component content) { 199 return setContent(content, true); 200 } 201 202 @Override 203 public ExtendedDialog setContent(Component content, boolean placeContentInScrollPane) { 204 this.content = content; 205 this.placeContentInScrollPane = placeContentInScrollPane; 206 return this; 207 } 208 209 @Override 210 public ExtendedDialog setContent(String message) { 211 return setContent(string2label(message), false); 212 } 213 214 @Override 215 public ExtendedDialog setIcon(Icon icon) { 216 this.icon = icon; 217 return this; 218 } 219 220 @Override 221 public ExtendedDialog setIcon(int messageType) { 222 switch (messageType) { 223 case JOptionPane.ERROR_MESSAGE: 224 return setIcon(UIManager.getIcon("OptionPane.errorIcon")); 225 case JOptionPane.INFORMATION_MESSAGE: 226 return setIcon(UIManager.getIcon("OptionPane.informationIcon")); 227 case JOptionPane.WARNING_MESSAGE: 228 return setIcon(UIManager.getIcon("OptionPane.warningIcon")); 229 case JOptionPane.QUESTION_MESSAGE: 230 return setIcon(UIManager.getIcon("OptionPane.questionIcon")); 231 case JOptionPane.PLAIN_MESSAGE: 232 return setIcon(null); 233 default: 234 throw new IllegalArgumentException("Unknown message type!"); 235 } 236 } 237 238 @Override 239 public ExtendedDialog showDialog() { 240 // Check if the user has set the dialog to not be shown again 241 if (toggleCheckState()) { 242 result = toggleValue; 243 return this; 244 } 245 246 setupDialog(); 247 if (defaultButton != null) { 248 getRootPane().setDefaultButton(defaultButton); 249 } 250 // Don't focus the "do not show this again" check box, but the default button. 251 if (toggleable || focusOnDefaultButton) { 252 requestFocusToDefaultButton(); 253 } 254 setVisible(true); 255 toggleSaveState(); 256 return this; 257 } 258 259 @Override 260 public int getValue() { 261 return result; 262 } 263 264 private boolean setupDone; 265 266 @Override 267 public void setupDialog() { 268 if (setupDone) 269 return; 270 setupDone = true; 271 272 setupEscListener(); 273 274 JButton button; 275 JPanel buttonsPanel = new JPanel(new GridBagLayout()); 276 277 for (int i = 0; i < bTexts.length; i++) { 278 button = new JButton(createButtonAction(i)); 279 if (i == defaultButtonIdx-1) { 280 defaultButton = button; 281 } 282 if (bIcons != null && bIcons[i] != null) { 283 button.setIcon(bIcons[i]); 284 } 285 if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) { 286 button.setToolTipText(bToolTipTexts[i]); 287 } 288 289 buttonsPanel.add(button, GBC.std().insets(2, 2, 2, 2)); 290 buttons.add(button); 291 } 292 if (showHelpButton) { 293 buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2, 2, 2, 2)); 294 HelpUtil.setHelpContext(getRootPane(), helpTopic); 295 } 296 297 JPanel cp = new JPanel(new GridBagLayout()); 298 299 GridBagConstraints gc = new GridBagConstraints(); 300 gc.gridx = 0; 301 int y = 0; 302 gc.gridy = y++; 303 gc.weightx = 0.0; 304 gc.weighty = 0.0; 305 306 if (icon != null) { 307 JLabel iconLbl = new JLabel(icon); 308 gc.insets = new Insets(10, 10, 10, 10); 309 gc.anchor = GridBagConstraints.NORTH; 310 gc.weighty = 1.0; 311 cp.add(iconLbl, gc); 312 gc.anchor = GridBagConstraints.CENTER; 313 gc.gridx = 1; 314 } 315 316 gc.fill = GridBagConstraints.BOTH; 317 gc.insets = contentInsets; 318 gc.weightx = 1.0; 319 gc.weighty = 1.0; 320 cp.add(content, gc); 321 322 gc.fill = GridBagConstraints.NONE; 323 gc.gridwidth = GridBagConstraints.REMAINDER; 324 gc.weightx = 0.0; 325 gc.weighty = 0.0; 326 327 if (toggleable) { 328 togglePanel = new ConditionalOptionPaneUtil.MessagePanel(null, ConditionalOptionPaneUtil.isInBulkOperation(togglePref)); 329 gc.gridx = icon != null ? 1 : 0; 330 gc.gridy = y++; 331 gc.anchor = GridBagConstraints.LINE_START; 332 gc.insets = new Insets(5, contentInsets.left, 5, contentInsets.right); 333 cp.add(togglePanel, gc); 334 } 335 336 gc.gridy = y; 337 gc.anchor = GridBagConstraints.CENTER; 338 gc.insets = new Insets(5, 5, 5, 5); 339 cp.add(buttonsPanel, gc); 340 if (placeContentInScrollPane) { 341 JScrollPane pane = new JScrollPane(cp); 342 GuiHelper.setDefaultIncrement(pane); 343 pane.setBorder(null); 344 setContentPane(pane); 345 } else { 346 setContentPane(cp); 347 } 348 pack(); 349 350 // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen 351 Dimension d = getSize(); 352 Dimension x = findMaxDialogSize(); 353 354 boolean limitedInWidth = d.width > x.width; 355 boolean limitedInHeight = d.height > x.height; 356 357 if (x.width > 0 && d.width > x.width) { 358 d.width = x.width; 359 } 360 if (x.height > 0 && d.height > x.height) { 361 d.height = x.height; 362 } 363 364 // We have a vertical scrollbar and enough space to prevent a horizontal one 365 if (!limitedInWidth && limitedInHeight) { 366 d.width += new JScrollBar().getPreferredSize().width; 367 } 368 369 setSize(d); 370 setLocationRelativeTo(parent); 371 } 372 373 protected Action createButtonAction(final int i) { 374 return new AbstractAction(bTexts[i]) { 375 @Override 376 public void actionPerformed(ActionEvent evt) { 377 buttonAction(i, evt); 378 } 379 }; 380 } 381 382 /** 383 * This gets performed whenever a button is clicked or activated 384 * @param buttonIndex the button index (first index is 0) 385 * @param evt the button event 386 */ 387 protected void buttonAction(int buttonIndex, ActionEvent evt) { 388 result = buttonIndex+1; 389 setVisible(false); 390 } 391 392 /** 393 * Tries to find a good value of how large the dialog should be 394 * @return Dimension Size of the parent component if visible or 2/3 of screen size if not available or hidden 395 */ 396 protected Dimension findMaxDialogSize() { 397 Dimension screenSize = GuiHelper.getScreenSize(); 398 Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3); 399 if (parent != null && parent.isVisible()) { 400 x = GuiHelper.getFrameForComponent(parent).getSize(); 401 } 402 return x; 403 } 404 405 /** 406 * Makes the dialog listen to ESC keypressed 407 */ 408 private void setupEscListener() { 409 Action actionListener = new AbstractAction() { 410 @Override 411 public void actionPerformed(ActionEvent actionEvent) { 412 // 0 means that the dialog has been closed otherwise. 413 // We need to set it to zero again, in case the dialog has been re-used 414 // and the result differs from its default value 415 result = ExtendedDialog.DialogClosedOtherwise; 416 if (Logging.isDebugEnabled()) { 417 Logging.debug("{0} ESC action performed ({1}) from {2}", 418 getClass().getName(), actionEvent, new Exception().getStackTrace()[1]); 419 } 420 setVisible(false); 421 } 422 }; 423 424 InputMapUtils.addEscapeAction(getRootPane(), actionListener); 425 } 426 427 protected final void rememberWindowGeometry(WindowGeometry geometry) { 428 if (geometry != null) { 429 geometry.remember(rememberSizePref); 430 } 431 } 432 433 protected final WindowGeometry initWindowGeometry() { 434 return new WindowGeometry(rememberSizePref, defaultWindowGeometry); 435 } 436 437 /** 438 * Override setVisible to be able to save the window geometry if required 439 */ 440 @Override 441 public void setVisible(boolean visible) { 442 if (visible) { 443 repaint(); 444 } 445 446 if (Logging.isDebugEnabled()) { 447 Logging.debug(getClass().getName()+".setVisible("+visible+") from "+new Exception().getStackTrace()[1]); 448 } 449 450 // Ensure all required variables are available 451 if (!rememberSizePref.isEmpty() && defaultWindowGeometry != null) { 452 if (visible) { 453 initWindowGeometry().applySafe(this); 454 } else if (isShowing()) { // should fix #6438, #6981, #8295 455 rememberWindowGeometry(new WindowGeometry(this)); 456 } 457 } 458 super.setVisible(visible); 459 460 if (!visible && disposeOnClose) { 461 dispose(); 462 } 463 } 464 465 @Override 466 public ExtendedDialog setRememberWindowGeometry(String pref, WindowGeometry wg) { 467 rememberSizePref = pref == null ? "" : pref; 468 defaultWindowGeometry = wg; 469 return this; 470 } 471 472 @Override 473 public ExtendedDialog toggleEnable(String togglePref) { 474 if (!modal) { 475 throw new IllegalStateException(); 476 } 477 this.toggleable = true; 478 this.togglePref = togglePref; 479 return this; 480 } 481 482 @Override 483 public ExtendedDialog setDefaultButton(int defaultButtonIdx) { 484 this.defaultButtonIdx = defaultButtonIdx; 485 return this; 486 } 487 488 @Override 489 public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) { 490 this.cancelButtonIdx = new HashSet<>(Arrays.<Integer>asList(cancelButtonIdx)); 491 return this; 492 } 493 494 @Override 495 public void setFocusOnDefaultButton(boolean focus) { 496 focusOnDefaultButton = focus; 497 } 498 499 private void requestFocusToDefaultButton() { 500 if (defaultButton != null) { 501 GuiHelper.runInEDT(defaultButton::requestFocusInWindow); 502 } 503 } 504 505 @Override 506 public final boolean toggleCheckState() { 507 toggleable = togglePref != null && !togglePref.isEmpty(); 508 toggleValue = ConditionalOptionPaneUtil.getDialogReturnValue(togglePref); 509 return toggleable && toggleValue != -1; 510 } 511 512 /** 513 * This function checks the state of the "Do not show again" checkbox and 514 * writes the corresponding pref. 515 */ 516 protected void toggleSaveState() { 517 if (!toggleable || 518 togglePanel == null || 519 cancelButtonIdx.contains(result) || 520 result == ExtendedDialog.DialogClosedOtherwise) 521 return; 522 togglePanel.getNotShowAgain().store(togglePref, result); 523 } 524 525 /** 526 * Convenience function that converts a given string into a JMultilineLabel 527 * @param msg the message to display 528 * @return JMultilineLabel displaying {@code msg} 529 */ 530 private static JMultilineLabel string2label(String msg) { 531 JMultilineLabel lbl = new JMultilineLabel(msg); 532 // Make it not wider than 1/2 of the screen 533 Dimension screenSize = GuiHelper.getScreenSize(); 534 lbl.setMaxWidth(screenSize.width/2); 535 // Disable default Enter key binding to allow dialog's one (then enables to hit default button from here) 536 lbl.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new Object()); 537 return lbl; 538 } 539 540 @Override 541 public ExtendedDialog configureContextsensitiveHelp(String helpTopic, boolean showHelpButton) { 542 this.helpTopic = helpTopic; 543 this.showHelpButton = showHelpButton; 544 return this; 545 } 546 547 class HelpAction extends AbstractAction { 548 /** 549 * Constructs a new {@code HelpAction}. 550 */ 551 HelpAction() { 552 putValue(SHORT_DESCRIPTION, tr("Show help information")); 553 putValue(NAME, tr("Help")); 554 new ImageProvider("help").getResource().attachImageIcon(this, true); 555 setEnabled(!Main.isOffline(OnlineResource.JOSM_WEBSITE)); 556 } 557 558 @Override 559 public void actionPerformed(ActionEvent e) { 560 HelpBrowser.setUrlForHelpTopic(helpTopic); 561 } 562 } 563}