forked from dorkbox/SystemTray
Removing logic to choose between swing/native menus (Swing w/
appindicators is broken)
This commit is contained in:
parent
ec9499c1bb
commit
3c24139544
@ -46,8 +46,6 @@ import dorkbox.systemTray.nativeUI._AppIndicatorNativeTray;
|
||||
import dorkbox.systemTray.nativeUI._AwtTray;
|
||||
import dorkbox.systemTray.nativeUI._GtkStatusIconNativeTray;
|
||||
import dorkbox.systemTray.swingUI.SwingUI;
|
||||
import dorkbox.systemTray.swingUI._AppIndicatorSwingTray;
|
||||
import dorkbox.systemTray.swingUI._GtkStatusIconSwingTray;
|
||||
import dorkbox.systemTray.swingUI._SwingTray;
|
||||
import dorkbox.systemTray.util.ImageUtils;
|
||||
import dorkbox.systemTray.util.JavaFX;
|
||||
@ -81,14 +79,12 @@ class SystemTray {
|
||||
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
|
||||
|
||||
public enum TrayType {
|
||||
/** Will choose as a 'best guess' which tray type to use based on if native is requested or not */
|
||||
/** Will choose as a 'best guess' which tray type to use */
|
||||
AutoDetect,
|
||||
/** if native, will have Gtk Menus. Non-native will have Swing menus */
|
||||
GtkStatusIcon,
|
||||
/** if native, will have Gtk Menus. Non-native will have Swing menus */
|
||||
AppIndicator,
|
||||
/** if native, will have AWT Menus. Non-native will have Swing menus */
|
||||
Swing
|
||||
Swing,
|
||||
AWT
|
||||
}
|
||||
|
||||
@Property
|
||||
@ -128,7 +124,7 @@ class SystemTray {
|
||||
|
||||
@Property
|
||||
/**
|
||||
* Forces the system tray detection to be AutoDetect, GtkStatusIcon, AppIndicator, or Swing.
|
||||
* Forces the system tray detection to be AutoDetect, GtkStatusIcon, AppIndicator, Swing, or AWT.
|
||||
* <p>
|
||||
* This is an advanced feature, and it is recommended to leave at AutoDetect.
|
||||
*/
|
||||
@ -194,45 +190,36 @@ class SystemTray {
|
||||
private static
|
||||
boolean isTrayType(final Class<? extends Tray> tray, final TrayType trayType) {
|
||||
switch (trayType) {
|
||||
case GtkStatusIcon: return (tray == _GtkStatusIconSwingTray.class || tray == _GtkStatusIconNativeTray.class);
|
||||
case AppIndicator: return (tray == _AppIndicatorSwingTray.class || tray == _AppIndicatorNativeTray.class);
|
||||
case Swing: return (tray == _SwingTray.class || tray == _AwtTray.class);
|
||||
case GtkStatusIcon: return tray == _GtkStatusIconNativeTray.class;
|
||||
case AppIndicator: return tray == _AppIndicatorNativeTray.class;
|
||||
case Swing: return tray == _SwingTray.class;
|
||||
case AWT: return tray == _AwtTray.class;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static
|
||||
Class<? extends Tray> selectType(final boolean useNativeMenus, final TrayType trayType) throws Exception {
|
||||
Class<? extends Tray> selectType(final TrayType trayType) throws Exception {
|
||||
if (trayType == TrayType.GtkStatusIcon) {
|
||||
if (useNativeMenus) {
|
||||
return _GtkStatusIconNativeTray.class;
|
||||
} else {
|
||||
return _GtkStatusIconSwingTray.class;
|
||||
}
|
||||
} else if (trayType == TrayType.AppIndicator) {
|
||||
if (useNativeMenus) {
|
||||
return _AppIndicatorNativeTray.class;
|
||||
}
|
||||
else {
|
||||
return _AppIndicatorSwingTray.class;
|
||||
}
|
||||
return _GtkStatusIconNativeTray.class;
|
||||
}
|
||||
else if (trayType == TrayType.AppIndicator) {
|
||||
return _AppIndicatorNativeTray.class;
|
||||
}
|
||||
else if (trayType == TrayType.Swing) {
|
||||
if (useNativeMenus) {
|
||||
return _AwtTray.class;
|
||||
}
|
||||
else {
|
||||
return _SwingTray.class;
|
||||
}
|
||||
return _SwingTray.class;
|
||||
}
|
||||
else if (trayType == TrayType.AWT) {
|
||||
return _AwtTray.class;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static
|
||||
Class<? extends Tray> selectTypeQuietly(final boolean useNativeMenus, final TrayType trayType) {
|
||||
Class<? extends Tray> selectTypeQuietly(final TrayType trayType) {
|
||||
try {
|
||||
return selectType(useNativeMenus, trayType);
|
||||
return selectType(trayType);
|
||||
} catch (Throwable t) {
|
||||
if (DEBUG) {
|
||||
logger.error("Cannot initialize {}", trayType.name(), t);
|
||||
@ -243,7 +230,7 @@ class SystemTray {
|
||||
}
|
||||
|
||||
@SuppressWarnings({"ConstantConditions", "StatementWithEmptyBody"})
|
||||
private static void init(boolean useNativeMenus) {
|
||||
private static void init() {
|
||||
if (systemTray != null) {
|
||||
return;
|
||||
}
|
||||
@ -279,10 +266,6 @@ class SystemTray {
|
||||
|
||||
// cannot mix Swing and SWT on MacOSX (for all versions of java) so we force native menus instead, which work just fine with SWT
|
||||
// http://mail.openjdk.java.net/pipermail/bsd-port-dev/2008-December/000173.html
|
||||
if (isSwtLoaded) {
|
||||
useNativeMenus = true;
|
||||
logger.warn("MacOSX does not support SWT + Swing at the same time. Forcing Native menus instead.");
|
||||
}
|
||||
}
|
||||
|
||||
// no tray in a headless environment
|
||||
@ -298,28 +281,17 @@ class SystemTray {
|
||||
// OSx can use Swing (non-native) or AWT (native) .
|
||||
// Linux can use Swing (non-native) menus + (native Icon via GTK or AppIndicator), GtkStatusIcon (native), or AppIndicator (native)
|
||||
if (OS.isWindows()) {
|
||||
if (useNativeMenus && AUTO_FIX_INCONSISTENCIES) {
|
||||
// windows MUST use swing non-native only. AWT (native) looks terrible!
|
||||
useNativeMenus = false;
|
||||
logger.warn("Windows cannot use a 'native' SystemTray, defaulting to non-native SwingUI");
|
||||
}
|
||||
|
||||
if (FORCE_TRAY_TYPE != TrayType.Swing) {
|
||||
// windows MUST use swing only!
|
||||
FORCE_TRAY_TYPE = TrayType.AutoDetect;
|
||||
logger.warn("Windows cannot use the '" + FORCE_TRAY_TYPE + "' SystemTray type, defaulting to SwingUI");
|
||||
logger.warn("Windows cannot use the '" + FORCE_TRAY_TYPE + "' SystemTray type, defaulting to Swing");
|
||||
}
|
||||
}
|
||||
else if (OS.isMacOsX()) {
|
||||
if (FORCE_TRAY_TYPE != TrayType.Swing ) {
|
||||
if (useNativeMenus) {
|
||||
logger.warn("MacOsX cannot use the '" + FORCE_TRAY_TYPE + "' SystemTray type, defaulting to SwingUI");
|
||||
} else {
|
||||
logger.warn("MacOsX cannot use the '" + FORCE_TRAY_TYPE + "' SystemTray type, defaulting to AWT Native UI");
|
||||
}
|
||||
|
||||
// MacOsX MUST use swing (and AWT) only!
|
||||
FORCE_TRAY_TYPE = TrayType.AutoDetect;
|
||||
logger.warn("MacOS cannot use the '" + FORCE_TRAY_TYPE + "' SystemTray type, defaulting to Awt");
|
||||
}
|
||||
}
|
||||
else if (OS.isLinux() || OS.isUnix()) {
|
||||
@ -399,7 +371,6 @@ class SystemTray {
|
||||
logger.debug("Is AutoTraySize? {}", AUTO_TRAY_SIZE);
|
||||
logger.debug("Is JavaFX detected? {}", isJavaFxLoaded);
|
||||
logger.debug("Is SWT detected? {}", isSwtLoaded);
|
||||
logger.debug("Is using native menus? {}", useNativeMenus);
|
||||
logger.debug("Forced tray type: {}", FORCE_TRAY_TYPE.name());
|
||||
logger.debug("FORCE_GTK2: {}", FORCE_GTK2);
|
||||
}
|
||||
@ -413,10 +384,22 @@ class SystemTray {
|
||||
if (OS.isWindows()) {
|
||||
// windows is funky, and is hardcoded to 16x16. We fix that.
|
||||
SystemTrayFixes.fixWindows();
|
||||
|
||||
try {
|
||||
trayType = selectType(TrayType.Swing);
|
||||
} catch (Throwable e) {
|
||||
logger.error("You might need to grant the AWTPermission `accessSystemTray` to the SecurityManager.");
|
||||
}
|
||||
}
|
||||
else if (OS.isMacOsX() && useNativeMenus) {
|
||||
else if (OS.isMacOsX()) {
|
||||
// macosx doesn't respond to all buttons (but should)
|
||||
SystemTrayFixes.fixMacOS();
|
||||
|
||||
try {
|
||||
trayType = selectType(TrayType.AWT);
|
||||
} catch (Throwable e) {
|
||||
logger.error("You might need to grant the AWTPermission `accessSystemTray` to the SecurityManager.");
|
||||
}
|
||||
}
|
||||
else if ((OS.isLinux() || OS.isUnix()) && FORCE_TRAY_TYPE != TrayType.Swing) {
|
||||
// see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running
|
||||
@ -427,7 +410,7 @@ class SystemTray {
|
||||
// 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(useNativeMenus, SystemTray.FORCE_TRAY_TYPE);
|
||||
trayType = selectTypeQuietly(SystemTray.FORCE_TRAY_TYPE);
|
||||
}
|
||||
|
||||
|
||||
@ -461,7 +444,7 @@ class SystemTray {
|
||||
if (trayType == null) {
|
||||
// Unity is a weird combination. It's "Gnome", but it's not "Gnome Shell".
|
||||
if ("unity".equalsIgnoreCase(XDG)) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.AppIndicator);
|
||||
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.
|
||||
@ -469,18 +452,18 @@ class SystemTray {
|
||||
// see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25
|
||||
|
||||
// so far, it is OK to use GtkStatusIcon on XFCE <-> XFCE4 inclusive
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if ("lxde".equalsIgnoreCase(XDG)) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if ("kde".equalsIgnoreCase(XDG)) {
|
||||
if (OSUtil.Linux.isFedora()) {
|
||||
// Fedora KDE requires GtkStatusIcon
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
} else {
|
||||
// kde (at least, plasma 5.5.6) requires appindicator
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.AppIndicator);
|
||||
trayType = selectTypeQuietly(TrayType.AppIndicator);
|
||||
}
|
||||
|
||||
// kde 5.8+ is "high DPI", so we need to adjust the scale. Image resize will do that
|
||||
@ -489,14 +472,9 @@ class SystemTray {
|
||||
// elementaryOS. It only supports appindicator (not gtkstatusicon)
|
||||
// http://bazaar.launchpad.net/~wingpanel-devs/wingpanel/trunk/view/head:/sample/SampleIndicator.vala
|
||||
|
||||
if (!useNativeMenus && AUTO_FIX_INCONSISTENCIES) {
|
||||
logger.warn("Cannot use non-native menus with pantheon DE. Forcing native menus.");
|
||||
useNativeMenus = true;
|
||||
}
|
||||
|
||||
// ElementaryOS shows the checkbox on the right, everyone else is on the left.
|
||||
// With eOS, we CANNOT show the spacer image. It does not work.
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.AppIndicator);
|
||||
trayType = selectTypeQuietly(TrayType.AppIndicator);
|
||||
}
|
||||
else if ("gnome".equalsIgnoreCase(XDG)) {
|
||||
// check other DE
|
||||
@ -517,22 +495,22 @@ class SystemTray {
|
||||
}
|
||||
|
||||
// 23 is gtk, 24/25 is gtk (but also wrong size unless we adjust it. ImageUtil automatically does this)
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if (OSUtil.Linux.isUbuntu()) {
|
||||
// so far, because of the interaction between gnome3 + ubuntu, the GtkStatusIcon miraculously works.
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if (OSUtil.Unix.isFreeBSD()) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else {
|
||||
// arch likely will have problems unless the correct/appropriate libraries are installed.
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.AppIndicator);
|
||||
trayType = selectTypeQuietly(TrayType.AppIndicator);
|
||||
}
|
||||
}
|
||||
else if ("cinnamon".equalsIgnoreCase(GDM)) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if ("default".equalsIgnoreCase(GDM)) {
|
||||
// this can be gnome3 on debian
|
||||
@ -542,16 +520,16 @@ class SystemTray {
|
||||
logger.warn("Debian with Gnome detected. SystemTray support is not known to work.");
|
||||
}
|
||||
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if ("gnome-classic".equalsIgnoreCase(GDM)) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if ("gnome-fallback".equalsIgnoreCase(GDM)) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
else if ("ubuntu".equalsIgnoreCase(GDM)) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.AppIndicator);
|
||||
trayType = selectTypeQuietly(TrayType.AppIndicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -584,7 +562,7 @@ class SystemTray {
|
||||
if (readLine != null && readLine.contains("indicator-app")) {
|
||||
// make sure we can also load the library (it might be the wrong version)
|
||||
try {
|
||||
trayType = selectType(useNativeMenus, TrayType.AppIndicator);
|
||||
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);
|
||||
@ -609,7 +587,7 @@ class SystemTray {
|
||||
|
||||
// fallback...
|
||||
if (trayType == null) {
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
logger.warn("Unable to determine the system window manager type. Falling back to GtkStatusIcon.");
|
||||
}
|
||||
|
||||
@ -659,18 +637,6 @@ class SystemTray {
|
||||
}
|
||||
}
|
||||
|
||||
// this is likely windows OR mac
|
||||
if (trayType == null) {
|
||||
try {
|
||||
trayType = selectType(useNativeMenus, TrayType.Swing);
|
||||
} catch (Throwable e) {
|
||||
if (DEBUG) {
|
||||
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.", e);
|
||||
} else {
|
||||
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trayType == null) {
|
||||
// unsupported tray, or unknown type
|
||||
@ -681,7 +647,7 @@ class SystemTray {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageUtils.determineIconSize(!useNativeMenus);
|
||||
ImageUtils.determineIconSize();
|
||||
|
||||
final AtomicReference<Tray> reference = new AtomicReference<Tray>();
|
||||
|
||||
@ -732,7 +698,7 @@ class SystemTray {
|
||||
if (isTrayType(trayType, TrayType.AppIndicator)) {
|
||||
if (Gtk.isGtk2 && AppIndicator.isVersion3) {
|
||||
try {
|
||||
trayType = selectType(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
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) {
|
||||
@ -753,7 +719,7 @@ class SystemTray {
|
||||
if (!AppIndicator.isLoaded) {
|
||||
// YIKES. Try to fallback to GtkStatusIndicator, since AppIndicator couldn't load.
|
||||
logger.warn("Unable to initialize the AppIndicator correctly, falling back to GtkStatusIcon type");
|
||||
trayType = selectTypeQuietly(useNativeMenus, TrayType.GtkStatusIcon);
|
||||
trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -827,9 +793,7 @@ class SystemTray {
|
||||
// verify that we have what we are expecting.
|
||||
if (OS.isWindows() && systemTrayMenu instanceof SwingUI) {
|
||||
// this configuration is OK.
|
||||
} else if (useNativeMenus && systemTrayMenu instanceof NativeUI) {
|
||||
// this configuration is OK.
|
||||
} else if (!useNativeMenus && systemTrayMenu instanceof SwingUI) {
|
||||
} else if (systemTrayMenu instanceof NativeUI) {
|
||||
// this configuration is OK.
|
||||
} else {
|
||||
logger.error("Unable to correctly initialize the System Tray. Please write an issue and include your " +
|
||||
@ -879,22 +843,6 @@ class SystemTray {
|
||||
return "2.20";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* be granted in order to get the {@code SystemTray} instance. Otherwise this will return null.
|
||||
*/
|
||||
public static
|
||||
SystemTray getSwing() {
|
||||
init(false);
|
||||
return systemTray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables native menus on Linux/OSX instead of the custom swing menu. Windows will always use a custom Swing menu. The drawback is
|
||||
* that this menu is native, and sometimes native menus looks absolutely HORRID.
|
||||
@ -906,8 +854,8 @@ class SystemTray {
|
||||
* be granted in order to get the {@code SystemTray} instance. Otherwise this will return null.
|
||||
*/
|
||||
public static
|
||||
SystemTray getNative() {
|
||||
init(true);
|
||||
SystemTray get() {
|
||||
init();
|
||||
return systemTray;
|
||||
}
|
||||
|
||||
|
@ -1,337 +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.swingUI;
|
||||
|
||||
import java.awt.MouseInfo;
|
||||
import java.awt.Point;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.ptr.PointerByReference;
|
||||
|
||||
import dorkbox.systemTray.MenuItem;
|
||||
import dorkbox.systemTray.SystemTray;
|
||||
import dorkbox.systemTray.Tray;
|
||||
import dorkbox.systemTray.gnomeShell.Extension;
|
||||
import dorkbox.systemTray.jna.linux.AppIndicator;
|
||||
import dorkbox.systemTray.jna.linux.AppIndicatorInstanceStruct;
|
||||
import dorkbox.systemTray.jna.linux.GEventCallback;
|
||||
import dorkbox.systemTray.jna.linux.GdkEventButton;
|
||||
import dorkbox.systemTray.jna.linux.Gobject;
|
||||
import dorkbox.systemTray.jna.linux.Gtk;
|
||||
import dorkbox.systemTray.util.ImageUtils;
|
||||
import dorkbox.util.SwingUtil;
|
||||
|
||||
/**
|
||||
* 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 _AppIndicatorSwingTray extends Tray implements SwingUI {
|
||||
private volatile AppIndicatorInstanceStruct appIndicator;
|
||||
private boolean isActive = false;
|
||||
private volatile Runnable popupRunnable;
|
||||
|
||||
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
|
||||
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
||||
|
||||
// necessary to prevent GC on these objects
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private GEventCallback gtkCallback;
|
||||
|
||||
|
||||
// necessary to provide a menu (which we draw over) so we get the "on open" event when the menu is opened via clicking
|
||||
private Pointer dummyMenu;
|
||||
|
||||
// is the system tray visible or not.
|
||||
private volatile boolean visible = true;
|
||||
private volatile File imageFile;
|
||||
|
||||
// has the name already been set for the indicator?
|
||||
private volatile boolean setName = false;
|
||||
|
||||
// appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus)
|
||||
// they ALSO do not support tooltips!!
|
||||
// https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12
|
||||
|
||||
public
|
||||
_AppIndicatorSwingTray(final SystemTray systemTray) {
|
||||
super();
|
||||
|
||||
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);
|
||||
|
||||
createAppIndicatorMenu();
|
||||
}
|
||||
});
|
||||
|
||||
Gtk.waitForStartup();
|
||||
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
// we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization.
|
||||
final SwingMenu swingMenu = new SwingMenu(null) {
|
||||
@Override
|
||||
public
|
||||
void setEnabled(final MenuItem menuItem) {
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
boolean enabled = menuItem.getEnabled();
|
||||
|
||||
if (visible && !enabled) {
|
||||
// STATUS_PASSIVE hides the indicator
|
||||
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE);
|
||||
visible = false;
|
||||
}
|
||||
else if (!visible && enabled) {
|
||||
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE);
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setImage(final MenuItem menuItem) {
|
||||
imageFile = menuItem.getImage();
|
||||
if (imageFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Gtk.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);
|
||||
|
||||
// now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set
|
||||
hookMenuOpen();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// needs to be on EDT
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
((TrayPopup) _native).setTitleBarImage(imageFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setText(final MenuItem menuItem) {
|
||||
// no op
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setShortcut(final MenuItem menuItem) {
|
||||
// no op
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void remove() {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// does not need to be called on the dispatch (it does that)
|
||||
Gtk.shutdownGui();
|
||||
|
||||
super.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TrayPopup popupMenu = (TrayPopup) swingMenu._native;
|
||||
popupMenu.pack();
|
||||
popupMenu.setFocusable(true);
|
||||
popupMenu.setOnHideRunnable(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
if (appIndicator == null) {
|
||||
// if we are shutting down, don't hook the menu again
|
||||
return;
|
||||
}
|
||||
|
||||
// Such ugly hacks to get AppIndicator support properly working. This is so horrible I am ashamed.
|
||||
Gtk.dispatchAndWait(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
createAppIndicatorMenu();
|
||||
hookMenuOpen();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
popupRunnable = new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
Point point = MouseInfo.getPointerInfo()
|
||||
.getLocation();
|
||||
|
||||
TrayPopup popupMenu = (TrayPopup) swingMenu._native;
|
||||
popupMenu.doShow(point, SystemTray.DEFAULT_TRAY_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
bind(swingMenu, null, systemTray);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private
|
||||
void hookMenuOpen() {
|
||||
// now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set
|
||||
PointerByReference menuServer = new PointerByReference();
|
||||
PointerByReference rootMenuItem = new PointerByReference();
|
||||
|
||||
Gobject.g_object_get(appIndicator.getPointer(), "dbus-menu-server", menuServer, null);
|
||||
Gobject.g_object_get(menuServer.getValue(), "root-node", rootMenuItem, null);
|
||||
|
||||
gtkCallback = new GEventCallback() {
|
||||
@Override
|
||||
public
|
||||
void callback(Pointer notUsed, final GdkEventButton event) {
|
||||
Gtk.gtk_menu_shell_deactivate(dummyMenu);
|
||||
SwingUtil.invokeLater(popupRunnable);
|
||||
}
|
||||
};
|
||||
|
||||
Gobject.g_signal_connect_object(rootMenuItem.getValue(), "about-to-show", gtkCallback, null, 0);
|
||||
}
|
||||
|
||||
private
|
||||
void createAppIndicatorMenu() {
|
||||
dummyMenu = Gtk.gtk_menu_new();
|
||||
Pointer item = Gtk.gtk_image_menu_item_new_with_mnemonic("");
|
||||
Gtk.gtk_menu_shell_append(dummyMenu, item);
|
||||
Gtk.gtk_widget_show_all(item);
|
||||
|
||||
AppIndicator.app_indicator_set_menu(appIndicator, dummyMenu);
|
||||
|
||||
if (!setName) {
|
||||
setName = true;
|
||||
|
||||
// in GNOME, 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
|
||||
|
||||
// can cause (potentially)
|
||||
// GLib-GIO-CRITICAL **: g_dbus_connection_emit_signal: assertion 'object_path != NULL && g_variant_is_object_path (object_path)' failed
|
||||
// Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed
|
||||
|
||||
// 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
|
||||
|
||||
// additionally, this is required to be set HERE (not somewhere else)
|
||||
AppIndicator.app_indicator_set_title(appIndicator, Extension.DEFAULT_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12
|
||||
@Override
|
||||
protected
|
||||
void setTooltip_(final String tooltipText) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
boolean hasImage() {
|
||||
return imageFile != null;
|
||||
}
|
||||
}
|
@ -1,259 +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.swingUI;
|
||||
|
||||
import java.awt.MouseInfo;
|
||||
import java.awt.Point;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import javax.swing.JPopupMenu;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
import dorkbox.systemTray.MenuItem;
|
||||
import dorkbox.systemTray.SystemTray;
|
||||
import dorkbox.systemTray.Tray;
|
||||
import dorkbox.systemTray.gnomeShell.Extension;
|
||||
import dorkbox.systemTray.jna.linux.GEventCallback;
|
||||
import dorkbox.systemTray.jna.linux.GdkEventButton;
|
||||
import dorkbox.systemTray.jna.linux.Gobject;
|
||||
import dorkbox.systemTray.jna.linux.Gtk;
|
||||
import dorkbox.util.SwingUtil;
|
||||
|
||||
/**
|
||||
* Class for handling all system tray interactions via GTK.
|
||||
* <p/>
|
||||
* 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.
|
||||
*
|
||||
* http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c
|
||||
* https://github.com/djdeath/glib/blob/master/gobject/gobject.c
|
||||
*/
|
||||
@SuppressWarnings("Duplicates")
|
||||
public
|
||||
class _GtkStatusIconSwingTray extends Tray implements SwingUI {
|
||||
private volatile Pointer trayIcon;
|
||||
|
||||
// have to save these in a field to prevent GC on the objects (since they go out-of-scope from java)
|
||||
// see: https://github.com/java-native-access/jna/blob/master/www/CallbacksAndClosures.md
|
||||
private GEventCallback gtkCallback = null;
|
||||
|
||||
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
||||
|
||||
private volatile boolean isActive = false;
|
||||
|
||||
// is the system tray visible or not.
|
||||
private volatile boolean visible = true;
|
||||
private volatile File imageFile;
|
||||
private volatile Runnable popupRunnable;
|
||||
|
||||
// called on the EDT
|
||||
public
|
||||
_GtkStatusIconSwingTray(final SystemTray systemTray) {
|
||||
super();
|
||||
|
||||
Gtk.startGui();
|
||||
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
trayIcon = Gtk.gtk_status_icon_new();
|
||||
|
||||
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) {
|
||||
// show the swing menu on the EDT
|
||||
SwingUtil.invokeLater(popupRunnable);
|
||||
}
|
||||
}
|
||||
};
|
||||
Gobject.g_signal_connect_object(trayIcon, "button_press_event", gtkCallback, null, 0);
|
||||
}
|
||||
});
|
||||
|
||||
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() {
|
||||
// in GNOME 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, Extension.DEFAULT_NAME);
|
||||
|
||||
// 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 or Gnome is dispatching the events.
|
||||
// BUT this is REQUIRED when running JavaFX or Gnome 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 || Tray.usingGnome) {
|
||||
Gtk.gtk_status_icon_set_name(trayIcon, Extension.DEFAULT_NAME);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization.
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
final SwingMenu swingMenu = new SwingMenu(null) {
|
||||
@Override
|
||||
public
|
||||
void setEnabled(final MenuItem menuItem) {
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
boolean enabled = menuItem.getEnabled();
|
||||
|
||||
if (visible && !enabled) {
|
||||
Gtk.gtk_status_icon_set_visible(trayIcon, enabled);
|
||||
visible = false;
|
||||
}
|
||||
else if (!visible && enabled) {
|
||||
Gtk.gtk_status_icon_set_visible(trayIcon, enabled);
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setImage(final MenuItem menuItem) {
|
||||
imageFile = menuItem.getImage();
|
||||
if (imageFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
Gtk.gtk_status_icon_set_from_file(trayIcon, imageFile.getAbsolutePath());
|
||||
|
||||
if (!isActive) {
|
||||
isActive = true;
|
||||
Gtk.gtk_status_icon_set_visible(trayIcon, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// needs to be on EDT
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
((TrayPopup) _native).setTitleBarImage(imageFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setText(final MenuItem menuItem) {
|
||||
// no op
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setShortcut(final MenuItem menuItem) {
|
||||
// no op
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void remove() {
|
||||
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// does not need to be called on the dispatch (it does that)
|
||||
Gtk.shutdownGui();
|
||||
|
||||
super.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
JPopupMenu popupMenu = (JPopupMenu) swingMenu._native;
|
||||
popupMenu.pack();
|
||||
popupMenu.setFocusable(true);
|
||||
|
||||
popupRunnable = new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
Point point = MouseInfo.getPointerInfo()
|
||||
.getLocation();
|
||||
|
||||
TrayPopup popupMenu = (TrayPopup) swingMenu._native;
|
||||
popupMenu.doShow(point, 0);
|
||||
}
|
||||
};
|
||||
|
||||
bind(swingMenu, null, systemTray);
|
||||
}
|
||||
});
|
||||
|
||||
// do we need to install the GNOME extension??
|
||||
Tray.installExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void setTooltip_(final String tooltipText) {
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
Gtk.gtk_status_icon_set_tooltip_text(trayIcon, tooltipText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean hasImage() {
|
||||
return imageFile != null;
|
||||
}
|
||||
}
|
@ -75,7 +75,7 @@ class ImageUtils {
|
||||
public static volatile Font ENTRY_FONT = null;
|
||||
|
||||
public static
|
||||
void determineIconSize(boolean trayHasSwingMenus) {
|
||||
void determineIconSize() {
|
||||
double trayScalingFactor = 0;
|
||||
double menuScalingFactor = 0;
|
||||
|
||||
@ -295,8 +295,8 @@ class ImageUtils {
|
||||
}
|
||||
|
||||
// this must be a JMenuItem component, because that is the component we are setting the font on.
|
||||
// this is only important to do if we are a swing tray type
|
||||
if (trayHasSwingMenus) {
|
||||
// this is only important to do if we are a swing tray type, which ONLY happens in Windows
|
||||
if (OS.isWindows()) {
|
||||
// must be a plain style font
|
||||
Font font = new JMenuItem().getFont().deriveFont(Font.PLAIN);
|
||||
|
||||
|
@ -56,8 +56,7 @@ class TestTray {
|
||||
|
||||
public
|
||||
TestTray() {
|
||||
this.systemTray = SystemTray.getSwing();
|
||||
// this.systemTray = SystemTray.getNative();
|
||||
this.systemTray = SystemTray.get();
|
||||
if (systemTray == null) {
|
||||
throw new RuntimeException("Unable to load SystemTray!");
|
||||
}
|
||||
|
@ -114,8 +114,7 @@ class TestTrayJavaFX {
|
||||
primaryStage.show();
|
||||
|
||||
|
||||
this.systemTray = SystemTray.getSwing();
|
||||
// this.systemTray = SystemTray.getNative();
|
||||
this.systemTray = SystemTray.get();
|
||||
if (systemTray == null) {
|
||||
throw new RuntimeException("Unable to load SystemTray!");
|
||||
}
|
||||
|
@ -73,13 +73,12 @@ class TestTraySwt {
|
||||
helloWorldTest.pack();
|
||||
|
||||
|
||||
systemTray.setTooltip("Mail Checker");
|
||||
this.systemTray = SystemTray.getSwing();
|
||||
// this.systemTray = SystemTray.getNative();
|
||||
this.systemTray = SystemTray.get();
|
||||
if (systemTray == null) {
|
||||
throw new RuntimeException("Unable to load SystemTray!");
|
||||
}
|
||||
|
||||
systemTray.setTooltip("Mail Checker");
|
||||
systemTray.setImage(LT_GRAY_TRAIN);
|
||||
systemTray.setStatus("No Mail");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user