From ad066c6e421f7e71c532076e24ca4ae3f67d61d3 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 26 Sep 2016 02:06:27 +0200 Subject: [PATCH] Image scaling + caching + error-icon if problems, fix for windows 'auto' scaling --- src/dorkbox/systemTray/ImageUtil.java | 241 ------- src/dorkbox/systemTray/SystemTray.java | 175 +++-- .../systemTray/linux/AppIndicatorTray.java | 19 +- .../systemTray/linux/GtkMenuEntry.java | 25 +- .../systemTray/linux/GtkSystemTray.java | 80 +-- .../systemTray/linux/GtkTypeSystemTray.java | 55 +- .../systemTray/linux/jna/AppIndicator.java | 2 +- src/dorkbox/systemTray/linux/jna/Gtk.java | 2 +- .../systemTray/swing/SwingMenuEntry.java | 146 +---- .../systemTray/swing/SwingSystemTray.java | 30 +- src/dorkbox/systemTray/util/ImageUtils.java | 611 ++++++++++++++++++ .../util/WindowsSystemTraySwing.java | 203 ++++++ src/dorkbox/systemTray/util/error_32.png | Bin 0 -> 980 bytes 13 files changed, 1072 insertions(+), 517 deletions(-) delete mode 100644 src/dorkbox/systemTray/ImageUtil.java create mode 100644 src/dorkbox/systemTray/util/ImageUtils.java create mode 100644 src/dorkbox/systemTray/util/WindowsSystemTraySwing.java create mode 100644 src/dorkbox/systemTray/util/error_32.png diff --git a/src/dorkbox/systemTray/ImageUtil.java b/src/dorkbox/systemTray/ImageUtil.java deleted file mode 100644 index bb43c7e..0000000 --- a/src/dorkbox/systemTray/ImageUtil.java +++ /dev/null @@ -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 resourceToFilePath = new HashMap(); - 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); - } -} diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index 2cb61f7..9746e6e 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -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. + *

+ * 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). + *

+ * 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. + *

+ * 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) + *

+ * 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 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, 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; diff --git a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java index 3c0f98f..5cfbe74 100644 --- a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java @@ -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); } } } diff --git a/src/dorkbox/systemTray/linux/jna/AppIndicator.java b/src/dorkbox/systemTray/linux/jna/AppIndicator.java index 1eb9578..6fe9fdd 100644 --- a/src/dorkbox/systemTray/linux/jna/AppIndicator.java +++ b/src/dorkbox/systemTray/linux/jna/AppIndicator.java @@ -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"); diff --git a/src/dorkbox/systemTray/linux/jna/Gtk.java b/src/dorkbox/systemTray/linux/jna/Gtk.java index 177b5f6..9a62854 100644 --- a/src/dorkbox/systemTray/linux/jna/Gtk.java +++ b/src/dorkbox/systemTray/linux/jna/Gtk.java @@ -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; } diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntry.java b/src/dorkbox/systemTray/swing/SwingMenuEntry.java index 1d5305a..6b421f4 100644 --- a/src/dorkbox/systemTray/swing/SwingMenuEntry.java +++ b/src/dorkbox/systemTray/swing/SwingMenuEntry.java @@ -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; - } } diff --git a/src/dorkbox/systemTray/swing/SwingSystemTray.java b/src/dorkbox/systemTray/swing/SwingSystemTray.java index 6a1517b..4d87c68 100644 --- a/src/dorkbox/systemTray/swing/SwingSystemTray.java +++ b/src/dorkbox/systemTray/swing/SwingSystemTray.java @@ -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); } } } diff --git a/src/dorkbox/systemTray/util/ImageUtils.java b/src/dorkbox/systemTray/util/ImageUtils.java new file mode 100644 index 0000000..262932a --- /dev/null +++ b/src/dorkbox/systemTray/util/ImageUtils.java @@ -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 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."); + } +} diff --git a/src/dorkbox/systemTray/util/WindowsSystemTraySwing.java b/src/dorkbox/systemTray/util/WindowsSystemTraySwing.java new file mode 100644 index 0000000..4933f05 --- /dev/null +++ b/src/dorkbox/systemTray/util/WindowsSystemTraySwing.java @@ -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); + } + } +} diff --git a/src/dorkbox/systemTray/util/error_32.png b/src/dorkbox/systemTray/util/error_32.png new file mode 100644 index 0000000000000000000000000000000000000000..2bbc6fc6bbdffef8cc241570053dfc21e188e59a GIT binary patch literal 980 zcmV;_11tQAP)= zI@19)k(DErQkJq1U=!O`Oh~iffCj}p&$hrcfZ70yZ5_ak3=cj2G!Sr7ZVO_t6@Z)C zZwKz^p)S{dy#S;6weqXLU2~wN0&qJ zk(gcS5p){^1x4W(p3wG5#K3^>EVWzEc5;AdcEmtq#TkrHZY97E;482U_yW8Fwo%@5 z-uPIe(Ol9y{-Ks@G?25#HNZE3OTc*z4m3_L=8QXl&pr`)s@(q#pp*b_fM-=0_=H|< zlolnQah%Wi{_wBBJz%DZi9G`Q0|Q;2fO-Q2wI&Lu2sCN`MPQ)Q8(0|_@Lp{mJc?{< zRVFtJG#djmBE(-?k&;wqVxJP72rLK?bbIIh!Q#of;Jz&iQdxb553w^kXE=`fa6mKAl5qtE({B~{9s%G%%nx3AG1;p=osWT=z~u}8 zwUOh0ATUSu`vTo_F>D`^%j&b_Upw!B4=K)_D?cGFP%f=^fCdK(UHQHf2d%EWn-<_b za0Ymo0pKGrXkJ>mG2pWPNhE*e4|%}n@FCzZ|4)vnPghg`OMtf-XeB42N2;Ht1w8>6 z1+-~heyu=;fZb-nZvFT6FX&&ZuQv6a`}}nOP|r_X@_+8az-rF`0000