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

Last change on this file since 5508 was 5275, checked in by bastiK, 12 years ago

doc improvements

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