Tabbed servers: multiple simultaneous connections

Each connection now lives in its own ServerTab with its own tree, info
and chat views plus its own mic-mute, deafen, away and commander state.
MainFrame keeps only the shell (menus, toolbar, status bar) and shows
one tab at a time; the tab bar below the toolbar appears once a second
server is open.

Incoming voice from every connection mixes into the same output — each
connection already renders one line per speaker. Capture is exclusive:
a connection only opens the microphone while it holds it, and a toolbar
button moves it to the visible tab. The mic follows a disconnect to
another connected tab, and push-to-talk drives whoever holds it.

Startup bookmarks are no longer mutually exclusive: every flagged
bookmark opens its own tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 15:18:47 +00:00
parent 5db3f4d8c4
commit 03c6ea9a3b
7 changed files with 843 additions and 373 deletions

View File

@@ -0,0 +1,75 @@
package com.ts3client.ui;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.util.List;
/**
* Strip of open server connections, shown between the toolbar and the server
* view. The bar only appears while more than one connection is open; with a
* single server the client looks exactly as it did before.
*/
final class ServerTabBar extends JPanel {
interface Listener {
void selectTab(ServerTab tab);
void closeTab(ServerTab tab);
}
private final Listener listener;
ServerTabBar(Listener listener) {
super(new FlowLayout(FlowLayout.LEFT, 3, 2));
this.listener = listener;
setBackground(Theme.TOOLBAR_BG);
setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Theme.WINDOW_BG));
setVisible(false);
}
/**
* @param micTab the connection holding the microphone, marked with an icon
*/
void rebuild(List<ServerTab> tabs, ServerTab selected, ServerTab micTab) {
removeAll();
for (ServerTab tab : tabs) {
add(buildCell(tab, tab == selected, tab == micTab, tabs.size() > 1));
}
setVisible(tabs.size() > 1);
revalidate();
repaint();
}
private JPanel buildCell(ServerTab tab, boolean selected, boolean hasMic, boolean closable) {
JPanel cell = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
cell.setOpaque(false);
JToggleButton button = new JToggleButton(tab.title(), hasMic ? Icons.micActive() : null);
button.setSelected(selected);
button.setFocusable(false);
button.setFont(selected ? Theme.UI_BOLD : Theme.UI_FONT);
button.setMargin(new Insets(2, 8, 2, 8));
button.setToolTipText(tab.status());
button.addActionListener(e -> listener.selectTab(tab));
cell.add(button);
if (closable) {
JButton close = new JButton("✕");
close.setFocusable(false);
close.setFont(Theme.UI_FONT);
close.setMargin(new Insets(2, 5, 2, 5));
// Keep the cell compact but never so small that the glyph is clipped.
Dimension size = close.getPreferredSize();
close.setPreferredSize(new Dimension(size.width, button.getPreferredSize().height));
close.setToolTipText("Close this connection");
close.addActionListener(e -> listener.closeTab(tab));
cell.add(close);
}
return cell;
}
}