WIP: native + swing menus
This commit is contained in:
parent
7e3fba9157
commit
7a2909abca
@ -168,6 +168,7 @@ interface Menu extends Entry {
|
|||||||
*/
|
*/
|
||||||
Menu addMenu(String menuText, InputStream imageStream);
|
Menu addMenu(String menuText, InputStream imageStream);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a swing widget as a menu entry.
|
* Adds a swing widget as a menu entry.
|
||||||
*
|
*
|
||||||
|
@ -32,12 +32,18 @@ import org.slf4j.LoggerFactory;
|
|||||||
import dorkbox.systemTray.linux.GnomeShellExtension;
|
import dorkbox.systemTray.linux.GnomeShellExtension;
|
||||||
import dorkbox.systemTray.linux.jna.AppIndicator;
|
import dorkbox.systemTray.linux.jna.AppIndicator;
|
||||||
import dorkbox.systemTray.linux.jna.Gtk;
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
import dorkbox.systemTray.swing._AppIndicatorTray;
|
import dorkbox.systemTray.nativeUI.NativeUI;
|
||||||
import dorkbox.systemTray.swing._GtkStatusIconTray;
|
import dorkbox.systemTray.nativeUI._AppIndicatorNativeTray;
|
||||||
import dorkbox.systemTray.swing._SwingTray;
|
import dorkbox.systemTray.nativeUI._AwtTray;
|
||||||
|
import dorkbox.systemTray.nativeUI._GtkStatusIconNativeTray;
|
||||||
|
import dorkbox.systemTray.swingUI.SwingUI;
|
||||||
|
import dorkbox.systemTray.swingUI._AppIndicatorTray;
|
||||||
|
import dorkbox.systemTray.swingUI._GtkStatusIconTray;
|
||||||
|
import dorkbox.systemTray.swingUI._SwingTray;
|
||||||
|
import dorkbox.systemTray.util.ImageUtils;
|
||||||
import dorkbox.systemTray.util.JavaFX;
|
import dorkbox.systemTray.util.JavaFX;
|
||||||
import dorkbox.systemTray.util.Swt;
|
import dorkbox.systemTray.util.Swt;
|
||||||
import dorkbox.systemTray.util.WindowsSystemTraySwing;
|
import dorkbox.systemTray.util.SystemTrayFixes;
|
||||||
import dorkbox.util.CacheUtil;
|
import dorkbox.util.CacheUtil;
|
||||||
import dorkbox.util.IO;
|
import dorkbox.util.IO;
|
||||||
import dorkbox.util.OS;
|
import dorkbox.util.OS;
|
||||||
@ -54,10 +60,12 @@ public
|
|||||||
class SystemTray implements Menu {
|
class SystemTray implements Menu {
|
||||||
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
|
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
|
||||||
|
|
||||||
public static final int TYPE_AUTO_DETECT = 0;
|
public enum TrayType {
|
||||||
public static final int TYPE_GTK_STATUSICON = 1;
|
AutoDetect,
|
||||||
public static final int TYPE_APP_INDICATOR = 2;
|
GtkStatusIcon,
|
||||||
public static final int TYPE_SWING = 3;
|
AppIndicator,
|
||||||
|
Swing
|
||||||
|
}
|
||||||
|
|
||||||
@Property
|
@Property
|
||||||
/** Enables auto-detection for the system tray. This should be mostly successful.
|
/** Enables auto-detection for the system tray. This should be mostly successful.
|
||||||
@ -97,11 +105,11 @@ class SystemTray implements Menu {
|
|||||||
|
|
||||||
@Property
|
@Property
|
||||||
/**
|
/**
|
||||||
* Forces the system tray detection to be Automatic (0), GtkStatusIcon (1), AppIndicator (2), or Swing (3).
|
* Forces the system tray detection to be AutoDetect, GtkStatusIcon, AppIndicator, or Swing.
|
||||||
* <p>
|
* <p>
|
||||||
* This is an advanced feature, and it is recommended to leave at 0.
|
* This is an advanced feature, and it is recommended to leave at AutoDetect.
|
||||||
*/
|
*/
|
||||||
public static int FORCE_TRAY_TYPE = 0;
|
public static TrayType FORCE_TRAY_TYPE = TrayType.Swing;
|
||||||
|
|
||||||
@Property
|
@Property
|
||||||
/**
|
/**
|
||||||
@ -124,6 +132,8 @@ class SystemTray implements Menu {
|
|||||||
|
|
||||||
public final static boolean isJavaFxLoaded;
|
public final static boolean isJavaFxLoaded;
|
||||||
public final static boolean isSwtLoaded;
|
public final static boolean isSwtLoaded;
|
||||||
|
private static boolean forceNativeMenus = false;
|
||||||
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
boolean isJavaFxLoaded_ = false;
|
boolean isJavaFxLoaded_ = false;
|
||||||
@ -151,7 +161,49 @@ class SystemTray implements Menu {
|
|||||||
isSwtLoaded = isSwtLoaded_;
|
isSwtLoaded = isSwtLoaded_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static
|
||||||
|
Class<? extends Menu> selectType(final TrayType trayType) throws Exception {
|
||||||
|
if (trayType == TrayType.GtkStatusIcon) {
|
||||||
|
if (forceNativeMenus) {
|
||||||
|
return _GtkStatusIconNativeTray.class;
|
||||||
|
} else {
|
||||||
|
return _GtkStatusIconTray.class;
|
||||||
|
}
|
||||||
|
} else if (trayType == TrayType.AppIndicator) {
|
||||||
|
if (forceNativeMenus) {
|
||||||
|
return _AppIndicatorNativeTray.class;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _AppIndicatorTray.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (trayType == TrayType.Swing) {
|
||||||
|
if (forceNativeMenus && !OS.isWindows()) {
|
||||||
|
// AWT on windows looks like crap
|
||||||
|
return _AwtTray.class;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _SwingTray.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static
|
||||||
|
Class<? extends Menu> selectTypeQuietly(final TrayType trayType) {
|
||||||
|
try {
|
||||||
|
return selectType(trayType);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (DEBUG) {
|
||||||
|
logger.error("Cannot initialize {}", trayType.name(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
private static void init() {
|
private static void init() {
|
||||||
if (systemTray != null) {
|
if (systemTray != null) {
|
||||||
return;
|
return;
|
||||||
@ -173,15 +225,15 @@ class SystemTray implements Menu {
|
|||||||
} else {
|
} else {
|
||||||
// windows and mac ONLY support the Swing SystemTray.
|
// windows and mac ONLY support the Swing SystemTray.
|
||||||
// Linux CAN support Swing SystemTray, but it looks like crap (so we wrote our own GtkStatusIcon/AppIndicator)
|
// Linux CAN support Swing SystemTray, but it looks like crap (so we wrote our own GtkStatusIcon/AppIndicator)
|
||||||
if (OS.isWindows() && FORCE_TRAY_TYPE != TYPE_SWING) {
|
if (OS.isWindows() && FORCE_TRAY_TYPE != TrayType.Swing) {
|
||||||
throw new RuntimeException("Windows is incompatible with the specified option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
|
throw new RuntimeException("Windows is incompatible with the specified option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
|
||||||
} else if (OS.isMacOsX() && FORCE_TRAY_TYPE != TYPE_SWING) {
|
} else if (OS.isMacOsX() && FORCE_TRAY_TYPE != TrayType.Swing) {
|
||||||
throw new RuntimeException("MacOSx is incompatible with the specified option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
|
throw new RuntimeException("MacOSx is incompatible with the specified option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// kablooie if SWT is not configured in a way that works with us.
|
// kablooie if SWT is not configured in a way that works with us.
|
||||||
if (FORCE_TRAY_TYPE != TYPE_SWING && OS.isLinux()) {
|
if (FORCE_TRAY_TYPE != TrayType.Swing && OS.isLinux()) {
|
||||||
if (isSwtLoaded) {
|
if (isSwtLoaded) {
|
||||||
// Necessary for us to work with SWT based on version info. We can try to set us to be compatible with whatever it is set to
|
// Necessary for us to work with SWT based on version info. We can try to set us to be compatible with whatever it is set to
|
||||||
// System.setProperty("SWT_GTK3", "0");
|
// System.setProperty("SWT_GTK3", "0");
|
||||||
@ -240,17 +292,11 @@ class SystemTray implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FORCE_TRAY_TYPE < 0 || FORCE_TRAY_TYPE > 3) {
|
|
||||||
throw new RuntimeException("Invalid option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
switch (FORCE_TRAY_TYPE) {
|
if (FORCE_TRAY_TYPE == TrayType.AutoDetect) {
|
||||||
case 1: logger.debug("Forced tray type: GtkStatusIcon"); break;
|
logger.debug("Auto-detecting tray type");
|
||||||
case 2: logger.debug("Forced tray type: AppIndicator"); break;
|
} else {
|
||||||
case 3: logger.debug("Forced tray type: Swing"); break;
|
logger.debug("Forced tray type: {}", FORCE_TRAY_TYPE.name());
|
||||||
|
|
||||||
default: logger.debug("Auto-detecting tray type"); break;
|
|
||||||
}
|
}
|
||||||
logger.debug("FORCE_GTK2: {}", FORCE_GTK2);
|
logger.debug("FORCE_GTK2: {}", FORCE_GTK2);
|
||||||
}
|
}
|
||||||
@ -259,7 +305,7 @@ class SystemTray implements Menu {
|
|||||||
// mouseover or something, but I don't know how to do that. It seems that tooltips for app-indicators are a custom job, as
|
// mouseover or something, but I don't know how to do that. It seems that tooltips for app-indicators are a custom job, as
|
||||||
// all examined ones sometimes have it (and it's more than just text), or they don't have it at all.
|
// all examined ones sometimes have it (and it's more than just text), or they don't have it at all.
|
||||||
|
|
||||||
if (FORCE_TRAY_TYPE != TYPE_SWING && OS.isLinux()) {
|
if (FORCE_TRAY_TYPE != TrayType.Swing && OS.isLinux()) {
|
||||||
// see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running
|
// see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running
|
||||||
|
|
||||||
// For funsies, SyncThing did a LOT of work on compatibility (unfortunate for us) in python.
|
// For funsies, SyncThing did a LOT of work on compatibility (unfortunate for us) in python.
|
||||||
@ -275,26 +321,12 @@ class SystemTray implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
|
|
||||||
try {
|
|
||||||
trayType = _GtkStatusIconTray.class;
|
|
||||||
} catch (Throwable e1) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _GtkStatusIconTray", e1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_APP_INDICATOR) {
|
|
||||||
try {
|
|
||||||
trayType = _AppIndicatorTray.class;
|
|
||||||
} catch (Throwable e1) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _AppIndicatorTray", e1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// don't check for SWING type at this spot, it is done elsewhere.
|
|
||||||
|
|
||||||
|
// this can never be swing
|
||||||
|
// don't check for SWING type at this spot, it is done elsewhere.
|
||||||
|
if (SystemTray.FORCE_TRAY_TYPE != TrayType.AutoDetect) {
|
||||||
|
trayType = selectTypeQuietly(SystemTray.FORCE_TRAY_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least
|
// quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least
|
||||||
@ -341,13 +373,7 @@ class SystemTray implements Menu {
|
|||||||
|
|
||||||
if (trayType == null) {
|
if (trayType == null) {
|
||||||
if ("unity".equalsIgnoreCase(XDG)) {
|
if ("unity".equalsIgnoreCase(XDG)) {
|
||||||
try {
|
trayType = selectTypeQuietly(TrayType.AppIndicator);
|
||||||
trayType = _AppIndicatorTray.class;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _AppIndicatorTray", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ("xfce".equalsIgnoreCase(XDG)) {
|
else if ("xfce".equalsIgnoreCase(XDG)) {
|
||||||
// NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted.
|
// NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted.
|
||||||
@ -355,32 +381,14 @@ class SystemTray implements Menu {
|
|||||||
// see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25
|
// see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25
|
||||||
|
|
||||||
// so far, it is OK to use GtkStatusIcon on XFCE <-> XFCE4 inclusive
|
// so far, it is OK to use GtkStatusIcon on XFCE <-> XFCE4 inclusive
|
||||||
try {
|
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||||
trayType = _GtkStatusIconTray.class;
|
|
||||||
} catch (Throwable e1) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _GtkStatusIconTray", e1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ("lxde".equalsIgnoreCase(XDG)) {
|
else if ("lxde".equalsIgnoreCase(XDG)) {
|
||||||
try {
|
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||||
trayType = _GtkStatusIconTray.class;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _GtkStatusIconTray", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ("kde".equalsIgnoreCase(XDG)) {
|
else if ("kde".equalsIgnoreCase(XDG)) {
|
||||||
// kde (at least, plasma 5.5.6) requires appindicator
|
// kde (at least, plasma 5.5.6) requires appindicator
|
||||||
try {
|
trayType = selectTypeQuietly(TrayType.AppIndicator);
|
||||||
trayType = _AppIndicatorTray.class;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _AppIndicatorTray", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ("gnome".equalsIgnoreCase(XDG)) {
|
else if ("gnome".equalsIgnoreCase(XDG)) {
|
||||||
// check other DE
|
// check other DE
|
||||||
@ -391,31 +399,13 @@ class SystemTray implements Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("cinnamon".equalsIgnoreCase(GDM)) {
|
if ("cinnamon".equalsIgnoreCase(GDM)) {
|
||||||
try {
|
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||||
trayType = _GtkStatusIconTray.class;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _GtkStatusIconTray", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ("gnome-classic".equalsIgnoreCase(GDM)) {
|
else if ("gnome-classic".equalsIgnoreCase(GDM)) {
|
||||||
try {
|
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||||
trayType = _GtkStatusIconTray.class;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _GtkStatusIconTray", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ("gnome-fallback".equalsIgnoreCase(GDM)) {
|
else if ("gnome-fallback".equalsIgnoreCase(GDM)) {
|
||||||
try {
|
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||||
trayType = _GtkStatusIconTray.class;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
logger.error("Cannot initialize _GtkStatusIconTray", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if ("ubuntu".equalsIgnoreCase(GDM)) {
|
else if ("ubuntu".equalsIgnoreCase(GDM)) {
|
||||||
// have to install the gnome extension AND customize the restart command
|
// have to install the gnome extension AND customize the restart command
|
||||||
@ -453,7 +443,7 @@ class SystemTray implements Menu {
|
|||||||
GnomeShellExtension.install(output);
|
GnomeShellExtension.install(output);
|
||||||
// we might be running gnome-shell, we MIGHT NOT. If we are forced to be app-indicator or swing, don't do this.
|
// we might be running gnome-shell, we MIGHT NOT. If we are forced to be app-indicator or swing, don't do this.
|
||||||
if (trayType == null) {
|
if (trayType == null) {
|
||||||
trayType = _GtkStatusIconTray.class;
|
trayType = selectType(TrayType.GtkStatusIcon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@ -491,8 +481,8 @@ class SystemTray implements Menu {
|
|||||||
if (readLine != null && readLine.contains("indicator-app")) {
|
if (readLine != null && readLine.contains("indicator-app")) {
|
||||||
// make sure we can also load the library (it might be the wrong version)
|
// make sure we can also load the library (it might be the wrong version)
|
||||||
try {
|
try {
|
||||||
trayType = _AppIndicatorTray.class;
|
trayType = selectType(TrayType.AppIndicator);
|
||||||
} catch (Throwable e) {
|
} catch (Exception e) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
logger.error("AppIndicator support detected, but unable to load the library. Falling back to GTK", e);
|
logger.error("AppIndicator support detected, but unable to load the library. Falling back to GTK", e);
|
||||||
} else {
|
} else {
|
||||||
@ -516,7 +506,7 @@ class SystemTray implements Menu {
|
|||||||
|
|
||||||
// fallback...
|
// fallback...
|
||||||
if (trayType == null) {
|
if (trayType == null) {
|
||||||
trayType = _GtkStatusIconTray.class;
|
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||||
logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " +
|
logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " +
|
||||||
"configuration");
|
"configuration");
|
||||||
}
|
}
|
||||||
@ -526,14 +516,19 @@ class SystemTray implements Menu {
|
|||||||
// this has to happen BEFORE any sort of swing system tray stuff is accessed
|
// this has to happen BEFORE any sort of swing system tray stuff is accessed
|
||||||
if (OS.isWindows()) {
|
if (OS.isWindows()) {
|
||||||
// windows is funky, and is hardcoded to 16x16. We fix that.
|
// windows is funky, and is hardcoded to 16x16. We fix that.
|
||||||
WindowsSystemTraySwing.fix();
|
SystemTrayFixes.fixWindows();
|
||||||
|
}
|
||||||
|
else if (OS.isMacOsX()) {
|
||||||
|
// macos doesn't respond to all buttons (but should)
|
||||||
|
SystemTrayFixes.fixMacOS();
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is windows OR mac
|
ImageUtils.determineIconSize();
|
||||||
if (trayType == null && java.awt.SystemTray.isSupported()) {
|
|
||||||
|
// this is likely windows OR mac
|
||||||
|
if (trayType == null) {
|
||||||
try {
|
try {
|
||||||
java.awt.SystemTray.getSystemTray();
|
trayType = selectType(TrayType.Swing);
|
||||||
trayType = _SwingTray.class;
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.", e);
|
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.", e);
|
||||||
@ -564,7 +559,7 @@ class SystemTray implements Menu {
|
|||||||
AppIndicator.isVersion3) {
|
AppIndicator.isVersion3) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
trayType = _GtkStatusIconTray.class;
|
trayType = selectType(TrayType.GtkStatusIcon);
|
||||||
logger.warn("AppIndicator3 detected with GTK2, falling back to GTK2 system tray type. " +
|
logger.warn("AppIndicator3 detected with GTK2, falling back to GTK2 system tray type. " +
|
||||||
"Please install libappindicator1 OR GTK3, for example: 'sudo apt-get install libappindicator1'");
|
"Please install libappindicator1 OR GTK3, for example: 'sudo apt-get install libappindicator1'");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@ -578,6 +573,17 @@ class SystemTray implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// if it's native + linux, have to do GTK instead. Don't need to be on the dispatch thread though.
|
||||||
|
// _AwtTray must be constructed on the EDT...
|
||||||
|
if (OS.isLinux() && NativeUI.class.isAssignableFrom(trayType) && trayType == _AwtTray.class) {
|
||||||
|
try {
|
||||||
|
reference.set((Menu) trayType.getConstructors()[0].newInstance(systemTray));
|
||||||
|
logger.info("Successfully Loaded: {}", trayType.getSimpleName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// have to construct swing stuff inside the swing EDT
|
// have to construct swing stuff inside the swing EDT
|
||||||
// this is the safest way to do this.
|
// this is the safest way to do this.
|
||||||
final Class<? extends Menu> finalTrayType = trayType;
|
final Class<? extends Menu> finalTrayType = trayType;
|
||||||
@ -593,12 +599,26 @@ class SystemTray implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
|
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
systemTrayMenu = reference.get();
|
systemTrayMenu = reference.get();
|
||||||
|
|
||||||
|
// verify that we have what we are expecting.
|
||||||
|
if (OS.isWindows() && systemTrayMenu instanceof SwingUI) {
|
||||||
|
// this configuration is OK.
|
||||||
|
}
|
||||||
|
else if (forceNativeMenus && systemTrayMenu instanceof NativeUI) {
|
||||||
|
// this configuration is OK.
|
||||||
|
} else if (!forceNativeMenus && systemTrayMenu instanceof SwingUI) {
|
||||||
|
// this configuration is OK.
|
||||||
|
} else {
|
||||||
|
logger.error("Unable to correctly initialize the System Tray. Please write an issue and include your OS type and " +
|
||||||
|
"configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// These install a shutdown hook in JavaFX/SWT, so that when the main window is closed -- the system tray is ALSO closed.
|
// These install a shutdown hook in JavaFX/SWT, so that when the main window is closed -- the system tray is ALSO closed.
|
||||||
if (ENABLE_SHUTDOWN_HOOK) {
|
if (ENABLE_SHUTDOWN_HOOK) {
|
||||||
@ -642,18 +662,41 @@ class SystemTray implements Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Returns a SystemTray instance that uses a custom Swing menus, which is more advanced than the native menus. The drawback is that
|
||||||
|
* this menu is not native, and so loses the specific Look and Feel of that platform.
|
||||||
|
* <p>
|
||||||
* This always returns the same instance per JVM (it's a singleton), and on some platforms the system tray may not be
|
* 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.
|
* supported, in which case this will return NULL.
|
||||||
*
|
* <p>
|
||||||
* <p>If this is using the Swing SystemTray and a SecurityManager is installed, the AWTPermission {@code accessSystemTray} must
|
* 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.
|
* be granted in order to get the {@code SystemTray} instance. Otherwise this will return null.
|
||||||
*/
|
*/
|
||||||
public static
|
public static
|
||||||
SystemTray get() {
|
SystemTray get() {
|
||||||
|
forceNativeMenus = true; // TODO set to false for final build
|
||||||
init();
|
init();
|
||||||
return systemTray;
|
return systemTray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables native menus on Linux/OSX instead of the custom swing menu. Windows will always use a custom Swing menu.
|
||||||
|
* <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.
|
||||||
|
*/
|
||||||
|
public static
|
||||||
|
SystemTray getNative() {
|
||||||
|
forceNativeMenus = true;
|
||||||
|
init();
|
||||||
|
return systemTray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts-down the SystemTray, by removing the menus + tray icon.
|
||||||
|
*/
|
||||||
public
|
public
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
final Menu menu = systemTrayMenu;
|
final Menu menu = systemTrayMenu;
|
||||||
@ -661,10 +704,19 @@ class SystemTray implements Menu {
|
|||||||
if (menu instanceof _AppIndicatorTray) {
|
if (menu instanceof _AppIndicatorTray) {
|
||||||
((_AppIndicatorTray) menu).shutdown();
|
((_AppIndicatorTray) menu).shutdown();
|
||||||
}
|
}
|
||||||
|
else if (menu instanceof _AppIndicatorNativeTray) {
|
||||||
|
((_AppIndicatorNativeTray) menu).shutdown();
|
||||||
|
}
|
||||||
else if (menu instanceof _GtkStatusIconTray) {
|
else if (menu instanceof _GtkStatusIconTray) {
|
||||||
((_GtkStatusIconTray) menu).shutdown();
|
((_GtkStatusIconTray) menu).shutdown();
|
||||||
} else {
|
}
|
||||||
// swing
|
else if (menu instanceof _GtkStatusIconNativeTray) {
|
||||||
|
((_GtkStatusIconNativeTray) menu).shutdown();
|
||||||
|
}
|
||||||
|
else if (menu instanceof _AwtTray) {
|
||||||
|
((_AwtTray) menu).shutdown();
|
||||||
|
}
|
||||||
|
else {
|
||||||
((_SwingTray) menu).shutdown();
|
((_SwingTray) menu).shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -675,13 +727,23 @@ class SystemTray implements Menu {
|
|||||||
public
|
public
|
||||||
String getStatus() {
|
String getStatus() {
|
||||||
final Menu menu = systemTrayMenu;
|
final Menu menu = systemTrayMenu;
|
||||||
|
|
||||||
if (menu instanceof _AppIndicatorTray) {
|
if (menu instanceof _AppIndicatorTray) {
|
||||||
return ((_AppIndicatorTray) menu).getStatus();
|
return ((_AppIndicatorTray) menu).getStatus();
|
||||||
}
|
}
|
||||||
|
else if (menu instanceof _AppIndicatorNativeTray) {
|
||||||
|
return ((_AppIndicatorNativeTray) menu).getStatus();
|
||||||
|
}
|
||||||
else if (menu instanceof _GtkStatusIconTray) {
|
else if (menu instanceof _GtkStatusIconTray) {
|
||||||
return ((_GtkStatusIconTray) menu).getStatus();
|
return ((_GtkStatusIconTray) menu).getStatus();
|
||||||
} else {
|
}
|
||||||
// swing
|
else if (menu instanceof _GtkStatusIconNativeTray) {
|
||||||
|
return ((_GtkStatusIconNativeTray) menu).getStatus();
|
||||||
|
}
|
||||||
|
else if (menu instanceof _AwtTray) {
|
||||||
|
return ((_AwtTray) menu).getStatus();
|
||||||
|
}
|
||||||
|
else {
|
||||||
return ((_SwingTray) menu).getStatus();
|
return ((_SwingTray) menu).getStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -698,10 +760,19 @@ class SystemTray implements Menu {
|
|||||||
if (menu instanceof _AppIndicatorTray) {
|
if (menu instanceof _AppIndicatorTray) {
|
||||||
((_AppIndicatorTray) menu).setStatus(statusText);
|
((_AppIndicatorTray) menu).setStatus(statusText);
|
||||||
}
|
}
|
||||||
|
else if (menu instanceof _AppIndicatorNativeTray) {
|
||||||
|
((_AppIndicatorNativeTray) menu).setStatus(statusText);
|
||||||
|
}
|
||||||
else if (menu instanceof _GtkStatusIconTray) {
|
else if (menu instanceof _GtkStatusIconTray) {
|
||||||
((_GtkStatusIconTray) menu).setStatus(statusText);
|
((_GtkStatusIconTray) menu).setStatus(statusText);
|
||||||
} else {
|
}
|
||||||
// swing
|
else if (menu instanceof _GtkStatusIconNativeTray) {
|
||||||
|
((_GtkStatusIconNativeTray) menu).setStatus(statusText);
|
||||||
|
}
|
||||||
|
else if (menu instanceof _AwtTray) {
|
||||||
|
((_AwtTray) menu).setStatus(statusText);
|
||||||
|
}
|
||||||
|
else {
|
||||||
((_SwingTray) menu).setStatus(statusText);
|
((_SwingTray) menu).setStatus(statusText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1063,6 +1134,7 @@ class SystemTray implements Menu {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This removes a menu entry from the dropdown menu.
|
* This removes a menu entry from the dropdown menu.
|
||||||
*
|
*
|
||||||
|
@ -50,7 +50,7 @@ class AppIndicator {
|
|||||||
// ALSO WHAT VERSION OF GTK to use? appindiactor1 -> GTk2, appindicator3 -> GTK3.
|
// ALSO WHAT VERSION OF GTK to use? appindiactor1 -> GTk2, appindicator3 -> GTK3.
|
||||||
// appindicator3 doesn't support menu icons via GTK2!!
|
// appindicator3 doesn't support menu icons via GTK2!!
|
||||||
|
|
||||||
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
|
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TrayType.GtkStatusIcon) {
|
||||||
// if we force GTK type system tray, don't attempt to load AppIndicator libs
|
// if we force GTK type system tray, don't attempt to load AppIndicator libs
|
||||||
if (SystemTray.DEBUG) {
|
if (SystemTray.DEBUG) {
|
||||||
logger.debug("Forcing GTK tray, not using appindicator");
|
logger.debug("Forcing GTK tray, not using appindicator");
|
||||||
|
29
src/dorkbox/systemTray/linux/jna/GCallback.java
Normal file
29
src/dorkbox/systemTray/linux/jna/GCallback.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 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.linux.jna;
|
||||||
|
|
||||||
|
import com.sun.jna.Callback;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import dorkbox.util.Keep;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public
|
||||||
|
interface GCallback extends Callback {
|
||||||
|
/**
|
||||||
|
* @return Gtk.TRUE if we handled this event
|
||||||
|
*/
|
||||||
|
int callback(Pointer instance, Pointer data);
|
||||||
|
}
|
@ -37,5 +37,8 @@ class Gobject {
|
|||||||
|
|
||||||
public static native void g_object_unref(Pointer object);
|
public static native void g_object_unref(Pointer object);
|
||||||
|
|
||||||
|
public static native void g_object_force_floating(Pointer object);
|
||||||
|
public static native void g_object_ref_sink(Pointer object);
|
||||||
|
|
||||||
public static native NativeLong g_signal_connect_object(Pointer instance, String detailed_signal, Callback c_handler, Pointer object, int connect_flags);
|
public static native NativeLong g_signal_connect_object(Pointer instance, String detailed_signal, Callback c_handler, Pointer object, int connect_flags);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,9 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import com.sun.jna.Function;
|
import com.sun.jna.Function;
|
||||||
import com.sun.jna.Pointer;
|
import com.sun.jna.Pointer;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.Entry;
|
||||||
|
import dorkbox.systemTray.Menu;
|
||||||
import dorkbox.systemTray.SystemTray;
|
import dorkbox.systemTray.SystemTray;
|
||||||
import dorkbox.systemTray.util.JavaFX;
|
import dorkbox.systemTray.util.JavaFX;
|
||||||
import dorkbox.systemTray.util.Swt;
|
import dorkbox.systemTray.util.Swt;
|
||||||
@ -71,7 +74,7 @@ class Gtk {
|
|||||||
String gtk3LibName = "libgtk-3.so.0";
|
String gtk3LibName = "libgtk-3.so.0";
|
||||||
|
|
||||||
// we can force the system to use the swing indicator, which WORKS, but doesn't support transparency in the icon.
|
// we can force the system to use the swing indicator, which WORKS, but doesn't support transparency in the icon.
|
||||||
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_SWING) {
|
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TrayType.Swing) {
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +250,7 @@ class Gtk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (SystemTray.isSwtLoaded) {
|
} else if (SystemTray.isSwtLoaded) {
|
||||||
if (SystemTray.FORCE_TRAY_TYPE != SystemTray.TYPE_GTK_STATUSICON) {
|
if (SystemTray.FORCE_TRAY_TYPE != SystemTray.TrayType.GtkStatusIcon) {
|
||||||
// GTK system tray has threading issues if we block here (because it is likely in the event thread)
|
// 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
|
// AppIndicator version doesn't have this problem
|
||||||
|
|
||||||
@ -375,6 +378,23 @@ class Gtk {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* required to properly setup the dispatch flag
|
||||||
|
* @param callback will never be null.
|
||||||
|
*/
|
||||||
|
public static
|
||||||
|
void proxyClick(final Menu parent, final Entry menuEntry, final Action callback) {
|
||||||
|
Gtk.isDispatch = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
callback.onClick(parent.getSystemTray(), parent, menuEntry);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
SystemTray.logger.error("Error calling menu entry {} click event.", menuEntry.getText(), throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.isDispatch = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This would NORMALLY have a 2nd argument that is a String[] -- however JNA direct-mapping DOES NOT support this. We are lucky
|
* This would NORMALLY have a 2nd argument that is a String[] -- however JNA direct-mapping DOES NOT support this. We are lucky
|
||||||
* enough that we just pass 'null' as the second argument, therefore, we don't have to define that parameter here.
|
* enough that we just pass 'null' as the second argument, therefore, we don't have to define that parameter here.
|
||||||
@ -400,10 +420,22 @@ class Gtk {
|
|||||||
|
|
||||||
|
|
||||||
public static native Pointer gtk_menu_new();
|
public static native Pointer gtk_menu_new();
|
||||||
|
public static native Pointer gtk_menu_item_set_submenu(Pointer menuEntry, Pointer menu);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static native Pointer gtk_separator_menu_item_new();
|
||||||
|
|
||||||
|
// to create a menu entry WITH an icon.
|
||||||
|
public static native Pointer gtk_image_new_from_file(String iconPath);
|
||||||
|
|
||||||
// uses '_' to define which key is the mnemonic
|
// uses '_' to define which key is the mnemonic
|
||||||
public static native Pointer gtk_image_menu_item_new_with_mnemonic(String label);
|
public static native Pointer gtk_image_menu_item_new_with_mnemonic(String label);
|
||||||
|
|
||||||
|
public static native void gtk_image_menu_item_set_image(Pointer image_menu_item, Pointer image);
|
||||||
|
|
||||||
|
public static native void gtk_image_menu_item_set_always_show_image(Pointer menu_item, int forceShow);
|
||||||
|
|
||||||
public static native Pointer gtk_status_icon_new();
|
public static native Pointer gtk_status_icon_new();
|
||||||
|
|
||||||
public static native void gtk_status_icon_set_from_file(Pointer widget, String label);
|
public static native void gtk_status_icon_set_from_file(Pointer widget, String label);
|
||||||
@ -417,8 +449,18 @@ class Gtk {
|
|||||||
|
|
||||||
public static native void gtk_status_icon_set_name(Pointer widget, String name);
|
public static native void gtk_status_icon_set_name(Pointer widget, String name);
|
||||||
|
|
||||||
|
public static native void gtk_menu_popup(Pointer menu, Pointer widget, Pointer bla, Function func, Pointer data, int button, int time);
|
||||||
|
|
||||||
|
public static native void gtk_menu_item_set_label(Pointer menu_item, String label);
|
||||||
|
|
||||||
public static native void gtk_menu_shell_append(Pointer menu_shell, Pointer child);
|
public static native void gtk_menu_shell_append(Pointer menu_shell, Pointer child);
|
||||||
|
|
||||||
|
public static native void gtk_menu_shell_deactivate(Pointer menu_shell, Pointer child);
|
||||||
|
|
||||||
|
public static native void gtk_widget_set_sensitive(Pointer widget, int sensitive);
|
||||||
|
|
||||||
|
public static native void gtk_container_remove(Pointer menu, Pointer subItem);
|
||||||
|
|
||||||
public static native void gtk_widget_show_all(Pointer widget);
|
public static native void gtk_widget_show_all(Pointer widget);
|
||||||
|
|
||||||
public static native void gtk_widget_destroy(Pointer widget);
|
public static native void gtk_widget_destroy(Pointer widget);
|
||||||
|
209
src/dorkbox/systemTray/nativeUI/AwtEntry.java
Normal file
209
src/dorkbox/systemTray/nativeUI/AwtEntry.java
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.awt.MenuItem;
|
||||||
|
import java.awt.MenuShortcut;
|
||||||
|
import java.awt.PopupMenu;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Entry;
|
||||||
|
import dorkbox.systemTray.Menu;
|
||||||
|
import dorkbox.systemTray.swingUI.SwingUI;
|
||||||
|
import dorkbox.systemTray.util.ImageUtils;
|
||||||
|
import dorkbox.systemTray.util.MenuBase;
|
||||||
|
import dorkbox.systemTray.util.SystemTrayFixes;
|
||||||
|
|
||||||
|
abstract
|
||||||
|
class AwtEntry implements Entry, SwingUI {
|
||||||
|
private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement();
|
||||||
|
|
||||||
|
private final AwtMenu parent;
|
||||||
|
final MenuItem _native;
|
||||||
|
|
||||||
|
// this have to be volatile, because they can be changed from any thread
|
||||||
|
private volatile String text;
|
||||||
|
|
||||||
|
// this is ALWAYS called on the EDT.
|
||||||
|
AwtEntry(final AwtMenu parent, final MenuItem menuItem) {
|
||||||
|
this.parent = parent;
|
||||||
|
this._native = menuItem;
|
||||||
|
|
||||||
|
parent._native.add(menuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
Menu getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* must always be called in the EDT thread
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void renderText(final String text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not always called on the EDT thread
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void setImage_(final File imageFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables, or disables the sub-menu entry.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
_native.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
if (!(_native instanceof PopupMenu)) {
|
||||||
|
// yikes...
|
||||||
|
final int vKey = SystemTrayFixes.getVirtualKey(key);
|
||||||
|
|
||||||
|
parent.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
_native.setShortcut(new MenuShortcut(vKey));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setText(final String newText) {
|
||||||
|
this.text = newText;
|
||||||
|
|
||||||
|
parent.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
renderText(newText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setImage(final File imageFile) {
|
||||||
|
if (imageFile == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final String imagePath) {
|
||||||
|
if (imagePath == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final URL imageUrl) {
|
||||||
|
if (imageUrl == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final String cacheName, final InputStream imageStream) {
|
||||||
|
if (imageStream == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final InputStream imageStream) {
|
||||||
|
if (imageStream == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void remove() {
|
||||||
|
parent.dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
removePrivate();
|
||||||
|
parent._native.remove(_native);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// called when this item is removed. Necessary to cleanup/remove itself
|
||||||
|
abstract
|
||||||
|
void removePrivate();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
int hashCode() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AwtEntry other = (AwtEntry) obj;
|
||||||
|
return this.id == other.id;
|
||||||
|
}
|
||||||
|
}
|
90
src/dorkbox/systemTray/nativeUI/AwtEntryItem.java
Normal file
90
src/dorkbox/systemTray/nativeUI/AwtEntryItem.java
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
|
||||||
|
class AwtEntryItem extends AwtEntry {
|
||||||
|
|
||||||
|
private final ActionListener swingCallback;
|
||||||
|
|
||||||
|
private volatile Action callback;
|
||||||
|
|
||||||
|
// this is ALWAYS called on the EDT.
|
||||||
|
AwtEntryItem(final AwtMenu parent, final Action callback) {
|
||||||
|
super(parent, new java.awt.MenuItem());
|
||||||
|
this.callback = callback;
|
||||||
|
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
_native.setEnabled(true);
|
||||||
|
swingCallback = new ActionListener() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void actionPerformed(ActionEvent e) {
|
||||||
|
// we want it to run on the EDT
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_native.addActionListener(swingCallback);
|
||||||
|
} else {
|
||||||
|
_native.setEnabled(false);
|
||||||
|
swingCallback = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setCallback(final Action callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private
|
||||||
|
void handle() {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onClick(getParent().getSystemTray(), getParent(), this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// always called in the EDT
|
||||||
|
@Override
|
||||||
|
void renderText(final String text) {
|
||||||
|
_native.setLabel(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// not supported!
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not supported!
|
||||||
|
@Override
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void removePrivate() {
|
||||||
|
_native.removeActionListener(swingCallback);
|
||||||
|
}
|
||||||
|
}
|
58
src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java
Normal file
58
src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.awt.MenuItem;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
|
||||||
|
class AwtEntrySeparator extends AwtEntry implements dorkbox.systemTray.Separator {
|
||||||
|
|
||||||
|
// this is ALWAYS called on the EDT.
|
||||||
|
AwtEntrySeparator(final AwtMenu parent) {
|
||||||
|
super(parent, new MenuItem("-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// called in the EDT thread
|
||||||
|
@Override
|
||||||
|
void renderText(final String text) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void removePrivate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setCallback(final Action callback) {
|
||||||
|
}
|
||||||
|
}
|
75
src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java
Normal file
75
src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import static java.awt.Font.DIALOG;
|
||||||
|
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.MenuItem;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.Status;
|
||||||
|
|
||||||
|
class AwtEntryStatus extends AwtEntry implements Status {
|
||||||
|
|
||||||
|
// this is ALWAYS called on the EDT.
|
||||||
|
AwtEntryStatus(final AwtMenu parent, final String label) {
|
||||||
|
super(parent, new MenuItem());
|
||||||
|
setText(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// called in the EDT thread
|
||||||
|
@Override
|
||||||
|
void renderText(final String text) {
|
||||||
|
Font font = _native.getFont();
|
||||||
|
if (font == null) {
|
||||||
|
font = new Font(DIALOG, Font.BOLD, 12); // the default font used for dialogs.
|
||||||
|
} else {
|
||||||
|
font = font.deriveFont(Font.BOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
_native.setFont(font);
|
||||||
|
_native.setLabel(text);
|
||||||
|
|
||||||
|
// this makes sure it can't be selected
|
||||||
|
_native.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void removePrivate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setCallback(final Action callback) {
|
||||||
|
}
|
||||||
|
}
|
323
src/dorkbox/systemTray/nativeUI/AwtMenu.java
Normal file
323
src/dorkbox/systemTray/nativeUI/AwtMenu.java
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
|
||||||
|
import java.awt.MenuShortcut;
|
||||||
|
import java.awt.PopupMenu;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.Entry;
|
||||||
|
import dorkbox.systemTray.Menu;
|
||||||
|
import dorkbox.systemTray.Status;
|
||||||
|
import dorkbox.systemTray.SystemTray;
|
||||||
|
import dorkbox.systemTray.util.MenuBase;
|
||||||
|
import dorkbox.systemTray.util.SystemTrayFixes;
|
||||||
|
import dorkbox.util.SwingUtil;
|
||||||
|
|
||||||
|
// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both
|
||||||
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
||||||
|
class AwtMenu extends MenuBase implements NativeUI {
|
||||||
|
|
||||||
|
// sub-menu = java.awt.Menu
|
||||||
|
// systemtray = java.awt.PopupMenu
|
||||||
|
volatile java.awt.Menu _native;
|
||||||
|
|
||||||
|
// this have to be volatile, because they can be changed from any thread
|
||||||
|
private volatile String text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called in the EDT
|
||||||
|
*
|
||||||
|
* @param systemTray the system tray (which is the object that sits in the system tray)
|
||||||
|
* @param parent the parent of this menu, null if the parent is the system tray
|
||||||
|
* @param _native the native element that represents this menu
|
||||||
|
*/
|
||||||
|
AwtMenu(final SystemTray systemTray, final Menu parent, final java.awt.Menu _native) {
|
||||||
|
super(systemTray, parent);
|
||||||
|
this._native = _native;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final
|
||||||
|
void dispatch(final Runnable runnable) {
|
||||||
|
// this will properly check if we are running on the EDT
|
||||||
|
SwingUtil.invokeLater(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final
|
||||||
|
void dispatchAndWait(final Runnable runnable) {
|
||||||
|
// this will properly check if we are running on the EDT
|
||||||
|
try {
|
||||||
|
SwingUtil.invokeAndWait(runnable);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SystemTray.logger.error("Error processing event on the dispatch thread.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// always called in the EDT
|
||||||
|
protected final
|
||||||
|
void renderText(final String text) {
|
||||||
|
_native.setLabel(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setText(final String newText) {
|
||||||
|
text = newText;
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
renderText(newText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a new menu entry, or update one if it already exists
|
||||||
|
* NOT ALWAYS CALLED ON EDT
|
||||||
|
*/
|
||||||
|
protected final
|
||||||
|
Entry addEntry_(final String menuText, final File imagePath, final Action callback) {
|
||||||
|
if (menuText == null) {
|
||||||
|
throw new NullPointerException("Menu text cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
final AtomicReference<Entry> value = new AtomicReference<Entry>();
|
||||||
|
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry entry = get(menuText);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
// must always be called on the EDT
|
||||||
|
entry = new AwtEntryItem(AwtMenu.this, callback);
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
|
||||||
|
menuEntries.add(entry);
|
||||||
|
} else if (entry instanceof AwtEntryItem) {
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
value.set(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a new sub-menu entry, or update one if it already exists
|
||||||
|
* NOT ALWAYS CALLED ON EDT
|
||||||
|
*/
|
||||||
|
protected final
|
||||||
|
Menu addMenu_(final String menuText, final File imagePath) {
|
||||||
|
if (menuText == null) {
|
||||||
|
throw new NullPointerException("Menu text cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
final AtomicReference<Menu> value = new AtomicReference<Menu>();
|
||||||
|
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry entry = get(menuText);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
// must always be called on the EDT
|
||||||
|
entry = new AwtMenu(getSystemTray(), AwtMenu.this, new java.awt.Menu());
|
||||||
|
_native.add(((AwtMenu) entry)._native); // have to add it separately
|
||||||
|
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
value.set((Menu) entry);
|
||||||
|
|
||||||
|
} else if (entry instanceof AwtMenu) {
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
menuEntries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
|
public
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
// not supported!
|
||||||
|
}
|
||||||
|
|
||||||
|
// not supported!
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
_native.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOT ALWAYS CALLED ON EDT
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void addSeparator() {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry entry = new AwtEntrySeparator(AwtMenu.this);
|
||||||
|
menuEntries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however.
|
||||||
|
// public
|
||||||
|
// Entry addWidget(final JComponent widget) {
|
||||||
|
// if (widget == null) {
|
||||||
|
// throw new NullPointerException("Widget cannot be null");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final AtomicReference<Entry> value = new AtomicReference<Entry>();
|
||||||
|
//
|
||||||
|
// dispatchAndWait(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public
|
||||||
|
// void run() {
|
||||||
|
// synchronized (menuEntries) {
|
||||||
|
// // must always be called on the EDT
|
||||||
|
// Entry entry = new SwingEntryWidget(SwingMenu.this, widget);
|
||||||
|
// value.set(entry);
|
||||||
|
// menuEntries.add(entry);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// return value.get();
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can access this
|
||||||
|
public final
|
||||||
|
void setStatus(final String statusText) {
|
||||||
|
final AwtMenu _this = this;
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
// status is ALWAYS at 0 index...
|
||||||
|
AwtEntry menuEntry = null;
|
||||||
|
if (!menuEntries.isEmpty()) {
|
||||||
|
menuEntry = (AwtEntry) menuEntries.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuEntry instanceof Status) {
|
||||||
|
// set the text or delete...
|
||||||
|
|
||||||
|
if (statusText == null) {
|
||||||
|
// delete
|
||||||
|
remove(menuEntry);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// set text
|
||||||
|
menuEntry.setText(statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// create a new one
|
||||||
|
menuEntry = new AwtEntryStatus(_this, statusText);
|
||||||
|
// status is ALWAYS at 0 index...
|
||||||
|
menuEntries.add(0, menuEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
if (!(_native instanceof PopupMenu)) {
|
||||||
|
// yikes...
|
||||||
|
final int vKey = SystemTrayFixes.getVirtualKey(key);
|
||||||
|
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
_native.setShortcut(new MenuShortcut(vKey));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void remove() {
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
AwtMenu parent = (AwtMenu) getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent._native.remove(_native);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
218
src/dorkbox/systemTray/nativeUI/GtkEntry.java
Normal file
218
src/dorkbox/systemTray/nativeUI/GtkEntry.java
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Entry;
|
||||||
|
import dorkbox.systemTray.Menu;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
|
import dorkbox.systemTray.util.ImageUtils;
|
||||||
|
|
||||||
|
abstract
|
||||||
|
class GtkEntry implements Entry {
|
||||||
|
private final int id = GtkMenu.MENU_ID_COUNTER.getAndIncrement();
|
||||||
|
|
||||||
|
private final GtkMenu parent;
|
||||||
|
final Pointer _native;
|
||||||
|
|
||||||
|
// this have to be volatile, because they can be changed from any thread
|
||||||
|
private volatile String text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
|
||||||
|
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
|
||||||
|
*/
|
||||||
|
GtkEntry(final GtkMenu parent, final Pointer menuItem) {
|
||||||
|
this.parent = parent;
|
||||||
|
this._native = menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public
|
||||||
|
Menu getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images
|
||||||
|
*
|
||||||
|
* always called on the DISPATCH thread
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void setSpacerImage(final boolean everyoneElseHasImages);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* must always be called in the GTK thread
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void renderText(final String text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* must always be called in the GTK thread
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void setImage_(final File imageFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* must always be called in the GTK thread
|
||||||
|
* called when this item is removed. Necessary to cleanup/remove itself
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void removePrivate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables, or disables the sub-menu entry.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
Gtk.gtk_widget_set_sensitive(_native, Gtk.TRUE);
|
||||||
|
} else {
|
||||||
|
Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setText(final String newText) {
|
||||||
|
text = newText;
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
renderText(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setImage(final File imageFile) {
|
||||||
|
if (imageFile == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final String imagePath) {
|
||||||
|
if (imagePath == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final URL imageUrl) {
|
||||||
|
if (imageUrl == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final String cacheName, final InputStream imageStream) {
|
||||||
|
if (imageStream == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final InputStream imageStream) {
|
||||||
|
if (imageStream == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a child will always remove itself from the parent.
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void remove() {
|
||||||
|
parent.dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
Gtk.gtk_container_remove(parent._native, _native);
|
||||||
|
Gtk.gtk_menu_shell_deactivate(parent._native, _native);
|
||||||
|
|
||||||
|
removePrivate();
|
||||||
|
|
||||||
|
Gtk.gtk_widget_destroy(_native);
|
||||||
|
|
||||||
|
// have to rebuild the menu now...
|
||||||
|
parent.deleteMenu();
|
||||||
|
parent.createMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
int hashCode() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkEntry other = (GtkEntry) obj;
|
||||||
|
return this.id == other.id;
|
||||||
|
}
|
||||||
|
}
|
192
src/dorkbox/systemTray/nativeUI/GtkEntryItem.java
Normal file
192
src/dorkbox/systemTray/nativeUI/GtkEntryItem.java
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import com.sun.jna.NativeLong;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.linux.jna.GCallback;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gobject;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
|
import dorkbox.systemTray.util.ImageUtils;
|
||||||
|
|
||||||
|
class GtkEntryItem extends GtkEntry implements GCallback {
|
||||||
|
private static File transparentIcon = null;
|
||||||
|
|
||||||
|
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||||
|
private final NativeLong nativeLong;
|
||||||
|
|
||||||
|
// these have to be volatile, because they can be changed from any thread
|
||||||
|
private volatile Action callback;
|
||||||
|
private volatile Pointer image;
|
||||||
|
|
||||||
|
// these are necessary BECAUSE GTK menus look funky as hell when there are some menu entries WITH icons and some WITHOUT
|
||||||
|
protected volatile boolean hasLegitIcon = true;
|
||||||
|
|
||||||
|
// The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then!
|
||||||
|
// AppIndicators will only show if you use the keyboard to navigate
|
||||||
|
// GtkStatusIconTray will show on mouse+keyboard movement
|
||||||
|
private volatile char mnemonicKey = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
|
||||||
|
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
|
||||||
|
*/
|
||||||
|
GtkEntryItem(final GtkMenu parent, final Action callback) {
|
||||||
|
super(parent, Gtk.gtk_image_menu_item_new_with_mnemonic(""));
|
||||||
|
this.callback = callback;
|
||||||
|
|
||||||
|
// cannot be done in a static initializer, because the tray icon size might not yet have been determined
|
||||||
|
if (transparentIcon == null) {
|
||||||
|
transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
Gtk.gtk_widget_set_sensitive(_native, Gtk.TRUE);
|
||||||
|
nativeLong = Gobject.g_signal_connect_object(_native, "activate", this, null, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE);
|
||||||
|
nativeLong = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
this.mnemonicKey = Character.toLowerCase(key);
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
renderText(getText());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setCallback(final Action callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// called by native code
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
int callback(final Pointer instance, final Pointer data) {
|
||||||
|
final Action cb = this.callback;
|
||||||
|
if (cb != null) {
|
||||||
|
Gtk.proxyClick(getParent(), GtkEntryItem.this, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Gtk.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return hasLegitIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images.
|
||||||
|
* This is primarily only with AppIndicators, although not always.
|
||||||
|
* <p>
|
||||||
|
* called on the DISPATCH thread
|
||||||
|
*/
|
||||||
|
void setSpacerImage(final boolean everyoneElseHasImages) {
|
||||||
|
if (hasLegitIcon) {
|
||||||
|
// we have a legit icon, so there is nothing else we can do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image != null) {
|
||||||
|
Gtk.gtk_widget_destroy(image);
|
||||||
|
image = null;
|
||||||
|
Gtk.gtk_widget_show_all(_native);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (everyoneElseHasImages) {
|
||||||
|
image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath());
|
||||||
|
Gtk.gtk_image_menu_item_set_image(_native, image);
|
||||||
|
|
||||||
|
// must always re-set always-show after setting the image
|
||||||
|
Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.gtk_widget_show_all(_native);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* must always be called in the GTK thread
|
||||||
|
*/
|
||||||
|
void renderText(String text) {
|
||||||
|
if (this.mnemonicKey != 0) {
|
||||||
|
// they are CASE INSENSITIVE!
|
||||||
|
int i = text.toLowerCase()
|
||||||
|
.indexOf(this.mnemonicKey);
|
||||||
|
|
||||||
|
if (i >= 0) {
|
||||||
|
text = text.substring(0, i) + "_" + text.substring(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.gtk_menu_item_set_label(_native, text);
|
||||||
|
Gtk.gtk_widget_show_all(_native);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
hasLegitIcon = imageFile != null;
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
if (image != null) {
|
||||||
|
Gtk.gtk_widget_destroy(image);
|
||||||
|
image = null;
|
||||||
|
Gtk.gtk_widget_show_all(_native);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFile != null) {
|
||||||
|
image = Gtk.gtk_image_new_from_file(imageFile.getAbsolutePath());
|
||||||
|
Gtk.gtk_image_menu_item_set_image(_native, image);
|
||||||
|
|
||||||
|
// must always re-set always-show after setting the image
|
||||||
|
Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.gtk_widget_show_all(_native);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePrivate() {
|
||||||
|
callback = null;
|
||||||
|
|
||||||
|
if (image != null) {
|
||||||
|
Gtk.gtk_widget_destroy(image);
|
||||||
|
image = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java
Normal file
66
src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.Separator;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
|
|
||||||
|
class GtkEntrySeparator extends GtkEntry implements Separator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
|
||||||
|
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
|
||||||
|
*/
|
||||||
|
GtkEntrySeparator(final GtkMenu parent) {
|
||||||
|
super(parent, Gtk.gtk_separator_menu_item_new());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setSpacerImage(final boolean everyoneElseHasImages) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// called in the GTK thread
|
||||||
|
@Override
|
||||||
|
void renderText(final String text) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void removePrivate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setCallback(final Action callback) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
}
|
||||||
|
}
|
57
src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java
Normal file
57
src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
|
|
||||||
|
// you might wonder WHY this extends MenuEntryItem -- the reason is that an AppIndicator "status" will be offset from everyone else,
|
||||||
|
// where a GtkStatusIconTray + SwingUI will have everything lined up. (with or without icons). This is to normalize how it looks
|
||||||
|
class GtkEntryStatus extends GtkEntryItem {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
|
||||||
|
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
|
||||||
|
*/
|
||||||
|
GtkEntryStatus(final GtkMenu parent, final String text) {
|
||||||
|
super(parent, null);
|
||||||
|
// need that extra space so it matches windows/mac
|
||||||
|
hasLegitIcon = false;
|
||||||
|
setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// called in the GTK thread
|
||||||
|
@Override
|
||||||
|
void renderText(final String text) {
|
||||||
|
// AppIndicator strips out markup text.
|
||||||
|
// https://mail.gnome.org/archives/commits-list/2016-March/msg05444.html
|
||||||
|
|
||||||
|
Gtk.gtk_menu_item_set_label(_native, text);
|
||||||
|
Gtk.gtk_widget_show_all(_native);
|
||||||
|
|
||||||
|
Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setCallback(final Action callback) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
}
|
||||||
|
}
|
500
src/dorkbox/systemTray/nativeUI/GtkMenu.java
Normal file
500
src/dorkbox/systemTray/nativeUI/GtkMenu.java
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.Entry;
|
||||||
|
import dorkbox.systemTray.Menu;
|
||||||
|
import dorkbox.systemTray.SystemTray;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gobject;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
|
import dorkbox.systemTray.util.MenuBase;
|
||||||
|
|
||||||
|
class GtkMenu extends MenuBase implements NativeUI {
|
||||||
|
static int TIMEOUT = 2;
|
||||||
|
|
||||||
|
// menu entry that this menu is attached to. Will be NULL when it's the system tray
|
||||||
|
private final GtkEntryItem menuEntry;
|
||||||
|
|
||||||
|
// must ONLY be created at the end of delete!
|
||||||
|
volatile Pointer _native;
|
||||||
|
|
||||||
|
// have to make sure no other methods can call obliterate, delete, or create menu once it's already started
|
||||||
|
private boolean obliterateInProgress = false;
|
||||||
|
|
||||||
|
// called on dispatch
|
||||||
|
GtkMenu(final SystemTray systemTray, final GtkMenu parent) {
|
||||||
|
super(systemTray, parent);
|
||||||
|
|
||||||
|
if (parent != null) {
|
||||||
|
this.menuEntry = new GtkEntryItem(parent, null);
|
||||||
|
// by default, no callback on a menu entry means it's DISABLED. we have to undo that, because we don't have a callback for menus
|
||||||
|
menuEntry.setEnabled(true);
|
||||||
|
} else {
|
||||||
|
this.menuEntry = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called inside the gdk_threads block
|
||||||
|
*/
|
||||||
|
protected
|
||||||
|
void onMenuAdded(final Pointer menu) {
|
||||||
|
// only needed for AppIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Necessary to guarantee all updates occur on the dispatch thread
|
||||||
|
*/
|
||||||
|
protected
|
||||||
|
void dispatch(final Runnable runnable) {
|
||||||
|
Gtk.dispatch(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Necessary to guarantee all updates occur on the dispatch thread
|
||||||
|
*/
|
||||||
|
protected
|
||||||
|
void dispatchAndWait(final Runnable runnable) {
|
||||||
|
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} finally {
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// this is slightly different than how swing does it. We have a timeout here so that we can make sure that updates on the GUI
|
||||||
|
// thread occur in REASONABLE time-frames, and alert the user if not.
|
||||||
|
try {
|
||||||
|
if (!countDownLatch.await(TIMEOUT, TimeUnit.SECONDS)) {
|
||||||
|
if (SystemTray.DEBUG) {
|
||||||
|
SystemTray.logger.error("Event dispatch queue took longer than " + TIMEOUT + " seconds to complete. Please adjust " +
|
||||||
|
"`SystemTray.TIMEOUT` to a value which better suites your environment.");
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Event dispatch queue took longer than " + TIMEOUT + " seconds to complete. Please adjust " +
|
||||||
|
"`SystemTray.TIMEOUT` to a value which better suites your environment.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
SystemTray.logger.error("Error waiting for dispatch to complete.", new Exception());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public
|
||||||
|
void shutdown() {
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
obliterateMenu();
|
||||||
|
|
||||||
|
Gtk.shutdownGui();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can access this
|
||||||
|
public final
|
||||||
|
void setStatus(final String statusText) {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
// status is ALWAYS at 0 index...
|
||||||
|
GtkEntry menuEntry = null;
|
||||||
|
if (!menuEntries.isEmpty()) {
|
||||||
|
menuEntry = (GtkEntry) menuEntries.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuEntry instanceof GtkEntryStatus) {
|
||||||
|
// always delete...
|
||||||
|
remove(menuEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
deleteMenu();
|
||||||
|
|
||||||
|
if (menuEntry == null) {
|
||||||
|
menuEntry = new GtkEntryStatus(GtkMenu.this, statusText);
|
||||||
|
// status is ALWAYS at 0 index...
|
||||||
|
menuEntries.add(0, menuEntry);
|
||||||
|
}
|
||||||
|
else if (menuEntry instanceof GtkEntryStatus) {
|
||||||
|
// change the text?
|
||||||
|
if (statusText != null) {
|
||||||
|
menuEntry = new GtkEntryStatus(GtkMenu.this, statusText);
|
||||||
|
menuEntries.add(0, menuEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return menuEntry.hasImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
|
@Override
|
||||||
|
protected
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
menuEntry.setImage_(imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
Gtk.gtk_widget_set_sensitive(menuEntry._native, Gtk.TRUE);
|
||||||
|
} else {
|
||||||
|
Gtk.gtk_widget_set_sensitive(menuEntry._native, Gtk.FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
String getText() {
|
||||||
|
return menuEntry.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setText(final String newText) {
|
||||||
|
menuEntry.setText(newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void addSeparator() {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
deleteMenu();
|
||||||
|
|
||||||
|
GtkEntry menuEntry = new GtkEntrySeparator(GtkMenu.this);
|
||||||
|
menuEntries.add(menuEntry);
|
||||||
|
|
||||||
|
createMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
menuEntry.setShortcut(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
/**
|
||||||
|
* Deletes the menu, and unreferences everything in it. ALSO recreates ONLY the menu object.
|
||||||
|
*/
|
||||||
|
void deleteMenu() {
|
||||||
|
if (obliterateInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_native != null) {
|
||||||
|
// have to remove all other menu entries
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
||||||
|
final Entry menuEntry__ = menuEntries.get(i);
|
||||||
|
if (menuEntry__ instanceof GtkEntry) {
|
||||||
|
GtkEntry entry = (GtkEntry) menuEntry__;
|
||||||
|
|
||||||
|
Gobject.g_object_force_floating(entry._native);
|
||||||
|
Gtk.gtk_container_remove(_native, entry._native);
|
||||||
|
}
|
||||||
|
else if (menuEntry__ instanceof GtkMenu) {
|
||||||
|
GtkMenu subMenu = (GtkMenu) menuEntry__;
|
||||||
|
|
||||||
|
Gobject.g_object_force_floating(subMenu.menuEntry._native);
|
||||||
|
Gtk.gtk_container_remove(_native, subMenu.menuEntry._native);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.gtk_widget_destroy(_native);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getParent() != null) {
|
||||||
|
((GtkMenu) getParent()).deleteMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
// makes a new one
|
||||||
|
_native = Gtk.gtk_menu_new();
|
||||||
|
|
||||||
|
// binds sub-menu to entry (if it exists! it does not for the root menu)
|
||||||
|
if (menuEntry != null) {
|
||||||
|
Gtk.gtk_menu_item_set_submenu(menuEntry._native, _native);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
void createMenu() {
|
||||||
|
if (obliterateInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getParent() != null) {
|
||||||
|
((GtkMenu) getParent()).createMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasImages = false;
|
||||||
|
|
||||||
|
// now add back other menu entries
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
||||||
|
final Entry menuEntry__ = menuEntries.get(i);
|
||||||
|
hasImages |= menuEntry__.hasImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
||||||
|
final Entry menuEntry__ = menuEntries.get(i);
|
||||||
|
// the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images
|
||||||
|
if (menuEntry__ instanceof GtkEntry) {
|
||||||
|
GtkEntry entry = (GtkEntry) menuEntry__;
|
||||||
|
entry.setSpacerImage(hasImages);
|
||||||
|
|
||||||
|
// will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu'
|
||||||
|
Gtk.gtk_menu_shell_append(this._native, entry._native);
|
||||||
|
Gobject.g_object_ref_sink(entry._native); // undoes "floating"
|
||||||
|
}
|
||||||
|
else if (menuEntry__ instanceof GtkMenu) {
|
||||||
|
GtkMenu subMenu = (GtkMenu) menuEntry__;
|
||||||
|
|
||||||
|
// will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu'
|
||||||
|
Gtk.gtk_menu_shell_append(this._native, subMenu.menuEntry._native);
|
||||||
|
Gobject.g_object_ref_sink(subMenu.menuEntry._native); // undoes "floating"
|
||||||
|
|
||||||
|
if (subMenu.getParent() != GtkMenu.this) {
|
||||||
|
// we don't want to "createMenu" on our sub-menu that is assigned to us directly, as they are already doing it
|
||||||
|
subMenu.createMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuAdded(_native);
|
||||||
|
Gtk.gtk_widget_show_all(_native);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* must be called on the dispatch thread
|
||||||
|
*
|
||||||
|
* Completely obliterates the menu, no possible way to reconstruct it.
|
||||||
|
*/
|
||||||
|
private
|
||||||
|
void obliterateMenu() {
|
||||||
|
if (_native != null && !obliterateInProgress) {
|
||||||
|
obliterateInProgress = true;
|
||||||
|
|
||||||
|
// have to remove all other menu entries
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
// a copy is made because sub-menus remove themselves from parents when .remove() is called. If we don't
|
||||||
|
// do this, errors will be had because indices don't line up anymore.
|
||||||
|
ArrayList<Entry> menuEntriesCopy = new ArrayList<Entry>(this.menuEntries);
|
||||||
|
|
||||||
|
for (int i = 0, menuEntriesSize = menuEntriesCopy.size(); i < menuEntriesSize; i++) {
|
||||||
|
final Entry menuEntry__ = menuEntriesCopy.get(i);
|
||||||
|
menuEntry__.remove();
|
||||||
|
}
|
||||||
|
this.menuEntries.clear();
|
||||||
|
menuEntriesCopy.clear();
|
||||||
|
|
||||||
|
Gtk.gtk_widget_destroy(_native);
|
||||||
|
}
|
||||||
|
|
||||||
|
obliterateInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a new menu entry, or update one if it already exists
|
||||||
|
*/
|
||||||
|
protected
|
||||||
|
Entry addEntry_(final String menuText, final File imagePath, final Action 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
|
||||||
|
|
||||||
|
if (menuText == null) {
|
||||||
|
throw new NullPointerException("Menu text cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// have to wait for the value
|
||||||
|
final AtomicReference<Entry> value = new AtomicReference<Entry>();
|
||||||
|
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry menuEntry = get(menuText);
|
||||||
|
if (menuEntry == null) {
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
deleteMenu();
|
||||||
|
|
||||||
|
menuEntry = new GtkEntryItem(GtkMenu.this, callback);
|
||||||
|
menuEntry.setText(menuText);
|
||||||
|
menuEntry.setImage(imagePath);
|
||||||
|
menuEntries.add(menuEntry);
|
||||||
|
|
||||||
|
createMenu();
|
||||||
|
} else if (menuEntry instanceof GtkEntryItem) {
|
||||||
|
menuEntry.setText(menuText);
|
||||||
|
menuEntry.setImage(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
value.set(menuEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a new menu entry, or update one if it already exists
|
||||||
|
*/
|
||||||
|
protected
|
||||||
|
Menu addMenu_(final String menuText, final File imagePath) {
|
||||||
|
// 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
|
||||||
|
|
||||||
|
if (menuText == null) {
|
||||||
|
throw new NullPointerException("Menu text cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
final AtomicReference<Menu> value = new AtomicReference<Menu>();
|
||||||
|
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry menuEntry = get(menuText);
|
||||||
|
if (menuEntry == null) {
|
||||||
|
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
|
||||||
|
// To work around this issue, we destroy then recreate the menu every time something is changed.
|
||||||
|
deleteMenu();
|
||||||
|
|
||||||
|
GtkMenu subMenu = new GtkMenu(getSystemTray(), GtkMenu.this);
|
||||||
|
subMenu.setText(menuText);
|
||||||
|
subMenu.setImage(imagePath);
|
||||||
|
|
||||||
|
menuEntries.add(subMenu);
|
||||||
|
|
||||||
|
value.set(subMenu);
|
||||||
|
|
||||||
|
createMenu();
|
||||||
|
} else if (menuEntry instanceof GtkMenu) {
|
||||||
|
menuEntry.setText(menuText);
|
||||||
|
menuEntry.setImage(imagePath);
|
||||||
|
|
||||||
|
value.set(((GtkMenu) menuEntry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// a child will always remove itself from the parent.
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void remove() {
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
GtkMenu parent = (GtkMenu) getParent();
|
||||||
|
|
||||||
|
// have to remove from the parent.menuEntries first
|
||||||
|
for (Iterator<Entry> iterator = parent.menuEntries.iterator(); iterator.hasNext(); ) {
|
||||||
|
final Entry entry = iterator.next();
|
||||||
|
if (entry == GtkMenu.this) {
|
||||||
|
iterator.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleans up the menu
|
||||||
|
// parent.remove__(null);
|
||||||
|
|
||||||
|
// delete all of the children of this submenu (must happen before the menuEntry is removed)
|
||||||
|
obliterateMenu();
|
||||||
|
|
||||||
|
// remove the gtk entry item from our parent menu NATIVE components
|
||||||
|
// NOTE: this will rebuild the parent menu
|
||||||
|
if (menuEntry != null) {
|
||||||
|
menuEntry.remove();
|
||||||
|
} else {
|
||||||
|
// have to rebuild the menu now...
|
||||||
|
parent.deleteMenu();
|
||||||
|
parent.createMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
30
src/dorkbox/systemTray/nativeUI/NativeUI.java
Normal file
30
src/dorkbox/systemTray/nativeUI/NativeUI.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.nativeUI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a System Tray or menu, that will have it's menu rendered via the native subsystem.
|
||||||
|
* <p>
|
||||||
|
* This is does not have as many features as the swing-based UI, however the trade off is that this will always have the native L&F of
|
||||||
|
* the system (with the exception of Windows, whose native menu looks absolutely terrible).
|
||||||
|
* <p>
|
||||||
|
* Noticeable differences that are limitations for the NativeUI only:
|
||||||
|
* - AppIndicator Status entries must be plain text (they are not bold as they are everywhere else).
|
||||||
|
* - MacOS cannot have images in their menu or sub-menu's -- only plain text is possible
|
||||||
|
*/
|
||||||
|
public
|
||||||
|
interface NativeUI
|
||||||
|
{}
|
185
src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java
Normal file
185
src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.SystemTray;
|
||||||
|
import dorkbox.systemTray.linux.jna.AppIndicator;
|
||||||
|
import dorkbox.systemTray.linux.jna.AppIndicatorInstanceStruct;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gobject;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
|
import dorkbox.systemTray.util.ImageUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling all system tray interactions.
|
||||||
|
* specialization for using app indicators in ubuntu unity
|
||||||
|
*
|
||||||
|
* Derived from
|
||||||
|
* Lantern: https://github.com/getlantern/lantern/ Apache 2.0 License Copyright 2010 Brave New Software Project, Inc.
|
||||||
|
*
|
||||||
|
* AppIndicators DO NOT support anything other than plain gtk-menus, because of how they use dbus so no tooltips AND no custom widgets
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* As a result of this decision by Canonical, we have to resort to hacks to get it to do what we want. BY NO MEANS IS THIS PERFECT.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* We still cannot have tooltips, but we *CAN* have custom widgets in the menu (because it's our swing menu now...)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* It would be too much work to re-implement AppIndicators, or even to use LD_PRELOAD + restart service to do what we want.
|
||||||
|
*
|
||||||
|
* As a result, we have some wicked little hacks which are rather effective (but have a small side-effect of very briefly
|
||||||
|
* showing a blank menu)
|
||||||
|
*
|
||||||
|
* // What are AppIndicators?
|
||||||
|
* http://unity.ubuntu.com/projects/appindicators/
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* // Entry-point into appindicators
|
||||||
|
* http://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/services/panel-main.c
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* // The idiocy of appindicators
|
||||||
|
* https://bugs.launchpad.net/screenlets/+bug/522152
|
||||||
|
*
|
||||||
|
* // Code of how the dbus menus work
|
||||||
|
* http://bazaar.launchpad.net/~dbusmenu-team/libdbusmenu/trunk.16.10/view/head:/libdbusmenu-gtk/client.c
|
||||||
|
* https://developer.ubuntu.com/api/devel/ubuntu-12.04/c/dbusmenugtk/index.html
|
||||||
|
*
|
||||||
|
* // more info about trying to put widgets into GTK menus
|
||||||
|
* http://askubuntu.com/questions/16431/putting-an-arbitrary-gtk-widget-into-an-appindicator-indicator
|
||||||
|
*
|
||||||
|
* // possible idea on how to get GTK widgets into GTK menus
|
||||||
|
* https://launchpad.net/ido
|
||||||
|
* http://bazaar.launchpad.net/~canonical-dx-team/ido/trunk/view/head:/src/idoentrymenuitem.c
|
||||||
|
* http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
|
public
|
||||||
|
class _AppIndicatorNativeTray extends GtkMenu {
|
||||||
|
private volatile AppIndicatorInstanceStruct appIndicator;
|
||||||
|
private boolean isActive = false;
|
||||||
|
|
||||||
|
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
|
||||||
|
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
||||||
|
|
||||||
|
// is the system tray visible or not.
|
||||||
|
private volatile boolean visible = true;
|
||||||
|
|
||||||
|
// appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus)
|
||||||
|
// they ALSO do not support tooltips, so we cater to the lowest common denominator
|
||||||
|
// trayIcon.setToolTip("app name");
|
||||||
|
|
||||||
|
public
|
||||||
|
_AppIndicatorNativeTray(final SystemTray systemTray) {
|
||||||
|
super(systemTray, null);
|
||||||
|
|
||||||
|
Gtk.startGui();
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
// we initialize with a blank image
|
||||||
|
File image = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE);
|
||||||
|
String id = System.nanoTime() + "DBST";
|
||||||
|
appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Gtk.waitForStartup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final
|
||||||
|
void shutdown() {
|
||||||
|
if (!shuttingDown.getAndSet(true)) {
|
||||||
|
// must happen asap, so our hook properly notices we are in shutdown mode
|
||||||
|
final AppIndicatorInstanceStruct savedAppIndicator = appIndicator;
|
||||||
|
appIndicator = null;
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
// STATUS_PASSIVE hides the indicator
|
||||||
|
AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE);
|
||||||
|
Pointer p = savedAppIndicator.getPointer();
|
||||||
|
Gobject.g_object_unref(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
super.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
boolean hasImage() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
AppIndicator.app_indicator_set_icon(appIndicator, imageFile.getAbsolutePath());
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
isActive = true;
|
||||||
|
|
||||||
|
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setEnabled(final boolean setEnabled) {
|
||||||
|
visible = !setEnabled;
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
if (visible && !setEnabled) {
|
||||||
|
// STATUS_PASSIVE hides the indicator
|
||||||
|
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE);
|
||||||
|
}
|
||||||
|
else if (!visible && setEnabled) {
|
||||||
|
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MUST BE AFTER THE ITEM IS ADDED/CHANGED from the menu
|
||||||
|
*/
|
||||||
|
protected final
|
||||||
|
void onMenuAdded(final Pointer menu) {
|
||||||
|
// see: https://code.launchpad.net/~mterry/libappindicator/fix-menu-leak/+merge/53247
|
||||||
|
AppIndicator.app_indicator_set_menu(appIndicator, menu);
|
||||||
|
}
|
||||||
|
}
|
125
src/dorkbox/systemTray/nativeUI/_AwtTray.java
Normal file
125
src/dorkbox/systemTray/nativeUI/_AwtTray.java
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.awt.AWTException;
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.PopupMenu;
|
||||||
|
import java.awt.SystemTray;
|
||||||
|
import java.awt.TrayIcon;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling all system tray interaction, via AWT.
|
||||||
|
*
|
||||||
|
* It doesn't work well on linux. See bugs:
|
||||||
|
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936
|
||||||
|
* 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 _AwtTray extends AwtMenu {
|
||||||
|
private volatile SystemTray tray;
|
||||||
|
private volatile TrayIcon trayIcon;
|
||||||
|
|
||||||
|
// is the system tray visible or not.
|
||||||
|
private volatile boolean visible = true;
|
||||||
|
|
||||||
|
// Called in the EDT
|
||||||
|
public
|
||||||
|
_AwtTray(final dorkbox.systemTray.SystemTray systemTray) {
|
||||||
|
super(systemTray, null, new PopupMenu());
|
||||||
|
|
||||||
|
if (!SystemTray.isSupported()) {
|
||||||
|
throw new RuntimeException("System Tray is not supported in this configuration! Please write an issue and include your OS " +
|
||||||
|
"type and configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
_AwtTray.this.tray = SystemTray.getSystemTray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public
|
||||||
|
void shutdown() {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
removeAll();
|
||||||
|
remove();
|
||||||
|
|
||||||
|
tray.remove(trayIcon);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public
|
||||||
|
void setImage_(final File iconFile) {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
if (trayIcon == null) {
|
||||||
|
// here we init. everything
|
||||||
|
trayIcon = new TrayIcon(trayImage);
|
||||||
|
|
||||||
|
// appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus)
|
||||||
|
// they ALSO do not support tooltips, so we cater to the lowest common denominator
|
||||||
|
// trayIcon.setToolTip("app name");
|
||||||
|
|
||||||
|
trayIcon.setPopupMenu((PopupMenu) _native);
|
||||||
|
|
||||||
|
try {
|
||||||
|
tray.add(trayIcon);
|
||||||
|
} catch (AWTException e) {
|
||||||
|
dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trayIcon.setImage(trayImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean setEnabled) {
|
||||||
|
visible = !setEnabled;
|
||||||
|
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
if (visible && !setEnabled) {
|
||||||
|
tray.remove(trayIcon);
|
||||||
|
}
|
||||||
|
else if (!visible && setEnabled) {
|
||||||
|
try {
|
||||||
|
tray.add(trayIcon);
|
||||||
|
} catch (AWTException e) {
|
||||||
|
dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
186
src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java
Normal file
186
src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.nativeUI;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import com.sun.jna.NativeLong;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.SystemTray;
|
||||||
|
import dorkbox.systemTray.linux.jna.GEventCallback;
|
||||||
|
import dorkbox.systemTray.linux.jna.GdkEventButton;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gobject;
|
||||||
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling all system tray interactions via GTK.
|
||||||
|
* <p/>
|
||||||
|
* This is the "old" way to do it, and does not work with some newer desktop environments.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
|
public
|
||||||
|
class _GtkStatusIconNativeTray extends GtkMenu {
|
||||||
|
private volatile Pointer trayIcon;
|
||||||
|
|
||||||
|
// http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c
|
||||||
|
// https://github.com/djdeath/glib/blob/master/gobject/gobject.c
|
||||||
|
|
||||||
|
// have to save these in a field to prevent GC on the objects (since they go out-of-scope from java)
|
||||||
|
private final List<Object> gtkCallbacks = new ArrayList<Object>();
|
||||||
|
|
||||||
|
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
|
||||||
|
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
||||||
|
|
||||||
|
private volatile boolean isActive = false;
|
||||||
|
|
||||||
|
// is the system tray visible or not.
|
||||||
|
private volatile boolean visible = true;
|
||||||
|
|
||||||
|
// called on the EDT
|
||||||
|
public
|
||||||
|
_GtkStatusIconNativeTray(final SystemTray systemTray) {
|
||||||
|
super(systemTray, null);
|
||||||
|
|
||||||
|
// appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus)
|
||||||
|
// they ALSO do not support tooltips, so we cater to the lowest common denominator
|
||||||
|
// trayIcon.setToolTip("app name");
|
||||||
|
|
||||||
|
Gtk.startGui();
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
trayIcon = Gtk.gtk_status_icon_new();
|
||||||
|
|
||||||
|
final GEventCallback gtkCallback = new GEventCallback() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void callback(Pointer notUsed, final GdkEventButton event) {
|
||||||
|
// show the swing menu on the EDT
|
||||||
|
// BUTTON_PRESS only (any mouse click)
|
||||||
|
if (event.type == 4) {
|
||||||
|
Gtk.gtk_menu_popup(_native, null, null, Gtk.gtk_status_icon_position_menu, trayIcon, 0, event.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
final NativeLong button_press_event = Gobject.g_signal_connect_object(trayIcon, "button_press_event",
|
||||||
|
gtkCallback, null, 0);
|
||||||
|
|
||||||
|
// have to do this to prevent GC on these objects
|
||||||
|
gtkCallbacks.add(gtkCallback);
|
||||||
|
gtkCallbacks.add(button_press_event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Gtk.waitForStartup();
|
||||||
|
|
||||||
|
// we have to be able to set our title, otherwise the gnome-shell extension WILL NOT work
|
||||||
|
Gtk.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
|
||||||
|
|
||||||
|
// ... so, bizzaro things going on here. These errors DO NOT happen if JavaFX is dispatching the events.
|
||||||
|
// BUT this is REQUIRED when running JavaFX. For unknown reasons, the title isn't pushed to GTK, so our
|
||||||
|
// gnome-shell extension cannot see our tray icon -- so naturally, it won't move it to the "top" area and
|
||||||
|
// we appear broken.
|
||||||
|
if (SystemTray.isJavaFxLoaded) {
|
||||||
|
Gtk.gtk_status_icon_set_name(trayIcon, "SystemTray");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldRepeatedlyAccessedInMethod")
|
||||||
|
public final
|
||||||
|
void shutdown() {
|
||||||
|
if (!shuttingDown.getAndSet(true)) {
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
// this hides the indicator
|
||||||
|
Gtk.gtk_status_icon_set_visible(trayIcon, false);
|
||||||
|
Gobject.g_object_unref(trayIcon);
|
||||||
|
|
||||||
|
// mark for GC
|
||||||
|
trayIcon = null;
|
||||||
|
gtkCallbacks.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
super.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
boolean hasImage() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage_(final File iconFile) {
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
Gtk.gtk_status_icon_set_from_file(trayIcon, iconFile.getAbsolutePath());
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
isActive = true;
|
||||||
|
Gtk.gtk_status_icon_set_visible(trayIcon, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setEnabled(final boolean setEnabled) {
|
||||||
|
visible = !setEnabled;
|
||||||
|
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
if (visible && !setEnabled) {
|
||||||
|
Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled);
|
||||||
|
} else if (!visible && setEnabled) {
|
||||||
|
Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,317 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2014 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.swing;
|
|
||||||
|
|
||||||
import java.awt.event.KeyEvent;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
import javax.swing.JComponent;
|
|
||||||
import javax.swing.JMenuItem;
|
|
||||||
|
|
||||||
import dorkbox.systemTray.Entry;
|
|
||||||
import dorkbox.systemTray.Menu;
|
|
||||||
import dorkbox.systemTray.util.ImageUtils;
|
|
||||||
|
|
||||||
abstract
|
|
||||||
class EntryImpl implements Entry {
|
|
||||||
private final int id = MenuImpl.MENU_ID_COUNTER.getAndIncrement();
|
|
||||||
|
|
||||||
private final MenuImpl parent;
|
|
||||||
final JComponent _native;
|
|
||||||
|
|
||||||
// this have to be volatile, because they can be changed from any thread
|
|
||||||
private volatile String text;
|
|
||||||
|
|
||||||
// this is ALWAYS called on the EDT.
|
|
||||||
EntryImpl(final MenuImpl parent, final JComponent menuItem) {
|
|
||||||
this.parent = parent;
|
|
||||||
this._native = menuItem;
|
|
||||||
|
|
||||||
parent._native.add(menuItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
Menu getParent() {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* must always be called in the EDT thread
|
|
||||||
*/
|
|
||||||
abstract
|
|
||||||
void renderText(final String text);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not always called on the EDT thread
|
|
||||||
*/
|
|
||||||
abstract
|
|
||||||
void setImage_(final File imageFile);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables, or disables the sub-menu entry.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setEnabled(final boolean enabled) {
|
|
||||||
_native.setEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setShortcut(final char key) {
|
|
||||||
if (_native instanceof JMenuItem) {
|
|
||||||
// yikes...
|
|
||||||
final int vKey = getVkKey(key);
|
|
||||||
|
|
||||||
parent.dispatch(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
((JMenuItem) _native).setMnemonic(vKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
String getText() {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setText(final String newText) {
|
|
||||||
this.text = newText;
|
|
||||||
|
|
||||||
parent.dispatch(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
renderText(newText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setImage(final File imageFile) {
|
|
||||||
if (imageFile == null) {
|
|
||||||
setImage_(null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void setImage(final String imagePath) {
|
|
||||||
if (imagePath == null) {
|
|
||||||
setImage_(null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void setImage(final URL imageUrl) {
|
|
||||||
if (imageUrl == null) {
|
|
||||||
setImage_(null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void setImage(final String cacheName, final InputStream imageStream) {
|
|
||||||
if (imageStream == null) {
|
|
||||||
setImage_(null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void setImage(final InputStream imageStream) {
|
|
||||||
if (imageStream == null) {
|
|
||||||
setImage_(null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void remove() {
|
|
||||||
parent.dispatchAndWait(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
removePrivate();
|
|
||||||
parent._native.remove(_native);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// called when this item is removed. Necessary to cleanup/remove itself
|
|
||||||
abstract
|
|
||||||
void removePrivate();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
int hashCode() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
boolean equals(Object obj) {
|
|
||||||
if (this == obj) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
EntryImpl other = (EntryImpl) obj;
|
|
||||||
return this.id == other.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a key character into it's corresponding VK entry
|
|
||||||
*/
|
|
||||||
static
|
|
||||||
int getVkKey(final char key) {
|
|
||||||
switch (key) {
|
|
||||||
case 0x08: return KeyEvent.VK_BACK_SPACE;
|
|
||||||
case 0x09: return KeyEvent.VK_TAB;
|
|
||||||
case 0x0a: return KeyEvent.VK_ENTER;
|
|
||||||
case 0x1B: return KeyEvent.VK_ESCAPE;
|
|
||||||
case 0x20AC: return KeyEvent.VK_EURO_SIGN;
|
|
||||||
case 0x20: return KeyEvent.VK_SPACE;
|
|
||||||
case 0x21: return KeyEvent.VK_EXCLAMATION_MARK;
|
|
||||||
case 0x22: return KeyEvent.VK_QUOTEDBL;
|
|
||||||
case 0x23: return KeyEvent.VK_NUMBER_SIGN;
|
|
||||||
case 0x24: return KeyEvent.VK_DOLLAR;
|
|
||||||
case 0x26: return KeyEvent.VK_AMPERSAND;
|
|
||||||
case 0x27: return KeyEvent.VK_QUOTE;
|
|
||||||
case 0x28: return KeyEvent.VK_LEFT_PARENTHESIS;
|
|
||||||
case 0x29: return KeyEvent.VK_RIGHT_PARENTHESIS;
|
|
||||||
case 0x2A: return KeyEvent.VK_ASTERISK;
|
|
||||||
case 0x2B: return KeyEvent.VK_PLUS;
|
|
||||||
case 0x2C: return KeyEvent.VK_COMMA;
|
|
||||||
case 0x2D: return KeyEvent.VK_MINUS;
|
|
||||||
case 0x2E: return KeyEvent.VK_PERIOD;
|
|
||||||
case 0x2F: return KeyEvent.VK_SLASH;
|
|
||||||
case 0x30: return KeyEvent.VK_0;
|
|
||||||
case 0x31: return KeyEvent.VK_1;
|
|
||||||
case 0x32: return KeyEvent.VK_2;
|
|
||||||
case 0x33: return KeyEvent.VK_3;
|
|
||||||
case 0x34: return KeyEvent.VK_4;
|
|
||||||
case 0x35: return KeyEvent.VK_5;
|
|
||||||
case 0x36: return KeyEvent.VK_6;
|
|
||||||
case 0x37: return KeyEvent.VK_7;
|
|
||||||
case 0x38: return KeyEvent.VK_8;
|
|
||||||
case 0x39: return KeyEvent.VK_9;
|
|
||||||
case 0x3A: return KeyEvent.VK_COLON;
|
|
||||||
case 0x3B: return KeyEvent.VK_SEMICOLON;
|
|
||||||
case 0x3C: return KeyEvent.VK_LESS;
|
|
||||||
case 0x3D: return KeyEvent.VK_EQUALS;
|
|
||||||
case 0x3E: return KeyEvent.VK_GREATER;
|
|
||||||
case 0x40: return KeyEvent.VK_AT;
|
|
||||||
case 0x41: return KeyEvent.VK_A;
|
|
||||||
case 0x42: return KeyEvent.VK_B;
|
|
||||||
case 0x43: return KeyEvent.VK_C;
|
|
||||||
case 0x44: return KeyEvent.VK_D;
|
|
||||||
case 0x45: return KeyEvent.VK_E;
|
|
||||||
case 0x46: return KeyEvent.VK_F;
|
|
||||||
case 0x47: return KeyEvent.VK_G;
|
|
||||||
case 0x48: return KeyEvent.VK_H;
|
|
||||||
case 0x49: return KeyEvent.VK_I;
|
|
||||||
case 0x4A: return KeyEvent.VK_J;
|
|
||||||
case 0x4B: return KeyEvent.VK_K;
|
|
||||||
case 0x4C: return KeyEvent.VK_L;
|
|
||||||
case 0x4D: return KeyEvent.VK_M;
|
|
||||||
case 0x4E: return KeyEvent.VK_N;
|
|
||||||
case 0x4F: return KeyEvent.VK_O;
|
|
||||||
case 0x50: return KeyEvent.VK_P;
|
|
||||||
case 0x51: return KeyEvent.VK_Q;
|
|
||||||
case 0x52: return KeyEvent.VK_R;
|
|
||||||
case 0x53: return KeyEvent.VK_S;
|
|
||||||
case 0x54: return KeyEvent.VK_T;
|
|
||||||
case 0x55: return KeyEvent.VK_U;
|
|
||||||
case 0x56: return KeyEvent.VK_V;
|
|
||||||
case 0x57: return KeyEvent.VK_W;
|
|
||||||
case 0x58: return KeyEvent.VK_X;
|
|
||||||
case 0x59: return KeyEvent.VK_Y;
|
|
||||||
case 0x5A: return KeyEvent.VK_Z;
|
|
||||||
case 0x5B: return KeyEvent.VK_OPEN_BRACKET;
|
|
||||||
case 0x5C: return KeyEvent.VK_BACK_SLASH;
|
|
||||||
case 0x5D: return KeyEvent.VK_CLOSE_BRACKET;
|
|
||||||
case 0x5E: return KeyEvent.VK_CIRCUMFLEX;
|
|
||||||
case 0x5F: return KeyEvent.VK_UNDERSCORE;
|
|
||||||
case 0x60: return KeyEvent.VK_BACK_QUOTE;
|
|
||||||
case 0x61: return KeyEvent.VK_A;
|
|
||||||
case 0x62: return KeyEvent.VK_B;
|
|
||||||
case 0x63: return KeyEvent.VK_C;
|
|
||||||
case 0x64: return KeyEvent.VK_D;
|
|
||||||
case 0x65: return KeyEvent.VK_E;
|
|
||||||
case 0x66: return KeyEvent.VK_F;
|
|
||||||
case 0x67: return KeyEvent.VK_G;
|
|
||||||
case 0x68: return KeyEvent.VK_H;
|
|
||||||
case 0x69: return KeyEvent.VK_I;
|
|
||||||
case 0x6A: return KeyEvent.VK_J;
|
|
||||||
case 0x6B: return KeyEvent.VK_K;
|
|
||||||
case 0x6C: return KeyEvent.VK_L;
|
|
||||||
case 0x6D: return KeyEvent.VK_M;
|
|
||||||
case 0x6E: return KeyEvent.VK_N;
|
|
||||||
case 0x6F: return KeyEvent.VK_O;
|
|
||||||
case 0x70: return KeyEvent.VK_P;
|
|
||||||
case 0x71: return KeyEvent.VK_Q;
|
|
||||||
case 0x72: return KeyEvent.VK_R;
|
|
||||||
case 0x73: return KeyEvent.VK_S;
|
|
||||||
case 0x74: return KeyEvent.VK_T;
|
|
||||||
case 0x75: return KeyEvent.VK_U;
|
|
||||||
case 0x76: return KeyEvent.VK_V;
|
|
||||||
case 0x77: return KeyEvent.VK_W;
|
|
||||||
case 0x78: return KeyEvent.VK_X;
|
|
||||||
case 0x79: return KeyEvent.VK_Y;
|
|
||||||
case 0x7A: return KeyEvent.VK_Z;
|
|
||||||
case 0x7B: return KeyEvent.VK_BRACELEFT;
|
|
||||||
case 0x7D: return KeyEvent.VK_BRACERIGHT;
|
|
||||||
case 0x7F: return KeyEvent.VK_DELETE;
|
|
||||||
case 0xA1: return KeyEvent.VK_INVERTED_EXCLAMATION_MARK;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.Insets;
|
import java.awt.Insets;
|
||||||
|
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.Insets;
|
import java.awt.Insets;
|
||||||
|
|
208
src/dorkbox/systemTray/swingUI/SwingEntry.java
Normal file
208
src/dorkbox/systemTray/swingUI/SwingEntry.java
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.swingUI;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JMenuItem;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Entry;
|
||||||
|
import dorkbox.systemTray.Menu;
|
||||||
|
import dorkbox.systemTray.util.ImageUtils;
|
||||||
|
import dorkbox.systemTray.util.MenuBase;
|
||||||
|
import dorkbox.systemTray.util.SystemTrayFixes;
|
||||||
|
|
||||||
|
abstract
|
||||||
|
class SwingEntry implements Entry, SwingUI {
|
||||||
|
private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement();
|
||||||
|
|
||||||
|
private final SwingMenu parent;
|
||||||
|
final JComponent _native;
|
||||||
|
|
||||||
|
// this have to be volatile, because they can be changed from any thread
|
||||||
|
private volatile String text;
|
||||||
|
|
||||||
|
// this is ALWAYS called on the EDT.
|
||||||
|
SwingEntry(final SwingMenu parent, final JComponent menuItem) {
|
||||||
|
this.parent = parent;
|
||||||
|
this._native = menuItem;
|
||||||
|
|
||||||
|
parent._native.add(menuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
Menu getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* must always be called in the EDT thread
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void renderText(final String text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not always called on the EDT thread
|
||||||
|
*/
|
||||||
|
abstract
|
||||||
|
void setImage_(final File imageFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables, or disables the sub-menu entry.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
_native.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
if (_native instanceof JMenuItem) {
|
||||||
|
// yikes...
|
||||||
|
final int vKey = SystemTrayFixes.getVirtualKey(key);
|
||||||
|
|
||||||
|
parent.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
((JMenuItem) _native).setMnemonic(vKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setText(final String newText) {
|
||||||
|
this.text = newText;
|
||||||
|
|
||||||
|
parent.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
renderText(newText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setImage(final File imageFile) {
|
||||||
|
if (imageFile == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final String imagePath) {
|
||||||
|
if (imagePath == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final URL imageUrl) {
|
||||||
|
if (imageUrl == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final String cacheName, final InputStream imageStream) {
|
||||||
|
if (imageStream == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setImage(final InputStream imageStream) {
|
||||||
|
if (imageStream == null) {
|
||||||
|
setImage_(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void remove() {
|
||||||
|
parent.dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
removePrivate();
|
||||||
|
parent._native.remove(_native);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// called when this item is removed. Necessary to cleanup/remove itself
|
||||||
|
abstract
|
||||||
|
void removePrivate();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
int hashCode() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SwingEntry other = (SwingEntry) obj;
|
||||||
|
return this.id == other.id;
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
@ -25,7 +25,7 @@ import javax.swing.JMenuItem;
|
|||||||
import dorkbox.systemTray.Action;
|
import dorkbox.systemTray.Action;
|
||||||
import dorkbox.util.SwingUtil;
|
import dorkbox.util.SwingUtil;
|
||||||
|
|
||||||
class EntryItem extends EntryImpl {
|
class SwingEntryItem extends SwingEntry {
|
||||||
|
|
||||||
private final ActionListener swingCallback;
|
private final ActionListener swingCallback;
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class EntryItem extends EntryImpl {
|
|||||||
private volatile Action callback;
|
private volatile Action callback;
|
||||||
|
|
||||||
// this is ALWAYS called on the EDT.
|
// this is ALWAYS called on the EDT.
|
||||||
EntryItem(final MenuImpl parent, final Action callback) {
|
SwingEntryItem(final SwingMenu parent, final Action callback) {
|
||||||
super(parent, new AdjustedJMenuItem());
|
super(parent, new AdjustedJMenuItem());
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@ -21,10 +21,10 @@ import javax.swing.JSeparator;
|
|||||||
|
|
||||||
import dorkbox.systemTray.Action;
|
import dorkbox.systemTray.Action;
|
||||||
|
|
||||||
class EntrySeparator extends EntryImpl implements dorkbox.systemTray.Separator {
|
class SwingEntrySeparator extends SwingEntry implements dorkbox.systemTray.Separator {
|
||||||
|
|
||||||
// this is ALWAYS called on the EDT.
|
// this is ALWAYS called on the EDT.
|
||||||
EntrySeparator(final MenuImpl parent) {
|
SwingEntrySeparator(final SwingMenu parent) {
|
||||||
super(parent, new JSeparator(JSeparator.HORIZONTAL));
|
super(parent, new JSeparator(JSeparator.HORIZONTAL));
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -23,10 +23,10 @@ import javax.swing.JMenuItem;
|
|||||||
import dorkbox.systemTray.Action;
|
import dorkbox.systemTray.Action;
|
||||||
import dorkbox.systemTray.Status;
|
import dorkbox.systemTray.Status;
|
||||||
|
|
||||||
class EntryStatus extends EntryImpl implements Status {
|
class SwingEntryStatus extends SwingEntry implements Status {
|
||||||
|
|
||||||
// this is ALWAYS called on the EDT.
|
// this is ALWAYS called on the EDT.
|
||||||
EntryStatus(final MenuImpl parent, final String label) {
|
SwingEntryStatus(final SwingMenu parent, final String label) {
|
||||||
super(parent, new JMenuItem());
|
super(parent, new JMenuItem());
|
||||||
setText(label);
|
setText(label);
|
||||||
}
|
}
|
||||||
@ -34,11 +34,12 @@ class EntryStatus extends EntryImpl implements Status {
|
|||||||
// called in the EDT thread
|
// called in the EDT thread
|
||||||
@Override
|
@Override
|
||||||
void renderText(final String text) {
|
void renderText(final String text) {
|
||||||
((JMenuItem) _native).setText(text);
|
|
||||||
Font font = _native.getFont();
|
Font font = _native.getFont();
|
||||||
Font font1 = font.deriveFont(Font.BOLD);
|
Font font1 = font.deriveFont(Font.BOLD);
|
||||||
_native.setFont(font1);
|
_native.setFont(font1);
|
||||||
|
|
||||||
|
((JMenuItem) _native).setText(text);
|
||||||
|
|
||||||
// this makes sure it can't be selected
|
// this makes sure it can't be selected
|
||||||
_native.setEnabled(false);
|
_native.setEnabled(false);
|
||||||
}
|
}
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@ -22,10 +22,10 @@ import javax.swing.JComponent;
|
|||||||
import dorkbox.systemTray.Action;
|
import dorkbox.systemTray.Action;
|
||||||
|
|
||||||
// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however.
|
// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however.
|
||||||
class EntryWidget extends EntryImpl implements dorkbox.systemTray.Separator {
|
class SwingEntryWidget extends SwingEntry implements dorkbox.systemTray.Separator {
|
||||||
|
|
||||||
// this is ALWAYS called on the EDT.
|
// this is ALWAYS called on the EDT.
|
||||||
EntryWidget(final MenuImpl parent, JComponent widget) {
|
SwingEntryWidget(final SwingMenu parent, JComponent widget) {
|
||||||
super(parent, widget);
|
super(parent, widget);
|
||||||
|
|
||||||
_native.setEnabled(true);
|
_native.setEnabled(true);
|
349
src/dorkbox/systemTray/swingUI/SwingMenu.java
Normal file
349
src/dorkbox/systemTray/swingUI/SwingMenu.java
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 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.swingUI;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JMenuItem;
|
||||||
|
|
||||||
|
import dorkbox.systemTray.Action;
|
||||||
|
import dorkbox.systemTray.Entry;
|
||||||
|
import dorkbox.systemTray.Menu;
|
||||||
|
import dorkbox.systemTray.Status;
|
||||||
|
import dorkbox.systemTray.SystemTray;
|
||||||
|
import dorkbox.systemTray.util.MenuBase;
|
||||||
|
import dorkbox.systemTray.util.SystemTrayFixes;
|
||||||
|
import dorkbox.util.SwingUtil;
|
||||||
|
|
||||||
|
// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both
|
||||||
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
||||||
|
class SwingMenu extends MenuBase implements SwingUI {
|
||||||
|
|
||||||
|
// sub-menu = AdjustedJMenu
|
||||||
|
// systemtray = TrayPopup
|
||||||
|
volatile JComponent _native;
|
||||||
|
|
||||||
|
// this have to be volatile, because they can be changed from any thread
|
||||||
|
private volatile String text;
|
||||||
|
private volatile boolean hasLegitIcon = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called in the EDT
|
||||||
|
*
|
||||||
|
* @param systemTray the system tray (which is the object that sits in the system tray)
|
||||||
|
* @param parent the parent of this menu, null if the parent is the system tray
|
||||||
|
* @param _native the native element that represents this menu
|
||||||
|
*/
|
||||||
|
SwingMenu(final SystemTray systemTray, final Menu parent, final JComponent _native) {
|
||||||
|
super(systemTray, parent);
|
||||||
|
this._native = _native;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final
|
||||||
|
void dispatch(final Runnable runnable) {
|
||||||
|
// this will properly check if we are running on the EDT
|
||||||
|
SwingUtil.invokeLater(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final
|
||||||
|
void dispatchAndWait(final Runnable runnable) {
|
||||||
|
// this will properly check if we are running on the EDT
|
||||||
|
try {
|
||||||
|
SwingUtil.invokeAndWait(runnable);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SystemTray.logger.error("Error processing event on the dispatch thread.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// always called in the EDT
|
||||||
|
protected final
|
||||||
|
void renderText(final String text) {
|
||||||
|
((JMenuItem) _native).setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setText(final String newText) {
|
||||||
|
text = newText;
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
renderText(newText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a new menu entry, or update one if it already exists
|
||||||
|
* NOT ALWAYS CALLED ON EDT
|
||||||
|
*/
|
||||||
|
protected final
|
||||||
|
Entry addEntry_(final String menuText, final File imagePath, final Action callback) {
|
||||||
|
if (menuText == null) {
|
||||||
|
throw new NullPointerException("Menu text cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
final AtomicReference<Entry> value = new AtomicReference<Entry>();
|
||||||
|
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry entry = get(menuText);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
// must always be called on the EDT
|
||||||
|
entry = new SwingEntryItem(SwingMenu.this, callback);
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
|
||||||
|
menuEntries.add(entry);
|
||||||
|
} else if (entry instanceof SwingEntryItem) {
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
value.set(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a new sub-menu entry, or update one if it already exists
|
||||||
|
* NOT ALWAYS CALLED ON EDT
|
||||||
|
*/
|
||||||
|
protected final
|
||||||
|
Menu addMenu_(final String menuText, final File imagePath) {
|
||||||
|
if (menuText == null) {
|
||||||
|
throw new NullPointerException("Menu text cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
final AtomicReference<Menu> value = new AtomicReference<Menu>();
|
||||||
|
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry entry = get(menuText);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
// must always be called on the EDT
|
||||||
|
entry = new SwingMenu(getSystemTray(), SwingMenu.this, new AdjustedJMenu());
|
||||||
|
_native.add(((SwingMenu) entry)._native); // have to add it separately
|
||||||
|
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
value.set((Menu) entry);
|
||||||
|
|
||||||
|
} else if (entry instanceof SwingMenu) {
|
||||||
|
entry.setText(menuText);
|
||||||
|
entry.setImage(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
menuEntries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
|
public
|
||||||
|
void setImage_(final File imageFile) {
|
||||||
|
hasLegitIcon = imageFile != null;
|
||||||
|
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
if (imageFile != null) {
|
||||||
|
ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath());
|
||||||
|
((JMenuItem) _native).setIcon(origIcon);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
((JMenuItem) _native).setIcon(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
boolean hasImage() {
|
||||||
|
return hasLegitIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void setEnabled(final boolean enabled) {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
_native.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOT ALWAYS CALLED ON EDT
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void addSeparator() {
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
Entry entry = new SwingEntrySeparator(SwingMenu.this);
|
||||||
|
menuEntries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however.
|
||||||
|
// public
|
||||||
|
// Entry addWidget(final JComponent widget) {
|
||||||
|
// if (widget == null) {
|
||||||
|
// throw new NullPointerException("Widget cannot be null");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final AtomicReference<Entry> value = new AtomicReference<Entry>();
|
||||||
|
//
|
||||||
|
// dispatchAndWait(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public
|
||||||
|
// void run() {
|
||||||
|
// synchronized (menuEntries) {
|
||||||
|
// // must always be called on the EDT
|
||||||
|
// Entry entry = new SwingEntryWidget(SwingMenu.this, widget);
|
||||||
|
// value.set(entry);
|
||||||
|
// menuEntries.add(entry);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// return value.get();
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can access this
|
||||||
|
public final
|
||||||
|
void setStatus(final String statusText) {
|
||||||
|
final SwingMenu _this = this;
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
synchronized (menuEntries) {
|
||||||
|
// status is ALWAYS at 0 index...
|
||||||
|
SwingEntry menuEntry = null;
|
||||||
|
if (!menuEntries.isEmpty()) {
|
||||||
|
menuEntry = (SwingEntry) menuEntries.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuEntry instanceof Status) {
|
||||||
|
// set the text or delete...
|
||||||
|
|
||||||
|
if (statusText == null) {
|
||||||
|
// delete
|
||||||
|
remove(menuEntry);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// set text
|
||||||
|
menuEntry.setText(statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// create a new one
|
||||||
|
menuEntry = new SwingEntryStatus(_this, statusText);
|
||||||
|
// status is ALWAYS at 0 index...
|
||||||
|
menuEntries.add(0, menuEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void setShortcut(final char key) {
|
||||||
|
if (_native instanceof JMenuItem) {
|
||||||
|
// yikes...
|
||||||
|
final int vKey = SystemTrayFixes.getVirtualKey(key);
|
||||||
|
dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
((JMenuItem) _native).setMnemonic(vKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
|
void remove() {
|
||||||
|
dispatchAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
_native.setVisible(false);
|
||||||
|
if (_native instanceof TrayPopup) {
|
||||||
|
((TrayPopup) _native).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
SwingMenu parent = (SwingMenu) getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent._native.remove(_native);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
30
src/dorkbox/systemTray/swingUI/SwingUI.java
Normal file
30
src/dorkbox/systemTray/swingUI/SwingUI.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.swingUI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a System Tray or menu, that will have it's menu rendered via Swing.
|
||||||
|
* <p>
|
||||||
|
* This has the most standard L&F across all systems (as all systems will render this menu the exact same way), however the tradeoff is that
|
||||||
|
* one loses the native L&F of the system (with the exception of Windows, whose native menu looks absolutely terrible).
|
||||||
|
* <p>
|
||||||
|
* Noticeable differences that are limitations for the NativeUI only:
|
||||||
|
* - AppIndicator Status entries must be plain text (they are not bold as they are everywhere else).
|
||||||
|
* - MacOS cannot have images in their menu or sub-menu's -- only plain text is possible
|
||||||
|
*/
|
||||||
|
public
|
||||||
|
interface SwingUI
|
||||||
|
{}
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.Frame;
|
import java.awt.Frame;
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.MouseInfo;
|
import java.awt.MouseInfo;
|
||||||
import java.awt.Point;
|
import java.awt.Point;
|
||||||
@ -79,8 +79,9 @@ import dorkbox.util.SwingUtil;
|
|||||||
* http://bazaar.launchpad.net/~canonical-dx-team/ido/trunk/view/head:/src/idoentrymenuitem.c
|
* http://bazaar.launchpad.net/~canonical-dx-team/ido/trunk/view/head:/src/idoentrymenuitem.c
|
||||||
* http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files
|
* http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
public
|
public
|
||||||
class _AppIndicatorTray extends MenuImpl {
|
class _AppIndicatorTray extends SwingMenu {
|
||||||
private volatile AppIndicatorInstanceStruct appIndicator;
|
private volatile AppIndicatorInstanceStruct appIndicator;
|
||||||
private boolean isActive = false;
|
private boolean isActive = false;
|
||||||
private final Runnable popupRunnable;
|
private final Runnable popupRunnable;
|
||||||
@ -89,7 +90,9 @@ class _AppIndicatorTray extends MenuImpl {
|
|||||||
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
||||||
|
|
||||||
// necessary to prevent GC on these objects
|
// necessary to prevent GC on these objects
|
||||||
|
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||||
private NativeLong nativeLong;
|
private NativeLong nativeLong;
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
private GEventCallback gtkCallback;
|
private GEventCallback gtkCallback;
|
||||||
|
|
||||||
|
|
||||||
@ -107,12 +110,6 @@ class _AppIndicatorTray extends MenuImpl {
|
|||||||
_AppIndicatorTray(final SystemTray systemTray) {
|
_AppIndicatorTray(final SystemTray systemTray) {
|
||||||
super(systemTray,null, new TrayPopup());
|
super(systemTray,null, new TrayPopup());
|
||||||
|
|
||||||
if (SystemTray.FORCE_TRAY_TYPE != 0 && SystemTray.FORCE_TRAY_TYPE != SystemTray.TYPE_APP_INDICATOR) {
|
|
||||||
throw new IllegalArgumentException("Unable to start AppIndicator Tray if 'SystemTray.FORCE_TRAY_TYPE' does not match");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageUtils.determineIconSize();
|
|
||||||
|
|
||||||
TrayPopup popupMenu = (TrayPopup) _native;
|
TrayPopup popupMenu = (TrayPopup) _native;
|
||||||
popupMenu.pack();
|
popupMenu.pack();
|
||||||
popupMenu.setFocusable(true);
|
popupMenu.setFocusable(true);
|
||||||
@ -198,11 +195,11 @@ class _AppIndicatorTray extends MenuImpl {
|
|||||||
AppIndicator.app_indicator_set_menu(appIndicator, dummyMenu);
|
AppIndicator.app_indicator_set_menu(appIndicator, dummyMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
public final
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
if (!shuttingDown.getAndSet(true)) {
|
if (!shuttingDown.getAndSet(true)) {
|
||||||
// must happen asap, so our hook properly notices we are in shutdown mode
|
// must happen asap, so our hook properly notices we are in shutdown mode
|
||||||
final AppIndicatorInstanceStruct savedAppI = appIndicator;
|
final AppIndicatorInstanceStruct savedAppIndicator = appIndicator;
|
||||||
appIndicator = null;
|
appIndicator = null;
|
||||||
|
|
||||||
Gtk.dispatch(new Runnable() {
|
Gtk.dispatch(new Runnable() {
|
||||||
@ -210,8 +207,8 @@ class _AppIndicatorTray extends MenuImpl {
|
|||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
// STATUS_PASSIVE hides the indicator
|
// STATUS_PASSIVE hides the indicator
|
||||||
AppIndicator.app_indicator_set_status(savedAppI, AppIndicator.STATUS_PASSIVE);
|
AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE);
|
||||||
Pointer p = savedAppI.getPointer();
|
Pointer p = savedAppIndicator.getPointer();
|
||||||
Gobject.g_object_unref(p);
|
Gobject.g_object_unref(p);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -224,7 +221,14 @@ class _AppIndicatorTray extends MenuImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
|
boolean hasImage() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final
|
||||||
void setImage_(final File imageFile) {
|
void setImage_(final File imageFile) {
|
||||||
dispatch(new Runnable() {
|
dispatch(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@ -252,7 +256,8 @@ class _AppIndicatorTray extends MenuImpl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
void setEnabled(final boolean setEnabled) {
|
void setEnabled(final boolean setEnabled) {
|
||||||
visible = !setEnabled;
|
visible = !setEnabled;
|
||||||
|
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.MouseInfo;
|
import java.awt.MouseInfo;
|
||||||
import java.awt.Point;
|
import java.awt.Point;
|
||||||
@ -32,7 +32,6 @@ import dorkbox.systemTray.linux.jna.GEventCallback;
|
|||||||
import dorkbox.systemTray.linux.jna.GdkEventButton;
|
import dorkbox.systemTray.linux.jna.GdkEventButton;
|
||||||
import dorkbox.systemTray.linux.jna.Gobject;
|
import dorkbox.systemTray.linux.jna.Gobject;
|
||||||
import dorkbox.systemTray.linux.jna.Gtk;
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
import dorkbox.systemTray.util.ImageUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling all system tray interactions via GTK.
|
* Class for handling all system tray interactions via GTK.
|
||||||
@ -40,8 +39,9 @@ import dorkbox.systemTray.util.ImageUtils;
|
|||||||
* This is the "old" way to do it, and does not work with some desktop environments. This is a hybrid class, because we want to show the
|
* This is the "old" way to do it, and does not work with some desktop environments. This is a hybrid class, because we want to show the
|
||||||
* swing menu popup INSTEAD of GTK menu popups. The "golden standard" is our swing menu popup, since we have 100% control over it.
|
* swing menu popup INSTEAD of GTK menu popups. The "golden standard" is our swing menu popup, since we have 100% control over it.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
public
|
public
|
||||||
class _GtkStatusIconTray extends MenuImpl {
|
class _GtkStatusIconTray extends SwingMenu {
|
||||||
private volatile Pointer trayIcon;
|
private volatile Pointer trayIcon;
|
||||||
|
|
||||||
// http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c
|
// http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c
|
||||||
@ -63,12 +63,6 @@ class _GtkStatusIconTray extends MenuImpl {
|
|||||||
_GtkStatusIconTray(final SystemTray systemTray) {
|
_GtkStatusIconTray(final SystemTray systemTray) {
|
||||||
super(systemTray, null, new TrayPopup());
|
super(systemTray, null, new TrayPopup());
|
||||||
|
|
||||||
if (SystemTray.FORCE_TRAY_TYPE != 0 && SystemTray.FORCE_TRAY_TYPE != SystemTray.TYPE_GTK_STATUSICON) {
|
|
||||||
throw new IllegalArgumentException("Unable to start GtkStatusIcon if 'SystemTray.FORCE_TRAY_TYPE' does not match");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageUtils.determineIconSize();
|
|
||||||
|
|
||||||
JPopupMenu popupMenu = (JPopupMenu) _native;
|
JPopupMenu popupMenu = (JPopupMenu) _native;
|
||||||
popupMenu.pack();
|
popupMenu.pack();
|
||||||
popupMenu.setFocusable(true);
|
popupMenu.setFocusable(true);
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.swingUI;
|
||||||
|
|
||||||
import java.awt.AWTException;
|
import java.awt.AWTException;
|
||||||
import java.awt.Image;
|
import java.awt.Image;
|
||||||
@ -26,19 +26,17 @@ import java.io.File;
|
|||||||
import javax.swing.ImageIcon;
|
import javax.swing.ImageIcon;
|
||||||
import javax.swing.JPopupMenu;
|
import javax.swing.JPopupMenu;
|
||||||
|
|
||||||
import dorkbox.systemTray.util.ImageUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling all system tray interaction, via SWING.
|
* Class for handling all system tray interaction, via Swing.
|
||||||
*
|
*
|
||||||
* It doesn't work well on linux. See bugs:
|
* It doesn't work well AT ALL on linux. See bugs:
|
||||||
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936
|
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936
|
||||||
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521
|
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521
|
||||||
* https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028
|
* https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"})
|
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"})
|
||||||
public
|
public
|
||||||
class _SwingTray extends MenuImpl {
|
class _SwingTray extends SwingMenu {
|
||||||
private volatile SystemTray tray;
|
private volatile SystemTray tray;
|
||||||
private volatile TrayIcon trayIcon;
|
private volatile TrayIcon trayIcon;
|
||||||
|
|
||||||
@ -50,12 +48,11 @@ class _SwingTray extends MenuImpl {
|
|||||||
_SwingTray(final dorkbox.systemTray.SystemTray systemTray) {
|
_SwingTray(final dorkbox.systemTray.SystemTray systemTray) {
|
||||||
super(systemTray, null, new TrayPopup());
|
super(systemTray, null, new TrayPopup());
|
||||||
|
|
||||||
if (dorkbox.systemTray.SystemTray.FORCE_TRAY_TYPE != 0 && dorkbox.systemTray.SystemTray.FORCE_TRAY_TYPE != dorkbox.systemTray.SystemTray.TYPE_SWING) {
|
if (!SystemTray.isSupported()) {
|
||||||
throw new IllegalArgumentException("Unable to start Swing SystemTray if 'SystemTray.FORCE_TRAY_TYPE' does not match");
|
throw new RuntimeException("System Tray is not supported in this configuration! Please write an issue and include your OS " +
|
||||||
|
"type and configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageUtils.determineIconSize();
|
|
||||||
|
|
||||||
_SwingTray.this.tray = SystemTray.getSystemTray();
|
_SwingTray.this.tray = SystemTray.getSystemTray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,10 +62,10 @@ class _SwingTray extends MenuImpl {
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
tray.remove(trayIcon);
|
|
||||||
|
|
||||||
removeAll();
|
removeAll();
|
||||||
remove();
|
remove();
|
||||||
|
|
||||||
|
tray.remove(trayIcon);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -118,6 +115,7 @@ class _SwingTray extends MenuImpl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
public
|
public
|
||||||
void setEnabled(final boolean setEnabled) {
|
void setEnabled(final boolean setEnabled) {
|
||||||
visible = !setEnabled;
|
visible = !setEnabled;
|
||||||
@ -126,7 +124,6 @@ class _SwingTray extends MenuImpl {
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
|
|
||||||
if (visible && !setEnabled) {
|
if (visible && !setEnabled) {
|
||||||
tray.remove(trayIcon);
|
tray.remove(trayIcon);
|
||||||
}
|
}
|
@ -13,246 +13,103 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.swing;
|
package dorkbox.systemTray.util;
|
||||||
|
|
||||||
|
|
||||||
import static dorkbox.systemTray.swing.EntryImpl.getVkKey;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import javax.swing.ImageIcon;
|
|
||||||
import javax.swing.JComponent;
|
|
||||||
import javax.swing.JMenuItem;
|
|
||||||
|
|
||||||
import dorkbox.systemTray.Action;
|
import dorkbox.systemTray.Action;
|
||||||
import dorkbox.systemTray.Entry;
|
import dorkbox.systemTray.Entry;
|
||||||
import dorkbox.systemTray.Menu;
|
import dorkbox.systemTray.Menu;
|
||||||
|
import dorkbox.systemTray.Separator;
|
||||||
import dorkbox.systemTray.Status;
|
import dorkbox.systemTray.Status;
|
||||||
import dorkbox.systemTray.SystemTray;
|
import dorkbox.systemTray.SystemTray;
|
||||||
import dorkbox.systemTray.util.ImageUtils;
|
|
||||||
import dorkbox.util.SwingUtil;
|
|
||||||
|
|
||||||
// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both
|
// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both
|
||||||
@SuppressWarnings("ForLoopReplaceableByForEach")
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
||||||
class MenuImpl implements Menu {
|
public abstract
|
||||||
static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger();
|
class MenuBase implements Menu {
|
||||||
private final int id = MenuImpl.MENU_ID_COUNTER.getAndIncrement();
|
public static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger();
|
||||||
|
private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement();
|
||||||
|
|
||||||
private final java.util.List<Entry> menuEntries = new ArrayList<Entry>();
|
protected final java.util.List<Entry> menuEntries = new ArrayList<Entry>();
|
||||||
|
|
||||||
private final SystemTray systemTray;
|
private final SystemTray systemTray;
|
||||||
private final Menu parent;
|
private final Menu parent;
|
||||||
|
|
||||||
// sub-menu = AdjustedJMenu
|
|
||||||
// systemtray = TrayPopup
|
|
||||||
volatile JComponent _native;
|
|
||||||
|
|
||||||
// this have to be volatile, because they can be changed from any thread
|
|
||||||
private volatile String text;
|
|
||||||
private volatile boolean hasLegitIcon = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called in the EDT
|
* Called in the EDT/GTK dispatch threads
|
||||||
*
|
*
|
||||||
* @param systemTray the system tray (which is the object that sits in the system tray)
|
* @param systemTray the system tray (which is the object that sits in the system tray)
|
||||||
* @param parent the parent of this menu, null if the parent is the system tray
|
* @param parent the parent of this menu, null if the parent is the system tray
|
||||||
* @param _native the native element that represents this menu
|
|
||||||
*/
|
*/
|
||||||
MenuImpl(final SystemTray systemTray, final Menu parent, final JComponent _native) {
|
public
|
||||||
|
MenuBase(final SystemTray systemTray, final Menu parent) {
|
||||||
this.systemTray = systemTray;
|
this.systemTray = systemTray;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this._native = _native;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispatch(final Runnable runnable) {
|
protected abstract
|
||||||
// this will properly check if we are running on the EDT
|
void dispatch(final Runnable runnable);
|
||||||
SwingUtil.invokeLater(runnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispatchAndWait(final Runnable runnable) {
|
|
||||||
// this will properly check if we are running on the EDT
|
|
||||||
try {
|
|
||||||
SwingUtil.invokeAndWait(runnable);
|
|
||||||
} catch (Exception e) {
|
|
||||||
SystemTray.logger.error("Error processing event on the dispatch thread.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// always called in the EDT
|
|
||||||
private
|
|
||||||
void renderText(final String text) {
|
|
||||||
((JMenuItem) _native).setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
protected abstract
|
||||||
|
void dispatchAndWait(final Runnable runnable);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will add a new menu entry, or update one if it already exists
|
* Will add a new menu entry, or update one if it already exists
|
||||||
* NOT ALWAYS CALLED ON EDT
|
* NOT ALWAYS CALLED ON DISPATCH
|
||||||
*/
|
*/
|
||||||
private
|
protected abstract
|
||||||
Entry addEntry_(final String menuText, final File imagePath, final Action callback) {
|
Entry addEntry_(final String menuText, final File imagePath, final Action callback);
|
||||||
if (menuText == null) {
|
|
||||||
throw new NullPointerException("Menu text cannot be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
final AtomicReference<Entry> value = new AtomicReference<Entry>();
|
|
||||||
|
|
||||||
dispatchAndWait(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
synchronized (menuEntries) {
|
|
||||||
Entry entry = get(menuText);
|
|
||||||
|
|
||||||
if (entry == null) {
|
|
||||||
// must always be called on the EDT
|
|
||||||
entry = new EntryItem(MenuImpl.this, callback);
|
|
||||||
entry.setText(menuText);
|
|
||||||
entry.setImage(imagePath);
|
|
||||||
|
|
||||||
menuEntries.add(entry);
|
|
||||||
} else if (entry instanceof EntryItem) {
|
|
||||||
entry.setText(menuText);
|
|
||||||
entry.setImage(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
value.set(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return value.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will add a new sub-menu entry, or update one if it already exists
|
* Will add a new sub-menu entry, or update one if it already exists
|
||||||
* NOT ALWAYS CALLED ON EDT
|
* NOT ALWAYS CALLED ON DISPATCH
|
||||||
*/
|
*/
|
||||||
private
|
protected abstract
|
||||||
Menu addMenu_(final String menuText, final File imagePath) {
|
Menu addMenu_(final String menuText, final File imagePath);
|
||||||
if (menuText == null) {
|
|
||||||
throw new NullPointerException("Menu text cannot be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
final AtomicReference<Menu> value = new AtomicReference<Menu>();
|
|
||||||
|
|
||||||
dispatchAndWait(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
synchronized (menuEntries) {
|
|
||||||
Entry entry = get(menuText);
|
|
||||||
|
|
||||||
if (entry == null) {
|
|
||||||
// must always be called on the EDT
|
|
||||||
entry = new MenuImpl(getSystemTray(), MenuImpl.this, new AdjustedJMenu());
|
|
||||||
_native.add(((MenuImpl) entry)._native); // have to add it separately
|
|
||||||
|
|
||||||
entry.setText(menuText);
|
|
||||||
entry.setImage(imagePath);
|
|
||||||
value.set((Menu) entry);
|
|
||||||
|
|
||||||
} else if (entry instanceof MenuImpl) {
|
|
||||||
entry.setText(menuText);
|
|
||||||
entry.setImage(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
menuEntries.add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return value.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// public here so that Swing/Gtk/AppIndicator can override this
|
// public here so that Swing/Gtk/AppIndicator can override this
|
||||||
public
|
protected abstract
|
||||||
void setImage_(final File imageFile) {
|
void setImage_(final File imageFile);
|
||||||
hasLegitIcon = imageFile != null;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dispatch(new Runnable() {
|
|
||||||
@Override
|
@Override
|
||||||
public
|
public final
|
||||||
void run() {
|
|
||||||
if (imageFile != null) {
|
|
||||||
ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath());
|
|
||||||
((JMenuItem) _native).setIcon(origIcon);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
((JMenuItem) _native).setIcon(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public
|
|
||||||
Menu getParent() {
|
Menu getParent() {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
SystemTray getSystemTray() {
|
SystemTray getSystemTray() {
|
||||||
return systemTray;
|
return systemTray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// public here so that Swing/Gtk/AppIndicator can access this
|
||||||
@Override
|
public final
|
||||||
public
|
String getStatus() {
|
||||||
boolean hasImage() {
|
|
||||||
return hasLegitIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables, or disables the sub-menu entry.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setEnabled(final boolean enabled) {
|
|
||||||
dispatch(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
_native.setEnabled(enabled);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NOT ALWAYS CALLED ON EDT
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void addSeparator() {
|
|
||||||
dispatch(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
synchronized (menuEntries) {
|
synchronized (menuEntries) {
|
||||||
synchronized (menuEntries) {
|
Entry entry = menuEntries.get(0);
|
||||||
Entry entry = new EntrySeparator(MenuImpl.this);
|
if (entry instanceof Status) {
|
||||||
menuEntries.add(entry);
|
return entry.getText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however.
|
// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however.
|
||||||
@ -281,7 +138,8 @@ class MenuImpl implements Menu {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry get(final String menuText) {
|
Entry get(final String menuText) {
|
||||||
if (menuText == null || menuText.isEmpty()) {
|
if (menuText == null || menuText.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@ -291,6 +149,11 @@ class MenuImpl implements Menu {
|
|||||||
synchronized (menuEntries) {
|
synchronized (menuEntries) {
|
||||||
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
||||||
final Entry entry = menuEntries.get(i);
|
final Entry entry = menuEntries.get(i);
|
||||||
|
|
||||||
|
if (entry instanceof Separator || entry instanceof Status) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
String text = entry.getText();
|
String text = entry.getText();
|
||||||
|
|
||||||
// text can be null
|
// text can be null
|
||||||
@ -304,13 +167,15 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignores status + separators
|
// ignores status + separators
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry getFirst() {
|
Entry getFirst() {
|
||||||
return get(0);
|
return get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignores status + separators
|
// ignores status + separators
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry getLast() {
|
Entry getLast() {
|
||||||
// Must be wrapped in a synchronized block for object visibility
|
// Must be wrapped in a synchronized block for object visibility
|
||||||
synchronized (menuEntries) {
|
synchronized (menuEntries) {
|
||||||
@ -319,7 +184,7 @@ class MenuImpl implements Menu {
|
|||||||
for (int i = menuEntries.size()-1; i >= 0; i--) {
|
for (int i = menuEntries.size()-1; i >= 0; i--) {
|
||||||
entry = menuEntries.get(i);
|
entry = menuEntries.get(i);
|
||||||
|
|
||||||
if (!(entry instanceof dorkbox.systemTray.Separator || entry instanceof Status)) {
|
if (!(entry instanceof Separator || entry instanceof Status)) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -330,7 +195,8 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignores status + separators
|
// ignores status + separators
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry get(final int menuIndex) {
|
Entry get(final int menuIndex) {
|
||||||
if (menuIndex < 0) {
|
if (menuIndex < 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -341,7 +207,7 @@ class MenuImpl implements Menu {
|
|||||||
if (!menuEntries.isEmpty()) {
|
if (!menuEntries.isEmpty()) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (Entry entry : menuEntries) {
|
for (Entry entry : menuEntries) {
|
||||||
if (entry instanceof dorkbox.systemTray.Separator || entry instanceof Status) {
|
if (entry instanceof Separator || entry instanceof Status) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,13 +223,14 @@ class MenuImpl implements Menu {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public
|
public final
|
||||||
Entry addEntry(String menuText, Action callback) {
|
Entry addEntry(String menuText, Action callback) {
|
||||||
return addEntry(menuText, (String) null, callback);
|
return addEntry(menuText, (String) null, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry addEntry(String menuText, String imagePath, Action callback) {
|
Entry addEntry(String menuText, String imagePath, Action callback) {
|
||||||
if (imagePath == null) {
|
if (imagePath == null) {
|
||||||
return addEntry_(menuText, null, callback);
|
return addEntry_(menuText, null, callback);
|
||||||
@ -373,7 +240,8 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry addEntry(String menuText, URL imageUrl, Action callback) {
|
Entry addEntry(String menuText, URL imageUrl, Action callback) {
|
||||||
if (imageUrl == null) {
|
if (imageUrl == null) {
|
||||||
return addEntry_(menuText, null, callback);
|
return addEntry_(menuText, null, callback);
|
||||||
@ -383,7 +251,8 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry addEntry(String menuText, String cacheName, InputStream imageStream, Action callback) {
|
Entry addEntry(String menuText, String cacheName, InputStream imageStream, Action callback) {
|
||||||
if (imageStream == null) {
|
if (imageStream == null) {
|
||||||
return addEntry_(menuText, null, callback);
|
return addEntry_(menuText, null, callback);
|
||||||
@ -393,7 +262,8 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Entry addEntry(String menuText, InputStream imageStream, Action callback) {
|
Entry addEntry(String menuText, InputStream imageStream, Action callback) {
|
||||||
if (imageStream == null) {
|
if (imageStream == null) {
|
||||||
return addEntry_(menuText, null, callback);
|
return addEntry_(menuText, null, callback);
|
||||||
@ -407,13 +277,14 @@ class MenuImpl implements Menu {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
public
|
public final
|
||||||
Menu addMenu(String menuText) {
|
Menu addMenu(String menuText) {
|
||||||
return addMenu(menuText, (String) null);
|
return addMenu(menuText, (String) null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Menu addMenu(String menuText, String imagePath) {
|
Menu addMenu(String menuText, String imagePath) {
|
||||||
if (imagePath == null) {
|
if (imagePath == null) {
|
||||||
return addMenu_(menuText, null);
|
return addMenu_(menuText, null);
|
||||||
@ -423,7 +294,8 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Menu addMenu(String menuText, URL imageUrl) {
|
Menu addMenu(String menuText, URL imageUrl) {
|
||||||
if (imageUrl == null) {
|
if (imageUrl == null) {
|
||||||
return addMenu_(menuText, null);
|
return addMenu_(menuText, null);
|
||||||
@ -433,7 +305,8 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Menu addMenu(String menuText, String cacheName, InputStream imageStream) {
|
Menu addMenu(String menuText, String cacheName, InputStream imageStream) {
|
||||||
if (imageStream == null) {
|
if (imageStream == null) {
|
||||||
return addMenu_(menuText, null);
|
return addMenu_(menuText, null);
|
||||||
@ -443,7 +316,8 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
Menu addMenu(String menuText, InputStream imageStream) {
|
Menu addMenu(String menuText, InputStream imageStream) {
|
||||||
if (imageStream == null) {
|
if (imageStream == null) {
|
||||||
return addMenu_(menuText, null);
|
return addMenu_(menuText, null);
|
||||||
@ -511,97 +385,16 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
|
||||||
String getStatus() {
|
|
||||||
synchronized (menuEntries) {
|
|
||||||
Entry entry = menuEntries.get(0);
|
|
||||||
if (entry instanceof EntryStatus) {
|
|
||||||
return entry.getText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
void setStatus(final String statusText) {
|
|
||||||
final MenuImpl _this = this;
|
|
||||||
dispatchAndWait(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
synchronized (menuEntries) {
|
|
||||||
// status is ALWAYS at 0 index...
|
|
||||||
EntryImpl menuEntry = null;
|
|
||||||
if (!menuEntries.isEmpty()) {
|
|
||||||
menuEntry = (EntryImpl) menuEntries.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menuEntry instanceof EntryStatus) {
|
|
||||||
// set the text or delete...
|
|
||||||
|
|
||||||
if (statusText == null) {
|
|
||||||
// delete
|
|
||||||
remove(menuEntry);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// set text
|
|
||||||
menuEntry.setText(statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// create a new one
|
|
||||||
menuEntry = new EntryStatus(_this, statusText);
|
|
||||||
// status is ALWAYS at 0 index...
|
|
||||||
menuEntries.add(0, menuEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public
|
public final
|
||||||
String getText() {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setText(final String newText) {
|
|
||||||
text = newText;
|
|
||||||
dispatch(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
renderText(newText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setCallback(final Action callback) {
|
void setCallback(final Action callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setShortcut(final char key) {
|
|
||||||
if (_native instanceof JMenuItem) {
|
|
||||||
// yikes...
|
|
||||||
final int vKey = getVkKey(key);
|
|
||||||
dispatch(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
((JMenuItem) _native).setMnemonic(vKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -628,7 +421,8 @@ class MenuImpl implements Menu {
|
|||||||
*
|
*
|
||||||
* @param entry This is the menu entry to remove
|
* @param entry This is the menu entry to remove
|
||||||
*/
|
*/
|
||||||
public
|
@Override
|
||||||
|
public final
|
||||||
void remove(final Entry entry) {
|
void remove(final Entry entry) {
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
throw new NullPointerException("No menu entry exists for entry");
|
throw new NullPointerException("No menu entry exists for entry");
|
||||||
@ -649,7 +443,7 @@ class MenuImpl implements Menu {
|
|||||||
* @param menu This is the menu entry to remove
|
* @param menu This is the menu entry to remove
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public
|
public final
|
||||||
void remove(final Menu menu) {
|
void remove(final Menu menu) {
|
||||||
final Menu parent = getParent();
|
final Menu parent = getParent();
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
@ -668,14 +462,14 @@ class MenuImpl implements Menu {
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
((MenuImpl) parent).remove__(_this);
|
((MenuBase) parent).remove__(_this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOT ALWAYS CALLED ON EDT
|
// NOT ALWAYS CALLED ON EDT
|
||||||
private
|
protected
|
||||||
void remove__(final Object menuEntry) {
|
void remove__(final Object menuEntry) {
|
||||||
try {
|
try {
|
||||||
synchronized (menuEntries) {
|
synchronized (menuEntries) {
|
||||||
@ -715,7 +509,7 @@ class MenuImpl implements Menu {
|
|||||||
*
|
*
|
||||||
* @param menuText This is the label for the menu entry or sub-menu to remove
|
* @param menuText This is the label for the menu entry or sub-menu to remove
|
||||||
*/
|
*/
|
||||||
public
|
public final
|
||||||
void remove(final String menuText) {
|
void remove(final String menuText) {
|
||||||
dispatchAndWait(new Runnable() {
|
dispatchAndWait(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@ -733,28 +527,28 @@ class MenuImpl implements Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// @Override
|
||||||
|
// public final
|
||||||
|
// void remove() {
|
||||||
|
// dispatchAndWait(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public
|
||||||
|
// void run() {
|
||||||
|
// _native.setVisible(false);
|
||||||
|
// if (_native instanceof TrayPopup) {
|
||||||
|
// ((TrayPopup) _native).close();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// MenuBase parent = (MenuBase) getParent();
|
||||||
|
// if (parent != null) {
|
||||||
|
// parent._native.remove(_native);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final
|
public final
|
||||||
void remove() {
|
|
||||||
dispatchAndWait(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
_native.setVisible(false);
|
|
||||||
if (_native instanceof TrayPopup) {
|
|
||||||
((TrayPopup) _native).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuImpl parent = (MenuImpl) getParent();
|
|
||||||
if (parent != null) {
|
|
||||||
parent._native.remove(_native);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void removeAll() {
|
void removeAll() {
|
||||||
dispatch(new Runnable() {
|
dispatch(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@ -762,7 +556,7 @@ class MenuImpl implements Menu {
|
|||||||
void run() {
|
void run() {
|
||||||
synchronized (menuEntries) {
|
synchronized (menuEntries) {
|
||||||
// have to make copy because we are deleting all of them, and sub-menus remove themselves from parents
|
// have to make copy because we are deleting all of them, and sub-menus remove themselves from parents
|
||||||
ArrayList<Entry> menuEntriesCopy = new ArrayList<Entry>(MenuImpl.this.menuEntries);
|
ArrayList<Entry> menuEntriesCopy = new ArrayList<Entry>(MenuBase.this.menuEntries);
|
||||||
for (Entry entry : menuEntriesCopy) {
|
for (Entry entry : menuEntriesCopy) {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
@ -792,7 +586,7 @@ class MenuImpl implements Menu {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuImpl other = (MenuImpl) obj;
|
MenuBase other = (MenuBase) obj;
|
||||||
return this.id == other.id;
|
return this.id == other.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ package dorkbox.systemTray.util;
|
|||||||
import static dorkbox.systemTray.SystemTray.logger;
|
import static dorkbox.systemTray.SystemTray.logger;
|
||||||
|
|
||||||
import java.awt.Robot;
|
import java.awt.Robot;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import dorkbox.systemTray.SystemTray;
|
import dorkbox.systemTray.SystemTray;
|
||||||
@ -31,11 +32,11 @@ import javassist.CtMethod;
|
|||||||
* Fixes issues with some java runtimes
|
* Fixes issues with some java runtimes
|
||||||
*/
|
*/
|
||||||
public
|
public
|
||||||
class WindowsSystemTraySwing {
|
class SystemTrayFixes {
|
||||||
|
|
||||||
// oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that!
|
// oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that!
|
||||||
public static void fix() {
|
// https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788
|
||||||
// if we are using swing (in windows only) the icon size is usually incorrect. Here we have to fix that.
|
public static void fixWindows() {
|
||||||
if (!OS.isWindows()) {
|
if (!OS.isWindows()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -61,7 +62,7 @@ class WindowsSystemTraySwing {
|
|||||||
(null != m.invoke(cl, "java.awt.SystemTray"));
|
(null != m.invoke(cl, "java.awt.SystemTray"));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
if (SystemTray.DEBUG) {
|
if (SystemTray.DEBUG) {
|
||||||
logger.debug("Error detecting javaFX/SWT mode", e);
|
logger.debug("Error detecting if the Swing SystemTray is loaded", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,4 +201,152 @@ class WindowsSystemTraySwing {
|
|||||||
logger.error("Error setting tray icon size to: {}", ImageUtils.TRAY_SIZE, e);
|
logger.error("Error setting tray icon size to: {}", ImageUtils.TRAY_SIZE, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MacOS AWT is hardcoded to respond only to "popup trigger" for menus, where it should be any mouse button
|
||||||
|
// https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788
|
||||||
|
// https://bugs.openjdk.java.net/browse/JDK-7158615
|
||||||
|
public static void fixMacOS() {
|
||||||
|
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 isMacSwingTrayLoaded = 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.
|
||||||
|
isMacSwingTrayLoaded = (null != m.invoke(cl, "sun.lwawt.macosx.CTrayIcon")) ||
|
||||||
|
(null != m.invoke(cl, "java.awt.SystemTray"));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (SystemTray.DEBUG) {
|
||||||
|
logger.debug("Error detecting if the MacOS SystemTray is loaded", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMacSwingTrayLoaded) {
|
||||||
|
throw new RuntimeException("Unable to initialize the swing tray in windows, it has already been created!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a key character into it's corresponding VK entry
|
||||||
|
*/
|
||||||
|
public static
|
||||||
|
int getVirtualKey(final char key) {
|
||||||
|
switch (key) {
|
||||||
|
case 0x08: return KeyEvent.VK_BACK_SPACE;
|
||||||
|
case 0x09: return KeyEvent.VK_TAB;
|
||||||
|
case 0x0a: return KeyEvent.VK_ENTER;
|
||||||
|
case 0x1B: return KeyEvent.VK_ESCAPE;
|
||||||
|
case 0x20AC: return KeyEvent.VK_EURO_SIGN;
|
||||||
|
case 0x20: return KeyEvent.VK_SPACE;
|
||||||
|
case 0x21: return KeyEvent.VK_EXCLAMATION_MARK;
|
||||||
|
case 0x22: return KeyEvent.VK_QUOTEDBL;
|
||||||
|
case 0x23: return KeyEvent.VK_NUMBER_SIGN;
|
||||||
|
case 0x24: return KeyEvent.VK_DOLLAR;
|
||||||
|
case 0x26: return KeyEvent.VK_AMPERSAND;
|
||||||
|
case 0x27: return KeyEvent.VK_QUOTE;
|
||||||
|
case 0x28: return KeyEvent.VK_LEFT_PARENTHESIS;
|
||||||
|
case 0x29: return KeyEvent.VK_RIGHT_PARENTHESIS;
|
||||||
|
case 0x2A: return KeyEvent.VK_ASTERISK;
|
||||||
|
case 0x2B: return KeyEvent.VK_PLUS;
|
||||||
|
case 0x2C: return KeyEvent.VK_COMMA;
|
||||||
|
case 0x2D: return KeyEvent.VK_MINUS;
|
||||||
|
case 0x2E: return KeyEvent.VK_PERIOD;
|
||||||
|
case 0x2F: return KeyEvent.VK_SLASH;
|
||||||
|
case 0x30: return KeyEvent.VK_0;
|
||||||
|
case 0x31: return KeyEvent.VK_1;
|
||||||
|
case 0x32: return KeyEvent.VK_2;
|
||||||
|
case 0x33: return KeyEvent.VK_3;
|
||||||
|
case 0x34: return KeyEvent.VK_4;
|
||||||
|
case 0x35: return KeyEvent.VK_5;
|
||||||
|
case 0x36: return KeyEvent.VK_6;
|
||||||
|
case 0x37: return KeyEvent.VK_7;
|
||||||
|
case 0x38: return KeyEvent.VK_8;
|
||||||
|
case 0x39: return KeyEvent.VK_9;
|
||||||
|
case 0x3A: return KeyEvent.VK_COLON;
|
||||||
|
case 0x3B: return KeyEvent.VK_SEMICOLON;
|
||||||
|
case 0x3C: return KeyEvent.VK_LESS;
|
||||||
|
case 0x3D: return KeyEvent.VK_EQUALS;
|
||||||
|
case 0x3E: return KeyEvent.VK_GREATER;
|
||||||
|
case 0x40: return KeyEvent.VK_AT;
|
||||||
|
case 0x41: return KeyEvent.VK_A;
|
||||||
|
case 0x42: return KeyEvent.VK_B;
|
||||||
|
case 0x43: return KeyEvent.VK_C;
|
||||||
|
case 0x44: return KeyEvent.VK_D;
|
||||||
|
case 0x45: return KeyEvent.VK_E;
|
||||||
|
case 0x46: return KeyEvent.VK_F;
|
||||||
|
case 0x47: return KeyEvent.VK_G;
|
||||||
|
case 0x48: return KeyEvent.VK_H;
|
||||||
|
case 0x49: return KeyEvent.VK_I;
|
||||||
|
case 0x4A: return KeyEvent.VK_J;
|
||||||
|
case 0x4B: return KeyEvent.VK_K;
|
||||||
|
case 0x4C: return KeyEvent.VK_L;
|
||||||
|
case 0x4D: return KeyEvent.VK_M;
|
||||||
|
case 0x4E: return KeyEvent.VK_N;
|
||||||
|
case 0x4F: return KeyEvent.VK_O;
|
||||||
|
case 0x50: return KeyEvent.VK_P;
|
||||||
|
case 0x51: return KeyEvent.VK_Q;
|
||||||
|
case 0x52: return KeyEvent.VK_R;
|
||||||
|
case 0x53: return KeyEvent.VK_S;
|
||||||
|
case 0x54: return KeyEvent.VK_T;
|
||||||
|
case 0x55: return KeyEvent.VK_U;
|
||||||
|
case 0x56: return KeyEvent.VK_V;
|
||||||
|
case 0x57: return KeyEvent.VK_W;
|
||||||
|
case 0x58: return KeyEvent.VK_X;
|
||||||
|
case 0x59: return KeyEvent.VK_Y;
|
||||||
|
case 0x5A: return KeyEvent.VK_Z;
|
||||||
|
case 0x5B: return KeyEvent.VK_OPEN_BRACKET;
|
||||||
|
case 0x5C: return KeyEvent.VK_BACK_SLASH;
|
||||||
|
case 0x5D: return KeyEvent.VK_CLOSE_BRACKET;
|
||||||
|
case 0x5E: return KeyEvent.VK_CIRCUMFLEX;
|
||||||
|
case 0x5F: return KeyEvent.VK_UNDERSCORE;
|
||||||
|
case 0x60: return KeyEvent.VK_BACK_QUOTE;
|
||||||
|
case 0x61: return KeyEvent.VK_A;
|
||||||
|
case 0x62: return KeyEvent.VK_B;
|
||||||
|
case 0x63: return KeyEvent.VK_C;
|
||||||
|
case 0x64: return KeyEvent.VK_D;
|
||||||
|
case 0x65: return KeyEvent.VK_E;
|
||||||
|
case 0x66: return KeyEvent.VK_F;
|
||||||
|
case 0x67: return KeyEvent.VK_G;
|
||||||
|
case 0x68: return KeyEvent.VK_H;
|
||||||
|
case 0x69: return KeyEvent.VK_I;
|
||||||
|
case 0x6A: return KeyEvent.VK_J;
|
||||||
|
case 0x6B: return KeyEvent.VK_K;
|
||||||
|
case 0x6C: return KeyEvent.VK_L;
|
||||||
|
case 0x6D: return KeyEvent.VK_M;
|
||||||
|
case 0x6E: return KeyEvent.VK_N;
|
||||||
|
case 0x6F: return KeyEvent.VK_O;
|
||||||
|
case 0x70: return KeyEvent.VK_P;
|
||||||
|
case 0x71: return KeyEvent.VK_Q;
|
||||||
|
case 0x72: return KeyEvent.VK_R;
|
||||||
|
case 0x73: return KeyEvent.VK_S;
|
||||||
|
case 0x74: return KeyEvent.VK_T;
|
||||||
|
case 0x75: return KeyEvent.VK_U;
|
||||||
|
case 0x76: return KeyEvent.VK_V;
|
||||||
|
case 0x77: return KeyEvent.VK_W;
|
||||||
|
case 0x78: return KeyEvent.VK_X;
|
||||||
|
case 0x79: return KeyEvent.VK_Y;
|
||||||
|
case 0x7A: return KeyEvent.VK_Z;
|
||||||
|
case 0x7B: return KeyEvent.VK_BRACELEFT;
|
||||||
|
case 0x7D: return KeyEvent.VK_BRACERIGHT;
|
||||||
|
case 0x7F: return KeyEvent.VK_DELETE;
|
||||||
|
case 0xA1: return KeyEvent.VK_INVERTED_EXCLAMATION_MARK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user