001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Component;
007    import java.awt.Dimension;
008    import java.awt.GridBagConstraints;
009    import java.awt.GridBagLayout;
010    import java.awt.Insets;
011    import java.awt.Toolkit;
012    import java.awt.event.ActionEvent;
013    import java.util.ArrayList;
014    import java.util.Arrays;
015    import java.util.Collections;
016    import java.util.List;
017    
018    import javax.swing.AbstractAction;
019    import javax.swing.Action;
020    import javax.swing.Icon;
021    import javax.swing.JButton;
022    import javax.swing.JCheckBox;
023    import javax.swing.JComponent;
024    import javax.swing.JDialog;
025    import javax.swing.JLabel;
026    import javax.swing.JOptionPane;
027    import javax.swing.JPanel;
028    import javax.swing.JScrollBar;
029    import javax.swing.JScrollPane;
030    import javax.swing.KeyStroke;
031    import javax.swing.SwingUtilities;
032    import javax.swing.UIManager;
033    
034    import org.openstreetmap.josm.Main;
035    import org.openstreetmap.josm.gui.help.HelpBrowser;
036    import org.openstreetmap.josm.gui.help.HelpUtil;
037    import org.openstreetmap.josm.tools.GBC;
038    import org.openstreetmap.josm.tools.ImageProvider;
039    import org.openstreetmap.josm.tools.WindowGeometry;
040    
041    /**
042     * General configurable dialog window.
043     *
044     * If dialog is modal, you can use {@link #getValue()} to retrieve the
045     * button index. Note that the user can close the dialog
046     * by other means. This is usually equivalent to cancel action.
047     *
048     * For non-modal dialogs, {@link #buttonAction(int, ActionEvent)} can be overridden.
049     *
050     * There are various options, see below.
051     *
052     * Note: The button indices are counted from 1 and upwards.
053     * So for {@link #getValue()}, {@link #setDefaultButton(int)} and 
054     * {@link #setCancelButton} the first button has index 1.
055     * 
056     * Simple example:
057     * <pre>
058     *  ExtendedDialog ed = new ExtendedDialog(
059     *          Main.parent, tr("Dialog Title"),
060     *          new String[] {tr("Ok"), tr("Cancel")});
061     *  ed.setButtonIcons(new String[] {"ok", "cancel"});   // optional
062     *  ed.setIcon(JOptionPane.WARNING_MESSAGE);            // optional
063     *  ed.setContent(tr("Really proceed? Interesting things may happen..."));
064     *  ed.showDialog();
065     *  if (ed.getValue() == 1) { // user clicked first button "Ok"
066     *      // proceed...
067     *  }
068     * </pre>
069     */
070    public class ExtendedDialog extends JDialog {
071        private final boolean disposeOnClose;
072        private int result = 0;
073        public static final int DialogClosedOtherwise = 0;
074        private boolean toggleable = false;
075        private String rememberSizePref = "";
076        private WindowGeometry defaultWindowGeometry = null;
077        private String togglePref = "";
078        private int toggleValue = -1;
079        private String toggleCheckboxText = tr("Do not show again (remembers choice)");
080        private JCheckBox toggleCheckbox = null;
081        private Component parent;
082        private Component content;
083        private final String[] bTexts;
084        private String[] bToolTipTexts;
085        private Icon[] bIcons;
086        private List<Integer> cancelButtonIdx = Collections.emptyList();
087        private int defaultButtonIdx = 1;
088        protected JButton defaultButton = null;
089        private Icon icon;
090        private boolean modal;
091    
092        /** true, if the dialog should include a help button */
093        private boolean showHelpButton;
094        /** the help topic */
095        private String helpTopic;
096    
097        /**
098         * set to true if the content of the extended dialog should
099         * be placed in a {@link 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 a String array with
174         * translated 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 {@link 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 {@link 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 {@link #getValue()}.
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         * Retrieve the user choice after the dialog has been closed.
276         * 
277         * @return <ul> <li>The selected button. The count starts with 1.</li>
278         *              <li>A return value of {@link #DialogClosedOtherwise} means the dialog has been closed otherwise.</li>
279         *         </ul>
280         */
281        public int getValue() {
282            return result;
283        }
284    
285        private boolean setupDone = false;
286    
287        /**
288         * This is called by {@link #showDialog()}.
289         * Only invoke from outside if you need to modify the contentPane
290         */
291        public void setupDialog() {
292            if (setupDone)
293                return;
294            setupDone = true;
295    
296            setupEscListener();
297    
298            JButton button;
299            JPanel buttonsPanel = new JPanel(new GridBagLayout());
300    
301            for (int i=0; i < bTexts.length; i++) {
302                final int final_i = i;
303                Action action = new AbstractAction(bTexts[i]) {
304                    @Override public void actionPerformed(ActionEvent evt) {
305                        buttonAction(final_i, evt);
306                    }
307                };
308    
309                button = new JButton(action);
310                if (i == defaultButtonIdx-1) {
311                    defaultButton = button;
312                }
313                if(bIcons != null && bIcons[i] != null) {
314                    button.setIcon(bIcons[i]);
315                }
316                if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) {
317                    button.setToolTipText(bToolTipTexts[i]);
318                }
319    
320                buttonsPanel.add(button, GBC.std().insets(2,2,2,2));
321                buttons.add(button);
322            }
323            if (showHelpButton) {
324                buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2,2,2,2));
325                HelpUtil.setHelpContext(getRootPane(),helpTopic);
326            }
327    
328            JPanel cp = new JPanel(new GridBagLayout());
329    
330            GridBagConstraints gc = new GridBagConstraints();
331            gc.gridx = 0;
332            int y = 0;
333            gc.gridy = y++;
334            gc.weightx = 0.0;
335            gc.weighty = 0.0;
336    
337            if (icon != null) {
338                JLabel iconLbl = new JLabel(icon);
339                gc.insets = new Insets(10,10,10,10);
340                gc.anchor = GridBagConstraints.NORTH;
341                gc.weighty = 1.0;
342                cp.add(iconLbl, gc);
343                gc.anchor = GridBagConstraints.CENTER;
344                gc.gridx = 1;
345            }
346    
347            gc.fill = GridBagConstraints.BOTH;
348            gc.insets = contentInsets;
349            gc.weightx = 1.0;
350            gc.weighty = 1.0;
351            cp.add(content, gc);
352    
353            gc.fill = GridBagConstraints.NONE;
354            gc.gridwidth = GridBagConstraints.REMAINDER;
355            gc.weightx = 0.0;
356            gc.weighty = 0.0;
357    
358            if (toggleable) {
359                toggleCheckbox = new JCheckBox(toggleCheckboxText);
360                boolean showDialog = Main.pref.getBoolean("message."+ togglePref, true);
361                toggleCheckbox.setSelected(!showDialog);
362                gc.gridx = icon != null ? 1 : 0;
363                gc.gridy = y++;
364                gc.anchor = GridBagConstraints.LINE_START;
365                gc.insets = new Insets(5,contentInsets.left,5,contentInsets.right);
366                cp.add(toggleCheckbox, gc);
367            }
368    
369            gc.gridy = y++;
370            gc.anchor = GridBagConstraints.CENTER;
371                gc.insets = new Insets(5,5,5,5);
372            cp.add(buttonsPanel, gc);
373            if (placeContentInScrollPane) {
374                JScrollPane pane = new JScrollPane(cp);
375                pane.setBorder(null);
376                setContentPane(pane);
377            } else {
378                setContentPane(cp);
379            }
380            pack();
381    
382            // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen
383            Dimension d = getSize();
384            Dimension x = findMaxDialogSize();
385    
386            boolean limitedInWidth = d.width > x.width;
387            boolean limitedInHeight = d.height > x.height;
388    
389            if(x.width  > 0 && d.width  > x.width) {
390                d.width  = x.width;
391            }
392            if(x.height > 0 && d.height > x.height) {
393                d.height = x.height;
394            }
395    
396            // We have a vertical scrollbar and enough space to prevent a horizontal one
397            if(!limitedInWidth && limitedInHeight) {
398                d.width += new JScrollBar().getPreferredSize().width;
399            }
400    
401            setSize(d);
402            setLocationRelativeTo(parent);
403        }
404    
405        /**
406         * This gets performed whenever a button is clicked or activated
407         * @param buttonIndex the button index (first index is 0)
408         * @param evt the button event
409         */
410        protected void buttonAction(int buttonIndex, ActionEvent evt) {
411            result = buttonIndex+1;
412            setVisible(false);
413        }
414    
415        /**
416         * Tries to find a good value of how large the dialog should be
417         * @return Dimension Size of the parent Component or 2/3 of screen size if not available
418         */
419        protected Dimension findMaxDialogSize() {
420            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
421            Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3);
422            try {
423                if(parent != null) {
424                    x = JOptionPane.getFrameForComponent(parent).getSize();
425                }
426            } catch(NullPointerException e) { }
427            return x;
428        }
429    
430        /**
431         * Makes the dialog listen to ESC keypressed
432         */
433        private void setupEscListener() {
434            Action actionListener = new AbstractAction() {
435                @Override public void actionPerformed(ActionEvent actionEvent) {
436                    // 0 means that the dialog has been closed otherwise.
437                    // We need to set it to zero again, in case the dialog has been re-used
438                    // and the result differs from its default value
439                    result = ExtendedDialog.DialogClosedOtherwise;
440                    setVisible(false);
441                }
442            };
443    
444            getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
445                .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
446            getRootPane().getActionMap().put("ESCAPE", actionListener);
447        }
448    
449        /**
450         * Override setVisible to be able to save the window geometry if required
451         */
452        @Override
453        public void setVisible(boolean visible) {
454            if (visible) {
455                repaint();
456            }
457    
458            // Ensure all required variables are available
459            if(rememberSizePref.length() != 0 && defaultWindowGeometry != null) {
460                if(visible) {
461                    new WindowGeometry(rememberSizePref,
462                            defaultWindowGeometry).applySafe(this);
463                } else {
464                    new WindowGeometry(this).remember(rememberSizePref);
465                }
466            }
467            super.setVisible(visible);
468    
469            if (!visible && disposeOnClose) {
470                dispose();
471            }
472        }
473    
474        /**
475         * Call this if you want the dialog to remember the size set by the user.
476         * Set the pref to <code>null</code> or to an empty string to disable again.
477         * By default, it's disabled.
478         *
479         * Note: If you want to set the width of this dialog directly use the usual
480         * setSize, setPreferredSize, setMaxSize, setMinSize
481         *
482         * @param pref  The preference to save the dimension to
483         * @param wg    The default window geometry that should be used if no
484         *              existing preference is found (only takes effect if
485         *              <code>pref</code> is not null or empty
486         *
487         */
488        public ExtendedDialog setRememberWindowGeometry(String pref, WindowGeometry wg) {
489            rememberSizePref = pref == null ? "" : pref;
490            defaultWindowGeometry = wg;
491            return this;
492        }
493    
494        /**
495         * Calling this will offer the user a "Do not show again" checkbox for the
496         * dialog. Default is to not offer the choice; the dialog will be shown
497         * every time.
498         * Currently, this is not supported for non-modal dialogs.
499         * @param togglePref  The preference to save the checkbox state to
500         */
501        public ExtendedDialog toggleEnable(String togglePref) {
502            if (!modal) {
503                throw new IllegalArgumentException();
504            }
505            this.toggleable = true;
506            this.togglePref = togglePref;
507            return this;
508        }
509    
510        /**
511         * Call this if you "accidentally" called toggleEnable. This doesn't need
512         * to be called for every dialog, as it's the default anyway.
513         */
514        public ExtendedDialog toggleDisable() {
515            this.toggleable = false;
516            return this;
517        }
518    
519        /**
520         * Overwrites the default "Don't show again" text of the toggle checkbox
521         * if you want to give more information. Only has an effect if
522         * <code>toggleEnable</code> is set.
523         * @param text
524         */
525        public ExtendedDialog setToggleCheckboxText(String text) {
526            this.toggleCheckboxText = text;
527            return this;
528        }
529    
530        /**
531         * Sets the button that will react to ENTER.
532         */
533        public ExtendedDialog setDefaultButton(int defaultButtonIdx) {
534            this.defaultButtonIdx = defaultButtonIdx;
535            return this;
536        }
537    
538        /**
539         * Used in combination with toggle:
540         * If the user presses 'cancel' the toggle settings are ignored and not saved to the pref
541         * @param cancelButton index of the button that stands for cancel, accepts
542         *                     multiple values
543         */
544        public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) {
545            this.cancelButtonIdx = Arrays.<Integer>asList(cancelButtonIdx);
546            return this;
547        }
548    
549        /**
550         * Don't focus the "do not show this again" check box, but the default button.
551         */
552        protected void fixFocus() {
553            if (toggleable && defaultButton != null) {
554                SwingUtilities.invokeLater(new Runnable() {
555                    @Override public void run() {
556                        defaultButton.requestFocusInWindow();
557                    }
558                });
559            }
560        }
561    
562        /**
563         * This function returns true if the dialog has been set to "do not show again"
564         * @return true if dialog should not be shown again
565         */
566        private boolean toggleCheckState(String togglePref) {
567            toggleable = togglePref != null && !togglePref.equals("");
568    
569            toggleValue = Main.pref.getInteger("message."+togglePref+".value", -1);
570            // No identifier given, so return false (= show the dialog)
571            if(!toggleable || toggleValue == -1)
572                return false;
573            this.togglePref = togglePref;
574            // The pref is true, if the dialog should be shown.
575            return !(Main.pref.getBoolean("message."+ togglePref, true));
576        }
577    
578        /**
579         * This function checks the state of the "Do not show again" checkbox and
580         * writes the corresponding pref.
581         */
582        private void toggleSaveState() {
583            if (!toggleable ||
584                    toggleCheckbox == null ||
585                    cancelButtonIdx.contains(result) ||
586                    result == ExtendedDialog.DialogClosedOtherwise)
587                return;
588            Main.pref.put("message."+ togglePref, !toggleCheckbox.isSelected());
589            Main.pref.putInteger("message."+togglePref+".value", result);
590        }
591    
592        /**
593         * Convenience function that converts a given string into a JMultilineLabel
594         * @param msg
595         * @return JMultilineLabel
596         */
597        private static JMultilineLabel string2label(String msg) {
598            JMultilineLabel lbl = new JMultilineLabel(msg);
599            // Make it not wider than 1/2 of the screen
600            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
601            lbl.setMaxWidth(screenSize.width/2);
602            return lbl;
603        }
604    
605        /**
606         * Configures how this dialog support for context sensitive help.
607         * <ul>
608         *  <li>if helpTopic is null, the dialog doesn't provide context sensitive help</li>
609         *  <li>if helpTopic != null, the dialog redirect user to the help page for this helpTopic when
610         *  the user clicks F1 in the dialog</li>
611         *  <li>if showHelpButton is true, the dialog displays "Help" button (rightmost button in
612         *  the button row)</li>
613         * </ul>
614         *
615         * @param helpTopic the help topic
616         * @param showHelpButton true, if the dialog displays a help button
617         */
618        public ExtendedDialog configureContextsensitiveHelp(String helpTopic, boolean showHelpButton) {
619            this.helpTopic = helpTopic;
620            this.showHelpButton = showHelpButton;
621            return this;
622        }
623    
624        class HelpAction extends AbstractAction {
625            public HelpAction() {
626                putValue(SHORT_DESCRIPTION, tr("Show help information"));
627                putValue(NAME, tr("Help"));
628                putValue(SMALL_ICON, ImageProvider.get("help"));
629            }
630    
631            @Override public void actionPerformed(ActionEvent e) {
632                HelpBrowser.setUrlForHelpTopic(helpTopic);
633            }
634        }
635    }