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

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

fix #13193 - Use a new bug report dialog (patch by michael2402) - gsoc-core

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