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

Last change on this file since 6340 was 6340, checked in by Don-vip, 5 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.