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