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.InputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.net.URL; import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.CountDownLatch; 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.AppIndicator;
import dorkbox.systemTray.linux.jna.Gtk; import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.swing.SwingSystemTray; import dorkbox.systemTray.swing.SwingSystemTray;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.systemTray.util.JavaFX; import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.Swt; import dorkbox.systemTray.util.Swt;
import dorkbox.systemTray.util.WindowsSystemTraySwing;
import dorkbox.util.CacheUtil;
import dorkbox.util.OS; import dorkbox.util.OS;
import dorkbox.util.Property; import dorkbox.util.Property;
import dorkbox.util.process.ShellProcessBuilder; import dorkbox.util.process.ShellProcessBuilder;
@ -50,22 +52,49 @@ import dorkbox.util.process.ShellProcessBuilder;
/** /**
* Factory and base-class for system tray implementations. * Factory and base-class for system tray implementations.
*/ */
@SuppressWarnings({"unused", "Duplicates"}) @SuppressWarnings({"unused", "Duplicates", "DanglingJavadoc", "WeakerAccess"})
public abstract public abstract
class SystemTray { class SystemTray {
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class); public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
public static final int LINUX_GTK = 1; public static final int TYPE_AUTO_DETECT = 0;
public static final int LINUX_APP_INDICATOR = 2; public static final int TYPE_GTKSTATUSICON = 1;
public static final int SWING_INDICATOR = 3; public static final int TYPE_APP_INDICATOR = 2;
public static final int TYPE_SWING = 3;
@Property @Property
/** How long to wait when updating menu entries before the request times-out */ /** How long to wait when updating menu entries before the request times-out */
public static final int TIMEOUT = 2; public static final int TIMEOUT = 2;
@Property @Property
/** Size of the tray, so that the icon can properly scale based on OS. (if it's not exact) */ /** Enables auto-detection for the system tray. This should be mostly successful.
public static int TRAY_SIZE = 22; * <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 @Property
/** Forces the system tray to always choose GTK2 (even when GTK3 might be available). */ /** Forces the system tray to always choose GTK2 (even when GTK3 might be available). */
@ -73,7 +102,7 @@ class SystemTray {
@Property @Property
/** Forces the system tray detection to be Automatic (0), GTK (1), AppIndicator (2), or Swing (3). This is an advanced feature. */ /** 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 @Property
/** /**
@ -90,7 +119,6 @@ class SystemTray {
private static volatile SystemTray systemTray = null; private static volatile SystemTray systemTray = null;
static boolean isKDE = false;
public final static boolean isJavaFxLoaded; public final static boolean isJavaFxLoaded;
public final static boolean isSwtLoaded; public final static boolean isSwtLoaded;
@ -135,6 +163,7 @@ class SystemTray {
Class<? extends SystemTray> trayType = null; Class<? extends SystemTray> trayType = null;
boolean isKDE = false;
if (DEBUG) { if (DEBUG) {
logger.debug("is JavaFX detected? {}", isJavaFxLoaded); 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. // 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) { 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 // 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"); // System.setProperty("SWT_GTK3", "0");
// was SWT forced? // 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) { if (!isSwt_GTK3) {
// check a different property // 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) { if (isSwt_GTK3 && FORCE_GTK2) {
@ -200,7 +231,7 @@ class SystemTray {
} }
if (DEBUG) { if (DEBUG) {
switch (FORCE_LINUX_TYPE) { switch (FORCE_TRAY_TYPE) {
case 1: logger.debug("Forced tray type: GtkStatusIcon"); break; case 1: logger.debug("Forced tray type: GtkStatusIcon"); break;
case 2: logger.debug("Forced tray type: AppIndicator"); break; case 2: logger.debug("Forced tray type: AppIndicator"); break;
case 3: logger.debug("Forced tray type: Swing"); break; case 3: logger.debug("Forced tray type: Swing"); break;
@ -210,17 +241,11 @@ class SystemTray {
logger.debug("FORCE_GTK2: {}", FORCE_GTK2); 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 // 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 // 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. // all examined ones sometimes have it (and it's more than just text), or they don't have it at all.
if (OS.isWindows()) { if (FORCE_TRAY_TYPE != TYPE_SWING && OS.isLinux()) {
// 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()) {
// see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running // 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. // 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 { try {
trayType = GtkSystemTray.class; trayType = GtkSystemTray.class;
} catch (Throwable e1) { } 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 { try {
trayType = AppIndicatorTray.class; trayType = AppIndicatorTray.class;
} catch (Throwable e1) { } 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 // 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"); String XDG = System.getenv("XDG_CURRENT_DESKTOP");
// BLEH. if gnome-shell is running, IT'S REALLY GNOME! // BLEH. if gnome-shell is running, IT'S REALLY GNOME!
// we must ALWAYS do this check!! // we must ALWAYS do this check!!
boolean isReallyGnome = false; boolean isReallyGnome = false;
@ -306,14 +332,54 @@ class SystemTray {
} }
} }
else if ("xfce".equalsIgnoreCase(XDG)) { 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 { 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) { } catch (Throwable e) {
if (DEBUG) { 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 { try {
trayType = GtkSystemTray.class; trayType = GtkSystemTray.class;
} catch (Throwable e1) { } 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 // this is windows OR mac
if (trayType == null && java.awt.SystemTray.isSupported()) { if (trayType == null && java.awt.SystemTray.isSupported()) {
try { try {
@ -519,9 +592,16 @@ class SystemTray {
else { else {
SystemTray systemTray_ = null; 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() && if (OS.isLinux() &&
trayType == AppIndicatorTray.class && trayType == AppIndicatorTray.class &&
Gtk.isGtk2 && Gtk.isGtk2 &&
@ -545,8 +625,6 @@ class SystemTray {
systemTray_ = (SystemTray) trayType.getConstructors()[0].newInstance(); systemTray_ = (SystemTray) trayType.getConstructors()[0].newInstance();
logger.info("Successfully Loaded: {}", trayType.getSimpleName()); logger.info("Successfully Loaded: {}", trayType.getSimpleName());
} catch (NoSuchAlgorithmException e) {
logger.error("Unsupported hashing algorithm!");
} catch (Exception e) { } catch (Exception e) {
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e); logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
} }
@ -653,7 +731,7 @@ class SystemTray {
void setStatus(String statusText); void setStatus(String statusText);
protected abstract protected abstract
void setIcon_(String iconPath); void setIcon_(File iconPath);
/** /**
* Changes the tray icon used. * Changes the tray icon used.
@ -665,8 +743,7 @@ class SystemTray {
*/ */
public public
void setIcon(String imagePath) { void setIcon(String imagePath) {
final String fullPath = ImageUtil.iconPath(imagePath); setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath));
setIcon_(fullPath);
} }
/** /**
@ -679,8 +756,7 @@ class SystemTray {
*/ */
public public
void setIcon(URL imageUrl) { void setIcon(URL imageUrl) {
final String fullPath = ImageUtil.iconPath(imageUrl); setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl));
setIcon_(fullPath);
} }
/** /**
@ -694,8 +770,7 @@ class SystemTray {
*/ */
public public
void setIcon(String cacheName, InputStream imageStream) { void setIcon(String cacheName, InputStream imageStream) {
final String fullPath = ImageUtil.iconPath(cacheName, imageStream); setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream));
setIcon_(fullPath);
} }
/** /**
@ -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 * 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. * 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 * @param imageStream the InputStream of the icon to use
*/ */
@Deprecated
public public
void setIcon(InputStream imageStream) { void setIcon(InputStream imageStream) {
@SuppressWarnings("deprecation") setIcon_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageStream));
final String fullPath = ImageUtil.iconPathNoCache(imageStream);
setIcon_(fullPath);
} }
@ -764,9 +833,6 @@ class SystemTray {
/** /**
* Adds a menu entry to the tray icon with text + image * 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 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 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 * @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) * @param newMenuText the new menu text (this will replace the original menu text)
*/ */
public final 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 // have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true); 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 origMenuText the original menu text
* @param imagePath the new path for the image to use or null to delete the image * @param imagePath the new path for the image to use or null to delete the image
*/ */
public final 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 // have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true); 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 * @param imageUrl the new URL for the image to use or null to delete the image
*/ */
public final 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 // have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true); 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 * @param imageStream the InputStream of the image to use or null to delete the image
*/ */
public final 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 // have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true); final AtomicBoolean hasValue = new AtomicBoolean(true);
@ -960,14 +1026,11 @@ class SystemTray {
/** /**
* Updates (or changes) the menu entry's text. * 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 origMenuText the original menu text
* @param imageStream the new path for the image to use or null to delete the image * @param imageStream the new path for the image to use or null to delete the image
*/ */
public final 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 // have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true); final AtomicBoolean hasValue = new AtomicBoolean(true);
@ -1013,7 +1076,7 @@ class SystemTray {
* @param newCallback the new callback (this will replace the original callback) * @param newCallback the new callback (this will replace the original callback)
*/ */
public final 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 // have to wait for the value
final CountDownLatch countDownLatch = new CountDownLatch(1); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true); final AtomicBoolean hasValue = new AtomicBoolean(true);

View File

@ -15,14 +15,17 @@
*/ */
package dorkbox.systemTray.linux; package dorkbox.systemTray.linux;
import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
import com.sun.jna.Pointer; import com.sun.jna.Pointer;
import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.linux.jna.AppIndicator; import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.AppIndicatorInstanceStruct; import dorkbox.systemTray.linux.jna.AppIndicatorInstanceStruct;
import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk; import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Class for handling all system tray interactions. * Class for handling all system tray interactions.
@ -42,9 +45,9 @@ class AppIndicatorTray extends GtkTypeSystemTray {
public public
AppIndicatorTray() { 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 // 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(); Gtk.startGui();
@ -56,6 +59,10 @@ class AppIndicatorTray extends GtkTypeSystemTray {
appIndicator = AppIndicator.app_indicator_new(System.nanoTime() + "DBST", "", AppIndicator.CATEGORY_APPLICATION_STATUS); appIndicator = AppIndicator.app_indicator_new(System.nanoTime() + "DBST", "", AppIndicator.CATEGORY_APPLICATION_STATUS);
} }
}); });
super.waitForStartup();
ImageUtils.determineIconSize(SystemTray.TYPE_APP_INDICATOR);
} }
@Override @Override
@ -81,12 +88,12 @@ class AppIndicatorTray extends GtkTypeSystemTray {
@Override @Override
protected protected
void setIcon_(final String iconPath) { void setIcon_(final File iconFile) {
dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
AppIndicator.app_indicator_set_icon(appIndicator, iconPath); AppIndicator.app_indicator_set_icon(appIndicator, iconFile.getAbsolutePath());
if (!isActive) { if (!isActive) {
isActive = true; isActive = true;

View File

@ -15,6 +15,7 @@
*/ */
package dorkbox.systemTray.linux; package dorkbox.systemTray.linux;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.concurrent.atomic.AtomicInteger; 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.NativeLong;
import com.sun.jna.Pointer; import com.sun.jna.Pointer;
import dorkbox.systemTray.ImageUtil;
import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.GCallback; import dorkbox.systemTray.linux.jna.GCallback;
import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk; import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
class GtkMenuEntry implements MenuEntry, GCallback { class GtkMenuEntry implements MenuEntry, GCallback {
private static final AtomicInteger ID_COUNTER = new AtomicInteger(); 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! * 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 * 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.parent = parent;
this.text = label; this.text = label;
this.callback = callback; this.callback = callback;
menuItem = Gtk.gtk_image_menu_item_new_with_label(label); menuItem = Gtk.gtk_image_menu_item_new_with_label(label);
if (imagePath != null && !imagePath.isEmpty()) { if (imagePath != null) {
// NOTE: XFCE uses appindicator3, which DOES NOT support images in the menu. This change was reverted. // 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://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 // 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); Gtk.gtk_image_menu_item_set_image(menuItem, image);
// must always re-set always-show after setting the image // must always re-set always-show after setting the image
@ -108,7 +109,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
} }
private private
void setImage_(final String imagePath) { void setImage_(final File imagePath) {
Gtk.dispatch(new Runnable() { Gtk.dispatch(new Runnable() {
@Override @Override
public public
@ -120,8 +121,8 @@ class GtkMenuEntry implements MenuEntry, GCallback {
Gtk.gtk_widget_show_all(menuItem); Gtk.gtk_widget_show_all(menuItem);
if (imagePath != null && !imagePath.isEmpty()) { if (imagePath != null) {
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); Gtk.gtk_image_menu_item_set_image(menuItem, image);
Gobject.g_object_ref_sink(image); Gobject.g_object_ref_sink(image);
@ -141,7 +142,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null); setImage_(null);
} }
else { else {
setImage_(ImageUtil.iconPath(imagePath)); setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath));
} }
} }
@ -152,7 +153,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null); setImage_(null);
} }
else { else {
setImage_(ImageUtil.iconPath(imageUrl)); setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl));
} }
} }
@ -163,7 +164,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null); setImage_(null);
} }
else { else {
setImage_(ImageUtil.iconPath(cacheName, imageStream)); setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream));
} }
} }
@ -175,7 +176,7 @@ class GtkMenuEntry implements MenuEntry, GCallback {
setImage_(null); setImage_(null);
} }
else { else {
setImage_(ImageUtil.iconPathNoCache(imageStream)); setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageStream));
} }
} }

