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