mirror of
https://github.com/openjdk/jdk.git
synced 2026-03-15 18:33:41 +00:00
611 lines
22 KiB
Java
611 lines
22 KiB
Java
/*
|
|
* Copyright (c) 2004, 2025, 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. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* 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.
|
|
*/
|
|
package sun.awt;
|
|
|
|
import java.awt.RenderingHints;
|
|
|
|
import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
|
|
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
|
|
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
|
|
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
|
|
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
|
|
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
|
|
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
|
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
|
|
|
import java.awt.color.ColorSpace;
|
|
|
|
import java.awt.Window;
|
|
import java.awt.event.WindowAdapter;
|
|
import java.awt.event.WindowEvent;
|
|
import java.awt.event.WindowFocusListener;
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.ColorModel;
|
|
import java.awt.image.ComponentColorModel;
|
|
import java.awt.image.DataBuffer;
|
|
import java.awt.image.DataBufferByte;
|
|
import java.awt.image.Raster;
|
|
import java.awt.image.WritableRaster;
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
|
|
import sun.awt.X11.XBaseWindow;
|
|
import com.sun.java.swing.plaf.gtk.GTKConstants.TextDirection;
|
|
import sun.java2d.opengl.OGLRenderQueue;
|
|
|
|
public abstract class UNIXToolkit extends SunToolkit
|
|
{
|
|
/** All calls into GTK should be synchronized on this lock */
|
|
public static final Object GTK_LOCK = new Object();
|
|
|
|
private static final int[] BAND_OFFSETS = { 0, 1, 2 };
|
|
private static final int[] BAND_OFFSETS_ALPHA = { 0, 1, 2, 3 };
|
|
private static final int DEFAULT_DATATRANSFER_TIMEOUT = 10000;
|
|
|
|
// Allowed GTK versions
|
|
public enum GtkVersions {
|
|
ANY(0),
|
|
GTK3(Constants.GTK3_MAJOR_NUMBER);
|
|
|
|
static final class Constants {
|
|
static final int GTK3_MAJOR_NUMBER = 3;
|
|
}
|
|
|
|
final int number;
|
|
|
|
GtkVersions(int number) {
|
|
this.number = number;
|
|
}
|
|
|
|
public static GtkVersions getVersion(int number) {
|
|
switch (number) {
|
|
case Constants.GTK3_MAJOR_NUMBER:
|
|
return GTK3;
|
|
default:
|
|
return ANY;
|
|
}
|
|
}
|
|
|
|
// major GTK version number
|
|
public int getNumber() {
|
|
return number;
|
|
}
|
|
}
|
|
|
|
private Boolean nativeGTKAvailable;
|
|
private Boolean nativeGTKLoaded;
|
|
private BufferedImage tmpImage = null;
|
|
|
|
public static int getDatatransferTimeout() {
|
|
Integer dt = Integer.getInteger("sun.awt.datatransfer.timeout");
|
|
if (dt == null || dt <= 0) {
|
|
return DEFAULT_DATATRANSFER_TIMEOUT;
|
|
} else {
|
|
return dt;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String getDesktop() {
|
|
String gnome = "gnome";
|
|
String gsi = System.getenv("GNOME_DESKTOP_SESSION_ID");
|
|
if (gsi != null) {
|
|
return gnome;
|
|
}
|
|
|
|
String desktop = System.getenv("XDG_CURRENT_DESKTOP");
|
|
return (desktop != null && desktop.toLowerCase().contains(gnome))
|
|
? gnome : null;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the native GTK libraries are capable of being
|
|
* loaded and are expected to work properly, false otherwise. Note
|
|
* that this method will not leave the native GTK libraries loaded if
|
|
* they haven't already been loaded. This allows, for example, Swing's
|
|
* GTK L&F to test for the presence of native GTK support without
|
|
* leaving the native libraries loaded. To attempt long-term loading
|
|
* of the native GTK libraries, use the loadGTK() method instead.
|
|
*/
|
|
@Override
|
|
public boolean isNativeGTKAvailable() {
|
|
synchronized (GTK_LOCK) {
|
|
if (nativeGTKLoaded != null) {
|
|
// We've already attempted to load GTK, so just return the
|
|
// status of that attempt.
|
|
return nativeGTKLoaded;
|
|
|
|
} else if (nativeGTKAvailable != null) {
|
|
// We've already checked the availability of the native GTK
|
|
// libraries, so just return the status of that attempt.
|
|
return nativeGTKAvailable;
|
|
|
|
} else {
|
|
boolean success = check_gtk(getEnabledGtkVersion().getNumber());
|
|
nativeGTKAvailable = success;
|
|
return success;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the GTK libraries, if necessary. The first time this method
|
|
* is called, it will attempt to load the native GTK library. If
|
|
* successful, it leaves the library open and returns true; otherwise,
|
|
* the library is left closed and returns false. On future calls to
|
|
* this method, the status of the first attempt is returned (a simple
|
|
* lightweight boolean check, no native calls required).
|
|
*/
|
|
public boolean loadGTK() {
|
|
synchronized (GTK_LOCK) {
|
|
if (nativeGTKLoaded == null) {
|
|
nativeGTKLoaded = load_gtk(getEnabledGtkVersion().getNumber(),
|
|
isGtkVerbose());
|
|
}
|
|
}
|
|
return nativeGTKLoaded;
|
|
}
|
|
|
|
/**
|
|
* Overridden to handle GTK icon loading
|
|
*/
|
|
@Override
|
|
protected Object lazilyLoadDesktopProperty(String name) {
|
|
if (name.startsWith("gtk.icon.")) {
|
|
return lazilyLoadGTKIcon(name);
|
|
}
|
|
return super.lazilyLoadDesktopProperty(name);
|
|
}
|
|
|
|
/**
|
|
* Load a native Gtk stock icon.
|
|
*
|
|
* @param longname a desktop property name. This contains icon name, size
|
|
* and orientation, e.g. {@code "gtk.icon.gtk-add.4.rtl"}
|
|
* @return an {@code Image} for the icon, or {@code null} if the
|
|
* icon could not be loaded
|
|
*/
|
|
protected Object lazilyLoadGTKIcon(String longname) {
|
|
// Check if we have already loaded it.
|
|
Object result = desktopProperties.get(longname);
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
|
|
// We need to have at least gtk.icon.<stock_id>.<size>.<orientation>
|
|
String[] str = longname.split("\\.");
|
|
if (str.length != 5) {
|
|
return null;
|
|
}
|
|
|
|
// Parse out the stock icon size we are looking for.
|
|
int size = 0;
|
|
try {
|
|
size = Integer.parseInt(str[3]);
|
|
} catch (NumberFormatException nfe) {
|
|
return null;
|
|
}
|
|
|
|
// Direction.
|
|
TextDirection dir = ("ltr".equals(str[4]) ? TextDirection.LTR :
|
|
TextDirection.RTL);
|
|
|
|
// Load the stock icon.
|
|
BufferedImage img = getStockIcon(-1, str[2], size, dir.ordinal(), null);
|
|
if (img != null) {
|
|
// Create the desktop property for the icon.
|
|
setDesktopProperty(longname, img);
|
|
}
|
|
return img;
|
|
}
|
|
|
|
private static volatile Boolean shouldDisableSystemTray = null;
|
|
|
|
/**
|
|
* There is an issue displaying the xembed icons in appIndicators
|
|
* area with certain Gnome Shell versions.
|
|
* To avoid any loss of quality of service, we are disabling
|
|
* SystemTray support in such cases.
|
|
*
|
|
* @return true if system tray should be disabled
|
|
*/
|
|
public boolean shouldDisableSystemTray() {
|
|
Boolean result = shouldDisableSystemTray;
|
|
if (result == null) {
|
|
synchronized (GTK_LOCK) {
|
|
result = shouldDisableSystemTray;
|
|
if (result == null) {
|
|
if ("gnome".equals(getDesktop())) {
|
|
Integer gnomeShellMajorVersion = getGnomeShellMajorVersion();
|
|
|
|
if (gnomeShellMajorVersion == null
|
|
|| gnomeShellMajorVersion < 45) {
|
|
|
|
return shouldDisableSystemTray = true;
|
|
}
|
|
}
|
|
shouldDisableSystemTray = result = false;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private Integer getGnomeShellMajorVersion() {
|
|
try {
|
|
Process process =
|
|
new ProcessBuilder("/usr/bin/gnome-shell", "--version")
|
|
.start();
|
|
try (InputStreamReader isr = new InputStreamReader(process.getInputStream());
|
|
BufferedReader reader = new BufferedReader(isr)) {
|
|
|
|
if (process.waitFor(2, SECONDS) && process.exitValue() == 0) {
|
|
String line = reader.readLine();
|
|
if (line != null) {
|
|
String[] versionComponents = line
|
|
.replaceAll("[^\\d.]", "")
|
|
.split("\\.");
|
|
|
|
if (versionComponents.length >= 1) {
|
|
return Integer.parseInt(versionComponents[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (IOException
|
|
| InterruptedException
|
|
| IllegalThreadStateException
|
|
| NumberFormatException ignored) {
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns a BufferedImage which contains the Gtk icon requested. If no
|
|
* such icon exists or an error occurs loading the icon the result will
|
|
* be null.
|
|
*
|
|
* @param filename
|
|
* @return The icon or null if it was not found or loaded.
|
|
*/
|
|
public BufferedImage getGTKIcon(final String filename) {
|
|
if (!loadGTK()) {
|
|
return null;
|
|
|
|
} else {
|
|
// Call the native method to load the icon.
|
|
synchronized (GTK_LOCK) {
|
|
if (!load_gtk_icon(filename)) {
|
|
tmpImage = null;
|
|
}
|
|
}
|
|
}
|
|
// Return local image the callback loaded the icon into.
|
|
return tmpImage;
|
|
}
|
|
|
|
/**
|
|
* Returns a BufferedImage which contains the Gtk stock icon requested.
|
|
* If no such stock icon exists the result will be null.
|
|
*
|
|
* @param widgetType one of WidgetType values defined in GTKNativeEngine or
|
|
* -1 for system default stock icon.
|
|
* @param stockId String which defines the stock id of the gtk item.
|
|
* For a complete list reference the API at www.gtk.org for StockItems.
|
|
* @param iconSize One of the GtkIconSize values defined in GTKConstants
|
|
* @param direction One of the TextDirection values defined in
|
|
* GTKConstants
|
|
* @param detail Render detail that is passed to the native engine (feel
|
|
* free to pass null)
|
|
* @return The stock icon or null if it was not found or loaded.
|
|
*/
|
|
public BufferedImage getStockIcon(final int widgetType, final String stockId,
|
|
final int iconSize, final int direction,
|
|
final String detail) {
|
|
if (!loadGTK()) {
|
|
return null;
|
|
|
|
} else {
|
|
// Call the native method to load the icon.
|
|
synchronized (GTK_LOCK) {
|
|
if (!load_stock_icon(widgetType, stockId, iconSize, direction, detail)) {
|
|
tmpImage = null;
|
|
}
|
|
}
|
|
}
|
|
// Return local image the callback loaded the icon into.
|
|
return tmpImage; // set by loadIconCallback
|
|
}
|
|
|
|
/**
|
|
* This method is used by JNI as a callback from load_stock_icon.
|
|
* Image data is passed back to us via this method and loaded into the
|
|
* local BufferedImage and then returned via getStockIcon.
|
|
*
|
|
* Do NOT call this method directly.
|
|
*/
|
|
public void loadIconCallback(byte[] data, int width, int height,
|
|
int rowStride, int bps, int channels, boolean alpha) {
|
|
// Reset the stock image to null.
|
|
tmpImage = null;
|
|
|
|
// Create a new BufferedImage based on the data returned from the
|
|
// JNI call.
|
|
DataBuffer dataBuf = new DataBufferByte(data, (rowStride * height));
|
|
// Maybe test # channels to determine band offsets?
|
|
WritableRaster raster = Raster.createInterleavedRaster(dataBuf,
|
|
width, height, rowStride, channels,
|
|
(alpha ? BAND_OFFSETS_ALPHA : BAND_OFFSETS), null);
|
|
ColorModel colorModel = new ComponentColorModel(
|
|
ColorSpace.getInstance(ColorSpace.CS_sRGB), alpha, false,
|
|
ColorModel.TRANSLUCENT, DataBuffer.TYPE_BYTE);
|
|
|
|
// Set the local image so we can return it later from
|
|
// getStockIcon().
|
|
tmpImage = new BufferedImage(colorModel, raster, false, null);
|
|
}
|
|
|
|
private static native boolean check_gtk(int version);
|
|
private static native boolean load_gtk(int version, boolean verbose);
|
|
private static native boolean unload_gtk();
|
|
private native boolean load_gtk_icon(String filename);
|
|
private native boolean load_stock_icon(int widget_type, String stock_id,
|
|
int iconSize, int textDirection, String detail);
|
|
|
|
private native void nativeSync();
|
|
private static native int get_gtk_version();
|
|
|
|
@Override
|
|
public void sync() {
|
|
// flush the X11 buffer
|
|
nativeSync();
|
|
// now flush the OGL pipeline (this is a no-op if OGL is not enabled)
|
|
OGLRenderQueue.sync();
|
|
}
|
|
|
|
/*
|
|
* This returns the value for the desktop property "awt.font.desktophints"
|
|
* It builds this by querying the Gnome desktop properties to return
|
|
* them as platform independent hints.
|
|
* This requires that the Gnome properties have already been gathered.
|
|
*/
|
|
public static final String FONTCONFIGAAHINT = "fontconfig/Antialias";
|
|
|
|
@Override
|
|
protected RenderingHints getDesktopAAHints() {
|
|
|
|
Object aaValue = getDesktopProperty("gnome.Xft/Antialias");
|
|
|
|
if (aaValue == null) {
|
|
/* On a KDE desktop running KWin the rendering hint will
|
|
* have been set as property "fontconfig/Antialias".
|
|
* No need to parse further in this case.
|
|
*/
|
|
aaValue = getDesktopProperty(FONTCONFIGAAHINT);
|
|
if (aaValue != null) {
|
|
return new RenderingHints(KEY_TEXT_ANTIALIASING, aaValue);
|
|
} else {
|
|
return null; // no Gnome or KDE Desktop properties available.
|
|
}
|
|
}
|
|
|
|
/* 0 means off, 1 means some ON. What would any other value mean?
|
|
* If we require "1" to enable AA then some new value would cause
|
|
* us to default to "OFF". I don't think that's the best guess.
|
|
* So if its !=0 then lets assume AA.
|
|
*/
|
|
boolean aa = ((aaValue instanceof Number)
|
|
&& ((Number) aaValue).intValue() != 0);
|
|
Object aaHint;
|
|
if (aa) {
|
|
String subpixOrder =
|
|
(String)getDesktopProperty("gnome.Xft/RGBA");
|
|
|
|
if (subpixOrder == null || subpixOrder.equals("none")) {
|
|
aaHint = VALUE_TEXT_ANTIALIAS_ON;
|
|
} else if (subpixOrder.equals("rgb")) {
|
|
aaHint = VALUE_TEXT_ANTIALIAS_LCD_HRGB;
|
|
} else if (subpixOrder.equals("bgr")) {
|
|
aaHint = VALUE_TEXT_ANTIALIAS_LCD_HBGR;
|
|
} else if (subpixOrder.equals("vrgb")) {
|
|
aaHint = VALUE_TEXT_ANTIALIAS_LCD_VRGB;
|
|
} else if (subpixOrder.equals("vbgr")) {
|
|
aaHint = VALUE_TEXT_ANTIALIAS_LCD_VBGR;
|
|
} else {
|
|
/* didn't recognise the string, but AA is requested */
|
|
aaHint = VALUE_TEXT_ANTIALIAS_ON;
|
|
}
|
|
} else {
|
|
aaHint = VALUE_TEXT_ANTIALIAS_DEFAULT;
|
|
}
|
|
return new RenderingHints(KEY_TEXT_ANTIALIASING, aaHint);
|
|
}
|
|
|
|
private native boolean gtkCheckVersionImpl(int major, int minor,
|
|
int micro);
|
|
|
|
/**
|
|
* Returns {@code true} if the GTK+ library is compatible with the given
|
|
* version.
|
|
*
|
|
* @param major
|
|
* The required major version.
|
|
* @param minor
|
|
* The required minor version.
|
|
* @param micro
|
|
* The required micro version.
|
|
* @return {@code true} if the GTK+ library is compatible with the given
|
|
* version.
|
|
*/
|
|
public boolean checkGtkVersion(int major, int minor, int micro) {
|
|
if (loadGTK()) {
|
|
return gtkCheckVersionImpl(major, minor, micro);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static GtkVersions getEnabledGtkVersion() {
|
|
String version = System.getProperty("jdk.gtk.version");
|
|
if ("3".equals(version)) {
|
|
return GtkVersions.GTK3;
|
|
}
|
|
return GtkVersions.ANY;
|
|
}
|
|
|
|
public static GtkVersions getGtkVersion() {
|
|
return GtkVersions.getVersion(get_gtk_version());
|
|
}
|
|
|
|
public static boolean isGtkVerbose() {
|
|
return Boolean.getBoolean("jdk.gtk.verbose");
|
|
}
|
|
|
|
private static volatile Boolean isOnWayland = null;
|
|
|
|
public static boolean isOnWayland() {
|
|
Boolean result = isOnWayland;
|
|
if (result == null) {
|
|
synchronized (GTK_LOCK) {
|
|
result = isOnWayland;
|
|
if (result == null) {
|
|
final String display = System.getenv("WAYLAND_DISPLAY");
|
|
isOnWayland
|
|
= result
|
|
= (display != null && !display.trim().isEmpty());
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean isRunningOnWayland() {
|
|
return isOnWayland();
|
|
}
|
|
|
|
// We rely on the X11 input grab mechanism, but for the Wayland session
|
|
// it only works inside the XWayland server, so mouse clicks outside of it
|
|
// will not be detected.
|
|
// (window decorations, pure Wayland applications, desktop, etc.)
|
|
//
|
|
// As a workaround, we can dismiss menus when the window loses focus.
|
|
//
|
|
// However, there are "blind spots" though, which, when clicked, don't
|
|
// transfer the focus away and don't dismiss the menu
|
|
// (e.g. the window's own title or the area in the side dock without
|
|
// application icons).
|
|
private static final WindowFocusListener waylandWindowFocusListener;
|
|
|
|
private static boolean containsWaylandWindowFocusListener(Window window) {
|
|
if (window == null) {
|
|
return false;
|
|
}
|
|
|
|
for (WindowFocusListener focusListener : window.getWindowFocusListeners()) {
|
|
if (focusListener == waylandWindowFocusListener) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static {
|
|
if (isOnWayland()) {
|
|
waylandWindowFocusListener = new WindowAdapter() {
|
|
@Override
|
|
public void windowLostFocus(WindowEvent e) {
|
|
Window window = e.getWindow();
|
|
Window oppositeWindow = e.getOppositeWindow();
|
|
|
|
// The focus can move between the window calling the popup,
|
|
// and the popup window itself or its children.
|
|
// We only dismiss the popup in other cases.
|
|
if (oppositeWindow != null) {
|
|
if (containsWaylandWindowFocusListener(oppositeWindow.getOwner())) {
|
|
addWaylandWindowFocusListenerToWindow(oppositeWindow);
|
|
return;
|
|
}
|
|
|
|
Window owner = window.getOwner();
|
|
while (owner != null) {
|
|
if (owner == oppositeWindow) {
|
|
return;
|
|
}
|
|
owner = owner.getOwner();
|
|
}
|
|
|
|
if (window.getParent() == oppositeWindow) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
window.removeWindowFocusListener(this);
|
|
|
|
// AWT
|
|
XBaseWindow.ungrabInput();
|
|
|
|
// Swing
|
|
window.dispatchEvent(new UngrabEvent(window));
|
|
}
|
|
};
|
|
} else {
|
|
waylandWindowFocusListener = null;
|
|
}
|
|
}
|
|
|
|
private static void addWaylandWindowFocusListenerToWindow(Window window) {
|
|
if (!containsWaylandWindowFocusListener(window)) {
|
|
window.addWindowFocusListener(waylandWindowFocusListener);
|
|
for (Window ownedWindow : window.getOwnedWindows()) {
|
|
addWaylandWindowFocusListenerToWindow(ownedWindow);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void dismissPopupOnFocusLostIfNeeded(Window invoker) {
|
|
if (!isOnWayland() || invoker == null) {
|
|
return;
|
|
}
|
|
|
|
addWaylandWindowFocusListenerToWindow(invoker);
|
|
}
|
|
|
|
@Override
|
|
public void dismissPopupOnFocusLostIfNeededCleanUp(Window invoker) {
|
|
if (!isOnWayland() || invoker == null) {
|
|
return;
|
|
}
|
|
|
|
invoker.removeWindowFocusListener(waylandWindowFocusListener);
|
|
for (Window ownedWindow : invoker.getOwnedWindows()) {
|
|
ownedWindow.removeWindowFocusListener(waylandWindowFocusListener);
|
|
}
|
|
}
|
|
}
|