diff --git a/ts3-client/README.md b/ts3-client/README.md index fba03c7..0ce1d36 100644 --- a/ts3-client/README.md +++ b/ts3-client/README.md @@ -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 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 - Java 26+ (developed/tested on Temurin 26) - The native Opus library on the system: @@ -269,6 +281,7 @@ swing/ com.ts3client ├── HotkeyActions carries a fired hotkey out on the client ├── NotificationsPanel sound pack + per-action sound/important configuration ├── IconPackPanel icon pack chooser + icon viewer (Options → Design) + ├── DisplayScale interface scale on Linux (fractional, via FlatLaf) ├── ConnectDialog connect form ├── BookmarksDialog manage saved servers ├── IdentitiesDialog manage identities (new/import/export/improve) diff --git a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java index f67a624..45a8dbc 100644 --- a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java +++ b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java @@ -154,6 +154,8 @@ public final class Settings { /** Look-and-feel variant; see {@link Appearance}. */ public Appearance appearance = Appearance.LIGHT; + /** Interface scale factor, 1.0 being 100 %; 0 follows the desktop. Read at startup. */ + public double uiScale = 0; // ---- window chrome ---- /** 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); iconPackDir = props.getProperty("iconPackDir", iconPackDir); appearance = parseAppearance(props.getProperty("appearance"), appearance); + uiScale = parseD(props.getProperty("uiScale"), uiScale); showStatusBar = parseB(props.getProperty("showStatusBar"), showStatusBar); showMasterVolumeSlider = parseB(props.getProperty("showMasterVolumeSlider"), showMasterVolumeSlider); infoSplitRatio = parseD(props.getProperty("infoSplitRatio"), infoSplitRatio); @@ -311,6 +314,7 @@ public final class Settings { props.setProperty("iconPack", iconPack); props.setProperty("iconPackDir", iconPackDir); props.setProperty("appearance", appearance.name()); + props.setProperty("uiScale", Double.toString(uiScale)); props.setProperty("showStatusBar", Boolean.toString(showStatusBar)); props.setProperty("showMasterVolumeSlider", Boolean.toString(showMasterVolumeSlider)); props.setProperty("infoSplitRatio", Double.toString(infoSplitRatio)); diff --git a/ts3-client/swing/src/main/java/com/ts3client/Main.java b/ts3-client/swing/src/main/java/com/ts3client/Main.java index 6aa28a9..687b190 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/Main.java +++ b/ts3-client/swing/src/main/java/com/ts3client/Main.java @@ -1,6 +1,7 @@ package com.ts3client; import com.ts3client.config.Settings; +import com.ts3client.ui.DisplayScale; import com.ts3client.ui.IconTheme; import com.ts3client.ui.LookAndFeelManager; import com.ts3client.ui.MainFrame; @@ -21,6 +22,7 @@ public final class Main { } final Settings settings = Settings.load(); + DisplayScale.configure(settings.uiScale); SwingUtilities.invokeLater(() -> { LookAndFeelManager.install(settings.appearance); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarDialog.java index a1301cc..c588e23 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarDialog.java @@ -1,5 +1,7 @@ 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.TeamspeakConnection; import com.ts3client.net.avatar.Avatar; @@ -39,16 +41,16 @@ final class AvatarDialog extends JDialog { JPanel picture = new JPanel(new BorderLayout()); picture.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createEmptyBorder(8, 8, 4, 8), + new ScaledEmptyBorder(8, 8, 4, 8), BorderFactory.createLineBorder(Theme.border()))); picture.add(preview, BorderLayout.CENTER); JLabel hint = new JLabel("Images up to " + (conn.maxAvatarBytes() / 1024) + " KiB may be uploaded to this server. Animated GIFs stay animated."); 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.setBorder(BorderFactory.createEmptyBorder(2, 8, 0, 8)); + status.setBorder(new ScaledEmptyBorder(2, 8, 0, 8)); JPanel text = new JPanel(); text.setLayout(new BoxLayout(text, BoxLayout.Y_AXIS)); @@ -57,13 +59,13 @@ final class AvatarDialog extends JDialog { JPanel buttons = new JPanel(); 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()); remove.addActionListener(e -> removeAvatar()); JButton close = new JButton("Close"); close.addActionListener(e -> dispose()); buttons.add(choose); - buttons.add(Box.createHorizontalStrut(4)); + buttons.add(Box.createHorizontalStrut(UIScale.scale(4))); buttons.add(remove); buttons.add(Box.createHorizontalGlue()); buttons.add(close); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarHtml.java b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarHtml.java index 9210f74..6cb76e8 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarHtml.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarHtml.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.net.avatar.Avatar; import javax.imageio.ImageIO; @@ -12,7 +13,8 @@ import java.util.Iterator; /** * Renders an avatar as an HTML {@code } 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 - * 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 { @@ -25,7 +27,8 @@ final class AvatarHtml { /** @return the tag, or an empty string if the avatar cannot be shown */ static String imgTag(Avatar avatar, int maxSize) { 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 + "'"; return ""; } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarView.java b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarView.java index cbf4903..dcd4960 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarView.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarView.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.JComponent; import java.awt.Dimension; import java.awt.Graphics; @@ -22,7 +24,7 @@ final class AvatarView extends JComponent { AvatarView(int boxSize) { this.boxSize = boxSize; - setPreferredSize(new Dimension(boxSize, boxSize)); + setPreferredSize(UIScale.scale(new Dimension(boxSize, boxSize))); setMinimumSize(getPreferredSize()); } @@ -47,7 +49,7 @@ final class AvatarView extends JComponent { imageHeight = image.getHeight(this); 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 h = Math.max(1, (int) Math.round(imageHeight * scale)); Graphics2D g2 = (Graphics2D) g.create(); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/AwayMessagesDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/AwayMessagesDialog.java index 34104c4..8e54da6 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/AwayMessagesDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/AwayMessagesDialog.java @@ -1,8 +1,9 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.AwayMessages; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -39,13 +40,13 @@ public final class AwayMessagesDialog extends JDialog { table.setTableHeader(null); table.setShowGrid(false); table.setFillsViewportHeight(true); - table.setRowHeight(Math.max(20, table.getRowHeight())); + table.setRowHeight(Math.max(UIScale.scale(20), table.getRowHeight())); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Single click selects, double click starts editing — like a renameable list. table.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); 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().add(scroll, BorderLayout.CENTER); @@ -53,14 +54,14 @@ public final class AwayMessagesDialog extends JDialog { Dialogs.closeOnEscape(this, this::closeDialog); setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setSize(new Dimension(360, 300)); + setSize(UIScale.scale(new Dimension(360, 300))); setLocationRelativeTo(owner); } private JPanel buildButtons() { JPanel buttons = new JPanel(); 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, "Remove", this::removeSelected); buttons.add(Box.createHorizontalGlue()); @@ -72,7 +73,7 @@ public final class AwayMessagesDialog extends JDialog { JButton b = new JButton(text); b.addActionListener(e -> action.run()); panel.add(b); - panel.add(Box.createHorizontalStrut(4)); + panel.add(Box.createHorizontalStrut(UIScale.scale(4))); } private void addEntry() { diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/BanAddDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/BanAddDialog.java index cd6de71..5cea0f7 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/BanAddDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/BanAddDialog.java @@ -1,11 +1,12 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.BanReasonPresets; import com.ts3client.net.ban.BanEntry; import com.ts3client.net.ban.BanPattern; import com.ts3client.net.ban.BanRule; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -75,24 +76,24 @@ final class BanAddDialog extends JDialog { JPanel form = new JPanel(); 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(ipField); - form.add(Box.createVerticalStrut(8)); + form.add(Box.createVerticalStrut(UIScale.scale(8))); form.add(ReasonDialog.label("Name:")); form.add(nameField); - form.add(Box.createVerticalStrut(8)); + form.add(Box.createVerticalStrut(UIScale.scale(8))); form.add(patternRow()); - form.add(Box.createVerticalStrut(8)); + form.add(Box.createVerticalStrut(UIScale.scale(8))); form.add(ReasonDialog.label("Unique ID:")); form.add(uidField); - form.add(Box.createVerticalStrut(8)); + form.add(Box.createVerticalStrut(UIScale.scale(8))); form.add(ReasonDialog.label("myTeamSpeak ID:")); form.add(mytsidField); - form.add(Box.createVerticalStrut(12)); + form.add(Box.createVerticalStrut(UIScale.scale(12))); form.add(reasonRow()); form.add(reasonField); - form.add(Box.createVerticalStrut(8)); + form.add(Box.createVerticalStrut(UIScale.scale(8))); form.add(ReasonDialog.label("Duration:")); form.add(duration); @@ -119,7 +120,7 @@ final class BanAddDialog extends JDialog { row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS)); row.setAlignmentX(Component.LEFT_ALIGNMENT); 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(Box.createHorizontalGlue()); ReasonDialog.fixHeight(row); @@ -137,13 +138,13 @@ final class BanAddDialog extends JDialog { JPanel row = new JPanel(); row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS)); 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(Box.createHorizontalGlue()); row.add(new JLabel("Preset:")); - row.add(Box.createHorizontalStrut(6)); + row.add(Box.createHorizontalStrut(UIScale.scale(6))); row.add(presetBox); - row.add(Box.createHorizontalStrut(6)); + row.add(Box.createHorizontalStrut(UIScale.scale(6))); row.add(save); ReasonDialog.fixHeight(row); return row; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/BanDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/BanDialog.java index e776518..6b7a688 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/BanDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/BanDialog.java @@ -1,8 +1,9 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.net.ban.BanRule; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -32,12 +33,12 @@ final class BanDialog extends JDialog { JPanel form = new JPanel(); 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(Box.createVerticalStrut(8)); + form.add(Box.createVerticalStrut(UIScale.scale(8))); form.add(ReasonDialog.label("Reason:")); form.add(reasonField); - form.add(Box.createVerticalStrut(8)); + form.add(Box.createVerticalStrut(UIScale.scale(8))); form.add(ReasonDialog.label("Duration:")); form.add(duration); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/BanDurationPicker.java b/ts3-client/swing/src/main/java/com/ts3client/ui/BanDurationPicker.java index 1650ed2..57a5adb 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/BanDurationPicker.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/BanDurationPicker.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -42,9 +44,9 @@ final class BanDurationPicker extends JPanel { max.addActionListener(e -> setSeconds(maxSeconds)); add(amount); - add(Box.createHorizontalStrut(4)); + add(Box.createHorizontalStrut(UIScale.scale(4))); add(unit); - add(Box.createHorizontalStrut(4)); + add(Box.createHorizontalStrut(UIScale.scale(4))); add(max); add(Box.createHorizontalGlue()); ReasonDialog.fixHeight(this); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/BanListDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/BanListDialog.java index 226e72d..268d737 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/BanListDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/BanListDialog.java @@ -1,5 +1,7 @@ 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.Settings; import com.ts3client.net.ServerModel; @@ -8,7 +10,6 @@ import com.ts3client.net.ban.BanEntry; import com.ts3client.net.ban.BanRule; import javax.swing.AbstractAction; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -122,9 +123,9 @@ final class BanListDialog extends JDialog { this.settings = settings; 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.setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); + content.setBorder(new ScaledEmptyBorder(10, 12, 10, 12)); content.add(buildToolbar(), BorderLayout.NORTH); content.add(buildTable(), BorderLayout.CENTER); content.add(buildBottom(), BorderLayout.SOUTH); @@ -133,7 +134,7 @@ final class BanListDialog extends JDialog { serverNameChanged(); setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setPreferredSize(new Dimension(920, 460)); + setPreferredSize(UIScale.scale(new Dimension(920, 460))); pack(); setLocationRelativeTo(owner); reload(); @@ -172,18 +173,18 @@ final class BanListDialog extends JDialog { searchField.setMaximumSize(new Dimension(Integer.MAX_VALUE, searchField.getPreferredSize().height)); bar.add(addButton); - bar.add(Box.createHorizontalStrut(6)); + bar.add(Box.createHorizontalStrut(UIScale.scale(6))); bar.add(removeButton); - bar.add(Box.createHorizontalStrut(6)); + bar.add(Box.createHorizontalStrut(UIScale.scale(6))); bar.add(editButton); - bar.add(Box.createHorizontalStrut(16)); + bar.add(Box.createHorizontalStrut(UIScale.scale(16))); bar.add(searchField); return bar; } private JScrollPane buildTable() { table.setFont(Theme.uiFont()); - table.setRowHeight(20); + table.setRowHeight(UIScale.scale(20)); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.setFillsViewportHeight(true); table.setAutoCreateColumnsFromModel(false); @@ -248,9 +249,9 @@ final class BanListDialog extends JDialog { options.setLayout(new BoxLayout(options, BoxLayout.X_AXIS)); options.setOpaque(false); options.add(reload); - options.add(Box.createHorizontalStrut(12)); + options.add(Box.createHorizontalStrut(UIScale.scale(12))); options.add(onlyOwn); - options.add(Box.createHorizontalStrut(12)); + options.add(Box.createHorizontalStrut(UIScale.scale(12))); options.add(highlightOwn); options.add(Box.createHorizontalGlue()); @@ -260,7 +261,7 @@ final class BanListDialog extends JDialog { closeRow.setOpaque(false); 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.add(status, BorderLayout.NORTH); bottom.add(options, BorderLayout.CENTER); @@ -659,7 +660,7 @@ final class BanListDialog extends JDialog { g2.setColor(Theme.chatEvent()); g2.setFont(getFont().deriveFont(Font.ITALIC)); 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(); } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/BookmarksDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/BookmarksDialog.java index 933ef34..b76edcf 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/BookmarksDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/BookmarksDialog.java @@ -1,5 +1,7 @@ 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.Bookmarks; import com.ts3client.config.IdentityStore; @@ -7,7 +9,6 @@ import com.ts3client.config.Settings; import com.ts3client.teamspeak.TeamSpeakImporter; import com.ts3client.teamspeak.TeamSpeakSettingsDb; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultListModel; @@ -81,10 +82,10 @@ public final class BookmarksDialog extends JDialog { details = buildDetails(); 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); - split.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); + split.setBorder(new ScaledEmptyBorder(8, 8, 4, 8)); split.setResizeWeight(0.3); split.setContinuousLayout(true); @@ -105,7 +106,7 @@ public final class BookmarksDialog extends JDialog { persist(); } }); - setSize(new Dimension(640, 400)); + setSize(UIScale.scale(new Dimension(640, 400))); setLocationRelativeTo(owner); } @@ -113,7 +114,7 @@ public final class BookmarksDialog extends JDialog { private JPanel buildDetails() { 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. " + "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) { GridBagConstraints c = new GridBagConstraints(); 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.gridx = 0; panel.add(new JLabel(text), c); @@ -156,7 +157,7 @@ public final class BookmarksDialog extends JDialog { private JPanel buildButtons() { JPanel buttons = new JPanel(); 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, "Remove", this::removeSelected); if (TeamSpeakSettingsDb.locate() != null) { @@ -174,7 +175,7 @@ public final class BookmarksDialog extends JDialog { JButton b = new JButton(text); b.addActionListener(e -> action.run()); panel.add(b); - panel.add(Box.createHorizontalStrut(4)); + panel.add(Box.createHorizontalStrut(UIScale.scale(4))); return b; } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAdvancedPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAdvancedPanel.java index 5084af6..0ed4399 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAdvancedPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAdvancedPanel.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.net.ChannelSettings; import javax.swing.BorderFactory; @@ -47,7 +49,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab { ChannelAdvancedPanel() { 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."); deleteDelay.setToolTipText("

Delay in seconds after temporary channels are deleted" @@ -79,12 +81,12 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab { other.setAlignmentX(0f); fixHeight(other); add(other); - add(Box.createVerticalStrut(6)); + add(Box.createVerticalStrut(UIScale.scale(6))); JPanel limits = new JPanel(); limits.setLayout(new BoxLayout(limits, BoxLayout.X_AXIS)); 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", new JRadioButton[]{familyInherited, familyUnlimited, familyLimited}, familyMaxUsers)); limits.setAlignmentX(0f); @@ -140,7 +142,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab { JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(BorderFactory.createTitledBorder("Other Settings")); 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.fill = GridBagConstraints.HORIZONTAL; @@ -155,7 +157,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab { c.gridy = 1; c.weightx = 0; 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(deleteDelayMax); c.gridx = 1; @@ -183,7 +185,7 @@ final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab { box.add(radio); 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.add(radio); row.add(spinner); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAudioPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAudioPanel.java index d7ccf79..46479b9 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAudioPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelAudioPanel.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.net.ChannelSettings; import javax.swing.BorderFactory; @@ -49,8 +51,8 @@ final class ChannelAudioPanel extends JPanel implements ChannelDialog.Tab { private final JPanel customSettings = new JPanel(new GridBagLayout()); ChannelAudioPanel() { - super(new BorderLayout(8, 8)); - setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + super(new BorderLayout(UIScale.scale(8), UIScale.scale(8))); + setBorder(new ScaledEmptyBorder(8, 8, 8, 8)); ButtonGroup presets = new ButtonGroup(); presets.add(voiceMobile); @@ -126,7 +128,7 @@ final class ChannelAudioPanel extends JPanel implements ChannelDialog.Tab { private JPanel customBox() { customSettings.setBorder(BorderFactory.createTitledBorder("Custom Settings")); 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.fill = GridBagConstraints.HORIZONTAL; @@ -158,7 +160,7 @@ final class ChannelAudioPanel extends JPanel implements ChannelDialog.Tab { } 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(bandwidth, BorderLayout.CENTER); return row; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelDialog.java index 2f26f11..293c147 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelDialog.java @@ -1,10 +1,11 @@ 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.ChannelSettings; import com.ts3client.net.TeamspeakConnection; -import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; 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 // 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(buttons(), BorderLayout.SOUTH); - getContentPane().setLayout(new BorderLayout(0, 6)); + getContentPane().setLayout(new BorderLayout(0, UIScale.scale(6))); getContentPane().add(header(), BorderLayout.CENTER); getContentPane().add(bottom, BorderLayout.SOUTH); setEnabledForLoading(false); Dialogs.closeOnEscape(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setSize(new Dimension(560, 620)); - setMinimumSize(new Dimension(480, 520)); + setSize(UIScale.scale(new Dimension(560, 620))); + setMinimumSize(UIScale.scale(new Dimension(480, 520))); setLocationRelativeTo(owner); if (channel != null) load(); @@ -167,9 +168,9 @@ final class ChannelDialog extends JDialog { private JPanel header() { 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(); - c.insets = new Insets(3, 4, 3, 4); + c.insets = UIScale.scale(new Insets(3, 4, 3, 4)); c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL; @@ -248,7 +249,7 @@ final class ChannelDialog extends JDialog { private JPanel iconField() { 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.addActionListener(a -> chooseIcon()); iconButton.addMouseListener(new MouseAdapter() { @@ -262,7 +263,7 @@ final class ChannelDialog extends JDialog { 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(iconButton, BorderLayout.CENTER); return row; @@ -273,7 +274,7 @@ final class ChannelDialog extends JDialog { ok.addActionListener(a -> save()); cancel.addActionListener(a -> dispose()); 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(); right.add(ok); right.add(cancel); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java index 46b8ab1..00e978b 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java @@ -1,5 +1,8 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; @@ -49,18 +52,18 @@ final class ChannelPermissionsPanel extends JPanel { ChannelPermissionsPanel() { 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(); groups.setLayout(new BoxLayout(groups, BoxLayout.X_AXIS)); 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.setAlignmentX(0f); add(groups); status.setAlignmentX(0f); - status.setBorder(BorderFactory.createEmptyBorder(6, 2, 0, 2)); + status.setBorder(new ScaledEmptyBorder(6, 2, 0, 2)); add(status); add(Box.createVerticalGlue()); setEditable(false); @@ -119,7 +122,7 @@ final class ChannelPermissionsPanel extends JPanel { JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(BorderFactory.createTitledBorder(title)); 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.fill = GridBagConstraints.HORIZONTAL; int row = 0; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelStandardPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelStandardPanel.java index 3f4aea7..8a09d60 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelStandardPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelStandardPanel.java @@ -1,5 +1,7 @@ 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.ChannelSettings; @@ -45,8 +47,8 @@ final class ChannelStandardPanel extends JPanel implements ChannelDialog.Tab { * excluding the channel being edited */ ChannelStandardPanel(List siblings) { - super(new BorderLayout(8, 8)); - setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + super(new BorderLayout(UIScale.scale(8), UIScale.scale(8))); + setBorder(new ScaledEmptyBorder(8, 8, 8, 8)); temporary.setToolTipText("Channel will be deleted when the last user left."); 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)); sortAfter.setModel(order); 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); add(typeBox(), BorderLayout.WEST); @@ -139,10 +141,10 @@ final class ChannelStandardPanel extends JPanel implements ChannelDialog.Tab { } JSeparator line = new JSeparator(); line.setAlignmentX(0f); - line.setMaximumSize(new Dimension(Integer.MAX_VALUE, 8)); - box.add(Box.createVerticalStrut(4)); + line.setMaximumSize(new Dimension(Integer.MAX_VALUE, UIScale.scale(8))); + box.add(Box.createVerticalStrut(UIScale.scale(4))); box.add(line); - box.add(Box.createVerticalStrut(4)); + box.add(Box.createVerticalStrut(UIScale.scale(4))); defaultChannel.setAlignmentX(0f); box.add(defaultChannel); 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.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")); JLabel label = new JLabel("Needed Talk Power:"); 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.fill = GridBagConstraints.HORIZONTAL; 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(moderation, c); c.weighty = 1; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java index a1d7ee0..b274786 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java @@ -1,5 +1,7 @@ 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.TsLink; @@ -96,8 +98,8 @@ public final class ChatPanel extends JPanel { dragReorder.attach(tabs); add(tabs, BorderLayout.CENTER); - JPanel bottom = new JPanel(new BorderLayout(4, 0)); - bottom.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0)); + JPanel bottom = new JPanel(new BorderLayout(UIScale.scale(4), 0)); + bottom.setBorder(new ScaledEmptyBorder(4, 0, 0, 0)); bottom.add(input, BorderLayout.CENTER); add(bottom, BorderLayout.SOUTH); @@ -345,7 +347,8 @@ public final class ChatPanel extends JPanel { final int clientId; private String clientUniqueId; 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; /** Set for a static-content tab (e.g. a moved-out description); null for a conversation. */ final String noteKey; @@ -408,17 +411,17 @@ public final class ChatPanel extends JPanel { Component header(boolean closable) { // 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). - JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 0)) { + JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(2), 0)) { @Override public Dimension 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); titleLabel = new JLabel(title, icon, JLabel.LEADING); titleLabel.setFont(Theme.uiFont()); - titleLabel.setIconTextGap(3); + titleLabel.setIconTextGap(UIScale.scale(3)); p.add(titleLabel); dragReorder.attach(p); dragReorder.attach(titleLabel); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVersionPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVersionPanel.java index 5073e13..fa002af 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVersionPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVersionPanel.java @@ -1,9 +1,10 @@ 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.Settings; -import javax.swing.BorderFactory; import javax.swing.DefaultListCellRenderer; import javax.swing.JCheckBox; import javax.swing.JComboBox; @@ -45,10 +46,10 @@ final class ClientVersionPanel extends JPanel { private boolean filling; ClientVersionPanel(Settings settings) { - super(new BorderLayout(0, 8)); + super(new BorderLayout(0, UIScale.scale(8))); this.settings = settings; - setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + setBorder(new ScaledEmptyBorder(12, 12, 12, 12)); add(buildForm(), BorderLayout.NORTH); load(); @@ -67,7 +68,7 @@ final class ClientVersionPanel extends JPanel { private JComponent buildForm() { JPanel p = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets(2, 2, 2, 2); + c.insets = UIScale.scale(new Insets(2, 2, 2, 2)); c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL; @@ -110,7 +111,7 @@ final class ClientVersionPanel extends JPanel { c.gridx = 0; c.gridy = row; c.gridwidth = 2; - p.add(new JLabel("The three fields belong together: the " + p.add(new JLabel("The three fields belong together: the " + "signature is TeamSpeak's, over that version and operating system. A server " + "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 " diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVolumeDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVolumeDialog.java index 754ce63..6f00aec 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVolumeDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVolumeDialog.java @@ -1,9 +1,10 @@ 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.TeamspeakConnection; -import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; @@ -41,11 +42,11 @@ final class ClientVolumeDialog extends JDialog { slider.addChangeListener(e -> onChanged()); FormPanel.limitWidth(value, 60); - JPanel content = new JPanel(new BorderLayout(8, 8)); - content.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12)); + JPanel content = new JPanel(new BorderLayout(UIScale.scale(8), UIScale.scale(8))); + content.setBorder(new ScaledEmptyBorder(12, 12, 8, 12)); content.add(new JLabel("Volume Modifier"), BorderLayout.NORTH); content.add(FormPanel.sliderWithLabel(slider, value), BorderLayout.CENTER); - JLabel hint = new JLabel("Values greater than +6 dB will cause clipping " + JLabel hint = new JLabel("Values greater than +6 dB will cause clipping " + "and distortion that will negatively affect your audio quality. It is advised to choose " + "lower values."); hint.setForeground(Theme.chatSystem()); @@ -67,7 +68,7 @@ final class ClientVolumeDialog extends JDialog { Dialogs.closeOnEscape(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); pack(); - setMinimumSize(new Dimension(360, getHeight())); + setMinimumSize(new Dimension(UIScale.scale(360), getHeight())); setLocationRelativeTo(owner); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectDialog.java index a7d2dca..8cae869 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectDialog.java @@ -1,9 +1,10 @@ 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.Settings; -import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; @@ -49,9 +50,9 @@ public final class ConnectDialog extends JDialog { identityChooser = new IdentityChooser(identities, false, settings.defaultIdentityId); 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(); - c.insets = new Insets(4, 4, 4, 4); + c.insets = UIScale.scale(new Insets(4, 4, 4, 4)); c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL; @@ -83,7 +84,7 @@ public final class ConnectDialog extends JDialog { Dialogs.closeOnEscape(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); pack(); - setMinimumSize(new Dimension(340, getHeight())); + setMinimumSize(new Dimension(UIScale.scale(340), getHeight())); setLocationRelativeTo(owner); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java index f709051..e3c28b5 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java @@ -1,9 +1,10 @@ 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.TeamspeakConnection; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -62,9 +63,9 @@ public final class ConnectionInfoDialog extends JDialog { this.conn = conn; 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.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14)); + content.setBorder(new ScaledEmptyBorder(12, 14, 12, 14)); content.add(buildSummary(), BorderLayout.NORTH); content.add(buildTabs(), BorderLayout.CENTER); content.add(buildButtons(), BorderLayout.SOUTH); @@ -138,7 +139,7 @@ public final class ConnectionInfoDialog extends JDialog { lc.gridx = 0; lc.gridy = row; 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); key.setFont(Theme.uiFont()); key.setForeground(new Color(0x5A6B7B)); @@ -150,7 +151,7 @@ public final class ConnectionInfoDialog extends JDialog { vc.weightx = 1; vc.anchor = GridBagConstraints.WEST; 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); } @@ -256,7 +257,7 @@ public final class ConnectionInfoDialog extends JDialog { KindTab() { super(new GridBagLayout()); setBackground(Theme.windowBg()); - setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); + setBorder(new ScaledEmptyBorder(10, 12, 10, 12)); int row = 0; addRow(this, row++, "Packet loss", packetLoss); addRow(this, row++, "Packets (↑/↓)", packets); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ContactDefaultsDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ContactDefaultsDialog.java index 1c05ba3..206845e 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ContactDefaultsDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ContactDefaultsDialog.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; import com.ts3client.contacts.ContactDefaults; import com.ts3client.contacts.ContactType; @@ -50,13 +52,13 @@ final class ContactDefaultsDialog extends JDialog { JPanel content = new JPanel(); 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"); intro.setAlignmentX(LEFT_ALIGNMENT); content.add(intro); - content.add(javax.swing.Box.createVerticalStrut(10)); + content.add(javax.swing.Box.createVerticalStrut(UIScale.scale(10))); content.add(buildFlags()); - content.add(javax.swing.Box.createVerticalStrut(10)); + content.add(javax.swing.Box.createVerticalStrut(UIScale.scale(10))); content.add(buildDisplays()); JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); @@ -157,7 +159,7 @@ final class ContactDefaultsDialog extends JDialog { GridBagConstraints c = new GridBagConstraints(); c.gridx = x; 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; return c; } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsDialog.java index 8df73e1..e066840 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsDialog.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; import com.ts3client.contacts.Contact; import com.ts3client.contacts.ContactStore; @@ -8,7 +10,6 @@ import com.ts3client.contacts.NicknameDisplay; import com.ts3client.teamspeak.TeamSpeakSettingsDb; import com.ts3client.text.TsLink; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; @@ -99,7 +100,7 @@ final class ContactsDialog extends JDialog { private final JCheckBox ignoreAway = new JCheckBox("Hide away message"); private final JRadioButton allowWhisper = new JRadioButton("Allow 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. */ private boolean filling; @@ -110,22 +111,22 @@ final class ContactsDialog extends JDialog { this.settings = settings; buildTree(); - JPanel left = new JPanel(new BorderLayout(0, 4)); - left.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 4)); + JPanel left = new JPanel(new BorderLayout(0, UIScale.scale(4))); + left.setBorder(new ScaledEmptyBorder(8, 8, 8, 4)); JLabel hint = new JLabel("Drag & Drop users here"); hint.setForeground(Theme.chatSystem()); left.add(hint, BorderLayout.NORTH); JScrollPane scroll = new JScrollPane(tree); - scroll.setPreferredSize(new Dimension(300, 340)); + scroll.setPreferredSize(UIScale.scale(new Dimension(300, 340))); 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(buildPages(), BorderLayout.CENTER); JPanel buttons = new JPanel(new BorderLayout()); - buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8)); - JPanel leftButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + buttons.setBorder(new ScaledEmptyBorder(0, 8, 8, 8)); + JPanel leftButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(6), 0)); JButton defaults = new JButton("Set Defaults"); defaults.setToolTipText("Set default display, ignore and hide options"); defaults.addActionListener(e -> new ContactDefaultsDialog(this, settings).setVisible(true)); @@ -160,7 +161,7 @@ final class ContactsDialog extends JDialog { Dialogs.closeOnEscape(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); pack(); - setMinimumSize(new Dimension(620, 400)); + setMinimumSize(UIScale.scale(new Dimension(620, 400))); setLocationRelativeTo(owner); } @@ -172,7 +173,7 @@ final class ContactsDialog extends JDialog { root.add(neutral); tree.setRootVisible(false); tree.setShowsRootHandles(true); - tree.setRowHeight(20); + tree.setRowHeight(UIScale.scale(20)); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setCellRenderer(new ContactRenderer()); tree.addTreeSelectionListener(e -> showSelected()); @@ -301,7 +302,7 @@ final class ContactsDialog extends JDialog { private JComponent buildNicknameRow() { JPanel row = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets(2, 2, 2, 2); + c.insets = UIScale.scale(new Insets(2, 2, 2, 2)); c.anchor = GridBagConstraints.WEST; c.gridy = 0; c.gridx = 0; @@ -325,7 +326,7 @@ final class ContactsDialog extends JDialog { JTabbedPane pages = new JTabbedPane(); pages.addTab("General", page( radioGroup(neutralRadio, blockedRadio, friendRadio), - Box.createVerticalStrut(8), + Box.createVerticalStrut(UIScale.scale(8)), labelled("Phonetic Nickname:", phoneticField))); neutralRadio.setToolTipText("Mark as neutral"); blockedRadio.setToolTipText("Mark as blocked"); @@ -375,7 +376,7 @@ final class ContactsDialog extends JDialog { private static JPanel page(Component... rows) { JPanel p = new JPanel(); 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) { if (row instanceof JComponent) ((JComponent) row).setAlignmentX(Component.LEFT_ALIGNMENT); p.add(row); @@ -397,7 +398,7 @@ final class ContactsDialog extends JDialog { } 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(field, BorderLayout.CENTER); p.setMaximumSize(new Dimension(Integer.MAX_VALUE, field.getPreferredSize().height)); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsPanel.java index f1e74e2..76d5d04 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ContactsPanel.java @@ -1,8 +1,9 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; -import javax.swing.BorderFactory; import javax.swing.DefaultListCellRenderer; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -32,11 +33,11 @@ final class ContactsPanel extends JPanel { ContactsPanel(Settings settings) { super(new BorderLayout()); this.settings = settings; - setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + setBorder(new ScaledEmptyBorder(12, 12, 12, 12)); JPanel p = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets(2, 2, 2, 2); + c.insets = UIScale.scale(new Insets(2, 2, 2, 2)); c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 0; @@ -48,10 +49,10 @@ final class ContactsPanel extends JPanel { p.add(colorContacts, c); 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); 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.setRenderer(new DefaultListCellRenderer() { @Override @@ -68,7 +69,7 @@ final class ContactsPanel extends JPanel { p.add(whisperPolicy, c); 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; JButton defaults = new JButton("Contact defaults…"); defaults.setToolTipText("Configure default settings when adding a client to the Contact Manager"); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java index 455aaf2..16ab826 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java @@ -1,8 +1,9 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.text.BBCode; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JColorChooser; @@ -47,19 +48,19 @@ final class DescriptionEditorDialog extends JDialog { area.setText(description); area.setLineWrap(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); preview.setEditable(false); 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(preview), PREVIEW_CARD); JLabel hint = new JLabel("Press Button to \"Preview\" the changes in channel info."); 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().add(buildToolbar(), BorderLayout.NORTH); @@ -71,8 +72,8 @@ final class DescriptionEditorDialog extends JDialog { Dialogs.closeOnEscape(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setSize(new Dimension(520, 380)); - setMinimumSize(new Dimension(360, 260)); + setSize(UIScale.scale(new Dimension(520, 380))); + setMinimumSize(UIScale.scale(new Dimension(360, 260))); 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. */ 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.setPreferredSize(size); button.setMaximumSize(size); @@ -162,7 +163,7 @@ final class DescriptionEditorDialog extends JDialog { private void showPreview(boolean on) { previewButton.setSelected(on); if (on) { - preview.setText("" + preview.setText("" + BBCode.toHtml(area.getText()) + ""); preview.setCaretPosition(0); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DesignPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DesignPanel.java index 84ea932..cfab7e0 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/DesignPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DesignPanel.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; import com.ts3client.gfx.IconPack; @@ -24,14 +26,15 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; 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 - * pack the user interface is drawn with — where to look for more of them, and a viewer - * showing everything the active pack contains. + * Options page for how the client looks: the light or dark look-and-feel, the interface + * scale, and the icon pack the user interface is drawn with — where to look for more of + * them, and a viewer showing everything the active pack contains. * - *

Both apply right away so the change can be seen in the window behind the dialog; - * cancelling puts the previous ones back. + *

Theme and pack apply right away so the change can be seen in the window behind the + * dialog; cancelling puts the previous ones back. The scale is only read at startup. */ final class DesignPanel extends JPanel { @@ -42,6 +45,9 @@ final class DesignPanel extends JPanel { /** Stands for "no pack": the icons the client draws itself. */ 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 String originalPackId; private final String originalPackDir; @@ -49,6 +55,7 @@ final class DesignPanel extends JPanel { private final JComboBox appearanceCombo = new JComboBox<>(Settings.Appearance.values()); + private final JComboBox scaleCombo = new JComboBox<>(); private final JComboBox packCombo = new JComboBox<>(); private final JLabel packInfo = new JLabel(); private final JTextField packDirField; @@ -56,14 +63,14 @@ final class DesignPanel extends JPanel { private final JList preview = new JList<>(previewModel); DesignPanel(Settings settings) { - super(new BorderLayout(0, 8)); + super(new BorderLayout(0, UIScale.scale(8))); this.settings = settings; this.originalPackId = settings.iconPack; this.originalPackDir = settings.iconPackDir; this.originalAppearance = settings.appearance; 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(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. */ void apply() { settings.appearance = selectedAppearance(); + if (scaleCombo.getSelectedItem() instanceof Double scale) settings.uiScale = scale; settings.iconPackDir = packDirField.getText().trim(); Object selected = packCombo.getSelectedItem(); settings.iconPack = selected instanceof IconPack pack ? pack.id() : ""; @@ -94,7 +102,7 @@ final class DesignPanel extends JPanel { private JComponent buildHeader() { JPanel p = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets(2, 2, 2, 2); + c.insets = UIScale.scale(new Insets(2, 2, 2, 2)); c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL; @@ -125,13 +133,17 @@ final class DesignPanel extends JPanel { int row = 0; 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); c.gridx = 1; c.gridy = row++; packInfo.setEnabled(false); 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…"); browse.addActionListener(e -> browseForPackFolder()); dirRow.add(packDirField, BorderLayout.CENTER); @@ -141,18 +153,42 @@ final class DesignPanel extends JPanel { 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() { preview.setLayoutOrientation(JList.HORIZONTAL_WRAP); preview.setVisibleRowCount(-1); - preview.setFixedCellWidth(TILE); - preview.setFixedCellHeight(TILE); + preview.setFixedCellWidth(UIScale.scale(TILE)); + preview.setFixedCellHeight(UIScale.scale(TILE)); preview.setCellRenderer(new DefaultListCellRenderer() { @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean selected, boolean focus) { super.getListCellRendererComponent(list, value, index, selected, focus); String key = String.valueOf(value); - ImageIcon icon = Icons.of(key, PREVIEW_SIZE); + ImageIcon icon = Icons.of(key, UIScale.scale(PREVIEW_SIZE)); setText(""); setIcon(icon); setToolTipText(key); @@ -162,7 +198,7 @@ final class DesignPanel extends JPanel { }); 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")); return scroll; } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DevicesPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DevicesPanel.java index f7fc663..9b87b67 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/DevicesPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DevicesPanel.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.audio.VoiceInput; import com.ts3client.audio.VoiceOutput; import com.ts3client.audio.desktop.AudioDevices; @@ -75,9 +76,9 @@ final class DevicesPanel extends FormPanel { c.gridx = 0; c.gridy = row++; 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); - c.insets = new Insets(4, 4, 4, 4); + c.insets = UIScale.scale(new Insets(4, 4, 4, 4)); c.gridwidth = 1; denoiseCheck = new JCheckBox("Remove background noise", settings.denoise); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DisplayScale.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DisplayScale.java new file mode 100644 index 0000000..04a2554 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DisplayScale.java @@ -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 %, it draws at 100 % — or, told + * 2 by the desktop, at 200 %, while FlatLaf's own scaling goes on top of that. + * + *

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. + * + *

The desktop's factor is read the way an X11 toolkit would learn it: + *

    + *
  1. {@code Xft/DPI} from XSETTINGS — GNOME and Plasma (through xsettingsd) publish + * the device-pixel DPI there, fractional scale and text scaling included;
  2. + *
  3. KWin's {@code [Xwayland] Scale}, which Plasma sets when X11 apps are left to + * scale themselves — for sessions with no XSETTINGS daemon running;
  4. + *
  5. {@code GDK_SCALE}, which Java2D would otherwise have honoured itself.
  6. + *
+ * With the compositor scaling X11 apps (GNOME's default, and Plasma's "Scaled by the + * system") the client sees 96 DPI and is drawn at 100 %, which is what those + * modes expect; they then upscale the window, blurry but the right size. + * + *

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 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"); + } +} diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DropDownToggleButton.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DropDownToggleButton.java index 1bb7227..546f694 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/DropDownToggleButton.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DropDownToggleButton.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.BorderFactory; import javax.swing.Icon; 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. Dimension plain = button.getPreferredSize(); 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)); add(button, BorderLayout.CENTER); @@ -109,12 +111,12 @@ public final class DropDownToggleButton extends JPanel { @Override public int getIconWidth() { - return 7; + return UIScale.scale(7); } @Override public int getIconHeight() { - return 4; + return UIScale.scale(4); } } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java index 5295f80..0f9f3ad 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.net.ChannelNode; import com.ts3client.net.ClientEntry; import com.ts3client.net.ServerModel; @@ -160,14 +161,16 @@ final class DropIndicatorTree extends JTree { if (highlight != null || loc.getChildIndex() < 0) { Rectangle r = getPathBounds(highlight != null ? highlight : loc.getPath()); if (r != null) { - g2.setStroke(new BasicStroke(2f)); - g2.drawRoundRect(r.x, r.y + 1, r.width - 1, r.height - 3, 4, 4); + g2.setStroke(new BasicStroke(UIScale.scale(2f))); + int arc = UIScale.scale(4); + g2.drawRoundRect(r.x, r.y + 1, r.width - 1, r.height - 3, arc, arc); } } else { Rectangle line = insertLine(loc); if (line != null) { - g2.fillRect(line.x, line.y - 1, line.width, 2); - g2.fillOval(line.x - 3, line.y - 4, 7, 7); + int dot = UIScale.scale(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(); @@ -210,19 +213,19 @@ final class DropIndicatorTree extends JTree { 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) { DefaultMutableTreeNode parent = (DefaultMutableTreeNode) loc.getPath().getLastPathComponent(); int index = loc.getChildIndex(); if (index < parent.getChildCount()) { 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) { Rectangle r = getPathBounds(loc.getPath()); if (r == null) return null; 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. TreePath lastChild = loc.getPath().pathByAddingChild(parent.getChildAt(parent.getChildCount() - 1)); @@ -250,7 +253,7 @@ final class DropIndicatorTree extends JTree { */ private void paintBadges(Graphics g) { 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++) { Rectangle bounds = getRowBounds(row); if (bounds == null || bounds.y + bounds.height < visible.y) continue; @@ -262,7 +265,7 @@ final class DropIndicatorTree extends JTree { if (icons.isEmpty()) continue; 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); } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/FileBrowserDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/FileBrowserDialog.java index 8da5064..5f0abd7 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/FileBrowserDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/FileBrowserDialog.java @@ -1,5 +1,7 @@ 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.TeamspeakConnection; import com.ts3client.net.filetransfer.FileTransfer; @@ -65,15 +67,15 @@ public final class FileBrowserDialog extends JDialog { this.channelId = channel.id; 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.setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); + content.setBorder(new ScaledEmptyBorder(10, 12, 10, 12)); content.add(buildToolbar(), BorderLayout.NORTH); content.add(buildTable(), BorderLayout.CENTER); content.add(buildTransfers(), BorderLayout.SOUTH); setContentPane(content); - setPreferredSize(new Dimension(640, 480)); + setPreferredSize(UIScale.scale(new Dimension(640, 480))); pack(); setLocationRelativeTo(owner); @@ -98,17 +100,17 @@ public final class FileBrowserDialog extends JDialog { deleteButton.addActionListener(e -> deleteSelected()); bar.add(upButton); - bar.add(Box.createHorizontalStrut(6)); + bar.add(Box.createHorizontalStrut(UIScale.scale(6))); bar.add(refresh); - bar.add(Box.createHorizontalStrut(16)); + bar.add(Box.createHorizontalStrut(UIScale.scale(16))); bar.add(pathLabel); bar.add(Box.createHorizontalGlue()); bar.add(mkdir); - bar.add(Box.createHorizontalStrut(6)); + bar.add(Box.createHorizontalStrut(UIScale.scale(6))); bar.add(upload); - bar.add(Box.createHorizontalStrut(6)); + bar.add(Box.createHorizontalStrut(UIScale.scale(6))); bar.add(downloadButton); - bar.add(Box.createHorizontalStrut(6)); + bar.add(Box.createHorizontalStrut(UIScale.scale(6))); bar.add(deleteButton); pathLabel.setFont(Theme.uiFont()); @@ -118,14 +120,14 @@ public final class FileBrowserDialog extends JDialog { private JScrollPane buildTable() { table.setFont(Theme.uiFont()); - table.setRowHeight(20); + table.setRowHeight(UIScale.scale(20)); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 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(1).setPreferredWidth(90); - table.getColumnModel().getColumn(2).setPreferredWidth(70); - table.getColumnModel().getColumn(3).setPreferredWidth(150); + table.getColumnModel().getColumn(1).setPreferredWidth(UIScale.scale(90)); + table.getColumnModel().getColumn(2).setPreferredWidth(UIScale.scale(70)); + table.getColumnModel().getColumn(3).setPreferredWidth(UIScale.scale(150)); table.getSelectionModel().addListSelectionListener(e -> updateButtons()); table.addMouseListener(new MouseAdapter() { @Override @@ -146,7 +148,7 @@ public final class FileBrowserDialog extends JDialog { transfersPanel.setBackground(Theme.windowBg()); JScrollPane scroll = new JScrollPane(transfersPanel); scroll.setBorder(BorderFactory.createTitledBorder("Transfers")); - scroll.setPreferredSize(new Dimension(10, 120)); + scroll.setPreferredSize(UIScale.scale(new Dimension(10, 120))); scroll.getViewport().setBackground(Theme.windowBg()); return scroll; } @@ -325,10 +327,10 @@ public final class FileBrowserDialog extends JDialog { private FileTransfer transfer; TransferRow() { - setLayout(new BorderLayout(8, 0)); + setLayout(new BorderLayout(UIScale.scale(8), 0)); setBackground(Theme.windowBg()); - setBorder(BorderFactory.createEmptyBorder(3, 2, 3, 2)); - setMaximumSize(new Dimension(Integer.MAX_VALUE, 44)); + setBorder(new ScaledEmptyBorder(3, 2, 3, 2)); + setMaximumSize(new Dimension(Integer.MAX_VALUE, UIScale.scale(44))); label.setFont(Theme.uiFont()); bar.setStringPainted(true); add(label, BorderLayout.NORTH); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/FormPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/FormPanel.java index 7b207cf..26a5d91 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/FormPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/FormPanel.java @@ -1,6 +1,8 @@ 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.JLabel; import javax.swing.JPanel; @@ -27,7 +29,7 @@ class FormPanel extends JPanel implements Scrollable { FormPanel() { super(new GridBagLayout()); - setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + setBorder(new ScaledEmptyBorder(12, 12, 12, 12)); } @Override @@ -57,7 +59,7 @@ class FormPanel extends JPanel implements Scrollable { static GridBagConstraints gbc() { 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.fill = GridBagConstraints.HORIZONTAL; return c; @@ -81,8 +83,8 @@ class FormPanel extends JPanel implements Scrollable { */ static void limitWidth(JComponent comp, int preferredWidth) { int height = comp.getPreferredSize().height; - comp.setPreferredSize(new Dimension(preferredWidth, height)); - comp.setMinimumSize(new Dimension(MIN_FIELD_WIDTH, height)); + comp.setPreferredSize(new Dimension(UIScale.scale(preferredWidth), height)); + comp.setMinimumSize(new Dimension(UIScale.scale(MIN_FIELD_WIDTH), height)); } 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. */ 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); panel.add(slider, BorderLayout.CENTER); valueLabel.setPreferredSize(new Dimension(labelWidth, valueLabel.getPreferredSize().height)); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/GroupIcons.java b/ts3-client/swing/src/main/java/com/ts3client/ui/GroupIcons.java index f25c331..bc2637b 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/GroupIcons.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/GroupIcons.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.net.Group; 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 cache, so those are not cached again here. 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; } @@ -54,8 +55,9 @@ public final class GroupIcons { ImageIcon icon = new ImageIcon(data); if (icon.getIconWidth() <= 0) return null; - if (icon.getIconWidth() != SIZE || icon.getIconHeight() != SIZE) { - icon = new ImageIcon(icon.getImage().getScaledInstance(SIZE, SIZE, Image.SCALE_SMOOTH)); + int size = UIScale.scale(SIZE); + if (icon.getIconWidth() != size || icon.getIconHeight() != size) { + icon = new ImageIcon(icon.getImage().getScaledInstance(size, size, Image.SCALE_SMOOTH)); } decoded.put(iconId, icon); return icon; @@ -89,15 +91,16 @@ public final class GroupIcons { int offset = x; for (Icon icon : icons) { icon.paintIcon(c, g, offset, y + (getIconHeight() - icon.getIconHeight()) / 2); - offset += icon.getIconWidth() + GAP; + offset += icon.getIconWidth() + UIScale.scale(GAP); } } @Override public int getIconWidth() { int width = 0; - for (Icon icon : icons) width += icon.getIconWidth() + GAP; - return Math.max(0, width - GAP); + int gap = UIScale.scale(GAP); + for (Icon icon : icons) width += icon.getIconWidth() + gap; + return Math.max(0, width - gap); } @Override diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeyDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeyDialog.java index 58b817a..eccbddd 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeyDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeyDialog.java @@ -1,11 +1,12 @@ 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.HotkeyAction; import com.ts3client.hotkey.HotkeyCombo; import com.ts3client.hotkey.HotkeyEngine; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.DefaultListCellRenderer; import javax.swing.JButton; @@ -120,8 +121,8 @@ final class HotkeyDialog extends JDialog { argumentField.setText(hotkey.argument == null ? "" : hotkey.argument); hint.setFont(hint.getFont().deriveFont(Font.ITALIC, hint.getFont().getSize2D() - 1f)); - getContentPane().setLayout(new BorderLayout(8, 8)); - ((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + getContentPane().setLayout(new BorderLayout(UIScale.scale(8), UIScale.scale(8))); + ((JComponent) getContentPane()).setBorder(new ScaledEmptyBorder(8, 8, 8, 8)); getContentPane().add(buildActionPane(), BorderLayout.CENTER); getContentPane().add(buildForm(), BorderLayout.SOUTH); @@ -129,12 +130,12 @@ final class HotkeyDialog extends JDialog { updateForAction(); Dialogs.closeOnEscape(this, this::cancel); setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setSize(new Dimension(470, 560)); + setSize(UIScale.scale(new Dimension(470, 560))); setLocationRelativeTo(owner); } 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 JScrollPane(tree), BorderLayout.CENTER); p.add(advancedCheck, BorderLayout.SOUTH); @@ -144,7 +145,7 @@ final class HotkeyDialog extends JDialog { private JPanel buildForm() { JPanel p = new JPanel(new GridBagLayout()); 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.fill = GridBagConstraints.HORIZONTAL; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeysPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeysPanel.java index 70e3134..c406ea8 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeysPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/HotkeysPanel.java @@ -1,8 +1,9 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.hotkey.Hotkey; -import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; @@ -34,18 +35,18 @@ final class HotkeysPanel extends JPanel { private final JTable table = new JTable(model); HotkeysPanel(HotkeyService service) { - super(new BorderLayout(6, 6)); + super(new BorderLayout(UIScale.scale(6), UIScale.scale(6))); this.service = service; - setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + setBorder(new ScaledEmptyBorder(8, 8, 8, 8)); synchronized (service.all()) { for (Hotkey h : service.all()) working.add(h.copy()); } table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - table.setRowHeight(table.getRowHeight() + 4); - table.getColumnModel().getColumn(0).setPreferredWidth(240); - table.getColumnModel().getColumn(1).setPreferredWidth(150); + table.setRowHeight(table.getRowHeight() + UIScale.scale(4)); + table.getColumnModel().getColumn(0).setPreferredWidth(UIScale.scale(240)); + table.getColumnModel().getColumn(1).setPreferredWidth(UIScale.scale(150)); table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java b/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java index 83644fd..8574586 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java @@ -1,13 +1,16 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.text.BBCode; import javax.swing.JEditorPane; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.text.html.HTMLEditorKit; +import javax.swing.text.AttributeSet; import javax.swing.text.html.StyleSheet; import java.awt.Color; +import java.awt.Font; /** * 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) { HTMLEditorKit kit = new HTMLEditorKit(); - StyleSheet css = new StyleSheet(); + StyleSheet css = new ScaledStyleSheet(); css.addStyleSheet(kit.getStyleSheet()); 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) { 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())); + } + } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/IconChooserDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/IconChooserDialog.java index f83285f..68c3147 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/IconChooserDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/IconChooserDialog.java @@ -1,5 +1,7 @@ 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.TeamspeakConnection; @@ -69,7 +71,7 @@ final class IconChooserDialog extends JDialog { group("Local", localList, localFooter())); split.setResizeWeight(0.6); - status.setBorder(BorderFactory.createEmptyBorder(2, 8, 2, 8)); + status.setBorder(new ScaledEmptyBorder(2, 8, 2, 8)); status.setForeground(Theme.chatSystem()); getContentPane().setLayout(new BorderLayout()); @@ -81,7 +83,7 @@ final class IconChooserDialog extends JDialog { Dialogs.closeOnEscape(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setSize(new Dimension(520, 360)); + setSize(UIScale.scale(new Dimension(520, 360))); setLocationRelativeTo(owner); loadRemoteIcons(); @@ -101,8 +103,8 @@ final class IconChooserDialog extends JDialog { list.setLayoutOrientation(JList.HORIZONTAL_WRAP); list.setVisibleRowCount(-1); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - list.setFixedCellWidth(32); - list.setFixedCellHeight(32); + list.setFixedCellWidth(UIScale.scale(32)); + list.setFixedCellHeight(UIScale.scale(32)); list.setCellRenderer(new IconCellRenderer()); list.addListSelectionListener(e -> onSelectionChanged(list)); list.addMouseListener(new MouseAdapter() { @@ -129,7 +131,7 @@ final class IconChooserDialog extends JDialog { } private JPanel group(String title, JList 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.add(new JScrollPane(list), BorderLayout.CENTER); panel.add(footer, BorderLayout.SOUTH); @@ -150,7 +152,7 @@ final class IconChooserDialog extends JDialog { private Component localFooter() { com.ts3client.gfx.IconPack active = IconTheme.get().activePack(); 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; } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/IconTheme.java b/ts3-client/swing/src/main/java/com/ts3client/ui/IconTheme.java index bbeaa30..c8a5ed7 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/IconTheme.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/IconTheme.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.github.weisj.jsvg.SVGDocument; import com.github.weisj.jsvg.parser.LoaderContext; import com.github.weisj.jsvg.parser.SVGLoader; @@ -33,6 +34,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * *

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. + * {@link #SIZE} and {@link #TOOLBAR_SIZE} are unscaled; sizes passed in are pixels. */ public final class IconTheme { @@ -86,12 +88,12 @@ public final class IconTheme { listeners.add(listener); } - /** @return the icon for a TeamSpeak icon key at 16×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) { - 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) { return INSTANCE.lookup(key, size); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java b/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java index 3bd324b..5584fb3 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.ImageIcon; import java.awt.BasicStroke; 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. * Everything else — menu and dialog decoration — simply stays blank without a pack, * which is why {@link #of(String)} may return {@code null}. + * + *

Icons come at the interface's scale; the variants taking a size take it in pixels. */ public final class Icons { + /** The box the built-in painters draw in. */ private static final int SZ = IconTheme.SIZE; private Icons() { } + private static int small() { + return UIScale.scale(IconTheme.SIZE); + } + + private static int large() { + return UIScale.scale(IconTheme.TOOLBAR_SIZE); + } + private interface Painter { void paint(Graphics2D g); } @@ -44,13 +57,13 @@ public final class Icons { 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) { return IconTheme.icon(key, size); } 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) { @@ -64,14 +77,14 @@ public final class Icons { */ private static ImageIcon themed(String[] keys, Painter fallback) { for (String key : keys) { - ImageIcon icon = IconTheme.icon(key, SZ); + ImageIcon icon = IconTheme.icon(key, small()); if (icon != null) return icon; } return make(fallback); } private static ImageIcon make(Painter p) { - return make(SZ, p); + return make(small(), p); } /** Runs a painter (which draws in a 16×16 box) scaled to {@code size}. */ @@ -129,7 +142,7 @@ public final class Icons { // ---- client status icons ---- public static ImageIcon clientIdle() { - return clientIdle(SZ); + return clientIdle(small()); } public static ImageIcon clientIdle(int size) { @@ -137,7 +150,7 @@ public final class Icons { } public static ImageIcon clientTalking() { - return clientTalking(SZ); + return clientTalking(small()); } public static ImageIcon clientTalking(int size) { @@ -145,7 +158,7 @@ public final class Icons { } public static ImageIcon clientAway() { - return clientAway(SZ); + return clientAway(small()); } public static ImageIcon clientAway(int size) { @@ -154,12 +167,12 @@ public final class Icons { /** A channel commander that is not talking. */ public static ImageIcon clientCommander() { - return clientCommander(SZ); + return clientCommander(small()); } /** A channel commander that is talking. */ public static ImageIcon clientCommanderTalking() { - return clientCommanderTalking(SZ); + return clientCommanderTalking(small()); } public static ImageIcon clientQuery() { @@ -167,7 +180,7 @@ public final class Icons { } public static ImageIcon micMuted() { - return micMuted(SZ); + return micMuted(small()); } public static ImageIcon micMuted(int size) { @@ -175,7 +188,7 @@ public final class Icons { } public static ImageIcon speakerMuted() { - return speakerMuted(SZ); + return speakerMuted(small()); } 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. */ public static ImageIcon micDisabled() { - return micDisabled(SZ); + return micDisabled(small()); } public static ImageIcon micDisabled(int size) { @@ -193,7 +206,7 @@ public final class Icons { /** The playback device is unavailable, as opposed to muted/deafened. */ public static ImageIcon speakerDisabled() { - return speakerDisabled(SZ); + return speakerDisabled(small()); } 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. */ public static ImageIcon micLocalMuted() { - return micLocalMuted(SZ); + return micLocalMuted(small()); } public static ImageIcon micLocalMuted(int size) { @@ -222,32 +235,32 @@ public final class Icons { // ---- toolbar / action icons ---- public static ImageIcon connect() { - return themed("CONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintConnect); + return themed("CONNECT", large(), Icons::paintConnect); } public static ImageIcon disconnect() { - return themed("DISCONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintDisconnect); + return themed("DISCONNECT", large(), Icons::paintDisconnect); } public static ImageIcon mic() { - return themed("CAPTURE", IconTheme.TOOLBAR_SIZE, Icons::paintMic); + return themed("CAPTURE", large(), Icons::paintMic); } public static ImageIcon speaker() { - return themed("PLAYBACK", IconTheme.TOOLBAR_SIZE, Icons::paintSpeaker); + return themed("PLAYBACK", large(), Icons::paintSpeaker); } public static ImageIcon micMutedLarge() { - return themed("INPUT_MUTED", IconTheme.TOOLBAR_SIZE, Icons::paintMicMuted); + return themed("INPUT_MUTED", large(), Icons::paintMicMuted); } 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. */ 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. */ @@ -262,7 +275,7 @@ public final class Icons { public static ImageIcon channelClientPair() { ImageIcon channel = channel(true); ImageIcon edit = themed("EDIT", Icons::paintEdit); - int gap = 8; + int gap = UIScale.scale(8); int width = channel.getIconWidth() + gap + edit.getIconWidth(); int height = Math.max(channel.getIconHeight(), edit.getIconHeight()); BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); @@ -282,15 +295,15 @@ public final class Icons { /** The toolbar's away marker. */ 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() { - return themed("SETTINGS", IconTheme.TOOLBAR_SIZE, Icons::paintSettings); + return themed("SETTINGS", large(), Icons::paintSettings); } public static ImageIcon app() { - return app(SZ); + return app(small()); } public static ImageIcon app(int size) { diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/IdentitiesDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/IdentitiesDialog.java index 1ccf863..75b7ea1 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/IdentitiesDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/IdentitiesDialog.java @@ -1,5 +1,7 @@ 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.IdentityEntry; import com.ts3client.config.IdentityStore; @@ -7,7 +9,6 @@ import com.ts3client.config.Settings; import com.ts3client.teamspeak.TeamSpeakImporter; import com.ts3client.teamspeak.TeamSpeakSettingsDb; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultListCellRenderer; @@ -73,12 +74,12 @@ public final class IdentitiesDialog extends JDialog { list.setVisibleRowCount(10); JScrollPane scroll = new JScrollPane(list); - scroll.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); - scroll.setPreferredSize(new Dimension(220, 260)); + scroll.setBorder(new ScaledEmptyBorder(8, 8, 8, 8)); + scroll.setPreferredSize(UIScale.scale(new Dimension(220, 260))); JPanel buttons = new JPanel(); 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, "Import…", this::importIdentity); if (TeamSpeakSettingsDb.locate() != null) { @@ -103,15 +104,15 @@ public final class IdentitiesDialog extends JDialog { Dialogs.closeOnEscape(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); pack(); - setMinimumSize(new Dimension(640, 320)); + setMinimumSize(UIScale.scale(new Dimension(640, 320))); setLocationRelativeTo(owner); } private JPanel buildDetails() { 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(); - c.insets = new Insets(4, 4, 4, 4); + c.insets = UIScale.scale(new Insets(4, 4, 4, 4)); c.anchor = GridBagConstraints.NORTHWEST; 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.addActionListener(e -> action.run()); panel.add(b); - panel.add(Box.createVerticalStrut(4)); + panel.add(Box.createVerticalStrut(UIScale.scale(4))); } private String label(IdentityEntry e) { @@ -335,8 +336,8 @@ public final class IdentitiesDialog extends JDialog { JDialog progress = new JDialog(this, "Please wait", true); JLabel status = new JLabel(message); - JPanel panel = new JPanel(new BorderLayout(8, 8)); - panel.setBorder(BorderFactory.createEmptyBorder(16, 16, 12, 16)); + JPanel panel = new JPanel(new BorderLayout(UIScale.scale(8), UIScale.scale(8))); + panel.setBorder(new ScaledEmptyBorder(16, 16, 12, 16)); panel.add(status, BorderLayout.CENTER); if (cancellable) { JButton cancel = new JButton("Stop"); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java index 08c2f13..7d27b0d 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.contacts.Contact; import com.ts3client.net.ChannelNode; import com.ts3client.net.ClientEntry; @@ -65,7 +67,7 @@ public final class InfoPanel extends JPanel { public InfoPanel() { super(new BorderLayout()); - pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); + pane.setBorder(new ScaledEmptyBorder(6, 8, 6, 8)); pane.addHyperlinkListener(e -> { if (e.getEventType() != javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) return; Links.open(e.getDescription(), this); @@ -82,7 +84,7 @@ public final class InfoPanel extends JPanel { toggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); toggle.setToolTipText("Show these details in a chat tab instead"); toggle.setOpaque(true); - toggle.setBorder(BorderFactory.createEmptyBorder(1, 4, 1, 4)); + toggle.setBorder(new ScaledEmptyBorder(1, 4, 1, 4)); toggle.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { @@ -188,7 +190,7 @@ public final class InfoPanel extends JPanel { private String body() { if (shownChannel != null) return channelBody(shownChannel); 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 "Select a channel or client to see details."; @@ -201,9 +203,11 @@ public final class InfoPanel extends JPanel { */ private int avatarBox() { int width = scroll.getViewport().getWidth(); - if (width <= 0) return AvatarHtml.MAX_SIZE; - int box = width * 2 / 5 / AVATAR_BOX_STEP * AVATAR_BOX_STEP; - return Math.max(AVATAR_MIN_SIZE, Math.min(AvatarHtml.MAX_SIZE, box)); + int max = UIScale.scale(AvatarHtml.MAX_SIZE); + if (width <= 0) return max; + 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) { @@ -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. return "" + "" - + "" + + "" + "
" + details + "" + picture + "" + picture + "
"; } @@ -297,11 +301,11 @@ public final class InfoPanel extends JPanel { if (group.iconId == 0) return ""; File file = icons.file(group.iconId); if (file == null) return ""; - return " "; + return " "; } private static String heading(String text) { - return "

" + text + "
"; + return "
" + text + "
"; } private static void row(StringBuilder sb, String label, String value) { diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/LevelMeter.java b/ts3-client/swing/src/main/java/com/ts3client/ui/LevelMeter.java index d874c27..1490238 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/LevelMeter.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/LevelMeter.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.JComponent; import java.awt.Color; import java.awt.Dimension; @@ -25,11 +27,11 @@ public final class LevelMeter extends JComponent { private volatile boolean transmitting; 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 // collapses the bar to nothing. Keep the height and let only the width give. - setMinimumSize(new Dimension(60, 18)); - setMaximumSize(new Dimension(Integer.MAX_VALUE, 18)); + setMinimumSize(UIScale.scale(new Dimension(60, 18))); + setMaximumSize(new Dimension(Integer.MAX_VALUE, UIScale.scale(18))); } public void setLevel(double db) { @@ -70,16 +72,17 @@ public final class LevelMeter extends JComponent { int h = getHeight(); 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); 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) { int tx = dbToX(thresholdDb, w - 2); g.setColor(new Color(0xF0C419)); - g.fillRect(tx, 1, 2, h - 3); + g.fillRect(tx, 1, UIScale.scale(2), h - 3); } } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java index 6b4117c..65151e6 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.audio.AudioBackend; import com.ts3client.audio.desktop.DesktopAudioBackend; import com.ts3client.config.AwayMessages; @@ -14,7 +15,6 @@ import com.ts3client.net.ChannelNode; import com.ts3client.sound.SoundNotifier; import com.ts3client.sound.SoundPlayer; -import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; 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. Runtime.getRuntime().addShutdownHook(shutdownHook); - setMinimumSize(new Dimension(720, 480)); + setMinimumSize(UIScale.scale(new Dimension(720, 480))); tray = new TrayController(this, this::quit); menuBar = buildMenuBar(); @@ -128,7 +128,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener { statusTimer = new javax.swing.Timer(3000, e -> updateConnectionStatus()); statusTimer.start(); - setSize(880, 560); + setSize(UIScale.scale(new Dimension(880, 560))); setLocationRelativeTo(null); // 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()); 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(labelField); if (!channelPath.isEmpty()) form.add(joinChannel); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java b/ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java index 37cc0d0..e5e4cd0 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java @@ -1,8 +1,9 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; @@ -74,7 +75,7 @@ final class MainToolbar extends JToolBar { setFloatable(false); setBackground(Theme.toolbarBg()); - setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); + setBorder(new ScaledEmptyBorder(4, 6, 4, 6)); setComponentPopupMenu(buildContextMenu()); connectButton = new JButton(Icons.connect()); @@ -152,7 +153,7 @@ final class MainToolbar extends JToolBar { /** The Master Volume slider: multiplies both voice and notification volume. */ 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.setAlignmentY(Component.CENTER_ALIGNMENT); @@ -164,7 +165,7 @@ final class MainToolbar extends JToolBar { slider.setOpaque(false); slider.setToolTipText("Master volume (voice + notifications) — scroll to adjust"); 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 -> { settings.masterVolume = slider.getValue() / 100.0; listener.onMasterVolumeChanged(); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/NicknameCellEditor.java b/ts3-client/swing/src/main/java/com/ts3client/ui/NicknameCellEditor.java index 918cdfe..36cfe75 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/NicknameCellEditor.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/NicknameCellEditor.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.net.ClientEntry; import javax.swing.DefaultCellEditor; @@ -103,7 +104,7 @@ final class NicknameCellEditor extends DefaultTreeCellEditor { if (!(c instanceof JLabel)) return; editingIcon = ((JLabel) c).getIcon(); 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. */ diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/NotificationsPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/NotificationsPanel.java index 38e8be7..1e5c84d 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/NotificationsPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/NotificationsPanel.java @@ -1,12 +1,13 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; import com.ts3client.sound.NotificationSettings; import com.ts3client.sound.SoundEvent; import com.ts3client.sound.SoundNotifier; import com.ts3client.sound.SoundPack; -import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; @@ -79,7 +80,7 @@ final class NotificationsPanel extends JPanel { private final EventTableModel tableModel = new EventTableModel(); NotificationsPanel(Settings settings, SoundNotifier sounds) { - super(new BorderLayout(0, 8)); + super(new BorderLayout(0, UIScale.scale(8))); this.settings = settings; this.sounds = sounds; this.working = settings.notifications.copy(); @@ -91,7 +92,7 @@ final class NotificationsPanel extends JPanel { buildRows(); this.table = buildTable(); - setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + setBorder(new ScaledEmptyBorder(12, 12, 12, 12)); add(buildHeader(), BorderLayout.NORTH); add(new JScrollPane(table), BorderLayout.CENTER); add(buildFooter(), BorderLayout.SOUTH); @@ -122,7 +123,7 @@ final class NotificationsPanel extends JPanel { private JComponent buildHeader() { JPanel p = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets(2, 2, 2, 2); + c.insets = UIScale.scale(new Insets(2, 2, 2, 2)); c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL; @@ -132,7 +133,7 @@ final class NotificationsPanel extends JPanel { test.setToolTipText("Play this pack's test sound"); test.addActionListener(e -> sounds.preview(SoundEvent.SPECIAL_SOUND_TEST)); - JPanel packRow = new JPanel(new BorderLayout(6, 0)); + JPanel packRow = new JPanel(new BorderLayout(UIScale.scale(6), 0)); packRow.add(packCombo, BorderLayout.CENTER); packRow.add(test, BorderLayout.EAST); @@ -146,7 +147,7 @@ final class NotificationsPanel extends JPanel { volume.setToolTipText("Volume of the notification sounds"); addRow(p, c, row++, new JLabel("Sound volume:"), volume); - JPanel dirRow = new JPanel(new BorderLayout(6, 0)); + JPanel dirRow = new JPanel(new BorderLayout(UIScale.scale(6), 0)); JButton browse = new JButton("Browse…"); browse.addActionListener(e -> browseForPackFolder()); dirRow.add(packDirField, BorderLayout.CENTER); @@ -157,10 +158,10 @@ final class NotificationsPanel extends JPanel { } private JComponent buildFooter() { - JPanel p = new JPanel(new BorderLayout(6, 0)); + JPanel p = new JPanel(new BorderLayout(UIScale.scale(6), 0)); JLabel hint = new JLabel("Bold actions are important: they are still played " + "while your speakers are muted."); - hint.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0)); + hint.setBorder(new ScaledEmptyBorder(4, 0, 0, 0)); JPanel buttons = new JPanel(); JButton important = new JButton("Toggle important"); @@ -187,12 +188,12 @@ final class NotificationsPanel extends JPanel { t.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); t.setShowGrid(false); t.setTableHeader(null); - t.setRowHeight(Math.max(t.getRowHeight(), t.getFontMetrics(t.getFont()).getHeight() + 6)); - t.getColumnModel().getColumn(0).setMaxWidth(TOGGLE_COLUMN_WIDTH); - t.getColumnModel().getColumn(0).setMinWidth(TOGGLE_COLUMN_WIDTH); + t.setRowHeight(Math.max(t.getRowHeight(), t.getFontMetrics(t.getFont()).getHeight() + UIScale.scale(6))); + t.getColumnModel().getColumn(0).setMaxWidth(UIScale.scale(TOGGLE_COLUMN_WIDTH)); + t.getColumnModel().getColumn(0).setMinWidth(UIScale.scale(TOGGLE_COLUMN_WIDTH)); t.getColumnModel().getColumn(0).setCellRenderer(new ToggleRenderer(t.getDefaultRenderer(Boolean.class))); t.getColumnModel().getColumn(1).setCellRenderer(new ActionRenderer()); - t.setPreferredScrollableViewportSize(new Dimension(360, 260)); + t.setPreferredScrollableViewportSize(UIScale.scale(new Dimension(360, 260))); t.addMouseListener(new MouseAdapter() { @Override @@ -381,11 +382,11 @@ final class NotificationsPanel extends JPanel { if (row.isHeading()) { setFont(base.deriveFont(Font.BOLD)); setForeground(selected ? table.getSelectionForeground() : table.getForeground()); - setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + setBorder(new ScaledEmptyBorder(2, 2, 2, 2)); setToolTipText(null); return this; } - setBorder(BorderFactory.createEmptyBorder(2, 18, 2, 2)); + setBorder(new ScaledEmptyBorder(2, 18, 2, 2)); setFont(base.deriveFont(working.isImportant(row.event) ? Font.BOLD : Font.PLAIN)); boolean silent = !sounds.hasSound(row.event); setForeground(selected ? table.getSelectionForeground() diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ReasonDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ReasonDialog.java index 218e2a8..fc2dd4a 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ReasonDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ReasonDialog.java @@ -1,6 +1,7 @@ package com.ts3client.ui; -import javax.swing.BorderFactory; +import com.formdev.flatlaf.util.ScaledEmptyBorder; + import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JLabel; @@ -54,7 +55,7 @@ final class ReasonDialog { static JLabel label(String text) { JLabel label = new JLabel(text); label.setAlignmentX(Component.LEFT_ALIGNMENT); - label.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0)); + label.setBorder(new ScaledEmptyBorder(0, 0, 4, 0)); return label; } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerGroupsDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerGroupsDialog.java index 5d0c412..9f5c7c4 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerGroupsDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerGroupsDialog.java @@ -1,10 +1,11 @@ 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.Group; import com.ts3client.net.TeamspeakConnection; -import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.GrayFilter; import javax.swing.ImageIcon; @@ -48,10 +49,10 @@ final class ServerGroupsDialog extends JDialog { list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS)); JScrollPane scroll = new JScrollPane(list); - scroll.setPreferredSize(new Dimension(280, 360)); + scroll.setPreferredSize(UIScale.scale(new Dimension(280, 360))); scroll.getVerticalScrollBar().setUnitIncrement(16); - header.setBorder(BorderFactory.createEmptyBorder(8, 10, 4, 10)); + header.setBorder(new ScaledEmptyBorder(8, 10, 4, 10)); header.setHorizontalAlignment(SwingConstants.LEFT); JButton close = new JButton("Close"); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java index 1911a2c..132e0fd 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java @@ -1,5 +1,8 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; @@ -150,9 +153,9 @@ final class ServerTabPane extends JPanel { /** A tab's label: title, microphone marker and a close button. */ 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.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); label.setFont(Theme.uiFont()); @@ -175,7 +178,7 @@ final class ServerTabPane extends JPanel { close.setContentAreaFilled(false); close.setMargin(new Insets(0, 0, 0, 0)); close.setForeground(Theme.chatSystem()); - close.setPreferredSize(new Dimension(14, 14)); + close.setPreferredSize(UIScale.scale(new Dimension(14, 14))); close.setToolTipText("Close this connection"); close.addActionListener(e -> listener.closeTab(tab)); cell.add(close); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java index a0a67e2..15644b9 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; import com.ts3client.contacts.ContactStore; import com.ts3client.contacts.ContactType; @@ -168,7 +169,7 @@ public final class ServerTreePanel extends JScrollPane { ui.setLeftChildIndent(4); ui.setRightChildIndent(10); } - tree.setRowHeight(20); + tree.setRowHeight(UIScale.scale(20)); tree.setBackground(Theme.treeBg()); tree.setFont(Theme.uiFont()); ServerTreeCellRenderer renderer = new ServerTreeCellRenderer(id -> id == selfClientId, contacts, settings); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/SettingsDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/SettingsDialog.java index 3707957..0039713 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/SettingsDialog.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/SettingsDialog.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.audio.OpusParameters; import com.ts3client.audio.VoiceInput; import com.ts3client.audio.VoiceOutput; @@ -92,8 +93,8 @@ public final class SettingsDialog extends JDialog { }); pack(); - setSize(new Dimension(480, 540)); - setMinimumSize(new Dimension(420, 360)); + setSize(UIScale.scale(new Dimension(480, 540))); + setMinimumSize(UIScale.scale(new Dimension(420, 360))); setLocationRelativeTo(owner); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/Spinners.java b/ts3-client/swing/src/main/java/com/ts3client/ui/Spinners.java index 7c38a27..18adbb3 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/Spinners.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/Spinners.java @@ -1,5 +1,7 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; + import javax.swing.JSpinner; import java.awt.Dimension; @@ -19,7 +21,7 @@ final class Spinners { static JSpinner compact(JSpinner spinner) { JSpinner.NumberEditor editor = new JSpinner.NumberEditor(spinner, "#"); 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.setMaximumSize(size); return spinner; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/StatusBar.java b/ts3-client/swing/src/main/java/com/ts3client/ui/StatusBar.java index 76ee6b2..5e303c2 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/StatusBar.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/StatusBar.java @@ -1,6 +1,7 @@ package com.ts3client.ui; -import javax.swing.BorderFactory; +import com.formdev.flatlaf.util.ScaledEmptyBorder; + import javax.swing.JLabel; import javax.swing.JPanel; import java.awt.BorderLayout; @@ -14,7 +15,7 @@ final class StatusBar extends JPanel { StatusBar() { super(new BorderLayout()); setBackground(Theme.statusBg()); - setBorder(BorderFactory.createEmptyBorder(3, 8, 3, 8)); + setBorder(new ScaledEmptyBorder(3, 8, 3, 8)); statusLabel.setFont(Theme.uiFont()); codecLabel.setFont(Theme.uiFont()); codecLabel.setForeground(Theme.chatSystem()); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java b/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java index 86a482b..05a58f9 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java @@ -1,6 +1,7 @@ package com.ts3client.ui; import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.UIScale; import javax.swing.UIManager; import java.awt.Color; @@ -36,7 +37,7 @@ public final class Theme { public static Font uiFont() { 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() { diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/VoiceActivationPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/VoiceActivationPanel.java index bf8c021..f0ef0d1 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/VoiceActivationPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/VoiceActivationPanel.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.formdev.flatlaf.util.UIScale; import com.ts3client.audio.InputLevel; import com.ts3client.audio.OpusParameters; import com.ts3client.audio.VoiceInput; @@ -105,11 +106,11 @@ final class VoiceActivationPanel extends FormPanel { c.gridx = 0; c.gridy = row; 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); c.gridy = ++row; add(meter, c); - c.insets = new Insets(4, 4, 4, 4); + c.insets = UIScale.scale(new Insets(4, 4, 4, 4)); c.gridy = ++row; add(buildTestControls(), c); c.gridwidth = 1; diff --git a/ts3-client/swing/src/test/java/com/ts3client/ui/DisplayScaleTest.java b/ts3-client/swing/src/test/java/com/ts3client/ui/DisplayScaleTest.java new file mode 100644 index 0000000..a4edf1a --- /dev/null +++ b/ts3-client/swing/src/test/java/com/ts3client/ui/DisplayScaleTest.java @@ -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 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")); + } +}