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

@@ -169,6 +169,18 @@ frontend supplies its own UI and audio backend while reusing `core` unchanged.
- **Pick one in Options → Design**, which also shows a viewer of everything the pack - **Pick one in Options → Design**, which also shows a viewer of everything the pack
contains; switching redraws the window right away. contains; switching redraws the window right away.
### Display scaling (Linux)
- Java2D on X11 (and so under XWayland) only scales by whole numbers, so 125 % or
150 % comes out at 100 %. On Linux the client pins Java2D to 1 and hands the full
factor to FlatLaf instead, scaling its own sizes and icons to match.
- The factor follows the desktop: XSETTINGS `Xft/DPI` (GNOME, Plasma), else KWin's
XWayland scale, else `GDK_SCALE`. For crisp text on Wayland, let X11 apps scale
themselves (Plasma: *Display Configuration → Legacy Applications → Apply scaling
themselves*; GNOME: the `xwayland-native-scaling` mutter feature), otherwise the
compositor upscales the window.
- **Options → Design → Interface scale** overrides it (after a restart);
`-Dflatlaf.uiScale=` or `-Dsun.java2d.uiScale=` on the command line bypasses all this.
## Requirements ## Requirements
- Java 26+ (developed/tested on Temurin 26) - Java 26+ (developed/tested on Temurin 26)
- The native Opus library on the system: - The native Opus library on the system:
@@ -269,6 +281,7 @@ swing/ com.ts3client
├── HotkeyActions carries a fired hotkey out on the client ├── HotkeyActions carries a fired hotkey out on the client
├── NotificationsPanel sound pack + per-action sound/important configuration ├── NotificationsPanel sound pack + per-action sound/important configuration
├── IconPackPanel icon pack chooser + icon viewer (Options → Design) ├── IconPackPanel icon pack chooser + icon viewer (Options → Design)
├── DisplayScale interface scale on Linux (fractional, via FlatLaf)
├── ConnectDialog connect form ├── ConnectDialog connect form
├── BookmarksDialog manage saved servers ├── BookmarksDialog manage saved servers
├── IdentitiesDialog manage identities (new/import/export/improve) ├── IdentitiesDialog manage identities (new/import/export/improve)

View File

