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 => 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}