diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java
index 026f2b2..d9be3e7 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java
@@ -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;
*
*
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 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();
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabBar.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabBar.java
deleted file mode 100644
index d22a2be..0000000
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabBar.java
+++ /dev/null
@@ -1,75 +0,0 @@
-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 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;
- }
-}
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java
new file mode 100644
index 0000000..9567c71
--- /dev/null
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java
@@ -0,0 +1,140 @@
+package com.ts3client.ui;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Container for the open server connections. With a single connection the
+ * server view fills the window as it always did; from the second connection on
+ * the views move into a {@link JTabbedPane} whose tab strip sits directly under
+ * the toolbar.
+ */
+final class ServerTabPane extends JPanel {
+
+ interface Listener {
+ void selectTab(ServerTab tab);
+
+ void closeTab(ServerTab tab);
+ }
+
+ private final Listener listener;
+ private final JTabbedPane tabbed = new JTabbedPane();
+ private final List tabs = new ArrayList<>();
+
+ /** True while the tab strip is in use, i.e. more than one connection is open. */
+ private boolean tabbedMode;
+ /** Suppresses selection callbacks while we rearrange the pane ourselves. */
+ private boolean updating;
+
+ ServerTabPane(Listener listener) {
+ super(new BorderLayout());
+ this.listener = listener;
+ tabbed.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
+ tabbed.setFocusable(false);
+ tabbed.addChangeListener(e -> {
+ if (updating) return;
+ int i = tabbed.getSelectedIndex();
+ if (i >= 0 && i < tabs.size()) listener.selectTab(tabs.get(i));
+ });
+ }
+
+ void addTab(ServerTab tab) {
+ tabs.add(tab);
+ relayout();
+ }
+
+ void removeTab(ServerTab tab) {
+ tabs.remove(tab);
+ relayout();
+ }
+
+ void setSelected(ServerTab tab) {
+ int i = tabs.indexOf(tab);
+ if (!tabbedMode || i < 0 || tabbed.getSelectedIndex() == i) return;
+ updating = true;
+ try {
+ tabbed.setSelectedIndex(i);
+ } finally {
+ updating = false;
+ }
+ }
+
+ /** Refreshes the tab labels; {@code micTab} is marked as owning the microphone. */
+ void refresh(ServerTab micTab) {
+ if (!tabbedMode) return;
+ for (int i = 0; i < tabs.size(); i++) {
+ ServerTab tab = tabs.get(i);
+ tabbed.setTitleAt(i, tab.title());
+ tabbed.setToolTipTextAt(i, tab.status());
+ tabbed.setTabComponentAt(i, buildTabComponent(tab, tab == micTab));
+ }
+ }
+
+ /**
+ * Moves the views between the plain single-server layout and the tabbed one,
+ * keeping whatever is currently selected on screen.
+ */
+ private void relayout() {
+ boolean wantTabs = tabs.size() > 1;
+ updating = true;
+ try {
+ removeAll();
+ tabbed.removeAll();
+ if (wantTabs) {
+ for (ServerTab tab : tabs) tabbed.addTab(tab.title(), tab.component());
+ add(tabbed, BorderLayout.CENTER);
+ } else if (!tabs.isEmpty()) {
+ add(tabs.get(0).component(), BorderLayout.CENTER);
+ }
+ tabbedMode = wantTabs;
+ } finally {
+ updating = false;
+ }
+ revalidate();
+ repaint();
+ }
+
+ /** A tab's label: title, microphone marker and a close button. */
+ private JComponent buildTabComponent(ServerTab tab, boolean hasMic) {
+ JPanel cell = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
+ cell.setOpaque(false);
+ cell.setBorder(BorderFactory.createEmptyBorder(1, 0, 1, 0));
+
+ JLabel label = new JLabel(tab.title(), hasMic ? Icons.micActive() : null, JLabel.LEADING);
+ label.setFont(Theme.UI_FONT);
+ label.setToolTipText(hasMic ? "Speaking on this server" : tab.status());
+ // A label with a tooltip swallows mouse events, so select the tab explicitly.
+ label.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ listener.selectTab(tab);
+ }
+ });
+ cell.add(label);
+
+ JButton close = new JButton("✕");
+ close.setFont(Theme.UI_FONT);
+ close.setFocusable(false);
+ close.setBorder(BorderFactory.createEmptyBorder());
+ close.setContentAreaFilled(false);
+ close.setMargin(new Insets(0, 0, 0, 0));
+ close.setForeground(Theme.CHAT_SYSTEM);
+ close.setPreferredSize(new Dimension(14, 14));
+ close.setToolTipText("Close this connection");
+ close.addActionListener(e -> listener.closeTab(tab));
+ cell.add(close);
+ return cell;
+ }
+}