Commit a8d02f9
fix: revalidate window tree on display change to recover stale layout
Dragging a JFrame across macOS displays with different DPI scaling factors
(external 1x → built-in Retina 2x) leaves child component sizes pinned to
the previous display's preferred-size values, clipping trailing glyphs of
labels and buttons. The JDK fires a "graphicsConfiguration"
PropertyChangeEvent on the affected Window when this happens but does not
trigger any layout invalidation in response.
Instrumentation confirmed the failure mode against a 35-char Noto Sans
Display 14pt label on JDK 17:
before fix:
initial 2x: prefSize=263 size=263 ✓
drag → 1x: prefSize=256 size=263 ✓ (slack)
drag → 2x: prefSize=263 size=256 ✗ TRUNCATED
after fix:
initial 2x: prefSize=263 size=263 ✓
drag → 1x: prefSize=256 size=256 ✓
drag → 2x: prefSize=263 size=263 ✓
`Component.getFontMetrics(font).stringWidth(text)` *does* go through a
per-display FRC path on JDK 9+ and returns the correct per-display width
(256 vs 263 for that string). So `getPreferredSize()` always reports the
right value — the bug is purely that the layout-manager-allocated `size`
never gets reassigned across the display change.
== Why typing into a JTextField masks the bug ==
Typing calls `JTextField.revalidate()`, which propagates invalidation up
through the panels via `Container.invalidate()` (which only marks the
receiver invalid and propagates UP). The first ancestor whose
`layoutContainer` re-runs picks up every child's new
`getPreferredSize()` and reassigns their `size`, incidentally fixing all
siblings of the typed-into field. Without typing, no parent layout ever
re-runs after the display change, so the stale `size` persists.
== The fix ==
In `MaterialLookAndFeel.initialize`, install a global `AWTEventListener`
for `WINDOW_OPENED` that attaches a per-window `PropertyChangeListener`
on the `"graphicsConfiguration"` property. On change, walk the window's
component tree downward and call `Container.invalidate()` on every
container, then `window.validate()` and `window.repaint()`. This forces
every layout manager in the tree to re-run against the new display's
per-DPI preferred sizes.
Walking the tree manually is necessary because `Container.invalidate()`
only marks the receiver invalid and propagates UP, never DOWN, and
`Container.validate()` only recurses into children that are already
invalid. So `window.invalidate(); window.validate();` alone leaves all
nested Containers (the `FlowLayout`/`BorderLayout` panels that hold the
truncating components) marked valid, and their `layoutContainer` is
never re-run.
`initialize()` also iterates `Window.getWindows()` to cover applications
that switch to MaterialLookAndFeel after windows are already visible.
`uninitialize()` symmetrically removes the AWTEventListener and the
per-window PCLs. `SecurityException` on AWT-listener install/remove is
swallowed for headless or sandboxed contexts — the L&F still works,
only mixed-DPI auto-relayout is unavailable.
The fix touches no UI delegates, does not reinstall the L&F, and does
not call `SwingUtilities.updateComponentTreeUI` — so downstream
component libraries (`JTextFieldPlaceholder`, `SwingSnackBar`, etc.)
keep their per-instance state, and host-app `UIManager.put(...)`
overrides are preserved.
== Why the JDK doesn't do this itself ==
`sun.lwawt.macosx.LWWindowPeer.displayChanged()` updates the GC and
fires the `"graphicsConfiguration"` PropertyChangeEvent and recurses to
children via `Container.updateChildGraphicsData`, but performs no
invalidate/revalidate cycle (`java/awt/Component.java:1187`,
`Container.java:1185`). No JEP since JEP 263 has rewired this. The
underlying defect is in the JDK; this PR routes around it from the L&F
side without any JDK changes.
== Relationship to existing PRs ==
- PR #203 ("Recompute font attributes when loading fonts"): attacked
the data side of the font factory (`TextAttribute.SIZE` pinning).
Was reverted on master. Orthogonal to this layout-allocation bug.
- PR #207 ("Display-change watcher"): solved the same symptom by
reinstalling the L&F and calling `updateComponentTreeUI(window)`
on every display change. Works but breaks downstream component
libraries during the `updateComponentTreeUI` cascade and resets
`UIManager.put(...)` overrides via the L&F reinstall. This PR uses
only the minimal cascade — `invalidateTree` + `validate` — without
rebuilding UI delegates.
- PR #209 (Codex's "FRACTIONALMETRICS as client property"):
targeted a fractional-vs-integer FRC mismatch. Instrumentation
showed that was not the actual cause; `fm.stringWidth` already
adapts per-display correctly on JDK 9+.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 5547a05 commit a8d02f9
1 file changed
Lines changed: 89 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
25 | 29 | | |
26 | 30 | | |
27 | 31 | | |
| |||
816 | 820 | | |
817 | 821 | | |
818 | 822 | | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
819 | 900 | | |
820 | 901 | | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
821 | 910 | | |
822 | 911 | | |
823 | 912 | | |
| |||
0 commit comments