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

Last change on this file since 6388 was 6340, checked in by Don-vip, 10 years ago

refactor of some GUI/widgets classes (impacts some plugins):

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