Show the local client's state in the system tray
The icon carries the badge the tree draws next to our own nickname on the active server — idle, talking, away, commander, microphone or speakers muted — and clicking it brings the window back to the front. Right click offers "Show TS3J" and "Quit". AWT's tray icon cannot be transparent on X11: the toolkit embeds a window of the screen's default, opaque visual and fills it with a background colour before drawing the image, so every icon sits in a box. Panels that can show transparent icons advertise an ARGB visual instead, so the icon is docked by hand over the system tray protocol, in that visual, with the image put on the window premultiplied. AWT's own icon stays as the fallback for everything else. Two details a panel will not forgive: an icon that publishes no size hints is allocated a one-pixel sliver, and docking is a request, so an icon that is never adopted has to hand over to the fallback rather than sit invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,9 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
private final JLabel statusLabel = new JLabel("Not connected");
|
||||
private final JLabel codecLabel = new JLabel();
|
||||
|
||||
/** Mirrors the active server's own client state next to the clock. */
|
||||
private TrayController tray;
|
||||
|
||||
private JToolBar toolbar;
|
||||
private JButton connectButton;
|
||||
private JButton disconnectButton;
|
||||
@@ -104,14 +107,14 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
shutdown();
|
||||
System.exit(0);
|
||||
quit();
|
||||
}
|
||||
});
|
||||
// Catches Ctrl+C / SIGTERM so we still leave the servers cleanly.
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||
setMinimumSize(new Dimension(720, 480));
|
||||
|
||||
tray = new TrayController(this, this::quit);
|
||||
setJMenuBar(buildMenuBar());
|
||||
|
||||
toolbar = buildToolbar();
|
||||
@@ -127,6 +130,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
selectTab(first);
|
||||
first.chat().appendSystem("Welcome to the TS3J Swing client.");
|
||||
first.chat().appendSystem("Use Connections → Connect to join a server.");
|
||||
first.chat().appendSystem(tray.status());
|
||||
|
||||
installPushToTalk();
|
||||
statusTimer = new javax.swing.Timer(3000, e -> updateConnectionStatus());
|
||||
@@ -158,10 +162,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
closeTab.setAccelerator(KeyStroke.getKeyStroke("control W"));
|
||||
closeTab.addActionListener(e -> closeTab(selected));
|
||||
JMenuItem quit = new JMenuItem("Quit", Icons.of("QUIT"));
|
||||
quit.addActionListener(e -> {
|
||||
shutdown();
|
||||
System.exit(0);
|
||||
});
|
||||
quit.addActionListener(e -> quit());
|
||||
connections.add(connect);
|
||||
connections.add(disconnect);
|
||||
connections.add(closeTab);
|
||||
@@ -307,6 +308,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
add(toolbar, BorderLayout.NORTH);
|
||||
updateToolbar();
|
||||
refreshTabs();
|
||||
if (tray != null) tray.refreshIcon();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
@@ -376,6 +378,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
ServerTab previous = micTab;
|
||||
micTab = tab;
|
||||
refreshTabs();
|
||||
updateTray();
|
||||
// Closing and reopening the capture line can block briefly; keep it off the EDT.
|
||||
new Thread(() -> {
|
||||
if (previous != null) previous.setMicrophoneActive(false);
|
||||
@@ -411,6 +414,12 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
updateToolbar();
|
||||
updateStatusLabel();
|
||||
}
|
||||
updateTray();
|
||||
}
|
||||
|
||||
/** The local client on {@code tab} started or stopped talking, muted itself, … */
|
||||
void selfStateChanged(ServerTab tab) {
|
||||
if (tabs.contains(tab)) updateTray();
|
||||
}
|
||||
|
||||
void tabConnected(ServerTab tab) {
|
||||
@@ -618,6 +627,13 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
tab.shutdown();
|
||||
}
|
||||
soundPlayer.shutdown();
|
||||
if (tray != null) tray.dispose();
|
||||
}
|
||||
|
||||
/** Leaves every server and ends the process. */
|
||||
private void quit() {
|
||||
shutdown();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private void showIdentities() {
|
||||
@@ -690,6 +706,26 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
awayItem.setSelected(away);
|
||||
awayButton.setSelected(away);
|
||||
commanderItem.setSelected(connected && selected.isCommander());
|
||||
updateTray();
|
||||
}
|
||||
|
||||
/**
|
||||
* The server the tray icon speaks for: the one holding the microphone, or the
|
||||
* visible one when nobody is capturing.
|
||||
*/
|
||||
private ServerTab trayTab() {
|
||||
if (micTab != null && micTab.isConnected()) return micTab;
|
||||
return selected != null && selected.isConnected() ? selected : null;
|
||||
}
|
||||
|
||||
private void updateTray() {
|
||||
if (tray == null) return;
|
||||
ServerTab tab = trayTab();
|
||||
if (tab == null) {
|
||||
tray.update(SelfState.DISCONNECTED, null);
|
||||
} else {
|
||||
tray.update(tab.selfState(), tab.title());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatusLabel() {
|
||||
|
||||
Reference in New Issue
Block a user