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.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 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 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 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 * {@link JOptionPane#YES_OPTION} if <code>GraphicsEnvironment.isHeadless</code> returns <code>true</code> 118 */ 119 public static int showOptionDialog(String preferenceKey, Component parent, Object message, String title, int optionType, 120 int messageType, Object[] options, Object defaultOption) { 121 int ret = getDialogReturnValue(preferenceKey); 122 if (isYesOrNo(ret)) 123 return ret; 124 MessagePanel pnl = new MessagePanel(message, isInBulkOperation(preferenceKey)); 125 if (GraphicsEnvironment.isHeadless()) { 126 // for unit tests 127 ret = JOptionPane.YES_OPTION; 128 } else { 129 ret = JOptionPane.showOptionDialog(parent, pnl, title, optionType, messageType, null, options, defaultOption); 130 } 131 if (isYesOrNo(ret)) { 132 pnl.getNotShowAgain().store(preferenceKey, ret); 133 } 134 return ret; 135 } 136 137 /** 138 * Displays a confirmation dialog with some option buttons given by <code>optionType</code>. 139 * It is always on top even if there are other open windows like detached dialogs, 140 * relation editors, history browsers and the like. 141 * 142 * Set <code>optionType</code> to {@link JOptionPane#YES_NO_OPTION} for a dialog with a YES and 143 * a NO button. 144 145 * Set <code>optionType</code> to {@link JOptionPane#YES_NO_CANCEL_OPTION} for a dialog with a YES, 146 * a NO and a CANCEL button 147 * 148 * Replies true, if the selected option is equal to <code>trueOption</code>, otherwise false. 149 * Replies true, if the dialog is not displayed because the respective preference option 150 * <code>preferenceKey</code> is set to false and the user has previously chosen 151 * <code>trueOption</code>. 152 * 153 * @param preferenceKey the preference key 154 * @param parent the parent component 155 * @param message the message 156 * @param title the title 157 * @param optionType the option type 158 * @param messageType the message type 159 * @param trueOption if this option is selected the method replies true 160 * 161 * 162 * @return true, if the selected option is equal to <code>trueOption</code>, otherwise false. 163 * {@code trueOption} if <code>GraphicsEnvironment.isHeadless</code> returns <code>true</code> 164 * 165 * @see JOptionPane#INFORMATION_MESSAGE 166 * @see JOptionPane#WARNING_MESSAGE 167 * @see JOptionPane#ERROR_MESSAGE 168 */ 169 public static boolean showConfirmationDialog(String preferenceKey, Component parent, Object message, String title, 170 int optionType, int messageType, int trueOption) { 171 int ret = getDialogReturnValue(preferenceKey); 172 if (isYesOrNo(ret)) 173 return ret == trueOption; 174 MessagePanel pnl = new MessagePanel(message, isInBulkOperation(preferenceKey)); 175 if (GraphicsEnvironment.isHeadless()) { 176 // for unit tests 177 ret = trueOption; 178 } else { 179 ret = JOptionPane.showConfirmDialog(parent, pnl, title, optionType, messageType); 180 } 181 if (isYesOrNo(ret)) { 182 pnl.getNotShowAgain().store(preferenceKey, ret); 183 } 184 return ret == trueOption; 185 } 186 187 private static boolean isYesOrNo(int returnCode) { 188 return (returnCode == JOptionPane.YES_OPTION) || (returnCode == JOptionPane.NO_OPTION); 189 } 190 191 /** 192 * Displays an message in modal dialog with an OK button. Makes sure the dialog 193 * is always on top even if there are other open windows like detached dialogs, 194 * relation editors, history browsers and the like. 195 * 196 * If there is a preference with key <code>preferenceKey</code> and value <code>false</code> 197 * the dialog is not show. 198 * 199 * @param preferenceKey the preference key 200 * @param parent the parent component 201 * @param message the message 202 * @param title the title 203 * @param messageType the message type 204 * 205 * @see JOptionPane#INFORMATION_MESSAGE 206 * @see JOptionPane#WARNING_MESSAGE 207 * @see JOptionPane#ERROR_MESSAGE 208 */ 209 public static void showMessageDialog(String preferenceKey, Component parent, Object message, String title, int messageType) { 210 if (getDialogReturnValue(preferenceKey) == Integer.MAX_VALUE) 211 return; 212 MessagePanel pnl = new MessagePanel(message, isInBulkOperation(preferenceKey)); 213 JOptionPane.showMessageDialog(parent, pnl, title, messageType); 214 pnl.getNotShowAgain().store(preferenceKey, Integer.MAX_VALUE); 215 } 216 217 /** 218 * An enum designating how long to not show this message again, i.e., for how long to store 219 */ 220 enum NotShowAgain { 221 NO, OPERATION, SESSION, PERMANENT; 222 223 /** 224 * Stores the dialog result {@code value} at the corresponding place. 225 * @param prefKey the preference key 226 * @param value the dialog result 227 */ 228 void store(String prefKey, Integer value) { 229 switch (this) { 230 case NO: 231 break; 232 case OPERATION: 233 immediateChoices.put(prefKey, value); 234 break; 235 case SESSION: 236 sessionChoices.put(prefKey, value); 237 break; 238 case PERMANENT: 239 Main.pref.put("message." + prefKey, false); 240 Main.pref.putInteger("message." + prefKey + ".value", value); 241 break; 242 } 243 } 244 245 String getLabel() { 246 switch (this) { 247 case NO: 248 return tr("Show this dialog again the next time"); 249 case OPERATION: 250 return tr("Do not show again (this operation)"); 251 case SESSION: 252 return tr("Do not show again (this session)"); 253 case PERMANENT: 254 return tr("Do not show again (remembers choice)"); 255 } 256 throw new IllegalStateException(); 257 } 258 } 259 260 /** 261 * This is a message panel used in dialogs which can be enabled/disabled with a preference setting. 262 * In addition to the normal message any {@link JOptionPane} would display it includes 263 * a checkbox for enabling/disabling this particular dialog. 264 * 265 */ 266 static class MessagePanel extends JPanel { 267 private final JRadioButton cbShowPermanentDialog = new JRadioButton(NotShowAgain.PERMANENT.getLabel()); 268 private final JRadioButton cbShowSessionDialog = new JRadioButton(NotShowAgain.SESSION.getLabel()); 269 private final JRadioButton cbShowImmediateDialog = new JRadioButton(NotShowAgain.OPERATION.getLabel()); 270 private final JRadioButton cbStandard = new JRadioButton(NotShowAgain.NO.getLabel()); 271 272 /** 273 * Constructs a new panel. 274 * @param message the the message (null to add no message, Component instances are added directly, 275 * otherwise a JLabel with the string representation is added) 276 * @param displayImmediateOption whether to provide "Do not show again (this session)" 277 */ 278 MessagePanel(Object message, boolean displayImmediateOption) { 279 cbStandard.setSelected(true); 280 ButtonGroup group = new ButtonGroup(); 281 group.add(cbShowPermanentDialog); 282 group.add(cbShowSessionDialog); 283 group.add(cbShowImmediateDialog); 284 group.add(cbStandard); 285 286 setLayout(new GridBagLayout()); 287 if (message instanceof Component) { 288 add((Component) message, GBC.eop()); 289 } else if (message != null) { 290 add(new JMultilineLabel(message.toString()), GBC.eop()); 291 } 292 add(cbShowPermanentDialog, GBC.eol()); 293 add(cbShowSessionDialog, GBC.eol()); 294 if (displayImmediateOption) { 295 add(cbShowImmediateDialog, GBC.eol()); 296 } 297 add(cbStandard, GBC.eol()); 298 } 299 300 NotShowAgain getNotShowAgain() { 301 return cbStandard.isSelected() 302 ? NotShowAgain.NO 303 : cbShowImmediateDialog.isSelected() 304 ? NotShowAgain.OPERATION 305 : cbShowSessionDialog.isSelected() 306 ? NotShowAgain.SESSION 307 : cbShowPermanentDialog.isSelected() 308 ? NotShowAgain.PERMANENT 309 : null; 310 } 311 } 312}