Make the tray menu close, and its window come forward
Swing never grabs the pointer for a popup, so a click on the desktop or another application is not seen and the menu stays up. The little window the menu hangs off is focusable now and closes it when the focus goes elsewhere, Escape closes it, and clicking the icon again toggles it. Raising the window was ignored for the same reason it usually is: a window manager refuses a raise from an application that does not have the focus. The icon asks for _NET_ACTIVE_WINDOW instead — the request meant for pagers and trays acting for the user — and falls back to Swing's always-on-top shuffle when it cannot find the window. Windows are matched by _NET_WM_PID and _NET_WM_NAME, since WM_NAME cannot carry the em dash in our title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,11 @@ import com.ts3client.ui.tray.AwtTray;
|
||||
import com.ts3client.ui.tray.TrayBackend;
|
||||
import com.ts3client.ui.tray.X11Tray;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JWindow;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
import javax.swing.event.PopupMenuListener;
|
||||
@@ -14,6 +16,10 @@ import java.awt.Frame;
|
||||
import java.awt.Image;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
/**
|
||||
@@ -37,6 +43,10 @@ final class TrayController {
|
||||
private SelfState state;
|
||||
private String tooltip = "";
|
||||
|
||||
/** The context menu while it is on screen, and the window it hangs off. */
|
||||
private JPopupMenu openMenu;
|
||||
private JDialog openAnchor;
|
||||
|
||||
TrayController(MainFrame frame, Runnable onQuit) {
|
||||
this.frame = frame;
|
||||
this.onQuit = onQuit;
|
||||
@@ -116,12 +126,38 @@ final class TrayController {
|
||||
/**
|
||||
* The same menu for backends that have none — a Swing popup needs a component to
|
||||
* hang off, so it is given an empty window at the pointer.
|
||||
*
|
||||
* <p>Swing dismisses a popup from events it sees itself, and it sees none of the
|
||||
* clicks that land on the desktop or another application, so the little window is
|
||||
* made focusable and the menu closed as soon as it loses the focus. Escape and a
|
||||
* second click on the icon close it too.
|
||||
*/
|
||||
private void showMenu(int x, int y) {
|
||||
JWindow anchor = new JWindow(frame);
|
||||
if (openMenu != null) { // a second click on the icon: close it again
|
||||
hideMenu();
|
||||
return;
|
||||
}
|
||||
JDialog anchor = new JDialog(frame);
|
||||
anchor.setUndecorated(true);
|
||||
anchor.setFocusableWindowState(true);
|
||||
anchor.setAlwaysOnTop(true);
|
||||
anchor.setLocation(x, y);
|
||||
anchor.setSize(1, 1);
|
||||
anchor.setVisible(true);
|
||||
anchor.toFront();
|
||||
anchor.requestFocus();
|
||||
anchor.addWindowFocusListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowLostFocus(WindowEvent e) {
|
||||
// The menu's own popup window may take the focus instead; only the
|
||||
// focus moving away from the menu altogether closes it.
|
||||
if (!ownedBy(e.getOppositeWindow(), anchor)) hideMenu();
|
||||
}
|
||||
});
|
||||
|
||||
anchor.getRootPane().registerKeyboardAction(e -> hideMenu(),
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||
JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
JMenuItem showItem = new JMenuItem("Show TS3J", Icons.app());
|
||||
@@ -138,22 +174,58 @@ final class TrayController {
|
||||
|
||||
@Override
|
||||
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
|
||||
anchor.dispose();
|
||||
closed(anchor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popupMenuCanceled(PopupMenuEvent e) {
|
||||
anchor.dispose();
|
||||
closed(anchor);
|
||||
}
|
||||
});
|
||||
openMenu = menu;
|
||||
openAnchor = anchor;
|
||||
menu.show(anchor, 0, 0);
|
||||
}
|
||||
|
||||
/** Brings the window back from the taskbar or an iconified state. */
|
||||
/** @return whether {@code window} is {@code owner} or one of its child windows */
|
||||
private static boolean ownedBy(Window window, Window owner) {
|
||||
for (Window w = window; w != null; w = w.getOwner()) {
|
||||
if (w == owner) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void hideMenu() {
|
||||
if (openMenu != null) openMenu.setVisible(false); // the listener cleans up
|
||||
closed(openAnchor);
|
||||
}
|
||||
|
||||
private void closed(JDialog anchor) {
|
||||
openMenu = null;
|
||||
openAnchor = null;
|
||||
if (anchor != null) anchor.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings the window back from the taskbar or an iconified state.
|
||||
*
|
||||
* <p>A window manager ignores {@link Frame#toFront()} from an application that is
|
||||
* not focused, which is every time the tray icon is used, so the backend is asked
|
||||
* first — it can make the request the window manager does honour.
|
||||
*/
|
||||
private void show() {
|
||||
hideMenu();
|
||||
frame.setVisible(true);
|
||||
frame.setExtendedState(frame.getExtendedState() & ~Frame.ICONIFIED);
|
||||
frame.toFront();
|
||||
frame.requestFocus();
|
||||
// Deferred so the window is mapped before anything tries to raise it.
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (tray != null && tray.activateWindow(frame.getTitle())) return;
|
||||
// Whatever Swing can manage: asking to be on top for a moment gets the
|
||||
// window in front even where a plain raise is refused.
|
||||
frame.setAlwaysOnTop(true);
|
||||
frame.toFront();
|
||||
frame.requestFocus();
|
||||
frame.setAlwaysOnTop(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user