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