The look-and-feel is FlatLaf now, chosen light or dark on the new Design tab and remembered in the settings. Theme keeps only the colours TeamSpeak gives meaning to (talking, away, muted, channel and server names) and takes its surfaces and fonts from the look-and-feel, so a switch recolours the whole window, chat logs included: the HTML panes share one themed stylesheet and re-parse their content when the theme changes. Also fixes the channel tree not scrolling: the tree listened for the mouse wheel to re-check its hover row, and AWT only forwards a wheel event to the enclosing scroll pane when the component under the pointer has no wheel listener of its own. The hover row now follows the viewport instead, which also covers scrollbar drags and keyboard scrolling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
2.8 KiB
Java
86 lines
2.8 KiB
Java
package com.ts3client.ui;
|
|
|
|
import javax.swing.JComponent;
|
|
import java.awt.Color;
|
|
import java.awt.Dimension;
|
|
import java.awt.Graphics;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.RenderingHints;
|
|
|
|
/**
|
|
* Horizontal audio level meter with an optional VAD threshold marker. The filled portion
|
|
* turns green once the level crosses the threshold, giving immediate visual feedback while
|
|
* tuning voice activation.
|
|
*
|
|
* <p>Scaled to {@link com.ts3client.audio.InputLevel}, matching the TS3 client's slider.
|
|
*/
|
|
public final class LevelMeter extends JComponent {
|
|
|
|
private static final double MIN_DB = com.ts3client.audio.InputLevel.MIN_DB;
|
|
private static final double MAX_DB = com.ts3client.audio.InputLevel.MAX_DB;
|
|
|
|
private volatile double levelDb = MIN_DB;
|
|
private volatile double thresholdDb = -40.0;
|
|
private volatile boolean showThreshold = true;
|
|
private volatile boolean transmitting;
|
|
|
|
public LevelMeter() {
|
|
setPreferredSize(new Dimension(240, 18));
|
|
// A bare JComponent reports no minimum of its own, so a layout tight on space
|
|
// collapses the bar to nothing. Keep the height and let only the width give.
|
|
setMinimumSize(new Dimension(60, 18));
|
|
setMaximumSize(new Dimension(Integer.MAX_VALUE, 18));
|
|
}
|
|
|
|
public void setLevel(double db) {
|
|
this.levelDb = db;
|
|
repaint();
|
|
}
|
|
|
|
public void setThreshold(double db) {
|
|
this.thresholdDb = db;
|
|
repaint();
|
|
}
|
|
|
|
public void setShowThreshold(boolean show) {
|
|
this.showThreshold = show;
|
|
repaint();
|
|
}
|
|
|
|
/**
|
|
* Whether the gate is currently open. This is the real transmit decision, which in the
|
|
* Automatic and Hybrid modes depends on the speech detector and the hangover as well as
|
|
* on the level, so the bar cannot infer it from the threshold alone.
|
|
*/
|
|
public void setTransmitting(boolean transmitting) {
|
|
this.transmitting = transmitting;
|
|
repaint();
|
|
}
|
|
|
|
private int dbToX(double db, int w) {
|
|
double clamped = Math.max(MIN_DB, Math.min(MAX_DB, db));
|
|
return (int) ((clamped - MIN_DB) / (MAX_DB - MIN_DB) * w);
|
|
}
|
|
|
|
@Override
|
|
protected void paintComponent(Graphics g0) {
|
|
Graphics2D g = (Graphics2D) g0;
|
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
int w = getWidth();
|
|
int h = getHeight();
|
|
|
|
g.setColor(new Color(0x2B2B2B));
|
|
g.fillRoundRect(0, 0, w - 1, h - 1, 6, 6);
|
|
|
|
int level = dbToX(levelDb, w - 2);
|
|
g.setColor(transmitting ? Theme.talking() : new Color(0x5A9BD4));
|
|
g.fillRoundRect(1, 1, Math.max(0, level), h - 3, 5, 5);
|
|
|
|
if (showThreshold) {
|
|
int tx = dbToX(thresholdDb, w - 2);
|
|
g.setColor(new Color(0xF0C419));
|
|
g.fillRect(tx, 1, 2, h - 3);
|
|
}
|
|
}
|
|
}
|