Compare commits

..

12 Commits

Author SHA1 Message Date
Maxim Kartashev
edf48dab0c JBR-9902 Wayland: support modal dialogs without native modality aid 2026-01-23 16:44:52 +04:00
Maxim Kartashev
cb2d78320c JBR-9912 Wayland: humburger menu disappears prematurely 2026-01-23 16:38:11 +04:00
Maxim Kartashev
b1f0e67f78 JBR-9911 Wayland: window can be moved by accident 2026-01-23 11:30:58 +04:00
Nikita Provotorov
4d1f4d9cbd JBR-9904: Wayland: double shift or double ctrl is not working well after press space or alt key.
Fixing the timestamps of each KeyEvent/MouseEvent/MouseWheelEvent created by WLToolkit.
2026-01-22 14:28:21 +01:00
Maxim Kartashev
8666af5ba2 JBR-9803 Wayland: MOUSE_CLICKED event follows MOUSE_DRAG 2026-01-22 10:01:15 +04:00
Nikita Tsarev
035ca4c97f JBR-9898 Wayland: Keep clipboard ownership when copying within the same process
This patch makes the WLClipboard not call lostOwnershipNow() when a data
offer is available that was made previously by this clipboard. To do
this, WLClipboard's data sources announce a random cookie string as one
of the supported mime types. This string is then checked for when
receiving a data offer.
2026-01-20 11:05:06 +01:00
Maxim Kartashev
bb6c3af905 JBR-9893 Closed and disposed windows leak through WLGraphicsDevice.toplevels 2026-01-20 11:58:39 +04:00
Maxim Kartashev
e188216507 JBR-9871 Wayland: undecorated window crashes on close 2026-01-15 09:39:12 +04:00
Maxim Kartashev
076709a0bd JBR-9875 On wayland modal dialog moves the parent window when dragged
Support for gnome-specific dialog modality is disabled by default.
Use -Dsun.awt.wl.NativeModality=true to enable.
2026-01-14 19:12:27 +04:00
Nikita Tsarev
111c0a49c9 JBR-9874 Wayland: Add certain XF86 keysyms 2026-01-14 14:46:53 +01:00
Vladimir Lagunov
1d28022400 JBR-9779 io-over-nio: RandomAccessFile now can open Windows pipes
The WinAPI function `GetFileAttributesW` actually opens a file and immediately closes it. If the file is a named pipe, it triggers a connection to the listener of the pipe. The listener closes the connection also asynchronously.

If the pipe is created with `nMaxInstances=1`, this code does not work:

```java
var path = Path.of("""\\.\pipe\openssh-ssh-agent""")

// `readAttributes` calls `GetFileAttributesW`, it opens the file and then closes it.
if (Files.readAttributes(path, BasicFileAttributes.class).isRegularFile()) {
  // Very probable race condition: the listener of the pipe has already received
  // the request of opening the file triggered by `GetFileAttributesW`
  // but hasn't closed the connection on their side yet.
  Files.newByteChannel(path);  // Fails with ERROR_PIPE_BUSY.
}
```

This revision adds an explicit check if some file looks like a Windows pipe. In these cases `RandomAccessFile` and `FileInputStream` don't call `Files.readAttributes` for the file.
2025-12-18 12:48:14 +01:00
Vladimir Lagunov
5df06e8e80 JBR-9779 Refactor: extract common logic for getting nio path in FileInputStream, RandomAccessFile, and FileOutputStream 2025-12-17 16:09:53 +01:00
20 changed files with 492 additions and 131 deletions

View File

