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