SystemTray/src/dorkbox/systemTray/SystemTray.java

1100 lines
49 KiB
Java

/*
* Copyright 2021 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 static dorkbox.systemTray.util.AutoDetectTrayType.fromClass;
import static dorkbox.systemTray.util.AutoDetectTrayType.isTrayType;
import static dorkbox.systemTray.util.AutoDetectTrayType.selectType;
import java.awt.Component;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.concurrent.atomic.AtomicReference;
import javax.imageio.stream.ImageInputStream;
import javax.swing.Icon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dorkbox.jna.linux.AppIndicator;
import dorkbox.jna.linux.Gtk;
import dorkbox.jna.linux.GtkCheck;
import dorkbox.jna.linux.GtkEventDispatch;
import dorkbox.jna.rendering.RenderProvider;
import dorkbox.os.OS;
import dorkbox.systemTray.ui.swing.SwingUIFactory;
import dorkbox.systemTray.util.AutoDetectTrayType;
import dorkbox.systemTray.util.EventDispatch;
import dorkbox.systemTray.util.ImageResizeUtil;
import dorkbox.systemTray.util.LinuxSwingUI;
import dorkbox.systemTray.util.SizeAndScaling;
import dorkbox.systemTray.util.SystemTrayFixesLinux;
import dorkbox.systemTray.util.SystemTrayFixesMacOS;
import dorkbox.systemTray.util.SystemTrayFixesWindows;
import dorkbox.systemTray.util.WindowsSwingUI;
import dorkbox.util.CacheUtil;
import dorkbox.util.SwingUtil;
/**
* Professional, cross-platform **SystemTray**, **AWT**, **GtkStatusIcon**, and **AppIndicator** support for Java applications.
* <p>
* This library provides **OS native** menus and **Swing** menus.
* <ul>
* <li> Swing menus are the default preferred type because they offer more features (images attached to menu entries, text styling, etc) and
* a consistent look & feel across all platforms.
* </li>
* <li> Native menus, should one want them, follow the specified look and feel of that OS, and thus are limited by what is supported on the
* OS and consequently not consistent across all platforms.
* </li>
* </ul>
*/
@SuppressWarnings({"unused", "Duplicates", "WeakerAccess"})
public final
class SystemTray {
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
public enum TrayType {
/** Will choose as a 'best guess' which tray type to use */
AutoDetect,
Gtk,
AppIndicator,
WindowsNative,
Swing,
Osx,
Awt;
public TrayType safeFromString(String trayName) {
try {
return valueOf(trayName);
} catch (Exception e) {
return AutoDetect;
}
}
}
/** Enables auto-detection for the system tray. This should be mostly successful. */
public static volatile boolean AUTO_SIZE = OS.INSTANCE.getBoolean(SystemTray.class.getCanonicalName() + ".AUTO_SIZE", true);
/** Forces the system tray to always choose GTK2 (even when GTK3 might be available). */
public static volatile boolean FORCE_GTK2 = OS.INSTANCE.getBoolean(SystemTray.class.getCanonicalName() + ".FORCE_GTK2", false);
/** Prefer to load GTK3 before trying to load GTK2. */
public static volatile boolean PREFER_GTK3 = OS.INSTANCE.getBoolean(SystemTray.class.getCanonicalName() + ".PREFER_GTK3", true);
/**
* Forces the system tray detection to be AutoDetect, GtkStatusIcon, AppIndicator, WindowsNotifyIcon, Swing, or AWT.
* <p>
* This is an advanced feature, and it is recommended to leave at AutoDetect.
*/
public static volatile TrayType FORCE_TRAY_TYPE = TrayType.AppIndicator.safeFromString(
OS.INSTANCE.getProperty(SystemTray.class.getCanonicalName() + ".FORCE_TRAY_TYPE", TrayType.AutoDetect.name()));
/**
* Allows the SystemTray logic to resolve OS inconsistencies for the SystemTray.
* <p>
* This is an advanced feature, and it is recommended to leave as true
*/
public static volatile boolean AUTO_FIX_INCONSISTENCIES = OS.INSTANCE.getBoolean(SystemTray.class.getCanonicalName() +
".AUTO_FIX_INCONSISTENCIES", true);
/**
* Allows the SystemTray logic to ignore if root is detected. Usually when running as root it won't work (because of how DBUS
* operates), but in rare situations, it might work.
* <p>
* This is an advanced feature, and it is recommended to leave as true
*/
public static volatile boolean ENABLE_ROOT_CHECK = OS.INSTANCE.getBoolean(SystemTray.class.getCanonicalName() + ".ENABLE_ROOT_CHECK", true);
/**
* This property is provided for debugging any errors in the logic used to determine the system-tray type.
*/
public static volatile boolean DEBUG = OS.INSTANCE.getBoolean(SystemTray.class.getCanonicalName() + ".DEBUG", false);
/**
* Allows a custom look and feel for the Swing UI, if defined. See the test example for specific use.
*/
public static volatile SwingUIFactory SWING_UI = null;
/**
* Gets the version number.
*/
public static
String getVersion() {
return "4.2";
}
static {
// Add this project to the updates system, which verifies this class + UUID + version information
dorkbox.updates.Updates.INSTANCE.add(SystemTray.class, "b35c107332d844559a3f877fcef42a21", getVersion());
}
/**
* Enables native menus on Windows/Linux/macOS instead of the swing menu. The drawback is that this menu is native, and sometimes
* native menus looks absolutely HORRID.
* <p>
* This always returns the same instance per JVM (it's a singleton), and on some platforms the system tray may not be
* supported, in which case this will return NULL.
* <p>
* If this is using the Swing SystemTray and a SecurityManager is installed, the AWTPermission {@code accessSystemTray} must
* be granted in order to get the {@code SystemTray} instance. Otherwise, this will return null.
* <p>
* If you create MORE than 1 system tray, you should use {{@link SystemTray#get(String)}} instead, and specify a unique name for
* each instance
*/
public static
SystemTray get() {
return get("SystemTray");
}
/**
* Enables native menus on Windows/Linux/macOS instead of the swing menu. The drawback is that this menu is native, and sometimes
* native menus looks absolutely HORRID.
* <p>
* This always returns the same instance per JVM (it's a singleton), and on some platforms the system tray may not be
* supported, in which case this will return NULL.
* <p>
* If this is using the Swing SystemTray and a SecurityManager is installed, the AWTPermission {@code accessSystemTray} must
* be granted in order to get the {@code SystemTray} instance. Otherwise, this will return null.
*
* @param trayName This is the name assigned to the system tray instance. If you create MORE than 1 system tray,
* you must make sure to use different names (or un-predicable things can happen!).
*/
@SuppressWarnings({"ConstantConditions", "StatementWithEmptyBody"})
public static synchronized
SystemTray get(String trayName) {
if (AUTO_FIX_INCONSISTENCIES) {
// we have to make sure to follow the system appearance (if possible)
System.setProperty("apple.awt.application.appearance", "system");
// Template images are the "black/white" enforced tray theme by macOS. This is left as a reference.
// System.setProperty("apple.awt.enableTemplateImages", "true");
}
// we must recreate the menu if we call get() after remove()!
// if (DEBUG) {
// Properties properties = System.getProperties();
// for (Map.Entry<Object, Object> entry : properties.entrySet()) {
// logger.debug(entry.getKey() + " : " + entry.getValue());
// }
// }
// no tray in a headless environment
if (GraphicsEnvironment.isHeadless()) {
logger.error("Cannot use the SystemTray in a headless environment");
return null;
}
// if we have a render provider, we must make sure that it is supported
if (!RenderProvider.isSupported()) {
// versions of SWT older than v4.4, are INCOMPATIBLE with us.
// Of note, v4.3 is the "last released" version of SWT by eclipse AND IT WILL NOT WORK!!
// for NEWER versions of SWT via maven, use http://maven-eclipse.github.io/maven
if (RenderProvider.isSwt()) {
logger.error("Unable to use currently loaded version of SWT, it is TOO OLD. Please use version 4.4+");
}
return null;
}
// if we already have a system tray by this name, return it (do not allow duplicate tray names)
SystemTray existingTray = AutoDetectTrayType.getInstance(trayName);
if (existingTray != null) {
if (DEBUG) {
logger.info("Returning existing tray: " + trayName);
}
return existingTray;
}
boolean isNix = OS.INSTANCE.isLinux() || OS.INSTANCE.isUnix();
boolean isWindows = OS.INSTANCE.isWindows();
boolean isMacOsX = OS.INSTANCE.isMacOsX();
// Windows can ONLY use Swing (non-native) or WindowsNotifyIcon (native) - AWT looks absolutely horrid and is not an option
// OSx can use Swing (non-native) or AWT (native).
// Linux can use Swing (non-native), AWT (native), GtkStatusIcon (native), or AppIndicator (native)
if (isWindows) {
if (FORCE_TRAY_TYPE != TrayType.AutoDetect && FORCE_TRAY_TYPE != TrayType.Swing && FORCE_TRAY_TYPE != TrayType.WindowsNative) {
logger.warn("Windows cannot use the '" + FORCE_TRAY_TYPE + "' SystemTray type on windows, auto-detecting implementation!");
// windows MUST use swing/windows-notify-icon only!
FORCE_TRAY_TYPE = TrayType.AutoDetect;
}
}
else if (isMacOsX) {
if (RenderProvider.isSwt() && FORCE_TRAY_TYPE == TrayType.Swing) {
// cannot mix Swing and SWT on MacOSX (for all versions of java) so we force ATW menus instead, which work just fine with SWT
// http://mail.openjdk.java.net/pipermail/bsd-port-dev/2008-December/000173.html
if (AUTO_FIX_INCONSISTENCIES) {
logger.warn("Unable to load Swing + SWT (for all versions of Java). Using the AWT Tray type instead.");
FORCE_TRAY_TYPE = TrayType.Awt;
}
else {
logger.error("Unable to load Swing + SWT (for all versions of Java). " +
"Please set `SystemTray.AUTO_FIX_INCONSISTENCIES=true;` to automatically fix this problem.\"");
return null;
}
}
if (FORCE_TRAY_TYPE != TrayType.AutoDetect && FORCE_TRAY_TYPE != TrayType.Swing && FORCE_TRAY_TYPE != TrayType.Awt) {
// MacOsX can only use swing and AWT
FORCE_TRAY_TYPE = TrayType.AutoDetect;
logger.warn("MacOS cannot use the '" + FORCE_TRAY_TYPE + "' SystemTray type, defaulting to the AWT Tray type instead.");
}
}
else if (isNix) {
// linux/unix can use all the tray types. AWT looks horrid. GTK versions are really sensitive...
// this checks to see if Swing/SWT/JavaFX has loaded GTK yet, and if so, what version they loaded.
// if swing is used, we have to do some extra checks...
int loadedGtkVersion = GtkCheck.getLoadedGtkVersion();
if (loadedGtkVersion == 2) {
if (AUTO_FIX_INCONSISTENCIES) {
if (!FORCE_GTK2) {
if (RenderProvider.isJavaFX()) {
// JavaFX Java7,8 is GTK2 only. Java9 can MAYBE have it be GTK3 if `-Djdk.gtk.version=3` is specified
// see
// http://mail.openjdk.java.net/pipermail/openjfx-dev/2016-May/019100.html
// https://docs.oracle.com/javafx/2/system_requirements_2-2-3/jfxpub-system_requirements_2-2-3.htm
// from the page: JavaFX 2.2.3 for Linux requires gtk2 2.18+.
if (OS.INSTANCE.getJavaVersion() < 11) {
// we must use GTK2, because JavaFX is GTK2 (java9 had javafx, but java11 DOES NOT)
FORCE_GTK2 = true;
if (DEBUG) {
logger.debug("Forcing GTK2 because JavaFX is GTK2");
}
} else {
// java 11+
// https://github.com/javafxports/openjdk-jfx/issues/217
// uses GTK3 by default now. so UNLESS we set the javafx parameter to force GTK2, we don't.
String gtkVer = System.getProperty("jdk.gtk.version", "0");
if (gtkVer.startsWith("2")) {
FORCE_GTK2 = true;
if (DEBUG) {
logger.debug("Forcing GTK2 because JavaFX System property `jdk.gtk.version` was set to 2 (so we force GTK2)");
}
}
}
}
else if (RenderProvider.isSwt() && RenderProvider.getGtkVersion() != 3) {
// 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"); // this doesn't have any affect on newer versions of SWT
// we must use GTK2, because SWT is GTK2
FORCE_GTK2 = true;
if (DEBUG) {
logger.debug("Forcing GTK2 because SWT is GTK2");
}
}
else {
// we are NOT using javaFX/SWT and our UI is GTK2, and we want GTK3
// JavaFX/SWT can be GTK3, but Swing is not GTK3.
// we must use GTK2 because Java is configured to use GTK2
FORCE_GTK2 = true;
if (DEBUG) {
logger.debug("Forcing GTK2 because Java has already loaded GTK2");
}
}
} else {
// we are already forcing GTK2, so no extra actions necessary
}
} else {
// !AUTO_FIX_INCONSISTENCIES
if (!FORCE_GTK2) {
// clearly the app developer did not want us to automatically fix anything, and have not correctly specified how
// to load GTK, so abort with an error message.
logger.error("Unable to use the SystemTray when there is a mismatch for GTK loaded preferences. Please correctly " +
"set `SystemTray.FORCE_GTK2=true` or set `SystemTray.AUTO_FIX_INCONSISTENCIES=true`. Aborting...");
return null;
}
}
}
else if (loadedGtkVersion == 3) {
if (AUTO_FIX_INCONSISTENCIES) {
if (RenderProvider.isJavaFX()) {
// JavaFX Java7,8 is GTK2 only. Java9 can MAYBE have it be GTK3 if `-Djdk.gtk.version=3` is specified
// see
// http://mail.openjdk.java.net/pipermail/openjfx-dev/2016-May/019100.html
// https://docs.oracle.com/javafx/2/system_requirements_2-2-3/jfxpub-system_requirements_2-2-3.htm
// from the page: JavaFX 2.2.3 for Linux requires gtk2 2.18+.
if (FORCE_GTK2) {
// if we are java9, then we can change it -- otherwise we cannot.
if (OS.INSTANCE.getJavaVersion() >= 9) {
FORCE_GTK2 = false;
logger.warn("Unable to use the SystemTray when JavaFX is configured to use GTK3 and the SystemTray is " +
"configured to use GTK2. Please configure JavaFX to use GTK2 (via `System.setProperty(\"jdk.gtk.version\", \"3\");`) " +
"before JavaFX is initialized, or set `SystemTray.FORCE_GTK2=false;` Undoing `FORCE_GTK2`.");
}
}
if (!PREFER_GTK3) {
// we should use GTK3, since that is what is already loaded
PREFER_GTK3 = true;
if (DEBUG) {
logger.debug("Preferring GTK3 even though specified otherwise, because JavaFX is GTK3");
}
}
}
else if (RenderProvider.isSwt()) {
if (FORCE_GTK2) {
FORCE_GTK2 = false;
logger.warn("Unable to use the SystemTray when SWT is configured to use GTK3 and the SystemTray is configured to use " +
"GTK2. Please set `SystemTray.FORCE_GTK2=false;`");
}
if (!PREFER_GTK3) {
// we should use GTK3, since that is what is already loaded
PREFER_GTK3 = true;
if (DEBUG) {
logger.debug("Preferring GTK3 even though specified otherwise, because SWT is GTK3");
}
}
}
else {
// we are NOT using javaFX/SWT and our UI is GTK3, and we want GTK3
// JavaFX/SWT can be GTK3, but Swing is (maybe in the future?) GTK3.
if (FORCE_GTK2) {
FORCE_GTK2 = false;
logger.warn("Unable to use the SystemTray when Swing is configured to use GTK3 and the SystemTray is " +
"configured to use GTK2. Undoing `FORCE_GTK2.");
}
if (!PREFER_GTK3) {
// we should use GTK3, since that is what is already loaded
PREFER_GTK3 = true;
if (DEBUG) {
logger.debug("Preferring GTK3 even though specified otherwise, because Java has already loaded GTK3");
}
}
}
} else {
// !AUTO_FIX_INCONSISTENCIES
if (RenderProvider.isJavaFX()) {
// JavaFX Java7,8 is GTK2 only. Java9 can MAYBE have it be GTK3 if `-Djdk.gtk.version=3` is specified
// see
// http://mail.openjdk.java.net/pipermail/openjfx-dev/2016-May/019100.html
// https://docs.oracle.com/javafx/2/system_requirements_2-2-3/jfxpub-system_requirements_2-2-3.htm
// from the page: JavaFX 2.2.3 for Linux requires gtk2 2.18+.
if (FORCE_GTK2) {
// if we are java9, then we can change it -- otherwise we cannot.
if (OS.INSTANCE.getJavaVersion() >= 9) {
logger.error("Unable to use the SystemTray when JavaFX is configured to use GTK3 and the SystemTray is " +
"configured to use GTK2. Please configure JavaFX to use GTK2 (via `System.setProperty(\"jdk.gtk.version\", \"3\");`) " +
"before JavaFX is initialized, or set `SystemTray.FORCE_GTK2=false;` Aborting.");
}
else {
logger.error("Unable to use the SystemTray when JavaFX is configured to use GTK3 and the SystemTray is configured to use " +
"GTK2. Please set `SystemTray.FORCE_GTK2=false;` Aborting.");
}
return null;
}
}
else if (RenderProvider.isSwt()) {
// 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
if (FORCE_GTK2) {
logger.error("Unable to use the SystemTray when SWT is configured to use GTK3 and the SystemTray is configured to use " +
"GTK2. Please set `SystemTray.FORCE_GTK2=false;`");
return null;
}
}
else if (FORCE_GTK2) {
logger.error("Unable to use the SystemTray when Swing is configured to use GTK3 and the SystemTray is " +
"configured to use GTK2. Aborting.");
return null;
}
}
}
else {
// we don't know what was loaded.
// this is only a big deal for us if we are DIFFERENT than what SWING is using. Since swing isn't always used
// (ie: headless/javaFX can also be used), we ** DO NOT ** want to accidentally load swing if we don't have to
if (RenderProvider.isDefault()) {
// we have to make sure that SWING/GTK stuff is GTK2!
// THIS IS NOT DOCUMENTED ANYWHERE...
// NOTE: Refer to bug 4912613 for details regarding support for GTK 2.0/2.2
// only do this is values have not already been set!
String previousValue = System.getProperty("swing.gtk.version", "0");
if (previousValue != null && previousValue.startsWith("3")) {
if (FORCE_GTK2) {
// whoops! there is something setting GTK to version 3! abort with an error message.
logger.error("Unable to use the SystemTray when there is a mismatch for GTK loaded preferences. Please correctly " +
"set `SystemTray.FORCE_GTK2=true` and System property `swing.gtk.version=\"2.2\". Aborting...");
return null;
}
}
// now check another setting
previousValue = System.getProperty("jdk.gtk.version", "0");
if (previousValue != null && previousValue.startsWith("3")) {
if (FORCE_GTK2) {
// whoops! there is something setting GTK to version 3! abort with an error message.
logger.error("Unable to use the SystemTray when there is a mismatch for GTK loaded preferences. Please correctly " +
"set `SystemTray.FORCE_GTK2=true` and System property `jdk.gtk.version=\"2.2\". Aborting...");
return null;
}
}
if ("0".equals(previousValue) && AUTO_FIX_INCONSISTENCIES) {
// this means nothing was set!
if (PREFER_GTK3) {
System.setProperty("swing.gtk.version", "3");
System.setProperty("jdk.gtk.version", "3");
} else {
System.setProperty("swing.gtk.version", "2");
System.setProperty("jdk.gtk.version", "2");
}
}
}
}
}
if (DEBUG) {
logger.debug("Version {}", getVersion());
logger.debug("OS: {}", System.getProperty("os.name"));
logger.debug("Arch: {}", System.getProperty("os.arch"));
String jvmName = System.getProperty("java.vm.name", "");
String jvmVersion = System.getProperty("java.version", "");
String jvmVendor = System.getProperty("java.vm.specification.vendor", "");
logger.debug("{} {} {}", jvmVendor, jvmName, jvmVersion);
logger.debug("JPMS enabled: {}", OS.INSTANCE.getUsesJpms());
logger.debug("Is Auto sizing tray/menu? {}", AUTO_SIZE);
logger.debug("Is JavaFX detected? {}", RenderProvider.isJavaFX());
logger.debug("Is SWT detected? {}", RenderProvider.isSwt());
logger.debug("Java Swing L&F: {}", UIManager.getLookAndFeel().getID());
if (FORCE_TRAY_TYPE == TrayType.AutoDetect) {
logger.debug("Auto-detecting tray type");
}
else {
logger.debug("Forced tray type: {}", FORCE_TRAY_TYPE.name());
}
if (OS.INSTANCE.isLinux()) {
logger.debug("Force GTK2: {}", FORCE_GTK2);
logger.debug("Prefer GTK3: {}", PREFER_GTK3);
}
}
// 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. There is no mouse-over event.
// this has to happen BEFORE any sort of swing system tray stuff is accessed
Class<? extends Tray> trayType;
if (SystemTray.FORCE_TRAY_TYPE == TrayType.AutoDetect) {
trayType = AutoDetectTrayType.get(trayName);
} else {
trayType = selectType(SystemTray.FORCE_TRAY_TYPE);
}
if (trayType == null) {
if (OS.DesktopEnv.INSTANCE.isChromeOS()) {
logger.error("ChromeOS detected and it is not supported. Aborting.");
}
return null;
}
// fix various incompatibilities with selected tray types
if (isNix) {
// Ubuntu UNITY has issues with GtkStatusIcon (it won't work at all...)
if (isTrayType(trayType, TrayType.Gtk)) {
OS.DesktopEnv.Env de = OS.DesktopEnv.INSTANCE.getEnv();
if (OS.Linux.INSTANCE.isUbuntu() && OS.DesktopEnv.INSTANCE.isUnity(de)) {
if (AUTO_FIX_INCONSISTENCIES) {
// GTK2 does not support AppIndicators!
if (Gtk.isGtk2) {
trayType = selectType(TrayType.Swing);
logger.warn("Forcing Swing Tray type because Ubuntu Unity display environment removed support for GtkStatusIcons " +
"and GTK2+ was specified.");
}
else {
// we must use AppIndicator because Ubuntu Unity removed GtkStatusIcon support
SystemTray.FORCE_TRAY_TYPE = TrayType.AppIndicator; // this is required because of checks inside AppIndicator...
trayType = selectType(TrayType.AppIndicator);
logger.warn("Forcing AppIndicator because Ubuntu Unity display environment removed support for GtkStatusIcons.");
}
}
else {
logger.error("Unable to use the GtkStatusIcons when running on Ubuntu with the Unity display environment, and thus" +
" the SystemTray will not work. " +
"Please set `SystemTray.AUTO_FIX_INCONSISTENCIES=true;` to automatically fix this problem.");
return null;
}
}
if (de == OS.DesktopEnv.Env.Gnome) {
boolean hasWeirdOsProblems = OS.Linux.INSTANCE.isKali() || (OS.Linux.INSTANCE.isFedora());
if (hasWeirdOsProblems) {
// Fedora and Kali linux has some WEIRD graphical oddities via GTK3. GTK2 looks just fine.
PREFER_GTK3 = false;
if (DEBUG) {
logger.debug("Preferring GTK2 because this OS has weird graphical issues with GTK3 status icons");
}
}
}
}
if (isTrayType(trayType, TrayType.AppIndicator)) {
if (SystemTray.ENABLE_ROOT_CHECK && OS.Linux.INSTANCE.isRoot()) {
// if are we running as ROOT, there can be issues (definitely on Ubuntu 16.04, maybe others)!
if (AUTO_FIX_INCONSISTENCIES) {
trayType = selectType(TrayType.Swing);
logger.warn("Attempting to load the SystemTray as the 'root/sudo' user. This will likely not work because of dbus " +
"restrictions. Using the Swing Tray type instead. Please refer to the readme notes or issue #63 on " +
"how to work around this.");
} else {
logger.error("Attempting to load the SystemTray as the 'root/sudo' user. This will likely NOT WORK because of dbus " +
"restrictions. Please refer to the readme notes or issue #63 on how to work around this.");
}
}
if (OS.Linux.INSTANCE.isElementaryOS() && OS.Linux.INSTANCE.getElementaryOSVersion()[0] >= 5) {
// in version 5.0+, they REMOVED support for appindicators. You can add it back via some extra work.
// see: https://git.dorkbox.com/dorkbox/elementary-indicators
// or you can download
// https://launchpad.net/~elementary-os/+archive/ubuntu/stable/+files/wingpanel-indicator-ayatana_2.0.3+r27+pkg17~ubuntu0.4.1.1_amd64.deb
// then dpkg -i filename
// check if this library is installed.
if (!new File("/usr/share/doc/wingpanel-indicator-ayatana").isDirectory()) {
logger.error("Unable to use the SystemTray as-is with this version of ElementaryOS. By default, tray icons *are not* supported, but a" +
" workaround has been developed. Please see: https://git.dorkbox.com/dorkbox/elementary-indicators");
return null;
}
}
}
}
if (trayType == null) {
// unsupported tray, or unknown type
trayType = selectType(TrayType.Swing);
logger.error("SystemTray initialization failed. (Unable to discover which implementation to use). Falling back to the Swing Tray.");
}
try {
// at this point, the tray type is what it should be. If there are failures or special cases, all types will fall back to Swing.
if (isNix) {
// linux/unix need access to GTK, so load it up before the tray is loaded!
// Swing gets the image size info VIA gtk, so this is important as well.
GtkEventDispatch.startGui(FORCE_GTK2, PREFER_GTK3, DEBUG);
GtkEventDispatch.waitForEventsToComplete();
if (DEBUG) {
// output what version of GTK we have loaded.
logger.debug("GTK Version: " + Gtk.MAJOR + "." + Gtk.MINOR + "." + Gtk.MICRO);
logger.debug("Is the system already running GTK? {}", Gtk.alreadyRunningGTK);
}
if (!Gtk.isLoaded) {
trayType = selectType(TrayType.Swing);
logger.error("Unable to initialize GTK! Something is severely wrong! Using the Swing Tray type instead.");
}
// this will to load the app-indicator library
else if (isTrayType(trayType, TrayType.AppIndicator)) {
if (!AppIndicator.isLoaded) {
// YIKES. AppIndicator couldn't load.
String installer = AppIndicator.getInstallString(GtkCheck.isGtk2);
String packageInstall = OS.Linux.PackageManager.INSTANCE.getType().getInstallString() + " " + installer;
String installString = "Please install " + installer + ", for example: '" + packageInstall + "'.";
// can we fallback to swing? KDE does not work for this...
if (AUTO_FIX_INCONSISTENCIES && java.awt.SystemTray.isSupported() && !OS.DesktopEnv.INSTANCE.isKDE()) {
trayType = selectType(TrayType.Swing);
logger.warn("Unable to initialize the AppIndicator correctly. Using the Swing Tray type instead.");
logger.warn(installString);
}
else {
// no swing, have to emit instructions how to fix the error.
logger.error("AppIndicator unable to load. " + installString);
return null;
}
}
}
}
// have to make adjustments BEFORE the tray/menu image size calculations
if (AUTO_FIX_INCONSISTENCIES && SystemTray.SWING_UI == null) {
if (isNix && isTrayType(trayType, TrayType.Swing)) {
SystemTray.SWING_UI = new LinuxSwingUI();
}
else if (isWindows && (isTrayType(trayType, TrayType.Swing) || isTrayType(trayType, TrayType.WindowsNative))) {
SystemTray.SWING_UI = new WindowsSwingUI();
}
}
// initialize tray/menu image sizes. This must be BEFORE the system tray has been created
int trayImageSize = SizeAndScaling.getTrayImageSize();
int menuImageSize = SizeAndScaling.getMenuImageSize(trayType);
if (DEBUG) {
logger.debug("Tray indicator image size: {}", trayImageSize);
logger.debug("Tray menu image size: {}", menuImageSize);
}
if (!RenderProvider.isDefault() && SwingUtilities.isEventDispatchThread()) {
// This WILL NOT WORK. Let the dev know
logger.error("SystemTray initialization for JavaFX or SWT **CAN NOT** occur on the Swing Event Dispatch Thread " +
"(EDT). Something is seriously wrong.");
return null;
}
if (isTrayType(trayType, TrayType.Swing) ||
isTrayType(trayType, TrayType.Awt) ||
isTrayType(trayType, TrayType.Osx) ||
isTrayType(trayType, TrayType.WindowsNative)) {
// ensure AWT toolkit is initialized.
// OSX is based off of AWT now, insteadof creating our UI + event dispatch
java.awt.Toolkit.getDefaultToolkit();
}
if (AUTO_FIX_INCONSISTENCIES) {
// this logic has to be before we create the system Tray, but after AWT/GTK is started (if applicable)
if (isWindows && isTrayType(trayType, TrayType.Swing)) {
// we don't permit AWT for windows (it looks absolutely HORRID)
// Our default for windows is now a native tray icon (instead of the swing tray icon), but we preserve the use of Swing
// windows hard-codes the image size for AWT/SWING tray types
SystemTrayFixesWindows.fix(trayImageSize);
}
else if (isMacOsX && (isTrayType(trayType, TrayType.Awt) ||
isTrayType(trayType, TrayType.Osx))) {
// Swing on macOS is pretty bland. AWT (with fixes) looks fantastic (and is native)
// AWT on macosx doesn't respond to all buttons (but should)
SystemTrayFixesMacOS.fix();
}
else if (isNix && isTrayType(trayType, TrayType.Swing)) {
// linux/mac doesn't have transparent backgrounds for swing and hard-codes the image size
SystemTrayFixesLinux.fix(trayImageSize);
}
}
// initialize the tray icon height
// this is during init, so we can statically access this everywhere else. Multiple instances of this will always have the same value
SizeAndScaling.getMenuImageSize(trayType);
// Permits us to take action when the menu is "removed" from the system tray, so we can correctly add it back later.
Runnable onRemoveEvent = ()->{
// must remove ourselves from the init() map (since we want to be able to access things)
AutoDetectTrayType.removeSystemTrayHook(trayName);
// this is thread-safe
if (!AutoDetectTrayType.hasOtherTrays()) {
EventDispatch.shutdown();
}
};
// the cache name **MUST** be combined with the currently logged-in user, otherwise permissions get screwed up
// when there is more than 1 user logged in at the same time!
CacheUtil cache = new CacheUtil(trayName + "Cache" + "_" + System.getProperty("user.name"));
ImageResizeUtil imageResizeUtil = new ImageResizeUtil(cache);
// the "menu" in this case is the ACTUAL menu that shows up in the system tray (the icon + submenu, etc)
final AtomicReference<Tray> reference = new AtomicReference<>();
// javaFX and SWT **CAN NOT** start on the EDT!!
// linux + GTK/AppIndicator + windows-native menus must not start on the EDT!
// AWT + Swing + AWT-macOS must be constructed on the EDT however...
if (RenderProvider.isDefault() &&
(isTrayType(trayType, TrayType.Swing) || isTrayType(trayType, TrayType.Awt) || isTrayType(trayType, TrayType.Osx))) {
// have to construct swing stuff inside the swing EDT
final Class<? extends Menu> finalTrayType = trayType;
SwingUtil.invokeAndWait(()->{
try {
reference.set((Tray) finalTrayType.getConstructors()[0].newInstance(trayName, imageResizeUtil, onRemoveEvent));
} catch (Exception e) {
logger.error("Unable to create tray type: '{}'", finalTrayType.getSimpleName(), e);
}
});
}
else {
reference.set((Tray) trayType.getConstructors()[0].newInstance(trayName, imageResizeUtil, onRemoveEvent));
}
// we have a weird circle dependency thing going on!
Tray systemTrayMenu = reference.get();
if (systemTrayMenu == null) {
logger.error("Unable to create tray type: '{}'", trayType.getSimpleName());
return null;
}
if (DEBUG) {
logger.info("Successfully loaded type: {}", trayType.getSimpleName());
} else {
logger.info("Successfully loaded");
}
SystemTray systemTray = new SystemTray(systemTrayMenu, imageResizeUtil);
AutoDetectTrayType.setInstance(trayName, systemTray);
// we ALWAYS want to add a **JVM** shutdown hook!
Runnable shutdownRunnable = AutoDetectTrayType.getShutdownHook(trayName);
Runtime.getRuntime().addShutdownHook(new Thread(shutdownRunnable));
return systemTray;
} catch (Exception e) {
logger.error("Unable to create tray type: '{}'", trayType.getSimpleName(), e);
}
return null;
}
/** Default name of the application, sometimes shows on tray-icon mouse over. Not used for all OSes, but mostly for Linux */
private final Tray menu;
private final ImageResizeUtil imageResizeUtil;
private
SystemTray(final Tray systemTrayMenu, final ImageResizeUtil imageResizeUtil) {
this.menu = systemTrayMenu;
this.imageResizeUtil = imageResizeUtil;
}
/**
* Shuts-down the SystemTray, by removing the menus + tray icon. After calling this method, you MUST call `get()` or `get(name)`
* again to obtain a new SystemTray instance.
*/
public
void shutdown() {
// this will shut down and do what it needs to. The onRemoveEvent cleans up.
menu.remove();
}
/**
* Shuts-down the SystemTray, by removing the menus + tray icon. After calling this method, you MUST call `get()` or `get(name)`
* again to obtain a new SystemTray instance.
*
* @param runOnShutdown the action to run after the system tray has finished shutting down
*/
public
void shutdown(Runnable runOnShutdown) {
shutdown();
// wait for the system tray Event dispatch to finish running, then run our shutdown logic. This must happen on a different thread
EventDispatch.runLater(()->{
Thread thread = new Thread(()->{
EventDispatch.waitForShutdown();
runOnShutdown.run();
});
thread.setName("SystemTrayShutdown");
thread.setDaemon(true);
thread.start();
});
}
/**
* Gets the 'status' string assigned to the system tray
*/
public
String getStatus() {
return menu.getStatus();
}
/**
* Sets a 'status' string at the first position in the popup menu. This 'status' string appears as a disabled menu entry.
*
* @param statusText the text you want displayed, null if you want to remove the 'status' string
*/
public
Menu setStatus(String statusText) {
menu.setStatus(statusText);
return menu;
}
/**
* @return the attached menu to this system tray
*/
public
Menu getMenu() {
return menu;
}
/**
* Converts the specified JMenu into a compatible SystemTray menu, using the JMenu icon as the image for the SystemTray. The currently
* supported menu items are `JMenu`, `JCheckBoxMenuItem`, `JMenuItem`, and `JSeparator`. Because this is a conversion, the JMenu
* is no longer valid after this action.
*
* @return the attached menu to this system tray based on the specified JMenu
*/
public
Menu setMenu(final JMenu jMenu) {
Icon icon = jMenu.getIcon();
if (icon != null) {
BufferedImage bimage = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
setImage(bimage);
}
Component[] menuComponents = jMenu.getMenuComponents();
for (Component c : menuComponents) {
if (c instanceof JMenu) {
menu.add((JMenu) c);
}
else if (c instanceof JCheckBoxMenuItem) {
menu.add((JCheckBoxMenuItem) c);
}
else if (c instanceof JMenuItem) {
menu.add((JMenuItem) c);
}
else if (c instanceof JSeparator) {
menu.add((JSeparator) c);
}
}
return menu;
}
/**
* Shows (if hidden), or hides (if showing) the system tray.
*/
public
Menu setEnabled(final boolean enabled) {
menu.setEnabled(enabled);
return menu;
}
/**
* Specifies the tooltip text, usually this is used to brand the SystemTray icon with your product's name.
* <p>
* The maximum length is 64 characters long, and it is not supported on all Operating Systems and Desktop
* Environments.
* <p>
* For more details on Linux see https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12.
*
* @param tooltipText the text to use as tooltip for the tray icon, null to remove
*/
public
Menu setTooltip(final String tooltipText) {
menu.setTooltip(tooltipText);
return menu;
}
/**
* Specifies the new image to set for a menu entry, NULL to delete the image
* <p>
* This method will cache the image if it needs to be resized to fit.
*
* @param imageFile the file of the image to use or null
*/
public
void setImage(final File imageFile) {
if (imageFile == null) {
throw new NullPointerException("imageFile");
}
menu.setImageFromTray(imageResizeUtil.shouldResizeOrCache(true, imageFile));
}
/**
* Specifies the new image to set for the tray icon.
* <p>
* If AUTO_SIZE, then this method resize the image (best guess), otherwise the image "as-is" will be used
*
* @param imagePath the full path of the image to use or null
*/
public
Menu setImage(final String imagePath) {
if (imagePath == null) {
throw new NullPointerException("imagePath");
}
menu.setImageFromTray(imageResizeUtil.shouldResizeOrCache(true, imagePath));
return menu;
}
/**
* Specifies the new image to set for the tray icon.
* <p>
* If AUTO_SIZE, then this method resize the image (best guess), otherwise the image "as-is" will be used
*
* @param imageUrl the URL of the image to use or null
*/
public
Menu setImage(final URL imageUrl) {
if (imageUrl == null) {
throw new NullPointerException("imageUrl");
}
menu.setImageFromTray(imageResizeUtil.shouldResizeOrCache(true, imageUrl));
return menu;
}
/**
* Specifies the new image to set for the tray icon.
* <p>
* If AUTO_SIZE, then this method resize the image (best guess), otherwise the image "as-is" will be used
*
* @param imageStream the InputStream of the image to use
*/
public
Menu setImage(final InputStream imageStream) {
if (imageStream == null) {
throw new NullPointerException("imageStream");
}
menu.setImageFromTray(imageResizeUtil.shouldResizeOrCache(true, imageStream));
return menu;
}
/**
* Specifies the new image to set for the tray icon.
* <p>
* If AUTO_SIZE, then this method resize the image (best guess), otherwise the image "as-is" will be used
*
* @param image the image of the image to use
*/
public
Menu setImage(final Image image) {
if (image == null) {
throw new NullPointerException("image");
}
menu.setImageFromTray(imageResizeUtil.shouldResizeOrCache(true, image));
return menu;
}
/**
* Specifies the new image to set for the tray icon.
* <p>
* If AUTO_SIZE, then this method resize the image (best guess), otherwise the image "as-is" will be used
*
*@param imageStream the ImageInputStream of the image to use
*/
public
Menu setImage(final ImageInputStream imageStream) {
if (imageStream == null) {
throw new NullPointerException("image");
}
menu.setImageFromTray(imageResizeUtil.shouldResizeOrCache(true, imageStream));
return menu;
}
/**
* @return the system tray image size, accounting for OS and theme differences
*/
public
int getTrayImageSize() {
return SizeAndScaling.getTrayImageSize();
}
/**
* This is called during tray initialization. Since multiple tray menus will always be the same type, this is can be cached
*
* @return the system tray menu image size, accounting for OS and theme differences.
*/
public
int getMenuImageSize() {
return SizeAndScaling.TRAY_MENU_SIZE;
}
/**
* @return the tray type used to create the system tray
*/
public
TrayType getType() {
return fromClass(menu.getClass());
}
/**
* This removes all menu entries from the tray icon menu AND removes the tray icon from the system tray!
* <p>
* You will need to recreate ALL parts of the menu to see the tray icon + menu again!
*/
public
void remove() {
// we must recreate the menu via init() if we call get() after remove()! (onRemoveEvent does this)
menu.remove();
}
}