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

Last change on this file was 18923, checked in by taylor.smock, 6 months ago

Fix #16485: Ensure windows lose always-on-top status when JOSM loses focus

Also fix some spotbugs/sonarlint issues.

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