Keep each server's session state in core

What a server tab knows beyond its views - the connection lifecycle,
title and status, the identity used, the microphone/speaker/away and
commander flags, chat sending and the channel lookups - moves from the
Swing ServerTab and ServerTabSelfState into core's ServerSession, so
another frontend gets the same behaviour without copying it.

ServerSession also takes over what the Swing event bridge decided on its
own: clearing the model on disconnect, naming the tab after the server,
showing senders under their contact names, and the chat's system lines.
The bridge now only updates views. SelfState moves along, while its
icons stay with the tray.

A server switch no longer needs to queue behind the UI thread: the
disconnect is reported synchronously, so the session is free again by
the time it reconnects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 08:09:23 +00:00
parent 24f4b3813d
commit 8f385b9343
10 changed files with 657 additions and 580 deletions

View File

@@ -1,9 +1,11 @@
package com.ts3client.ui;
import com.ts3client.session.SelfState;
import com.ts3client.ui.tray.AwtTray;
import com.ts3client.ui.tray.TrayBackend;
import com.ts3client.ui.tray.X11Tray;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JMenuItem;
@@ -76,7 +78,7 @@ final class TrayController {
// -Dts3j.tray=awt|x11 pins the backend; by default the transparent one is
// tried first and AWT's picks up whatever it leaves.
String choice = System.getProperty("ts3j.tray", "auto");
IntFunction<Image> initial = size -> SelfState.DISCONNECTED.icon(size).getImage();
IntFunction<Image> initial = size -> icon(SelfState.DISCONNECTED, size).getImage();
TrayBackend backend = choice.equals("awt") ? null : X11Tray.create(listener, initial);
if (backend == null && !choice.equals("x11")) {
backend = AwtTray.create(listener, buildAwtMenu(), initial);
@@ -93,7 +95,7 @@ final class TrayController {
if (newState == state && newTooltip.equals(tooltip)) return;
state = newState;
tooltip = newTooltip;
tray.setIcon(size -> newState.icon(size).getImage());
tray.setIcon(size -> icon(newState, size).getImage());
tray.setTooltip(newTooltip);
}
@@ -101,13 +103,28 @@ final class TrayController {
void refreshIcon() {
if (tray == null || state == null) return;
SelfState current = state;
tray.setIcon(size -> current.icon(size).getImage());
tray.setIcon(size -> icon(current, size).getImage());
}
void dispose() {
if (tray != null) tray.dispose();
}
/** The icon a state is drawn with, from the active icon pack. */
private static ImageIcon icon(SelfState state, int size) {
return switch (state) {
case DISCONNECTED -> Icons.app(size);
case DEAFENED -> Icons.speakerMuted(size);
case MIC_MUTED -> Icons.micMuted(size);
case MIC_LOCAL_MUTED -> Icons.micLocalMuted(size);
case AWAY -> Icons.clientAway(size);
case COMMANDER_TALKING -> Icons.clientCommanderTalking(size);
case COMMANDER -> Icons.clientCommander(size);
case TALKING -> Icons.clientTalking(size);
case IDLE -> Icons.clientIdle(size);
};
}
// ---- menus ----
/** The menu for AWT's icon, which shows one of its own. */