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

Last change on this file since 6595 was 6595, checked in by simon04, 10 years ago

see #8969 - add "Do not show again (this operation)" for "Really delete selection from relation"

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