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

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

global cleanup of IllegalArgumentExceptions thrown by JOSM

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