@@ -154,6 +154,8 @@ public final class Settings {
/** Look-and-feel variant; see {@link Appearance}. */ /** Look-and-feel variant; see {@link Appearance}. */
public Appearance appearance = Appearance.LIGHT; public Appearance appearance = Appearance.LIGHT;
/** Interface scale factor, 1.0 being 100&nbsp;%; 0 follows the desktop. Read at startup. */
public double uiScale = 0;
// ---- window chrome ---- // ---- window chrome ----
/** Whether the status bar at the bottom of the window is shown. */ /** Whether the status bar at the bottom of the window is shown. */
@@ -263,6 +265,7 @@ public final class Settings {
iconPack = props.getProperty("iconPack", iconPack); iconPack = props.getProperty("iconPack", iconPack);
iconPackDir = props.getProperty("iconPackDir", iconPackDir); iconPackDir = props.getProperty("iconPackDir", iconPackDir);
appearance = parseAppearance(props.getProperty("appearance"), appearance); appearance = parseAppearance(props.getProperty("appearance"), appearance);
uiScale = parseD(props.getProperty("uiScale"), uiScale);
showStatusBar = parseB(props.getProperty("showStatusBar"), showStatusBar); showStatusBar = parseB(props.getProperty("showStatusBar"), showStatusBar);
showMasterVolumeSlider = parseB(props.getProperty("showMasterVolumeSlider"), showMasterVolumeSlider); showMasterVolumeSlider = parseB(props.getProperty("showMasterVolumeSlider"), showMasterVolumeSlider);
infoSplitRatio = parseD(props.getProperty("infoSplitRatio"), infoSplitRatio); infoSplitRatio = parseD(props.getProperty("infoSplitRatio"), infoSplitRatio);
@@ -311,6 +314,7 @@ public final class Settings {
props.setProperty("iconPack", iconPack); props.setProperty("iconPack", iconPack);
props.setProperty("iconPackDir", iconPackDir); props.setProperty("iconPackDir", iconPackDir);
props.setProperty("appearance", appearance.name()); props.setProperty("appearance", appearance.name());
props.setProperty("uiScale", Double.toString(uiScale));
props.setProperty("showStatusBar", Boolean.toString(showStatusBar)); props.setProperty("showStatusBar", Boolean.toString(showStatusBar));
props.setProperty("showMasterVolumeSlider", Boolean.toString(showMasterVolumeSlider)); props.setProperty("showMasterVolumeSlider", Boolean.toString(showMasterVolumeSlider));
props.setProperty("infoSplitRatio", Double.toString(infoSplitRatio)); props.setProperty("infoSplitRatio", Double.toString(infoSplitRatio));

View File

@@ -1,6 +1,7 @@
package com.ts3client; package com.ts3client;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import com.ts3client.ui.DisplayScale;
import com.ts3client.ui.IconTheme; import com.ts3client.ui.IconTheme;
import com.ts3client.ui.LookAndFeelManager; import com.ts3client.ui.LookAndFeelManager;
import com.ts3client.ui.MainFrame; import com.ts3client.ui.MainFrame;
@@ -21,6 +22,7 @@ public final class Main {
} }
final Settings settings = Settings.load(); final Settings settings = Settings.load();
DisplayScale.configure(settings.uiScale);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
LookAndFeelManager.install(settings.appearance); LookAndFeelManager.install(settings.appearance);

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ClientEntry; import com.ts3client.net.ClientEntry;
import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.TeamspeakConnection;
import com.ts3client.net.avatar.Avatar; import com.ts3client.net.avatar.Avatar;
@@ -39,16 +41,16 @@ final class AvatarDialog extends JDialog {
JPanel picture = new JPanel(new BorderLayout()); JPanel picture = new JPanel(new BorderLayout());
picture.setBorder(BorderFactory.createCompoundBorder( picture.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(8, 8, 4, 8), new ScaledEmptyBorder(8, 8, 4, 8),
BorderFactory.createLineBorder(Theme.border()))); BorderFactory.createLineBorder(Theme.border())));
picture.add(preview, BorderLayout.CENTER); picture.add(preview, BorderLayout.CENTER);
JLabel hint = new JLabel("<html>Images up to " + (conn.maxAvatarBytes() / 1024) JLabel hint = new JLabel("<html>Images up to " + (conn.maxAvatarBytes() / 1024)
+ " KiB may be uploaded to this server. Animated GIFs stay animated.</html>"); + " KiB may be uploaded to this server. Animated GIFs stay animated.</html>");
hint.setFont(Theme.uiSmall()); hint.setFont(Theme.uiSmall());
hint.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8)); hint.setBorder(new ScaledEmptyBorder(0, 8, 0, 8));
status.setFont(Theme.uiSmall()); status.setFont(Theme.uiSmall());
status.setBorder(BorderFactory.createEmptyBorder(2, 8, 0, 8)); status.setBorder(new ScaledEmptyBorder(2, 8, 0, 8));
JPanel text = new JPanel(); JPanel text = new JPanel();
text.setLayout(new BoxLayout(text, BoxLayout.Y_AXIS)); text.setLayout(new BoxLayout(text, BoxLayout.Y_AXIS));
@@ -57,13 +59,13 @@ final class AvatarDialog extends JDialog {
JPanel buttons = new JPanel(); JPanel buttons = new JPanel();
buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
buttons.setBorder(BorderFactory.createEmptyBorder(6, 8, 8, 8)); buttons.setBorder(new ScaledEmptyBorder(6, 8, 8, 8));
choose.addActionListener(e -> chooseFile()); choose.addActionListener(e -> chooseFile());
remove.addActionListener(e -> removeAvatar()); remove.addActionListener(e -> removeAvatar());
JButton close = new JButton("Close"); JButton close = new JButton("Close");
close.addActionListener(e -> dispose()); close.addActionListener(e -> dispose());
buttons.add(choose); buttons.add(choose);
buttons.add(Box.createHorizontalStrut(4)); buttons.add(Box.createHorizontalStrut(UIScale.scale(4)));
buttons.add(remove); buttons.add(remove);
buttons.add(Box.createHorizontalGlue()); buttons.add(Box.createHorizontalGlue());
buttons.add(close); buttons.add(close);

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.avatar.Avatar; import com.ts3client.net.avatar.Avatar;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@@ -12,7 +13,8 @@ import java.util.Iterator;
/** /**
* Renders an avatar as an HTML {@code <img>} for the info views. Swing's HTML renderer * Renders an avatar as an HTML {@code <img>} for the info views. Swing's HTML renderer
* loads images by URL and plays animated GIFs on its own, so the tag simply points at * loads images by URL and plays animated GIFs on its own, so the tag simply points at
* the repository's on-disk copy, scaled to fit a box while keeping its aspect ratio. * the repository's on-disk copy, drawn at the interface scale and fitted to a box while
* keeping its aspect ratio.
*/ */
final class AvatarHtml { final class AvatarHtml {
@@ -25,7 +27,8 @@ final class AvatarHtml {
/** @return the tag, or an empty string if the avatar cannot be shown */ /** @return the tag, or an empty string if the avatar cannot be shown */
static String imgTag(Avatar avatar, int maxSize) { static String imgTag(Avatar avatar, int maxSize) {
if (avatar == null || avatar.file() == null) return ""; if (avatar == null || avatar.file() == null) return "";
Dimension size = fit(dimensions(avatar.data()), maxSize); Dimension natural = dimensions(avatar.data());
Dimension size = fit(natural == null ? null : UIScale.scale(natural), maxSize);
String dims = size == null ? "" : " width='" + size.width + "' height='" + size.height + "'"; String dims = size == null ? "" : " width='" + size.width + "' height='" + size.height + "'";
return "<img src='" + avatar.file().toURI() + "'" + dims + ">"; return "<img src='" + avatar.file().toURI() + "'" + dims + ">";
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.JComponent; import javax.swing.JComponent;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.Graphics; import java.awt.Graphics;
@@ -22,7 +24,7 @@ final class AvatarView extends JComponent {
AvatarView(int boxSize) { AvatarView(int boxSize) {
this.boxSize = boxSize; this.boxSize = boxSize;
setPreferredSize(new Dimension(boxSize, boxSize)); setPreferredSize(UIScale.scale(new Dimension(boxSize, boxSize)));
setMinimumSize(getPreferredSize()); setMinimumSize(getPreferredSize());
} }
@@ -47,7 +49,7 @@ final class AvatarView extends JComponent {
imageHeight = image.getHeight(this); imageHeight = image.getHeight(this);
if (imageWidth < 0 || imageHeight < 0) return; if (imageWidth < 0 || imageHeight < 0) return;
} }
double scale = Math.min(1.0, Math.min((double) getWidth() / imageWidth, (double) getHeight() / imageHeight)); double scale = Math.min(UIScale.getUserScaleFactor(), Math.min((double) getWidth() / imageWidth, (double) getHeight() / imageHeight));
int w = Math.max(1, (int) Math.round(imageWidth * scale)); int w = Math.max(1, (int) Math.round(imageWidth * scale));
int h = Math.max(1, (int) Math.round(imageHeight * scale)); int h = Math.max(1, (int) Math.round(imageHeight * scale));
Graphics2D g2 = (Graphics2D) g.create(); Graphics2D g2 = (Graphics2D) g.create();

View File

@@ -1,8 +1,9 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.AwayMessages; import com.ts3client.config.AwayMessages;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JButton; import javax.swing.JButton;
@@ -39,13 +40,13 @@ public final class AwayMessagesDialog extends JDialog {
table.setTableHeader(null); table.setTableHeader(null);
table.setShowGrid(false); table.setShowGrid(false);
table.setFillsViewportHeight(true); table.setFillsViewportHeight(true);
table.setRowHeight(Math.max(20, table.getRowHeight())); table.setRowHeight(Math.max(UIScale.scale(20), table.getRowHeight()));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Single click selects, double click starts editing — like a renameable list. // Single click selects, double click starts editing — like a renameable list.
table.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); table.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
JScrollPane scroll = new JScrollPane(table); JScrollPane scroll = new JScrollPane(table);
scroll.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); scroll.setBorder(new ScaledEmptyBorder(8, 8, 4, 8));
getContentPane().setLayout(new BorderLayout()); getContentPane().setLayout(new BorderLayout());
getContentPane().add(scroll, BorderLayout.CENTER); getContentPane().add(scroll, BorderLayout.CENTER);
@@ -53,14 +54,14 @@ public final class AwayMessagesDialog extends JDialog {
Dialogs.closeOnEscape(this, this::closeDialog); Dialogs.closeOnEscape(this, this::closeDialog);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(new Dimension(360, 300)); setSize(UIScale.scale(new Dimension(360, 300)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }
private JPanel buildButtons() { private JPanel buildButtons() {
JPanel buttons = new JPanel(); JPanel buttons = new JPanel();
buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8)); buttons.setBorder(new ScaledEmptyBorder(0, 8, 8, 8));
addButton(buttons, "Add", this::addEntry); addButton(buttons, "Add", this::addEntry);
addButton(buttons, "Remove", this::removeSelected); addButton(buttons, "Remove", this::removeSelected);
buttons.add(Box.createHorizontalGlue()); buttons.add(Box.createHorizontalGlue());
@@ -72,7 +73,7 @@ public final class AwayMessagesDialog extends JDialog {
JButton b = new JButton(text); JButton b = new JButton(text);
b.addActionListener(e -> action.run()); b.addActionListener(e -> action.run());
panel.add(b); panel.add(b);
panel.add(Box.createHorizontalStrut(4)); panel.add(Box.createHorizontalStrut(UIScale.scale(4)));
} }
private void addEntry() { private void addEntry() {

View File

@@ -1,11 +1,12 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.BanReasonPresets; import com.ts3client.config.BanReasonPresets;
import com.ts3client.net.ban.BanEntry; import com.ts3client.net.ban.BanEntry;
import com.ts3client.net.ban.BanPattern; import com.ts3client.net.ban.BanPattern;
import com.ts3client.net.ban.BanRule; import com.ts3client.net.ban.BanRule;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JButton; import javax.swing.JButton;
@@ -75,24 +76,24 @@ final class BanAddDialog extends JDialog {
JPanel form = new JPanel(); JPanel form = new JPanel();
form.setLayout(new BoxLayout(form, BoxLayout.Y_AXIS)); form.setLayout(new BoxLayout(form, BoxLayout.Y_AXIS));
form.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12)); form.setBorder(new ScaledEmptyBorder(12, 12, 8, 12));
form.add(ReasonDialog.label("IP:")); form.add(ReasonDialog.label("IP:"));
form.add(ipField); form.add(ipField);
form.add(Box.createVerticalStrut(8)); form.add(Box.createVerticalStrut(UIScale.scale(8)));
form.add(ReasonDialog.label("Name:")); form.add(ReasonDialog.label("Name:"));
form.add(nameField); form.add(nameField);
form.add(Box.createVerticalStrut(8)); form.add(Box.createVerticalStrut(UIScale.scale(8)));
form.add(patternRow()); form.add(patternRow());
form.add(Box.createVerticalStrut(8)); form.add(Box.createVerticalStrut(UIScale.scale(8)));
form.add(ReasonDialog.label("Unique ID:")); form.add(ReasonDialog.label("Unique ID:"));
form.add(uidField); form.add(uidField);
form.add(Box.createVerticalStrut(8)); form.add(Box.createVerticalStrut(UIScale.scale(8)));
form.add(ReasonDialog.label("myTeamSpeak ID:")); form.add(ReasonDialog.label("myTeamSpeak ID:"));
form.add(mytsidField); form.add(mytsidField);
form.add(Box.createVerticalStrut(12)); form.add(Box.createVerticalStrut(UIScale.scale(12)));
form.add(reasonRow()); form.add(reasonRow());
form.add(reasonField); form.add(reasonField);
form.add(Box.createVerticalStrut(8)); form.add(Box.createVerticalStrut(UIScale.scale(8)));
form.add(ReasonDialog.label("Duration:")); form.add(ReasonDialog.label("Duration:"));
form.add(duration); form.add(duration);
@@ -119,7 +120,7 @@ final class BanAddDialog extends JDialog {
row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS)); row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
row.setAlignmentX(Component.LEFT_ALIGNMENT); row.setAlignmentX(Component.LEFT_ALIGNMENT);
row.add(new JLabel("Interpret IP/Name as:")); row.add(new JLabel("Interpret IP/Name as:"));
row.add(Box.createHorizontalStrut(8)); row.add(Box.createHorizontalStrut(UIScale.scale(8)));
row.add(patternBox); row.add(patternBox);
row.add(Box.createHorizontalGlue()); row.add(Box.createHorizontalGlue());
ReasonDialog.fixHeight(row); ReasonDialog.fixHeight(row);
@@ -137,13 +138,13 @@ final class BanAddDialog extends JDialog {
JPanel row = new JPanel(); JPanel row = new JPanel();
row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS)); row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
row.setAlignmentX(Component.LEFT_ALIGNMENT); row.setAlignmentX(Component.LEFT_ALIGNMENT);
row.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0)); row.setBorder(new ScaledEmptyBorder(0, 0, 4, 0));
row.add(new JLabel("Reason:")); row.add(new JLabel("Reason:"));
row.add(Box.createHorizontalGlue()); row.add(Box.createHorizontalGlue());
row.add(new JLabel("Preset:")); row.add(new JLabel("Preset:"));
row.add(Box.createHorizontalStrut(6)); row.add(Box.createHorizontalStrut(UIScale.scale(6)));
row.add(presetBox); row.add(presetBox);
row.add(Box.createHorizontalStrut(6)); row.add(Box.createHorizontalStrut(UIScale.scale(6)));
row.add(save); row.add(save);
ReasonDialog.fixHeight(row); ReasonDialog.fixHeight(row);
return row; return row;

View File

@@ -1,8 +1,9 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ban.BanRule; import com.ts3client.net.ban.BanRule;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JButton; import javax.swing.JButton;
@@ -32,12 +33,12 @@ final class BanDialog extends JDialog {
JPanel form = new JPanel(); JPanel form = new JPanel();
form.setLayout(new BoxLayout(form, BoxLayout.Y_AXIS)); form.setLayout(new BoxLayout(form, BoxLayout.Y_AXIS));
form.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12)); form.setBorder(new ScaledEmptyBorder(12, 12, 8, 12));
form.add(ReasonDialog.label("Ban " + nickname + " from the server.")); form.add(ReasonDialog.label("Ban " + nickname + " from the server."));
form.add(Box.createVerticalStrut(8)); form.add(Box.createVerticalStrut(UIScale.scale(8)));
form.add(ReasonDialog.label("Reason:")); form.add(ReasonDialog.label("Reason:"));
form.add(reasonField); form.add(reasonField);
form.add(Box.createVerticalStrut(8)); form.add(Box.createVerticalStrut(UIScale.scale(8)));
form.add(ReasonDialog.label("Duration:")); form.add(ReasonDialog.label("Duration:"));
form.add(duration); form.add(duration);

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JButton; import javax.swing.JButton;
@@ -42,9 +44,9 @@ final class BanDurationPicker extends JPanel {
max.addActionListener(e -> setSeconds(maxSeconds)); max.addActionListener(e -> setSeconds(maxSeconds));
add(amount); add(amount);
add(Box.createHorizontalStrut(4)); add(Box.createHorizontalStrut(UIScale.scale(4)));
add(unit); add(unit);
add(Box.createHorizontalStrut(4)); add(Box.createHorizontalStrut(UIScale.scale(4)));
add(max); add(max);
add(Box.createHorizontalGlue()); add(Box.createHorizontalGlue());
ReasonDialog.fixHeight(this); ReasonDialog.fixHeight(this);

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.BanReasonPresets; import com.ts3client.config.BanReasonPresets;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import com.ts3client.net.ServerModel; import com.ts3client.net.ServerModel;
@@ -8,7 +10,6 @@ import com.ts3client.net.ban.BanEntry;
import com.ts3client.net.ban.BanRule; import com.ts3client.net.ban.BanRule;
import javax.swing.AbstractAction; import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JButton; import javax.swing.JButton;
@@ -122,9 +123,9 @@ final class BanListDialog extends JDialog {
this.settings = settings; this.settings = settings;
this.presets = presets; this.presets = presets;
JPanel content = new JPanel(new BorderLayout(0, 6)); JPanel content = new JPanel(new BorderLayout(0, UIScale.scale(6)));
content.setBackground(Theme.windowBg()); content.setBackground(Theme.windowBg());
content.setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); content.setBorder(new ScaledEmptyBorder(10, 12, 10, 12));
content.add(buildToolbar(), BorderLayout.NORTH); content.add(buildToolbar(), BorderLayout.NORTH);
content.add(buildTable(), BorderLayout.CENTER); content.add(buildTable(), BorderLayout.CENTER);
content.add(buildBottom(), BorderLayout.SOUTH); content.add(buildBottom(), BorderLayout.SOUTH);
@@ -133,7 +134,7 @@ final class BanListDialog extends JDialog {
serverNameChanged(); serverNameChanged();
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setPreferredSize(new Dimension(920, 460)); setPreferredSize(UIScale.scale(new Dimension(920, 460)));
pack(); pack();
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
reload(); reload();
@@ -172,18 +173,18 @@ final class BanListDialog extends JDialog {
searchField.setMaximumSize(new Dimension(Integer.MAX_VALUE, searchField.getPreferredSize().height)); searchField.setMaximumSize(new Dimension(Integer.MAX_VALUE, searchField.getPreferredSize().height));
bar.add(addButton); bar.add(addButton);
bar.add(Box.createHorizontalStrut(6)); bar.add(Box.createHorizontalStrut(UIScale.scale(6)));
bar.add(removeButton); bar.add(removeButton);
bar.add(Box.createHorizontalStrut(6)); bar.add(Box.createHorizontalStrut(UIScale.scale(6)));
bar.add(editButton); bar.add(editButton);
bar.add(Box.createHorizontalStrut(16)); bar.add(Box.createHorizontalStrut(UIScale.scale(16)));
bar.add(searchField); bar.add(searchField);
return bar; return bar;
} }
private JScrollPane buildTable() { private JScrollPane buildTable() {
table.setFont(Theme.uiFont()); table.setFont(Theme.uiFont());
table.setRowHeight(20); table.setRowHeight(UIScale.scale(20));
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
table.setFillsViewportHeight(true); table.setFillsViewportHeight(true);
table.setAutoCreateColumnsFromModel(false); table.setAutoCreateColumnsFromModel(false);
@@ -248,9 +249,9 @@ final class BanListDialog extends JDialog {
options.setLayout(new BoxLayout(options, BoxLayout.X_AXIS)); options.setLayout(new BoxLayout(options, BoxLayout.X_AXIS));
options.setOpaque(false); options.setOpaque(false);
options.add(reload); options.add(reload);
options.add(Box.createHorizontalStrut(12)); options.add(Box.createHorizontalStrut(UIScale.scale(12)));
options.add(onlyOwn); options.add(onlyOwn);
options.add(Box.createHorizontalStrut(12)); options.add(Box.createHorizontalStrut(UIScale.scale(12)));
options.add(highlightOwn); options.add(highlightOwn);
options.add(Box.createHorizontalGlue()); options.add(Box.createHorizontalGlue());
@@ -260,7 +261,7 @@ final class BanListDialog extends JDialog {
closeRow.setOpaque(false); closeRow.setOpaque(false);
closeRow.add(close, BorderLayout.EAST); closeRow.add(close, BorderLayout.EAST);
JPanel bottom = new JPanel(new BorderLayout(0, 8)); JPanel bottom = new JPanel(new BorderLayout(0, UIScale.scale(8)));
bottom.setOpaque(false); bottom.setOpaque(false);
bottom.add(status, BorderLayout.NORTH); bottom.add(status, BorderLayout.NORTH);
bottom.add(options, BorderLayout.CENTER); bottom.add(options, BorderLayout.CENTER);
@@ -659,7 +660,7 @@ final class BanListDialog extends JDialog {
g2.setColor(Theme.chatEvent()); g2.setColor(Theme.chatEvent());
g2.setFont(getFont().deriveFont(Font.ITALIC)); g2.setFont(getFont().deriveFont(Font.ITALIC));
int y = (getHeight() + g2.getFontMetrics().getAscent() - g2.getFontMetrics().getDescent()) / 2; int y = (getHeight() + g2.getFontMetrics().getAscent() - g2.getFontMetrics().getDescent()) / 2;
g2.drawString("Search", getInsets().left + 2, y); g2.drawString("Search", getInsets().left + UIScale.scale(2), y);
g2.dispose(); g2.dispose();
} }
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Bookmark; import com.ts3client.config.Bookmark;
import com.ts3client.config.Bookmarks; import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityStore; import com.ts3client.config.IdentityStore;
@@ -7,7 +9,6 @@ import com.ts3client.config.Settings;
import com.ts3client.teamspeak.TeamSpeakImporter; import com.ts3client.teamspeak.TeamSpeakImporter;
import com.ts3client.teamspeak.TeamSpeakSettingsDb; import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.DefaultListModel; import javax.swing.DefaultListModel;
@@ -81,10 +82,10 @@ public final class BookmarksDialog extends JDialog {
details = buildDetails(); details = buildDetails();
JScrollPane listScroll = new JScrollPane(list); JScrollPane listScroll = new JScrollPane(list);
listScroll.setPreferredSize(new Dimension(180, 320)); listScroll.setPreferredSize(UIScale.scale(new Dimension(180, 320)));
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, listScroll, details); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, listScroll, details);
split.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); split.setBorder(new ScaledEmptyBorder(8, 8, 4, 8));
split.setResizeWeight(0.3); split.setResizeWeight(0.3);
split.setContinuousLayout(true); split.setContinuousLayout(true);
@@ -105,7 +106,7 @@ public final class BookmarksDialog extends JDialog {
persist(); persist();
} }
}); });
setSize(new Dimension(640, 400)); setSize(UIScale.scale(new Dimension(640, 400)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }
@@ -113,7 +114,7 @@ public final class BookmarksDialog extends JDialog {
private JPanel buildDetails() { private JPanel buildDetails() {
JPanel panel = new JPanel(new GridBagLayout()); JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0)); panel.setBorder(new ScaledEmptyBorder(0, 8, 0, 0));
channel.setToolTipText("Channel path, e.g. \"Lobby/Games\", or a channel id. " channel.setToolTipText("Channel path, e.g. \"Lobby/Games\", or a channel id. "
+ "Leave empty for the server's default channel."); + "Leave empty for the server's default channel.");
@@ -143,7 +144,7 @@ public final class BookmarksDialog extends JDialog {
private void addRow(JPanel panel, int row, String text, java.awt.Component field) { private void addRow(JPanel panel, int row, String text, java.awt.Component field) {
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.gridy = row; c.gridy = row;
c.insets = new Insets(2, 0, 2, 4); c.insets = UIScale.scale(new Insets(2, 0, 2, 4));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.gridx = 0; c.gridx = 0;
panel.add(new JLabel(text), c); panel.add(new JLabel(text), c);
@@ -156,7 +157,7 @@ public final class BookmarksDialog extends JDialog {
private JPanel buildButtons() { private JPanel buildButtons() {
JPanel buttons = new JPanel(); JPanel buttons = new JPanel();
buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8)); buttons.setBorder(new ScaledEmptyBorder(0, 8, 8, 8));
addButton(buttons, "Add", this::addBookmark); addButton(buttons, "Add", this::addBookmark);
addButton(buttons, "Remove", this::removeSelected); addButton(buttons, "Remove", this::removeSelected);
if (TeamSpeakSettingsDb.locate() != null) { if (TeamSpeakSettingsDb.locate() != null) {
@@ -174,7 +175,7 @@ public final class BookmarksDialog extends JDialog {
JButton b = new JButton(text); JButton b = new JButton(text);
b.addActionListener(e -> action.run()); b.addActionListener(e -> action.run());
panel.add(b); panel.add(b);
panel.add(Box.createHorizontalStrut(4)); panel.add(Box.createHorizontalStrut(UIScale.scale(4)));
return b; return b;
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ChannelSettings; import com.ts3client.net.ChannelSettings;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
@@ -47,7 +49,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab {
ChannelAdvancedPanel() { ChannelAdvancedPanel() {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
phoneticName.setToolTipText("Set phonetic nickname used for this channel by text-to-speech."); phoneticName.setToolTipText("Set phonetic nickname used for this channel by text-to-speech.");
deleteDelay.setToolTipText("<html><p>Delay in seconds after temporary channels are deleted" deleteDelay.setToolTipText("<html><p>Delay in seconds after temporary channels are deleted"
@@ -79,12 +81,12 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab {
other.setAlignmentX(0f); other.setAlignmentX(0f);
fixHeight(other); fixHeight(other);
add(other); add(other);
add(Box.createVerticalStrut(6)); add(Box.createVerticalStrut(UIScale.scale(6)));
JPanel limits = new JPanel(); JPanel limits = new JPanel();
limits.setLayout(new BoxLayout(limits, BoxLayout.X_AXIS)); limits.setLayout(new BoxLayout(limits, BoxLayout.X_AXIS));
limits.add(limitBox("Max Users", new JRadioButton[]{maxUsersUnlimited, maxUsersLimited}, maxUsers)); limits.add(limitBox("Max Users", new JRadioButton[]{maxUsersUnlimited, maxUsersLimited}, maxUsers));
limits.add(Box.createHorizontalStrut(8)); limits.add(Box.createHorizontalStrut(UIScale.scale(8)));
limits.add(limitBox("Family Max Users", limits.add(limitBox("Family Max Users",
new JRadioButton[]{familyInherited, familyUnlimited, familyLimited}, familyMaxUsers)); new JRadioButton[]{familyInherited, familyUnlimited, familyLimited}, familyMaxUsers));
limits.setAlignmentX(0f); limits.setAlignmentX(0f);
@@ -140,7 +142,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab {
JPanel panel = new JPanel(new GridBagLayout()); JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder("Other Settings")); panel.setBorder(BorderFactory.createTitledBorder("Other Settings"));
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(3, 4, 3, 4); c.insets = UIScale.scale(new Insets(3, 4, 3, 4));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
@@ -155,7 +157,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab {
c.gridy = 1; c.gridy = 1;
c.weightx = 0; c.weightx = 0;
panel.add(new JLabel("Delete delay:"), c); panel.add(new JLabel("Delete delay:"), c);
JPanel delay = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); JPanel delay = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(4), 0));
delay.add(deleteDelay); delay.add(deleteDelay);
delay.add(deleteDelayMax); delay.add(deleteDelayMax);
c.gridx = 1; c.gridx = 1;
@@ -183,7 +185,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab {
box.add(radio); box.add(radio);
continue; continue;
} }
JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(6), 0));
row.setAlignmentX(0f); row.setAlignmentX(0f);
row.add(radio); row.add(radio);
row.add(spinner); row.add(spinner);

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ChannelSettings; import com.ts3client.net.ChannelSettings;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
@@ -49,8 +51,8 @@ final class ChannelAudioPanel extends JPanel implements ChannelDialog.Tab {
private final JPanel customSettings = new JPanel(new GridBagLayout()); private final JPanel customSettings = new JPanel(new GridBagLayout());
ChannelAudioPanel() { ChannelAudioPanel() {
super(new BorderLayout(8, 8)); super(new BorderLayout(UIScale.scale(8), UIScale.scale(8)));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
ButtonGroup presets = new ButtonGroup(); ButtonGroup presets = new ButtonGroup();
presets.add(voiceMobile); presets.add(voiceMobile);
@@ -126,7 +128,7 @@ final class ChannelAudioPanel extends JPanel implements ChannelDialog.Tab {
private JPanel customBox() { private JPanel customBox() {
customSettings.setBorder(BorderFactory.createTitledBorder("Custom Settings")); customSettings.setBorder(BorderFactory.createTitledBorder("Custom Settings"));
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(4, 4, 4, 4); c.insets = UIScale.scale(new Insets(4, 4, 4, 4));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
@@ -158,7 +160,7 @@ final class ChannelAudioPanel extends JPanel implements ChannelDialog.Tab {
} }
private JPanel bandwidthRow() { private JPanel bandwidthRow() {
JPanel row = new JPanel(new BorderLayout(6, 0)); JPanel row = new JPanel(new BorderLayout(UIScale.scale(6), 0));
row.add(new JLabel("Bandwidth usage:"), BorderLayout.WEST); row.add(new JLabel("Bandwidth usage:"), BorderLayout.WEST);
row.add(bandwidth, BorderLayout.CENTER); row.add(bandwidth, BorderLayout.CENTER);
return row; return row;

View File

@@ -1,10 +1,11 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ChannelNode; import com.ts3client.net.ChannelNode;
import com.ts3client.net.ChannelSettings; import com.ts3client.net.ChannelSettings;
import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.TeamspeakConnection;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JDialog; import javax.swing.JDialog;
@@ -144,19 +145,19 @@ final class ChannelDialog extends JDialog {
// The tabs need no more than their natural height, so they sit at the bottom // The tabs need no more than their natural height, so they sit at the bottom
// with the buttons and every spare pixel goes to the description instead. // with the buttons and every spare pixel goes to the description instead.
JPanel bottom = new JPanel(new BorderLayout(0, 6)); JPanel bottom = new JPanel(new BorderLayout(0, UIScale.scale(6)));
bottom.add(tabs, BorderLayout.CENTER); bottom.add(tabs, BorderLayout.CENTER);
bottom.add(buttons(), BorderLayout.SOUTH); bottom.add(buttons(), BorderLayout.SOUTH);
getContentPane().setLayout(new BorderLayout(0, 6)); getContentPane().setLayout(new BorderLayout(0, UIScale.scale(6)));
getContentPane().add(header(), BorderLayout.CENTER); getContentPane().add(header(), BorderLayout.CENTER);
getContentPane().add(bottom, BorderLayout.SOUTH); getContentPane().add(bottom, BorderLayout.SOUTH);
setEnabledForLoading(false); setEnabledForLoading(false);
Dialogs.closeOnEscape(this); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(new Dimension(560, 620)); setSize(UIScale.scale(new Dimension(560, 620)));
setMinimumSize(new Dimension(480, 520)); setMinimumSize(UIScale.scale(new Dimension(480, 520)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
if (channel != null) load(); if (channel != null) load();
@@ -167,9 +168,9 @@ final class ChannelDialog extends JDialog {
private JPanel header() { private JPanel header() {
JPanel panel = new JPanel(new GridBagLayout()); JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 8)); panel.setBorder(new ScaledEmptyBorder(8, 8, 0, 8));
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(3, 4, 3, 4); c.insets = UIScale.scale(new Insets(3, 4, 3, 4));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
@@ -248,7 +249,7 @@ final class ChannelDialog extends JDialog {
private JPanel iconField() { private JPanel iconField() {
iconButton.setToolTipText("Set channel icon."); iconButton.setToolTipText("Set channel icon.");
iconButton.setPreferredSize(new Dimension(26, 24)); iconButton.setPreferredSize(UIScale.scale(new Dimension(26, 24)));
iconButton.setMargin(new Insets(1, 1, 1, 1)); iconButton.setMargin(new Insets(1, 1, 1, 1));
iconButton.addActionListener(a -> chooseIcon()); iconButton.addActionListener(a -> chooseIcon());
iconButton.addMouseListener(new MouseAdapter() { iconButton.addMouseListener(new MouseAdapter() {
@@ -262,7 +263,7 @@ final class ChannelDialog extends JDialog {
if (e.isPopupTrigger()) showIconMenu(e); if (e.isPopupTrigger()) showIconMenu(e);
} }
}); });
JPanel row = new JPanel(new BorderLayout(4, 0)); JPanel row = new JPanel(new BorderLayout(UIScale.scale(4), 0));
row.add(new JLabel("Icon:"), BorderLayout.WEST); row.add(new JLabel("Icon:"), BorderLayout.WEST);
row.add(iconButton, BorderLayout.CENTER); row.add(iconButton, BorderLayout.CENTER);
return row; return row;
@@ -273,7 +274,7 @@ final class ChannelDialog extends JDialog {
ok.addActionListener(a -> save()); ok.addActionListener(a -> save());
cancel.addActionListener(a -> dispose()); cancel.addActionListener(a -> dispose());
JPanel panel = new JPanel(new BorderLayout()); JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8)); panel.setBorder(new ScaledEmptyBorder(0, 8, 8, 8));
JPanel right = new JPanel(); JPanel right = new JPanel();
right.add(ok); right.add(ok);
right.add(cancel); right.add(cancel);

View File

@@ -1,5 +1,8 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
@@ -49,18 +52,18 @@ final class ChannelPermissionsPanel extends JPanel {
ChannelPermissionsPanel() { ChannelPermissionsPanel() {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
JPanel groups = new JPanel(); JPanel groups = new JPanel();
groups.setLayout(new BoxLayout(groups, BoxLayout.X_AXIS)); groups.setLayout(new BoxLayout(groups, BoxLayout.X_AXIS));
groups.add(group("Regular Needed Powers", REGULAR)); groups.add(group("Regular Needed Powers", REGULAR));
groups.add(Box.createHorizontalStrut(8)); groups.add(Box.createHorizontalStrut(UIScale.scale(8)));
groups.add(group("File Transfer Needed Powers", FILE_TRANSFER)); groups.add(group("File Transfer Needed Powers", FILE_TRANSFER));
groups.setAlignmentX(0f); groups.setAlignmentX(0f);
add(groups); add(groups);
status.setAlignmentX(0f); status.setAlignmentX(0f);
status.setBorder(BorderFactory.createEmptyBorder(6, 2, 0, 2)); status.setBorder(new ScaledEmptyBorder(6, 2, 0, 2));
add(status); add(status);
add(Box.createVerticalGlue()); add(Box.createVerticalGlue());
setEditable(false); setEditable(false);
@@ -119,7 +122,7 @@ final class ChannelPermissionsPanel extends JPanel {
JPanel panel = new JPanel(new GridBagLayout()); JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder(title)); panel.setBorder(BorderFactory.createTitledBorder(title));
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(3, 4, 3, 4); c.insets = UIScale.scale(new Insets(3, 4, 3, 4));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
int row = 0; int row = 0;

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ChannelNode; import com.ts3client.net.ChannelNode;
import com.ts3client.net.ChannelSettings; import com.ts3client.net.ChannelSettings;
@@ -45,8 +47,8 @@ final class ChannelStandardPanel extends JPanel implements ChannelDialog.Tab {
* excluding the channel being edited * excluding the channel being edited
*/ */
ChannelStandardPanel(List<ChannelNode> siblings) { ChannelStandardPanel(List<ChannelNode> siblings) {
super(new BorderLayout(8, 8)); super(new BorderLayout(UIScale.scale(8), UIScale.scale(8)));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
temporary.setToolTipText("Channel will be deleted when the last user left."); temporary.setToolTipText("Channel will be deleted when the last user left.");
semiPermanent.setToolTipText("Channel exists until server is restarted."); semiPermanent.setToolTipText("Channel exists until server is restarted.");
@@ -67,7 +69,7 @@ final class ChannelStandardPanel extends JPanel implements ChannelDialog.Tab {
for (ChannelNode sibling : siblings) order.addElement(new SortEntry(sibling.id, sibling.name)); for (ChannelNode sibling : siblings) order.addElement(new SortEntry(sibling.id, sibling.name));
sortAfter.setModel(order); sortAfter.setModel(order);
sortAfter.setToolTipText("Channel will be sorted below this channel."); sortAfter.setToolTipText("Channel will be sorted below this channel.");
sortAfter.setPreferredSize(new Dimension(200, sortAfter.getPreferredSize().height)); sortAfter.setPreferredSize(new Dimension(UIScale.scale(200), sortAfter.getPreferredSize().height));
Spinners.compact(talkPower); Spinners.compact(talkPower);
add(typeBox(), BorderLayout.WEST); add(typeBox(), BorderLayout.WEST);
@@ -139,10 +141,10 @@ final class ChannelStandardPanel extends JPanel implements ChannelDialog.Tab {
} }
JSeparator line = new JSeparator(); JSeparator line = new JSeparator();
line.setAlignmentX(0f); line.setAlignmentX(0f);
line.setMaximumSize(new Dimension(Integer.MAX_VALUE, 8)); line.setMaximumSize(new Dimension(Integer.MAX_VALUE, UIScale.scale(8)));
box.add(Box.createVerticalStrut(4)); box.add(Box.createVerticalStrut(UIScale.scale(4)));
box.add(line); box.add(line);
box.add(Box.createVerticalStrut(4)); box.add(Box.createVerticalStrut(UIScale.scale(4)));
defaultChannel.setAlignmentX(0f); defaultChannel.setAlignmentX(0f);
box.add(defaultChannel); box.add(defaultChannel);
box.add(Box.createVerticalGlue()); box.add(Box.createVerticalGlue());
@@ -154,7 +156,7 @@ final class ChannelStandardPanel extends JPanel implements ChannelDialog.Tab {
sort.setBorder(BorderFactory.createTitledBorder("Sort This Channel After:")); sort.setBorder(BorderFactory.createTitledBorder("Sort This Channel After:"));
sort.add(sortAfter, BorderLayout.CENTER); sort.add(sortAfter, BorderLayout.CENTER);
JPanel moderation = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 2)); JPanel moderation = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(6), UIScale.scale(2)));
moderation.setBorder(BorderFactory.createTitledBorder("Moderation")); moderation.setBorder(BorderFactory.createTitledBorder("Moderation"));
JLabel label = new JLabel("Needed Talk Power:"); JLabel label = new JLabel("Needed Talk Power:");
label.setToolTipText("Talk Power required to speak in this channel."); label.setToolTipText("Talk Power required to speak in this channel.");
@@ -168,7 +170,7 @@ final class ChannelStandardPanel extends JPanel implements ChannelDialog.Tab {
c.gridx = 0; c.gridx = 0;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1; c.weightx = 1;
c.insets = new Insets(0, 0, 6, 0); c.insets = UIScale.scale(new Insets(0, 0, 6, 0));
column.add(sort, c); column.add(sort, c);
column.add(moderation, c); column.add(moderation, c);
c.weighty = 1; c.weighty = 1;

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.text.BBCode; import com.ts3client.text.BBCode;
import com.ts3client.text.TsLink; import com.ts3client.text.TsLink;
@@ -96,8 +98,8 @@ public final class ChatPanel extends JPanel {
dragReorder.attach(tabs); dragReorder.attach(tabs);
add(tabs, BorderLayout.CENTER); add(tabs, BorderLayout.CENTER);
JPanel bottom = new JPanel(new BorderLayout(4, 0)); JPanel bottom = new JPanel(new BorderLayout(UIScale.scale(4), 0));
bottom.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0)); bottom.setBorder(new ScaledEmptyBorder(4, 0, 0, 0));
bottom.add(input, BorderLayout.CENTER); bottom.add(input, BorderLayout.CENTER);
add(bottom, BorderLayout.SOUTH); add(bottom, BorderLayout.SOUTH);
@@ -345,7 +347,8 @@ public final class ChatPanel extends JPanel {
final int clientId; final int clientId;
private String clientUniqueId; private String clientUniqueId;
private String title; private String title;
final JEditorPane log = HtmlStyles.pane("font-family:sans-serif; font-size:11px; margin:4px 6px;"); final JEditorPane log = HtmlStyles.pane("font-family:sans-serif; font-size:11px; margin:"
+ HtmlStyles.px(4) + " " + HtmlStyles.px(6) + ";");
final JScrollPane scroll; final JScrollPane scroll;
/** Set for a static-content tab (e.g. a moved-out description); null for a conversation. */ /** Set for a static-content tab (e.g. a moved-out description); null for a conversation. */
final String noteKey; final String noteKey;
@@ -408,17 +411,17 @@ public final class ChatPanel extends JPanel {
Component header(boolean closable) { Component header(boolean closable) {
// Only the height is pinned: a fixed width would freeze the tab at the width of // Only the height is pinned: a fixed width would freeze the tab at the width of
// whatever name it was built with, clipping longer ones (and the bold unread font). // whatever name it was built with, clipping longer ones (and the bold unread font).
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 0)) { JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(2), 0)) {
@Override @Override
public Dimension getPreferredSize() { public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize(); Dimension d = super.getPreferredSize();
return new Dimension(d.width, Math.max(d.height, HEADER_HEIGHT)); return new Dimension(d.width, Math.max(d.height, UIScale.scale(HEADER_HEIGHT)));
} }
}; };
p.setOpaque(false); p.setOpaque(false);
titleLabel = new JLabel(title, icon, JLabel.LEADING); titleLabel = new JLabel(title, icon, JLabel.LEADING);
titleLabel.setFont(Theme.uiFont()); titleLabel.setFont(Theme.uiFont());
titleLabel.setIconTextGap(3); titleLabel.setIconTextGap(UIScale.scale(3));
p.add(titleLabel); p.add(titleLabel);
dragReorder.attach(p); dragReorder.attach(p);
dragReorder.attach(titleLabel); dragReorder.attach(titleLabel);

View File

@@ -1,9 +1,10 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.ClientVersion; import com.ts3client.config.ClientVersion;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBox; import javax.swing.JCheckBox;
import javax.swing.JComboBox; import javax.swing.JComboBox;
@@ -45,10 +46,10 @@ final class ClientVersionPanel extends JPanel {
private boolean filling; private boolean filling;
ClientVersionPanel(Settings settings) { ClientVersionPanel(Settings settings) {
super(new BorderLayout(0, 8)); super(new BorderLayout(0, UIScale.scale(8)));
this.settings = settings; this.settings = settings;
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); setBorder(new ScaledEmptyBorder(12, 12, 12, 12));
add(buildForm(), BorderLayout.NORTH); add(buildForm(), BorderLayout.NORTH);
load(); load();
@@ -67,7 +68,7 @@ final class ClientVersionPanel extends JPanel {
private JComponent buildForm() { private JComponent buildForm() {
JPanel p = new JPanel(new GridBagLayout()); JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints(); 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.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
@@ -110,7 +111,7 @@ final class ClientVersionPanel extends JPanel {
c.gridx = 0; c.gridx = 0;
c.gridy = row; c.gridy = row;
c.gridwidth = 2; c.gridwidth = 2;
p.add(new JLabel("<html><body style='width:320px'>The three fields belong together: the " p.add(new JLabel("<html><body style='width:" + HtmlStyles.px(320) + "'>The three fields belong together: the "
+ "signature is TeamSpeak's, over that version and operating system. A server " + "signature is TeamSpeak's, over that version and operating system. A server "
+ "answers \"client is modified\" and refuses the connection when it does not " + "answers \"client is modified\" and refuses the connection when it does not "
+ "accept the version — made up, or older than the server's minimum — so prefer " + "accept the version — made up, or older than the server's minimum — so prefer "

View File

@@ -1,9 +1,10 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ClientEntry; import com.ts3client.net.ClientEntry;
import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.TeamspeakConnection;
import javax.swing.BorderFactory;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JDialog; import javax.swing.JDialog;
import javax.swing.JLabel; import javax.swing.JLabel;
@@ -41,11 +42,11 @@ final class ClientVolumeDialog extends JDialog {
slider.addChangeListener(e -> onChanged()); slider.addChangeListener(e -> onChanged());
FormPanel.limitWidth(value, 60); FormPanel.limitWidth(value, 60);
JPanel content = new JPanel(new BorderLayout(8, 8)); JPanel content = new JPanel(new BorderLayout(UIScale.scale(8), UIScale.scale(8)));
content.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12)); content.setBorder(new ScaledEmptyBorder(12, 12, 8, 12));
content.add(new JLabel("Volume Modifier"), BorderLayout.NORTH); content.add(new JLabel("Volume Modifier"), BorderLayout.NORTH);
content.add(FormPanel.sliderWithLabel(slider, value), BorderLayout.CENTER); content.add(FormPanel.sliderWithLabel(slider, value), BorderLayout.CENTER);
JLabel hint = new JLabel("<html><body style='width:300px'>Values greater than +6 dB will cause clipping " JLabel hint = new JLabel("<html><body style='width:" + HtmlStyles.px(300) + "'>Values greater than +6 dB will cause clipping "
+ "and distortion that will negatively affect your audio quality. It is advised to choose " + "and distortion that will negatively affect your audio quality. It is advised to choose "
+ "lower values.</body></html>"); + "lower values.</body></html>");
hint.setForeground(Theme.chatSystem()); hint.setForeground(Theme.chatSystem());
@@ -67,7 +68,7 @@ final class ClientVolumeDialog extends JDialog {
Dialogs.closeOnEscape(this); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack(); pack();
setMinimumSize(new Dimension(360, getHeight())); setMinimumSize(new Dimension(UIScale.scale(360), getHeight()));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }

View File

@@ -1,9 +1,10 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.IdentityStore; import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import javax.swing.BorderFactory;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JDialog; import javax.swing.JDialog;
import javax.swing.JLabel; import javax.swing.JLabel;
@@ -49,9 +50,9 @@ public final class ConnectDialog extends JDialog {
identityChooser = new IdentityChooser(identities, false, settings.defaultIdentityId); identityChooser = new IdentityChooser(identities, false, settings.defaultIdentityId);
JPanel form = new JPanel(new GridBagLayout()); JPanel form = new JPanel(new GridBagLayout());
form.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12)); form.setBorder(new ScaledEmptyBorder(12, 12, 8, 12));
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(4, 4, 4, 4); c.insets = UIScale.scale(new Insets(4, 4, 4, 4));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
@@ -83,7 +84,7 @@ public final class ConnectDialog extends JDialog {
Dialogs.closeOnEscape(this); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack(); pack();
setMinimumSize(new Dimension(340, getHeight())); setMinimumSize(new Dimension(UIScale.scale(340), getHeight()));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }

View File

@@ -1,9 +1,10 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ConnectionStats; import com.ts3client.net.ConnectionStats;
import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.TeamspeakConnection;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JButton; import javax.swing.JButton;
@@ -62,9 +63,9 @@ public final class ConnectionInfoDialog extends JDialog {
this.conn = conn; this.conn = conn;
this.clientId = clientId; this.clientId = clientId;
JPanel content = new JPanel(new BorderLayout(0, 10)); JPanel content = new JPanel(new BorderLayout(0, UIScale.scale(10)));
content.setBackground(Theme.windowBg()); content.setBackground(Theme.windowBg());
content.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14)); content.setBorder(new ScaledEmptyBorder(12, 14, 12, 14));
content.add(buildSummary(), BorderLayout.NORTH); content.add(buildSummary(), BorderLayout.NORTH);
content.add(buildTabs(), BorderLayout.CENTER); content.add(buildTabs(), BorderLayout.CENTER);
content.add(buildButtons(), BorderLayout.SOUTH); content.add(buildButtons(), BorderLayout.SOUTH);
@@ -138,7 +139,7 @@ public final class ConnectionInfoDialog extends JDialog {
lc.gridx = 0; lc.gridx = 0;
lc.gridy = row; lc.gridy = row;
lc.anchor = GridBagConstraints.WEST; lc.anchor = GridBagConstraints.WEST;
lc.insets = new Insets(2, 0, 2, 16); lc.insets = UIScale.scale(new Insets(2, 0, 2, 16));
JLabel key = new JLabel(label); JLabel key = new JLabel(label);
key.setFont(Theme.uiFont()); key.setFont(Theme.uiFont());
key.setForeground(new Color(0x5A6B7B)); key.setForeground(new Color(0x5A6B7B));
@@ -150,7 +151,7 @@ public final class ConnectionInfoDialog extends JDialog {
vc.weightx = 1; vc.weightx = 1;
vc.anchor = GridBagConstraints.WEST; vc.anchor = GridBagConstraints.WEST;
vc.fill = GridBagConstraints.HORIZONTAL; vc.fill = GridBagConstraints.HORIZONTAL;
vc.insets = new Insets(2, 0, 2, 0); vc.insets = UIScale.scale(new Insets(2, 0, 2, 0));
grid.add(valueLabel, vc); grid.add(valueLabel, vc);
} }
@@ -256,7 +257,7 @@ public final class ConnectionInfoDialog extends JDialog {
KindTab() { KindTab() {
super(new GridBagLayout()); super(new GridBagLayout());
setBackground(Theme.windowBg()); setBackground(Theme.windowBg());
setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); setBorder(new ScaledEmptyBorder(10, 12, 10, 12));
int row = 0; int row = 0;
addRow(this, row++, "Packet loss", packetLoss); addRow(this, row++, "Packet loss", packetLoss);
addRow(this, row++, "Packets (↑/↓)", packets); addRow(this, row++, "Packets (↑/↓)", packets);

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import com.ts3client.contacts.ContactDefaults; import com.ts3client.contacts.ContactDefaults;
import com.ts3client.contacts.ContactType; import com.ts3client.contacts.ContactType;
@@ -50,13 +52,13 @@ final class ContactDefaultsDialog extends JDialog {
JPanel content = new JPanel(); JPanel content = new JPanel();
content.setLayout(new javax.swing.BoxLayout(content, javax.swing.BoxLayout.Y_AXIS)); content.setLayout(new javax.swing.BoxLayout(content, javax.swing.BoxLayout.Y_AXIS));
content.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12)); content.setBorder(new ScaledEmptyBorder(12, 12, 8, 12));
JLabel intro = new JLabel("Configure default settings when adding a client to the Contact Manager"); JLabel intro = new JLabel("Configure default settings when adding a client to the Contact Manager");
intro.setAlignmentX(LEFT_ALIGNMENT); intro.setAlignmentX(LEFT_ALIGNMENT);
content.add(intro); content.add(intro);
content.add(javax.swing.Box.createVerticalStrut(10)); content.add(javax.swing.Box.createVerticalStrut(UIScale.scale(10)));
content.add(buildFlags()); content.add(buildFlags());
content.add(javax.swing.Box.createVerticalStrut(10)); content.add(javax.swing.Box.createVerticalStrut(UIScale.scale(10)));
content.add(buildDisplays()); content.add(buildDisplays());
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
@@ -157,7 +159,7 @@ final class ContactDefaultsDialog extends JDialog {
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.gridx = x; c.gridx = x;
c.gridy = y; c.gridy = y;
c.insets = new Insets(1, 6, 1, 6); c.insets = UIScale.scale(new Insets(1, 6, 1, 6));
c.anchor = GridBagConstraints.CENTER; c.anchor = GridBagConstraints.CENTER;
return c; return c;
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import com.ts3client.contacts.Contact; import com.ts3client.contacts.Contact;
import com.ts3client.contacts.ContactStore; import com.ts3client.contacts.ContactStore;
@@ -8,7 +10,6 @@ import com.ts3client.contacts.NicknameDisplay;
import com.ts3client.teamspeak.TeamSpeakSettingsDb; import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import com.ts3client.text.TsLink; import com.ts3client.text.TsLink;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.ButtonGroup; import javax.swing.ButtonGroup;
@@ -99,7 +100,7 @@ final class ContactsDialog extends JDialog {
private final JCheckBox ignoreAway = new JCheckBox("Hide away message"); private final JCheckBox ignoreAway = new JCheckBox("Hide away message");
private final JRadioButton allowWhisper = new JRadioButton("Allow whisper"); private final JRadioButton allowWhisper = new JRadioButton("Allow whisper");
private final JRadioButton denyWhisper = new JRadioButton("Deny whisper"); private final JRadioButton denyWhisper = new JRadioButton("Deny whisper");
private final JPanel details = new JPanel(new BorderLayout(0, 8)); private final JPanel details = new JPanel(new BorderLayout(0, UIScale.scale(8)));
/** Set while the fields are being filled from a contact, so that does not count as an edit. */ /** Set while the fields are being filled from a contact, so that does not count as an edit. */
private boolean filling; private boolean filling;
@@ -110,22 +111,22 @@ final class ContactsDialog extends JDialog {
this.settings = settings; this.settings = settings;
buildTree(); buildTree();
JPanel left = new JPanel(new BorderLayout(0, 4)); JPanel left = new JPanel(new BorderLayout(0, UIScale.scale(4)));
left.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 4)); left.setBorder(new ScaledEmptyBorder(8, 8, 8, 4));
JLabel hint = new JLabel("Drag & Drop users here"); JLabel hint = new JLabel("Drag & Drop users here");
hint.setForeground(Theme.chatSystem()); hint.setForeground(Theme.chatSystem());
left.add(hint, BorderLayout.NORTH); left.add(hint, BorderLayout.NORTH);
JScrollPane scroll = new JScrollPane(tree); JScrollPane scroll = new JScrollPane(tree);
scroll.setPreferredSize(new Dimension(300, 340)); scroll.setPreferredSize(UIScale.scale(new Dimension(300, 340)));
left.add(scroll, BorderLayout.CENTER); left.add(scroll, BorderLayout.CENTER);
details.setBorder(BorderFactory.createEmptyBorder(8, 4, 8, 8)); details.setBorder(new ScaledEmptyBorder(8, 4, 8, 8));
details.add(buildNicknameRow(), BorderLayout.NORTH); details.add(buildNicknameRow(), BorderLayout.NORTH);
details.add(buildPages(), BorderLayout.CENTER); details.add(buildPages(), BorderLayout.CENTER);
JPanel buttons = new JPanel(new BorderLayout()); JPanel buttons = new JPanel(new BorderLayout());
buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8)); buttons.setBorder(new ScaledEmptyBorder(0, 8, 8, 8));
JPanel leftButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); JPanel leftButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(6), 0));
JButton defaults = new JButton("Set Defaults"); JButton defaults = new JButton("Set Defaults");
defaults.setToolTipText("Set default display, ignore and hide options"); defaults.setToolTipText("Set default display, ignore and hide options");
defaults.addActionListener(e -> new ContactDefaultsDialog(this, settings).setVisible(true)); defaults.addActionListener(e -> new ContactDefaultsDialog(this, settings).setVisible(true));
@@ -160,7 +161,7 @@ final class ContactsDialog extends JDialog {
Dialogs.closeOnEscape(this); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack(); pack();
setMinimumSize(new Dimension(620, 400)); setMinimumSize(UIScale.scale(new Dimension(620, 400)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }
@@ -172,7 +173,7 @@ final class ContactsDialog extends JDialog {
root.add(neutral); root.add(neutral);
tree.setRootVisible(false); tree.setRootVisible(false);
tree.setShowsRootHandles(true); tree.setShowsRootHandles(true);
tree.setRowHeight(20); tree.setRowHeight(UIScale.scale(20));
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setCellRenderer(new ContactRenderer()); tree.setCellRenderer(new ContactRenderer());
tree.addTreeSelectionListener(e -> showSelected()); tree.addTreeSelectionListener(e -> showSelected());
@@ -301,7 +302,7 @@ final class ContactsDialog extends JDialog {
private JComponent buildNicknameRow() { private JComponent buildNicknameRow() {
JPanel row = new JPanel(new GridBagLayout()); JPanel row = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints(); 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.anchor = GridBagConstraints.WEST;
c.gridy = 0; c.gridy = 0;
c.gridx = 0; c.gridx = 0;
@@ -325,7 +326,7 @@ final class ContactsDialog extends JDialog {
JTabbedPane pages = new JTabbedPane(); JTabbedPane pages = new JTabbedPane();
pages.addTab("General", page( pages.addTab("General", page(
radioGroup(neutralRadio, blockedRadio, friendRadio), radioGroup(neutralRadio, blockedRadio, friendRadio),
Box.createVerticalStrut(8), Box.createVerticalStrut(UIScale.scale(8)),
labelled("Phonetic Nickname:", phoneticField))); labelled("Phonetic Nickname:", phoneticField)));
neutralRadio.setToolTipText("Mark as neutral"); neutralRadio.setToolTipText("Mark as neutral");
blockedRadio.setToolTipText("Mark as blocked"); blockedRadio.setToolTipText("Mark as blocked");
@@ -375,7 +376,7 @@ final class ContactsDialog extends JDialog {
private static JPanel page(Component... rows) { private static JPanel page(Component... rows) {
JPanel p = new JPanel(); JPanel p = new JPanel();
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); p.setBorder(new ScaledEmptyBorder(10, 10, 10, 10));
for (Component row : rows) { for (Component row : rows) {
if (row instanceof JComponent) ((JComponent) row).setAlignmentX(Component.LEFT_ALIGNMENT); if (row instanceof JComponent) ((JComponent) row).setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(row); p.add(row);
@@ -397,7 +398,7 @@ final class ContactsDialog extends JDialog {
} }
private static JPanel labelled(String label, JComponent field) { private static JPanel labelled(String label, JComponent field) {
JPanel p = new JPanel(new BorderLayout(6, 0)); JPanel p = new JPanel(new BorderLayout(UIScale.scale(6), 0));
p.add(new JLabel(label), BorderLayout.WEST); p.add(new JLabel(label), BorderLayout.WEST);
p.add(field, BorderLayout.CENTER); p.add(field, BorderLayout.CENTER);
p.setMaximumSize(new Dimension(Integer.MAX_VALUE, field.getPreferredSize().height)); p.setMaximumSize(new Dimension(Integer.MAX_VALUE, field.getPreferredSize().height));

View File

@@ -1,8 +1,9 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JCheckBox; import javax.swing.JCheckBox;
@@ -32,11 +33,11 @@ final class ContactsPanel extends JPanel {
ContactsPanel(Settings settings) { ContactsPanel(Settings settings) {
super(new BorderLayout()); super(new BorderLayout());
this.settings = settings; this.settings = settings;
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); setBorder(new ScaledEmptyBorder(12, 12, 12, 12));
JPanel p = new JPanel(new GridBagLayout()); JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints(); 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.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
c.gridx = 0; c.gridx = 0;
@@ -48,10 +49,10 @@ final class ContactsPanel extends JPanel {
p.add(colorContacts, c); p.add(colorContacts, c);
c.gridy = 1; c.gridy = 1;
c.insets = new Insets(12, 2, 2, 2); c.insets = UIScale.scale(new Insets(12, 2, 2, 2));
p.add(new JLabel("Whispers: who is allowed to whisper to you"), c); p.add(new JLabel("Whispers: who is allowed to whisper to you"), c);
c.gridy = 2; c.gridy = 2;
c.insets = new Insets(2, 2, 2, 2); c.insets = UIScale.scale(new Insets(2, 2, 2, 2));
whisperPolicy.setSelectedItem(settings.whisperPolicy); whisperPolicy.setSelectedItem(settings.whisperPolicy);
whisperPolicy.setRenderer(new DefaultListCellRenderer() { whisperPolicy.setRenderer(new DefaultListCellRenderer() {
@Override @Override
@@ -68,7 +69,7 @@ final class ContactsPanel extends JPanel {
p.add(whisperPolicy, c); p.add(whisperPolicy, c);
c.gridy = 3; c.gridy = 3;
c.insets = new Insets(12, 2, 2, 2); c.insets = UIScale.scale(new Insets(12, 2, 2, 2));
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.NONE;
JButton defaults = new JButton("Contact defaults…"); JButton defaults = new JButton("Contact defaults…");
defaults.setToolTipText("Configure default settings when adding a client to the Contact Manager"); defaults.setToolTipText("Configure default settings when adding a client to the Contact Manager");

View File

@@ -1,8 +1,9 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.text.BBCode; import com.ts3client.text.BBCode;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JColorChooser; import javax.swing.JColorChooser;
@@ -47,19 +48,19 @@ final class DescriptionEditorDialog extends JDialog {
area.setText(description); area.setText(description);
area.setLineWrap(true); area.setLineWrap(true);
area.setWrapStyleWord(true); area.setWrapStyleWord(true);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, UIScale.scale(12)));
area.setCaretPosition(0); area.setCaretPosition(0);
preview.setEditable(false); preview.setEditable(false);
preview.setBackground(Theme.chatBg()); preview.setBackground(Theme.chatBg());
preview.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); preview.setBorder(new ScaledEmptyBorder(6, 8, 6, 8));
body.add(new JScrollPane(area), EDIT_CARD); body.add(new JScrollPane(area), EDIT_CARD);
body.add(new JScrollPane(preview), PREVIEW_CARD); body.add(new JScrollPane(preview), PREVIEW_CARD);
JLabel hint = new JLabel("Press Button to \"Preview\" the changes in channel info."); JLabel hint = new JLabel("Press Button to \"Preview\" the changes in channel info.");
hint.setForeground(Theme.chatSystem()); hint.setForeground(Theme.chatSystem());
hint.setBorder(BorderFactory.createEmptyBorder(4, 6, 0, 6)); hint.setBorder(new ScaledEmptyBorder(4, 6, 0, 6));
getContentPane().setLayout(new BorderLayout()); getContentPane().setLayout(new BorderLayout());
getContentPane().add(buildToolbar(), BorderLayout.NORTH); getContentPane().add(buildToolbar(), BorderLayout.NORTH);
@@ -71,8 +72,8 @@ final class DescriptionEditorDialog extends JDialog {
Dialogs.closeOnEscape(this); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(new Dimension(520, 380)); setSize(UIScale.scale(new Dimension(520, 380)));
setMinimumSize(new Dimension(360, 260)); setMinimumSize(UIScale.scale(new Dimension(360, 260)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }
@@ -134,7 +135,7 @@ final class DescriptionEditorDialog extends JDialog {
* box, which would otherwise let one of them soak up all the free width. * box, which would otherwise let one of them soak up all the free width.
*/ */
private static JButton square(JButton button) { private static JButton square(JButton button) {
Dimension size = new Dimension(Math.max(28, button.getPreferredSize().width), Dimension size = new Dimension(Math.max(UIScale.scale(28), button.getPreferredSize().width),
button.getPreferredSize().height); button.getPreferredSize().height);
button.setPreferredSize(size); button.setPreferredSize(size);
button.setMaximumSize(size); button.setMaximumSize(size);
@@ -162,7 +163,7 @@ final class DescriptionEditorDialog extends JDialog {
private void showPreview(boolean on) { private void showPreview(boolean on) {
previewButton.setSelected(on); previewButton.setSelected(on);
if (on) { if (on) {
preview.setText("<html><body style=\"font-family:sans-serif;font-size:9pt\">" preview.setText("<html><body style=\"font-family:sans-serif;font-size:" + UIScale.scale(9) + "pt\">"
+ BBCode.toHtml(area.getText()) + "</body></html>"); + BBCode.toHtml(area.getText()) + "</body></html>");
preview.setCaretPosition(0); preview.setCaretPosition(0);
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import com.ts3client.gfx.IconPack; import com.ts3client.gfx.IconPack;
@@ -24,14 +26,15 @@ import java.awt.GridBagConstraints;
import java.awt.GridBagLayout; import java.awt.GridBagLayout;
import java.awt.Insets; import java.awt.Insets;
import java.io.File; import java.io.File;
import java.util.Arrays;
/** /**
* Options page for how the client looks: the light or dark look-and-feel, and the icon * Options page for how the client looks: the light or dark look-and-feel, the interface
* pack the user interface is drawn with — where to look for more of them, and a viewer * scale, and the icon pack the user interface is drawn with — where to look for more of
* showing everything the active pack contains. * them, and a viewer showing everything the active pack contains.
* *
* <p>Both apply right away so the change can be seen in the window behind the dialog; * <p>Theme and pack apply right away so the change can be seen in the window behind the
* cancelling puts the previous ones back. * dialog; cancelling puts the previous ones back. The scale is only read at startup.
*/ */
final class DesignPanel extends JPanel { final class DesignPanel extends JPanel {
@@ -42,6 +45,9 @@ final class DesignPanel extends JPanel {
/** Stands for "no pack": the icons the client draws itself. */ /** Stands for "no pack": the icons the client draws itself. */
private static final String BUILT_IN = "Built-in icons"; private static final String BUILT_IN = "Built-in icons";
/** The scale factors offered besides following the desktop (0). */
private static final double[] SCALES = {0, 1, 1.25, 1.5, 1.75, 2, 2.5, 3};
private final Settings settings; private final Settings settings;
private final String originalPackId; private final String originalPackId;
private final String originalPackDir; private final String originalPackDir;
@@ -49,6 +55,7 @@ final class DesignPanel extends JPanel {
private final JComboBox<Settings.Appearance> appearanceCombo = private final JComboBox<Settings.Appearance> appearanceCombo =
new JComboBox<>(Settings.Appearance.values()); new JComboBox<>(Settings.Appearance.values());
private final JComboBox<Double> scaleCombo = new JComboBox<>();
private final JComboBox<Object> packCombo = new JComboBox<>(); private final JComboBox<Object> packCombo = new JComboBox<>();
private final JLabel packInfo = new JLabel(); private final JLabel packInfo = new JLabel();
private final JTextField packDirField; private final JTextField packDirField;
@@ -56,14 +63,14 @@ final class DesignPanel extends JPanel {
private final JList<String> preview = new JList<>(previewModel); private final JList<String> preview = new JList<>(previewModel);
DesignPanel(Settings settings) { DesignPanel(Settings settings) {
super(new BorderLayout(0, 8)); super(new BorderLayout(0, UIScale.scale(8)));
this.settings = settings; this.settings = settings;
this.originalPackId = settings.iconPack; this.originalPackId = settings.iconPack;
this.originalPackDir = settings.iconPackDir; this.originalPackDir = settings.iconPackDir;
this.originalAppearance = settings.appearance; this.originalAppearance = settings.appearance;
this.packDirField = new JTextField(settings.iconPackDir, 18); this.packDirField = new JTextField(settings.iconPackDir, 18);
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); setBorder(new ScaledEmptyBorder(12, 12, 12, 12));
add(buildHeader(), BorderLayout.NORTH); add(buildHeader(), BorderLayout.NORTH);
add(buildPreview(), BorderLayout.CENTER); add(buildPreview(), BorderLayout.CENTER);
@@ -73,6 +80,7 @@ final class DesignPanel extends JPanel {
/** Copies the chosen appearance and pack into the settings; the caller saves them. */ /** Copies the chosen appearance and pack into the settings; the caller saves them. */
void apply() { void apply() {
settings.appearance = selectedAppearance(); settings.appearance = selectedAppearance();
if (scaleCombo.getSelectedItem() instanceof Double scale) settings.uiScale = scale;
settings.iconPackDir = packDirField.getText().trim(); settings.iconPackDir = packDirField.getText().trim();
Object selected = packCombo.getSelectedItem(); Object selected = packCombo.getSelectedItem();
settings.iconPack = selected instanceof IconPack pack ? pack.id() : ""; settings.iconPack = selected instanceof IconPack pack ? pack.id() : "";
@@ -94,7 +102,7 @@ final class DesignPanel extends JPanel {
private JComponent buildHeader() { private JComponent buildHeader() {
JPanel p = new JPanel(new GridBagLayout()); JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints(); 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.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
@@ -125,13 +133,17 @@ final class DesignPanel extends JPanel {
int row = 0; int row = 0;
addRow(p, c, row++, new JLabel("Theme:"), appearanceCombo); addRow(p, c, row++, new JLabel("Theme:"), appearanceCombo);
if (DisplayScale.isManaged()) {
buildScaleCombo();
addRow(p, c, row++, new JLabel("Interface scale:"), scaleCombo);
}
addRow(p, c, row++, new JLabel("Icon pack:"), packCombo); addRow(p, c, row++, new JLabel("Icon pack:"), packCombo);
c.gridx = 1; c.gridx = 1;
c.gridy = row++; c.gridy = row++;
packInfo.setEnabled(false); packInfo.setEnabled(false);
p.add(packInfo, c); p.add(packInfo, c);
JPanel dirRow = new JPanel(new BorderLayout(6, 0)); JPanel dirRow = new JPanel(new BorderLayout(UIScale.scale(6), 0));
JButton browse = new JButton("Browse…"); JButton browse = new JButton("Browse…");
browse.addActionListener(e -> browseForPackFolder()); browse.addActionListener(e -> browseForPackFolder());
dirRow.add(packDirField, BorderLayout.CENTER); dirRow.add(packDirField, BorderLayout.CENTER);
@@ -141,18 +153,42 @@ final class DesignPanel extends JPanel {
return p; return p;
} }
private void buildScaleCombo() {
for (double scale : SCALES) scaleCombo.addItem(scale);
if (settings.uiScale > 0 && Arrays.stream(SCALES).noneMatch(s -> s == settings.uiScale)) {
scaleCombo.addItem(settings.uiScale);
}
scaleCombo.setSelectedItem(Math.max(0, settings.uiScale));
scaleCombo.setToolTipText("Size of text, icons and controls; takes effect after a restart");
scaleCombo.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
boolean selected, boolean focus) {
super.getListCellRendererComponent(list, value, index, selected, focus);
double scale = value instanceof Double d ? d : 0;
setText(scale > 0 ? percent(scale)
: "Automatic (" + percent(DisplayScale.detected()) + ")");
return this;
}
});
}
private static String percent(double scale) {
return Math.round(scale * 100) + "%";
}
private JComponent buildPreview() { private JComponent buildPreview() {
preview.setLayoutOrientation(JList.HORIZONTAL_WRAP); preview.setLayoutOrientation(JList.HORIZONTAL_WRAP);
preview.setVisibleRowCount(-1); preview.setVisibleRowCount(-1);
preview.setFixedCellWidth(TILE); preview.setFixedCellWidth(UIScale.scale(TILE));
preview.setFixedCellHeight(TILE); preview.setFixedCellHeight(UIScale.scale(TILE));
preview.setCellRenderer(new DefaultListCellRenderer() { preview.setCellRenderer(new DefaultListCellRenderer() {
@Override @Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, public Component getListCellRendererComponent(JList<?> list, Object value, int index,
boolean selected, boolean focus) { boolean selected, boolean focus) {
super.getListCellRendererComponent(list, value, index, selected, focus); super.getListCellRendererComponent(list, value, index, selected, focus);
String key = String.valueOf(value); String key = String.valueOf(value);
ImageIcon icon = Icons.of(key, PREVIEW_SIZE); ImageIcon icon = Icons.of(key, UIScale.scale(PREVIEW_SIZE));
setText(""); setText("");
setIcon(icon); setIcon(icon);
setToolTipText(key); setToolTipText(key);
@@ -162,7 +198,7 @@ final class DesignPanel extends JPanel {
}); });
JScrollPane scroll = new JScrollPane(preview); JScrollPane scroll = new JScrollPane(preview);
scroll.setPreferredSize(new Dimension(360, 200)); scroll.setPreferredSize(UIScale.scale(new Dimension(360, 200)));
scroll.setBorder(BorderFactory.createTitledBorder("Icons in this pack")); scroll.setBorder(BorderFactory.createTitledBorder("Icons in this pack"));
return scroll; return scroll;
} }

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.audio.VoiceInput; import com.ts3client.audio.VoiceInput;
import com.ts3client.audio.VoiceOutput; import com.ts3client.audio.VoiceOutput;
import com.ts3client.audio.desktop.AudioDevices; import com.ts3client.audio.desktop.AudioDevices;
@@ -75,9 +76,9 @@ final class DevicesPanel extends FormPanel {
c.gridx = 0; c.gridx = 0;
c.gridy = row++; c.gridy = row++;
c.gridwidth = 2; c.gridwidth = 2;
c.insets = new Insets(14, 4, 2, 4); c.insets = UIScale.scale(new Insets(14, 4, 2, 4));
add(new JLabel("Noise reduction"), c); add(new JLabel("Noise reduction"), c);
c.insets = new Insets(4, 4, 4, 4); c.insets = UIScale.scale(new Insets(4, 4, 4, 4));
c.gridwidth = 1; c.gridwidth = 1;
denoiseCheck = new JCheckBox("Remove background noise", settings.denoise); denoiseCheck = new JCheckBox("Remove background noise", settings.denoise);

View File

@@ -0,0 +1,134 @@
package com.ts3client.ui;
import java.awt.Toolkit;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
/**
* Decides how large the user interface is drawn on Linux, where Java2D only knows
* whole-number scale factors: asked for 150&nbsp;%, it draws at 100&nbsp;% — or, told
* 2 by the desktop, at 200&nbsp;%, while FlatLaf's own scaling goes on top of that.
*
* <p>So on Linux Java2D is pinned to 1 and the whole factor, fractional or not, is
* handed to FlatLaf, which scales fonts and its own metrics; the client scales its own
* sizes through FlatLaf's {@link com.formdev.flatlaf.util.UIScale} to match.
*
* <p>The desktop's factor is read the way an X11 toolkit would learn it:
* <ol>
* <li>{@code Xft/DPI} from XSETTINGS — GNOME and Plasma (through xsettingsd) publish
* the device-pixel DPI there, fractional scale and text scaling included;</li>
* <li>KWin's {@code [Xwayland] Scale}, which Plasma sets when X11 apps are left to
* scale themselves — for sessions with no XSETTINGS daemon running;</li>
* <li>{@code GDK_SCALE}, which Java2D would otherwise have honoured itself.</li>
* </ol>
* With the compositor scaling X11 apps (GNOME's default, and Plasma's "Scaled by the
* system") the client sees 96&nbsp;DPI and is drawn at 100&nbsp;%, which is what those
* modes expect; they then upscale the window, blurry but the right size.
*
* <p>A {@code sun.java2d.uiScale} or {@code flatlaf.uiScale} given on the command line
* turns all of this off.
*/
public final class DisplayScale {
private static final String JAVA2D_SCALE = "sun.java2d.uiScale";
private static final String FLATLAF_SCALE = "flatlaf.uiScale";
private static final double BASE_DPI = 96;
private static final double MAX_SCALE = 4;
private static boolean managed;
private static double detected = 1;
private DisplayScale() {
}
/**
* Sets the scale up; call before anything touches AWT.
*
* @param override the user's chosen factor, or 0 to follow the desktop
*/
public static void configure(double override) {
if (!isLinux() || System.getProperty(JAVA2D_SCALE) != null
|| System.getProperty(FLATLAF_SCALE) != null) {
return;
}
System.setProperty(JAVA2D_SCALE, "1");
managed = true;
detected = detect();
double factor = override > 0 ? override : detected;
System.setProperty(FLATLAF_SCALE, Double.toString(factor));
}
/** Whether the client sets the scale itself, so that the user may choose one. */
public static boolean isManaged() {
return managed;
}
/** The desktop's factor, as found at startup. */
public static double detected() {
return detected;
}
private static double detect() {
double scale = xftScale();
if (scale <= 0) scale = kwinXwaylandScale();
scale = Math.max(scale, gdkScale());
return Math.max(1, Math.min(MAX_SCALE, scale));
}
private static double xftScale() {
// The XSETTINGS value is in 1024ths of a DPI; -1 means "unset".
Object dpi = Toolkit.getDefaultToolkit().getDesktopProperty("gnome.Xft/DPI");
return dpi instanceof Integer d && d > 0 ? d / 1024.0 / BASE_DPI : 0;
}
private static double kwinXwaylandScale() {
// Left behind by a Wayland session, the key would be stale in an X11 one.
if (System.getenv("WAYLAND_DISPLAY") == null) return 0;
String configHome = System.getenv("XDG_CONFIG_HOME");
Path kwinrc = (configHome == null || configHome.isEmpty()
? Path.of(System.getProperty("user.home"), ".config")
: Path.of(configHome)).resolve("kwinrc");
try {
return parseDouble(iniValue(Files.readAllLines(kwinrc), "Xwayland", "Scale"));
} catch (IOException e) {
return 0;
}
}
private static double gdkScale() {
return parseDouble(System.getenv("GDK_SCALE"));
}
/** The value of {@code key} in {@code [group]} of a KConfig-style file, or {@code null}. */
static String iniValue(List<String> lines, String group, String key) {
boolean inGroup = false;
for (String line : lines) {
String t = line.trim();
if (t.startsWith("[")) {
inGroup = t.equals("[" + group + "]");
} else if (inGroup) {
int eq = t.indexOf('=');
if (eq > 0 && t.substring(0, eq).trim().equals(key)) {
return t.substring(eq + 1).trim();
}
}
}
return null;
}
private static double parseDouble(String s) {
if (s == null) return 0;
try {
return Double.parseDouble(s.trim());
} catch (NumberFormatException e) {
return 0;
}
}
private static boolean isLinux() {
return System.getProperty("os.name", "").toLowerCase(Locale.ROOT).startsWith("linux");
}
}

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.JButton; import javax.swing.JButton;
@@ -52,7 +54,7 @@ public final class DropDownToggleButton extends JPanel {
// width comes out of the toggle half instead of being added to it. // width comes out of the toggle half instead of being added to it.
Dimension plain = button.getPreferredSize(); Dimension plain = button.getPreferredSize();
button.setPreferredSize(new Dimension( button.setPreferredSize(new Dimension(
Math.max(icon.getIconWidth() + 8, plain.width - arrow.getPreferredSize().width), Math.max(icon.getIconWidth() + UIScale.scale(8), plain.width - arrow.getPreferredSize().width),
plain.height)); plain.height));
add(button, BorderLayout.CENTER); add(button, BorderLayout.CENTER);
@@ -109,12 +111,12 @@ public final class DropDownToggleButton extends JPanel {
@Override @Override
public int getIconWidth() { public int getIconWidth() {
return 7; return UIScale.scale(7);
} }
@Override @Override
public int getIconHeight() { public int getIconHeight() {
return 4; return UIScale.scale(4);
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ChannelNode; import com.ts3client.net.ChannelNode;
import com.ts3client.net.ClientEntry; import com.ts3client.net.ClientEntry;
import com.ts3client.net.ServerModel; import com.ts3client.net.ServerModel;
@@ -160,14 +161,16 @@ final class DropIndicatorTree extends JTree {
if (highlight != null || loc.getChildIndex() < 0) { if (highlight != null || loc.getChildIndex() < 0) {
Rectangle r = getPathBounds(highlight != null ? highlight : loc.getPath()); Rectangle r = getPathBounds(highlight != null ? highlight : loc.getPath());
if (r != null) { if (r != null) {
g2.setStroke(new BasicStroke(2f)); g2.setStroke(new BasicStroke(UIScale.scale(2f)));
g2.drawRoundRect(r.x, r.y + 1, r.width - 1, r.height - 3, 4, 4); int arc = UIScale.scale(4);
g2.drawRoundRect(r.x, r.y + 1, r.width - 1, r.height - 3, arc, arc);
} }
} else { } else {
Rectangle line = insertLine(loc); Rectangle line = insertLine(loc);
if (line != null) { if (line != null) {
g2.fillRect(line.x, line.y - 1, line.width, 2); int dot = UIScale.scale(7);
g2.fillOval(line.x - 3, line.y - 4, 7, 7); g2.fillRect(line.x, line.y - line.height / 2, line.width, line.height);
g2.fillOval(line.x - dot / 2, line.y - dot / 2 - 1, dot, dot);
} }
} }
g2.dispose(); g2.dispose();
@@ -210,19 +213,19 @@ final class DropIndicatorTree extends JTree {
return obj instanceof ChannelNode && Spacers.isSpacer(((ChannelNode) obj).name); return obj instanceof ChannelNode && Spacers.isSpacer(((ChannelNode) obj).name);
} }
/** The 1px-tall strip where the insertion line goes, in tree coordinates. */ /** The strip where the insertion line goes, in tree coordinates. */
private Rectangle insertLine(JTree.DropLocation loc) { private Rectangle insertLine(JTree.DropLocation loc) {
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) loc.getPath().getLastPathComponent(); DefaultMutableTreeNode parent = (DefaultMutableTreeNode) loc.getPath().getLastPathComponent();
int index = loc.getChildIndex(); int index = loc.getChildIndex();
if (index < parent.getChildCount()) { if (index < parent.getChildCount()) {
Rectangle r = getPathBounds(loc.getPath().pathByAddingChild(parent.getChildAt(index))); Rectangle r = getPathBounds(loc.getPath().pathByAddingChild(parent.getChildAt(index)));
return r == null ? null : new Rectangle(r.x, r.y, getWidth() - r.x, 2); return r == null ? null : new Rectangle(r.x, r.y, getWidth() - r.x, UIScale.scale(2));
} }
if (parent.getChildCount() == 0) { if (parent.getChildCount() == 0) {
Rectangle r = getPathBounds(loc.getPath()); Rectangle r = getPathBounds(loc.getPath());
if (r == null) return null; if (r == null) return null;
int x = r.x + getRowHeight(); int x = r.x + getRowHeight();
return new Rectangle(x, r.y + r.height, getWidth() - x, 2); return new Rectangle(x, r.y + r.height, getWidth() - x, UIScale.scale(2));
} }
// Past the last child: below that child's whole (expanded) subtree. // Past the last child: below that child's whole (expanded) subtree.
TreePath lastChild = loc.getPath().pathByAddingChild(parent.getChildAt(parent.getChildCount() - 1)); TreePath lastChild = loc.getPath().pathByAddingChild(parent.getChildAt(parent.getChildCount() - 1));
@@ -250,7 +253,7 @@ final class DropIndicatorTree extends JTree {
*/ */
private void paintBadges(Graphics g) { private void paintBadges(Graphics g) {
Rectangle visible = getVisibleRect(); Rectangle visible = getVisibleRect();
int right = visible.x + visible.width - BADGE_MARGIN; int right = visible.x + visible.width - UIScale.scale(BADGE_MARGIN);
for (int row = 0; row < getRowCount(); row++) { for (int row = 0; row < getRowCount(); row++) {
Rectangle bounds = getRowBounds(row); Rectangle bounds = getRowBounds(row);
if (bounds == null || bounds.y + bounds.height < visible.y) continue; if (bounds == null || bounds.y + bounds.height < visible.y) continue;
@@ -262,7 +265,7 @@ final class DropIndicatorTree extends JTree {
if (icons.isEmpty()) continue; if (icons.isEmpty()) continue;
GroupIcons.Row strip = new GroupIcons.Row(icons); GroupIcons.Row strip = new GroupIcons.Row(icons);
int x = Math.max(bounds.x + bounds.width + BADGE_GAP, right - strip.getIconWidth()); int x = Math.max(bounds.x + bounds.width + UIScale.scale(BADGE_GAP), right - strip.getIconWidth());
strip.paintIcon(this, g, x, bounds.y + (bounds.height - strip.getIconHeight()) / 2); strip.paintIcon(this, g, x, bounds.y + (bounds.height - strip.getIconHeight()) / 2);
} }
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ChannelNode; import com.ts3client.net.ChannelNode;
import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.TeamspeakConnection;
import com.ts3client.net.filetransfer.FileTransfer; import com.ts3client.net.filetransfer.FileTransfer;
@@ -65,15 +67,15 @@ public final class FileBrowserDialog extends JDialog {
this.channelId = channel.id; this.channelId = channel.id;
this.channelPassword = ""; this.channelPassword = "";
JPanel content = new JPanel(new BorderLayout(0, 8)); JPanel content = new JPanel(new BorderLayout(0, UIScale.scale(8)));
content.setBackground(Theme.windowBg()); content.setBackground(Theme.windowBg());
content.setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); content.setBorder(new ScaledEmptyBorder(10, 12, 10, 12));
content.add(buildToolbar(), BorderLayout.NORTH); content.add(buildToolbar(), BorderLayout.NORTH);
content.add(buildTable(), BorderLayout.CENTER); content.add(buildTable(), BorderLayout.CENTER);
content.add(buildTransfers(), BorderLayout.SOUTH); content.add(buildTransfers(), BorderLayout.SOUTH);
setContentPane(content); setContentPane(content);
setPreferredSize(new Dimension(640, 480)); setPreferredSize(UIScale.scale(new Dimension(640, 480)));
pack(); pack();
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
@@ -98,17 +100,17 @@ public final class FileBrowserDialog extends JDialog {
deleteButton.addActionListener(e -> deleteSelected()); deleteButton.addActionListener(e -> deleteSelected());
bar.add(upButton); bar.add(upButton);
bar.add(Box.createHorizontalStrut(6)); bar.add(Box.createHorizontalStrut(UIScale.scale(6)));
bar.add(refresh); bar.add(refresh);
bar.add(Box.createHorizontalStrut(16)); bar.add(Box.createHorizontalStrut(UIScale.scale(16)));
bar.add(pathLabel); bar.add(pathLabel);
bar.add(Box.createHorizontalGlue()); bar.add(Box.createHorizontalGlue());
bar.add(mkdir); bar.add(mkdir);
bar.add(Box.createHorizontalStrut(6)); bar.add(Box.createHorizontalStrut(UIScale.scale(6)));
bar.add(upload); bar.add(upload);
bar.add(Box.createHorizontalStrut(6)); bar.add(Box.createHorizontalStrut(UIScale.scale(6)));
bar.add(downloadButton); bar.add(downloadButton);
bar.add(Box.createHorizontalStrut(6)); bar.add(Box.createHorizontalStrut(UIScale.scale(6)));
bar.add(deleteButton); bar.add(deleteButton);
pathLabel.setFont(Theme.uiFont()); pathLabel.setFont(Theme.uiFont());
@@ -118,14 +120,14 @@ public final class FileBrowserDialog extends JDialog {
private JScrollPane buildTable() { private JScrollPane buildTable() {
table.setFont(Theme.uiFont()); table.setFont(Theme.uiFont());
table.setRowHeight(20); table.setRowHeight(UIScale.scale(20));
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
table.setFillsViewportHeight(true); table.setFillsViewportHeight(true);
table.getColumnModel().getColumn(0).setPreferredWidth(300); table.getColumnModel().getColumn(0).setPreferredWidth(UIScale.scale(300));
table.getColumnModel().getColumn(0).setCellRenderer(new NameRenderer()); table.getColumnModel().getColumn(0).setCellRenderer(new NameRenderer());
table.getColumnModel().getColumn(1).setPreferredWidth(90); table.getColumnModel().getColumn(1).setPreferredWidth(UIScale.scale(90));
table.getColumnModel().getColumn(2).setPreferredWidth(70); table.getColumnModel().getColumn(2).setPreferredWidth(UIScale.scale(70));
table.getColumnModel().getColumn(3).setPreferredWidth(150); table.getColumnModel().getColumn(3).setPreferredWidth(UIScale.scale(150));
table.getSelectionModel().addListSelectionListener(e -> updateButtons()); table.getSelectionModel().addListSelectionListener(e -> updateButtons());
table.addMouseListener(new MouseAdapter() { table.addMouseListener(new MouseAdapter() {
@Override @Override
@@ -146,7 +148,7 @@ public final class FileBrowserDialog extends JDialog {
transfersPanel.setBackground(Theme.windowBg()); transfersPanel.setBackground(Theme.windowBg());
JScrollPane scroll = new JScrollPane(transfersPanel); JScrollPane scroll = new JScrollPane(transfersPanel);
scroll.setBorder(BorderFactory.createTitledBorder("Transfers")); scroll.setBorder(BorderFactory.createTitledBorder("Transfers"));
scroll.setPreferredSize(new Dimension(10, 120)); scroll.setPreferredSize(UIScale.scale(new Dimension(10, 120)));
scroll.getViewport().setBackground(Theme.windowBg()); scroll.getViewport().setBackground(Theme.windowBg());
return scroll; return scroll;
} }
@@ -325,10 +327,10 @@ public final class FileBrowserDialog extends JDialog {
private FileTransfer transfer; private FileTransfer transfer;
TransferRow() { TransferRow() {
setLayout(new BorderLayout(8, 0)); setLayout(new BorderLayout(UIScale.scale(8), 0));
setBackground(Theme.windowBg()); setBackground(Theme.windowBg());
setBorder(BorderFactory.createEmptyBorder(3, 2, 3, 2)); setBorder(new ScaledEmptyBorder(3, 2, 3, 2));
setMaximumSize(new Dimension(Integer.MAX_VALUE, 44)); setMaximumSize(new Dimension(Integer.MAX_VALUE, UIScale.scale(44)));
label.setFont(Theme.uiFont()); label.setFont(Theme.uiFont());
bar.setStringPainted(true); bar.setStringPainted(true);
add(label, BorderLayout.NORTH); add(label, BorderLayout.NORTH);

View File

@@ -1,6 +1,8 @@
package com.ts3client.ui; package com.ts3client.ui;
import javax.swing.BorderFactory; import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JPanel; import javax.swing.JPanel;
@@ -27,7 +29,7 @@ class FormPanel extends JPanel implements Scrollable {
FormPanel() { FormPanel() {
super(new GridBagLayout()); super(new GridBagLayout());
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); setBorder(new ScaledEmptyBorder(12, 12, 12, 12));
} }
@Override @Override
@@ -57,7 +59,7 @@ class FormPanel extends JPanel implements Scrollable {
static GridBagConstraints gbc() { static GridBagConstraints gbc() {
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(4, 4, 4, 4); c.insets = UIScale.scale(new Insets(4, 4, 4, 4));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
return c; return c;
@@ -81,8 +83,8 @@ class FormPanel extends JPanel implements Scrollable {
*/ */
static void limitWidth(JComponent comp, int preferredWidth) { static void limitWidth(JComponent comp, int preferredWidth) {
int height = comp.getPreferredSize().height; int height = comp.getPreferredSize().height;
comp.setPreferredSize(new Dimension(preferredWidth, height)); comp.setPreferredSize(new Dimension(UIScale.scale(preferredWidth), height));
comp.setMinimumSize(new Dimension(MIN_FIELD_WIDTH, height)); comp.setMinimumSize(new Dimension(UIScale.scale(MIN_FIELD_WIDTH), height));
} }
static JPanel sliderWithLabel(JSlider slider, JLabel valueLabel) { static JPanel sliderWithLabel(JSlider slider, JLabel valueLabel) {
@@ -94,7 +96,7 @@ class FormPanel extends JPanel implements Scrollable {
* the longest value so the slider does not jump around as it is dragged. * the longest value so the slider does not jump around as it is dragged.
*/ */
static JPanel sliderWithLabel(JSlider slider, JLabel valueLabel, int labelWidth) { static JPanel sliderWithLabel(JSlider slider, JLabel valueLabel, int labelWidth) {
JPanel panel = new JPanel(new BorderLayout(6, 0)); JPanel panel = new JPanel(new BorderLayout(UIScale.scale(6), 0));
limitWidth(slider, SLIDER_WIDTH); limitWidth(slider, SLIDER_WIDTH);
panel.add(slider, BorderLayout.CENTER); panel.add(slider, BorderLayout.CENTER);
valueLabel.setPreferredSize(new Dimension(labelWidth, valueLabel.getPreferredSize().height)); valueLabel.setPreferredSize(new Dimension(labelWidth, valueLabel.getPreferredSize().height));

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.Group; import com.ts3client.net.Group;
import com.ts3client.net.IconRepository; import com.ts3client.net.IconRepository;
@@ -42,7 +43,7 @@ public final class GroupIcons {
// its own style; only server-uploaded icons come off the wire. The theme keeps // its own style; only server-uploaded icons come off the wire. The theme keeps
// its own cache, so those are not cached again here. // its own cache, so those are not cached again here.
if (iconId > 0 && iconId <= IconRepository.MAX_BUNDLED_ID) { if (iconId > 0 && iconId <= IconRepository.MAX_BUNDLED_ID) {
ImageIcon themed = IconTheme.icon("GROUP_" + iconId, SIZE); ImageIcon themed = IconTheme.icon("GROUP_" + iconId, UIScale.scale(SIZE));
if (themed != null) return themed; if (themed != null) return themed;
} }
@@ -54,8 +55,9 @@ public final class GroupIcons {
ImageIcon icon = new ImageIcon(data); ImageIcon icon = new ImageIcon(data);
if (icon.getIconWidth() <= 0) return null; if (icon.getIconWidth() <= 0) return null;
if (icon.getIconWidth() != SIZE || icon.getIconHeight() != SIZE) { int size = UIScale.scale(SIZE);
icon = new ImageIcon(icon.getImage().getScaledInstance(SIZE, SIZE, Image.SCALE_SMOOTH)); if (icon.getIconWidth() != size || icon.getIconHeight() != size) {
icon = new ImageIcon(icon.getImage().getScaledInstance(size, size, Image.SCALE_SMOOTH));
} }
decoded.put(iconId, icon); decoded.put(iconId, icon);
return icon; return icon;
@@ -89,15 +91,16 @@ public final class GroupIcons {
int offset = x; int offset = x;
for (Icon icon : icons) { for (Icon icon : icons) {
icon.paintIcon(c, g, offset, y + (getIconHeight() - icon.getIconHeight()) / 2); icon.paintIcon(c, g, offset, y + (getIconHeight() - icon.getIconHeight()) / 2);
offset += icon.getIconWidth() + GAP; offset += icon.getIconWidth() + UIScale.scale(GAP);
} }
} }
@Override @Override
public int getIconWidth() { public int getIconWidth() {
int width = 0; int width = 0;
for (Icon icon : icons) width += icon.getIconWidth() + GAP; int gap = UIScale.scale(GAP);
return Math.max(0, width - GAP); for (Icon icon : icons) width += icon.getIconWidth() + gap;
return Math.max(0, width - gap);
} }
@Override @Override

View File

@@ -1,11 +1,12 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.hotkey.Hotkey; import com.ts3client.hotkey.Hotkey;
import com.ts3client.hotkey.HotkeyAction; import com.ts3client.hotkey.HotkeyAction;
import com.ts3client.hotkey.HotkeyCombo; import com.ts3client.hotkey.HotkeyCombo;
import com.ts3client.hotkey.HotkeyEngine; import com.ts3client.hotkey.HotkeyEngine;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton; import javax.swing.JButton;
@@ -120,8 +121,8 @@ final class HotkeyDialog extends JDialog {
argumentField.setText(hotkey.argument == null ? "" : hotkey.argument); argumentField.setText(hotkey.argument == null ? "" : hotkey.argument);
hint.setFont(hint.getFont().deriveFont(Font.ITALIC, hint.getFont().getSize2D() - 1f)); hint.setFont(hint.getFont().deriveFont(Font.ITALIC, hint.getFont().getSize2D() - 1f));
getContentPane().setLayout(new BorderLayout(8, 8)); getContentPane().setLayout(new BorderLayout(UIScale.scale(8), UIScale.scale(8)));
((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); ((JComponent) getContentPane()).setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
getContentPane().add(buildActionPane(), BorderLayout.CENTER); getContentPane().add(buildActionPane(), BorderLayout.CENTER);
getContentPane().add(buildForm(), BorderLayout.SOUTH); getContentPane().add(buildForm(), BorderLayout.SOUTH);
@@ -129,12 +130,12 @@ final class HotkeyDialog extends JDialog {
updateForAction(); updateForAction();
Dialogs.closeOnEscape(this, this::cancel); Dialogs.closeOnEscape(this, this::cancel);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(new Dimension(470, 560)); setSize(UIScale.scale(new Dimension(470, 560)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }
private JPanel buildActionPane() { private JPanel buildActionPane() {
JPanel p = new JPanel(new BorderLayout(0, 4)); JPanel p = new JPanel(new BorderLayout(0, UIScale.scale(4)));
p.add(new JLabel("Action:"), BorderLayout.NORTH); p.add(new JLabel("Action:"), BorderLayout.NORTH);
p.add(new JScrollPane(tree), BorderLayout.CENTER); p.add(new JScrollPane(tree), BorderLayout.CENTER);
p.add(advancedCheck, BorderLayout.SOUTH); p.add(advancedCheck, BorderLayout.SOUTH);
@@ -144,7 +145,7 @@ final class HotkeyDialog extends JDialog {
private JPanel buildForm() { private JPanel buildForm() {
JPanel p = new JPanel(new GridBagLayout()); JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(3, 3, 3, 3); c.insets = UIScale.scale(new Insets(3, 3, 3, 3));
c.anchor = GridBagConstraints.WEST; c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;

View File

@@ -1,8 +1,9 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.hotkey.Hotkey; import com.ts3client.hotkey.Hotkey;
import javax.swing.BorderFactory;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JPanel; import javax.swing.JPanel;
@@ -34,18 +35,18 @@ final class HotkeysPanel extends JPanel {
private final JTable table = new JTable(model); private final JTable table = new JTable(model);
HotkeysPanel(HotkeyService service) { HotkeysPanel(HotkeyService service) {
super(new BorderLayout(6, 6)); super(new BorderLayout(UIScale.scale(6), UIScale.scale(6)));
this.service = service; this.service = service;
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
synchronized (service.all()) { synchronized (service.all()) {
for (Hotkey h : service.all()) working.add(h.copy()); for (Hotkey h : service.all()) working.add(h.copy());
} }
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(table.getRowHeight() + 4); table.setRowHeight(table.getRowHeight() + UIScale.scale(4));
table.getColumnModel().getColumn(0).setPreferredWidth(240); table.getColumnModel().getColumn(0).setPreferredWidth(UIScale.scale(240));
table.getColumnModel().getColumn(1).setPreferredWidth(150); table.getColumnModel().getColumn(1).setPreferredWidth(UIScale.scale(150));
table.addMouseListener(new MouseAdapter() { table.addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) {

View File

@@ -1,13 +1,16 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.text.BBCode; import com.ts3client.text.BBCode;
import javax.swing.JEditorPane; import javax.swing.JEditorPane;
import javax.swing.JViewport; import javax.swing.JViewport;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.AttributeSet;
import javax.swing.text.html.StyleSheet; import javax.swing.text.html.StyleSheet;
import java.awt.Color; import java.awt.Color;
import java.awt.Font;
/** /**
* The HTML panes showing TeamSpeak text — chat logs, client and channel information — * The HTML panes showing TeamSpeak text — chat logs, client and channel information —
@@ -70,7 +73,7 @@ final class HtmlStyles {
private static void installKit(JEditorPane pane, String html, boolean atEnd) { private static void installKit(JEditorPane pane, String html, boolean atEnd) {
HTMLEditorKit kit = new HTMLEditorKit(); HTMLEditorKit kit = new HTMLEditorKit();
StyleSheet css = new StyleSheet(); StyleSheet css = new ScaledStyleSheet();
css.addStyleSheet(kit.getStyleSheet()); css.addStyleSheet(kit.getStyleSheet());
Object bodyStyle = pane.getClientProperty(BODY_STYLE); Object bodyStyle = pane.getClientProperty(BODY_STYLE);
@@ -102,7 +105,24 @@ final class HtmlStyles {
} }
} }
/** A CSS length of {@code px} at the interface scale. */
static String px(int px) {
return UIScale.scale(px) + "px";
}
private static String hex(Color c) { private static String hex(Color c) {
return String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue()); return String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue());
} }
/**
* Scales every font size, the way a browser scales CSS pixels: Swing takes a
* {@code font-size} in pixels literally, and the text is written in unscaled ones.
*/
private static final class ScaledStyleSheet extends StyleSheet {
@Override
public Font getFont(AttributeSet a) {
Font font = super.getFont(a);
return UIScale.getUserScaleFactor() == 1f ? font : font.deriveFont(UIScale.scale(font.getSize2D()));
}
}
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.IconRepository; import com.ts3client.net.IconRepository;
import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.TeamspeakConnection;
@@ -69,7 +71,7 @@ final class IconChooserDialog extends JDialog {
group("Local", localList, localFooter())); group("Local", localList, localFooter()));
split.setResizeWeight(0.6); split.setResizeWeight(0.6);
status.setBorder(BorderFactory.createEmptyBorder(2, 8, 2, 8)); status.setBorder(new ScaledEmptyBorder(2, 8, 2, 8));
status.setForeground(Theme.chatSystem()); status.setForeground(Theme.chatSystem());
getContentPane().setLayout(new BorderLayout()); getContentPane().setLayout(new BorderLayout());
@@ -81,7 +83,7 @@ final class IconChooserDialog extends JDialog {
Dialogs.closeOnEscape(this); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(new Dimension(520, 360)); setSize(UIScale.scale(new Dimension(520, 360)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
loadRemoteIcons(); loadRemoteIcons();
@@ -101,8 +103,8 @@ final class IconChooserDialog extends JDialog {
list.setLayoutOrientation(JList.HORIZONTAL_WRAP); list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
list.setVisibleRowCount(-1); list.setVisibleRowCount(-1);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setFixedCellWidth(32); list.setFixedCellWidth(UIScale.scale(32));
list.setFixedCellHeight(32); list.setFixedCellHeight(UIScale.scale(32));
list.setCellRenderer(new IconCellRenderer()); list.setCellRenderer(new IconCellRenderer());
list.addListSelectionListener(e -> onSelectionChanged(list)); list.addListSelectionListener(e -> onSelectionChanged(list));
list.addMouseListener(new MouseAdapter() { list.addMouseListener(new MouseAdapter() {
@@ -129,7 +131,7 @@ final class IconChooserDialog extends JDialog {
} }
private JPanel group(String title, JList<Long> list, Component footer) { private JPanel group(String title, JList<Long> list, Component footer) {
JPanel panel = new JPanel(new BorderLayout(0, 4)); JPanel panel = new JPanel(new BorderLayout(0, UIScale.scale(4)));
panel.setBorder(BorderFactory.createTitledBorder(title)); panel.setBorder(BorderFactory.createTitledBorder(title));
panel.add(new JScrollPane(list), BorderLayout.CENTER); panel.add(new JScrollPane(list), BorderLayout.CENTER);
panel.add(footer, BorderLayout.SOUTH); panel.add(footer, BorderLayout.SOUTH);
@@ -150,7 +152,7 @@ final class IconChooserDialog extends JDialog {
private Component localFooter() { private Component localFooter() {
com.ts3client.gfx.IconPack active = IconTheme.get().activePack(); com.ts3client.gfx.IconPack active = IconTheme.get().activePack();
JLabel pack = new JLabel("Icon Pack: " + (active == null ? "none" : active.name())); JLabel pack = new JLabel("Icon Pack: " + (active == null ? "none" : active.name()));
pack.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4)); pack.setBorder(new ScaledEmptyBorder(0, 4, 0, 4));
return pack; return pack;
} }

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.github.weisj.jsvg.SVGDocument; import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.parser.LoaderContext; import com.github.weisj.jsvg.parser.LoaderContext;
import com.github.weisj.jsvg.parser.SVGLoader; import com.github.weisj.jsvg.parser.SVGLoader;
@@ -33,6 +34,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
* *
* <p>Rendering is cached per key and size: vector art is rasterised at the size it * <p>Rendering is cached per key and size: vector art is rasterised at the size it
* is asked for, bitmaps are scaled from the closest resolution the pack ships. * is asked for, bitmaps are scaled from the closest resolution the pack ships.
* {@link #SIZE} and {@link #TOOLBAR_SIZE} are unscaled; sizes passed in are pixels.
*/ */
public final class IconTheme { public final class IconTheme {
@@ -86,12 +88,12 @@ public final class IconTheme {
listeners.add(listener); listeners.add(listener);
} }
/** @return the icon for a TeamSpeak icon key at 16&times;16, or {@code null} if no pack has it */ /** @return the icon for a TeamSpeak icon key at tree-row size, or {@code null} if no pack has it */
public static ImageIcon icon(String key) { public static ImageIcon icon(String key) {
return icon(key, SIZE); return icon(key, UIScale.scale(SIZE));
} }
/** @return the icon for a TeamSpeak icon key, or {@code null} if no pack has it */ /** @return the icon for a TeamSpeak icon key at {@code size} pixels, or {@code null} if no pack has it */
public static ImageIcon icon(String key, int size) { public static ImageIcon icon(String key, int size) {
return INSTANCE.lookup(key, size); return INSTANCE.lookup(key, size);
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import java.awt.BasicStroke; import java.awt.BasicStroke;
import java.awt.Color; import java.awt.Color;
@@ -15,14 +17,25 @@ import java.awt.image.BufferedImage;
* so a machine with no icon pack installed still gets a complete (if plain) set. * so a machine with no icon pack installed still gets a complete (if plain) set.
* Everything else — menu and dialog decoration — simply stays blank without a pack, * Everything else — menu and dialog decoration — simply stays blank without a pack,
* which is why {@link #of(String)} may return {@code null}. * which is why {@link #of(String)} may return {@code null}.
*
* <p>Icons come at the interface's scale; the variants taking a size take it in pixels.
*/ */
public final class Icons { public final class Icons {
/** The box the built-in painters draw in. */
private static final int SZ = IconTheme.SIZE; private static final int SZ = IconTheme.SIZE;
private Icons() { private Icons() {
} }
private static int small() {
return UIScale.scale(IconTheme.SIZE);
}
private static int large() {
return UIScale.scale(IconTheme.TOOLBAR_SIZE);
}
private interface Painter { private interface Painter {
void paint(Graphics2D g); void paint(Graphics2D g);
} }
@@ -44,13 +57,13 @@ public final class Icons {
return null; return null;
} }
/** @return the pack's icon at a given size, or {@code null} when it has none */ /** @return the pack's icon at {@code size} pixels, or {@code null} when it has none */
public static ImageIcon of(String key, int size) { public static ImageIcon of(String key, int size) {
return IconTheme.icon(key, size); return IconTheme.icon(key, size);
} }
private static ImageIcon themed(String key, Painter fallback) { private static ImageIcon themed(String key, Painter fallback) {
return themed(key, SZ, fallback); return themed(key, small(), fallback);
} }
private static ImageIcon themed(String key, int size, Painter fallback) { private static ImageIcon themed(String key, int size, Painter fallback) {
@@ -64,14 +77,14 @@ public final class Icons {
*/ */
private static ImageIcon themed(String[] keys, Painter fallback) { private static ImageIcon themed(String[] keys, Painter fallback) {
for (String key : keys) { for (String key : keys) {
ImageIcon icon = IconTheme.icon(key, SZ); ImageIcon icon = IconTheme.icon(key, small());
if (icon != null) return icon; if (icon != null) return icon;
} }
return make(fallback); return make(fallback);
} }
private static ImageIcon make(Painter p) { private static ImageIcon make(Painter p) {
return make(SZ, p); return make(small(), p);
} }
/** Runs a painter (which draws in a 16&times;16 box) scaled to {@code size}. */ /** Runs a painter (which draws in a 16&times;16 box) scaled to {@code size}. */
@@ -129,7 +142,7 @@ public final class Icons {
// ---- client status icons ---- // ---- client status icons ----
public static ImageIcon clientIdle() { public static ImageIcon clientIdle() {
return clientIdle(SZ); return clientIdle(small());
} }
public static ImageIcon clientIdle(int size) { public static ImageIcon clientIdle(int size) {
@@ -137,7 +150,7 @@ public final class Icons {
} }
public static ImageIcon clientTalking() { public static ImageIcon clientTalking() {
return clientTalking(SZ); return clientTalking(small());
} }
public static ImageIcon clientTalking(int size) { public static ImageIcon clientTalking(int size) {
@@ -145,7 +158,7 @@ public final class Icons {
} }
public static ImageIcon clientAway() { public static ImageIcon clientAway() {
return clientAway(SZ); return clientAway(small());
} }
public static ImageIcon clientAway(int size) { public static ImageIcon clientAway(int size) {
@@ -154,12 +167,12 @@ public final class Icons {
/** A channel commander that is not talking. */ /** A channel commander that is not talking. */
public static ImageIcon clientCommander() { public static ImageIcon clientCommander() {
return clientCommander(SZ); return clientCommander(small());
} }
/** A channel commander that is talking. */ /** A channel commander that is talking. */
public static ImageIcon clientCommanderTalking() { public static ImageIcon clientCommanderTalking() {
return clientCommanderTalking(SZ); return clientCommanderTalking(small());
} }
public static ImageIcon clientQuery() { public static ImageIcon clientQuery() {
@@ -167,7 +180,7 @@ public final class Icons {
} }
public static ImageIcon micMuted() { public static ImageIcon micMuted() {
return micMuted(SZ); return micMuted(small());
} }
public static ImageIcon micMuted(int size) { public static ImageIcon micMuted(int size) {
@@ -175,7 +188,7 @@ public final class Icons {
} }
public static ImageIcon speakerMuted() { public static ImageIcon speakerMuted() {
return speakerMuted(SZ); return speakerMuted(small());
} }
public static ImageIcon speakerMuted(int size) { public static ImageIcon speakerMuted(int size) {
@@ -184,7 +197,7 @@ public final class Icons {
/** The capture device is unavailable (another tab holds it), as opposed to muted. */ /** The capture device is unavailable (another tab holds it), as opposed to muted. */
public static ImageIcon micDisabled() { public static ImageIcon micDisabled() {
return micDisabled(SZ); return micDisabled(small());
} }
public static ImageIcon micDisabled(int size) { public static ImageIcon micDisabled(int size) {
@@ -193,7 +206,7 @@ public final class Icons {
/** The playback device is unavailable, as opposed to muted/deafened. */ /** The playback device is unavailable, as opposed to muted/deafened. */
public static ImageIcon speakerDisabled() { public static ImageIcon speakerDisabled() {
return speakerDisabled(SZ); return speakerDisabled(small());
} }
public static ImageIcon speakerDisabled(int size) { public static ImageIcon speakerDisabled(int size) {
@@ -202,7 +215,7 @@ public final class Icons {
/** TS3's "Local Mic Mute": silenced, but not reported as muted. */ /** TS3's "Local Mic Mute": silenced, but not reported as muted. */
public static ImageIcon micLocalMuted() { public static ImageIcon micLocalMuted() {
return micLocalMuted(SZ); return micLocalMuted(small());
} }
public static ImageIcon micLocalMuted(int size) { public static ImageIcon micLocalMuted(int size) {
@@ -222,32 +235,32 @@ public final class Icons {
// ---- toolbar / action icons ---- // ---- toolbar / action icons ----
public static ImageIcon connect() { public static ImageIcon connect() {
return themed("CONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintConnect); return themed("CONNECT", large(), Icons::paintConnect);
} }
public static ImageIcon disconnect() { public static ImageIcon disconnect() {
return themed("DISCONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintDisconnect); return themed("DISCONNECT", large(), Icons::paintDisconnect);
} }
public static ImageIcon mic() { public static ImageIcon mic() {
return themed("CAPTURE", IconTheme.TOOLBAR_SIZE, Icons::paintMic); return themed("CAPTURE", large(), Icons::paintMic);
} }
public static ImageIcon speaker() { public static ImageIcon speaker() {
return themed("PLAYBACK", IconTheme.TOOLBAR_SIZE, Icons::paintSpeaker); return themed("PLAYBACK", large(), Icons::paintSpeaker);
} }
public static ImageIcon micMutedLarge() { public static ImageIcon micMutedLarge() {
return themed("INPUT_MUTED", IconTheme.TOOLBAR_SIZE, Icons::paintMicMuted); return themed("INPUT_MUTED", large(), Icons::paintMicMuted);
} }
public static ImageIcon speakerMutedLarge() { public static ImageIcon speakerMutedLarge() {
return themed("OUTPUT_MUTED", IconTheme.TOOLBAR_SIZE, Icons::paintSpeakerMuted); return themed("OUTPUT_MUTED", large(), Icons::paintSpeakerMuted);
} }
/** Marks the server that currently owns the microphone. */ /** Marks the server that currently owns the microphone. */
public static ImageIcon micActive() { public static ImageIcon micActive() {
return themed("ACTIVATE_MICROPHONE", IconTheme.TOOLBAR_SIZE, Icons::paintMicActive); return themed("ACTIVATE_MICROPHONE", large(), Icons::paintMicActive);
} }
/** The same marker at tab-label size. */ /** The same marker at tab-label size. */
@@ -262,7 +275,7 @@ public final class Icons {
public static ImageIcon channelClientPair() { public static ImageIcon channelClientPair() {
ImageIcon channel = channel(true); ImageIcon channel = channel(true);
ImageIcon edit = themed("EDIT", Icons::paintEdit); ImageIcon edit = themed("EDIT", Icons::paintEdit);
int gap = 8; int gap = UIScale.scale(8);
int width = channel.getIconWidth() + gap + edit.getIconWidth(); int width = channel.getIconWidth() + gap + edit.getIconWidth();
int height = Math.max(channel.getIconHeight(), edit.getIconHeight()); int height = Math.max(channel.getIconHeight(), edit.getIconHeight());
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
@@ -282,15 +295,15 @@ public final class Icons {
/** The toolbar's away marker. */ /** The toolbar's away marker. */
public static ImageIcon away() { public static ImageIcon away() {
return themed("AWAY", IconTheme.TOOLBAR_SIZE, g -> paintPerson(g, Theme.away())); return themed("AWAY", large(), g -> paintPerson(g, Theme.away()));
} }
public static ImageIcon settings() { public static ImageIcon settings() {
return themed("SETTINGS", IconTheme.TOOLBAR_SIZE, Icons::paintSettings); return themed("SETTINGS", large(), Icons::paintSettings);
} }
public static ImageIcon app() { public static ImageIcon app() {
return app(SZ); return app(small());
} }
public static ImageIcon app(int size) { public static ImageIcon app(int size) {

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Bookmarks; import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityEntry; import com.ts3client.config.IdentityEntry;
import com.ts3client.config.IdentityStore; import com.ts3client.config.IdentityStore;
@@ -7,7 +9,6 @@ import com.ts3client.config.Settings;
import com.ts3client.teamspeak.TeamSpeakImporter; import com.ts3client.teamspeak.TeamSpeakImporter;
import com.ts3client.teamspeak.TeamSpeakSettingsDb; import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListCellRenderer;
@@ -73,12 +74,12 @@ public final class IdentitiesDialog extends JDialog {
list.setVisibleRowCount(10); list.setVisibleRowCount(10);
JScrollPane scroll = new JScrollPane(list); JScrollPane scroll = new JScrollPane(list);
scroll.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); scroll.setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
scroll.setPreferredSize(new Dimension(220, 260)); scroll.setPreferredSize(UIScale.scale(new Dimension(220, 260)));
JPanel buttons = new JPanel(); JPanel buttons = new JPanel();
buttons.setLayout(new BoxLayout(buttons, BoxLayout.Y_AXIS)); buttons.setLayout(new BoxLayout(buttons, BoxLayout.Y_AXIS));
buttons.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 8)); buttons.setBorder(new ScaledEmptyBorder(8, 0, 8, 8));
addButton(buttons, "New…", this::newIdentity); addButton(buttons, "New…", this::newIdentity);
addButton(buttons, "Import…", this::importIdentity); addButton(buttons, "Import…", this::importIdentity);
if (TeamSpeakSettingsDb.locate() != null) { if (TeamSpeakSettingsDb.locate() != null) {
@@ -103,15 +104,15 @@ public final class IdentitiesDialog extends JDialog {
Dialogs.closeOnEscape(this); Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack(); pack();
setMinimumSize(new Dimension(640, 320)); setMinimumSize(UIScale.scale(new Dimension(640, 320)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }
private JPanel buildDetails() { private JPanel buildDetails() {
JPanel form = new JPanel(new GridBagLayout()); JPanel form = new JPanel(new GridBagLayout());
form.setBorder(BorderFactory.createEmptyBorder(12, 4, 12, 8)); form.setBorder(new ScaledEmptyBorder(12, 4, 12, 8));
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(4, 4, 4, 4); c.insets = UIScale.scale(new Insets(4, 4, 4, 4));
c.anchor = GridBagConstraints.NORTHWEST; c.anchor = GridBagConstraints.NORTHWEST;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
@@ -149,7 +150,7 @@ public final class IdentitiesDialog extends JDialog {
b.setMaximumSize(new Dimension(Integer.MAX_VALUE, b.getPreferredSize().height)); b.setMaximumSize(new Dimension(Integer.MAX_VALUE, b.getPreferredSize().height));
b.addActionListener(e -> action.run()); b.addActionListener(e -> action.run());
panel.add(b); panel.add(b);
panel.add(Box.createVerticalStrut(4)); panel.add(Box.createVerticalStrut(UIScale.scale(4)));
} }
private String label(IdentityEntry e) { private String label(IdentityEntry e) {
@@ -335,8 +336,8 @@ public final class IdentitiesDialog extends JDialog {
JDialog progress = new JDialog(this, "Please wait", true); JDialog progress = new JDialog(this, "Please wait", true);
JLabel status = new JLabel(message); JLabel status = new JLabel(message);
JPanel panel = new JPanel(new BorderLayout(8, 8)); JPanel panel = new JPanel(new BorderLayout(UIScale.scale(8), UIScale.scale(8)));
panel.setBorder(BorderFactory.createEmptyBorder(16, 16, 12, 16)); panel.setBorder(new ScaledEmptyBorder(16, 16, 12, 16));
panel.add(status, BorderLayout.CENTER); panel.add(status, BorderLayout.CENTER);
if (cancellable) { if (cancellable) {
JButton cancel = new JButton("Stop"); JButton cancel = new JButton("Stop");

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.contacts.Contact; import com.ts3client.contacts.Contact;
import com.ts3client.net.ChannelNode; import com.ts3client.net.ChannelNode;
import com.ts3client.net.ClientEntry; import com.ts3client.net.ClientEntry;
@@ -65,7 +67,7 @@ public final class InfoPanel extends JPanel {
public InfoPanel() { public InfoPanel() {
super(new BorderLayout()); super(new BorderLayout());
pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); pane.setBorder(new ScaledEmptyBorder(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;
Links.open(e.getDescription(), this); Links.open(e.getDescription(), this);
@@ -82,7 +84,7 @@ public final class InfoPanel extends JPanel {
toggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); toggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
toggle.setToolTipText("Show these details in a chat tab instead"); toggle.setToolTipText("Show these details in a chat tab instead");
toggle.setOpaque(true); toggle.setOpaque(true);
toggle.setBorder(BorderFactory.createEmptyBorder(1, 4, 1, 4)); toggle.setBorder(new ScaledEmptyBorder(1, 4, 1, 4));
toggle.addMouseListener(new MouseAdapter() { toggle.addMouseListener(new MouseAdapter() {
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
@@ -188,7 +190,7 @@ public final class InfoPanel extends JPanel {
private String body() { private String body() {
if (shownChannel != null) return channelBody(shownChannel); if (shownChannel != null) return channelBody(shownChannel);
if (shownClient != null) { if (shownClient != null) {
renderedAvatarBox = inChatTab ? AvatarHtml.MAX_SIZE : avatarBox(); renderedAvatarBox = inChatTab ? UIScale.scale(AvatarHtml.MAX_SIZE) : avatarBox();
return clientBody(shownClient, shownModel, shownIcons, shownContact, shownAvatar, renderedAvatarBox); return clientBody(shownClient, shownModel, shownIcons, shownContact, shownAvatar, renderedAvatarBox);
} }
return "<i class='muted'>Select a channel or client to see details.</i>"; return "<i class='muted'>Select a channel or client to see details.</i>";
@@ -201,9 +203,11 @@ public final class InfoPanel extends JPanel {
*/ */
private int avatarBox() { private int avatarBox() {
int width = scroll.getViewport().getWidth(); int width = scroll.getViewport().getWidth();
if (width <= 0) return AvatarHtml.MAX_SIZE; int max = UIScale.scale(AvatarHtml.MAX_SIZE);
int box = width * 2 / 5 / AVATAR_BOX_STEP * AVATAR_BOX_STEP; if (width <= 0) return max;
return Math.max(AVATAR_MIN_SIZE, Math.min(AvatarHtml.MAX_SIZE, box)); int step = UIScale.scale(AVATAR_BOX_STEP);
int box = width * 2 / 5 / step * step;
return Math.max(UIScale.scale(AVATAR_MIN_SIZE), Math.min(max, box));
} }
private static String channelBody(ChannelNode ch) { private static String channelBody(ChannelNode ch) {
@@ -231,7 +235,7 @@ public final class InfoPanel extends JPanel {
// The avatar sits top-right beside the details, like the official client's info frame. // The avatar sits top-right beside the details, like the official client's info frame.
return "<table width='100%' cellpadding='0' cellspacing='0'><tr>" return "<table width='100%' cellpadding='0' cellspacing='0'><tr>"
+ "<td valign='top'>" + details + "</td>" + "<td valign='top'>" + details + "</td>"
+ "<td valign='top' align='right' style='padding-left:8px'>" + picture + "</td>" + "<td valign='top' align='right' style='padding-left:" + HtmlStyles.px(8) + "'>" + picture + "</td>"
+ "</tr></table>"; + "</tr></table>";
} }
@@ -297,11 +301,11 @@ public final class InfoPanel extends JPanel {
if (group.iconId == 0) return ""; if (group.iconId == 0) return "";
File file = icons.file(group.iconId); File file = icons.file(group.iconId);
if (file == null) return ""; if (file == null) return "";
return "<img src='" + file.toURI() + "' width='14' height='14'> "; return "<img src='" + file.toURI() + "' width='" + UIScale.scale(14) + "' height='" + UIScale.scale(14) + "'> ";
} }
private static String heading(String text) { private static String heading(String text) {
return "<div style='font-weight:bold;font-size:13px;margin-bottom:4px'>" + text + "</div>"; return "<div style='font-weight:bold;font-size:13px;margin-bottom:" + HtmlStyles.px(4) + "'>" + text + "</div>";
} }
private static void row(StringBuilder sb, String label, String value) { private static void row(StringBuilder sb, String label, String value) {

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.JComponent; import javax.swing.JComponent;
import java.awt.Color; import java.awt.Color;
import java.awt.Dimension; import java.awt.Dimension;
@@ -25,11 +27,11 @@ public final class LevelMeter extends JComponent {
private volatile boolean transmitting; private volatile boolean transmitting;
public LevelMeter() { public LevelMeter() {
setPreferredSize(new Dimension(240, 18)); setPreferredSize(UIScale.scale(new Dimension(240, 18)));
// A bare JComponent reports no minimum of its own, so a layout tight on space // 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. // collapses the bar to nothing. Keep the height and let only the width give.
setMinimumSize(new Dimension(60, 18)); setMinimumSize(UIScale.scale(new Dimension(60, 18)));
setMaximumSize(new Dimension(Integer.MAX_VALUE, 18)); setMaximumSize(new Dimension(Integer.MAX_VALUE, UIScale.scale(18)));
} }
public void setLevel(double db) { public void setLevel(double db) {
@@ -70,16 +72,17 @@ public final class LevelMeter extends JComponent {
int h = getHeight(); int h = getHeight();
g.setColor(new Color(0x2B2B2B)); g.setColor(new Color(0x2B2B2B));
g.fillRoundRect(0, 0, w - 1, h - 1, 6, 6); int arc = UIScale.scale(6);
g.fillRoundRect(0, 0, w - 1, h - 1, arc, arc);
int level = dbToX(levelDb, w - 2); int level = dbToX(levelDb, w - 2);
g.setColor(transmitting ? Theme.talking() : new Color(0x5A9BD4)); g.setColor(transmitting ? Theme.talking() : new Color(0x5A9BD4));
g.fillRoundRect(1, 1, Math.max(0, level), h - 3, 5, 5); g.fillRoundRect(1, 1, Math.max(0, level), h - 3, arc - 1, arc - 1);
if (showThreshold) { if (showThreshold) {
int tx = dbToX(thresholdDb, w - 2); int tx = dbToX(thresholdDb, w - 2);
g.setColor(new Color(0xF0C419)); g.setColor(new Color(0xF0C419));
g.fillRect(tx, 1, 2, h - 3); g.fillRect(tx, 1, UIScale.scale(2), h - 3);
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.audio.AudioBackend; import com.ts3client.audio.AudioBackend;
import com.ts3client.audio.desktop.DesktopAudioBackend; import com.ts3client.audio.desktop.DesktopAudioBackend;
import com.ts3client.config.AwayMessages; import com.ts3client.config.AwayMessages;
@@ -14,7 +15,6 @@ import com.ts3client.net.ChannelNode;
import com.ts3client.sound.SoundNotifier; import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPlayer; import com.ts3client.sound.SoundPlayer;
import javax.swing.BorderFactory;
import javax.swing.JCheckBox; import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem; import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame; import javax.swing.JFrame;
@@ -104,7 +104,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
}); });
// Catches Ctrl+C / SIGTERM so we still leave the servers cleanly. // Catches Ctrl+C / SIGTERM so we still leave the servers cleanly.
Runtime.getRuntime().addShutdownHook(shutdownHook); Runtime.getRuntime().addShutdownHook(shutdownHook);
setMinimumSize(new Dimension(720, 480)); setMinimumSize(UIScale.scale(new Dimension(720, 480)));
tray = new TrayController(this, this::quit); tray = new TrayController(this, this::quit);
menuBar = buildMenuBar(); menuBar = buildMenuBar();
@@ -128,7 +128,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
statusTimer = new javax.swing.Timer(3000, e -> updateConnectionStatus()); statusTimer = new javax.swing.Timer(3000, e -> updateConnectionStatus());
statusTimer.start(); statusTimer.start();
setSize(880, 560); setSize(UIScale.scale(new Dimension(880, 560)));
setLocationRelativeTo(null); setLocationRelativeTo(null);
// Deferred so the window is on screen before we start connecting. // Deferred so the window is on screen before we start connecting.
@@ -639,7 +639,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
JTextField labelField = new JTextField(selected.title()); JTextField labelField = new JTextField(selected.title());
JCheckBox joinChannel = new JCheckBox("Join \"" + channelPath + "\" on connect", !channelPath.isEmpty()); JCheckBox joinChannel = new JCheckBox("Join \"" + channelPath + "\" on connect", !channelPath.isEmpty());
JPanel form = new JPanel(new GridLayout(0, 1, 0, 2)); JPanel form = new JPanel(new GridLayout(0, 1, 0, UIScale.scale(2)));
form.add(new JLabel("Bookmark label:")); form.add(new JLabel("Bookmark label:"));
form.add(labelField); form.add(labelField);
if (!channelPath.isEmpty()) form.add(joinChannel); if (!channelPath.isEmpty()) form.add(joinChannel);

View File

@@ -1,8 +1,9 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import javax.swing.BorderFactory;
import javax.swing.Box; import javax.swing.Box;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem; import javax.swing.JCheckBoxMenuItem;
@@ -74,7 +75,7 @@ final class MainToolbar extends JToolBar {
setFloatable(false); setFloatable(false);
setBackground(Theme.toolbarBg()); setBackground(Theme.toolbarBg());
setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); setBorder(new ScaledEmptyBorder(4, 6, 4, 6));
setComponentPopupMenu(buildContextMenu()); setComponentPopupMenu(buildContextMenu());
connectButton = new JButton(Icons.connect()); connectButton = new JButton(Icons.connect());
@@ -152,7 +153,7 @@ final class MainToolbar extends JToolBar {
/** The Master Volume slider: multiplies both voice and notification volume. */ /** The Master Volume slider: multiplies both voice and notification volume. */
private JComponent buildMasterVolumeControl() { private JComponent buildMasterVolumeControl() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0)); JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT, UIScale.scale(4), 0));
panel.setOpaque(false); panel.setOpaque(false);
panel.setAlignmentY(Component.CENTER_ALIGNMENT); panel.setAlignmentY(Component.CENTER_ALIGNMENT);
@@ -164,7 +165,7 @@ final class MainToolbar extends JToolBar {
slider.setOpaque(false); slider.setOpaque(false);
slider.setToolTipText("Master volume (voice + notifications) — scroll to adjust"); slider.setToolTipText("Master volume (voice + notifications) — scroll to adjust");
slider.setAlignmentY(Component.CENTER_ALIGNMENT); slider.setAlignmentY(Component.CENTER_ALIGNMENT);
slider.setPreferredSize(new Dimension(135, slider.getPreferredSize().height)); slider.setPreferredSize(new Dimension(UIScale.scale(135), slider.getPreferredSize().height));
slider.addChangeListener(e -> { slider.addChangeListener(e -> {
settings.masterVolume = slider.getValue() / 100.0; settings.masterVolume = slider.getValue() / 100.0;
listener.onMasterVolumeChanged(); listener.onMasterVolumeChanged();

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ClientEntry; import com.ts3client.net.ClientEntry;
import javax.swing.DefaultCellEditor; import javax.swing.DefaultCellEditor;
@@ -103,7 +104,7 @@ final class NicknameCellEditor extends DefaultTreeCellEditor {
if (!(c instanceof JLabel)) return; if (!(c instanceof JLabel)) return;
editingIcon = ((JLabel) c).getIcon(); editingIcon = ((JLabel) c).getIcon();
offset = editingIcon == null ? 0 offset = editingIcon == null ? 0
: editingIcon.getIconWidth() + renderer.getIconTextGap() - FIELD_NUDGE; : editingIcon.getIconWidth() + renderer.getIconTextGap() - UIScale.scale(FIELD_NUDGE);
} }
/** The edited nickname, trimmed; empty when nothing usable was typed. */ /** The edited nickname, trimmed; empty when nothing usable was typed. */

View File

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

View File

@@ -1,6 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import javax.swing.BorderFactory; import com.formdev.flatlaf.util.ScaledEmptyBorder;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.JLabel; import javax.swing.JLabel;
@@ -54,7 +55,7 @@ final class ReasonDialog {
static JLabel label(String text) { static JLabel label(String text) {
JLabel label = new JLabel(text); JLabel label = new JLabel(text);
label.setAlignmentX(Component.LEFT_ALIGNMENT); label.setAlignmentX(Component.LEFT_ALIGNMENT);
label.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0)); label.setBorder(new ScaledEmptyBorder(0, 0, 4, 0));
return label; return label;
} }

View File

@@ -1,10 +1,11 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ClientEntry; import com.ts3client.net.ClientEntry;
import com.ts3client.net.Group; import com.ts3client.net.Group;
import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.TeamspeakConnection;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout; import javax.swing.BoxLayout;
import javax.swing.GrayFilter; import javax.swing.GrayFilter;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
@@ -48,10 +49,10 @@ final class ServerGroupsDialog extends JDialog {
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS)); list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS));
JScrollPane scroll = new JScrollPane(list); JScrollPane scroll = new JScrollPane(list);
scroll.setPreferredSize(new Dimension(280, 360)); scroll.setPreferredSize(UIScale.scale(new Dimension(280, 360)));
scroll.getVerticalScrollBar().setUnitIncrement(16); scroll.getVerticalScrollBar().setUnitIncrement(16);
header.setBorder(BorderFactory.createEmptyBorder(8, 10, 4, 10)); header.setBorder(new ScaledEmptyBorder(8, 10, 4, 10));
header.setHorizontalAlignment(SwingConstants.LEFT); header.setHorizontalAlignment(SwingConstants.LEFT);
JButton close = new JButton("Close"); JButton close = new JButton("Close");

View File

@@ -1,5 +1,8 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComponent; import javax.swing.JComponent;
@@ -150,9 +153,9 @@ final class ServerTabPane extends JPanel {
/** A tab's label: title, microphone marker and a close button. */ /** A tab's label: title, microphone marker and a close button. */
private JComponent buildTabComponent(ServerTab tab, boolean hasMic) { private JComponent buildTabComponent(ServerTab tab, boolean hasMic) {
JPanel cell = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); JPanel cell = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(4), 0));
cell.setOpaque(false); cell.setOpaque(false);
cell.setBorder(BorderFactory.createEmptyBorder(1, 0, 1, 0)); cell.setBorder(new ScaledEmptyBorder(1, 0, 1, 0));
JLabel label = new JLabel(tab.title(), hasMic ? Icons.micActiveSmall() : null, JLabel.LEADING); JLabel label = new JLabel(tab.title(), hasMic ? Icons.micActiveSmall() : null, JLabel.LEADING);
label.setFont(Theme.uiFont()); label.setFont(Theme.uiFont());
@@ -175,7 +178,7 @@ final class ServerTabPane extends JPanel {
close.setContentAreaFilled(false); close.setContentAreaFilled(false);
close.setMargin(new Insets(0, 0, 0, 0)); close.setMargin(new Insets(0, 0, 0, 0));
close.setForeground(Theme.chatSystem()); close.setForeground(Theme.chatSystem());
close.setPreferredSize(new Dimension(14, 14)); close.setPreferredSize(UIScale.scale(new Dimension(14, 14)));
close.setToolTipText("Close this connection"); close.setToolTipText("Close this connection");
close.addActionListener(e -> listener.closeTab(tab)); close.addActionListener(e -> listener.closeTab(tab));
cell.add(close); cell.add(close);

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import com.ts3client.contacts.ContactStore; import com.ts3client.contacts.ContactStore;
import com.ts3client.contacts.ContactType; import com.ts3client.contacts.ContactType;
@@ -168,7 +169,7 @@ public final class ServerTreePanel extends JScrollPane {
ui.setLeftChildIndent(4); ui.setLeftChildIndent(4);
ui.setRightChildIndent(10); ui.setRightChildIndent(10);
} }
tree.setRowHeight(20); tree.setRowHeight(UIScale.scale(20));
tree.setBackground(Theme.treeBg()); tree.setBackground(Theme.treeBg());
tree.setFont(Theme.uiFont()); tree.setFont(Theme.uiFont());
ServerTreeCellRenderer renderer = new ServerTreeCellRenderer(id -> id == selfClientId, contacts, settings); ServerTreeCellRenderer renderer = new ServerTreeCellRenderer(id -> id == selfClientId, contacts, settings);

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.audio.OpusParameters; import com.ts3client.audio.OpusParameters;
import com.ts3client.audio.VoiceInput; import com.ts3client.audio.VoiceInput;
import com.ts3client.audio.VoiceOutput; import com.ts3client.audio.VoiceOutput;
@@ -92,8 +93,8 @@ public final class SettingsDialog extends JDialog {
}); });
pack(); pack();
setSize(new Dimension(480, 540)); setSize(UIScale.scale(new Dimension(480, 540)));
setMinimumSize(new Dimension(420, 360)); setMinimumSize(UIScale.scale(new Dimension(420, 360)));
setLocationRelativeTo(owner); setLocationRelativeTo(owner);
} }

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.JSpinner; import javax.swing.JSpinner;
import java.awt.Dimension; import java.awt.Dimension;
@@ -19,7 +21,7 @@ final class Spinners {
static JSpinner compact(JSpinner spinner) { static JSpinner compact(JSpinner spinner) {
JSpinner.NumberEditor editor = new JSpinner.NumberEditor(spinner, "#"); JSpinner.NumberEditor editor = new JSpinner.NumberEditor(spinner, "#");
spinner.setEditor(editor); spinner.setEditor(editor);
Dimension size = new Dimension(WIDTH, spinner.getPreferredSize().height); Dimension size = new Dimension(UIScale.scale(WIDTH), spinner.getPreferredSize().height);
spinner.setPreferredSize(size); spinner.setPreferredSize(size);
spinner.setMaximumSize(size); spinner.setMaximumSize(size);
return spinner; return spinner;

View File

@@ -1,6 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import javax.swing.BorderFactory; import com.formdev.flatlaf.util.ScaledEmptyBorder;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JPanel; import javax.swing.JPanel;
import java.awt.BorderLayout; import java.awt.BorderLayout;
@@ -14,7 +15,7 @@ final class StatusBar extends JPanel {
StatusBar() { StatusBar() {
super(new BorderLayout()); super(new BorderLayout());
setBackground(Theme.statusBg()); setBackground(Theme.statusBg());
setBorder(BorderFactory.createEmptyBorder(3, 8, 3, 8)); setBorder(new ScaledEmptyBorder(3, 8, 3, 8));
statusLabel.setFont(Theme.uiFont()); statusLabel.setFont(Theme.uiFont());
codecLabel.setFont(Theme.uiFont()); codecLabel.setFont(Theme.uiFont());
codecLabel.setForeground(Theme.chatSystem()); codecLabel.setForeground(Theme.chatSystem());

View File

@@ -1,6 +1,7 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.UIScale;
import javax.swing.UIManager; import javax.swing.UIManager;
import java.awt.Color; import java.awt.Color;
@@ -36,7 +37,7 @@ public final class Theme {
public static Font uiFont() { public static Font uiFont() {
Font f = UIManager.getFont("Label.font"); Font f = UIManager.getFont("Label.font");
return f != null ? f : new Font("SansSerif", Font.PLAIN, 12); return f != null ? f : new Font("SansSerif", Font.PLAIN, UIScale.scale(12));
} }
public static Font uiBold() { public static Font uiBold() {

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui; package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.audio.InputLevel; import com.ts3client.audio.InputLevel;
import com.ts3client.audio.OpusParameters; import com.ts3client.audio.OpusParameters;
import com.ts3client.audio.VoiceInput; import com.ts3client.audio.VoiceInput;
@@ -105,11 +106,11 @@ final class VoiceActivationPanel extends FormPanel {
c.gridx = 0; c.gridx = 0;
c.gridy = row; c.gridy = row;
c.gridwidth = 2; c.gridwidth = 2;
c.insets = new Insets(10, 4, 2, 4); c.insets = UIScale.scale(new Insets(10, 4, 2, 4));
add(new JLabel("Input level:"), c); add(new JLabel("Input level:"), c);
c.gridy = ++row; c.gridy = ++row;
add(meter, c); add(meter, c);
c.insets = new Insets(4, 4, 4, 4); c.insets = UIScale.scale(new Insets(4, 4, 4, 4));
c.gridy = ++row; c.gridy = ++row;
add(buildTestControls(), c); add(buildTestControls(), c);
c.gridwidth = 1; c.gridwidth = 1;

View File

@@ -0,0 +1,32 @@
package com.ts3client.ui;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class DisplayScaleTest {
private static final List<String> KWINRC = List.of(
"[Plugins]",
"Scale=9",
"",
"[Xwayland]",
"Scale = 1.5",
"",
"[Xwayland][Extra]",
"Scale=2");
@Test
void readsTheKeyFromItsOwnGroupOnly() {
assertEquals("1.5", DisplayScale.iniValue(KWINRC, "Xwayland", "Scale"));
}
@Test
void missingKeyIsNull() {
assertNull(DisplayScale.iniValue(KWINRC, "Xwayland", "Other"));
assertNull(DisplayScale.iniValue(KWINRC, "Compositing", "Scale"));
}
}