Float the info panel's "Show in chat tab" link in its corner

As a real label over the text, it stays reachable however long a description
runs, instead of trailing off the end of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 07:46:42 +00:00
parent ffe98be649
commit 5a00015824

View File

@@ -9,7 +9,16 @@ import com.ts3client.text.BBCode;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.JEditorPane; import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
@@ -18,7 +27,7 @@ import java.util.List;
* the TeamSpeak 3 info box: channel topic/description, or a client's server and * the TeamSpeak 3 info box: channel topic/description, or a client's server and
* channel groups, platform and version. * channel groups, platform and version.
*/ */
public final class InfoPanel extends JScrollPane { public final class InfoPanel extends JPanel {
/** Moves the whole info panel's content out to a persistent chat tab. */ /** Moves the whole info panel's content out to a persistent chat tab. */
public interface DescriptionHandler { public interface DescriptionHandler {
@@ -32,9 +41,13 @@ public final class InfoPanel extends JScrollPane {
void setPanelHidden(boolean hidden); void setPanelHidden(boolean hidden);
} }
private static final String TOGGLE_HREF = "app:toggle-info-tab"; /** Gap between the floating link and the panel's edges. */
private static final int TOGGLE_MARGIN = 6;
private final JEditorPane pane = HtmlStyles.pane("font-family:sans-serif; font-size:11px;"); private final JEditorPane pane = HtmlStyles.pane("font-family:sans-serif; font-size:11px;");
private final JScrollPane scroll = new JScrollPane(pane);
/** Floats over the bottom-right corner of the details, out of the text's way. */
private final JLabel toggle = new JLabel("Show in chat tab");
private DescriptionHandler descriptionHandler; private DescriptionHandler descriptionHandler;
private ChannelNode shownChannel; private ChannelNode shownChannel;
private ClientEntry shownClient; private ClientEntry shownClient;
@@ -43,26 +56,57 @@ public final class InfoPanel extends JScrollPane {
private boolean inChatTab; private boolean inChatTab;
public InfoPanel() { public InfoPanel() {
super(new BorderLayout());
pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
pane.addHyperlinkListener(e -> { pane.addHyperlinkListener(e -> {
if (e.getEventType() != javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) return; if (e.getEventType() != javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) return;
if (TOGGLE_HREF.equals(e.getDescription())) { Links.open(e.getDescription(), this);
inChatTab = !inChatTab; });
if (!inChatTab && descriptionHandler != null) descriptionHandler.closeInfoTab(); HtmlStyles.fillViewport(pane);
toggle.setFont(Theme.uiSmall());
toggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
toggle.setToolTipText("Show these details in a chat tab instead");
toggle.setOpaque(true);
toggle.setBorder(BorderFactory.createEmptyBorder(1, 4, 1, 4));
toggle.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
inChatTab = true;
render(); render();
} else {
Links.open(e.getDescription(), this);
} }
}); });
setViewportView(pane);
HtmlStyles.fillViewport(pane); // The link floats on top of the text rather than following it, so it stays
// in the corner however long the description is.
JLayeredPane layers = new JLayeredPane() {
@Override
public void doLayout() {
scroll.setBounds(0, 0, getWidth(), getHeight());
Insets in = scroll.getInsets();
Dimension d = toggle.getPreferredSize();
toggle.setBounds(getWidth() - in.right - TOGGLE_MARGIN - d.width,
getHeight() - in.bottom - TOGGLE_MARGIN - d.height, d.width, d.height);
}
};
layers.add(scroll, JLayeredPane.DEFAULT_LAYER);
layers.add(toggle, JLayeredPane.PALETTE_LAYER);
add(layers, BorderLayout.CENTER);
applyTheme();
clear(); clear();
} }
@Override @Override
public void updateUI() { public void updateUI() {
super.updateUI(); super.updateUI();
setBorder(BorderFactory.createLineBorder(Theme.border())); // Runs from the superclass constructor too, before our own fields exist.
if (scroll != null) applyTheme();
}
private void applyTheme() {
scroll.setBorder(BorderFactory.createLineBorder(Theme.border()));
toggle.setBackground(pane.getBackground());
toggle.setForeground(Theme.link());
} }
public void clear() { public void clear() {
@@ -100,10 +144,11 @@ public final class InfoPanel extends JScrollPane {
boolean selected = shownChannel != null || shownClient != null; boolean selected = shownChannel != null || shownClient != null;
boolean hide = inChatTab && selected; boolean hide = inChatTab && selected;
if (descriptionHandler != null) descriptionHandler.setPanelHidden(hide); if (descriptionHandler != null) descriptionHandler.setPanelHidden(hide);
toggle.setVisible(selected && !hide);
if (hide) { if (hide) {
descriptionHandler.showInfoTab(body(), this::onInfoTabClosed); descriptionHandler.showInfoTab(body(), this::onInfoTabClosed);
} else { } else {
setHtml(body() + (selected ? toggleLink(true) : "")); setHtml(body());
} }
} }
@@ -172,12 +217,6 @@ public final class InfoPanel extends JScrollPane {
return sb.toString(); return sb.toString();
} }
/** The link that toggles between showing details here and in a chat tab. */
private static String toggleLink(boolean toChat) {
return "<div style='margin-top:8px'><a href='" + TOGGLE_HREF + "' style='font-size:10px'>"
+ (toChat ? "Show in chat tab" : "Show here") + "</a></div>";
}
private void setHtml(String body) { private void setHtml(String body) {
pane.setText("<html><body>" + body + "</body></html>"); pane.setText("<html><body>" + body + "</body></html>");
pane.setCaretPosition(0); pane.setCaretPosition(0);