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 final 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 final 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 if (MainApplication.getMainFrame() != null) { 256 applyComponentOrientation(MainApplication.getMainFrame().getComponentOrientation()); 257 } 258 setVisible(true); 259 toggleSaveState(); 260 return this; 261 } 262 263 @Override 264 public int getValue() { 265 return result; 266 } 267 268 private boolean setupDone; 269 270 @Override 271 public void setupDialog() { 272 if (setupDone) 273 return; 274 setupDone = true; 275 276 setupEscListener(); 277 278 JButton button; 279 JPanel buttonsPanel = new JPanel(new GridBagLayout()); 280 281 for (int i = 0; i < bTexts.length; i++) { 282 button = new JButton(createButtonAction(i)); 283 if (i == defaultButtonIdx-1) { 284 defaultButton = button; 285 } 286 if (bIcons != null && bIcons[i] != null) { 287 button.setIcon(bIcons[i]); 288 } 289 if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) { 290 button.setToolTipText(bToolTipTexts[i]); 291 } 292 293 buttonsPanel.add(button, GBC.std().insets(2, 2, 2, 2)); 294 buttons.add(button); 295 } 296 if (showHelpButton) { 297 buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2, 2, 2, 2)); 298 HelpUtil.setHelpContext(getRootPane(), helpTopic); 299 } 300 301 JPanel cp = new JPanel(new GridBagLayout()); 302 303 GridBagConstraints gc = new GridBagConstraints(); 304 gc.gridx = 0; 305 int y = 0; 306 gc.gridy = y++; 307 gc.weightx = 0.0; 308 gc.weighty = 0.0; 309 310 if (icon != null) { 311 JLabel iconLbl = new JLabel(icon); 312 gc.insets = new Insets(10, 10, 10, 10); 313 gc.anchor = GridBagConstraints.NORTH; 314 gc.weighty = 1.0; 315 cp.add(iconLbl, gc); 316 gc.anchor = GridBagConstraints.CENTER; 317 gc.gridx = 1; 318 } 319 320 gc.fill = GridBagConstraints.BOTH; 321 gc.insets = contentInsets; 322 gc.weightx = 1.0; 323 gc.weighty = 1.0; 324 cp.add(content, gc); 325 326 gc.fill = GridBagConstraints.NONE; 327 gc.gridwidth = GridBagConstraints.REMAINDER; 328 gc.weightx = 0.0; 329 gc.weighty = 0.0; 330 331 if (toggleable) { 332 togglePanel = new ConditionalOptionPaneUtil.MessagePanel(null, ConditionalOptionPaneUtil.isInBulkOperation(togglePref)); 333 gc.gridx = icon != null ? 1 : 0; 334 gc.gridy = y++; 335 gc.anchor = GridBagConstraints.LINE_START; 336 gc.insets = new Insets(5, contentInsets.left, 5, contentInsets.right); 337 cp.add(togglePanel, gc); 338 } 339 340 gc.gridy = y; 341 gc.anchor = GridBagConstraints.CENTER; 342 gc.insets = new Insets(5, 5, 5, 5); 343 cp.add(buttonsPanel, gc); 344 if (placeContentInScrollPane) { 345 JScrollPane pane = new JScrollPane(cp); 346 GuiHelper.setDefaultIncrement(pane); 347 pane.setBorder(null); 348 setContentPane(pane); 349 } else { 350 setContentPane(cp); 351 } 352 pack(); 353 354 // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen 355 Dimension d = getSize(); 356 Dimension x = findMaxDialogSize(); 357 358 boolean limitedInWidth = d.width > x.width; 359 boolean limitedInHeight = d.height > x.height; 360 361 if (x.width > 0 && d.width > x.width) { 362 d.width = x.width; 363 } 364 if (x.height > 0 && d.height > x.height) { 365 d.height = x.height; 366 } 367 368 // We have a vertical scrollbar and enough space to prevent a horizontal one 369 if (!limitedInWidth && limitedInHeight) { 370 d.width += new JScrollBar().getPreferredSize().width; 371 } 372 373 setSize(d); 374 setLocationRelativeTo(parent); 375 } 376 377 protected Action createButtonAction(final int i) { 378 return new AbstractAction(bTexts[i]) { 379 @Override 380 public void actionPerformed(ActionEvent evt) { 381 buttonAction(i, evt); 382 } 383 }; 384 } 385 386 /** 387 * This gets performed whenever a button is clicked or activated 388 * @param buttonIndex the button index (first index is 0) 389 * @param evt the button event 390 */ 391 protected void buttonAction(int buttonIndex, ActionEvent evt) { 392 result = buttonIndex+1; 393 setVisible(false); 394 } 395 396 /** 397 * Tries to find a good value of how large the dialog should be 398 * @return Dimension Size of the parent component if visible or 2/3 of screen size if not available or hidden 399 */ 400 protected Dimension findMaxDialogSize() { 401 Dimension screenSize = GuiHelper.getScreenSize(); 402 Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3); 403 if (parent != null && parent.isVisible()) { 404 x = GuiHelper.getFrameForComponent(parent).getSize(); 405 } 406 return x; 407 } 408 409 /** 410 * Makes the dialog listen to ESC keypressed 411 */ 412 private void setupEscListener() { 413 Action actionListener = new AbstractAction() { 414 @Override 415 public void actionPerformed(ActionEvent actionEvent) { 416 // 0 means that the dialog has been closed otherwise. 417 // We need to set it to zero again, in case the dialog has been re-used 418 // and the result differs from its default value 419 result = ExtendedDialog.DialogClosedOtherwise; 420 if (Logging.isDebugEnabled()) { 421 Logging.debug("{0} ESC action performed ({1}) from {2}", 422 getClass().getName(), actionEvent, new Exception().getStackTrace()[1]); 423 } 424 setVisible(false); 425 } 426 }; 427 428 InputMapUtils.addEscapeAction(getRootPane(), actionListener); 429 } 430 431 protected final void rememberWindowGeometry(WindowGeometry geometry) { 432 if (geometry != null) { 433 geometry.remember(rememberSizePref); 434 } 435 } 436 437 protected final WindowGeometry initWindowGeometry() { 438 return new WindowGeometry(rememberSizePref, defaultWindowGeometry); 439 } 440 441 /** 442 * Override setVisible to be able to save the window geometry if required 443 */ 444 @Override 445 public void setVisible(boolean visible) { 446 if (visible) { 447 repaint(); 448 } 449 450 if (Logging.isDebugEnabled()) { 451 Logging.debug(getClass().getName()+".setVisible("+visible+") from "+new Exception().getStackTrace()[1]); 452 } 453 454 // Ensure all required variables are available 455 if (!rememberSizePref.isEmpty() && defaultWindowGeometry != null) { 456 if (visible) { 457 initWindowGeometry().applySafe(this); 458 } else if (isShowing()) { // should fix #6438, #6981, #8295 459 rememberWindowGeometry(new WindowGeometry(this)); 460 } 461 } 462 super.setVisible(visible); 463 464 if (!visible && disposeOnClose) { 465 dispose(); 466 } 467 } 468 469 @Override 470 public ExtendedDialog setRememberWindowGeometry(String pref, WindowGeometry wg) { 471 rememberSizePref = pref == null ? "" : pref; 472 defaultWindowGeometry = wg; 473 return this; 474 } 475 476 @Override 477 public ExtendedDialog toggleEnable(String togglePref) { 478 if (!modal) { 479 throw new IllegalStateException(); 480 } 481 this.toggleable = true; 482 this.togglePref = togglePref; 483 return this; 484 } 485 486 @Override 487 public ExtendedDialog setDefaultButton(int defaultButtonIdx) { 488 this.defaultButtonIdx = defaultButtonIdx; 489 return this; 490 } 491 492 @Override 493 public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) { 494 this.cancelButtonIdx = new HashSet<>(Arrays.<Integer>asList(cancelButtonIdx)); 495 return this; 496 } 497 498 @Override 499 public void setFocusOnDefaultButton(boolean focus) { 500 focusOnDefaultButton = focus; 501 } 502 503 private void requestFocusToDefaultButton() { 504 if (defaultButton != null) { 505 GuiHelper.runInEDT(defaultButton::requestFocusInWindow); 506 } 507 } 508 509 @Override 510 public final boolean toggleCheckState() { 511 toggleable = !Utils.isEmpty(togglePref); 512 toggleValue = ConditionalOptionPaneUtil.getDialogReturnValue(togglePref); 513 return toggleable && toggleValue != -1; 514 } 515 516 /** 517 * This function checks the state of the "Do not show again" checkbox and 518 * writes the corresponding pref. 519 */ 520 protected void toggleSaveState() { 521 if (!toggleable || 522 togglePanel == null || 523 cancelButtonIdx.contains(result) || 524 result == ExtendedDialog.DialogClosedOtherwise) 525 return; 526 togglePanel.getNotShowAgain().store(togglePref, result); 527 } 528 529 /** 530 * Convenience function that converts a given string into a JMultilineLabel 531 * @param msg the message to display 532 * @return JMultilineLabel displaying {@code msg} 533 */ 534 private static JMultilineLabel string2label(String msg) { 535 JMultilineLabel lbl = new JMultilineLabel(msg); 536 // Make it not wider than 1/2 of the screen 537 Dimension screenSize = GuiHelper.getScreenSize(); 538 lbl.setMaxWidth(screenSize.width/2); 539 // Disable default Enter key binding to allow dialog's one (then enables to hit default button from here) 540 lbl.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new Object()); 541 return lbl; 542 } 543 544 @Override 545 public ExtendedDialog configureContextsensitiveHelp(String helpTopic, boolean showHelpButton) { 546 this.helpTopic = helpTopic; 547 this.showHelpButton = showHelpButton; 548 return this; 549 } 550 551 class HelpAction extends AbstractAction { 552 /** 553 * Constructs a new {@code HelpAction}. 554 */ 555 HelpAction() { 556 putValue(SHORT_DESCRIPTION, tr("Show help information")); 557 putValue(NAME, tr("Help")); 558 new ImageProvider("help").getResource().attachImageIcon(this, true); 559 setEnabled(!NetworkManager.isOffline(OnlineResource.JOSM_WEBSITE)); 560 } 561 562 @Override 563 public void actionPerformed(ActionEvent e) { 564 HelpBrowser.setUrlForHelpTopic(helpTopic); 565 } 566 } 567}