View File

@ -15,10 +15,9 @@
*/ */
package dorkbox.systemTray.linux; package dorkbox.systemTray.linux;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import com.sun.jna.NativeLong; 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.GdkEventButton;
import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk; 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. * Class for handling all system tray interactions via GTK.
@ -56,37 +55,12 @@ class GtkSystemTray extends GtkTypeSystemTray {
super(); super();
Gtk.startGui(); Gtk.startGui();
final CountDownLatch blockUntilStarted = new CountDownLatch(1);
dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
final Pointer trayIcon_ = Gtk.gtk_status_icon_new(); final Pointer trayIcon_ = Gtk.gtk_status_icon_new();
Gtk.gtk_status_icon_set_visible(trayIcon_, false); // immediately set false visibility
trayIcon = trayIcon_; 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() { final GEventCallback gtkCallback = new GEventCallback() {
@Override @Override
@ -104,37 +78,31 @@ class GtkSystemTray extends GtkTypeSystemTray {
// have to do this to prevent GC on these objects // have to do this to prevent GC on these objects
gtkCallbacks.add(gtkCallback); gtkCallbacks.add(gtkCallback);
gtkCallbacks.add(button_press_event); gtkCallbacks.add(button_press_event);
blockUntilStarted.countDown();
} }
}); });
super.waitForStartup();
if (SystemTray.isJavaFxLoaded) { ImageUtils.determineIconSize(SystemTray.TYPE_GTKSTATUSICON);
if (!JavaFX.isEventThread()) {
try { // we have to be able to set our title, otherwise the gnome-shell extension WILL NOT work
blockUntilStarted.await(10, TimeUnit.SECONDS); dispatch(new Runnable() {
} catch (InterruptedException e) { @Override
e.printStackTrace(); 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 @Override
protected protected
void setIcon_(final String iconPath) { void setIcon_(final File iconFile) {
dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
Gtk.gtk_status_icon_set_from_file(trayIcon, iconPath); Gtk.gtk_status_icon_set_from_file(trayIcon, iconFile.getAbsolutePath());
if (!isActive) { if (!isActive) {
isActive = true; isActive = true;

View File

@ -16,16 +16,20 @@
package dorkbox.systemTray.linux; package dorkbox.systemTray.linux;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.sun.jna.Pointer; import com.sun.jna.Pointer;
import dorkbox.systemTray.ImageUtil;
import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk; import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.systemTray.util.JavaFX;
/** /**
* Derived from * Derived from
@ -44,6 +48,45 @@ class GtkTypeSystemTray extends SystemTray {
Gtk.dispatch(runnable); 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 @Override
public public
void shutdown() { void shutdown() {
@ -215,7 +258,7 @@ class GtkTypeSystemTray extends SystemTray {
} }
private 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. // 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 // see: https://bugs.launchpad.net/glipper/+bug/1203888
@ -252,7 +295,7 @@ class GtkTypeSystemTray extends SystemTray {
addMenuEntry_(menuText, null, callback); addMenuEntry_(menuText, null, callback);
} }
else { 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); addMenuEntry_(menuText, null, callback);
} }
else { 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); addMenuEntry_(menuText, null, callback);
} }
else { 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); addMenuEntry_(menuText, null, callback);
} }
else { 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. // ALSO WHAT VERSION OF GTK to use? appindiactor1 -> GTk2, appindicator3 -> GTK3.
// appindicator3 doesn't support menu icons via GTK2!! // 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 we force GTK type system tray, don't attempt to load AppIndicator libs
if (LIBRARY_DEBUG) { if (LIBRARY_DEBUG) {
logger.error("Forcing GTK tray, not using appindicator"); logger.error("Forcing GTK tray, not using appindicator");

View File

@ -69,7 +69,7 @@ class Gtk {
String gtk3LibName = "libgtk-3.so.0"; 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. // 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; isLoaded = true;
} }

View File

@ -16,31 +16,22 @@
package dorkbox.systemTray.swing; 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.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
class SwingMenuEntry implements MenuEntry { import javax.swing.ImageIcon;
private static final String tempDirPath = ImageUtil.TEMP_DIR.getAbsolutePath(); 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 SwingSystemTrayMenuPopup parent;
private final SystemTray systemTray; private final SystemTray systemTray;
private final JMenuItem menuItem; private final JMenuItem menuItem;
@ -49,12 +40,8 @@ class SwingMenuEntry implements MenuEntry {
private volatile String text; private volatile String text;
private volatile SystemTrayMenuAction callback; private volatile SystemTrayMenuAction callback;
private int iconHeight = -1; // this is ALWAYS called on the EDT.
SwingMenuEntry(final SwingSystemTrayMenuPopup parentMenu, final String label, final File imagePath, final SystemTrayMenuAction callback,
SwingMenuEntry(final SwingSystemTrayMenuPopup parentMenu, final String label, final String imagePath, final SystemTrayMenuAction callback,
final SystemTray systemTray) { final SystemTray systemTray) {
this.parent = parentMenu; this.parent = parentMenu;
this.text = label; this.text = label;
@ -73,7 +60,7 @@ class SwingMenuEntry implements MenuEntry {
menuItem = new JMenuItem(label); menuItem = new JMenuItem(label);
menuItem.addActionListener(swingCallback); menuItem.addActionListener(swingCallback);
if (imagePath != null && !imagePath.isEmpty()) { if (imagePath != null) {
setImageIcon(imagePath); setImageIcon(imagePath);
} }
@ -109,7 +96,7 @@ class SwingMenuEntry implements MenuEntry {
} }
private private
void setImage_(final String imagePath) { void setImage_(final File imagePath) {
SwingUtil.invokeLater(new Runnable() { SwingUtil.invokeLater(new Runnable() {
@Override @Override
public public
@ -119,58 +106,11 @@ class SwingMenuEntry implements MenuEntry {
}); });
} }
// always called on the EDT
private private
void setImageIcon(final String imagePath) { void setImageIcon(final File imagePath) {
if (imagePath != null && !imagePath.isEmpty()) { if (imagePath != null) {
ImageIcon origIcon = new ImageIcon(imagePath.getAbsolutePath());
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();
}
}
}
}
menuItem.setIcon(origIcon); menuItem.setIcon(origIcon);
} }
else { else {
@ -185,7 +125,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null); setImage_(null);
} }
else { else {
setImage_(ImageUtil.iconPath(imagePath)); setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imagePath));
} }
} }
@ -196,7 +136,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null); setImage_(null);
} }
else { else {
setImage_(ImageUtil.iconPath(imageUrl)); setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, imageUrl));
} }
} }
@ -207,7 +147,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null); setImage_(null);
} }
else { else {
setImage_(ImageUtil.iconPath(cacheName, imageStream)); setImage_(ImageUtils.resizeAndCache(ImageUtils.SIZE, cacheName, imageStream));
} }
} }
@ -219,7 +159,7 @@ class SwingMenuEntry implements MenuEntry {
setImage_(null); setImage_(null);
} }
else { 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.TrayIcon;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JMenuItem; import javax.swing.JMenuItem;
import dorkbox.systemTray.ImageUtil;
import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.ScreenUtil; import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil; import dorkbox.util.SwingUtil;
@ -45,6 +46,7 @@ import dorkbox.util.SwingUtil;
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521 * 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 * https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028
*/ */
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"})
public public
class SwingSystemTray extends dorkbox.systemTray.SystemTray { class SwingSystemTray extends dorkbox.systemTray.SystemTray {
volatile SwingSystemTrayMenuPopup menu; volatile SwingSystemTrayMenuPopup menu;
@ -63,6 +65,9 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
public public
SwingSystemTray() { SwingSystemTray() {
super(); super();
ImageUtils.determineIconSize(dorkbox.systemTray.SystemTray.TYPE_SWING);
SwingUtil.invokeAndWait(new Runnable() { SwingUtil.invokeAndWait(new Runnable() {
@Override @Override
public public
@ -141,21 +146,23 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
@Override @Override
protected protected
void setIcon_(final String iconPath) { void setIcon_(final File iconFile) {
dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
SwingSystemTray tray = SwingSystemTray.this; 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) { synchronized (tray) {
if (!isActive) { if (!isActive) {
// here we init. everything // here we init. everything
isActive = true; isActive = true;
menu = new SwingSystemTrayMenuPopup(); menu = new SwingSystemTrayMenuPopup();
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
trayIcon = new TrayIcon(trayImage); trayIcon = new TrayIcon(trayImage);
// appindicators don't support this, so we cater to the lowest common denominator // 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); logger.error("TrayIcon could not be added.", e);
} }
} else { } else {
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
tray.trayIcon.setImage(trayImage); 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 * Will add a new menu entry, or update one if it already exists
*/ */
private 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) { if (menuText == null) {
throw new NullPointerException("Menu text cannot be null"); throw new NullPointerException("Menu text cannot be null");
} }
@ -255,7 +259,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
addMenuEntry_(menuText, null, callback); addMenuEntry_(menuText, null, callback);
} }
else { 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); addMenuEntry_(menuText, null, callback);
} }
else { 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); addMenuEntry_(menuText, null, callback);
} }
else { 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); addMenuEntry_(menuText, null, callback);
} }
else { 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