Close the client's dialogs with Escape

Only the option-pane prompts closed on Escape, because Swing installs
that binding on the option pane itself. Dialogs.closeOnEscape puts the
same window-wide binding on a dialog's root pane, routed to whatever
that dialog does when its Close or Cancel button is pressed — reverting
the live-previewing settings, or flushing the bookmark being edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 14:36:17 +00:00
parent 0f8de796ea
commit 69003ced9a
8 changed files with 59 additions and 25 deletions

View File

@@ -51,6 +51,7 @@ public final class AwayMessagesDialog extends JDialog {
getContentPane().add(scroll, BorderLayout.CENTER); getContentPane().add(scroll, BorderLayout.CENTER);
getContentPane().add(buildButtons(), BorderLayout.SOUTH); getContentPane().add(buildButtons(), BorderLayout.SOUTH);
Dialogs.closeOnEscape(this, this::closeDialog);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(new Dimension(360, 300)); setSize(new Dimension(360, 300));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);

View File

@@ -1,24 +1,18 @@
package com.ts3client.ui; package com.ts3client.ui;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComboBox; import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog; import javax.swing.JDialog;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JSpinner; import javax.swing.JSpinner;
import javax.swing.JTextField; import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel; import javax.swing.SpinnerNumberModel;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Component; import java.awt.Component;
import java.awt.Frame; import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
/** /**
* Asks for a ban's reason and duration, like the official client's ban dialog: * Asks for a ban's reason and duration, like the official client's ban dialog:
@@ -64,7 +58,7 @@ final class BanDialog extends JDialog {
getContentPane().add(buttons(), BorderLayout.SOUTH); getContentPane().add(buttons(), BorderLayout.SOUTH);
ReasonDialog.focusWhenShown(reasonField); ReasonDialog.focusWhenShown(reasonField);
closeOnEscape(); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack(); pack();
setResizable(false); setResizable(false);
@@ -100,19 +94,6 @@ final class BanDialog extends JDialog {
return panel; return panel;
} }
/** Escape cancels the dialog, as it does in the kick prompts. */
private void closeOnEscape() {
JRootPane root = getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
root.getActionMap().put("cancel", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
dispose();
}
});
}
private boolean isPermanent() { private boolean isPermanent() {
return UNIT_SECONDS[unit.getSelectedIndex()] == 0; return UNIT_SECONDS[unit.getSelectedIndex()] == 0;
} }

View File

@@ -88,6 +88,7 @@ public final class BookmarksDialog extends JDialog {
if (!listModel.isEmpty()) list.setSelectedIndex(0); if (!listModel.isEmpty()) list.setSelectedIndex(0);
else showBookmark(null); else showBookmark(null);
Dialogs.closeOnEscape(this, this::closeDialog);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
addWindowListener(new java.awt.event.WindowAdapter() { addWindowListener(new java.awt.event.WindowAdapter() {
@Override @Override

View File

@@ -80,6 +80,7 @@ public final class ConnectDialog extends JDialog {
getContentPane().add(form, BorderLayout.CENTER); getContentPane().add(form, BorderLayout.CENTER);
getContentPane().add(buttons, BorderLayout.SOUTH); getContentPane().add(buttons, BorderLayout.SOUTH);
Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack(); pack();
setMinimumSize(new Dimension(340, getHeight())); setMinimumSize(new Dimension(340, getHeight()));

View File

@@ -70,6 +70,7 @@ public final class ConnectionInfoDialog extends JDialog {
content.add(buildButtons(), BorderLayout.SOUTH); content.add(buildButtons(), BorderLayout.SOUTH);
setContentPane(content); setContentPane(content);
Dialogs.closeOnEscape(this);
pack(); pack();
setLocationRelativeTo(owner); setLocationRelativeTo(owner);

View File

@@ -0,0 +1,44 @@
package com.ts3client.ui;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
/** Behaviour shared by the client's dialogs. */
final class Dialogs {
private Dialogs() {
}
/**
* Makes Escape close a dialog, the way Swing's own option panes do.
*
* <p>The binding lives on the root pane's window-wide input map, so it fires
* wherever the focus sits — but only where nothing nearer to the focused
* component claims Escape first, which is what leaves a table's or combo
* box's own "cancel the edit" behaviour intact.
*/
static void closeOnEscape(JDialog dialog) {
closeOnEscape(dialog, dialog::dispose);
}
/**
* Binds Escape to the dialog's own way of closing, for dialogs that have to
* tidy up (or discard edits) on the way out.
*/
static void closeOnEscape(JDialog dialog, Runnable close) {
JRootPane root = dialog.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ts3-close");
root.getActionMap().put("ts3-close", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
close.run();
}
});
}
}

View File

@@ -95,6 +95,7 @@ public final class IdentitiesDialog extends JDialog {
reload(); reload();
if (!listModel.isEmpty()) list.setSelectedIndex(0); if (!listModel.isEmpty()) list.setSelectedIndex(0);
Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack(); pack();
setMinimumSize(new Dimension(640, 320)); setMinimumSize(new Dimension(640, 320));

View File

@@ -116,11 +116,7 @@ public final class SettingsDialog extends JDialog {
apply(); apply();
close(); close();
}); });
cancel.addActionListener(e -> { cancel.addActionListener(e -> cancel());
notificationsPanel.revert();
iconPackPanel.revert();
close();
});
right.add(ok); right.add(ok);
right.add(cancel); right.add(cancel);
buttons.add(right, BorderLayout.EAST); buttons.add(right, BorderLayout.EAST);
@@ -129,6 +125,7 @@ public final class SettingsDialog extends JDialog {
getContentPane().add(tabs, BorderLayout.CENTER); getContentPane().add(tabs, BorderLayout.CENTER);
getContentPane().add(buttons, BorderLayout.SOUTH); getContentPane().add(buttons, BorderLayout.SOUTH);
Dialogs.closeOnEscape(this, this::cancel);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
addWindowListener(new java.awt.event.WindowAdapter() { addWindowListener(new java.awt.event.WindowAdapter() {
@Override @Override
@@ -576,6 +573,13 @@ public final class SettingsDialog extends JDialog {
if (onApply != null) onApply.run(); if (onApply != null) onApply.run();
} }
/** Leaves without applying, putting back the settings that preview themselves live. */
private void cancel() {
notificationsPanel.revert();
iconPackPanel.revert();
close();
}
private void close() { private void close() {
stopMeter(); stopMeter();
dispose(); dispose();