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

Revision 4932, 21.5 KB checked in by stoecker, 3 months ago (diff)

fix 6833 - use WindowGeometry for toggle dialogs and mainwindow replacing old custom methods, improve dual screen handling

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