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

Last change on this file since 6310 was 6310, checked in by Don-vip, 11 years ago

Sonar/FindBugs - Nested blocks of code should not be left empty

  • Property svn:eol-style set to native
File size: 23.2 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.Toolkit;
12import java.awt.event.ActionEvent;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.Collections;
16import java.util.List;
17
18import javax.swing.AbstractAction;
19import javax.swing.Action;
20import javax.swing.Icon;
21import javax.swing.JButton;
22import javax.swing.JCheckBox;
23import javax.swing.JComponent;
24import javax.swing.JDialog;
25import javax.swing.JLabel;
26import javax.swing.JOptionPane;
27import javax.swing.JPanel;
28import javax.swing.JScrollBar;
29import javax.swing.JScrollPane;
30import javax.swing.KeyStroke;
31import javax.swing.SwingUtilities;
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.tools.GBC;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Utils;
40import org.openstreetmap.josm.tools.WindowGeometry;
41
42/**
43 * General configurable dialog window.
44 *
45 * If dialog is modal, you can use {@link #getValue()} to retrieve the
46 * button index. Note that the user can close the dialog
47 * by other means. This is usually equivalent to cancel action.
48 *
49 * For non-modal dialogs, {@link #buttonAction(int, ActionEvent)} can be overridden.
50 *
51 * There are various options, see below.
52 *
53 * Note: The button indices are counted from 1 and upwards.
54 * So for {@link #getValue()}, {@link #setDefaultButton(int)} and
55 * {@link #setCancelButton} the first button has index 1.
56 *
57 * Simple example:
58 * <pre>
59 * ExtendedDialog ed = new ExtendedDialog(
60 * Main.parent, tr("Dialog Title"),
61 * new String[] {tr("Ok"), tr("Cancel")});
62 * ed.setButtonIcons(new String[] {"ok", "cancel"}); // optional
63 * ed.setIcon(JOptionPane.WARNING_MESSAGE); // optional
64 * ed.setContent(tr("Really proceed? Interesting things may happen..."));
65 * ed.showDialog();
66 * if (ed.getValue() == 1) { // user clicked first button "Ok"
67 * // proceed...
68 * }
69 * </pre>
70 */
71public class ExtendedDialog extends JDialog {
72 private final boolean disposeOnClose;
73 private int result = 0;
74 public static final int DialogClosedOtherwise = 0;
75 private boolean toggleable = false;
76 private String rememberSizePref = "";
77 private WindowGeometry defaultWindowGeometry = null;
78 private String togglePref = "";
79 private int toggleValue = -1;
80 private String toggleCheckboxText = tr("Do not show again (remembers choice)");
81 private JCheckBox toggleCheckbox = null;
82 private Component parent;
83 private Component content;
84 private final String[] bTexts;
85 private String[] bToolTipTexts;
86 private Icon[] bIcons;
87 private List<Integer> cancelButtonIdx = Collections.emptyList();
88 private int defaultButtonIdx = 1;
89 protected JButton defaultButton = null;
90 private Icon icon;
91 private boolean modal;
92
93 /** true, if the dialog should include a help button */
94 private boolean showHelpButton;
95 /** the help topic */
96 private String helpTopic;
97
98 /**
99 * set to true if the content of the extended dialog should
100 * be placed in a {@link JScrollPane}
101 */
102 private boolean placeContentInScrollPane;
103
104 // For easy access when inherited
105 protected Insets contentInsets = new Insets(10,5,0,5);
106 protected ArrayList<JButton> buttons = new ArrayList<JButton>();
107
108 /**
109 * This method sets up the most basic options for the dialog. Add more
110 * advanced features with dedicated methods.
111 * Possible features:
112 * <ul>
113 * <li><code>setButtonIcons</code></li>
114 * <li><code>setContent</code></li>
115 * <li><code>toggleEnable</code></li>
116 * <li><code>toggleDisable</code></li>
117 * <li><code>setToggleCheckboxText</code></li>
118 * <li><code>setRememberWindowGeometry</code></li>
119 * </ul>
120 *
121 * When done, call <code>showDialog</code> to display it. You can receive
122 * the user's choice using <code>getValue</code>. Have a look at this function
123 * for possible return values.
124 *
125 * @param parent The parent element that will be used for position and maximum size
126 * @param title The text that will be shown in the window titlebar
127 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
128 */
129 public ExtendedDialog(Component parent, String title, String[] buttonTexts) {
130 this(parent, title, buttonTexts, true, true);
131 }
132
133 /**
134 * Same as above but lets you define if the dialog should be modal.
135 * @param parent The parent element that will be used for position and maximum size
136 * @param title The text that will be shown in the window titlebar
137 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
138 * @param modal Set it to {@code true} if you want the dialog to be modal
139 */
140 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal) {
141 this(parent, title, buttonTexts, modal, true);
142 }
143
144 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal, boolean disposeOnClose) {
145 super(JOptionPane.getFrameForComponent(parent), title, modal ? ModalityType.DOCUMENT_MODAL : ModalityType.MODELESS);
146 this.parent = parent;
147 this.modal = modal;
148 bTexts = Utils.copyArray(buttonTexts);
149 if (disposeOnClose) {
150 setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
151 }
152 this.disposeOnClose = disposeOnClose;
153 }
154
155 /**
156 * Allows decorating the buttons with icons.
157 * @param buttonIcons The button icons
158 * @return {@code this}
159 */
160 public ExtendedDialog setButtonIcons(Icon[] buttonIcons) {
161 this.bIcons = Utils.copyArray(buttonIcons);
162 return this;
163 }
164
165 /**
166 * Convenience method to provide image names instead of images.
167 * @param buttonIcons The button icon names
168 * @return {@code this}
169 */
170 public ExtendedDialog setButtonIcons(String[] buttonIcons) {
171 bIcons = new Icon[buttonIcons.length];
172 for (int i=0; i<buttonIcons.length; ++i) {
173 bIcons[i] = ImageProvider.get(buttonIcons[i]);
174 }
175 return this;
176 }
177
178 /**
179 * Allows decorating the buttons with tooltips. Expects a String array with
180 * translated tooltip texts.
181 *
182 * @param toolTipTexts the tool tip texts. Ignored, if null.
183 * @return {@code this}
184 */
185 public ExtendedDialog setToolTipTexts(String[] toolTipTexts) {
186 this.bToolTipTexts = Utils.copyArray(toolTipTexts);
187 return this;
188 }
189
190 /**
191 * Sets the content that will be displayed in the message dialog.
192 *
193 * Note that depending on your other settings more UI elements may appear.
194 * The content is played on top of the other elements though.
195 *
196 * @param content Any element that can be displayed in the message dialog
197 * @return {@code this}
198 */
199 public ExtendedDialog setContent(Component content) {
200 return setContent(content, true);
201 }
202
203 /**
204 * Sets the content that will be displayed in the message dialog.
205 *
206 * Note that depending on your other settings more UI elements may appear.
207 * The content is played on top of the other elements though.
208 *
209 * @param content Any element that can be displayed in the message dialog
210 * @param placeContentInScrollPane if true, places the content in a JScrollPane
211 * @return {@code this}
212 */
213 public ExtendedDialog setContent(Component content, boolean placeContentInScrollPane) {
214 this.content = content;
215 this.placeContentInScrollPane = placeContentInScrollPane;
216 return this;
217 }
218
219 /**
220 * Sets the message that will be displayed. The String will be automatically
221 * wrapped if it is too long.
222 *
223 * Note that depending on your other settings more UI elements may appear.
224 * The content is played on top of the other elements though.
225 *
226 * @param message The text that should be shown to the user
227 * @return {@code this}
228 */
229 public ExtendedDialog setContent(String message) {
230 return setContent(string2label(message), false);
231 }
232
233 /**
234 * Decorate the dialog with an icon that is shown on the left part of
235 * the window area. (Similar to how it is done in {@link JOptionPane})
236 * @param icon The icon to display
237 * @return {@code this}
238 */
239 public ExtendedDialog setIcon(Icon icon) {
240 this.icon = icon;
241 return this;
242 }
243
244 /**
245 * Convenience method to allow values that would be accepted by {@link JOptionPane} as messageType.
246 * @param messageType The {@link JOptionPane} messageType
247 * @return {@code this}
248 */
249 public ExtendedDialog setIcon(int messageType) {
250 switch (messageType) {
251 case JOptionPane.ERROR_MESSAGE:
252 return setIcon(UIManager.getIcon("OptionPane.errorIcon"));
253 case JOptionPane.INFORMATION_MESSAGE:
254 return setIcon(UIManager.getIcon("OptionPane.informationIcon"));
255 case JOptionPane.WARNING_MESSAGE:
256 return setIcon(UIManager.getIcon("OptionPane.warningIcon"));
257 case JOptionPane.QUESTION_MESSAGE:
258 return setIcon(UIManager.getIcon("OptionPane.questionIcon"));
259 case JOptionPane.PLAIN_MESSAGE:
260 return setIcon(null);
261 default:
262 throw new IllegalArgumentException("Unknown message type!");
263 }
264 }
265
266 /**
267 * Show the dialog to the user. Call this after you have set all options
268 * for the dialog. You can retrieve the result using {@link #getValue()}.
269 * @return {@code this}
270 */
271 public ExtendedDialog showDialog() {
272 // Check if the user has set the dialog to not be shown again
273 if (toggleCheckState(togglePref)) {
274 result = toggleValue;
275 return this;
276 }
277
278 setupDialog();
279 if (defaultButton != null) {
280 getRootPane().setDefaultButton(defaultButton);
281 }
282 fixFocus();
283 setVisible(true);
284 toggleSaveState();
285 return this;
286 }
287
288 /**
289 * Retrieve the user choice after the dialog has been closed.
290 *
291 * @return <ul> <li>The selected button. The count starts with 1.</li>
292 * <li>A return value of {@link #DialogClosedOtherwise} means the dialog has been closed otherwise.</li>
293 * </ul>
294 */
295 public int getValue() {
296 return result;
297 }
298
299 private boolean setupDone = false;
300
301 /**
302 * This is called by {@link #showDialog()}.
303 * Only invoke from outside if you need to modify the contentPane
304 */
305 public void setupDialog() {
306 if (setupDone)
307 return;
308 setupDone = true;
309
310 setupEscListener();
311
312 JButton button;
313 JPanel buttonsPanel = new JPanel(new GridBagLayout());
314
315 for (int i=0; i < bTexts.length; i++) {
316 final int final_i = i;
317 Action action = new AbstractAction(bTexts[i]) {
318 @Override public void actionPerformed(ActionEvent evt) {
319 buttonAction(final_i, evt);
320 }
321 };
322
323 button = new JButton(action);
324 if (i == defaultButtonIdx-1) {
325 defaultButton = button;
326 }
327 if(bIcons != null && bIcons[i] != null) {
328 button.setIcon(bIcons[i]);
329 }
330 if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) {
331 button.setToolTipText(bToolTipTexts[i]);
332 }
333
334 buttonsPanel.add(button, GBC.std().insets(2,2,2,2));
335 buttons.add(button);
336 }
337 if (showHelpButton) {
338 buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2,2,2,2));
339 HelpUtil.setHelpContext(getRootPane(),helpTopic);
340 }
341
342 JPanel cp = new JPanel(new GridBagLayout());
343
344 GridBagConstraints gc = new GridBagConstraints();
345 gc.gridx = 0;
346 int y = 0;
347 gc.gridy = y++;
348 gc.weightx = 0.0;
349 gc.weighty = 0.0;
350
351 if (icon != null) {
352 JLabel iconLbl = new JLabel(icon);
353 gc.insets = new Insets(10,10,10,10);
354 gc.anchor = GridBagConstraints.NORTH;
355 gc.weighty = 1.0;
356 cp.add(iconLbl, gc);
357 gc.anchor = GridBagConstraints.CENTER;
358 gc.gridx = 1;
359 }
360
361 gc.fill = GridBagConstraints.BOTH;
362 gc.insets = contentInsets;
363 gc.weightx = 1.0;
364 gc.weighty = 1.0;
365 cp.add(content, gc);
366
367 gc.fill = GridBagConstraints.NONE;
368 gc.gridwidth = GridBagConstraints.REMAINDER;
369 gc.weightx = 0.0;
370 gc.weighty = 0.0;
371
372 if (toggleable) {
373 toggleCheckbox = new JCheckBox(toggleCheckboxText);
374 boolean showDialog = Main.pref.getBoolean("message."+ togglePref, true);
375 toggleCheckbox.setSelected(!showDialog);
376 gc.gridx = icon != null ? 1 : 0;
377 gc.gridy = y++;
378 gc.anchor = GridBagConstraints.LINE_START;
379 gc.insets = new Insets(5,contentInsets.left,5,contentInsets.right);
380 cp.add(toggleCheckbox, gc);
381 }
382
383 gc.gridy = y++;
384 gc.anchor = GridBagConstraints.CENTER;
385 gc.insets = new Insets(5,5,5,5);
386 cp.add(buttonsPanel, gc);
387 if (placeContentInScrollPane) {
388 JScrollPane pane = new JScrollPane(cp);
389 pane.setBorder(null);
390 setContentPane(pane);
391 } else {
392 setContentPane(cp);
393 }
394 pack();
395
396 // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen
397 Dimension d = getSize();
398 Dimension x = findMaxDialogSize();
399
400 boolean limitedInWidth = d.width > x.width;
401 boolean limitedInHeight = d.height > x.height;
402
403 if(x.width > 0 && d.width > x.width) {
404 d.width = x.width;
405 }
406 if(x.height > 0 && d.height > x.height) {
407 d.height = x.height;
408 }
409
410 // We have a vertical scrollbar and enough space to prevent a horizontal one
411 if(!limitedInWidth && limitedInHeight) {
412 d.width += new JScrollBar().getPreferredSize().width;
413 }
414
415 setSize(d);
416 setLocationRelativeTo(parent);
417 }
418
419 /**
420 * This gets performed whenever a button is clicked or activated
421 * @param buttonIndex the button index (first index is 0)
422 * @param evt the button event
423 */
424 protected void buttonAction(int buttonIndex, ActionEvent evt) {
425 result = buttonIndex+1;
426 setVisible(false);
427 }
428
429 /**
430 * Tries to find a good value of how large the dialog should be
431 * @return Dimension Size of the parent Component or 2/3 of screen size if not available
432 */
433 protected Dimension findMaxDialogSize() {
434 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
435 Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3);
436 try {
437 if(parent != null) {
438 x = JOptionPane.getFrameForComponent(parent).getSize();
439 }
440 } catch(NullPointerException e) {
441 Main.warn(e);
442 }
443 return x;
444 }
445
446 /**
447 * Makes the dialog listen to ESC keypressed
448 */
449 private void setupEscListener() {
450 Action actionListener = new AbstractAction() {
451 @Override 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 * Overwrites the default "Don't show again" text of the toggle checkbox
548 * if you want to give more information. Only has an effect if
549 * <code>toggleEnable</code> is set.
550 * @param text The toggle checkbox text
551 * @return {@code this}
552 */
553 public ExtendedDialog setToggleCheckboxText(String text) {
554 this.toggleCheckboxText = text;
555 return this;
556 }
557
558 /**
559 * Sets the button that will react to ENTER.
560 * @param defaultButtonIdx The button index (starts to )
561 * @return {@code this}
562 */
563 public ExtendedDialog setDefaultButton(int defaultButtonIdx) {
564 this.defaultButtonIdx = defaultButtonIdx;
565 return this;
566 }
567
568 /**
569 * Used in combination with toggle:
570 * If the user presses 'cancel' the toggle settings are ignored and not saved to the pref
571 * @param cancelButtonIdx index of the button that stands for cancel, accepts multiple values
572 * @return {@code this}
573 */
574 public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) {
575 this.cancelButtonIdx = Arrays.<Integer>asList(cancelButtonIdx);
576 return this;
577 }
578
579 /**
580 * Don't focus the "do not show this again" check box, but the default button.
581 */
582 protected void fixFocus() {
583 if (toggleable && defaultButton != null) {
584 SwingUtilities.invokeLater(new Runnable() {
585 @Override public void run() {
586 defaultButton.requestFocusInWindow();
587 }
588 });
589 }
590 }
591
592 /**
593 * This function returns true if the dialog has been set to "do not show again"
594 * @return true if dialog should not be shown again
595 */
596 private boolean toggleCheckState(String togglePref) {
597 toggleable = togglePref != null && !togglePref.isEmpty();
598
599 toggleValue = Main.pref.getInteger("message."+togglePref+".value", -1);
600 // No identifier given, so return false (= show the dialog)
601 if(!toggleable || toggleValue == -1)
602 return false;
603 this.togglePref = togglePref;
604 // The pref is true, if the dialog should be shown.
605 return !(Main.pref.getBoolean("message."+ togglePref, true));
606 }
607
608 /**
609 * This function checks the state of the "Do not show again" checkbox and
610 * writes the corresponding pref.
611 */
612 private void toggleSaveState() {
613 if (!toggleable ||
614 toggleCheckbox == null ||
615 cancelButtonIdx.contains(result) ||
616 result == ExtendedDialog.DialogClosedOtherwise)
617 return;
618 Main.pref.put("message."+ togglePref, !toggleCheckbox.isSelected());
619 Main.pref.putInteger("message."+togglePref+".value", result);
620 }
621
622 /**
623 * Convenience function that converts a given string into a JMultilineLabel
624 * @param msg
625 * @return JMultilineLabel
626 */
627 private static JMultilineLabel string2label(String msg) {
628 JMultilineLabel lbl = new JMultilineLabel(msg);
629 // Make it not wider than 1/2 of the screen
630 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
631 lbl.setMaxWidth(screenSize.width/2);
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 public HelpAction() {
657 putValue(SHORT_DESCRIPTION, tr("Show help information"));
658 putValue(NAME, tr("Help"));
659 putValue(SMALL_ICON, ImageProvider.get("help"));
660 }
661
662 @Override public void actionPerformed(ActionEvent e) {
663 HelpBrowser.setUrlForHelpTopic(helpTopic);
664 }
665 }
666}
Note: See TracBrowser for help on using the repository browser.