Added transparent workaround fix for Swing system tray icons by sampling

a pixel, and setting that as the background color.
This commit is contained in:
nathan 2017-06-25 02:13:27 +02:00
parent 11103dfe46
commit e46ab7d4ad

View File

@ -50,29 +50,27 @@ import javassist.CtMethod;
* Important distinction: We are not DISTRIBUTING java, nor modifying the distribution class files. * Important distinction: We are not DISTRIBUTING java, nor modifying the distribution class files.
* *
* What we are doing is modifying what is already present, post-distribution, and it is impossible to distribute what is modified * What we are doing is modifying what is already present, post-distribution, and it is impossible to distribute what is modified
*
* To see what files we need to fix...
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/windows/native/sun/windows/awt_TrayIcon.cpp
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/windows/classes/sun/awt/windows/WTrayIconPeer.java
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/141beb4d854d/src/macosx/classes/sun/lwawt/macosx/CTrayIcon.java#l216
*/ */
/** /**
* Fixes issues with some java runtimes * Fixes issues with some java runtimes
*/ */
public public
class SystemTrayFixes { class SystemTrayFixes {
private static
boolean isSwingTrayLoaded() {
String className;
// oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that! if (OS.isWindows()) {
public static void fixWindows(int trayIconSize) { className = "sun.awt.windows.WTrayIconPeer";
String vendor = System.getProperty("java.vendor").toLowerCase(Locale.US); }
// spaces at the end to make sure we check for words else if (OS.isMacOsX()){
if (!(vendor.contains("sun ") || vendor.contains("oracle "))) { className = "sun.lwawt.macosx.CTrayIcon";
// not fixing things that are not broken. }
return; else {
className = "sun.awt.X11.XTrayIconPeer";
} }
boolean isWindowsSwingTrayLoaded = false;
try { try {
// this is important to use reflection, because if JavaFX is not being used, calling getToolkit() will initialize it... // this is important to use reflection, because if JavaFX is not being used, calling getToolkit() will initialize it...
@ -80,17 +78,42 @@ class SystemTrayFixes {
m.setAccessible(true); m.setAccessible(true);
ClassLoader cl = ClassLoader.getSystemClassLoader(); ClassLoader cl = ClassLoader.getSystemClassLoader();
// if we are using swing (in windows only) the icon size is usually incorrect. We cannot fix that if it's already loaded. // if we are using swing the classes are already created and we cannot fix that if it's already loaded.
isWindowsSwingTrayLoaded = (null != m.invoke(cl, "sun.awt.windows.WTrayIconPeer")) || return (null != m.invoke(cl, className)) || (null != m.invoke(cl, "java.awt.SystemTray"));
(null != m.invoke(cl, "java.awt.SystemTray"));
} catch (Throwable e) { } catch (Throwable e) {
if (SystemTray.DEBUG) { if (SystemTray.DEBUG) {
logger.debug("Error detecting if the Swing SystemTray is loaded", e); logger.debug("Error detecting if the Swing SystemTray is loaded, unexpected error.", e);
} }
} }
if (isWindowsSwingTrayLoaded) { return true;
throw new RuntimeException("Unable to initialize the swing tray in windows, it has already been created!"); }
private static
boolean isOracleVM() {
String vendor = System.getProperty("java.vendor")
.toLowerCase(Locale.US);
// spaces at the end to make sure we check for words
return !(vendor.contains("sun ") || vendor.contains("oracle "));
}
/**
* oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that!
*
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/windows/native/sun/windows/awt_TrayIcon.cpp
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/windows/classes/sun/awt/windows/WTrayIconPeer.java
*/
public static void fixWindows(int trayIconSize) {
if (isOracleVM()) {
// not fixing things that are not broken.
return;
}
if (isSwingTrayLoaded()) {
// we have to throw a significant error.
throw new RuntimeException("Unable to initialize the Swing System Tray, it has already been created!");
} }
try { try {
@ -198,37 +221,24 @@ class SystemTrayFixes {
} }
} }
// MacOS AWT is hardcoded to respond only to lef-click for menus, where it should be any mouse button /**
// https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788 * MacOS AWT is hardcoded to respond only to lef-click for menus, where it should be any mouse button
// https://bugs.openjdk.java.net/browse/JDK-7158615 *
* https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788
* https://bugs.openjdk.java.net/browse/JDK-7158615
*
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/macosx/classes/sun/lwawt/macosx/CTrayIcon.java
*/
public static void fixMacOS() { public static void fixMacOS() {
String vendor = System.getProperty("java.vendor").toLowerCase(Locale.US); if (isOracleVM()) {
// spaces at the end to make sure we check for words
if (!(vendor.contains("sun ") || vendor.contains("oracle "))) {
// not fixing things that are not broken. // not fixing things that are not broken.
return; return;
} }
boolean isMacTrayLoaded = false; if (isSwingTrayLoaded()) {
// we have to throw a significant error.
try { throw new RuntimeException("Unable to initialize the AWT System Tray, it has already been created!");
// this is important to use reflection, because if JavaFX is not being used, calling getToolkit() will initialize it...
java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class);
m.setAccessible(true);
ClassLoader cl = ClassLoader.getSystemClassLoader();
// if we are using AWT (in MacOS only) the menu trigger is incomplete. We cannot fix that if it's already loaded.
isMacTrayLoaded = (null != m.invoke(cl, "sun.lwawt.macosx.CTrayIcon")) ||
(null != m.invoke(cl, "java.awt.SystemTray"));
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error detecting if the MacOS SystemTray is loaded", e);
}
}
if (isMacTrayLoaded) {
throw new RuntimeException("Unable to initialize the AWT tray in MacOSx, it has already been created!");
} }
try { try {
@ -387,5 +397,149 @@ class SystemTrayFixes {
} }
} }
/**
* Linux/Unix/Solaris use X11 + AWT to add an AWT window to a spot in the notification panel. UNFORTUNATELY, AWT
* components are heavyweight, and DO NOT support transparency -- so one gets a "grey" box as the background of the icon.
*
* Spectacularly enough, because this uses X11, it works on any X backend -- regardless of GtkStatusIcon or AppIndicator support. This
* actually provides **more** support than GtkStatusIcons or AppIndicators, since this will ALWAYS work.
*
* Additionally, the size of the tray is hard-coded to be 24 -- so we want to fix that as well.
*
*
* The down side, is that there is a "grey" box -- so hack around this issue by getting the color of a pixel in the notification area 1
* off the corner, and setting that as the background.
*
* It would be better to take a screenshot of the space BEHIND the tray icon, but we can't do that because there is no way to get
* the info BEFORE the AWT is added to the notification area. See comments below for more details.
*
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936
*
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/solaris/classes/sun/awt/X11/XTrayIconPeer.java
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/solaris/classes/sun/awt/X11/XSystemTrayPeer.java
*/
public static
void fixLinux() {
// linux/mac doesn't have transparent backgrounds for "swing" system tray icons
// TODO Fix the tray icon size to something other than (hardcoded) 24. This will be a lot of work, since this value is in the
// constructor, and there is a significant amount of code and anonymous classes there - and javassist does not support non-staic
// anonymous classes.
if (isOracleVM()) {
// not fixing things that are not broken.
return;
}
if (isSwingTrayLoaded()) {
// we have to throw a significant error.
throw new RuntimeException("Unable to initialize the Swing System Tray, it has already been created!");
}
try {
ClassPool pool = ClassPool.getDefault();
byte[] iconCanvasBytes;
{
CtClass trayIconClass = pool.get("sun.awt.X11.XTrayIconPeer");
CtClass[] nestedClasses = trayIconClass.getNestedClasses();
// looking for IconCanvas
CtClass iconCanvasClass = null;
for (CtClass nestedClass : nestedClasses) {
if (nestedClass.getName()
.equals(trayIconClass.getName() + "$IconCanvas")) {
iconCanvasClass = nestedClass;
break;
}
}
if (iconCanvasClass == null) {
throw new RuntimeException("Unable to find required classes. Unable to continue initialization.");
}
CtField ctField;
ctField = new CtField(pool.get("java.awt.Color"), "color", iconCanvasClass);
iconCanvasClass.addField(ctField);
ctField = new CtField(pool.get("java.awt.Robot"), "robot", iconCanvasClass);
iconCanvasClass.addField(ctField);
CtMethod ctMethodRepaintImage = iconCanvasClass.getDeclaredMethod("repaintImage");
String body = "{" +
"boolean doClear = $1;" +
"java.lang.System.err.println(\"update image: \" + doClear);" +
"java.awt.Graphics g = getGraphics();" +
"if (g != null) {" +
" try {" +
" if (isVisible()) {" +
" if (robot == null) {" +
" robot = new java.awt.Robot();" +
" }" +
" if (doClear) {" +
// gets the pixel color just to the side of the icon. The CRITICAL thing to notice, is that this happens before the
// AWT window is positioned, so there can be a different system tray icon at this position (at this exact point in
// time. This means we cannot take a screen shot, because before the window is placed, another icon is in this spot,
// and when the window is placed, it's too late to take a screenshot. The second best option is to take a sample of
// the pixel color, so at least we can fake transparency. This only works if the notification area is a solid color,
// and not an image or gradient.
" java.awt.Point loc = getLocationOnScreen();" +
" color = robot.getPixelColor(loc.x-1, loc.y-1);" +
" update(g);" +
" } else {" +
" paint(g);" +
" }" +
" }" +
" } finally {" +
" g.dispose();" +
" }" +
"}" +
"}";
ctMethodRepaintImage.setBody(body);
CtMethod ctMethodPaint = iconCanvasClass.getDeclaredMethod("paint");
body = "{" +
"java.awt.Graphics g = $1;" +
"if (g != null && curW > 0 && curH > 0) {" +
" java.awt.image.BufferedImage bufImage = new java.awt.image.BufferedImage(curW, curH, java.awt.image.BufferedImage.TYPE_INT_ARGB);" +
" java.awt.Graphics2D gr = bufImage.createGraphics();" +
" if (gr != null) {" +
" try {" +
// this will render the image "nicely"
" gr.addRenderingHints(new java.awt.RenderingHints(java.awt.RenderingHints.KEY_RENDERING," +
" java.awt.RenderingHints.VALUE_RENDER_QUALITY));" +
// Have to replace the color with the correct pixel color to simulate transparency
" gr.setColor(color);" +
" gr.fillRect(0, 0, curW, curH);" +
" gr.drawImage(image, 0, 0, curW, curH, observer);" +
" gr.dispose();" +
" g.drawImage(bufImage, 0, 0, curW, curH, null);" +
" } finally {" +
" g.dispose();" +
" }" +
" }" +
"}" +
"}";
ctMethodPaint.setBody(body);
iconCanvasBytes = iconCanvasClass.toBytecode();
}
// whoosh, past the classloader and directly into memory.
BootStrapClassLoader.defineClass(iconCanvasBytes);
if (SystemTray.DEBUG) {
logger.debug("Successfully changed tray icon background color");
}
} catch (Exception e) {
logger.error("Error setting tray icon background color", e);
}
}
} }