blob: 4bbf132934b68f11c89c18ae3531dc097862384f [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.wm.impl;
import com.intellij.Patches;
import com.intellij.execution.util.ExecUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.WindowManager;
import org.jetbrains.annotations.Nullable;
import sun.misc.Unsafe;
import javax.swing.*;
import java.awt.*;
import java.awt.peer.ComponentPeer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.intellij.util.ArrayUtil.newLongArray;
import static com.intellij.util.containers.ContainerUtil.newHashSet;
public class X11UiUtil {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.wm.impl.X11UiUtil");
private static final int True = 1;
private static final int False = 0;
private static final long None = 0;
private static final long XA_ATOM = 4;
private static final long XA_WINDOW = 33;
private static final int CLIENT_MESSAGE = 33;
private static final int FORMAT_BYTE = 8;
private static final int FORMAT_LONG = 32;
private static final long EVENT_MASK = (3l << 19);
private static final long NET_WM_STATE_TOGGLE = 2;
@SuppressWarnings("SpellCheckingInspection")
private static class Xlib {
private Unsafe unsafe;
private Method XGetWindowProperty;
private Method XFree;
private Method RootWindow;
private Method XSendEvent;
private Method getWindow;
private Method getScreenNumber;
private Method awtLock;
private Method awtUnlock;
private long display;
private long UTF8_STRING;
private long NET_SUPPORTING_WM_CHECK;
private long NET_WM_NAME;
private long NET_WM_ALLOWED_ACTIONS;
private long NET_WM_STATE;
private long NET_WM_ACTION_FULLSCREEN;
private long NET_WM_STATE_FULLSCREEN;
@Nullable
private static Xlib getInstance() {
Class<? extends Toolkit> toolkitClass = Toolkit.getDefaultToolkit().getClass();
if (!SystemInfo.isXWindow || !"sun.awt.X11.XToolkit".equals(toolkitClass.getName())) {
return null;
}
try {
Xlib x11 = new Xlib();
// reflect on Xlib method wrappers and important structures
Class<?> XlibWrapper = Class.forName("sun.awt.X11.XlibWrapper");
x11.unsafe = (Unsafe)field(XlibWrapper, "unsafe").get(null);
x11.XGetWindowProperty = method(XlibWrapper, "XGetWindowProperty", 12);
x11.XFree = method(XlibWrapper, "XFree", 1);
x11.RootWindow = method(XlibWrapper, "RootWindow", 2);
x11.XSendEvent = method(XlibWrapper, "XSendEvent", 5);
Class<?> XBaseWindow = Class.forName("sun.awt.X11.XBaseWindow");
x11.getWindow = method(XBaseWindow, "getWindow");
x11.getScreenNumber = method(XBaseWindow, "getScreenNumber");
x11.display = (Long)method(toolkitClass, "getDisplay").invoke(null);
x11.awtLock = method(toolkitClass, "awtLock");
x11.awtUnlock = method(toolkitClass, "awtUnlock");
// intern atoms
Class<?> XAtom = Class.forName("sun.awt.X11.XAtom");
Method get = method(XAtom, "get", String.class);
Field atom = field(XAtom, "atom");
x11.UTF8_STRING = (Long)atom.get(get.invoke(null, "UTF8_STRING"));
x11.NET_SUPPORTING_WM_CHECK = (Long)atom.get(get.invoke(null, "_NET_SUPPORTING_WM_CHECK"));
x11.NET_WM_NAME = (Long)atom.get(get.invoke(null, "_NET_WM_NAME"));
x11.NET_WM_ALLOWED_ACTIONS = (Long)atom.get(get.invoke(null, "_NET_WM_ALLOWED_ACTIONS"));
x11.NET_WM_STATE = (Long)atom.get(get.invoke(null, "_NET_WM_STATE"));
x11.NET_WM_ACTION_FULLSCREEN = (Long)atom.get(get.invoke(null, "_NET_WM_ACTION_FULLSCREEN"));
x11.NET_WM_STATE_FULLSCREEN = (Long)atom.get(get.invoke(null, "_NET_WM_STATE_FULLSCREEN"));
// check for _NET protocol support
Long netWmWindow = x11.getNetWmWindow();
if (netWmWindow == null) {
LOG.info("_NET protocol is not supported");
return null;
}
return x11;
}
catch (Throwable t) {
LOG.info("cannot initialize", t);
}
return null;
}
@Nullable
private Long getNetWmWindow() throws Exception {
long rootWindow = (Long)RootWindow.invoke(null, display, 0);
long[] values = getLongArrayProperty(rootWindow, NET_SUPPORTING_WM_CHECK, XA_WINDOW);
return values != null && values.length > 0 ? values[0] : null;
}
@Nullable
private long[] getLongArrayProperty(long window, long name, long type) throws Exception {
return getWindowProperty(window, name, type, FORMAT_LONG);
}
@Nullable
private String getUtfStringProperty(long window, long name) throws Exception {
byte[] bytes = getWindowProperty(window, name, UTF8_STRING, FORMAT_BYTE);
return bytes != null ? new String(bytes, CharsetToolkit.UTF8_CHARSET) : null;
}
@Nullable
private <T> T getWindowProperty(long window, long name, long type, long expectedFormat) throws Exception {
T property = null;
long data = unsafe.allocateMemory(64);
awtLock.invoke(null);
try {
unsafe.setMemory(data, 64, (byte)0);
int result = (Integer)XGetWindowProperty.invoke(
null, display, window, name, 0l, 65535l, (long)False, type, data, data + 8, data + 16, data + 24, data + 32);
if (result == 0) {
int format = unsafe.getInt(data + 8);
long pointer = SystemInfo.is64Bit ? unsafe.getLong(data + 32) : unsafe.getInt(data + 32);
if (pointer != None && format == expectedFormat) {
int length = SystemInfo.is64Bit ? (int)unsafe.getLong(data + 16) : unsafe.getInt(data + 16);
if (format == FORMAT_BYTE) {
byte[] bytes = new byte[length];
for (int i = 0; i < length; i++) bytes[i] = unsafe.getByte(pointer + i);
property = (T)bytes;
}
else if (format == FORMAT_LONG) {
long[] values = newLongArray(length);
for (int i = 0; i < length; i++) {
values[i] = SystemInfo.is64Bit ? unsafe.getLong(pointer + 8 * i) : unsafe.getInt(pointer + 4 * i);
}
property = (T)values;
}
else if (format != None) {
LOG.info("unexpected format: " + format);
}
}
if (pointer != None) XFree.invoke(null, pointer);
}
}
finally {
awtUnlock.invoke(null);
unsafe.freeMemory(data);
}
return property;
}
private void sendClientMessage(long target, long window, long type, long... data) throws Exception {
assert data.length <= 5;
long event = unsafe.allocateMemory(128);
awtLock.invoke(null);
try {
unsafe.setMemory(event, 128, (byte)0);
unsafe.putInt(event, CLIENT_MESSAGE);
if (!SystemInfo.is64Bit) {
unsafe.putInt(event + 8, True);
unsafe.putInt(event + 16, (int)window);
unsafe.putInt(event + 20, (int)type);
unsafe.putInt(event + 24, FORMAT_LONG);
for (int i = 0; i < data.length; i++) {
unsafe.putInt(event + 28 + 4 * i, (int)data[i]);
}
}
else {
unsafe.putInt(event + 16, True);
unsafe.putLong(event + 32, window);
unsafe.putLong(event + 40, NET_WM_STATE);
unsafe.putInt(event + 48, FORMAT_LONG);
for (int i = 0; i < data.length; i++) {
unsafe.putLong(event + 56 + 8 * i, data[i]);
}
}
XSendEvent.invoke(null, display, target, false, EVENT_MASK, event);
}
finally {
awtUnlock.invoke(null);
unsafe.freeMemory(event);
}
}
}
@Nullable private static final Xlib X11 = Xlib.getInstance();
// WM detection and patching
@Nullable
public static String getWmName() {
if (X11 == null) return null;
try {
Long netWmWindow = X11.getNetWmWindow();
if (netWmWindow != null) {
return X11.getUtfStringProperty(netWmWindow, X11.NET_WM_NAME);
}
}
catch (Throwable t) {
LOG.info("cannot get WM name", t);
}
return null;
}
@SuppressWarnings("SpellCheckingInspection")
public static void patchDetectedWm(String wmName) {
if (X11 == null || !Registry.is("ide.x11.override.wm")) return;
try {
if (wmName.startsWith("Mutter") || "Muffin".equals(wmName) || "GNOME Shell".equals(wmName)) {
setWM("MUTTER_WM", "METACITY_WM");
}
else if ("Marco".equals(wmName)) {
setWM("MARCO_WM", "METACITY_WM");
}
else if ("awesome".equals(wmName)) {
String version = getAwesomeWMVersion();
if (StringUtil.compareVersionNumbers(version, "3.5") >= 0) {
setWM("SAWFISH_WM");
}
else if (version != null) {
setWM("OTHER_NONREPARENTING_WM", "LG3D_WM");
}
}
}
catch (Throwable t) {
LOG.warn(t);
}
}
private static void setWM(String... wmConstants) throws Exception {
Class<?> xwmClass = Class.forName("sun.awt.X11.XWM");
Object xwm = method(xwmClass, "getWM").invoke(null);
if (xwm != null) {
for (String wmConstant : wmConstants) {
try {
Field wm = field(xwmClass, wmConstant);
Object id = wm.get(null);
if (id != null) {
field(xwmClass, "awt_wmgr").set(null, id);
field(xwmClass, "WMID").set(xwm, id);
LOG.info("impersonated WM: " + wmConstant);
break;
}
}
catch (NoSuchFieldException ignore) { }
}
}
}
@Nullable
private static String getAwesomeWMVersion() {
try {
String version = ExecUtil.execAndReadLine("awesome", "--version");
if (version != null) {
Matcher m = Pattern.compile("awesome v([0-9.]+)").matcher(version);
if (m.find()) {
return m.group(1);
}
}
}
catch (Throwable t) {
LOG.warn(t);
}
return null;
}
// full-screen support
public static boolean isFullScreenSupported() {
if (X11 == null) return false;
if (Patches.SUN_BUG_ID_8013359) {
String wmName = getWmName();
if (wmName != null &&
(wmName.startsWith("Mutter") || newHashSet("Metacity", "GNOME Shell", "Muffin", "Marco").contains(wmName))) {
return false; // Metacity clones suffer from child window placement bug
}
}
IdeFrame[] frames = WindowManager.getInstance().getAllProjectFrames();
if (frames.length == 0) return true; // no frame to check the property so be optimistic here
return frames[0] instanceof JFrame && hasWindowProperty((JFrame)frames[0], X11.NET_WM_ALLOWED_ACTIONS, X11.NET_WM_ACTION_FULLSCREEN);
}
public static boolean isInFullScreenMode(JFrame frame) {
return X11 != null && hasWindowProperty(frame, X11.NET_WM_STATE, X11.NET_WM_STATE_FULLSCREEN);
}
private static boolean hasWindowProperty(JFrame frame, long name, long expected) {
if (X11 == null) return false;
try {
@SuppressWarnings("deprecation") ComponentPeer peer = frame.getPeer();
long window = (Long)X11.getWindow.invoke(peer);
long[] values = X11.getLongArrayProperty(window, name, XA_ATOM);
if (values != null) {
for (long value : values) {
if (value == expected) return true;
}
}
return false;
}
catch (Throwable t) {
LOG.info("cannot check window property", t);
return false;
}
}
public static void toggleFullScreenMode(JFrame frame) {
if (X11 == null) return;
try {
@SuppressWarnings("deprecation") ComponentPeer peer = frame.getPeer();
long window = (Long)X11.getWindow.invoke(peer);
long screen = (Long)X11.getScreenNumber.invoke(peer);
long rootWindow = (Long)X11.RootWindow.invoke(null, X11.display, screen);
X11.sendClientMessage(rootWindow, window, X11.NET_WM_STATE, NET_WM_STATE_TOGGLE, X11.NET_WM_STATE_FULLSCREEN);
}
catch (Throwable t) {
LOG.info("cannot toggle mode", t);
}
}
// reflection utilities
private static Method method(Class<?> aClass, String name, Class<?>... parameterTypes) throws Exception {
while (aClass != null) {
try {
Method method = aClass.getDeclaredMethod(name, parameterTypes);
method.setAccessible(true);
return method;
}
catch (NoSuchMethodException e) {
aClass = aClass.getSuperclass();
}
}
throw new NoSuchMethodException(name);
}
private static Method method(Class<?> aClass, String name, int parameters) throws Exception {
for (Method method : aClass.getDeclaredMethods()) {
if (name.equals(method.getName()) && method.getParameterTypes().length == parameters) {
method.setAccessible(true);
return method;
}
}
throw new NoSuchMethodException(name);
}
private static Field field(Class<?> aClass, String name) throws Exception {
Field field = aClass.getDeclaredField(name);
field.setAccessible(true);
return field;
}
}