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:
@@ -9,7 +9,6 @@ import com.ts3client.config.Settings;
|
|||||||
|
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.Box;
|
import javax.swing.Box;
|
||||||
import javax.swing.BoxLayout;
|
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JCheckBox;
|
import javax.swing.JCheckBox;
|
||||||
import javax.swing.JCheckBoxMenuItem;
|
import javax.swing.JCheckBoxMenuItem;
|
||||||
@@ -26,7 +25,6 @@ import javax.swing.JToolBar;
|
|||||||
import javax.swing.KeyStroke;
|
import javax.swing.KeyStroke;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.CardLayout;
|
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.GridLayout;
|
import java.awt.GridLayout;
|
||||||
import java.awt.KeyEventDispatcher;
|
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
|
* <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
|
* 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 Settings settings;
|
||||||
private final Bookmarks bookmarks = Bookmarks.load();
|
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 AudioBackend audio = new JavaSoundAudioBackend();
|
||||||
|
|
||||||
private final List<ServerTab> tabs = new ArrayList<>();
|
private final List<ServerTab> tabs = new ArrayList<>();
|
||||||
private final CardLayout cards = new CardLayout();
|
private final ServerTabPane tabPane = new ServerTabPane(this);
|
||||||
private final JPanel cardPanel = new JPanel(cards);
|
|
||||||
private final ServerTabBar tabBar = new ServerTabBar(this);
|
|
||||||
private int tabCounter;
|
|
||||||
|
|
||||||
/** The tab whose views are on screen. */
|
/** The tab whose views are on screen. */
|
||||||
private ServerTab selected;
|
private ServerTab selected;
|
||||||
@@ -105,12 +100,8 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
|
|||||||
|
|
||||||
setJMenuBar(buildMenuBar());
|
setJMenuBar(buildMenuBar());
|
||||||
|
|
||||||
JPanel top = new JPanel();
|
add(buildToolbar(), BorderLayout.NORTH);
|
||||||
top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS));
|
add(tabPane, BorderLayout.CENTER);
|
||||||
top.add(buildToolbar());
|
|
||||||
top.add(tabBar);
|
|
||||||
add(top, BorderLayout.NORTH);
|
|
||||||
add(cardPanel, BorderLayout.CENTER);
|
|
||||||
add(buildStatusBar(), BorderLayout.SOUTH);
|
add(buildStatusBar(), BorderLayout.SOUTH);
|
||||||
|
|
||||||
codecLabel.setText(audio.description());
|
codecLabel.setText(audio.description());
|
||||||
@@ -292,11 +283,9 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
|
|||||||
|
|
||||||
private ServerTab newTab() {
|
private ServerTab newTab() {
|
||||||
ServerTab tab = new ServerTab(this, settings, identities, audio);
|
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);
|
tabs.add(tab);
|
||||||
cardPanel.add(tab.component(), name);
|
tabPane.addTab(tab);
|
||||||
refreshTabBar();
|
refreshTabs();
|
||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,8 +301,8 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
|
|||||||
public void selectTab(ServerTab tab) {
|
public void selectTab(ServerTab tab) {
|
||||||
if (tab == null || !tabs.contains(tab)) return;
|
if (tab == null || !tabs.contains(tab)) return;
|
||||||
selected = tab;
|
selected = tab;
|
||||||
cards.show(cardPanel, tab.component().getName());
|
tabPane.setSelected(tab);
|
||||||
refreshTabBar();
|
refreshTabs();
|
||||||
updateToolbar();
|
updateToolbar();
|
||||||
updateStatusLabel();
|
updateStatusLabel();
|
||||||
}
|
}
|
||||||
@@ -325,13 +314,13 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
|
|||||||
if (tabs.size() == 1) return; // always keep one view around
|
if (tabs.size() == 1) return; // always keep one view around
|
||||||
|
|
||||||
tabs.remove(tab);
|
tabs.remove(tab);
|
||||||
cardPanel.remove(tab.component());
|
tabPane.removeTab(tab);
|
||||||
if (micTab == tab) micTab = null;
|
if (micTab == tab) micTab = null;
|
||||||
if (selected == tab) {
|
if (selected == tab) {
|
||||||
selected = null;
|
selected = null;
|
||||||
selectTab(tabs.get(0));
|
selectTab(tabs.get(0));
|
||||||
} else {
|
} else {
|
||||||
refreshTabBar();
|
selectTab(selected); // the pane rebuilt itself; restore the selection
|
||||||
}
|
}
|
||||||
assignMicrophoneIfFree();
|
assignMicrophoneIfFree();
|
||||||
}
|
}
|
||||||
@@ -341,7 +330,7 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
|
|||||||
if (micTab == tab) return;
|
if (micTab == tab) return;
|
||||||
ServerTab previous = micTab;
|
ServerTab previous = micTab;
|
||||||
micTab = tab;
|
micTab = tab;
|
||||||
refreshTabBar();
|
refreshTabs();
|
||||||
// Closing and reopening the capture line can block briefly; keep it off the EDT.
|
// Closing and reopening the capture line can block briefly; keep it off the EDT.
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
if (previous != null) previous.setMicrophoneActive(false);
|
if (previous != null) previous.setMicrophoneActive(false);
|
||||||
@@ -359,13 +348,12 @@ public final class MainFrame extends JFrame implements ServerTabBar.Listener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshTabBar();
|
refreshTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshTabBar() {
|
/** Repaints the tab labels (title, microphone marker). */
|
||||||
tabBar.rebuild(tabs, selected, micTab);
|
private void refreshTabs() {
|
||||||
// The bar appears/disappears with the second connection.
|
tabPane.refresh(micTab);
|
||||||
getContentPane().revalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- callbacks from ServerTab ----
|
// ---- 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. */
|
/** A tab's title, status or connection state changed. */
|
||||||
void tabUpdated(ServerTab tab) {
|
void tabUpdated(ServerTab tab) {
|
||||||
if (!tabs.contains(tab)) return;
|
if (!tabs.contains(tab)) return;
|
||||||
refreshTabBar();
|
refreshTabs();
|
||||||
if (tab == selected) {
|
if (tab == selected) {
|
||||||
updateToolbar();
|
updateToolbar();
|
||||||
updateStatusLabel();
|
updateStatusLabel();
|
||||||
|
|||||||
@@ -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<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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerTab> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user