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