Image scaling + caching + error-icon if problems, fix for windows

'auto'
scaling
This commit is contained in:
nathan 2016-09-26 02:06:27 +02:00
parent a985827f5b
commit ad066c6e42
13 changed files with 1072 additions and 517 deletions

View File

@ -1,241 +0,0 @@
/*
* Copyright 2016 dorkbox, llc
*
* 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 dorkbox.systemTray;
import dorkbox.util.LocationResolver;
import dorkbox.util.OS;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public
class ImageUtil {
public static final File TEMP_DIR = new File(System.getProperty("java.io.tmpdir"));
private static MessageDigest digest;
private static final Map<String, String> resourceToFilePath = new HashMap<String, String>();
private static final long runtimeRandom = new SecureRandom().nextLong();
public static synchronized
void init() throws NoSuchAlgorithmException {
ImageUtil.digest = MessageDigest.getInstance("MD5");
}
/**
* appIndicator/gtk require strings (which is the path)
* swing version loads as an image (which can be stream or path, we use path)
*/
public static synchronized
String iconPath(String fileName) {
// if we already have this fileName, reuse it
final String cachedFile = resourceToFilePath.get(fileName);
if (cachedFile != null) {
return cachedFile;
}
// is file sitting on drive
File iconTest = new File(fileName);
if (iconTest.isFile() && iconTest.canRead()) {
final String absolutePath = iconTest.getAbsolutePath();
resourceToFilePath.put(fileName, absolutePath);
return absolutePath;
}
else {
// suck it out of a URL/Resource (with debugging if necessary)
final URL systemResource = LocationResolver.getResource(fileName);
final String filePath = makeFileViaUrl(systemResource);
resourceToFilePath.put(fileName, filePath);
return filePath;
}
}
/**
* appIndicator/gtk require strings (which is the path)
* swing version loads as an image (which can be stream or path, we use path)
*/
public static synchronized
String iconPath(final URL fileResource) {
// if we already have this fileName, reuse it
final String cachedFile = resourceToFilePath.get(fileResource.getPath());
if (cachedFile != null) {
return cachedFile;
}
final String filePath = makeFileViaUrl(fileResource);
resourceToFilePath.put(fileResource.getPath(), filePath);
return filePath;
}
/**
* appIndicator/gtk require strings (which is the path)
* swing version loads as an image (which can be stream or path, we use path)
*/
public static synchronized
String iconPath(final String cacheName, final InputStream fileStream) {
// if we already have this fileName, reuse it
final String cachedFile = resourceToFilePath.get(cacheName);
if (cachedFile != null) {
return cachedFile;
}
final String filePath = makeFileViaStream(cacheName, fileStream);
resourceToFilePath.put(cacheName, filePath);
return filePath;
}
/**
* NO CACHING OF INPUTSTREAM!
*
* appIndicator/gtk require strings (which is the path)
* swing version loads as an image (which can be stream or path, we use path)
*/
@Deprecated
public static synchronized
String iconPathNoCache(final InputStream fileStream) {
return makeFileViaStream(Long.toString(System.currentTimeMillis()), fileStream);
}
/**
* @param resourceUrl the url to copy to a file on disk
* @return the full path of the resource copied to disk, or null if invalid
*/
private static
String makeFileViaUrl(final URL resourceUrl) {
if (resourceUrl == null) {
throw new RuntimeException("resourceUrl is null");
}
InputStream inStream;
try {
inStream = resourceUrl.openStream();
} catch (IOException e) {
String message = "Unable to open icon at '" + resourceUrl + "'";
SystemTray.logger.error(message, e);
throw new RuntimeException(message, e);
}
// suck it out of a URL/Resource (with debugging if necessary)
String cacheName = resourceUrl.getPath();
return makeFileViaStream(cacheName, inStream);
}
/**
* @param cacheName needs name+extension for the resource
* @param resourceStream the resource to copy to a file on disk
*
* @return the full path of the resource copied to disk, or null if invalid
*/
private static
String makeFileViaStream(final String cacheName, final InputStream resourceStream) {
if (cacheName == null) {
throw new RuntimeException("cacheName is null");
}
if (resourceStream == null) {
throw new RuntimeException("resourceStream is null");
}
// figure out the fileName
byte[] bytes = cacheName.getBytes(OS.UTF_8);
File newFile;
// can be wimpy, only one at a time
String hash = hashName(bytes);
String extension = getExtension(cacheName);
newFile = new File(TEMP_DIR, "SYSTRAY_" + hash + '.' + extension).getAbsoluteFile();
if (SystemTray.isKDE) {
// KDE is unique per run, so this prevents buildup
newFile.deleteOnExit();
}
// copy out to a temp file, as a hash of the file name
OutputStream outStream = null;
try {
outStream = new FileOutputStream(newFile);
byte[] buffer = new byte[2048];
int read;
while ((read = resourceStream.read(buffer)) > 0) {
outStream.write(buffer, 0, read);
}
} catch (IOException e) {
// Send up exception
String message = "Unable to copy icon '" + cacheName + "' to temporary location: '" + newFile.getAbsolutePath() + "'";
SystemTray.logger.error(message, e);
throw new RuntimeException(message, e);
} finally {
try {
resourceStream.close();
} catch (Exception ignored) {
}
try {
if (outStream != null) {
outStream.close();
}
} catch (Exception ignored) {
}
}
return newFile.getAbsolutePath();
}
public static
String getExtension(final String fileName) {
String extension = "";
int dot = fileName.lastIndexOf('.');
if (dot > -1) {
extension = fileName.substring(dot + 1);
}
return extension;
}
// must be called from synchronized block
private static
String hashName(byte[] nameChars) {
digest.reset();
digest.update(nameChars);
// For KDE4, it must also be unique across runs
if (SystemTray.isKDE) {
byte[] longBytes = new byte[8];
ByteBuffer wrap = ByteBuffer.wrap(longBytes);
wrap.putLong(runtimeRandom);
digest.update(longBytes);
}
// convert to alpha-numeric. see https://stackoverflow.com/questions/29183818/why-use-tostring32-and-not-tostring36
return new BigInteger(1, digest.digest()).toString(32).toUpperCase(Locale.US);
}
}

View File

