Files
ts3j/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java
ericek111 676e95023e Draw the interface with TeamSpeak icon packs
Icon packs are read in TeamSpeak's own format — a zip (or unpacked folder)
of SVG/PNG art plus a settings.ini mapping icon keys to files — so the packs
of an installed client and the ones from its add-on site work unchanged.
Legacy packs without that mapping (default.zip) are resolved by their file
naming convention instead, picking the resolution closest to the drawn size.

The pack draws the tree, toolbar, menus, context menus, file browser and the
default group icons; a pack's FALLBACK option decides whether what it lacks
comes from default.zip, and anything still missing falls back to the icons
the client draws itself. Options → Design picks the pack and shows a viewer
of everything in it.

Vector art is rasterised with JSVG, and the pack search roots are shared
with the sound packs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:46:39 +00:00

161 lines
5.5 KiB
Java

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.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
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));
});
tabbed.addMouseWheelListener(this::onWheel);
}
/**
* Scrolling over the tab strip steps through the connections. The content
* area keeps its own scrolling, so only wheel events on the strip itself
* (and on the tab labels, which forward theirs) change tabs.
*/
private void onWheel(MouseWheelEvent e) {
if (!tabbedMode) return;
Component content = tabbed.getSelectedComponent();
if (content != null && content.getBounds().contains(e.getPoint())) return;
int steps = e.getWheelRotation();
if (steps == 0) return;
int target = Math.max(0, Math.min(tabs.size() - 1, tabbed.getSelectedIndex() + steps));
if (target != tabbed.getSelectedIndex()) listener.selectTab(tabs.get(target));
e.consume();
}
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.micActiveSmall() : 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;
}
}