|
57 | 57 | import javax.swing.*; |
58 | 58 | import java.awt.*; |
59 | 59 | import java.awt.datatransfer.StringSelection; |
| 60 | +import java.io.BufferedReader; |
60 | 61 | import java.io.File; |
61 | 62 | import java.io.IOException; |
| 63 | +import java.io.InputStreamReader; |
62 | 64 | import java.lang.management.ManagementFactory; |
63 | 65 | import java.lang.management.MemoryPoolMXBean; |
64 | 66 | import java.net.CookieHandler; |
|
68 | 70 | import java.nio.file.Paths; |
69 | 71 | import java.text.DecimalFormat; |
70 | 72 | import java.util.ArrayList; |
| 73 | +import java.util.List; |
71 | 74 | import java.util.Objects; |
72 | 75 | import java.util.Optional; |
73 | 76 | import java.util.concurrent.TimeUnit; |
| 77 | +import java.util.stream.Stream; |
74 | 78 |
|
75 | 79 | import static org.jackhuang.hmcl.setting.SettingsManager.settings; |
76 | 80 | import static org.jackhuang.hmcl.ui.FXUtils.runInFX; |
77 | 81 | import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES; |
78 | 82 | import static org.jackhuang.hmcl.util.i18n.I18n.i18n; |
79 | 83 | import static org.jackhuang.hmcl.util.logging.Logger.LOG; |
80 | 84 |
|
| 85 | +/// The main JavaFX application entry point. |
81 | 86 | public final class Launcher extends Application { |
82 | 87 | public static final CookieManager COOKIE_MANAGER = new CookieManager(); |
83 | 88 |
|
@@ -396,6 +401,8 @@ private static void setupWindowsAppUserModelID() { |
396 | 401 | } |
397 | 402 | } |
398 | 403 |
|
| 404 | + /// Sets JavaFX VM options before the toolkit starts. |
| 405 | + /// On Wayland, the UI scale is auto-detected when the user did not configure one explicitly. |
399 | 406 | private static void setupJavaFXVMOptions() { |
400 | 407 | if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) { |
401 | 408 | LOG.info("HMCL_FORCE_GPU: true"); |
@@ -451,47 +458,85 @@ private static void setupJavaFXVMOptions() { |
451 | 458 | } |
452 | 459 | } |
453 | 460 |
|
| 461 | + float uiScaleLowerBound; |
| 462 | + float uiScaleUpperBound; |
| 463 | + |
| 464 | + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { |
| 465 | + // JavaFX behavior may be abnormal when the DPI scaling factor is too high |
| 466 | + uiScaleLowerBound = 0.25f; |
| 467 | + uiScaleUpperBound = 4f; |
| 468 | + } else { |
| 469 | + uiScaleLowerBound = 0.01f; |
| 470 | + uiScaleUpperBound = 10f; |
| 471 | + } |
| 472 | + |
454 | 473 | String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE")); |
455 | | - if (uiScale != null) { |
456 | | - uiScale = uiScale.trim(); |
457 | 474 |
|
| 475 | + float scaleValue = Float.NaN; |
| 476 | + if (uiScale != null && !(uiScale = uiScale.trim()).isEmpty()) { |
458 | 477 | LOG.info("HMCL_UI_SCALE: " + uiScale); |
459 | 478 |
|
460 | 479 | try { |
461 | | - float scaleValue; |
462 | 480 | if (uiScale.endsWith("%")) { |
463 | 481 | scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f; |
464 | 482 | } else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) { |
465 | 483 | scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f; |
466 | 484 | } else { |
467 | 485 | scaleValue = Float.parseFloat(uiScale); |
468 | 486 | } |
| 487 | + } catch (Throwable e) { |
| 488 | + LOG.warning("Invalid UI scale: " + uiScale); |
| 489 | + } |
| 490 | + } else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) { |
| 491 | + String xdgSessionType = Objects.requireNonNullElse(System.getenv("XDG_SESSION_TYPE"), ""); |
| 492 | + String xdgCurrentDesktop = Objects.requireNonNullElse(System.getenv("XDG_CURRENT_DESKTOP"), ""); |
469 | 493 |
|
470 | | - float lowerBound; |
471 | | - float upperBound; |
| 494 | + if ("wayland".equals(xdgSessionType) |
| 495 | + && StringUtils.startsWithIgnoreCase(xdgCurrentDesktop, "KDE")) { |
| 496 | + try { |
| 497 | + @Nullable Path xrdb = SystemUtils.which("xrdb"); |
| 498 | + @Nullable String xftDpi = xrdb != null ? SystemUtils.run(List.of(FileUtils.getAbsolutePath(xrdb), "-query"), inputStream -> { |
| 499 | + @Nullable String dpiLine; |
| 500 | + |
| 501 | + try (var reader = new InputStreamReader(inputStream, OperatingSystem.NATIVE_CHARSET); |
| 502 | + BufferedReader bufferedReader = new BufferedReader(reader); |
| 503 | + Stream<String> lines = bufferedReader.lines()) { |
| 504 | + dpiLine = lines.filter(line -> line.trim().startsWith("Xft.dpi:")) |
| 505 | + .findFirst().orElse(null); |
| 506 | + } |
472 | 507 |
|
473 | | - if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { |
474 | | - // JavaFX behavior may be abnormal when the DPI scaling factor is too high |
475 | | - lowerBound = 0.25f; |
476 | | - upperBound = 4f; |
477 | | - } else { |
478 | | - lowerBound = 0.01f; |
479 | | - upperBound = 10f; |
480 | | - } |
| 508 | + return dpiLine != null |
| 509 | + ? dpiLine.substring("Xft.dpi:".length()).trim() |
| 510 | + : null; |
| 511 | + }, java.time.Duration.ofSeconds(1)) : null; |
| 512 | + |
| 513 | + if (xftDpi != null) { |
| 514 | + float dpiValue = Float.parseFloat(xftDpi); |
| 515 | + float scale = dpiValue / 96f; |
481 | 516 |
|
482 | | - if (scaleValue >= lowerBound && scaleValue <= upperBound) { |
483 | | - if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { |
484 | | - System.getProperties().putIfAbsent("glass.win.uiScale", uiScale); |
485 | | - } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { |
486 | | - LOG.warning("macOS does not support setting UI scale, so it will be ignored"); |
487 | | - } else { |
488 | | - System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale); |
| 517 | + if (scale > 1 && scale <= 10) { |
| 518 | + LOG.info("Detected Xft.dpi: %s (%.1fx)".formatted(dpiValue, scale)); |
| 519 | + scaleValue = scale; |
| 520 | + uiScale = Float.toString(scale); |
| 521 | + } |
489 | 522 | } |
| 523 | + } catch (Exception e) { |
| 524 | + LOG.warning("Failed to read Xft.dpi from xrdb", e); |
| 525 | + } |
| 526 | + } |
| 527 | + } |
| 528 | + |
| 529 | + if (Float.isFinite(scaleValue)) { |
| 530 | + if (scaleValue >= uiScaleLowerBound && scaleValue <= uiScaleUpperBound) { |
| 531 | + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { |
| 532 | + System.getProperties().putIfAbsent("glass.win.uiScale", uiScale); |
| 533 | + } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { |
| 534 | + LOG.warning("macOS does not support setting UI scale, so it will be ignored"); |
490 | 535 | } else { |
491 | | - LOG.warning("UI scale out of range: " + uiScale); |
| 536 | + System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale); |
492 | 537 | } |
493 | | - } catch (Throwable e) { |
494 | | - LOG.warning("Invalid UI scale: " + uiScale); |
| 538 | + } else { |
| 539 | + LOG.warning("UI scale out of range: " + uiScale); |
495 | 540 | } |
496 | 541 | } |
497 | 542 | } |
|
0 commit comments