@ -24,7 +24,6 @@ import java.io.FileReader;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;
@ -40,8 +39,11 @@ import dorkbox.systemTray.linux.GtkSystemTray;
import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.swing.SwingSystemTray;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.Swt;
import dorkbox.systemTray.util.WindowsSystemTraySwing;
import dorkbox.util.CacheUtil;
import dorkbox.util.OS;
import dorkbox.util.Property;
import dorkbox.util.process.ShellProcessBuilder;
@ -50,22 +52,49 @@ import dorkbox.util.process.ShellProcessBuilder;
/**
* Factory and base-class for system tray implementations.
*/
@SuppressWarnings({"unused", "Duplicates"})
@SuppressWarnings({"unused", "Duplicates", "DanglingJavadoc", "WeakerAccess"})
public abstract
class SystemTray {
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
public static final int LINUX_GTK = 1;
public static final int LINUX_APP_INDICATOR = 2;
public static final int SWING_INDICATOR = 3;
public static final int TYPE_AUTO_DETECT = 0;
public static final int TYPE_GTKSTATUSICON = 1;
public static final int TYPE_APP_INDICATOR = 2;
public static final int TYPE_SWING = 3;
@Property
/** How long to wait when updating menu entries before the request times-out */
public static final int TIMEOUT = 2;
@Property
/** Size of the tray, so that the icon can properly scale based on OS. (if it's not exact) */
public static int TRAY_SIZE = 22;
/** Enables auto-detection for the system tray. This should be mostly successful.
* <p>
* Auto-detection will use DEFAULT_WINDOWS_SIZE or DEFAULT_LINUX_SIZE as a 'base-line' for determining what size to use. On Linux,
* `gsettings get org.gnome.desktop.interface scaling-factor` is used to determine the scale factor (for HiDPI configurations).
* <p>
* If auto-detection fails and the incorrect size is detected or used, disable this and specify the correct DEFAULT_WINDOWS_SIZE or
* DEFAULT_LINUX_SIZE to use them instead
*/
public static boolean AUTO_TRAY_SIZE = true;
@Property
/**
* Size of the tray, so that the icon can be properly scaled based on OS.
* - Windows will automatically scale up/down.
* <p>
* You will experience WEIRD graphical glitches if this is NOT a power of 2.
*/
public static int DEFAULT_WINDOWS_SIZE = 32;
@Property
/**
* Size of the tray, so that the icon can be properly scaled based on OS.
* - GtkStatusIcon will usually automatically scale up/down
* - AppIndicators will not always automatically scale (it will sometimes display whatever is specified here)
* <p>
* You will experience WEIRD graphical glitches if this is NOT a power of 2.
*/
public static int DEFAULT_LINUX_SIZE = 16;
@Property
/** Forces the system tray to always choose GTK2 (even when GTK3 might be available). */
@ -73,7 +102,7 @@ class SystemTray {
@Property
/** Forces the system tray detection to be Automatic (0), GTK (1), AppIndicator (2), or Swing (3). This is an advanced feature. */
public static int FORCE_LINUX_TYPE = 0;
public static int FORCE_TRAY_TYPE = 1;
@Property
/**
@ -90,7 +119,6 @@ class SystemTray {
private static volatile SystemTray systemTray = null;
static boolean isKDE = false;
public final static boolean isJavaFxLoaded;
public final static boolean isSwtLoaded;
@ -135,6 +163,7 @@ class SystemTray {
Class<? extends SystemTray> trayType = null;
boolean isKDE = false;
if (DEBUG) {
logger.debug("is JavaFX detected? {}", isJavaFxLoaded);
@ -142,16 +171,18 @@ class SystemTray {
}
// kablooie if SWT is not configured in a way that works with us.
if (FORCE_LINUX_TYPE != SWING_INDICATOR && OS.isLinux()) {
if (FORCE_TRAY_TYPE != TYPE_SWING && OS.isLinux()) {
if (isSwtLoaded) {
// Necessary for us to work with SWT based on version info. We can try to set us to be compatible with whatever it is set to
// System.setProperty("SWT_GTK3", "0");
// was SWT forced?
boolean isSwt_GTK3 = !System.getProperty("SWT_GTK3").equals("0");
String swt_gtk3 = System.getProperty("SWT_GTK3");
boolean isSwt_GTK3 = swt_gtk3 != null && !swt_gtk3.equals("0");
if (!isSwt_GTK3) {
// check a different property
isSwt_GTK3 = !System.getProperty("org.eclipse.swt.internal.gtk.version").startsWith("2.");
String property = System.getProperty("org.eclipse.swt.internal.gtk.version");
isSwt_GTK3 = property != null && !property.startsWith("2.");
}
if (isSwt_GTK3 && FORCE_GTK2) {
@ -200,7 +231,7 @@ class SystemTray {
}
if (DEBUG) {
switch (FORCE_LINUX_TYPE) {
switch (FORCE_TRAY_TYPE) {
case 1: logger.debug("Forced tray type: GtkStatusIcon"); break;
case 2: logger.debug("Forced tray type: AppIndicator"); break;
case 3: logger.debug("Forced tray type: Swing"); break;
@ -210,17 +241,11 @@ class SystemTray {
logger.debug("FORCE_GTK2: {}", FORCE_GTK2);
}
// Note: AppIndicators DO NOT support tooltips. We could try to create one, by creating a GTK widget and attaching it on
// mouseover or something, but I don't know how to do that. It seems that tooltips for app-indicators are a custom job, as
// all examined ones sometimes have it (and it's more than just text), or they don't have it at all.
if (OS.isWindows()) {
// the tray icon size in windows is DIFFERENT than on Mac (TODO: test on mac with retina stuff. Also check HiDpi setups).
TRAY_SIZE -= 4;
}
if (FORCE_LINUX_TYPE != SWING_INDICATOR && OS.isLinux()) {
if (FORCE_TRAY_TYPE != TYPE_SWING && OS.isLinux()) {
// see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running
// For funsies, SyncThing did a LOT of work on compatibility (unfortunate for us) in python.
@ -236,7 +261,7 @@ class SystemTray {
}
}
if (SystemTray.FORCE_LINUX_TYPE == SystemTray.LINUX_GTK) {
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTKSTATUSICON) {
try {
trayType = GtkSystemTray.class;
} catch (Throwable e1) {
@ -245,7 +270,7 @@ class SystemTray {
}
}
}
else if (SystemTray.FORCE_LINUX_TYPE == SystemTray.LINUX_APP_INDICATOR) {
else if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_APP_INDICATOR) {
try {
trayType = AppIndicatorTray.class;
} catch (Throwable e1) {
@ -261,6 +286,7 @@ class SystemTray {
// quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least
String XDG = System.getenv("XDG_CURRENT_DESKTOP");
// BLEH. if gnome-shell is running, IT'S REALLY GNOME!
// we must ALWAYS do this check!!
boolean isReallyGnome = false;
@ -306,14 +332,54 @@ class SystemTray {
}
}
else if ("xfce".equalsIgnoreCase(XDG)) {
// NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted.
// see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/
// see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25
// XFCE4 is OK to use appindicator, <XFCE4 we use GTKStatusIcon. God i wish there was an easy way to do this.
boolean isNewXFCE = false;
try {
trayType = AppIndicatorTray.class;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
PrintStream outputStream = new PrintStream(byteArrayOutputStream);
// ps aux | grep [x]fce
final ShellProcessBuilder shell = new ShellProcessBuilder(outputStream);
shell.setExecutable("ps");
shell.addArgument("aux");
shell.start();
String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
// should last us the next 20 years or so. XFCE development is glacially slow.
isNewXFCE = output.contains("/xfce4/") || output.contains("/xfce5/") ||
output.contains("/xfce6/") || output.contains("/xfce7/");
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize AppIndicatorTray", e);
logger.error("Cannot detect what version of XFCE is running", e);
}
}
// we can fail on AppIndicator, so this is the fallback
if (DEBUG) {
logger.error("Is 'new' version of XFCE? {}", isNewXFCE);
}
if (isNewXFCE) {
try {
trayType = AppIndicatorTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize AppIndicatorTray", e);
}
// we can fail on AppIndicator, so this is the fallback
try {
trayType = GtkSystemTray.class;
} catch (Throwable e1) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e1);
}
}
}
} else {
try {
trayType = GtkSystemTray.class;
} catch (Throwable e1) {
@ -497,6 +563,13 @@ class SystemTray {
}
}
// this has to happen BEFORE any sort of swing system tray stuff is accessed
if (OS.isWindows()) {
// windows is funky, and is hardcoded to 16x16. We fix that.
WindowsSystemTraySwing.fix();
}
// this is windows OR mac
if (trayType == null && java.awt.SystemTray.isSupported()) {
try {
@ -519,9 +592,16 @@ class SystemTray {
else {
SystemTray systemTray_ = null;
try {
ImageUtil.init();
/*
* appIndicator/gtk require strings (which is the path)
* swing version loads as an image (which can be stream or path, we use path)
*
* For KDE4, it must also be unique across runs
*/
CacheUtil.setUniqueCachePerRun = isKDE;
CacheUtil.tempDir = "SysTray";
try {
if (OS.isLinux() &&
trayType == AppIndicatorTray.class &&
Gtk.isGtk2 &&
@ -545,8 +625,6 @@ class SystemTray {
systemTray_ = (SystemTray) trayType.getConstructors()[0].newInstance();
logger.info("Successfully Loaded: {}", trayType.getSimpleName());
} catch (NoSuchAlgorithmException e) {
logger.error("Unsupported hashing algorithm!");
} catch (Exception e) {
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
}
@ -653,7 +731,7 @@ class SystemTray {
void setStatus(String statusText);
protected abstract
void setIcon_(String iconPath);
void setIcon_(File iconPath);
/**
* Changes the tray icon used.
@ -665,8 +743,7 @@ class SystemTray {
*/
public
void setIcon(String imagePath) {
final String fullPath = ImageUtil.iconPath(imagePath);
setIcon_(fullPath);
setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath));
}
/**
@ -679,8 +756,7 @@ class SystemTray {
*/
public
void setIcon(URL imageUrl) {
final String fullPath = ImageUtil.iconPath(imageUrl);
setIcon_(fullPath);
setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl));
}
/**
@ -694,8 +770,7 @@ class SystemTray {
*/
public
void setIcon(String cacheName, InputStream imageStream) {
final String fullPath = ImageUtil.iconPath(cacheName, imageStream);
setIcon_(fullPath);
setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream));
}
/**
@ -704,17 +779,11 @@ class SystemTray {
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will copy the contents of
* the imageStream to a temporary location on disk.
*
* This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is
* also NOT RECOMMENDED, but is provided for simplicity.
*
* @param imageStream the InputStream of the icon to use
*/
@Deprecated
public
void setIcon(InputStream imageStream) {
@SuppressWarnings("deprecation")
final String fullPath = ImageUtil.iconPathNoCache(imageStream);
setIcon_(fullPath);
setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageStream));
}
@ -764,9 +833,6 @@ class SystemTray {
/**
* Adds a menu entry to the tray icon with text + image
*
* This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is
* also NOT RECOMMENDED, but is provided for simplicity.
*
* @param menuText string of the text you want to appear
* @param imageStream the InputStream of the image to use. If null, no image will be used
* @param callback callback that will be executed when this menu entry is clicked
@ -783,7 +849,7 @@ class SystemTray {
* @param newMenuText the new menu text (this will replace the original menu text)
*/
public final
void updateMenuEntry_Text(final String origMenuText, final String newMenuText) {
void updateMenuEntry(final String origMenuText, final String newMenuText) {
// have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
@ -822,13 +888,13 @@ class SystemTray {
}
/**
* Updates (or changes) the menu entry's text.
* Updates (or changes) the menu entry's image (as a String).
*
* @param origMenuText the original menu text
* @param imagePath the new path for the image to use or null to delete the image
*/
public final
void updateMenuEntry_Image(final String origMenuText, final String imagePath) {
void updateMenuEntry_AsImage(final String origMenuText, final String imagePath) {
// have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
@ -873,7 +939,7 @@ class SystemTray {
* @param imageUrl the new URL for the image to use or null to delete the image
*/
public final
void updateMenuEntry_Image(final String origMenuText, final URL imageUrl) {
void updateMenuEntry(final String origMenuText, final URL imageUrl) {
// have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
@ -919,7 +985,7 @@ class SystemTray {
* @param imageStream the InputStream of the image to use or null to delete the image
*/
public final
void updateMenuEntry_Image(final String origMenuText, final String cacheName, final InputStream imageStream) {
void updateMenuEntry(final String origMenuText, final String cacheName, final InputStream imageStream) {
// have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
@ -960,14 +1026,11 @@ class SystemTray {
/**
* Updates (or changes) the menu entry's text.
*
* This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is
* also NOT RECOMMENDED, but is provided for simplicity.
*
* @param origMenuText the original menu text
* @param imageStream the new path for the image to use or null to delete the image
*/
public final
void updateMenuEntry_Image(final String origMenuText, final InputStream imageStream) {
void updateMenuEntry(final String origMenuText, final InputStream imageStream) {
// have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
@ -1013,7 +1076,7 @@ class SystemTray {
* @param newCallback the new callback (this will replace the original callback)
*/
public final
void updateMenuEntry_Callback(final String origMenuText, final SystemTrayMenuAction newCallback) {
void updateMenuEntry(final String origMenuText, final SystemTrayMenuAction newCallback) {
// have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);

View File

@ -15,14 +15,17 @@
*/
package dorkbox.systemTray.linux;
import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
import com.sun.jna.Pointer;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.AppIndicatorInstanceStruct;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import java.util.concurrent.atomic.AtomicBoolean;
import dorkbox.systemTray.util.ImageUtils;
/**
* Class for handling all system tray interactions.
@ -42,9 +45,9 @@ class AppIndicatorTray extends GtkTypeSystemTray {
public
AppIndicatorTray() {
if (SystemTray.FORCE_LINUX_TYPE == SystemTray.LINUX_GTK) {
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTKSTATUSICON) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
throw new IllegalArgumentException("Unable to start AppIndicator if 'SystemTray.FORCE_LINUX_TYPE' is set to GTK");
throw new IllegalArgumentException("Unable to start AppIndicator if 'SystemTray.FORCE_TRAY_TYPE' is set to GTK");
}
Gtk.startGui();
@ -56,6 +59,10 @@ class AppIndicatorTray extends GtkTypeSystemTray {
appIndicator = AppIndicator.app_indicator_new(System.nanoTime() + "DBST", "", AppIndicator.CATEGORY_APPLICATION_STATUS);
}
});
super.waitForStartup();
ImageUtils.determineIconSize(SystemTray.TYPE_APP_INDICATOR);
}
@Override
@ -81,12 +88,12 @@ class AppIndicatorTray extends GtkTypeSystemTray {
@Override
protected
void setIcon_(final String iconPath) {
void setIcon_(final File iconFile) {
dispatch(new Runnable() {
@Override
public
void run() {
AppIndicator.app_indicator_set_icon(appIndicator, iconPath);
AppIndicator.app_indicator_set_icon(appIndicator, iconFile.getAbsolutePath());
if (!isActive) {
isActive = true;

View File

@ -15,6 +15,7 @@
*/
package dorkbox.systemTray.linux;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.concurrent.atomic.AtomicInteger;
@ -22,12 +23,12 @@ import java.util.concurrent.atomic.AtomicInteger;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import dorkbox.systemTray.ImageUtil;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.GCallback;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
class GtkMenuEntry implements MenuEntry, GCallback {
private static final AtomicInteger ID_COUNTER = new AtomicInteger();
@ -48,18 +49,18 @@ class GtkMenuEntry implements MenuEntry, GCallback {
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
*/
GtkMenuEntry(final String label, final String imagePath, final SystemTrayMenuAction callback, final GtkTypeSystemTray parent) {
GtkMenuEntry(final String label, final File imagePath, final SystemTrayMenuAction callback, final GtkTypeSystemTray parent) {
this.parent = parent;
this.text = label;
this.callback = callback;
menuItem = Gtk.gtk_image_menu_item_new_with_label(label);
if (imagePath != null && !imagePath.isEmpty()) {
// NOTE: XFCE uses appindicator3, which DOES NOT support images in the menu. This change was reverted.
if (imagePath != null) {
// NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted.
// see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/
// see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25
image = Gtk.gtk_image_new_from_file(imagePath);
image = Gtk.gtk_image_new_from_file(imagePath.getAbsolutePath());
Gtk.gtk_image_menu_item_set_image(menuItem, image);
// must always re-set always-show after setting the image
@ -108,7 +109,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
}
private
void setImage_(final String imagePath) {
void setImage_(final File imagePath) {
Gtk.dispatch(new Runnable() {
@Override
public
@ -120,8 +121,8 @@ class GtkMenuEntry implements MenuEntry, GCallback {
Gtk.gtk_widget_show_all(menuItem);
if (imagePath != null && !imagePath.isEmpty()) {
image = Gtk.gtk_image_new_from_file(imagePath);
if (imagePath != null) {
image = Gtk.gtk_image_new_from_file(imagePath.getAbsolutePath());
Gtk.gtk_image_menu_item_set_image(menuItem, image);
Gobject.g_object_ref_sink(image);
@ -141,7 +142,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPath(imagePath));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath));
}
}
@ -152,7 +153,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPath(imageUrl));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl));
}
}
@ -163,7 +164,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPath(cacheName, imageStream));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream));
}
}
@ -175,7 +176,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPathNoCache(imageStream));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageStream));
}
}

View File

@ -15,10 +15,9 @@
*/
package dorkbox.systemTray.linux;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import com.sun.jna.NativeLong;
@ -29,7 +28,7 @@ import dorkbox.systemTray.linux.jna.GEventCallback;
import dorkbox.systemTray.linux.jna.GdkEventButton;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.ImageUtils;
/**
* Class for handling all system tray interactions via GTK.
@ -56,37 +55,12 @@ class GtkSystemTray extends GtkTypeSystemTray {
super();
Gtk.startGui();
final CountDownLatch blockUntilStarted = new CountDownLatch(1);
dispatch(new Runnable() {
@Override
public
void run() {
final Pointer trayIcon_ = Gtk.gtk_status_icon_new();
Gtk.gtk_status_icon_set_visible(trayIcon_, false); // immediately set false visibility
trayIcon = trayIcon_;
}
});
// we have to be able to set our name/title, otherwise the gnome-shell extension WILL NOT work
// prevent these from happening...
// Gdk-CRITICAL **: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed
// Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed
dispatch(new Runnable() {
@Override
public
void run() {
// by default, the title/name of the tray icon is "java". We are the only java-based tray icon, so we just use that.
// If you change "SystemTray" to something else, make sure to change it in extension.js as well
// necessary for gnome icon detection/placement because we move tray icons around by name. The name is hardcoded
// in extension.js, so don't change it
Gtk.gtk_status_icon_set_title(trayIcon, "SystemTray");
// ALSO necessary to make sure our gnome-shell extension has the correct name/title! (sometimes it does not)
// can cause
// Gdk-CRITICAL **: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed
// Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed
Gtk.gtk_status_icon_set_name(trayIcon, "SystemTray");
final GEventCallback gtkCallback = new GEventCallback() {
@Override
@ -104,37 +78,31 @@ class GtkSystemTray extends GtkTypeSystemTray {
// have to do this to prevent GC on these objects
gtkCallbacks.add(gtkCallback);
gtkCallbacks.add(button_press_event);
blockUntilStarted.countDown();
}
});
super.waitForStartup();
if (SystemTray.isJavaFxLoaded) {
if (!JavaFX.isEventThread()) {
try {
blockUntilStarted.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
ImageUtils.determineIconSize(SystemTray.TYPE_GTKSTATUSICON);
// we have to be able to set our title, otherwise the gnome-shell extension WILL NOT work
dispatch(new Runnable() {
@Override
public
void run() {
// by default, the title/name of the tray icon is "java". We are the only java-based tray icon, so we just use that.
// If you change "SystemTray" to something else, make sure to change it in extension.js as well
// necessary for gnome icon detection/placement because we move tray icons around by title. This is hardcoded
// in extension.js, so don't change it
Gtk.gtk_status_icon_set_title(trayIcon, "SystemTray");
// can cause
// Gdk-CRITICAL **: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed
// Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed
// Gtk.gtk_status_icon_set_name(trayIcon, "SystemTray");
}
} else if (SystemTray.isSwtLoaded) {
if (SystemTray.FORCE_LINUX_TYPE != SystemTray.LINUX_GTK) {
// GTK system tray has threading issues if we block here (because it is likely in the event thread)
// AppIndicator version doesn't have this problem
try {
blockUntilStarted.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
try {
blockUntilStarted.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
@ -163,12 +131,12 @@ class GtkSystemTray extends GtkTypeSystemTray {
@Override
protected
void setIcon_(final String iconPath) {
void setIcon_(final File iconFile) {
dispatch(new Runnable() {
@Override
public
void run() {
Gtk.gtk_status_icon_set_from_file(trayIcon, iconPath);
Gtk.gtk_status_icon_set_from_file(trayIcon, iconFile.getAbsolutePath());
if (!isActive) {
isActive = true;

View File

@ -16,16 +16,20 @@
package dorkbox.systemTray.linux;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.sun.jna.Pointer;
import dorkbox.systemTray.ImageUtil;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.systemTray.util.JavaFX;
/**
* Derived from
@ -44,6 +48,45 @@ class GtkTypeSystemTray extends SystemTray {
Gtk.dispatch(runnable);
}
protected
void waitForStartup() {
final CountDownLatch blockUntilStarted = new CountDownLatch(1);
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
blockUntilStarted.countDown();
}
});
if (SystemTray.isJavaFxLoaded) {
if (!JavaFX.isEventThread()) {
try {
blockUntilStarted.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else if (SystemTray.isSwtLoaded) {
if (SystemTray.FORCE_TRAY_TYPE != SystemTray.TYPE_GTKSTATUSICON) {
// GTK system tray has threading issues if we block here (because it is likely in the event thread)
// AppIndicator version doesn't have this problem
try {
blockUntilStarted.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
try {
blockUntilStarted.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public
void shutdown() {
@ -215,7 +258,7 @@ class GtkTypeSystemTray extends SystemTray {
}
private
void addMenuEntry_(final String menuText, final String imagePath, final SystemTrayMenuAction callback) {
void addMenuEntry_(final String menuText, final File imagePath, final SystemTrayMenuAction callback) {
// some implementations of appindicator, do NOT like having a menu added, which has no menu items yet.
// see: https://bugs.launchpad.net/glipper/+bug/1203888
@ -252,7 +295,7 @@ class GtkTypeSystemTray extends SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPath(imagePath), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath), callback);
}
}
@ -263,7 +306,7 @@ class GtkTypeSystemTray extends SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPath(imageUrl), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl), callback);
}
}
@ -274,7 +317,7 @@ class GtkTypeSystemTray extends SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPath(cacheName, imageStream), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream), callback);
}
}
@ -286,7 +329,7 @@ class GtkTypeSystemTray extends SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPathNoCache(imageStream), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, imageStream), callback);
}
}
}

View File

@ -51,7 +51,7 @@ class AppIndicator {
// ALSO WHAT VERSION OF GTK to use? appindiactor1 -> GTk2, appindicator3 -> GTK3.
// appindicator3 doesn't support menu icons via GTK2!!
if (SystemTray.FORCE_LINUX_TYPE == SystemTray.LINUX_GTK) {
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTKSTATUSICON) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
if (LIBRARY_DEBUG) {
logger.error("Forcing GTK tray, not using appindicator");

View File

@ -69,7 +69,7 @@ class Gtk {
String gtk3LibName = "libgtk-3.so.0";
// we can force the system to use the swing indicator, which WORKS, but doesn't support transparency in the icon.
if (SystemTray.FORCE_LINUX_TYPE == SystemTray.SWING_INDICATOR) {
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_SWING) {
isLoaded = true;
}

View File

@ -16,31 +16,22 @@
package dorkbox.systemTray.swing;
import dorkbox.systemTray.ImageUtil;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.util.SwingUtil;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JMenuItem;
import javax.swing.UIManager;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
class SwingMenuEntry implements MenuEntry {
private static final String tempDirPath = ImageUtil.TEMP_DIR.getAbsolutePath();
import javax.swing.ImageIcon;
import javax.swing.JMenuItem;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.SwingUtil;
class SwingMenuEntry implements MenuEntry {
private final SwingSystemTrayMenuPopup parent;
private final SystemTray systemTray;
private final JMenuItem menuItem;
@ -49,12 +40,8 @@ class SwingMenuEntry implements MenuEntry {
private volatile String text;
private volatile SystemTrayMenuAction callback;
private int iconHeight = -1;
SwingMenuEntry(final SwingSystemTrayMenuPopup parentMenu, final String label, final String imagePath, final SystemTrayMenuAction callback,
// this is ALWAYS called on the EDT.
SwingMenuEntry(final SwingSystemTrayMenuPopup parentMenu, final String label, final File imagePath, final SystemTrayMenuAction callback,
final SystemTray systemTray) {
this.parent = parentMenu;
this.text = label;
@ -73,7 +60,7 @@ class SwingMenuEntry implements MenuEntry {
menuItem = new JMenuItem(label);
menuItem.addActionListener(swingCallback);
if (imagePath != null && !imagePath.isEmpty()) {
if (imagePath != null) {
setImageIcon(imagePath);
}
@ -109,7 +96,7 @@ class SwingMenuEntry implements MenuEntry {
}
private
void setImage_(final String imagePath) {
void setImage_(final File imagePath) {
SwingUtil.invokeLater(new Runnable() {
@Override
public
@ -119,58 +106,11 @@ class SwingMenuEntry implements MenuEntry {
});
}
// always called on the EDT
private
void setImageIcon(final String imagePath) {
if (imagePath != null && !imagePath.isEmpty()) {
if (iconHeight != 0) {
// this will (and should) be the correct size for the system. On the systems tested, it was 16
// see: http://en-human-begin.blogspot.de/2007/11/javas-icons-by-default.html
Icon icon = UIManager.getIcon("FileView.fileIcon");
iconHeight = icon.getIconHeight();
}
ImageIcon origIcon = new ImageIcon(imagePath);
int origIconHeight = origIcon.getIconHeight();
int origIconWidth = origIcon.getIconWidth();
int savedIconHeight = this.iconHeight;
// it is necessary to resize this icon, so that it matches what our preferred size is for icons
if (origIconHeight != savedIconHeight && savedIconHeight != 0) {
//noinspection SuspiciousNameCombination
Dimension scaledDimension = getScaledDimension(origIconWidth, origIconHeight, savedIconHeight, savedIconHeight);
Image image = origIcon.getImage();
// scale it the smoothly
Image newImage = image.getScaledInstance(scaledDimension.width, scaledDimension.height, java.awt.Image.SCALE_SMOOTH);
origIcon = new ImageIcon(newImage);
// save it to temp spot on disk (so we don't have to KEEP on doing this). (but it MUST be the temp location, otherwise
// it's always 'on the fly')
if (imagePath.startsWith(tempDirPath)) {
// have to delete the old one
File file = new File(imagePath);
boolean delete = file.delete();
if (delete) {
// now write out the new one
String extension = ImageUtil.getExtension(imagePath);
if (extension.equals("")) {
extension = "png"; // made up
}
BufferedImage bufferedImage = getBufferedImage(image);
try {
ImageIO.write(bufferedImage, extension, file);
} catch (IOException e) {
// this shouldn't happen, but you never know...
e.printStackTrace();
}
}
}
}
void setImageIcon(final File imagePath) {
if (imagePath != null) {
ImageIcon origIcon = new ImageIcon(imagePath.getAbsolutePath());
menuItem.setIcon(origIcon);
}
else {
@ -185,7 +125,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPath(imagePath));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath));
}
}
@ -196,7 +136,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPath(imageUrl));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl));
}
}
@ -207,7 +147,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPath(cacheName, imageStream));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream));
}
}
@ -219,7 +159,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null);
}
else {
setImage_(ImageUtil.iconPathNoCache(imageStream));
setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageStream));
}
}
@ -241,48 +181,4 @@ class SwingMenuEntry implements MenuEntry {
}
});
}
private static
Dimension getScaledDimension(int originalWidth, int originalHeight, int boundWidth, int boundHeight) {
//this function comes from http://stackoverflow.com/questions/10245220/java-image-resize-maintain-aspect-ratio
int newWidth = originalWidth;
int newHeight = originalHeight;
// first check if we need to scale width
if (originalWidth > boundWidth) {
//scale width to fit
newWidth = boundWidth;
//scale height to maintain aspect ratio
newHeight = (newWidth * originalHeight) / originalWidth;
}
// then check if we need to scale even with the new height
if (newHeight > boundHeight) {
//scale height to fit instead
newHeight = boundHeight;
//scale width to maintain aspect ratio
newWidth = (newHeight * originalWidth) / originalHeight;
}
return new Dimension(newWidth, newHeight);
}
private static
BufferedImage getBufferedImage(Image image) {
if (image instanceof BufferedImage) {
return (BufferedImage) image;
}
BufferedImage bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
Graphics2D bGr = bimage.createGraphics();
bGr.drawImage(image, 0, 0, null);
bGr.dispose();
// Return the buffered image
return bimage;
}
}

View File

@ -25,15 +25,16 @@ import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import javax.swing.ImageIcon;
import javax.swing.JMenuItem;
import dorkbox.systemTray.ImageUtil;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
@ -45,6 +46,7 @@ import dorkbox.util.SwingUtil;
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521
* https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028
*/
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"})
public
class SwingSystemTray extends dorkbox.systemTray.SystemTray {
volatile SwingSystemTrayMenuPopup menu;
@ -63,6 +65,9 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
public
SwingSystemTray() {
super();
ImageUtils.determineIconSize(dorkbox.systemTray.SystemTray.TYPE_SWING);
SwingUtil.invokeAndWait(new Runnable() {
@Override
public
@ -141,21 +146,23 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
@Override
protected
void setIcon_(final String iconPath) {
void setIcon_(final File iconFile) {
dispatch(new Runnable() {
@Override
public
void run() {
SwingSystemTray tray = SwingSystemTray.this;
// stupid java won't scale it right away, so we have to do this twice to get the correct size
final Image trayImage = new ImageIcon(iconFile.getAbsolutePath()).getImage();
trayImage.flush();
synchronized (tray) {
if (!isActive) {
// here we init. everything
isActive = true;
menu = new SwingSystemTrayMenuPopup();
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
trayIcon = new TrayIcon(trayImage);
// appindicators don't support this, so we cater to the lowest common denominator
@ -207,9 +214,6 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
logger.error("TrayIcon could not be added.", e);
}
} else {
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
tray.trayIcon.setImage(trayImage);
}
}
@ -221,7 +225,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
* Will add a new menu entry, or update one if it already exists
*/
private
void addMenuEntry_(final String menuText, final String imagePath, final SystemTrayMenuAction callback) {
void addMenuEntry_(final String menuText, final File imagePath, final SystemTrayMenuAction callback) {
if (menuText == null) {
throw new NullPointerException("Menu text cannot be null");
}
@ -255,7 +259,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPath(imagePath), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath), callback);
}
}
@ -266,7 +270,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPath(imageUrl), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl), callback);
}
}
@ -277,7 +281,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPath(cacheName, imageStream), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream), callback);
}
}
@ -289,7 +293,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
addMenuEntry_(menuText, null, callback);
}
else {
addMenuEntry_(menuText, ImageUtil.iconPathNoCache(imageStream), callback);
addMenuEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.SIZE, imageStream), callback);
}
}
}

View File

@ -0,0 +1,611 @@
/*
* Copyright 2016 dorkbox, llc
*
* 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 dorkbox.systemTray.util;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.ImageIcon;
import dorkbox.systemTray.SystemTray;
import dorkbox.util.CacheUtil;
import dorkbox.util.FileUtil;
import dorkbox.util.LocationResolver;
import dorkbox.util.OS;
import dorkbox.util.process.ShellProcessBuilder;
public
class ImageUtils {
private static final File TEMP_DIR = new File(CacheUtil.TEMP_DIR, "ResizedImages");
// tray/menu-entry size.
public static volatile int SIZE = 0;
/**
* @param trayType
* LINUX_GTK = 1;
* LINUX_APP_INDICATOR = 2;
* SWING_INDICATOR = 3;
*/
public static
void determineIconSize(int trayType) {
if (SystemTray.AUTO_TRAY_SIZE) {
if (OS.isWindows()) {
// windows will automatically scale the tray size
SIZE = SystemTray.DEFAULT_WINDOWS_SIZE;
} else {
// GtkStatusIcon will USUALLY automatically scale the icon
// AppIndicator will NOT scale the icon
if (trayType == SystemTray.TYPE_SWING || trayType == SystemTray.TYPE_GTKSTATUSICON) {
// swing or GtkStatusIcon on linux/mac? use the default settings
SIZE = SystemTray.DEFAULT_LINUX_SIZE;
} else {
int uiScalingFactor = 0;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
PrintStream outputStream = new PrintStream(byteArrayOutputStream);
// gsettings get org.gnome.desktop.interface scaling-factor
final ShellProcessBuilder shellVersion = new ShellProcessBuilder(outputStream);
shellVersion.setExecutable("gsettings");
shellVersion.addArgument("get");
shellVersion.addArgument("org.gnome.desktop.interface");
shellVersion.addArgument("scaling-factor");
shellVersion.start();
String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
if (!output.isEmpty()) {
if (SystemTray.DEBUG) {
SystemTray.logger.info("Checking scaling factor for GTK environment, should start with 'uint32', value: '{}'", output);
}
// DEFAULT icon size is 16. HiDpi changes this scale, so we should use it as well.
// should be: uint32 0 or something
if (output.startsWith("uint32")) {
String value = output.substring(output.indexOf(" ")+1, output.length()-1);
uiScalingFactor = Integer.parseInt(value);
// 0 is disabled (no scaling)
// 1 is enabled (default scale)
// 2 is 2x scale
// 3 is 3x scale
// etc
// A setting of 2, 3, etc, which is all you can do with scaling-factor
// To enable HiDPI, use gsettings:
// gsettings set org.gnome.desktop.interface scaling-factor 2
}
}
} catch (Throwable e) {
if (SystemTray.DEBUG) {
SystemTray.logger.error("Cannot check scaling factor", e);
}
}
// the DEFAULT scale is 16
if (uiScalingFactor > 1) {
SIZE = SystemTray.DEFAULT_LINUX_SIZE * uiScalingFactor;
} else {
SIZE = SystemTray.DEFAULT_LINUX_SIZE;
}
if (SystemTray.DEBUG) {
SystemTray.logger.info("uiScaling factor is '{}', auto tray size is '{}'.", uiScalingFactor, SIZE);
}
}
}
} else {
if (OS.isWindows()) {
SIZE = SystemTray.DEFAULT_WINDOWS_SIZE;
} else {
SIZE = SystemTray.DEFAULT_LINUX_SIZE;
}
}
}
private static
File getErrorImage(final String cacheName) {
try {
File save = CacheUtil.save(cacheName, ImageUtils.class.getResource("error_32.png"));
// since it's the error file, we want to delete it on exit!
save.deleteOnExit();
return save;
} catch (IOException e) {
throw new RuntimeException("Serious problems! Unable to extract error image, this should NEVER happen!", e);
}
}
private static
File getIfCachedOrError(final String cacheName) {
try {
File check = CacheUtil.check(cacheName);
if (check != null) {
return check;
}
} catch (IOException e) {
SystemTray.logger.error("Error checking cache for information. Using error icon instead", e);
return getErrorImage(cacheName);
}
return null;
}
public static synchronized
File resizeAndCache(final int size, final String fileName) {
// check if we already have this file information saved to disk, based on size
String cacheName = size + "_" + fileName;
// if we already have this fileName, reuse it
File check = getIfCachedOrError(cacheName);
if (check != null) {
return check;
}
// no cached file, so we resize then save the new one.
String newFileOnDisk;
try {
newFileOnDisk = resizeFile(size, fileName);
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
return getErrorImage(cacheName);
}
try {
return CacheUtil.save(cacheName, newFileOnDisk);
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error caching image. Using error icon instead", e);
return getErrorImage(cacheName);
}
}
@SuppressWarnings("Duplicates")
public static synchronized
File resizeAndCache(final int size, final URL imageUrl) {
String cacheName = size + "_" + imageUrl.getPath();
// if we already have this fileName, reuse it
File check = getIfCachedOrError(cacheName);
if (check != null) {
return check;
}
// no cached file, so we resize then save the new one.
boolean needsResize = true;
try {
InputStream inputStream = imageUrl.openStream();
Dimension imageSize = getImageSize(inputStream);
//noinspection NumericCastThatLosesPrecision
if (size == ((int) imageSize.getWidth()) && size == ((int) imageSize.getHeight())) {
// we can reuse this URL (it's the correct size).
needsResize = false;
}
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
return getErrorImage(cacheName);
}
if (needsResize) {
// we have to hop through hoops.
try {
File resizedFile = resizeFileNoCheck(size, imageUrl);
// now cache that file
try {
return CacheUtil.save(cacheName, resizedFile);
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error caching image. Using error icon instead", e);
return getErrorImage(cacheName);
}
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
return getErrorImage(cacheName);
}
} else {
// no resize necessary, just cache as is.
try {
return CacheUtil.save(cacheName, imageUrl);
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error caching image. Using error icon instead", e);
return getErrorImage(cacheName);
}
}
}
@SuppressWarnings("Duplicates")
public static synchronized
File resizeAndCache(final int size, String cacheName, final InputStream imageStream) {
if (cacheName == null) {
cacheName = CacheUtil.createNameAsHash(imageStream);
}
// check if we already have this file information saved to disk, based on size
cacheName = size + "_" + cacheName;
// if we already have this fileName, reuse it
File check = getIfCachedOrError(cacheName);
if (check != null) {
return check;
}
// no cached file, so we resize then save the new one.
boolean needsResize = true;
try {
Dimension imageSize = getImageSize(imageStream);
//noinspection NumericCastThatLosesPrecision
if (size == ((int) imageSize.getWidth()) && size == ((int) imageSize.getHeight())) {
// we can reuse this URL (it's the correct size).
needsResize = false;
}
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
return getErrorImage(cacheName);
}
if (needsResize) {
// we have to hop through hoops.
try {
File resizedFile = resizeFileNoCheck(size, imageStream);
// now cache that file
try {
return CacheUtil.save(cacheName, resizedFile);
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error caching image. Using error icon instead", e);
return getErrorImage(cacheName);
}
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
return getErrorImage(cacheName);
}
} else {
// no resize necessary, just cache as is.
try {
return CacheUtil.save(cacheName, imageStream);
} catch (IOException e) {
// have to serve up the error image instead.
SystemTray.logger.error("Error caching image. Using error icon instead", e);
return getErrorImage(cacheName);
}
}
}
public static
File resizeAndCache(final int size, final InputStream imageStream) {
return resizeAndCache(size, null, imageStream);
}
// static void asdasd () {
//
// ImageUtils.resizeAndCache(imagePath);
//
// Image trayImage1 = new ImageIcon(iconPath).getImage().getScaledInstance(dorkbox.systemTray.SystemTray.TRAY_SIZE, -1,
// Image.SCALE_SMOOTH);
// trayImage1.flush();
//
//
// Dimension imageSize = null;
// try {
// imageSize = ImageUtils.getImageSize(imagePath);
// } catch (IOException e) {
// SystemTray.logger.error("Unable to get the image size for '{}'. Unable to set image for menu entry.", imagePath, e);
// return;
// }
//
// int origIconHeight = (int) imageSize.getHeight();
// int origIconWidth = (int) imageSize.getWidth();
//
// int savedIconHeight = this.iconHeight;
//
// // it is necessary to resize this icon, so that it matches what our preferred size is for icons
// if (origIconHeight != savedIconHeight && savedIconHeight != 0) {
// //noinspection SuspiciousNameCombination
// Dimension newDimension = getScaledDimension(origIconWidth, origIconHeight, savedIconHeight, savedIconHeight);
//
// ImageIcon origIcon = new ImageIcon(imagePath);
// Image image = origIcon.getImage();
//
// // scale it the smoothly
// Image newImage = image.getScaledInstance(newDimension.width, newDimension.height, java.awt.Image.SCALE_SMOOTH);
// origIcon = new ImageIcon(newImage);
//
// // save it to temp spot on disk (so we don't have to KEEP on doing this). (but it MUST be the temp location, otherwise
// // it's always 'on the fly')
// if (imagePath.startsWith(tempDirPath)) {
// // have to delete the old one
// File file = new File(imagePath);
// boolean delete = file.delete();
//
// if (delete) {
// // now write out the new one
// String extension = CacheUtil.getExtension(imagePath);
// if (extension.equals("")) {
// extension = "png"; // made up
// }
// BufferedImage bufferedImage = getBufferedImage(image);
// try {
// ImageIO.write(bufferedImage, extension, file);
// } catch (IOException e) {
// // this shouldn't happen, but you never know...
// e.printStackTrace();
// }
// }
// }
// }
// }
// private static
// Dimension getScaledDimension(int originalWidth, int originalHeight, int boundWidth, int boundHeight) {
// //this function comes from http://stackoverflow.com/questions/10245220/java-image-resize-maintain-aspect-ratio
//
// int newWidth = originalWidth;
// int newHeight = originalHeight;
//
// // first check if we need to scale width
// if (originalWidth > boundWidth) {
// //scale width to fit
// newWidth = boundWidth;
//
// //scale height to maintain aspect ratio
// newHeight = (newWidth * originalHeight) / originalWidth;
// }
//
// // then check if we need to scale even with the new height
// if (newHeight > boundHeight) {
// //scale height to fit instead
// newHeight = boundHeight;
//
// //scale width to maintain aspect ratio
// newWidth = (newHeight * originalWidth) / originalHeight;
// }
//
// return new Dimension(newWidth, newHeight);
// }
/**
* Resizes the given URL to the specified size. No checks are performed if it's the correct size to begin with.
*
* @return the file on disk that is the resized icon
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private static
File resizeFileNoCheck(final int size, final URL fileUrl) throws IOException {
InputStream inputStream = fileUrl.openStream();
// have to resize the file (and return the new path)
// now have to resize this file.
File newFile = new File(TEMP_DIR, "temp_resize").getAbsoluteFile();
Image image;
// resize the image, keep aspect
image = new ImageIcon(ImageIO.read(inputStream)).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
image.flush();
// have to do this twice, so that it will finish loading the image (weird callback stuff is required if we don't do this)
image = new ImageIcon(image).getImage();
image.flush();
// make whatever dirs we need to.
newFile.getParentFile().mkdirs();
// if it's already there, we have to delete it
newFile.delete();
// now write out the new one
String extension = FileUtil.getExtension(fileUrl.getPath());
if (extension.equals("")) {
extension = "png"; // made up
}
BufferedImage bufferedImage = getBufferedImage(image);
ImageIO.write(bufferedImage, extension, newFile);
return newFile;
}
/**
* Resizes the given URL to the specified size. No checks are performed if it's the correct size to begin with.
*
* @return the file on disk that is the resized icon
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private static
File resizeFileNoCheck(final int size, InputStream inputStream) throws IOException {
// have to resize the file (and return the new path)
// now have to resize this file.
File newFile = new File(TEMP_DIR, "temp_resize").getAbsoluteFile();
Image image;
// resize the image, keep aspect
image = new ImageIcon(ImageIO.read(inputStream)).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
image.flush();
// have to do this twice, so that it will finish loading the image (weird callback stuff is required if we don't do this)
image = new ImageIcon(image).getImage();
image.flush();
// make whatever dirs we need to.
newFile.getParentFile().mkdirs();
// if it's already there, we have to delete it
newFile.delete();
// now write out the new one
BufferedImage bufferedImage = getBufferedImage(image);
ImageIO.write(bufferedImage, "png", newFile); // made up extension
return newFile;
}
/**
* Resizes the image (as a FILE on disk, or as a RESOURCE name), saves it as a file on disk. This file will be OVER-WRITTEN by any
* operation that calls this method.
*
* @return the file string on disk that is the resized icon
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private static
String resizeFile(final int size, final String fileName) throws IOException {
FileInputStream fileInputStream = new FileInputStream(fileName);
Dimension imageSize = getImageSize(fileInputStream);
//noinspection NumericCastThatLosesPrecision
if (size == ((int) imageSize.getWidth()) && size == ((int) imageSize.getHeight())) {
// we can reuse this file.
return fileName;
}
// have to resize the file (and return the new path)
// now have to resize this file.
File newFile = new File(TEMP_DIR, "temp_resize").getAbsoluteFile();
Image image;
// is file sitting on drive
File iconTest = new File(fileName);
if (iconTest.isFile() && iconTest.canRead()) {
final String absolutePath = iconTest.getAbsolutePath();
// resize the image, keep aspect
image = new ImageIcon(absolutePath).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
image.flush();
}
else {
// suck it out of a URL/Resource (with debugging if necessary)
final URL systemResource = LocationResolver.getResource(fileName);
// resize the image, keep aspect
image = new ImageIcon(systemResource).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
image.flush();
}
// have to do this twice, so that it will finish loading the image (weird callback stuff is required if we don't do this)
image = new ImageIcon(image).getImage();
image.flush();
// make whatever dirs we need to.
newFile.getParentFile().mkdirs();
// if it's already there, we have to delete it
newFile.delete();
// now write out the new one
String extension = FileUtil.getExtension(fileName);
if (extension.equals("")) {
extension = "png"; // made up
}
BufferedImage bufferedImage = getBufferedImage(image);
ImageIO.write(bufferedImage, extension, newFile);
return newFile.getAbsolutePath();
}
private static
BufferedImage getBufferedImage(Image image) {
if (image instanceof BufferedImage) {
return (BufferedImage) image;
}
BufferedImage bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
Graphics2D bGr = bimage.createGraphics();
bGr.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY));
bGr.drawImage(image, 0, 0, null);
bGr.dispose();
// Return the buffered image
return bimage;
}
/**
* Reads the image size information from the specified file, without loading the entire file.
*
* @param fileStream the input stream of the file
*
* @return the image size dimensions. IOException if it could not be read
*/
private static
Dimension getImageSize(InputStream fileStream) throws IOException {
ImageInputStream in = null;
ImageReader reader = null;
try {
in = ImageIO.createImageInputStream(fileStream);
final Iterator<ImageReader> readers = ImageIO.getImageReaders(in);
if (readers.hasNext()) {
reader = readers.next();
reader.setInput(in);
return new Dimension(reader.getWidth(0), reader.getHeight(0));
}
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {
}
}
if (reader != null) {
reader.dispose();
}
}
throw new IOException("Unable to read file inputStream for image size data.");
}
}

