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

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

fix #10175 - ask for JOSM update in case of crash with old version

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