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