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