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>
This commit is contained in:
2026-09-23 22:33:27 +00:00
parent 1fddae3f0b
commit 9d996f48a2
57 changed files with 643 additions and 311 deletions

View File

@@ -1,12 +1,13 @@
package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Settings;
import com.ts3client.sound.NotificationSettings;
import com.ts3client.sound.SoundEvent;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPack;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
@@ -79,7 +80,7 @@ final class NotificationsPanel extends JPanel {
private final EventTableModel tableModel = new EventTableModel();
NotificationsPanel(Settings settings, SoundNotifier sounds) {
super(new BorderLayout(0, 8));
super(new BorderLayout(0, UIScale.scale(8)));
this.settings = settings;
this.sounds = sounds;
this.working = settings.notifications.copy();
@@ -91,7 +92,7 @@ final class NotificationsPanel extends JPanel {
buildRows();
this.table = buildTable();
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
setBorder(new ScaledEmptyBorder(12, 12, 12, 12));
add(buildHeader(), BorderLayout.NORTH);
add(new JScrollPane(table), BorderLayout.CENTER);
add(buildFooter(), BorderLayout.SOUTH);
@@ -122,7 +123,7 @@ final class NotificationsPanel extends JPanel {
private JComponent buildHeader() {
JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 2, 2, 2);
c.insets = UIScale.scale(new Insets(2, 2, 2, 2));
c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL;
@@ -132,7 +133,7 @@ final class NotificationsPanel extends JPanel {
test.setToolTipText("Play this pack's test sound");
test.addActionListener(e -> sounds.preview(SoundEvent.SPECIAL_SOUND_TEST));
JPanel packRow = new JPanel(new BorderLayout(6, 0));
JPanel packRow = new JPanel(new BorderLayout(UIScale.scale(6), 0));
packRow.add(packCombo, BorderLayout.CENTER);
packRow.add(test, BorderLayout.EAST);
@@ -146,7 +147,7 @@ final class NotificationsPanel extends JPanel {
volume.setToolTipText("Volume of the notification sounds");
addRow(p, c, row++, new JLabel("Sound volume:"), volume);
JPanel dirRow = new JPanel(new BorderLayout(6, 0));
JPanel dirRow = new JPanel(new BorderLayout(UIScale.scale(6), 0));
JButton browse = new JButton("Browse…");
browse.addActionListener(e -> browseForPackFolder());
dirRow.add(packDirField, BorderLayout.CENTER);
@@ -157,10 +158,10 @@ final class NotificationsPanel extends JPanel {
}
private JComponent buildFooter() {
JPanel p = new JPanel(new BorderLayout(6, 0));
JPanel p = new JPanel(new BorderLayout(UIScale.scale(6), 0));
JLabel hint = new JLabel("<html><b>Bold</b> actions are important: they are still played "
+ "while your speakers are muted.</html>");
hint.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0));
hint.setBorder(new ScaledEmptyBorder(4, 0, 0, 0));
JPanel buttons = new JPanel();
JButton important = new JButton("Toggle important");
@@ -187,12 +188,12 @@ final class NotificationsPanel extends JPanel {
t.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
t.setShowGrid(false);
t.setTableHeader(null);
t.setRowHeight(Math.max(t.getRowHeight(), t.getFontMetrics(t.getFont()).getHeight() + 6));
t.getColumnModel().getColumn(0).setMaxWidth(TOGGLE_COLUMN_WIDTH);
t.getColumnModel().getColumn(0).setMinWidth(TOGGLE_COLUMN_WIDTH);
t.setRowHeight(Math.max(t.getRowHeight(), t.getFontMetrics(t.getFont()).getHeight() + UIScale.scale(6)));
t.getColumnModel().getColumn(0).setMaxWidth(UIScale.scale(TOGGLE_COLUMN_WIDTH));
t.getColumnModel().getColumn(0).setMinWidth(UIScale.scale(TOGGLE_COLUMN_WIDTH));
t.getColumnModel().getColumn(0).setCellRenderer(new ToggleRenderer(t.getDefaultRenderer(Boolean.class)));
t.getColumnModel().getColumn(1).setCellRenderer(new ActionRenderer());
t.setPreferredScrollableViewportSize(new Dimension(360, 260));
t.setPreferredScrollableViewportSize(UIScale.scale(new Dimension(360, 260)));
t.addMouseListener(new MouseAdapter() {
@Override
@@ -381,11 +382,11 @@ final class NotificationsPanel extends JPanel {
if (row.isHeading()) {
setFont(base.deriveFont(Font.BOLD));
setForeground(selected ? table.getSelectionForeground() : table.getForeground());
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
setBorder(new ScaledEmptyBorder(2, 2, 2, 2));
setToolTipText(null);
return this;
}
setBorder(BorderFactory.createEmptyBorder(2, 18, 2, 2));
setBorder(new ScaledEmptyBorder(2, 18, 2, 2));
setFont(base.deriveFont(working.isImportant(row.event) ? Font.BOLD : Font.PLAIN));
boolean silent = !sounds.hasSound(row.event);
setForeground(selected ? table.getSelectionForeground()