WIP: native + swing menus

This commit is contained in:
nathan 2016-10-11 15:23:58 +02:00
parent 7e3fba9157
commit 7a2909abca
36 changed files with 3478 additions and 807 deletions

View File

@ -168,6 +168,7 @@ interface Menu extends Entry {
*/
Menu addMenu(String menuText, InputStream imageStream);
/**
* Adds a swing widget as a menu entry.
*

View File

@ -32,12 +32,18 @@ import org.slf4j.LoggerFactory;
import dorkbox.systemTray.linux.GnomeShellExtension;
import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.swing._AppIndicatorTray;
import dorkbox.systemTray.swing._GtkStatusIconTray;
import dorkbox.systemTray.swing._SwingTray;
import dorkbox.systemTray.nativeUI.NativeUI;
import dorkbox.systemTray.nativeUI._AppIndicatorNativeTray;
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.Swt;
import dorkbox.systemTray.util.WindowsSystemTraySwing;
import dorkbox.systemTray.util.SystemTrayFixes;
import dorkbox.util.CacheUtil;
import dorkbox.util.IO;
import dorkbox.util.OS;
@ -54,10 +60,12 @@ public
class SystemTray implements Menu {
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
public static final int TYPE_AUTO_DETECT = 0;
public static final int TYPE_GTK_STATUSICON = 1;
public static final int TYPE_APP_INDICATOR = 2;
public static final int TYPE_SWING = 3;
public enum TrayType {
AutoDetect,
GtkStatusIcon,
AppIndicator,
Swing
}
@Property
/** Enables auto-detection for the system tray. This should be mostly successful.
@ -97,11 +105,11 @@ class SystemTray implements Menu {
@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>
* 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
/**
@ -124,6 +132,8 @@ class SystemTray implements Menu {
public final static boolean isJavaFxLoaded;
public final static boolean isSwtLoaded;
private static boolean forceNativeMenus = false;
static {
boolean isJavaFxLoaded_ = false;
@ -151,7 +161,49 @@ class SystemTray implements Menu {
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() {
if (systemTray != null) {
return;
@ -173,15 +225,15 @@ class SystemTray implements Menu {
} else {
// 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)
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);
} 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);
}
}
// 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) {
// 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");
@ -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) {
switch (FORCE_TRAY_TYPE) {
case 1: logger.debug("Forced tray type: GtkStatusIcon"); break;
case 2: logger.debug("Forced tray type: AppIndicator"); break;
case 3: logger.debug("Forced tray type: Swing"); break;
default: logger.debug("Auto-detecting tray type"); break;
if (FORCE_TRAY_TYPE == TrayType.AutoDetect) {
logger.debug("Auto-detecting tray type");
} else {
logger.debug("Forced tray type: {}", FORCE_TRAY_TYPE.name());
}
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
// 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
// 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
@ -341,13 +373,7 @@ class SystemTray implements Menu {
if (trayType == null) {
if ("unity".equalsIgnoreCase(XDG)) {
try {
trayType = _AppIndicatorTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize _AppIndicatorTray", e);
}
}
trayType = selectTypeQuietly(TrayType.AppIndicator);
}
else if ("xfce".equalsIgnoreCase(XDG)) {
// 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
// so far, it is OK to use GtkStatusIcon on XFCE <-> XFCE4 inclusive
try {
trayType = _GtkStatusIconTray.class;
} catch (Throwable e1) {
if (DEBUG) {
logger.error("Cannot initialize _GtkStatusIconTray", e1);
}
}
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("lxde".equalsIgnoreCase(XDG)) {
try {
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("kde".equalsIgnoreCase(XDG)) {
// kde (at least, plasma 5.5.6) requires appindicator
try {
trayType = _AppIndicatorTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize _AppIndicatorTray", e);
}
}
trayType = selectTypeQuietly(TrayType.AppIndicator);
}
else if ("gnome".equalsIgnoreCase(XDG)) {
// check other DE
@ -391,31 +399,13 @@ class SystemTray implements Menu {
}
if ("cinnamon".equalsIgnoreCase(GDM)) {
try {
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("gnome-classic".equalsIgnoreCase(GDM)) {
try {
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("gnome-fallback".equalsIgnoreCase(GDM)) {
try {
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("ubuntu".equalsIgnoreCase(GDM)) {
// have to install the gnome extension AND customize the restart command
@ -453,7 +443,7 @@ class SystemTray implements Menu {
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.
if (trayType == null) {
trayType = _GtkStatusIconTray.class;
trayType = selectType(TrayType.GtkStatusIcon);
}
}
} catch (Throwable e) {
@ -491,8 +481,8 @@ class SystemTray implements Menu {
if (readLine != null && readLine.contains("indicator-app")) {
// make sure we can also load the library (it might be the wrong version)
try {
trayType = _AppIndicatorTray.class;
} catch (Throwable e) {
trayType = selectType(TrayType.AppIndicator);
} catch (Exception e) {
if (DEBUG) {
logger.error("AppIndicator support detected, but unable to load the library. Falling back to GTK", e);
} else {
@ -516,7 +506,7 @@ class SystemTray implements Menu {
// fallback...
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 " +
"configuration");
}
@ -526,14 +516,19 @@ class SystemTray implements Menu {
// this has to happen BEFORE any sort of swing system tray stuff is accessed
if (OS.isWindows()) {
// windows is funky, and is hardcoded to 16x16. We fix that.
WindowsSystemTraySwing.fix();
SystemTrayFixes.fixWindows();
}
else if (OS.isMacOsX()) {
// macos doesn't respond to all buttons (but should)
SystemTrayFixes.fixMacOS();
}
// this is windows OR mac
if (trayType == null && java.awt.SystemTray.isSupported()) {
ImageUtils.determineIconSize();
// this is likely windows OR mac
if (trayType == null) {
try {
java.awt.SystemTray.getSystemTray();
trayType = _SwingTray.class;
trayType = selectType(TrayType.Swing);
} catch (Throwable e) {
if (DEBUG) {
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.", e);
@ -564,7 +559,7 @@ class SystemTray implements Menu {
AppIndicator.isVersion3) {
try {
trayType = _GtkStatusIconTray.class;
trayType = selectType(TrayType.GtkStatusIcon);
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'");
} catch (Throwable e) {
@ -578,27 +573,52 @@ class SystemTray implements Menu {
}
}
// have to construct swing stuff inside the swing EDT
// this is the safest way to do this.
final Class<? extends Menu> finalTrayType = trayType;
SwingUtil.invokeAndWait(new Runnable() {
@Override
public
void run() {
try {
reference.set((Menu) finalTrayType.getConstructors()[0].newInstance(systemTray));
logger.info("Successfully Loaded: {}", finalTrayType.getSimpleName());
} catch (Exception e) {
logger.error("Unable to create tray type: '" + finalTrayType.getSimpleName() + "'", e);
}
// 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
// this is the safest way to do this.
final Class<? extends Menu> finalTrayType = trayType;
SwingUtil.invokeAndWait(new Runnable() {
@Override
public
void run() {
try {
reference.set((Menu) finalTrayType.getConstructors()[0].newInstance(systemTray));
logger.info("Successfully Loaded: {}", finalTrayType.getSimpleName());
} catch (Exception e) {
logger.error("Unable to create tray type: '" + finalTrayType.getSimpleName() + "'", e);
}
}
});
}
} catch (Exception e) {
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
}
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.
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
* 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
* <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 get() {
forceNativeMenus = true; // TODO set to false for final build
init();
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
void shutdown() {
final Menu menu = systemTrayMenu;
@ -661,10 +704,19 @@ class SystemTray implements Menu {
if (menu instanceof _AppIndicatorTray) {
((_AppIndicatorTray) menu).shutdown();
}
else if (menu instanceof _AppIndicatorNativeTray) {
((_AppIndicatorNativeTray) menu).shutdown();
}
else if (menu instanceof _GtkStatusIconTray) {
((_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();
}
}
@ -675,13 +727,23 @@ class SystemTray implements Menu {
public
String getStatus() {
final Menu menu = systemTrayMenu;
if (menu instanceof _AppIndicatorTray) {
return ((_AppIndicatorTray) menu).getStatus();
}
else if (menu instanceof _AppIndicatorNativeTray) {
return ((_AppIndicatorNativeTray) menu).getStatus();
}
else if (menu instanceof _GtkStatusIconTray) {
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();
}
}
@ -698,10 +760,19 @@ class SystemTray implements Menu {
if (menu instanceof _AppIndicatorTray) {
((_AppIndicatorTray) menu).setStatus(statusText);
}
else if (menu instanceof _AppIndicatorNativeTray) {
((_AppIndicatorNativeTray) menu).setStatus(statusText);
}
else if (menu instanceof _GtkStatusIconTray) {
((_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);
}
}
@ -1063,6 +1134,7 @@ class SystemTray implements Menu {
/**
* This removes a menu entry from the dropdown menu.
*

View File

@ -50,7 +50,7 @@ class AppIndicator {
// ALSO WHAT VERSION OF GTK to use? appindiactor1 -> GTk2, appindicator3 -> GTK3.
// appindicator3 doesn't support menu icons via GTK2!!
if (SystemTray.FORCE_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 (SystemTray.DEBUG) {
logger.debug("Forcing GTK tray, not using appindicator");

View 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);
}

View File

@ -37,5 +37,8 @@ class Gobject {
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);
}

View File

@ -24,6 +24,9 @@ import java.util.concurrent.TimeUnit;
import com.sun.jna.Function;
import com.sun.jna.Pointer;
import dorkbox.systemTray.Action;
import dorkbox.systemTray.Entry;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.Swt;
@ -71,7 +74,7 @@ class Gtk {
String gtk3LibName = "libgtk-3.so.0";
// we can force the system to use the swing indicator, which WORKS, but doesn't support transparency in the icon.
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_SWING) {
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TrayType.Swing) {
isLoaded = true;
}
@ -247,7 +250,7 @@ class Gtk {
}
}
} 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)
// 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
* 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_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
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 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_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_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_destroy(Pointer widget);

View 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;
}
}

View 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);
}
}

View 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) {
}
}

View 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) {
}
}

View 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);
}
}
});
}
}

View 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;
}
}

View 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;
}
}
}

View 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) {
}
}

View 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) {
}
}

View 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();
}
}
});
}
}

View 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
{}

View 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);
}
}

View 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");
}
}
}
});
}
}

View 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);
}
}
});
}
}

View File

@ -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;
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.Insets;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.Insets;

View 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;
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@ -25,7 +25,7 @@ import javax.swing.JMenuItem;
import dorkbox.systemTray.Action;
import dorkbox.util.SwingUtil;
class EntryItem extends EntryImpl {
class SwingEntryItem extends SwingEntry {
private final ActionListener swingCallback;
@ -33,7 +33,7 @@ class EntryItem extends EntryImpl {
private volatile Action callback;
// 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());
this.callback = callback;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.io.File;
@ -21,10 +21,10 @@ import javax.swing.JSeparator;
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.
EntrySeparator(final MenuImpl parent) {
SwingEntrySeparator(final SwingMenu parent) {
super(parent, new JSeparator(JSeparator.HORIZONTAL));
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.Font;
import java.io.File;
@ -23,10 +23,10 @@ import javax.swing.JMenuItem;
import dorkbox.systemTray.Action;
import dorkbox.systemTray.Status;
class EntryStatus extends EntryImpl implements Status {
class SwingEntryStatus extends SwingEntry implements Status {
// 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());
setText(label);
}
@ -34,11 +34,12 @@ class EntryStatus extends EntryImpl implements Status {
// called in the EDT thread
@Override
void renderText(final String text) {
((JMenuItem) _native).setText(text);
Font font = _native.getFont();
Font font1 = font.deriveFont(Font.BOLD);
_native.setFont(font1);
((JMenuItem) _native).setText(text);
// this makes sure it can't be selected
_native.setEnabled(false);
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.io.File;
@ -22,10 +22,10 @@ import javax.swing.JComponent;
import dorkbox.systemTray.Action;
// 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.
EntryWidget(final MenuImpl parent, JComponent widget) {
SwingEntryWidget(final SwingMenu parent, JComponent widget) {
super(parent, widget);
_native.setEnabled(true);

View 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);
}
}
});
}
}

View 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
{}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.Dimension;
import java.awt.Frame;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.MouseInfo;
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/~ubuntu-desktop/ido/gtk3/files
*/
@SuppressWarnings("Duplicates")
public
class _AppIndicatorTray extends MenuImpl {
class _AppIndicatorTray extends SwingMenu {
private volatile AppIndicatorInstanceStruct appIndicator;
private boolean isActive = false;
private final Runnable popupRunnable;
@ -89,7 +90,9 @@ class _AppIndicatorTray extends MenuImpl {
private AtomicBoolean shuttingDown = new AtomicBoolean();
// necessary to prevent GC on these objects
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private NativeLong nativeLong;
@SuppressWarnings("FieldCanBeLocal")
private GEventCallback gtkCallback;
@ -107,12 +110,6 @@ class _AppIndicatorTray extends MenuImpl {
_AppIndicatorTray(final SystemTray systemTray) {
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;
popupMenu.pack();
popupMenu.setFocusable(true);
@ -198,11 +195,11 @@ class _AppIndicatorTray extends MenuImpl {
AppIndicator.app_indicator_set_menu(appIndicator, dummyMenu);
}
public
public final
void shutdown() {
if (!shuttingDown.getAndSet(true)) {
// must happen asap, so our hook properly notices we are in shutdown mode
final AppIndicatorInstanceStruct savedAppI = appIndicator;
final AppIndicatorInstanceStruct savedAppIndicator = appIndicator;
appIndicator = null;
Gtk.dispatch(new Runnable() {
@ -210,8 +207,8 @@ class _AppIndicatorTray extends MenuImpl {
public
void run() {
// STATUS_PASSIVE hides the indicator
AppIndicator.app_indicator_set_status(savedAppI, AppIndicator.STATUS_PASSIVE);
Pointer p = savedAppI.getPointer();
AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE);
Pointer p = savedAppIndicator.getPointer();
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) {
dispatch(new Runnable() {
@Override
@ -252,7 +256,8 @@ class _AppIndicatorTray extends MenuImpl {
});
}
public
@Override
public final
void setEnabled(final boolean setEnabled) {
visible = !setEnabled;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.MouseInfo;
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.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
/**
* 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
* 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
class _GtkStatusIconTray extends MenuImpl {
class _GtkStatusIconTray extends SwingMenu {
private volatile Pointer trayIcon;
// 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) {
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;
popupMenu.pack();
popupMenu.setFocusable(true);

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
package dorkbox.systemTray.swingUI;
import java.awt.AWTException;
import java.awt.Image;
@ -26,19 +26,17 @@ import java.io.File;
import javax.swing.ImageIcon;
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=6453521
* https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028
*/
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"})
public
class _SwingTray extends MenuImpl {
class _SwingTray extends SwingMenu {
private volatile SystemTray tray;
private volatile TrayIcon trayIcon;
@ -50,12 +48,11 @@ class _SwingTray extends MenuImpl {
_SwingTray(final dorkbox.systemTray.SystemTray systemTray) {
super(systemTray, null, new TrayPopup());
if (dorkbox.systemTray.SystemTray.FORCE_TRAY_TYPE != 0 && dorkbox.systemTray.SystemTray.FORCE_TRAY_TYPE != dorkbox.systemTray.SystemTray.TYPE_SWING) {
throw new IllegalArgumentException("Unable to start Swing SystemTray if 'SystemTray.FORCE_TRAY_TYPE' does not match");
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");
}
ImageUtils.determineIconSize();
_SwingTray.this.tray = SystemTray.getSystemTray();
}
@ -65,10 +62,10 @@ class _SwingTray extends MenuImpl {
@Override
public
void run() {
tray.remove(trayIcon);
removeAll();
remove();
tray.remove(trayIcon);
}
});
}
@ -118,6 +115,7 @@ class _SwingTray extends MenuImpl {
});
}
@SuppressWarnings("Duplicates")
public
void setEnabled(final boolean setEnabled) {
visible = !setEnabled;
@ -126,7 +124,6 @@ class _SwingTray extends MenuImpl {
@Override
public
void run() {
if (visible && !setEnabled) {
tray.remove(trayIcon);
}

View File

@ -13,186 +13,72 @@
* See the License for the specific language governing permissions and
* 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.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
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.Entry;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.Separator;
import dorkbox.systemTray.Status;
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
@SuppressWarnings("ForLoopReplaceableByForEach")
class MenuImpl implements Menu {
static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger();
private final int id = MenuImpl.MENU_ID_COUNTER.getAndIncrement();
public abstract
class MenuBase implements Menu {
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 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 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.parent = parent;
this._native = _native;
}
void dispatch(final Runnable runnable) {
// this will properly check if we are running on the EDT
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 dispatch(final Runnable runnable);
protected abstract
void dispatchAndWait(final Runnable runnable);
/**
* Will add a new menu entry, or update one if it already exists
* NOT ALWAYS CALLED ON EDT
* NOT ALWAYS CALLED ON DISPATCH
*/
private
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();
}
protected abstract
Entry addEntry_(final String menuText, final File imagePath, final Action callback);
/**
* 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
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();
}
protected abstract
Menu addMenu_(final String menuText, final File imagePath);
// 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);
}
}
});
}
protected abstract
void setImage_(final File imageFile);
@ -201,58 +87,29 @@ class MenuImpl implements Menu {
public
@Override
public final
Menu getParent() {
return parent;
}
public
@Override
public final
SystemTray getSystemTray() {
return systemTray;
}
@Override
public
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);
// public here so that Swing/Gtk/AppIndicator can access this
public final
String getStatus() {
synchronized (menuEntries) {
Entry entry = menuEntries.get(0);
if (entry instanceof Status) {
return entry.getText();
}
});
}
}
/**
* NOT ALWAYS CALLED ON EDT
*/
@Override
public
void addSeparator() {
dispatch(new Runnable() {
@Override
public
void run() {
synchronized (menuEntries) {
synchronized (menuEntries) {
Entry entry = new EntrySeparator(MenuImpl.this);
menuEntries.add(entry);
}
}
}
});
return null;
}
// 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) {
if (menuText == null || menuText.isEmpty()) {
return null;
@ -291,6 +149,11 @@ class MenuImpl implements Menu {
synchronized (menuEntries) {
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
final Entry entry = menuEntries.get(i);
if (entry instanceof Separator || entry instanceof Status) {
continue;
}
String text = entry.getText();
// text can be null
@ -304,13 +167,15 @@ class MenuImpl implements Menu {
}
// ignores status + separators
public
@Override
public final
Entry getFirst() {
return get(0);
}
// ignores status + separators
public
@Override
public final
Entry getLast() {
// Must be wrapped in a synchronized block for object visibility
synchronized (menuEntries) {
@ -319,7 +184,7 @@ class MenuImpl implements Menu {
for (int i = menuEntries.size()-1; i >= 0; i--) {
entry = menuEntries.get(i);
if (!(entry instanceof dorkbox.systemTray.Separator || entry instanceof Status)) {
if (!(entry instanceof Separator || entry instanceof Status)) {
return entry;
}
}
@ -330,7 +195,8 @@ class MenuImpl implements Menu {
}
// ignores status + separators
public
@Override
public final
Entry get(final int menuIndex) {
if (menuIndex < 0) {
return null;
@ -341,7 +207,7 @@ class MenuImpl implements Menu {
if (!menuEntries.isEmpty()) {
int count = 0;
for (Entry entry : menuEntries) {
if (entry instanceof dorkbox.systemTray.Separator || entry instanceof Status) {
if (entry instanceof Separator || entry instanceof Status) {
continue;
}
@ -357,13 +223,14 @@ class MenuImpl implements Menu {
return null;
}
public
@Override
public final
Entry addEntry(String menuText, Action callback) {
return addEntry(menuText, (String) null, callback);
}
public
@Override
public final
Entry addEntry(String menuText, String imagePath, Action callback) {
if (imagePath == null) {
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) {
if (imageUrl == null) {
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) {
if (imageStream == null) {
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) {
if (imageStream == null) {
return addEntry_(menuText, null, callback);
@ -407,13 +277,14 @@ class MenuImpl implements Menu {
public
@Override
public final
Menu addMenu(String menuText) {
return addMenu(menuText, (String) null);
}
public
@Override
public final
Menu addMenu(String menuText, String imagePath) {
if (imagePath == null) {
return addMenu_(menuText, null);
@ -423,7 +294,8 @@ class MenuImpl implements Menu {
}
}
public
@Override
public final
Menu addMenu(String menuText, URL imageUrl) {
if (imageUrl == 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) {
if (imageStream == null) {
return addMenu_(menuText, null);
@ -443,7 +316,8 @@ class MenuImpl implements Menu {
}
}
public
@Override
public final
Menu addMenu(String menuText, InputStream imageStream) {
if (imageStream == 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
public
String getText() {
return text;
}
@Override
public
void setText(final String newText) {
text = newText;
dispatch(new Runnable() {
@Override
public
void run() {
renderText(newText);
}
});
}
@Override
public
public final
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
*/
public
@Override
public final
void remove(final Entry entry) {
if (entry == null) {
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
*/
@Override
public
public final
void remove(final Menu menu) {
final Menu parent = getParent();
if (parent == null) {
@ -668,14 +462,14 @@ class MenuImpl implements Menu {
@Override
public
void run() {
((MenuImpl) parent).remove__(_this);
((MenuBase) parent).remove__(_this);
}
});
}
}
// NOT ALWAYS CALLED ON EDT
private
protected
void remove__(final Object menuEntry) {
try {
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
*/
public
public final
void remove(final String menuText) {
dispatchAndWait(new Runnable() {
@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
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() {
dispatch(new Runnable() {
@Override
@ -762,7 +556,7 @@ class MenuImpl implements Menu {
void run() {
synchronized (menuEntries) {
// 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) {
entry.remove();
}
@ -792,7 +586,7 @@ class MenuImpl implements Menu {
return false;
}
MenuImpl other = (MenuImpl) obj;
MenuBase other = (MenuBase) obj;
return this.id == other.id;
}
}

View File

@ -18,6 +18,7 @@ package dorkbox.systemTray.util;
import static dorkbox.systemTray.SystemTray.logger;
import java.awt.Robot;
import java.awt.event.KeyEvent;
import java.util.Locale;
import dorkbox.systemTray.SystemTray;
@ -31,11 +32,11 @@ import javassist.CtMethod;
* Fixes issues with some java runtimes
*/
public
class WindowsSystemTraySwing {
class SystemTrayFixes {
// oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that!
public static void fix() {
// if we are using swing (in windows only) the icon size is usually incorrect. Here we have to fix that.
// https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788
public static void fixWindows() {
if (!OS.isWindows()) {
return;
}
@ -61,7 +62,7 @@ class WindowsSystemTraySwing {
(null != m.invoke(cl, "java.awt.SystemTray"));
} catch (Throwable e) {
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);
}
}
// 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;
}
}