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.Frame; 009import java.awt.GridBagConstraints; 010import java.awt.GridBagLayout; 011import java.awt.Insets; 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.HashSet; 018import java.util.List; 019import java.util.Set; 020 021import javax.swing.AbstractAction; 022import javax.swing.Action; 023import javax.swing.Icon; 024import javax.swing.JButton; 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.gui.help.HelpBrowser; 035import org.openstreetmap.josm.gui.help.HelpUtil; 036import org.openstreetmap.josm.gui.util.GuiHelper; 037import org.openstreetmap.josm.gui.util.WindowGeometry; 038import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 039import org.openstreetmap.josm.io.NetworkManager; 040import org.openstreetmap.josm.io.OnlineResource; 041import org.openstreetmap.josm.tools.GBC; 042import org.openstreetmap.josm.tools.ImageProvider; 043import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 044import org.openstreetmap.josm.tools.InputMapUtils; 045import org.openstreetmap.josm.tools.Logging; 046import org.openstreetmap.josm.tools.Utils; 047 048/** 049 * General configurable dialog window. 050 * 051 * If dialog is modal, you can use {@link #getValue()} to retrieve the 052 * button index. Note that the user can close the dialog 053 * by other means. This is usually equivalent to cancel action. 054 * 055 * For non-modal dialogs, {@link #buttonAction(int, ActionEvent)} can be overridden. 056 * 057 * There are various options, see below. 058 * 059 * Note: The button indices are counted from 1 and upwards. 060 * So for {@link #getValue()}, {@link #setDefaultButton(int)} and 061 * {@link #setCancelButton} the first button has index 1. 062 * 063 * Simple example: 064 * <pre> 065 * ExtendedDialog ed = new ExtendedDialog( 066 * MainApplication.getMainFrame(), tr("Dialog Title"), 067 * new String[] {tr("Ok"), tr("Cancel")}); 068 * ed.setButtonIcons(new String[] {"ok", "cancel"}); // optional 069 * ed.setIcon(JOptionPane.WARNING_MESSAGE); // optional 070 * ed.setContent(tr("Really proceed? Interesting things may happen...")); 071 * ed.showDialog(); 072 * if (ed.getValue() == 1) { // user clicked first button "Ok" 073 * // proceed... 074 * } 075 * </pre> 076 */ 077public class ExtendedDialog extends JDialog implements IExtendedDialog { 078 private final boolean disposeOnClose; 079 private volatile int result; 080 public static final int DialogClosedOtherwise = 0; 081 private boolean toggleable; 082 private String rememberSizePref = ""; 083 private transient WindowGeometry defaultWindowGeometry; 084 private String togglePref = ""; 085 private int toggleValue = -1; 086 private ConditionalOptionPaneUtil.MessagePanel togglePanel; 087 private Component parent; 088 private Component content; 089 private final String[] bTexts; 090 private String[] bToolTipTexts; 091 private transient Icon[] bIcons; 092 private Set<Integer> cancelButtonIdx = Collections.emptySet(); 093 private int defaultButtonIdx = 1; 094 protected JButton defaultButton; 095 private transient Icon icon; 096 private boolean modal; 097 private boolean focusOnDefaultButton; 098 099 /** true, if the dialog should include a help button */ 100 private boolean showHelpButton; 101 /** the help topic */ 102 private String helpTopic; 103 104 /** 105 * set to true if the content of the extended dialog should 106 * be placed in a {@link JScrollPane} 107 */ 108 private boolean placeContentInScrollPane; 109 110 // For easy access when inherited 111 protected transient Insets contentInsets = new Insets(10, 5, 0, 5); 112 protected transient List<JButton> buttons = new ArrayList<>(); 113 114 /** 115 * This method sets up the most basic options for the dialog. Add more 116 * advanced features with dedicated methods. 117 * Possible features: 118 * <ul> 119 * <li><code>setButtonIcons</code></li> 120 * <li><code>setContent</code></li> 121 * <li><code>toggleEnable</code></li> 122 * <li><code>toggleDisable</code></li> 123 * <li><code>setToggleCheckboxText</code></li> 124 * <li><code>setRememberWindowGeometry</code></li> 125 * </ul> 126 * 127 * When done, call <code>showDialog</code> to display it. You can receive 128 * the user's choice using <code>getValue</code>. Have a look at this function 129 * for possible return values. 130 * 131 * @param parent The parent element that will be used for position and maximum size 132 * @param title The text that will be shown in the window titlebar 133 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 134 */ 135 public ExtendedDialog(Component parent, String title, String... buttonTexts) { 136 this(parent, title, buttonTexts, true, true); 137 } 138 139 /** 140 * Same as above but lets you define if the dialog should be modal. 141 * @param parent The parent element that will be used for position and maximum size 142 * @param title The text that will be shown in the window titlebar 143 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 144 * @param modal Set it to {@code true} if you want the dialog to be modal 145 */ 146 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal) { 147 this(parent, title, buttonTexts, modal, true); 148 } 149 150 /** 151 * Same as above but lets you define if the dialog should be disposed on close. 152 * @param parent The parent element that will be used for position and maximum size 153 * @param title The text that will be shown in the window titlebar 154 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 155 * @param modal Set it to {@code true} if you want the dialog to be modal 156 * @param disposeOnClose whether to call {@link #dispose} when closing the dialog 157 */ 158 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal, boolean disposeOnClose) { 159 super(searchRealParent(parent), title, modal ? ModalityType.DOCUMENT_MODAL : ModalityType.MODELESS); 160 this.parent = parent; 161 this.modal = modal; 162 bTexts = Utils.copyArray(buttonTexts); 163 if (disposeOnClose) { 164 setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 165 } 166 this.disposeOnClose = disposeOnClose; 167 } 168 169 private static Frame searchRealParent(Component parent) { 170 if (parent == null) { 171 return null; 172 } else { 173 return GuiHelper.getFrameForComponent(parent); 174 } 175 } 176 177 @Override 178 public ExtendedDialog setButtonIcons(Icon... buttonIcons) { 179 this.bIcons = Utils.copyArray(buttonIcons); 180 return this; 181 } 182 183 @Override 184 public ExtendedDialog setButtonIcons(String... buttonIcons) { 185 bIcons = new Icon[buttonIcons.length]; 186 for (int i = 0; i < buttonIcons.length; ++i) { 187 bIcons[i] = ImageProvider.get(buttonIcons[i], ImageSizes.LARGEICON); 188 } 189 return this; 190 } 191 192 @Override 193 public ExtendedDialog setToolTipTexts(String... toolTipTexts) { 194 this.bToolTipTexts = Utils.copyArray(toolTipTexts); 195 return this; 196 } 197 198 @Override 199 public ExtendedDialog setContent(Component content) { 200 return setContent(content, true); 201 } 202 203 @Override 204 public ExtendedDialog setContent(Component content, boolean placeContentInScrollPane) { 205 this.content = content; 206 this.placeContentInScrollPane = placeContentInScrollPane; 207 return this; 208 } 209 210 @Override 211 public ExtendedDialog setContent(String message) { 212 return setContent(string2label(message), false); 213 } 214 215 @Override 216 public ExtendedDialog setIcon(Icon icon) { 217 this.icon = icon; 218 return this; 219 } 220 221 @Override 222 public ExtendedDialog setIcon(int messageType) { 223 switch (messageType) { 224 case JOptionPane.ERROR_MESSAGE: 225 return setIcon(UIManager.getIcon("OptionPane.errorIcon")); 226 case JOptionPane.INFORMATION_MESSAGE: 227 return setIcon(UIManager.getIcon("OptionPane.informationIcon")); 228 case JOptionPane.WARNING_MESSAGE: 229 return setIcon(UIManager.getIcon("OptionPane.warningIcon")); 230 case JOptionPane.QUESTION_MESSAGE: 231 return setIcon(UIManager.getIcon("OptionPane.questionIcon")); 232 case JOptionPane.PLAIN_MESSAGE: 233 return setIcon(null); 234 default: 235 throw new IllegalArgumentException("Unknown message type!"); 236 } 237 } 238 239 @Override 240 public ExtendedDialog showDialog() { 241 // Check if the user has set the dialog to not be shown again 242 if (toggleCheckState()) { 243 result = toggleValue; 244 return this; 245 } 246 247 setupDialog(); 248 if (defaultButton != null) { 249 getRootPane().setDefaultButton(defaultButton); 250 } 251 // Don't focus the "do not show this again" check box, but the default button. 252 if (toggleable || focusOnDefaultButton) { 253 requestFocusToDefaultButton(); 254 } 255 setVisible(true); 256 toggleSaveState(); 257 return this; 258 } 259 260 @Override 261 public int getValue() { 262 return result; 263 } 264 265 private boolean setupDone; 266 267 @Override 268 public void setupDialog() { 269 if (setupDone) 270 return; 271 setupDone = true; 272 273 setupEscListener(); 274 275 JButton button; 276 JPanel buttonsPanel = new JPanel(new GridBagLayout()); 277 278 for (int i = 0; i < bTexts.length; i++) { 279 button = new JButton(createButtonAction(i)); 280 if (i == defaultButtonIdx-1) { 281 defaultButton = button; 282 } 283 if (bIcons != null && bIcons[i] != null) { 284 button.setIcon(bIcons[i]); 285 } 286 if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) { 287 button.setToolTipText(bToolTipTexts[i]); 288 } 289 290 buttonsPanel.add(button, GBC.std().insets(2, 2, 2, 2)); 291 buttons.add(button); 292 } 293 if (showHelpButton) { 294 buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2, 2, 2, 2)); 295 HelpUtil.setHelpContext(getRootPane(), helpTopic); 296 } 297 298 JPanel cp = new JPanel(new GridBagLayout()); 299 300 GridBagConstraints gc = new GridBagConstraints(); 301 gc.gridx = 0; 302 int y = 0; 303 gc.gridy = y++; 304 gc.weightx = 0.0; 305 gc.weighty = 0.0; 306 307 if (icon != null) { 308 JLabel iconLbl = new JLabel(icon); 309 gc.insets = new Insets(10, 10, 10, 10); 310 gc.anchor = GridBagConstraints.NORTH; 311 gc.weighty = 1.0; 312 cp.add(iconLbl, gc); 313 gc.anchor = GridBagConstraints.CENTER; 314 gc.gridx = 1; 315 } 316 317 gc.fill = GridBagConstraints.BOTH; 318 gc.insets = contentInsets; 319 gc.weightx = 1.0; 320 gc.weighty = 1.0; 321 cp.add(content, gc); 322 323 gc.fill = GridBagConstraints.NONE; 324 gc.gridwidth = GridBagConstraints.REMAINDER; 325 gc.weightx = 0.0; 326 gc.weighty = 0.0; 327 328 if (toggleable) { 329 togglePanel = new ConditionalOptionPaneUtil.MessagePanel(null, ConditionalOptionPaneUtil.isInBulkOperation(togglePref)); 330 gc.gridx = icon != null ? 1 : 0; 331 gc.gridy = y++; 332 gc.anchor = GridBagConstraints.LINE_START; 333 gc.insets = new Insets(5, contentInsets.left, 5, contentInsets.right); 334 cp.add(togglePanel, gc); 335 } 336 337 gc.gridy = y; 338 gc.anchor = GridBagConstraints.CENTER; 339 gc.insets = new Insets(5, 5, 5, 5); 340 cp.add(buttonsPanel, gc); 341 if (placeContentInScrollPane) { 342 JScrollPane pane = new JScrollPane(cp); 343 GuiHelper.setDefaultIncrement(pane); 344 pane.setBorder(null); 345 setContentPane(pane); 346 } else { 347 setContentPane(cp); 348 } 349 pack(); 350 351 // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen 352 Dimension d = getSize(); 353 Dimension x = findMaxDialogSize(); 354 355 boolean limitedInWidth = d.width > x.width; 356 boolean limitedInHeight = d.height > x.height; 357 358 if (x.width > 0 && d.width > x.width) { 359 d.width = x.width; 360 } 361 if (x.height > 0 && d.height > x.height) { 362 d.height = x.height; 363 } 364 365 // We have a vertical scrollbar and enough space to prevent a horizontal one 366 if (!limitedInWidth && limitedInHeight) { 367 d.width += new JScrollBar().getPreferredSize().width; 368 } 369 370 setSize(d); 371 setLocationRelativeTo(parent); 372 } 373 374 protected Action createButtonAction(final int i) { 375 return new AbstractAction(bTexts[i]) { 376 @Override 377 public void actionPerformed(ActionEvent evt) { 378 buttonAction(i, evt); 379 } 380 }; 381 } 382 383 /** 384 * This gets performed whenever a button is clicked or activated 385 * @param buttonIndex the button index (first index is 0) 386 * @param evt the button event 387 */ 388 protected void buttonAction(int buttonIndex, ActionEvent evt) { 389 result = buttonIndex+1; 390 setVisible(false); 391 } 392 393 /** 394 * Tries to find a good value of how large the dialog should be 395 * @return Dimension Size of the parent component if visible or 2/3 of screen size if not available or hidden 396 */ 397 protected Dimension findMaxDialogSize() { 398 Dimension screenSize = GuiHelper.getScreenSize(); 399 Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3); 400 if (parent != null && parent.isVisible()) { 401 x = GuiHelper.getFrameForComponent(parent).getSize(); 402 } 403 return x; 404 } 405 406 /** 407 * Makes the dialog listen to ESC keypressed 408 */ 409 private void setupEscListener() { 410 Action actionListener = new AbstractAction() { 411 @Override 412 public void actionPerformed(ActionEvent actionEvent) { 413 // 0 means that the dialog has been closed otherwise. 414 // We need to set it to zero again, in case the dialog has been re-used 415 // and the result differs from its default value 416 result = ExtendedDialog.DialogClosedOtherwise; 417 if (Logging.isDebugEnabled()) { 418 Logging.debug("{0} ESC action performed ({1}) from {2}", 419 getClass().getName(), actionEvent, new Exception().getStackTrace()[1]); 420 } 421 setVisible(false); 422 } 423 }; 424 425 InputMapUtils.addEscapeAction(getRootPane(), actionListener); 426 } 427 428 protected final void rememberWindowGeometry(WindowGeometry geometry) { 429 if (geometry != null) { 430 geometry.remember(rememberSizePref); 431 } 432 } 433 434 protected final WindowGeometry initWindowGeometry() { 435 return new WindowGeometry(rememberSizePref, defaultWindowGeometry); 436 } 437 438 /** 439 * Override setVisible to be able to save the window geometry if required 440 */ 441 @Override 442 public void setVisible(boolean visible) { 443 if (visible) { 444 repaint(); 445 } 446 447 if (Logging.isDebugEnabled()) { 448 Logging.debug(getClass().getName()+".setVisible("+visible+") from "+new Exception().getStackTrace()[1]); 449 } 450 451 // Ensure all required variables are available 452 if (!rememberSizePref.isEmpty() && defaultWindowGeometry != null) { 453 if (visible) { 454 initWindowGeometry().applySafe(this); 455 } else if (isShowing()) { // should fix #6438, #6981, #8295 456 rememberWindowGeometry(new WindowGeometry(this)); 457 } 458 } 459 super.setVisible(visible); 460 461 if (!visible && disposeOnClose) { 462 dispose(); 463 } 464 } 465 466 @Override 467 public ExtendedDialog setRememberWindowGeometry(String pref, WindowGeometry wg) { 468 rememberSizePref = pref == null ? "" : pref; 469 defaultWindowGeometry = wg; 470 return this; 471 } 472 473 @Override 474 public ExtendedDialog toggleEnable(String togglePref) { 475 if (!modal) { 476 throw new IllegalStateException(); 477 } 478 this.toggleable = true; 479 this.togglePref = togglePref; 480 return this; 481 } 482 483 @Override 484 public ExtendedDialog setDefaultButton(int defaultButtonIdx) { 485 this.defaultButtonIdx = defaultButtonIdx; 486 return this; 487 } 488 489 @Override 490 public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) { 491 this.cancelButtonIdx = new HashSet<>(Arrays.<Integer>asList(cancelButtonIdx)); 492 return this; 493 } 494 495 @Override 496 public void setFocusOnDefaultButton(boolean focus) { 497 focusOnDefaultButton = focus; 498 } 499 500 private void requestFocusToDefaultButton() { 501 if (defaultButton != null) { 502 GuiHelper.runInEDT(defaultButton::requestFocusInWindow); 503 } 504 } 505 506 @Override 507 public final boolean toggleCheckState() { 508 toggleable = togglePref != null && !togglePref.isEmpty(); 509 toggleValue = ConditionalOptionPaneUtil.getDialogReturnValue(togglePref); 510 return toggleable && toggleValue != -1; 511 } 512 513 /** 514 * This function checks the state of the "Do not show again" checkbox and 515 * writes the corresponding pref. 516 */ 517 protected void toggleSaveState() { 518 if (!toggleable || 519 togglePanel == null || 520 cancelButtonIdx.contains(result) || 521 result == ExtendedDialog.DialogClosedOtherwise) 522 return; 523 togglePanel.getNotShowAgain().store(togglePref, result); 524 } 525 526 /** 527 * Convenience function that converts a given string into a JMultilineLabel 528 * @param msg the message to display 529 * @return JMultilineLabel displaying {@code msg} 530 */ 531 private static JMultilineLabel string2label(String msg) { 532 JMultilineLabel lbl = new JMultilineLabel(msg); 533 // Make it not wider than 1/2 of the screen 534 Dimension screenSize = GuiHelper.getScreenSize(); 535 lbl.setMaxWidth(screenSize.width/2); 536 // Disable default Enter key binding to allow dialog's one (then enables to hit default button from here) 537 lbl.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new Object()); 538 return lbl; 539 } 540 541 @Override 542 public ExtendedDialog configureContextsensitiveHelp(String helpTopic, boolean showHelpButton) { 543 this.helpTopic = helpTopic; 544 this.showHelpButton = showHelpButton; 545 return this; 546 } 547 548 class HelpAction extends AbstractAction { 549 /** 550 * Constructs a new {@code HelpAction}. 551 */ 552 HelpAction() { 553 putValue(SHORT_DESCRIPTION, tr("Show help information")); 554 putValue(NAME, tr("Help")); 555 new ImageProvider("help").getResource().attachImageIcon(this, true); 556 setEnabled(!NetworkManager.isOffline(OnlineResource.JOSM_WEBSITE)); 557 } 558 559 @Override 560 public void actionPerformed(ActionEvent e) { 561 HelpBrowser.setUrlForHelpTopic(helpTopic); 562 } 563 } 564}