Use a real tab strip for servers instead of a button bar

ServerTabPane replaces ServerTabBar: the views live in a JTabbedPane
whose strip sits under the toolbar, with the native tab shapes and the
same look as the chat tabs. Each tab carries its server name, the
microphone marker and a close glyph. With a single connection the
server view is mounted directly, so the strip appears only once a
second server is open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 15:23:25 +00:00
parent 03c6ea9a3b
commit 41bf9506e3
3 changed files with 157 additions and 104 deletions

View File

@@ -9,7 +9,6 @@ import com.ts3client.config.Settings;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
@@ -26,7 +25,6 @@ import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.KeyEventDispatcher;
@@ -44,9 +42,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
*
* <p>Several servers can be connected at once — their incoming voice is mixed
* into the same output — but only one of them owns the microphone at a time
* ("active" tab, marked in the tab bar and switched from the toolbar).
* ("active" tab, marked in the tab strip and switched from the toolbar).
*/
public final class MainFrame extends JFrame implements ServerTabBar.Listener {
public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private final Settings settings;
private final Bookmarks bookmarks = Bookmarks.load();
@@ -54,10 +52,7 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
private final AudioBackend audio = new JavaSoundAudioBackend();
private final List<ServerTab> tabs = new ArrayList<>();
private final CardLayout cards = new CardLayout();
private final JPanel cardPanel = new JPanel(cards);
private final ServerTabBar tabBar = new ServerTabBar(this);
private int tabCounter;
private final ServerTabPane tabPane = new ServerTabPane(this);
/** The tab whose views are on screen. */
private ServerTab selected;
@@ -105,12 +100,8 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
setJMenuBar(buildMenuBar());
JPanel top = new JPanel();
top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS));
top.add(buildToolbar());
top.add(tabBar);
add(top, BorderLayout.NORTH);
add(cardPanel, BorderLayout.CENTER);
add(buildToolbar(), BorderLayout.NORTH);
add(tabPane, BorderLayout.CENTER);
add(buildStatusBar(), BorderLayout.SOUTH);
codecLabel.setText(audio.description());
@@ -292,11 +283,9 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
private ServerTab newTab() {
ServerTab tab = new ServerTab(this, settings, identities, audio);
String name = "tab" + (tabCounter++);
tab.component().setName(name); // CardLayout addresses cards by this name
tabs.add(tab);
cardPanel.add(tab.component(), name);
refreshTabBar();
tabPane.addTab(tab);
refreshTabs();
return tab;
}
@@ -312,8 +301,8 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
public void selectTab(ServerTab tab) {
if (tab == null || !tabs.contains(tab)) return;
selected = tab;
cards.show(cardPanel, tab.component().getName());
refreshTabBar();
tabPane.setSelected(tab);
refreshTabs();
updateToolbar();
updateStatusLabel();
}
@@ -325,13 +314,13 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
if (tabs.size() == 1) return; // always keep one view around
tabs.remove(tab);
cardPanel.remove(tab.component());
tabPane.removeTab(tab);
if (micTab == tab) micTab = null;
if (selected == tab) {
selected = null;
selectTab(tabs.get(0));
} else {
refreshTabBar();
selectTab(selected); // the pane rebuilt itself; restore the selection
}
assignMicrophoneIfFree();
}
@@ -341,7 +330,7 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
if (micTab == tab) return;
ServerTab previous = micTab;
micTab = tab;
refreshTabBar();
refreshTabs();
// Closing and reopening the capture line can block briefly; keep it off the EDT.
new Thread(() -> {
if (previous != null) previous.setMicrophoneActive(false);
@@ -359,13 +348,12 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
return;
}
}
refreshTabBar();
refreshTabs();
}
private void refreshTabBar() {
tabBar.rebuild(tabs, selected, micTab);
// The bar appears/disappears with the second connection.
getContentPane().revalidate();
/** Repaints the tab labels (title, microphone marker). */
private void refreshTabs() {
tabPane.refresh(micTab);
}
// ---- callbacks from ServerTab ----
@@ -373,7 +361,7 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
/** A tab's title, status or connection state changed. */
void tabUpdated(ServerTab tab) {
if (!tabs.contains(tab)) return;
refreshTabBar();
refreshTabs();
if (tab == selected) {
updateToolbar();
updateStatusLabel();