/* * 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... 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); } } }