Files
ts3java/ts3-client/swing/src/main/java/com/ts3client/ui/LevelMeter.java
ericek111 9d996f48a2 Scale the interface fractionally on Linux
Java2D on X11 (and so under XWayland) truncates its scale to a whole
number, drawing 150 % at 100 %. On Linux, pin Java2D to 1 and hand the
full factor to FlatLaf, taken from XSETTINGS Xft/DPI, KWin's Xwayland
scale or GDK_SCALE, or chosen in Options → Design.

The client's own sizes now go through UIScale: borders, gaps, dialog and
row sizes, icons (rendered at the scaled size, so they stay crisp),
custom-painted components and font sizes in the HTML views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 22:33:27 +00:00

89 lines
3.0 KiB
Java

package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
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(UIScale.scale(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(UIScale.scale(new Dimension(60, 18)));
setMaximumSize(new Dimension(Integer.MAX_VALUE, UIScale.scale(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));
int arc = UIScale.scale(6);
g.fillRoundRect(0, 0, w - 1, h - 1, arc, arc);
int level = dbToX(levelDb, w - 2);
g.setColor(transmitting ? Theme.talking() : new Color(0x5A9BD4));
g.fillRoundRect(1, 1, Math.max(0, level), h - 3, arc - 1, arc - 1);
if (showThreshold) {
int tx = dbToX(thresholdDb, w - 2);
g.setColor(new Color(0xF0C419));
g.fillRect(tx, 1, UIScale.scale(2), h - 3);
}
}
}