View File

@ -0,0 +1,203 @@
/*
* Copyright 2016 dorkbox, llc
*
* 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 dorkbox.systemTray.util;
import static dorkbox.systemTray.SystemTray.logger;
import java.awt.Robot;
import java.util.Locale;
import dorkbox.systemTray.SystemTray;
import dorkbox.util.BootStrapClassLoader;
import dorkbox.util.OS;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
/**
* Fixes issues with some java runtimes
*/
public
class WindowsSystemTraySwing {
// oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that!
public static void fix() {
// if we are using swing (in windows only) the icon size is usually incorrect. Here we have to fix that.
if (!OS.isWindows()) {
return;
}
String vendor = System.getProperty("java.vendor").toLowerCase(Locale.US);
// 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.
return;
}
boolean isWindowsSwingTrayLoaded = false;
try {
// 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 swing (in windows only) the icon size is usually incorrect. We cannot fix that if it's already loaded.
isWindowsSwingTrayLoaded = (null != m.invoke(cl, "sun.awt.windows.WTrayIconPeer")) ||
(null != m.invoke(cl, "java.awt.SystemTray"));
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error detecting javaFX/SWT mode", e);
}
}
if (isWindowsSwingTrayLoaded) {
throw new RuntimeException("Unable to initialize the swing tray in windows, it has already been created!");
}
/*
* When DISTRIBUTING the JRE/JDK by Sun/Oracle, the license agreement states that we cannot create/modify specific files.
*
************* (when DISTRIBUTING the JRE/JDK...)
* C. Java Technology Restrictions. You may not create, modify, or change the behavior of, or authorize your licensees to create, modify,
* or change the behavior of, classes, interfaces, or subpackages that are in any way identified as "java", "javax", "sun" or similar
* convention as specified by Oracle in any naming convention designation.
*************
*
* Since we are not distributing a modified file, it does not apply to us.
*
* Again, just to be ABSOLUTELY CLEAR. This is for DISTRIBUTING the runtime.
*
* ************************************
* To follow the license for DISTRIBUTION, these files themselves CANNOT BE MODIFIED in any way,
* and if they are modified THEY CANNOT BE DISTRIBUTED.
* ************************************
*
* 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 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
*/
try {
// necessary to initialize sun.awt.windows.WObjectPeer native initIDs()
@SuppressWarnings("unused")
Robot robot = new Robot();
ClassPool pool = ClassPool.getDefault();
byte[] trayBytes;
byte[] trayIconBytes;
{
CtClass trayClass = pool.get("sun.awt.windows.WSystemTrayPeer");
// now have to make a new "system tray" (that is null) in order to init/load this class completely
// have to modify the SystemTray.getIconSize as well.
trayClass.setModifiers(trayClass.getModifiers() & javassist.Modifier.PUBLIC);
trayClass.getConstructors()[0].setModifiers(trayClass.getConstructors()[0].getModifiers() & javassist.Modifier.PUBLIC);
CtMethod ctMethodGet = trayClass.getDeclaredMethod("getTrayIconSize");
ctMethodGet.setBody("{" +
"return new java.awt.Dimension(" + ImageUtils.SIZE + ", " + ImageUtils.SIZE + ");" +
"}");
trayBytes = trayClass.toBytecode();
}
{
CtClass trayIconClass = pool.get("sun.awt.windows.WTrayIconPeer");
CtMethod ctMethodCreate = trayIconClass.getDeclaredMethod("createNativeImage");
CtMethod ctMethodUpdate = trayIconClass.getDeclaredMethod("updateNativeImage");
int TRAY_MASK = (ImageUtils.SIZE * ImageUtils.SIZE) / 8;
ctMethodCreate.setBody("{" +
"java.awt.image.BufferedImage bufferedImage = $1;\n" +
"java.awt.image.Raster rasterImage = bufferedImage.getRaster();\n" +
"final byte[] mask = new byte[" + TRAY_MASK + "];\n" +
"final int pixels[] = ((java.awt.image.DataBufferInt)rasterImage.getDataBuffer()).getData();\n" +
"int numberOfPixels = pixels.length;\n" +
"int rasterImageWidth = rasterImage.getWidth();\n" +
"for (int i = 0; i < numberOfPixels; i++) {\n" +
" int iByte = i / 8;\n" +
" int augmentMask = 1 << (7 - (i % 8));\n" +
" if ((pixels[i] & 0xFF000000) == 0) {\n" +
" if (iByte < mask.length) {\n" +
" mask[iByte] |= augmentMask;\n" +
" }\n" +
" }\n" +
"}\n" +
"if (rasterImage instanceof sun.awt.image.IntegerComponentRaster) {\n" +
" rasterImageWidth = ((sun.awt.image.IntegerComponentRaster)rasterImage).getScanlineStride();\n" +
"}\n" +
"setNativeIcon(((java.awt.image.DataBufferInt)bufferedImage.getRaster().getDataBuffer()).getData(), " +
"mask, rasterImageWidth, rasterImage.getWidth(), rasterImage.getHeight());\n" +
"}");
ctMethodUpdate.setBody("{" +
"java.awt.Image image = $1;\n" +
"if (isDisposed()) {\n" +
" return;\n" +
"}\n" +
"int imageWidth = image.getWidth(observer);\n" +
"int imageHeight = image.getWidth(observer);\n" +
"java.awt.image.BufferedImage trayIcon = new java.awt.image.BufferedImage(imageWidth, imageHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);\n" +
"java.awt.Graphics2D g = trayIcon.createGraphics();\n" +
"if (g != null) {\n" +
" try {\n" +
// this will render the image "nicely"
" g.addRenderingHints(new java.awt.RenderingHints(java.awt.RenderingHints.KEY_RENDERING," +
"java.awt.RenderingHints.VALUE_RENDER_QUALITY));\n" +
" g.drawImage(image, 0, 0, imageWidth, imageHeight, observer);\n" +
" createNativeImage(trayIcon);\n" +
" updateNativeIcon(!firstUpdate);\n" +
" if (firstUpdate) {" +
" firstUpdate = false;\n" +
" }\n" +
" } finally {\n" +
" g.dispose();\n" +
" }\n" +
"}" +
"}");
trayIconBytes = trayIconClass.toBytecode();
}
// whoosh, past the classloader and directly into memory.
BootStrapClassLoader.defineClass(trayBytes);
BootStrapClassLoader.defineClass(trayIconBytes);
if (SystemTray.DEBUG) {
logger.info("Successfully changed tray icon size to: {}", ImageUtils.SIZE);
}
} catch (Exception e) {
logger.error("Error setting tray icon size to: {}", ImageUtils.SIZE, e);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B