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

Last change on this file since 13265 was 13130, checked in by Don-vip, 10 months ago

fix #15572 - use ImageProvider attach API for all JOSM actions to ensure proper icon size everywhere

  • Property svn:eol-style set to native
File size: 19.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.Frame;
9import java.awt.GridBagConstraints;
10import java.awt.GridBagLayout;
11import java.awt.Insets;
12import java.awt.event.ActionEvent;
13import java.awt.event.KeyEvent;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.Collections;
17import java.util.HashSet;
18import java.util.List;
19import java.util.Set;
20
21import javax.swing.AbstractAction;
22import javax.swing.Action;
23import javax.swing.Icon;
24import javax.swing.JButton;
25import javax.swing.JDialog;
26import javax.swing.JLabel;
27import javax.swing.JOptionPane;
28import javax.swing.JPanel;
29import javax.swing.JScrollBar;
30import javax.swing.JScrollPane;
31import javax.swing.KeyStroke;
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.util.GuiHelper;
38import org.openstreetmap.josm.gui.util.WindowGeometry;
39import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
40import org.openstreetmap.josm.io.OnlineResource;
41import org.openstreetmap.josm.tools.GBC;
42import org.openstreetmap.josm.tools.ImageProvider;
43import org.openstreetmap.josm.tools.InputMapUtils;
44import org.openstreetmap.josm.tools.Logging;
45import org.openstreetmap.josm.tools.Utils;
46
47/**
48 * General configurable dialog window.
49 *
50 * If dialog is modal, you can use {@link #getValue()} to retrieve the
51 * button index. Note that the user can close the dialog
52 * by other means. This is usually equivalent to cancel action.
53 *
54 * For non-modal dialogs, {@link #buttonAction(int, ActionEvent)} can be overridden.
55 *
56 * There are various options, see below.
57 *
58 * Note: The button indices are counted from 1 and upwards.
59 * So for {@link #getValue()}, {@link #setDefaultButton(int)} and
60 * {@link #setCancelButton} the first button has index 1.
61 *
62 * Simple example:
63 * <pre>
64 *  ExtendedDialog ed = new ExtendedDialog(
65 *          Main.parent, tr("Dialog Title"),
66 *          new String[] {tr("Ok"), tr("Cancel")});
67 *  ed.setButtonIcons(new String[] {"ok", "cancel"});   // optional
68 *  ed.setIcon(JOptionPane.WARNING_MESSAGE);            // optional
69 *  ed.setContent(tr("Really proceed? Interesting things may happen..."));
70 *  ed.showDialog();
71 *  if (ed.getValue() == 1) { // user clicked first button "Ok"
72 *      // proceed...
73 *  }
74 * </pre>
75 */
76public class ExtendedDialog extends JDialog implements IExtendedDialog {
77    private final boolean disposeOnClose;
78    private volatile int result;
79    public static final int DialogClosedOtherwise = 0;
80    private boolean toggleable;
81    private String rememberSizePref = "";
82    private transient WindowGeometry defaultWindowGeometry;
83    private String togglePref = "";
84    private int toggleValue = -1;
85    private ConditionalOptionPaneUtil.MessagePanel togglePanel;
86    private Component parent;
87    private Component content;
88    private final String[] bTexts;
89    private String[] bToolTipTexts;
90    private transient Icon[] bIcons;
91    private Set<Integer> cancelButtonIdx = Collections.emptySet();
92    private int defaultButtonIdx = 1;
93    protected JButton defaultButton;
94    private transient Icon icon;
95    private boolean modal;
96    private boolean focusOnDefaultButton;
97
98    /** true, if the dialog should include a help button */
99    private boolean showHelpButton;
100    /** the help topic */
101    private String helpTopic;
102
103    /**
104     * set to true if the content of the extended dialog should
105     * be placed in a {@link JScrollPane}
106     */
107    private boolean placeContentInScrollPane;
108
109    // For easy access when inherited
110    protected transient Insets contentInsets = new Insets(10, 5, 0, 5);
111    protected transient List<JButton> buttons = new ArrayList<>();
112
113    /**
114     * This method sets up the most basic options for the dialog. Add more
115     * advanced features with dedicated methods.
116     * Possible features:
117     * <ul>
118     *   <li><code>setButtonIcons</code></li>
119     *   <li><code>setContent</code></li>
120     *   <li><code>toggleEnable</code></li>
121     *   <li><code>toggleDisable</code></li>
122     *   <li><code>setToggleCheckboxText</code></li>
123     *   <li><code>setRememberWindowGeometry</code></li>
124     * </ul>
125     *
126     * When done, call <code>showDialog</code> to display it. You can receive
127     * the user's choice using <code>getValue</code>. Have a look at this function
128     * for possible return values.
129     *
130     * @param parent       The parent element that will be used for position and maximum size
131     * @param title        The text that will be shown in the window titlebar
132     * @param buttonTexts  String Array of the text that will appear on the buttons. The first button is the default one.
133     */
134    public ExtendedDialog(Component parent, String title, String... buttonTexts) {
135        this(parent, title, buttonTexts, true, true);
136    }
137
138    /**
139     * Same as above but lets you define if the dialog should be modal.
140     * @param parent The parent element that will be used for position and maximum size
141     * @param title The text that will be shown in the window titlebar
142     * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
143     * @param modal Set it to {@code true} if you want the dialog to be modal
144     */
145    public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal) {
146        this(parent, title, buttonTexts, modal, true);
147    }
148
149    /**
150     * Same as above but lets you define if the dialog should be disposed on close.
151     * @param parent The parent element that will be used for position and maximum size
152     * @param title The text that will be shown in the window titlebar
153     * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
154     * @param modal Set it to {@code true} if you want the dialog to be modal
155     * @param disposeOnClose whether to call {@link #dispose} when closing the dialog
156     */
157    public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal, boolean disposeOnClose) {
158        super(searchRealParent(parent), title, modal ? ModalityType.DOCUMENT_MODAL : ModalityType.MODELESS);
159        this.parent = parent;
160        this.modal = modal;
161        bTexts = Utils.copyArray(buttonTexts);
162        if (disposeOnClose) {
163            setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
164        }
165        this.disposeOnClose = disposeOnClose;
166    }
167
168    private static Frame searchRealParent(Component parent) {
169        if (parent == null) {
170            return null;
171        } else {
172            return GuiHelper.getFrameForComponent(parent);
173        }
174    }
175
176    @Override
177    public ExtendedDialog setButtonIcons(Icon... buttonIcons) {
178        this.bIcons = Utils.copyArray(buttonIcons);
179        return this;
180    }
181
182    @Override
183    public ExtendedDialog setButtonIcons(String... buttonIcons) {
184        bIcons = new Icon[buttonIcons.length];
185        for (int i = 0; i < buttonIcons.length; ++i) {
186            bIcons[i] = ImageProvider.get(buttonIcons[i]);
187        }
188        return this;
189    }
190
191    @Override
192    public ExtendedDialog setToolTipTexts(String... toolTipTexts) {
193        this.bToolTipTexts = Utils.copyArray(toolTipTexts);
194        return this;
195    }
196
197    @Override
198    public ExtendedDialog setContent(Component content) {
199        return setContent(content, true);
200    }
201
202    @Override
203    public ExtendedDialog setContent(Component content, boolean placeContentInScrollPane) {
204        this.content = content;
205        this.placeContentInScrollPane = placeContentInScrollPane;
206        return this;
207    }
208
209    @Override
210    public ExtendedDialog setContent(String message) {
211        return setContent(string2label(message), false);
212    }
213
214    @Override
215    public ExtendedDialog setIcon(Icon icon) {
216        this.icon = icon;
217        return this;
218    }
219
220    @Override
221    public ExtendedDialog setIcon(int messageType) {
222        switch (messageType) {
223            case JOptionPane.ERROR_MESSAGE:
224                return setIcon(UIManager.getIcon("OptionPane.errorIcon"));
225            case JOptionPane.INFORMATION_MESSAGE:
226                return setIcon(UIManager.getIcon("OptionPane.informationIcon"));
227            case JOptionPane.WARNING_MESSAGE:
228                return setIcon(UIManager.getIcon("OptionPane.warningIcon"));
229            case JOptionPane.QUESTION_MESSAGE:
230                return setIcon(UIManager.getIcon("OptionPane.questionIcon"));
231            case JOptionPane.PLAIN_MESSAGE:
232                return setIcon(null);
233            default:
234                throw new IllegalArgumentException("Unknown message type!");
235        }
236    }
237
238    @Override
239    public ExtendedDialog showDialog() {
240        // Check if the user has set the dialog to not be shown again
241        if (toggleCheckState()) {
242            result = toggleValue;
243            return this;
244        }
245
246        setupDialog();
247        if (defaultButton != null) {
248            getRootPane().setDefaultButton(defaultButton);
249        }
250        // Don't focus the "do not show this again" check box, but the default button.
251        if (toggleable || focusOnDefaultButton) {
252            requestFocusToDefaultButton();
253        }
254        setVisible(true);
255        toggleSaveState();
256        return this;
257    }
258
259    @Override
260    public int getValue() {
261        return result;
262    }
263
264    private boolean setupDone;
265
266    @Override
267    public void setupDialog() {
268        if (setupDone)
269            return;
270        setupDone = true;
271
272        setupEscListener();
273
274        JButton button;
275        JPanel buttonsPanel = new JPanel(new GridBagLayout());
276
277        for (int i = 0; i < bTexts.length; i++) {
278            button = new JButton(createButtonAction(i));
279            if (i == defaultButtonIdx-1) {
280                defaultButton = button;
281            }
282            if (bIcons != null && bIcons[i] != null) {
283                button.setIcon(bIcons[i]);
284            }
285            if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) {
286                button.setToolTipText(bToolTipTexts[i]);
287            }
288
289            buttonsPanel.add(button, GBC.std().insets(2, 2, 2, 2));
290            buttons.add(button);
291        }
292        if (showHelpButton) {
293            buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2, 2, 2, 2));
294            HelpUtil.setHelpContext(getRootPane(), helpTopic);
295        }
296
297        JPanel cp = new JPanel(new GridBagLayout());
298
299        GridBagConstraints gc = new GridBagConstraints();
300        gc.gridx = 0;
301        int y = 0;
302        gc.gridy = y++;
303        gc.weightx = 0.0;
304        gc.weighty = 0.0;
305
306        if (icon != null) {
307            JLabel iconLbl = new JLabel(icon);
308            gc.insets = new Insets(10, 10, 10, 10);
309            gc.anchor = GridBagConstraints.NORTH;
310            gc.weighty = 1.0;
311            cp.add(iconLbl, gc);
312            gc.anchor = GridBagConstraints.CENTER;
313            gc.gridx = 1;
314        }
315
316        gc.fill = GridBagConstraints.BOTH;
317        gc.insets = contentInsets;
318        gc.weightx = 1.0;
319        gc.weighty = 1.0;
320        cp.add(content, gc);
321
322        gc.fill = GridBagConstraints.NONE;
323        gc.gridwidth = GridBagConstraints.REMAINDER;
324        gc.weightx = 0.0;
325        gc.weighty = 0.0;
326
327        if (toggleable) {
328            togglePanel = new ConditionalOptionPaneUtil.MessagePanel(null, ConditionalOptionPaneUtil.isInBulkOperation(togglePref));
329            gc.gridx = icon != null ? 1 : 0;
330            gc.gridy = y++;
331            gc.anchor = GridBagConstraints.LINE_START;
332            gc.insets = new Insets(5, contentInsets.left, 5, contentInsets.right);
333            cp.add(togglePanel, gc);
334        }
335
336        gc.gridy = y;
337        gc.anchor = GridBagConstraints.CENTER;
338            gc.insets = new Insets(5, 5, 5, 5);
339        cp.add(buttonsPanel, gc);
340        if (placeContentInScrollPane) {
341            JScrollPane pane = new JScrollPane(cp);
342            GuiHelper.setDefaultIncrement(pane);
343            pane.setBorder(null);
344            setContentPane(pane);
345        } else {
346            setContentPane(cp);
347        }
348        pack();
349
350        // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen
351        Dimension d = getSize();
352        Dimension x = findMaxDialogSize();
353
354        boolean limitedInWidth = d.width > x.width;
355        boolean limitedInHeight = d.height > x.height;
356
357        if (x.width > 0 && d.width > x.width) {
358            d.width = x.width;
359        }
360        if (x.height > 0 && d.height > x.height) {
361            d.height = x.height;
362        }
363
364        // We have a vertical scrollbar and enough space to prevent a horizontal one
365        if (!limitedInWidth && limitedInHeight) {
366            d.width += new JScrollBar().getPreferredSize().width;
367        }
368
369        setSize(d);
370        setLocationRelativeTo(parent);
371    }
372
373    protected Action createButtonAction(final int i) {
374        return new AbstractAction(bTexts[i]) {
375            @Override
376            public void actionPerformed(ActionEvent evt) {
377                buttonAction(i, evt);
378            }
379        };
380    }
381
382    /**
383     * This gets performed whenever a button is clicked or activated
384     * @param buttonIndex the button index (first index is 0)
385     * @param evt the button event
386     */
387    protected void buttonAction(int buttonIndex, ActionEvent evt) {
388        result = buttonIndex+1;
389        setVisible(false);
390    }
391
392    /**
393     * Tries to find a good value of how large the dialog should be
394     * @return Dimension Size of the parent component if visible or 2/3 of screen size if not available or hidden
395     */
396    protected Dimension findMaxDialogSize() {
397        Dimension screenSize = GuiHelper.getScreenSize();
398        Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3);
399        if (parent != null && parent.isVisible()) {
400            x = GuiHelper.getFrameForComponent(parent).getSize();
401        }
402        return x;
403    }
404
405    /**
406     * Makes the dialog listen to ESC keypressed
407     */
408    private void setupEscListener() {
409        Action actionListener = new AbstractAction() {
410            @Override
411            public void actionPerformed(ActionEvent actionEvent) {
412                // 0 means that the dialog has been closed otherwise.
413                // We need to set it to zero again, in case the dialog has been re-used
414                // and the result differs from its default value
415                result = ExtendedDialog.DialogClosedOtherwise;
416                if (Logging.isDebugEnabled()) {
417                    Logging.debug("{0} ESC action performed ({1}) from {2}",
418                            getClass().getName(), actionEvent, new Exception().getStackTrace()[1]);
419                }
420                setVisible(false);
421            }
422        };
423
424        InputMapUtils.addEscapeAction(getRootPane(), actionListener);
425    }
426
427    protected final void rememberWindowGeometry(WindowGeometry geometry) {
428        if (geometry != null) {
429            geometry.remember(rememberSizePref);
430        }
431    }
432
433    protected final WindowGeometry initWindowGeometry() {
434        return new WindowGeometry(rememberSizePref, defaultWindowGeometry);
435    }
436
437    /**
438     * Override setVisible to be able to save the window geometry if required
439     */
440    @Override
441    public void setVisible(boolean visible) {
442        if (visible) {
443            repaint();
444        }
445
446        if (Logging.isDebugEnabled()) {
447            Logging.debug(getClass().getName()+".setVisible("+visible+") from "+new Exception().getStackTrace()[1]);
448        }
449
450        // Ensure all required variables are available
451        if (!rememberSizePref.isEmpty() && defaultWindowGeometry != null) {
452            if (visible) {
453                initWindowGeometry().applySafe(this);
454            } else if (isShowing()) { // should fix #6438, #6981, #8295
455                rememberWindowGeometry(new WindowGeometry(this));
456            }
457        }
458        super.setVisible(visible);
459
460        if (!visible && disposeOnClose) {
461            dispose();
462        }
463    }
464
465    @Override
466    public ExtendedDialog setRememberWindowGeometry(String pref, WindowGeometry wg) {
467        rememberSizePref = pref == null ? "" : pref;
468        defaultWindowGeometry = wg;
469        return this;
470    }
471
472    @Override
473    public ExtendedDialog toggleEnable(String togglePref) {
474        if (!modal) {
475            throw new IllegalStateException();
476        }
477        this.toggleable = true;
478        this.togglePref = togglePref;
479        return this;
480    }
481
482    @Override
483    public ExtendedDialog setDefaultButton(int defaultButtonIdx) {
484        this.defaultButtonIdx = defaultButtonIdx;
485        return this;
486    }
487
488    @Override
489    public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) {
490        this.cancelButtonIdx = new HashSet<>(Arrays.<Integer>asList(cancelButtonIdx));
491        return this;
492    }
493
494    @Override
495    public void setFocusOnDefaultButton(boolean focus) {
496        focusOnDefaultButton = focus;
497    }
498
499    private void requestFocusToDefaultButton() {
500        if (defaultButton != null) {
501            GuiHelper.runInEDT(defaultButton::requestFocusInWindow);
502        }
503    }
504
505    @Override
506    public final boolean toggleCheckState() {
507        toggleable = togglePref != null && !togglePref.isEmpty();
508        toggleValue = ConditionalOptionPaneUtil.getDialogReturnValue(togglePref);
509        return toggleable && toggleValue != -1;
510    }
511
512    /**
513     * This function checks the state of the "Do not show again" checkbox and
514     * writes the corresponding pref.
515     */
516    protected void toggleSaveState() {
517        if (!toggleable ||
518                togglePanel == null ||
519                cancelButtonIdx.contains(result) ||
520                result == ExtendedDialog.DialogClosedOtherwise)
521            return;
522        togglePanel.getNotShowAgain().store(togglePref, result);
523    }
524
525    /**
526     * Convenience function that converts a given string into a JMultilineLabel
527     * @param msg the message to display
528     * @return JMultilineLabel displaying {@code msg}
529     */
530    private static JMultilineLabel string2label(String msg) {
531        JMultilineLabel lbl = new JMultilineLabel(msg);
532        // Make it not wider than 1/2 of the screen
533        Dimension screenSize = GuiHelper.getScreenSize();
534        lbl.setMaxWidth(screenSize.width/2);
535        // Disable default Enter key binding to allow dialog's one (then enables to hit default button from here)
536        lbl.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new Object());
537        return lbl;
538    }
539
540    @Override
541    public ExtendedDialog configureContextsensitiveHelp(String helpTopic, boolean showHelpButton) {
542        this.helpTopic = helpTopic;
543        this.showHelpButton = showHelpButton;
544        return this;
545    }
546
547    class HelpAction extends AbstractAction {
548        /**
549         * Constructs a new {@code HelpAction}.
550         */
551        HelpAction() {
552            putValue(SHORT_DESCRIPTION, tr("Show help information"));
553            putValue(NAME, tr("Help"));
554            new ImageProvider("help").getResource().attachImageIcon(this, true);
555            setEnabled(!Main.isOffline(OnlineResource.JOSM_WEBSITE));
556        }
557
558        @Override
559        public void actionPerformed(ActionEvent e) {
560            HelpBrowser.setUrlForHelpTopic(helpTopic);
561        }
562    }
563}
Note: See TracBrowser for help on using the repository browser.