@@ -27,8 +27,6 @@ package java.io;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
@@ -177,25 +175,11 @@ public class FileInputStream extends InputStream
try (var guard = IoOverNio.RecursionGuard.create(FileInputStream.class)) {
IoOverNio.blackhole(guard);
java.nio.file.FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
Path nioPath = null;
if (nioFs != null && path != null) {
try {
nioPath = nioFs.getPath(path);
isRegularFile = Files.isRegularFile(nioPath);
} catch (InvalidPathException|SecurityException ignored) {
// Nothing.
}
}
// Two significant differences between the legacy java.io and java.nio.files:
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
useNio = nioPath != null && isRegularFile == Boolean.TRUE;
Path nioPath = IoOverNioFileSystem.getNioPath(file, true);
useNio = nioPath != null;
if (useNio) {
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, Set.of(StandardOpenOption.READ), channelCleanable);
this, nioPath.getFileSystem(), file, nioPath, Set.of(StandardOpenOption.READ), channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();

View File

@@ -249,11 +249,10 @@ public class FileOutputStream extends OutputStream
this.path = name;
try (var guard = IoOverNio.RecursionGuard.create(FileOutputStream.class)) {
IoOverNio.blackhole(guard);
java.nio.file.FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
useNio = path != null && nioFs != null;
Path nioPath = IoOverNioFileSystem.getNioPath(file, false);
useNio = nioPath != null;
if (useNio) {
Path nioPath = nioFs.getPath(path);
java.nio.file.FileSystem nioFs = nioPath.getFileSystem();
// java.io backend doesn't open DOS hidden files for writing, but java.nio.file opens.
// This code mimics the old behavior.
if (nioFs.getSeparator().equals("\\")) {

View File

@@ -41,6 +41,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@@ -58,6 +59,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@@ -123,6 +125,69 @@ class IoOverNioFileSystem extends FileSystem {
return result;
}
static Path getNioPath(File file, boolean mustBeRegularFile) {
String path = file.getPath();
java.nio.file.FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
if (nioFs == null) {
return null;
}
Path nioPath;
try {
nioPath = nioFs.getPath(path);
} catch (InvalidPathException ignored) {
return null;
}
if (!mustBeRegularFile) {
return nioPath;
}
if (isWindowsPipe(nioPath)) {
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea see nMaxInstances:
//
// Getting file attributes in this case is dangerous.
// GetFileAttributesW acquires a connection to the pipe internally,
// occupying a place on the server side.
// The server and the client are very likely two different processes, and it takes time to deliver
// the connection closing message to the server.
// If the caller invokes CreateFileW fast enough after GetFileAttributesW and nMaxInstances = 1,
// CreateFileW is called before the server closes the previous connection created by GetFileAttributesW
// and ERROR_PIPE_BUSY is returned.
//
// Anyway, `readAttributes(nioPath).isRegularFile()` returns true for pipes, so it's safe to return here.
return nioPath;
}
// Two significant differences between the legacy java.io and java.nio.files:
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
try {
if (Files.readAttributes(nioPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).isRegularFile()) {
return nioPath;
}
} catch (NoSuchFileException ignored) {
return nioPath;
} catch (IOException ignored) {
// Ignored.
}
return null;
}
/**
* <a href="https://learn.microsoft.com/en-us/windows/win32/ipc/pipe-names">
* The pipe path format: {@code ^\\(\w+|\.)\pipe\.*}
* </a>
*/
private static boolean isWindowsPipe(Path path) {
// A small JMH benchmark shows that this code takes less than a microsecond,
// and the JIT compiler does its job very well here.
return path.isAbsolute() &&
path.getRoot().toString().startsWith("\\\\") &&
path.getRoot().toString().toLowerCase(Locale.getDefault()).endsWith("\\pipe\\");
}
private static boolean setPermission0(java.nio.file.FileSystem nioFs, File f, int access, boolean enable, boolean owneronly) {
if (f.getPath().isEmpty()) {
if (nioFs.getSeparator().equals("\\")) {

View File

@@ -34,23 +34,10 @@ import sun.nio.ch.FileChannelImpl;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import java.nio.channels.NonWritableChannelException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashSet;
@@ -305,35 +292,11 @@ public class RandomAccessFile implements DataOutput, DataInput, Closeable {
try (var guard = IoOverNio.RecursionGuard.create(RandomAccessFile.class)) {
IoOverNio.blackhole(guard);
FileSystem nioFs = IoOverNioFileSystem.acquireNioFs(path);
Path nioPath = null;
if (nioFs != null) {
try {
nioPath = nioFs.getPath(path);
} catch (InvalidPathException ignored) {
// Nothing.
}
}
// Two significant differences between the legacy java.io and java.nio.files:
// * java.nio.file allows to open directories as streams, java.io.FileInputStream doesn't.
// * java.nio.file doesn't work well with pseudo devices, i.e., `seek()` fails, while java.io works well.
boolean isRegularFile;
try {
isRegularFile = nioPath != null &&
Files.readAttributes(nioPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).isRegularFile();
}
catch (NoSuchFileException ignored) {
isRegularFile = true;
}
catch (IOException ignored) {
isRegularFile = false;
}
useNio = nioPath != null && isRegularFile;
Path nioPath = IoOverNioFileSystem.getNioPath(file, true);
useNio = nioPath != null;
if (useNio) {
var bundle = IoOverNioFileSystem.initializeStreamUsingNio(
this, nioFs, file, nioPath, optionsForChannel(imode), channelCleanable);
this, nioPath.getFileSystem(), file, nioPath, optionsForChannel(imode), channelCleanable);
channel = bundle.channel();
fd = bundle.fd();
externalChannelHolder = bundle.externalChannelHolder();

View File

@@ -245,7 +245,10 @@ public abstract class FullFrameDecorationHelper extends FrameDecoration {
}
if (e.getID() == MouseEvent.MOUSE_PRESSED) {
pressedLocation = point;
} else if (e.getID() == MouseEvent.MOUSE_DRAGGED && pressedInDragStartArea(point) && isSignificantDrag(point)) {
} else if (e.getID() == MouseEvent.MOUSE_DRAGGED
&& pressedLocation != null
&& pressedInDragStartArea(pressedLocation)
&& isSignificantDrag(point)) {
startDrag();
} else if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() == 2 && pressedInDragStartArea(point)
&& peer.isFrameStateSupported(Frame.MAXIMIZED_BOTH)) {

View File

@@ -34,6 +34,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
public final class WLClipboard extends SunClipboard {
private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.wl.WLClipboard");
@@ -61,6 +62,10 @@ public final class WLClipboard extends SunClipboard {
// Guarded by dataLock.
private WLDataSource ourDataSource;
// Set when announcing a clipboard data source to a random value
// Guarded by dataLock.
private String ourDataSourceCookie = null;
static {
flavorTable = DataTransferer.adaptFlavorMap(getDefaultFlavorTable());
}
@@ -77,6 +82,10 @@ public final class WLClipboard extends SunClipboard {
}
}
private static String generateRandomMimeTypeCookie() {
return "JAVA_DATATRANSFER_COOKIE_" + UUID.randomUUID();
}
private int getProtocol() {
if (isPrimary) {
return WLDataDevice.DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION;
@@ -146,14 +155,26 @@ public final class WLClipboard extends SunClipboard {
log.fine("Clipboard: Offering new contents (" + contents + ")");
}
WLDataSource newOffer = null;
newOffer = new WLDataSource(dataDevice, getProtocol(), contents);
WLDataSource newOffer = new WLDataSource(dataDevice, getProtocol(), contents) {
@Override
protected void handleCancelled() {
synchronized (dataLock) {
if (ourDataSource == this) {
ourDataSource = null;
ourDataSourceCookie = null;
}
destroy();
}
}
};
synchronized (dataLock) {
if (ourDataSource != null) {
ourDataSource.destroy();
}
ourDataSource = newOffer;
ourDataSourceCookie = generateRandomMimeTypeCookie();
ourDataSource.offerExtraMime(ourDataSourceCookie);
dataDevice.setSelection(getProtocol(), newOffer, eventSerial);
}
}
@@ -240,9 +261,11 @@ public final class WLClipboard extends SunClipboard {
}
void handleClipboardOffer(WLDataOffer offer /* nullable */) {
lostOwnershipNow(null);
synchronized (dataLock) {
if (offer == null || ourDataSourceCookie == null || !offer.getMimes().contains(ourDataSourceCookie)) {
lostOwnershipNow(null);
}
if (clipboardDataOfferedToUs != null) {
clipboardDataOfferedToUs.destroy();
}

View File

@@ -120,6 +120,11 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("sun.awt.wl.Shadow", "true")));
@SuppressWarnings("removal")
private static final boolean nativeModalityEnabled = Boolean.parseBoolean(
java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("sun.awt.wl.NativeModality", "false")));
static {
initIDs();
}
@@ -460,9 +465,12 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
}
private boolean targetIsModal() {
return target instanceof Dialog dialog
&& (dialog.getModalityType() == Dialog.ModalityType.APPLICATION_MODAL
|| dialog.getModalityType() == Dialog.ModalityType.TOOLKIT_MODAL);
if (nativeModalityEnabled) {
return target instanceof Dialog dialog
&& (dialog.getModalityType() == Dialog.ModalityType.APPLICATION_MODAL
|| dialog.getModalityType() == Dialog.ModalityType.TOOLKIT_MODAL);
}
return false;
}
void updateSurfaceData() {
@@ -1141,6 +1149,14 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
}
}
final void reactivate(long serial, long surface) {
performLocked(() -> {
if (serial != 0 &&wlSurface != null && surface != 0) {
wlSurface.activateByAnotherSurface(serial, surface);
}
});
}
private static long getSerialForActivation() {
long serial;
if (WLToolkit.isKDE()) {
@@ -1230,14 +1246,14 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
* the freshly updated WLInputState, and the previous WLInputState.
*/
void dispatchPointerEventInContext(WLPointerEvent e, WLInputState oldInputState, WLInputState newInputState) {
final long timestamp = System.currentTimeMillis();
final int x = newInputState.getPointerX();
final int y = newInputState.getPointerY();
final Point abs = relativePointToAbsolute(new Point(x, y));
int xAbsolute = abs.x;
int yAbsolute = abs.y;
final long timestamp = newInputState.getTimestamp();
if (e.hasEnterEvent()) {
updateCursorImmediately();
final MouseEvent mouseEvent = new MouseEvent(getTarget(), MouseEvent.MOUSE_ENTERED,
@@ -1273,7 +1289,7 @@ public class WLComponentPeer implements ComponentPeer, WLSurfaceSizeListener {
postMouseEvent(mouseEvent);
final boolean isButtonReleased = !e.getIsButtonPressed();
final boolean wasSameButtonPressed = oldInputState.hasThisPointerButtonPressed(e.getButtonCode());
final boolean wasSameButtonPressed = oldInputState.hasThisPointerButtonPressedAt(e.getButtonCode(), x, y);
final boolean isButtonClicked = isButtonReleased && wasSameButtonPressed;
if (isButtonClicked) {
final MouseEvent mouseClickEvent = new MouseEvent(getTarget(),

View File

@@ -121,6 +121,13 @@ public class WLDataSource {
setDnDIconImpl(nativePtr, scale, width, height, offsetX, offsetY, pixels);
}
public void offerExtraMime(String mime) {
if (nativePtr == 0) {
throw new IllegalStateException("Native pointer is null");
}
offerMimeImpl(nativePtr, mime);
}
public synchronized void destroy() {
if (nativePtr != 0) {
destroyImpl(nativePtr);

View File

@@ -27,12 +27,10 @@ package sun.awt.wl;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.MouseEvent;
import java.awt.event.WindowEvent;
public abstract class WLDecoratedPeer extends WLWindowPeer {
private FrameDecoration decoration; // protected by stateLock
@@ -97,14 +95,6 @@ public abstract class WLDecoratedPeer extends WLWindowPeer {
return ((WLToolkit) WLToolkit.getDefaultToolkit()).checkGtkVersion(3, 20, 0);
}
private static native void initIDs();
static {
if (!GraphicsEnvironment.isHeadless()) {
initIDs();
}
}
public abstract boolean isInteractivelyResizable();
public abstract boolean isFrameStateSupported(int state);
public abstract void setState(int newState);
@@ -171,11 +161,6 @@ public abstract class WLDecoratedPeer extends WLWindowPeer {
super.updateWindow();
}
// called from native code
void postWindowClosing() {
WLToolkit.postEvent(new WindowEvent((Window) target, WindowEvent.WINDOW_CLOSING));
}
@Override
void postMouseEvent(MouseEvent e) {
boolean processed = getDecoration().processMouseEvent(e);

View File

@@ -24,8 +24,13 @@
*/
package sun.awt.wl;
import java.awt.*;
import sun.awt.AWTAccessor;
import java.awt.Dialog;
import java.awt.Frame;
import java.awt.Window;
import java.awt.peer.DialogPeer;
import java.awt.peer.WindowPeer;
import java.util.List;
public class WLDialogPeer extends WLDecoratedPeer implements DialogPeer {
@@ -41,7 +46,12 @@ public class WLDialogPeer extends WLDecoratedPeer implements DialogPeer {
@Override
public void blockWindows(List<Window> windows) {
for (Window w : windows) {
WindowPeer wp = AWTAccessor.getComponentAccessor().getPeer(w);
if (wp != null) {
wp.setModalBlocked((Dialog)getTarget(), true);
}
}
}
@Override

View File

@@ -35,7 +35,10 @@ import java.awt.GraphicsDevice;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Window;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
@@ -119,7 +122,7 @@ public class WLGraphicsDevice extends GraphicsDevice {
* Top-level window peers that consider this device as their primary one
* and get their graphics configuration from it
*/
private final Set<WLComponentPeer> toplevels = new HashSet<>(); // guarded by 'this'
private final Set<WeakReference<WLComponentPeer>> toplevels = new HashSet<>(); // guarded by 'this'
private WLGraphicsDevice(int id,
String name,
@@ -205,9 +208,14 @@ public class WLGraphicsDevice extends GraphicsDevice {
}
private void notifyToplevels() {
Set<WLComponentPeer> toplevelsCopy = new HashSet<>(toplevels.size());
List<WLComponentPeer> toplevelsCopy = new ArrayList<>(toplevels.size());
synchronized (this) {
toplevelsCopy.addAll(toplevels);
for (var toplevel: toplevels) {
WLComponentPeer peer = toplevel.get();
if (peer != null) {
toplevelsCopy.add(peer);
}
}
}
toplevelsCopy.forEach(WLComponentPeer::checkIfOnNewScreen);
}
@@ -369,13 +377,16 @@ public class WLGraphicsDevice extends GraphicsDevice {
public void addWindow(WLComponentPeer peer) {
synchronized (this) {
toplevels.add(peer);
toplevels.add(new WeakReference<>(peer));
}
}
public void removeWindow(WLComponentPeer peer) {
synchronized (this) {
toplevels.remove(peer);
toplevels.removeIf(ref -> {
WLComponentPeer p = ref.get();
return p == null || p == peer;
});
}
}

View File

@@ -295,8 +295,9 @@ record WLInputState(WLPointerEvent eventWithSurface,
return newModifiers;
}
public boolean hasThisPointerButtonPressed(int linuxCode) {
return pointerButtonPressedEvent != null && pointerButtonPressedEvent.linuxCode == linuxCode;
public boolean hasThisPointerButtonPressedAt(int linuxCode, long x, long y) {
return pointerButtonPressedEvent != null && pointerButtonPressedEvent.linuxCode == linuxCode
&& pointerButtonPressedEvent.surfaceX == x && pointerButtonPressedEvent.surfaceY == y;
}
public boolean hasPointerButtonPressed() {
@@ -353,6 +354,12 @@ record WLInputState(WLPointerEvent eventWithSurface,
return true;
}
/**
* @return the timestamp of the most recent known {@code wl_pointer::*} event or 0 if no such event is known.
*
* @apiNote don't use these timestamps for constructing any new {@link java.awt.event.InputEvent}s
* because they are not guaranteed to be based on the midnight of Jan 1, 1970 UTC.
*/
public long getTimestamp() {
return eventWithTimestamp != null ? eventWithTimestamp.getTimestamp() : 0;
}

View File

@@ -6,11 +6,12 @@ import sun.util.logging.PlatformLogger;
import java.awt.Component;
import java.awt.Window;
import java.lang.ref.WeakReference;
public class WLKeyboardFocusManagerPeer extends KeyboardFocusManagerPeerImpl {
private static final PlatformLogger focusLog = PlatformLogger.getLogger("sun.awt.wl.focus.WLKeyboardFocusManagerPeer");
private Window currentFocusedWindow;
private WeakReference<Window> currentFocusedWindow = new WeakReference<>(null);
private static final WLKeyboardFocusManagerPeer instance = new WLKeyboardFocusManagerPeer();
public static WLKeyboardFocusManagerPeer getInstance() {
@@ -23,14 +24,14 @@ public class WLKeyboardFocusManagerPeer extends KeyboardFocusManagerPeerImpl {
if (focusLog.isLoggable(PlatformLogger.Level.FINER)) {
focusLog.finer("Current focused window -> " + win);
}
currentFocusedWindow = win;
currentFocusedWindow = new WeakReference<>(win);
}
}
@Override
public Window getCurrentFocusedWindow() {
synchronized (this) {
return currentFocusedWindow;
return currentFocusedWindow.get();
}
}
@@ -61,7 +62,7 @@ public class WLKeyboardFocusManagerPeer extends KeyboardFocusManagerPeerImpl {
@Override
public Component getCurrentFocusOwner() {
synchronized (this) {
return currentFocusedWindow;
return currentFocusedWindow.get();
}
}
}

View File

@@ -367,7 +367,6 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
}
private static void dispatchKeyboardKeyEvent(long serial,
long timestamp,
int id,
int keyCode,
int keyLocation,
@@ -378,13 +377,9 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
// Invoked from the native code
assert EventQueue.isDispatchThread() : "Method must only be invoked on EDT";
inputState = inputState.updatedFromKeyEvent(serial);
final long timestamp = System.currentTimeMillis();
if (timestamp == 0) {
// Happens when a surface was focused with keys already pressed.
// Fake the timestamp by peeking at the last known event.
timestamp = inputState.getTimestamp();
}
inputState = inputState.updatedFromKeyEvent(serial);
final long surfacePtr = inputState.surfaceForKeyboardInput();
final WLComponentPeer peer = peerFromSurface(surfacePtr);
@@ -453,22 +448,32 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
+ Long.toHexString(surfacePtr));
}
final WLInputState newInputState = inputState.updatedFromKeyboardEnterEvent(serial, surfacePtr);
final WLWindowPeer peer = peerFromSurface(surfacePtr);
final WLInputState newInputState = inputState.updatedFromKeyboardEnterEvent(serial, surfacePtr);
if (peer != null) {
Window window = (Window) peer.getTarget();
Window winToFocus = window;
Component s = peer.getSyntheticFocusOwner();
if (s instanceof Window synthWindow) {
if (synthWindow.isVisible() && synthWindow.isFocusableWindow()) {
winToFocus = synthWindow;
Dialog blocker = peer.getBlocker();
if (blocker != null) { // Modality support
long activationSerial = serial;
if (WLToolkit.isKDE()) {
activationSerial = inputState.latestInputSerial();
}
}
WLWindowPeer blockerPeer = AWTAccessor.getComponentAccessor().getPeer(blocker);
blockerPeer.reactivate(activationSerial, surfacePtr);
} else {
Window window = (Window) peer.getTarget();
Window winToFocus = window;
WLKeyboardFocusManagerPeer.getInstance().setCurrentFocusedWindow(window);
WindowEvent windowEnterEvent = new WindowEvent(winToFocus, WindowEvent.WINDOW_GAINED_FOCUS);
postPriorityEvent(windowEnterEvent);
Component s = peer.getSyntheticFocusOwner();
if (s instanceof Window synthWindow) {
if (synthWindow.isVisible() && synthWindow.isFocusableWindow()) {
winToFocus = synthWindow;
}
}
WLKeyboardFocusManagerPeer.getInstance().setCurrentFocusedWindow(window);
WindowEvent windowEnterEvent = new WindowEvent(winToFocus, WindowEvent.WINDOW_GAINED_FOCUS);
postPriorityEvent(windowEnterEvent);
}
}
inputState = newInputState;
}
@@ -484,12 +489,13 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
keyboard.onLostFocus();
WLKeyboardFocusManagerPeer.getInstance().setCurrentFocusedWindow(null);
WLKeyboardFocusManagerPeer.getInstance().setCurrentFocusOwner(null);
final WLInputState newInputState = inputState.updatedFromKeyboardLeaveEvent(serial, surfacePtr);
final WLWindowPeer peer = peerFromSurface(surfacePtr);
if (peer != null && peer.getTarget() instanceof Window window) {
final WindowEvent winLostFocusEvent = new WindowEvent(window, WindowEvent.WINDOW_LOST_FOCUS);
WLKeyboardFocusManagerPeer.getInstance().setCurrentFocusedWindow(null);
WLKeyboardFocusManagerPeer.getInstance().setCurrentFocusOwner(null);
postPriorityEvent(winLostFocusEvent);
}
inputState = newInputState;
@@ -1140,6 +1146,17 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
protected static void targetDisposedPeer(Object target, Object peer) {
SunToolkit.targetDisposedPeer(target, peer);
if (target instanceof Window window) {
// TODO: focusedWindow and activeWindow of class java.awt.KeyboardFocusManager
// may still retain references to 'window' because disposed peer may not
// get the keyboard_leave event and therefore will not send the WINDOW_LOST_FOCUS
// event that would've cleared those references.
var gc = window.getGraphicsConfiguration();
if (gc != null && peer instanceof WLWindowPeer windowPeer) {
WLGraphicsDevice gd = (WLGraphicsDevice) gc.getDevice();
gd.removeWindow(windowPeer);
}
}
}
static void postEvent(AWTEvent event) {

View File

@@ -41,6 +41,7 @@ import java.awt.Dialog;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
@@ -57,7 +58,7 @@ import java.util.List;
public class WLWindowPeer extends WLComponentPeer implements WindowPeer, SurfacePixelGrabber {
private static Font defaultFont;
private Dialog blocker;
private Dialog blocker; // guarded by getStateLock()
private static WLWindowPeer grabbingWindow; // fake, kept for UngrabEvent only
// If this window gets focus from Wayland, we need to transfer focus synthFocusOwner, if any
@@ -72,6 +73,12 @@ public class WLWindowPeer extends WLComponentPeer implements WindowPeer, Surface
private Path2D.Double bottomRightMask; // guarded by stateLock
private SunGraphics2D graphics; // guarded by stateLock
static {
if (!GraphicsEnvironment.isHeadless()) {
initIDs();
}
}
static synchronized Font getDefaultFont() {
if (null == defaultFont) {
defaultFont = new Font(Font.DIALOG, Font.PLAIN, 12);
@@ -173,7 +180,15 @@ public class WLWindowPeer extends WLComponentPeer implements WindowPeer, Surface
@Override
public void setModalBlocked(Dialog blocker, boolean blocked) {
this.blocker = blocked ? blocker : null;
synchronized (getStateLock()) {
this.blocker = blocked ? blocker : null;
}
}
public Dialog getBlocker() {
synchronized (getStateLock()) {
return blocker;
}
}
@Override
@@ -311,6 +326,11 @@ public class WLWindowPeer extends WLComponentPeer implements WindowPeer, Surface
}
}
// called from native code
void postWindowClosing() {
WLToolkit.postEvent(new WindowEvent((Window) target, WindowEvent.WINDOW_CLOSING));
}
@Override
public BufferedImage getClientAreaSnapshot(int x, int y, int width, int height) {
// Move the coordinate system to the client area
@@ -464,4 +484,6 @@ public class WLWindowPeer extends WLComponentPeer implements WindowPeer, Surface
graphics.fill(bottomRightMask);
}
}
private static native void initIDs();
}

View File

@@ -43,7 +43,7 @@
#include "wakefield-client-protocol.h"
#endif
static jmethodID postWindowClosingMID;
static jmethodID postWindowClosingMID; // WLWindowPeer.postWindowClosing
static jmethodID notifyConfiguredMID;
static jmethodID notifyPopupDoneMID;
@@ -171,7 +171,7 @@ xdg_toplevel_close(void *data,
struct WLFrame *frame = (struct WLFrame *) data;
JNIEnv *env = getEnv();
const jobject nativeFramePeer = (*env)->NewLocalRef(env, frame->nativeFramePeer);
if (nativeFramePeer) {
if (nativeFramePeer) { // a reference to WLWinowPeer or its descendant
(*env)->CallVoidMethod(env, nativeFramePeer, postWindowClosingMID);
(*env)->DeleteLocalRef(env, nativeFramePeer);
JNU_CHECK_EXCEPTION(env);
@@ -223,12 +223,12 @@ Java_sun_awt_wl_WLComponentPeer_initIDs
}
JNIEXPORT void JNICALL
Java_sun_awt_wl_WLDecoratedPeer_initIDs
Java_sun_awt_wl_WLWindowPeer_initIDs
(JNIEnv *env, jclass clazz)
{
CHECK_NULL_THROW_IE(env,
postWindowClosingMID = (*env)->GetMethodID(env, clazz, "postWindowClosing", "()V"),
"Failed to find method WLDecoratedPeer.postWindowClosing");
"Failed to find method WLWindowPeer.postWindowClosing");
}
JNIEXPORT jlong JNICALL

View File

@@ -386,6 +386,10 @@ static const struct KeysymToJavaKeycodeMapItem {
{0x1005ff72 /* XKB_KEY_SunCopy */, java_awt_event_KeyEvent_VK_COPY, java_awt_event_KeyEvent_KEY_LOCATION_STANDARD},
{0x1005ff74 /* XKB_KEY_SunPaste */, java_awt_event_KeyEvent_VK_PASTE, java_awt_event_KeyEvent_KEY_LOCATION_STANDARD},
{0x1005ff75 /* XKB_KEY_SunCut */, java_awt_event_KeyEvent_VK_CUT, java_awt_event_KeyEvent_KEY_LOCATION_STANDARD},
{0x1008ff55 /* XKB_KEY_XF86Clear */, java_awt_event_KeyEvent_VK_CLEAR, java_awt_event_KeyEvent_KEY_LOCATION_STANDARD},
{0x1008ff57 /* XKB_KEY_XF86Copy */, java_awt_event_KeyEvent_VK_COPY, java_awt_event_KeyEvent_KEY_LOCATION_STANDARD},
{0x1008ff58 /* XKB_KEY_XF86Cut */, java_awt_event_KeyEvent_VK_CUT, java_awt_event_KeyEvent_KEY_LOCATION_STANDARD},
{0x1008ff6d /* XKB_KEY_XF86Paste */, java_awt_event_KeyEvent_VK_PASTE, java_awt_event_KeyEvent_KEY_LOCATION_STANDARD},
{0, 0, 0}
};

View File

@@ -447,7 +447,6 @@ wlPostKeyEvent(const struct WLKeyEvent* event)
tkClass,
dispatchKeyboardKeyEventMID,
event->serial,
event->timestamp,
event->id,
event->keyCode,
event->keyLocation,
@@ -795,7 +794,7 @@ initJavaRefs(JNIEnv *env, jclass clazz)
JNI_FALSE);
CHECK_NULL_RETURN(dispatchKeyboardKeyEventMID = (*env)->GetStaticMethodID(env, tkClass,
"dispatchKeyboardKeyEvent",
"(JJIIIIICI)V"),
"(JIIIIICI)V"),
JNI_FALSE);
CHECK_NULL_RETURN(dispatchKeyboardModifiersEventMID = (*env)->GetStaticMethodID(env, tkClass,
"dispatchKeyboardModifiersEvent",

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2026 JetBrains s.r.o.
* Copyright (c) 2026, 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @key headful
* @summary Tests that nothing prevents windows from being removed from the global list
* after they have been disposed
* @library /javax/swing/regtesthelpers
* @modules java.desktop/sun.awt
* java.desktop/sun.java2d
* @build Util
* @run main/othervm -Xms32M -Xmx32M WLWindowsLeak
*/
import java.awt.Color;
import java.awt.Frame;
import java.awt.Robot;
import java.awt.Window;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import com.sun.management.HotSpotDiagnosticMXBean;
import sun.awt.AppContext;
import sun.java2d.Disposer;
public class WLWindowsLeak {
public static final int WINDOWS_COUNT = 42;
private static volatile boolean disposerPhantomComplete;
public static void main(String[] args) throws Exception {
spawnWindows();
Disposer.addRecord(new Object(), () -> disposerPhantomComplete = true);
while (!disposerPhantomComplete) {
Util.generateOOME();
}
Vector<WeakReference<Window>> windowList =
(Vector<WeakReference<Window>>) AppContext.getAppContext().get(Window.class);
// Note: focusedWindow and activeWindow of class java.awt.KeyboardFocusManager
// may still retain references to the last window shown. This makes the test flaky and
// therefore less useful. Until this is fixed, the test allows for 1 retained window.
if (windowList != null && !windowList.isEmpty() && windowList.size() > 1) {
dumpHeap("heap_dump_live_windows.hprof");
System.out.println("Live window list:");
windowList.forEach(ref -> System.out.println(ref.get()));
throw new RuntimeException("Test FAILED: Window list is not empty: " + windowList.size());
}
System.out.println("Test PASSED");
}
private static void dumpHeap(String filePath) {
try {
HotSpotDiagnosticMXBean mxBean = ManagementFactory
.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
mxBean.dumpHeap(filePath, true); // true = live objects only
System.out.println("Heap dump created at: " + filePath);
} catch (IOException e) {
System.err.println("Failed to dump heap: " + e.getMessage());
}
}
private static void spawnWindows() throws Exception {
List<WeakReference<Frame>> frameList = new ArrayList<>();
Robot r = new Robot();
for (int i = 0; i < WINDOWS_COUNT; i++) {
Frame f = new Frame(String.format("Frame %d", i));
f.setBackground(Color.WHITE);
f.setSize(100, 100);
f.setVisible(true);
frameList.add(new WeakReference<>(f));
Thread.sleep(42);
}
r.waitForIdle();
frameList.forEach(ref -> {
ref.get().setVisible(false);
});
r.waitForIdle();
frameList.forEach(ref -> {
var f = ref.get();
if (f != null) f.dispose();
});
frameList.clear();
r.waitForIdle(); // to make sure no events hold a reference to frames
System.gc();
}
}

View File

@@ -24,11 +24,14 @@
/* @test
* @summary java.io.RandomAccessFileTest uses java.nio.file inside.
* @library testNio
* @compile --enable-preview --source 21 RandomAccessFileTest.java
* @run junit/othervm
* -Djava.nio.file.spi.DefaultFileSystemProvider=testNio.ManglingFileSystemProvider
* -Djbr.java.io.use.nio=true
* --add-opens jdk.unsupported/com.sun.nio.file=ALL-UNNAMED
* --add-opens java.base/java.io=ALL-UNNAMED
* --enable-native-access=ALL-UNNAMED
* --enable-preview
* RandomAccessFileTest
*/
@@ -45,6 +48,9 @@ import testNio.ManglingFileSystemProvider;
import java.io.EOFException;
import java.io.File;
import java.io.RandomAccessFile;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
@@ -55,6 +61,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -293,4 +300,122 @@ public class RandomAccessFileTest {
FileSystems.getDefault().provider().newFileChannel(file.toPath(), Collections.singleton(option)).close();
}
}
/**
* JBR-9779
*/
@Test
public void testWindowsPipe() throws Throwable {
Assume.assumeTrue("Windows-only test", System.getProperty("os.name").toLowerCase().startsWith("win"));
// Creating a pipe.
Linker linker = Linker.nativeLinker();
SymbolLookup loader = SymbolLookup.libraryLookup("kernel32", Arena.global());
StructLayout captureLayout = Linker.Option.captureStateLayout();
VarHandle GetLastError = captureLayout.varHandle(MemoryLayout.PathElement.groupElement("GetLastError"));
MethodHandle CreateNamedPipeW = linker.downcallHandle(
loader.find("CreateNamedPipeW").get(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT,
ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT,
ValueLayout.ADDRESS),
Linker.Option.captureCallState("GetLastError")
);
MethodHandle ConnectNamedPipe = linker.downcallHandle(
loader.find("ConnectNamedPipe").get(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS),
Linker.Option.captureCallState("GetLastError")
);
MethodHandle DisconnectNamedPipe = linker.downcallHandle(
loader.find("DisconnectNamedPipe").get(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG)
);
MethodHandle PeekNamedPipe = linker.downcallHandle(
loader.find("PeekNamedPipe").get(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS,
ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
MethodHandle CloseHandle = linker.downcallHandle(
loader.find("CloseHandle").get(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG)
);
String pipeName = "\\\\.\\pipe\\jbr-test-pipe-" + System.nanoTime();
Arena arena = Arena.ofAuto();
char[] nameChars = (pipeName + "\0").toCharArray(); // `char` on Windows is UTF-16, as WinAPI expects.
MemorySegment pipeWinPath = arena.allocateArray(ValueLayout.JAVA_CHAR, nameChars);
MemorySegment capturedState = arena.allocate(captureLayout);
final long INVALID_HANDLE_VALUE = -1L;
long hPipe = (long) CreateNamedPipeW.invokeExact(
capturedState,
pipeWinPath,
0x00000003, // dwOpenMode = PIPE_ACCESS_DUPLEX
0x00000000, // dwPipeMode = PIPE_TYPE_BYTE
1, // nMaxInstances. Limit to 1 to force ERROR_PIPE_BUSY.
1024, // nOutBufferSize
1024, // nInBufferSize
0, // nDefaultTimeOut
MemorySegment.NULL // lpSecurityAttributes
);
if (hPipe == INVALID_HANDLE_VALUE) {
int errorCode = (int) GetLastError.get(capturedState);
throw new Exception("CreateNamedPipeW failed: " + errorCode);
}
AtomicBoolean keepRunning = new AtomicBoolean(true);
Thread serverThread = new Thread(() -> {
// This server accepts a connection and does nothing until the client disconnects explicitly.
try {
int i = 0;
while (keepRunning.get()) {
int connected = (int) ConnectNamedPipe.invokeExact(capturedState, hPipe, MemorySegment.NULL);
if (connected == 0) {
int errorCode = (int) GetLastError.get(capturedState);
// https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
if (errorCode == 6 && !keepRunning.get()) { // ERROR_INVALID_HANDLE
break;
}
throw new Exception("ConnectNamedPipe failed: " + errorCode);
}
try {
int peekResult;
do {
// Random timeout. The timeout must be big enough to reveal possible consequent
// attempts to connect to the pipe.
Thread.sleep(1000);
// Check if the pipe is still connected by peeking at it.
peekResult = (int) PeekNamedPipe.invokeExact(hPipe, MemorySegment.NULL, 0,
MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL);
}
while (keepRunning.get() && peekResult != 0);
} finally {
int disconnected = (int) DisconnectNamedPipe.invokeExact(hPipe);
}
}
} catch (Throwable e) {
e.printStackTrace();
}
});
serverThread.setDaemon(true);
serverThread.start();
try {
new RandomAccessFile(pipeName, "rw").close();
} finally {
keepRunning.set(false);
int closed = (int) CloseHandle.invokeExact(hPipe);
}
}
}