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

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

Sonar/FindBugs - Loose coupling

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