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>
161 lines
5.5 KiB
Java
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;
|
|
}
|
|
}
|