source: josm/trunk/src/org/openstreetmap/josm/gui/ExtendedDialog.java@ 9870

Last change on this file since 9870 was 9866, checked in by Don-vip, 8 years ago

fix #12559 - The “Skip Download” popup displays incorrectly

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