001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.GridBagLayout;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.Map;
011import java.util.Set;
012
013import javax.swing.ButtonGroup;
014import javax.swing.JOptionPane;
015import javax.swing.JPanel;
016import javax.swing.JRadioButton;
017
018import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
019import org.openstreetmap.josm.spi.preferences.Config;
020import org.openstreetmap.josm.tools.GBC;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * ConditionalOptionPaneUtil provides static utility methods for displaying modal message dialogs
025 * which can be enabled/disabled by the user.
026 *
027 * They wrap the methods provided by {@link JOptionPane}. Within JOSM you should use these
028 * methods rather than the bare methods from {@link JOptionPane} because the methods provided
029 * by ConditionalOptionPaneUtil ensure that a dialog window is always on top and isn't hidden by one of the
030 * JOSM windows for detached dialogs, relation editors, history browser and the like.
031 *
032 */
033public final class ConditionalOptionPaneUtil {
034    public static final int DIALOG_DISABLED_OPTION = Integer.MIN_VALUE;
035
036    /** (preference key => return value) mappings valid for the current operation (no, those two maps cannot be combined) */
037    private static final Map<String, Integer> sessionChoices = new HashMap<>();
038    /** (preference key =&gt; return value) mappings valid for the current session */
039    private static final Map<String, Integer> immediateChoices = new HashMap<>();
040    /** a set indication that (preference key) is or may be stored for the currently active bulk operation */
041    private static final Set<String> immediateActive = new HashSet<>();
042
043    /**
044     * this is a static utility class only
045     */
046    private ConditionalOptionPaneUtil() {
047        // Hide default constructor for utility classes
048    }
049
050    /**
051     * Returns the preference value for the preference key "message." + <code>prefKey</code> + ".value".
052     * The default value if the preference key is missing is -1.
053     *
054     * @param  prefKey the preference key
055     * @return the preference value for the preference key "message." + <code>prefKey</code> + ".value"
056     */
057    public static int getDialogReturnValue(String prefKey) {
058        return Utils.firstNonNull(immediateChoices.get(prefKey),
059                sessionChoices.get(prefKey),
060                !Config.getPref().getBoolean("message." + prefKey, true) ? Config.getPref().getInt("message." + prefKey + ".value", -1) : -1
061        );
062    }
063
064    /**
065     * Marks the beginning of a bulk operation in order to provide a "Do not show again (this operation)" option.
066     * @param prefKey the preference key
067     */
068    public static void startBulkOperation(final String prefKey) {
069        immediateActive.add(prefKey);
070    }
071
072    /**
073     * Determines whether the key has been marked to be part of a bulk operation
074     * (in order to provide a "Do not show again (this operation)" option).
075     * @param prefKey the preference key
076     * @return {@code true} if the key has been marked to be part of a bulk operation
077     */
078    public static boolean isInBulkOperation(final String prefKey) {
079        return immediateActive.contains(prefKey);
080    }
081
082    /**
083     * Marks the ending of a bulk operation. Removes the "Do not show again (this operation)" result value.
084     * @param prefKey the preference key
085     */
086    public static void endBulkOperation(final String prefKey) {
087        immediateActive.remove(prefKey);
088        immediateChoices.remove(prefKey);
089    }
090
091    /**
092     * Displays an confirmation dialog with some option buttons given by <code>optionType</code>.
093     * It is always on top even if there are other open windows like detached dialogs,
094     * relation editors, history browsers and the like.
095     *
096     * Set <code>optionType</code> to {@link JOptionPane#YES_NO_OPTION} for a dialog with a YES and
097     * a NO button.
098
099     * Set <code>optionType</code> to {@link JOptionPane#YES_NO_CANCEL_OPTION} for a dialog with a YES,
100     * a NO and a CANCEL button
101     *
102     * Returns one of the constants JOptionPane.YES_OPTION, JOptionPane.NO_OPTION,
103     * JOptionPane.CANCEL_OPTION or JOptionPane.CLOSED_OPTION depending on the action chosen by
104     * the user.
105     *
106     * @param preferenceKey the preference key
107     * @param parent  the parent component
108     * @param message  the message
109     * @param title the title
110     * @param optionType  the option type
111     * @param messageType the message type
112     * @param options a list of options
113     * @param defaultOption the default option; only meaningful if options is used; can be null
114     *
115     * @return the option selected by user.
116     *         {@link JOptionPane#CLOSED_OPTION} if the dialog was closed.
117     */
118    public static int showOptionDialog(String preferenceKey, Component parent, Object message, String title, int optionType,
119            int messageType, Object[] options, Object defaultOption) {
120        int ret = getDialogReturnValue(preferenceKey);
121        if (isYesOrNo(ret))
122            return ret;
123        MessagePanel pnl = new MessagePanel(message, isInBulkOperation(preferenceKey));
124        ret = JOptionPane.showOptionDialog(parent, pnl, title, optionType, messageType, null, options, defaultOption);
125        if (isYesOrNo(ret)) {
126            pnl.getNotShowAgain().store(preferenceKey, ret);
127        }
128        return ret;
129    }
130
131    /**
132     * Displays a confirmation dialog with some option buttons given by <code>optionType</code>.
133     * It is always on top even if there are other open windows like detached dialogs,
134     * relation editors, history browsers and the like.
135     *
136     * Set <code>optionType</code> to {@link JOptionPane#YES_NO_OPTION} for a dialog with a YES and
137     * a NO button.
138
139     * Set <code>optionType</code> to {@link JOptionPane#YES_NO_CANCEL_OPTION} for a dialog with a YES,
140     * a NO and a CANCEL button
141     *
142     * Replies true, if the selected option is equal to <code>trueOption</code>, otherwise false.
143     * Replies true, if the dialog is not displayed because the respective preference option
144     * <code>preferenceKey</code> is set to false and the user has previously chosen
145     * <code>trueOption</code>.
146     *
147     * @param preferenceKey the preference key
148     * @param parent  the parent component
149     * @param message  the message
150     * @param title the title
151     * @param optionType  the option type
152     * @param messageType the message type
153     * @param trueOption if this option is selected the method replies true
154     *
155     *
156     * @return true, if the selected option is equal to <code>trueOption</code>, otherwise false.
157     *
158     * @see JOptionPane#INFORMATION_MESSAGE
159     * @see JOptionPane#WARNING_MESSAGE
160     * @see JOptionPane#ERROR_MESSAGE
161     */
162    public static boolean showConfirmationDialog(String preferenceKey, Component parent, Object message, String title,
163            int optionType, int messageType, int trueOption) {
164        int ret = getDialogReturnValue(preferenceKey);
165        if (isYesOrNo(ret))
166            return ret == trueOption;
167        MessagePanel pnl = new MessagePanel(message, isInBulkOperation(preferenceKey));
168        ret = JOptionPane.showConfirmDialog(parent, pnl, title, optionType, messageType);
169        if (isYesOrNo(ret)) {
170            pnl.getNotShowAgain().store(preferenceKey, ret);
171        }
172        return ret == trueOption;
173    }
174
175    private static boolean isYesOrNo(int returnCode) {
176        return (returnCode == JOptionPane.YES_OPTION) || (returnCode == JOptionPane.NO_OPTION);
177    }
178
179    /**
180     * Displays an message in modal dialog with an OK button. Makes sure the dialog
181     * is always on top even if there are other open windows like detached dialogs,
182     * relation editors, history browsers and the like.
183     *
184     * If there is a preference with key <code>preferenceKey</code> and value <code>false</code>
185     * the dialog is not show.
186     *
187     * @param preferenceKey the preference key
188     * @param parent  the parent component
189     * @param message  the message
190     * @param title the title
191     * @param messageType the message type
192     *
193     * @see JOptionPane#INFORMATION_MESSAGE
194     * @see JOptionPane#WARNING_MESSAGE
195     * @see JOptionPane#ERROR_MESSAGE
196     */
197    public static void showMessageDialog(String preferenceKey, Component parent, Object message, String title, int messageType) {
198        if (getDialogReturnValue(preferenceKey) == Integer.MAX_VALUE)
199            return;
200        MessagePanel pnl = new MessagePanel(message, isInBulkOperation(preferenceKey));
201        JOptionPane.showMessageDialog(parent, pnl, title, messageType);
202        pnl.getNotShowAgain().store(preferenceKey, Integer.MAX_VALUE);
203    }
204
205    /**
206     * An enum designating how long to not show this message again, i.e., for how long to store
207     */
208    enum NotShowAgain {
209        NO, OPERATION, SESSION, PERMANENT;
210
211        /**
212         * Stores the dialog result {@code value} at the corresponding place.
213         * @param prefKey the preference key
214         * @param value the dialog result
215         */
216        void store(String prefKey, Integer value) {
217            switch (this) {
218                case NO:
219                    break;
220                case OPERATION:
221                    immediateChoices.put(prefKey, value);
222                    break;
223                case SESSION:
224                    sessionChoices.put(prefKey, value);
225                    break;
226                case PERMANENT:
227                    Config.getPref().putBoolean("message." + prefKey, false);
228                    Config.getPref().putInt("message." + prefKey + ".value", value);
229                    break;
230            }
231        }
232
233        String getLabel() {
234            switch (this) {
235                case NO:
236                    return tr("Show this dialog again the next time");
237                case OPERATION:
238                    return tr("Do not show again (this operation)");
239                case SESSION:
240                    return tr("Do not show again (this session)");
241                case PERMANENT:
242                    return tr("Do not show again (remembers choice)");
243            }
244            throw new IllegalStateException();
245        }
246    }
247
248    /**
249     * This is a message panel used in dialogs which can be enabled/disabled with a preference setting.
250     * In addition to the normal message any {@link JOptionPane} would display it includes
251     * a checkbox for enabling/disabling this particular dialog.
252     *
253     */
254    public static class MessagePanel extends JPanel {
255        private final JRadioButton cbShowPermanentDialog = new JRadioButton(NotShowAgain.PERMANENT.getLabel());
256        private final JRadioButton cbShowSessionDialog = new JRadioButton(NotShowAgain.SESSION.getLabel());
257        private final JRadioButton cbShowImmediateDialog = new JRadioButton(NotShowAgain.OPERATION.getLabel());
258        private final JRadioButton cbStandard = new JRadioButton(NotShowAgain.NO.getLabel());
259
260        /**
261         * Constructs a new panel.
262         * @param message the the message (null to add no message, Component instances are added directly,
263         *                otherwise a JLabel with the string representation is added)
264         * @param displayImmediateOption whether to provide "Do not show again (this session)"
265         */
266        MessagePanel(Object message, boolean displayImmediateOption) {
267            cbStandard.setSelected(true);
268            ButtonGroup group = new ButtonGroup();
269            group.add(cbShowPermanentDialog);
270            group.add(cbShowSessionDialog);
271            group.add(cbShowImmediateDialog);
272            group.add(cbStandard);
273
274            setLayout(new GridBagLayout());
275            if (message instanceof Component) {
276                add((Component) message, GBC.eop());
277            } else if (message != null) {
278                add(new JMultilineLabel(message.toString()), GBC.eop());
279            }
280            add(cbShowPermanentDialog, GBC.eol());
281            add(cbShowSessionDialog, GBC.eol());
282            if (displayImmediateOption) {
283                add(cbShowImmediateDialog, GBC.eol());
284            }
285            add(cbStandard, GBC.eol());
286        }
287
288        NotShowAgain getNotShowAgain() {
289            return cbStandard.isSelected()
290                    ? NotShowAgain.NO
291                    : cbShowImmediateDialog.isSelected()
292                    ? NotShowAgain.OPERATION
293                    : cbShowSessionDialog.isSelected()
294                    ? NotShowAgain.SESSION
295                    : cbShowPermanentDialog.isSelected()
296                    ? NotShowAgain.PERMANENT
297                    : null;
298        }
299    }
300}