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