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