source: josm/trunk/src/org/openstreetmap/josm/gui/ExtendedDialog.java@ 17903

Last change on this file since 17903 was 17738, checked in by simon04, 3 years ago

see #16163 - Fix NPE in unit tests

  • Property svn:eol-style set to native
File size: 19.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Component;
7import java.awt.Dimension;
8import java.awt.Frame;
9import java.awt.GridBagConstraints;
10import java.awt.GridBagLayout;
11import java.awt.Insets;
12import java.awt.event.ActionEvent;
13import java.awt.event.KeyEvent;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.Collections;
17import java.util.HashSet;
18import java.util.List;
19import java.util.Set;
20
21import javax.swing.AbstractAction;
22import javax.swing.Action;
23import javax.swing.Icon;
24import javax.swing.JButton;
25import javax.swing.JDialog;
26import javax.swing.JLabel;
27import javax.swing.JOptionPane;
28import javax.swing.JPanel;
29import javax.swing.JScrollBar;
30import javax.swing.JScrollPane;
31import javax.swing.KeyStroke;
32import javax.swing.UIManager;
33
34import org.openstreetmap.josm.gui.help.HelpBrowser;
35import org.openstreetmap.josm.gui.help.HelpUtil;
36import org.openstreetmap.josm.gui.util.GuiHelper;
37import org.openstreetmap.josm.gui.util.WindowGeometry;
38import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
39import org.openstreetmap.josm.io.NetworkManager;
40import org.openstreetmap.josm.io.OnlineResource;
41import org.openstreetmap.josm.tools.GBC;
42import org.openstreetmap.josm.tools.ImageProvider;
43import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
44import org.openstreetmap.josm.tools.InputMapUtils;
45import org.openstreetmap.josm.tools.Logging;
46import org.openstreetmap.josm.tools.Utils;
47
48/**
49 * General configurable dialog window.
50 *
51 * If dialog is modal, you can use {@link #getValue()} to retrieve the
52 * button index. Note that the user can close the dialog
53 * by other means. This is usually equivalent to cancel action.
54 *
55 * For non-modal dialogs, {@link #buttonAction(int, ActionEvent)} can be overridden.
56 *
57 * There are various options, see below.
58 *
59 * Note: The button indices are counted from 1 and upwards.
60 * So for {@link #getValue()}, {@link #setDefaultButton(int)} and
61 * {@link #setCancelButton} the first button has index 1.
62 *
63 * Simple example:
64 * <pre>
65 * ExtendedDialog ed = new ExtendedDialog(
66 * MainApplication.getMainFrame(), tr("Dialog Title"),
67 * new String[] {tr("Ok"), tr("Cancel")});
68 * ed.setButtonIcons(new String[] {"ok", "cancel"}); // optional
69 * ed.setIcon(JOptionPane.WARNING_MESSAGE); // optional
70 * ed.setContent(tr("Really proceed? Interesting things may happen..."));
71 * ed.showDialog();
72 * if (ed.getValue() == 1) { // user clicked first button "Ok"
73 * // proceed...
74 * }
75 * </pre>
76 */
77public class ExtendedDialog extends JDialog implements IExtendedDialog {
78 private final boolean disposeOnClose;
79 private volatile int result;
80 public static final int DialogClosedOtherwise = 0;
81 private boolean toggleable;
82 private String rememberSizePref = "";
83 private transient WindowGeometry defaultWindowGeometry;
84 private String togglePref = "";
85 private int toggleValue = -1;
86 private ConditionalOptionPaneUtil.MessagePanel togglePanel;
87 private final Component parent;
88 private Component content;
89 private final String[] bTexts;
90 private String[] bToolTipTexts;
91 private transient Icon[] bIcons;
92 private Set<Integer> cancelButtonIdx = Collections.emptySet();
93 private int defaultButtonIdx = 1;
94 protected JButton defaultButton;
95 private transient Icon icon;
96 private final boolean modal;
97 private boolean focusOnDefaultButton;
98
99 /** true, if the dialog should include a help button */
100 private boolean showHelpButton;
101 /** the help topic */
102 private String helpTopic;
103
104 /**
105 * set to true if the content of the extended dialog should
106 * be placed in a {@link JScrollPane}
107 */
108 private boolean placeContentInScrollPane;
109
110 // For easy access when inherited
111 protected transient Insets contentInsets = new Insets(10, 5, 0, 5);
112 protected transient List<JButton> buttons = new ArrayList<>();
113
114 /**
115 * This method sets up the most basic options for the dialog. Add more
116 * advanced features with dedicated methods.
117 * Possible features:
118 * <ul>
119 * <li><code>setButtonIcons</code></li>
120 * <li><code>setContent</code></li>
121 * <li><code>toggleEnable</code></li>
122 * <li><code>toggleDisable</code></li>
123 * <li><code>setToggleCheckboxText</code></li>
124 * <li><code>setRememberWindowGeometry</code></li>
125 * </ul>
126 *
127 * When done, call <code>showDialog</code> to display it. You can receive
128 * the user's choice using <code>getValue</code>. Have a look at this function
129 * for possible return values.
130 *
131 * @param parent The parent element that will be used for position and maximum size
132 * @param title The text that will be shown in the window titlebar
133 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
134 */
135 public ExtendedDialog(Component parent, String title, String... buttonTexts) {
136 this(parent, title, buttonTexts, true, true);
137 }
138
139 /**
140 * Same as above but lets you define if the dialog should be modal.
141 * @param parent The parent element that will be used for position and maximum size
142 * @param title The text that will be shown in the window titlebar
143 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
144 * @param modal Set it to {@code true} if you want the dialog to be modal
145 */
146 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal) {
147 this(parent, title, buttonTexts, modal, true);
148 }
149
150 /**
151 * Same as above but lets you define if the dialog should be disposed on close.
152 * @param parent The parent element that will be used for position and maximum size
153 * @param title The text that will be shown in the window titlebar
154 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
155 * @param modal Set it to {@code true} if you want the dialog to be modal
156 * @param disposeOnClose whether to call {@link #dispose} when closing the dialog
157 */
158 public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal, boolean disposeOnClose) {
159 super(searchRealParent(parent), title, modal ? ModalityType.DOCUMENT_MODAL : ModalityType.MODELESS);
160 this.parent = parent;
161 this.modal = modal;
162 bTexts = Utils.copyArray(buttonTexts);
163 if (disposeOnClose) {
164 setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
165 }
166 this.disposeOnClose = disposeOnClose;
167 }
168
169 private static Frame searchRealParent(Component parent) {
170 if (parent == null) {
171 return null;
172 } else {
173 return GuiHelper.getFrameForComponent(parent);
174 }
175 }
176
177 @Override
178 public ExtendedDialog setButtonIcons(Icon... buttonIcons) {
179 this.bIcons = Utils.copyArray(buttonIcons);
180 return this;
181 }
182
183 @Override
184 public ExtendedDialog setButtonIcons(String... buttonIcons) {
185 bIcons = new Icon[buttonIcons.length];
186 for (int i = 0; i < buttonIcons.length; ++i) {
187 bIcons[i] = ImageProvider.get(buttonIcons[i], ImageSizes.LARGEICON);
188 }
189 return this;
190 }
191
192 @Override
193 public ExtendedDialog setToolTipTexts(String... toolTipTexts) {
194 this.bToolTipTexts = Utils.copyArray(toolTipTexts);
195 return this;
196 }
197
198 @Override
199 public ExtendedDialog setContent(Component content) {
200 return setContent(content, true);
201 }
202
203 @Override
204 public ExtendedDialog setContent(Component content, boolean placeContentInScrollPane) {
205 this.content = content;
206 this.placeContentInScrollPane = placeContentInScrollPane;
207 return this;
208 }
209
210 @Override
211 public ExtendedDialog setContent(String message) {
212 return setContent(string2label(message), false);
213 }
214
215 @Override
216 public ExtendedDialog setIcon(Icon icon) {
217 this.icon = icon;
218 return this;
219 }
220
221 @Override
222 public ExtendedDialog setIcon(int messageType) {
223 switch (messageType) {
224 case JOptionPane.ERROR_MESSAGE:
225 return setIcon(UIManager.getIcon("OptionPane.errorIcon"));
226 case JOptionPane.INFORMATION_MESSAGE:
227 return setIcon(UIManager.getIcon("OptionPane.informationIcon"));
228 case JOptionPane.WARNING_MESSAGE:
229 return setIcon(UIManager.getIcon("OptionPane.warningIcon"));
230 case JOptionPane.QUESTION_MESSAGE:
231 return setIcon(UIManager.getIcon("OptionPane.questionIcon"));
232 case JOptionPane.PLAIN_MESSAGE:
233 return setIcon(null);
234 default:
235 throw new IllegalArgumentException("Unknown message type!");
236 }
237 }
238
239 @Override
240 public ExtendedDialog showDialog() {
241 // Check if the user has set the dialog to not be shown again
242 if (toggleCheckState()) {
243 result = toggleValue;
244 return this;
245 }
246
247 setupDialog();
248 if (defaultButton != null) {
249 getRootPane().setDefaultButton(defaultButton);
250 }
251 // Don't focus the "do not show this again" check box, but the default button.
252 if (toggleable || focusOnDefaultButton) {
253 requestFocusToDefaultButton();
254 }
255 if (MainApplication.getMainFrame() != null) {
256 applyComponentOrientation(MainApplication.getMainFrame().getComponentOrientation());
257 }
258 setVisible(true);
259 toggleSaveState();
260 return this;
261 }
262
263 @Override
264 public int getValue() {
265 return result;
266 }
267
268 private boolean setupDone;
269
270 @Override
271 public void setupDialog() {
272 if (setupDone)
273 return;
274 setupDone = true;
275
276 setupEscListener();
277
278 JButton button;
279 JPanel buttonsPanel = new JPanel(new GridBagLayout());
280
281 for (int i = 0; i < bTexts.length; i++) {
282 button = new JButton(createButtonAction(i));
283 if (i == defaultButtonIdx-1) {
284 defaultButton = button;
285 }
286 if (bIcons != null && bIcons[i] != null) {
287 button.setIcon(bIcons[i]);
288 }
289 if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) {
290 button.setToolTipText(bToolTipTexts[i]);
291 }
292
293 buttonsPanel.add(button, GBC.std().insets(2, 2, 2, 2));
294 buttons.add(button);
295 }
296 if (showHelpButton) {
297 buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2, 2, 2, 2));
298 HelpUtil.setHelpContext(getRootPane(), helpTopic);
299 }
300
301 JPanel cp = new JPanel(new GridBagLayout());
302
303 GridBagConstraints gc = new GridBagConstraints();
304 gc.gridx = 0;
305 int y = 0;
306 gc.gridy = y++;
307 gc.weightx = 0.0;
308 gc.weighty = 0.0;
309
310 if (icon != null) {
311 JLabel iconLbl = new JLabel(icon);
312 gc.insets = new Insets(10, 10, 10, 10);
313 gc.anchor = GridBagConstraints.NORTH;
314 gc.weighty = 1.0;
315 cp.add(iconLbl, gc);
316 gc.anchor = GridBagConstraints.CENTER;
317 gc.gridx = 1;
318 }
319
320 gc.fill = GridBagConstraints.BOTH;
321 gc.insets = contentInsets;
322 gc.weightx = 1.0;
323 gc.weighty = 1.0;
324 cp.add(content, gc);
325
326 gc.fill = GridBagConstraints.NONE;
327 gc.gridwidth = GridBagConstraints.REMAINDER;
328 gc.weightx = 0.0;
329 gc.weighty = 0.0;
330
331 if (toggleable) {
332 togglePanel = new ConditionalOptionPaneUtil.MessagePanel(null, ConditionalOptionPaneUtil.isInBulkOperation(togglePref));
333 gc.gridx = icon != null ? 1 : 0;
334 gc.gridy = y++;
335 gc.anchor = GridBagConstraints.LINE_START;
336 gc.insets = new Insets(5, contentInsets.left, 5, contentInsets.right);
337 cp.add(togglePanel, gc);
338 }
339
340 gc.gridy = y;
341 gc.anchor = GridBagConstraints.CENTER;
342 gc.insets = new Insets(5, 5, 5, 5);
343 cp.add(buttonsPanel, gc);
344 if (placeContentInScrollPane) {
345 JScrollPane pane = new JScrollPane(cp);
346 GuiHelper.setDefaultIncrement(pane);
347 pane.setBorder(null);
348 setContentPane(pane);
349 } else {
350 setContentPane(cp);
351 }
352 pack();
353
354 // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen
355 Dimension d = getSize();
356 Dimension x = findMaxDialogSize();
357
358 boolean limitedInWidth = d.width > x.width;
359 boolean limitedInHeight = d.height > x.height;
360
361 if (x.width > 0 && d.width > x.width) {
362 d.width = x.width;
363 }
364 if (x.height > 0 && d.height > x.height) {
365 d.height = x.height;
366 }
367
368 // We have a vertical scrollbar and enough space to prevent a horizontal one
369 if (!limitedInWidth && limitedInHeight) {
370 d.width += new JScrollBar().getPreferredSize().width;
371 }
372
373 setSize(d);
374 setLocationRelativeTo(parent);
375 }
376
377 protected Action createButtonAction(final int i) {
378 return new AbstractAction(bTexts[i]) {
379 @Override
380 public void actionPerformed(ActionEvent evt) {
381 buttonAction(i, evt);
382 }
383 };
384 }
385
386 /**
387 * This gets performed whenever a button is clicked or activated
388 * @param buttonIndex the button index (first index is 0)
389 * @param evt the button event
390 */
391 protected void buttonAction(int buttonIndex, ActionEvent evt) {
392 result = buttonIndex+1;
393 setVisible(false);
394 }
395
396 /**
397 * Tries to find a good value of how large the dialog should be
398 * @return Dimension Size of the parent component if visible or 2/3 of screen size if not available or hidden
399 */
400 protected Dimension findMaxDialogSize() {
401 Dimension screenSize = GuiHelper.getScreenSize();
402 Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3);
403 if (parent != null && parent.isVisible()) {
404 x = GuiHelper.getFrameForComponent(parent).getSize();
405 }
406 return x;
407 }
408
409 /**
410 * Makes the dialog listen to ESC keypressed
411 */
412 private void setupEscListener() {
413 Action actionListener = new AbstractAction() {
414 @Override
415 public void actionPerformed(ActionEvent actionEvent) {
416 // 0 means that the dialog has been closed otherwise.
417 // We need to set it to zero again, in case the dialog has been re-used
418 // and the result differs from its default value
419 result = ExtendedDialog.DialogClosedOtherwise;
420 if (Logging.isDebugEnabled()) {
421 Logging.debug("{0} ESC action performed ({1}) from {2}",
422 getClass().getName(), actionEvent, new Exception().getStackTrace()[1]);
423 }
424 setVisible(false);
425 }
426 };
427
428 InputMapUtils.addEscapeAction(getRootPane(), actionListener);
429 }
430
431 protected final void rememberWindowGeometry(WindowGeometry geometry) {
432 if (geometry != null) {
433 geometry.remember(rememberSizePref);
434 }
435 }
436
437 protected final WindowGeometry initWindowGeometry() {
438 return new WindowGeometry(rememberSizePref, defaultWindowGeometry);
439 }
440
441 /**
442 * Override setVisible to be able to save the window geometry if required
443 */
444 @Override
445 public void setVisible(boolean visible) {
446 if (visible) {
447 repaint();
448 }
449
450 if (Logging.isDebugEnabled()) {
451 Logging.debug(getClass().getName()+".setVisible("+visible+") from "+new Exception().getStackTrace()[1]);
452 }
453
454 // Ensure all required variables are available
455 if (!rememberSizePref.isEmpty() && defaultWindowGeometry != null) {
456 if (visible) {
457 initWindowGeometry().applySafe(this);
458 } else if (isShowing()) { // should fix #6438, #6981, #8295
459 rememberWindowGeometry(new WindowGeometry(this));
460 }
461 }
462 super.setVisible(visible);
463
464 if (!visible && disposeOnClose) {
465 dispose();
466 }
467 }
468
469 @Override
470 public ExtendedDialog setRememberWindowGeometry(String pref, WindowGeometry wg) {
471 rememberSizePref = pref == null ? "" : pref;
472 defaultWindowGeometry = wg;
473 return this;
474 }
475
476 @Override
477 public ExtendedDialog toggleEnable(String togglePref) {
478 if (!modal) {
479 throw new IllegalStateException();
480 }
481 this.toggleable = true;
482 this.togglePref = togglePref;
483 return this;
484 }
485
486 @Override
487 public ExtendedDialog setDefaultButton(int defaultButtonIdx) {
488 this.defaultButtonIdx = defaultButtonIdx;
489 return this;
490 }
491
492 @Override
493 public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) {
494 this.cancelButtonIdx = new HashSet<>(Arrays.<Integer>asList(cancelButtonIdx));
495 return this;
496 }
497
498 @Override
499 public void setFocusOnDefaultButton(boolean focus) {
500 focusOnDefaultButton = focus;
501 }
502
503 private void requestFocusToDefaultButton() {
504 if (defaultButton != null) {
505 GuiHelper.runInEDT(defaultButton::requestFocusInWindow);
506 }
507 }
508
509 @Override
510 public final boolean toggleCheckState() {
511 toggleable = togglePref != null && !togglePref.isEmpty();
512 toggleValue = ConditionalOptionPaneUtil.getDialogReturnValue(togglePref);
513 return toggleable && toggleValue != -1;
514 }
515
516 /**
517 * This function checks the state of the "Do not show again" checkbox and
518 * writes the corresponding pref.
519 */
520 protected void toggleSaveState() {
521 if (!toggleable ||
522 togglePanel == null ||
523 cancelButtonIdx.contains(result) ||
524 result == ExtendedDialog.DialogClosedOtherwise)
525 return;
526 togglePanel.getNotShowAgain().store(togglePref, result);
527 }
528
529 /**
530 * Convenience function that converts a given string into a JMultilineLabel
531 * @param msg the message to display
532 * @return JMultilineLabel displaying {@code msg}
533 */
534 private static JMultilineLabel string2label(String msg) {
535 JMultilineLabel lbl = new JMultilineLabel(msg);
536 // Make it not wider than 1/2 of the screen
537 Dimension screenSize = GuiHelper.getScreenSize();
538 lbl.setMaxWidth(screenSize.width/2);
539 // Disable default Enter key binding to allow dialog's one (then enables to hit default button from here)
540 lbl.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new Object());
541 return lbl;
542 }
543
544 @Override
545 public ExtendedDialog configureContextsensitiveHelp(String helpTopic, boolean showHelpButton) {
546 this.helpTopic = helpTopic;
547 this.showHelpButton = showHelpButton;
548 return this;
549 }
550
551 class HelpAction extends AbstractAction {
552 /**
553 * Constructs a new {@code HelpAction}.
554 */
555 HelpAction() {
556 putValue(SHORT_DESCRIPTION, tr("Show help information"));
557 putValue(NAME, tr("Help"));
558 new ImageProvider("help").getResource().attachImageIcon(this, true);
559 setEnabled(!NetworkManager.isOffline(OnlineResource.JOSM_WEBSITE));
560 }
561
562 @Override
563 public void actionPerformed(ActionEvent e) {
564 HelpBrowser.setUrlForHelpTopic(helpTopic);
565 }
566 }
567}
Note: See TracBrowser for help on using the repository browser.