SystemTray/src/dorkbox/systemTray/util/AutoDetectTrayType.java

459 lines
20 KiB
Java

/*
* Copyright 2021 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.util;
import static dorkbox.systemTray.SystemTray.DEBUG;
import static dorkbox.systemTray.SystemTray.logger;
import java.io.BufferedReader;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import dorkbox.os.OS;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTray.TrayType;
import dorkbox.systemTray.Tray;
import dorkbox.systemTray.gnomeShell.AppIndicatorExtension;
import dorkbox.systemTray.gnomeShell.DummyFile;
import dorkbox.systemTray.gnomeShell.LegacyExtension;
import dorkbox.systemTray.ui.awt._AwtTray;
import dorkbox.systemTray.ui.gtk._AppIndicatorNativeTray;
import dorkbox.systemTray.ui.gtk._GtkStatusIconNativeTray;
import dorkbox.systemTray.ui.osx._OsxAwtTray;
import dorkbox.systemTray.ui.swing._SwingTray;
import dorkbox.systemTray.ui.swing._WindowsNativeTray;
import dorkbox.util.FileUtil;
/**
* Auto-Detection of system tray type, with basic conversion utilities
*/
public
class AutoDetectTrayType {
// we want to have "singleton" access for a specified SystemTray NAME, so that (should one want) different parts of an application
// can add entries to the menu without having to pass the SystemTray object around
private static final Map<String, SystemTray> traySingletons = new HashMap<>();
public static
Class<? extends Tray> selectType(final TrayType trayType) {
if (trayType == TrayType.Gtk) {
return _GtkStatusIconNativeTray.class;
}
else if (trayType == TrayType.AppIndicator) {
return _AppIndicatorNativeTray.class;
}
else if (trayType == TrayType.WindowsNative) {
return _WindowsNativeTray.class;
}
else if (trayType == TrayType.Swing) {
return _SwingTray.class;
}
else if (trayType == TrayType.Osx) {
return _OsxAwtTray.class;
}
else if (trayType == TrayType.Awt) {
return _AwtTray.class;
}
return null;
}
public static
TrayType fromClass(final Class<? extends Tray> trayClass) {
if (trayClass == _GtkStatusIconNativeTray.class) {
return TrayType.Gtk;
}
else if (trayClass == _AppIndicatorNativeTray.class) {
return TrayType.AppIndicator;
}
else if (trayClass == _WindowsNativeTray.class) {
return TrayType.WindowsNative;
}
else if (trayClass == _SwingTray.class) {
return TrayType.Swing;
}
else if (trayClass == _OsxAwtTray.class) {
return TrayType.Osx;
}
else if (trayClass == _AwtTray.class) {
return TrayType.Awt;
}
return null;
}
public static
boolean isTrayType(final Class<? extends Tray> tray, final TrayType trayType) {
switch (trayType) {
case Gtk: return tray == _GtkStatusIconNativeTray.class;
case AppIndicator: return tray == _AppIndicatorNativeTray.class;
case WindowsNative: return tray == _WindowsNativeTray.class;
case Swing: return tray == _SwingTray.class;
case Osx: return tray == _OsxAwtTray.class;
case Awt: return tray == _AwtTray.class;
}
return false;
}
/**
* @return what the default "autodetect" tray type should be
*/
@SuppressWarnings("DuplicateBranchesInSwitch")
public static
Class<? extends Tray> get(final String trayName) {
if (OS.INSTANCE.isWindows()) {
return selectType(TrayType.WindowsNative);
}
else if (OS.INSTANCE.isMacOsX()) {
// macOS can ONLY use AWT if you want it to follow the L&F of the OS. It is the default.
return selectType(TrayType.Osx);
}
else if ((OS.INSTANCE.isLinux() || OS.INSTANCE.isUnix())) {
// 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.
// https://github.com/syncthing/syncthing-gtk/blob/b7a3bc00e3bb6d62365ae62b5395370f3dcc7f55/syncthing_gtk/statusicon.py
// quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least
OS.DesktopEnv.Env de = OS.DesktopEnv.INSTANCE.getEnv();
if (DEBUG) {
logger.debug("Currently using the '{}' desktop environment" + OS.INSTANCE.getLINE_SEPARATOR() + OS.Linux.INSTANCE.getInfo(), de);
}
switch (de) {
case Gnome: {
// check other DE / OS combos that are based on gnome
String GDM = System.getenv("GDMSESSION");
// fix for some linux OS where this session variable is not set
if (GDM == null) {
if (DEBUG) {
logger.debug("GDMSESSION value is not set by OS. Checking '/etc/os-release' for more info.");
}
// see: https://github.com/dorkbox/SystemTray/issues/125
if (OS.Linux.INSTANCE.isPop()) {
GDM = "ubuntu"; // special case for popOS! (it is ubuntu-like, but does not set GDMSESSION)
if (DEBUG) {
logger.debug("Detected popOS! Using 'ubuntu' for that configuration.");
}
}
}
if (DEBUG) {
logger.debug("Currently using the '{}' session type", GDM);
}
if ("gnome".equalsIgnoreCase(GDM) || "default".equalsIgnoreCase(GDM)) {
// UGH. At least ubuntu un-butchers gnome.
if (OS.Linux.INSTANCE.isUbuntu()) {
// so far, because of the interaction between gnome3 + ubuntu, the GtkStatusIcon miraculously works.
return selectType(TrayType.Gtk);
}
// "default" can be gnome3 on debian/kali
// for everyone else, we have to check the gnome version.
// gnome2 -> everything is glorious and just works.
// gnome3 -> someone started sniffing glue.
// < 3.16 - It's in the notification tray. SystemTray works, but will only show via SUPER+M.
// < 3.26 - (3.16 introduced the legacy tray, and removed gtkstatus icon "normal" placement) legacy icons via shell extensions work + GTK workarounds
// >= 3.26 - (3.26 removed the legacy tray) app-indicator icons via shell extensions + libappindicator work
String gnomeVersion = OS.DesktopEnv.INSTANCE.getGnomeVersion();
if (gnomeVersion == null) {
// this shouldn't ever happen!
logger.error("GNOME shell detected, but UNDEFINED shell version. This should never happen. Falling back to GtkStatusIcon. " +
"Please create an issue with as many details as possible.");
return selectType(TrayType.Gtk);
}
if (DEBUG) {
logger.debug("Gnome Version: {}", gnomeVersion);
}
// get the major/minor/patch, if possible.
int major = 0;
double minorAndPatch = 0.0D;
// this isn't the BEST way to do this, but it's simple and easy to understand
String[] split = gnomeVersion.split("\\.",2);
try {
major = Integer.parseInt(split[0]);
minorAndPatch = Double.parseDouble(split[1]);
} catch (Exception ignored) {
}
if (major == 2) {
return selectType(TrayType.Gtk);
}
else if (major == 3) {
if (minorAndPatch < 16.0D) {
logger.warn("SystemTray works, but will only show via SUPER+M.");
return selectType(TrayType.Gtk);
}
else if (minorAndPatch < 26.0D) {
Tray.gtkGnomeWorkaround = true;
LegacyExtension.install(trayName);
// now, what VERSION of fedora? "normal" fedora doesn't have AppIndicator installed, so we have to use GtkStatusIcon
// 23 is gtk, 24/25/26 is gtk (but also wrong size unless we adjust it. ImageUtil automatically does this)
return selectType(TrayType.Gtk);
}
else {
// 'pure' gnome3 DOES NOT support legacy tray icons any more. This ability has ENTIRELY been removed. NOTE: Ubuntu still supports these via app-indicators.
// the work-around for fedora is to install libappindicator + the appindicator extension
// install the appindicator Gnome extension
if (!AppIndicatorExtension.isInstalled()) {
AppIndicatorExtension.install();
logger.error("You must log out and then in again for system tray settings to apply.");
return null;
}
return selectType(TrayType.AppIndicator);
}
}
else {
logger.error("GNOME shell detected, but UNSUPPORTED shell version (" + gnomeVersion + "). Falling back to GtkStatusIcon. " +
"Please create an issue with as many details as possible.");
return selectType(TrayType.Gtk);
}
}
else if ("cinnamon".equalsIgnoreCase(GDM)) {
return selectType(TrayType.Gtk);
}
else if ("gnome-classic".equalsIgnoreCase(GDM)) {
return selectType(TrayType.Gtk);
}
else if ("gnome-fallback".equalsIgnoreCase(GDM)) {
return selectType(TrayType.Gtk);
}
else if ("awesome".equalsIgnoreCase(GDM)) {
return selectType(TrayType.Gtk);
}
else if ("ubuntu".equalsIgnoreCase(GDM)) {
// NOTE: popOS can also get here. It will also version check (since it's ubuntu-like)
int[] version = OS.Linux.INSTANCE.getUbuntuVersion();
// ubuntu 17.10+ uses the NEW gnome DE, which screws up previous Ubuntu workarounds, since it's now mostly Gnome
if (version[0] == 17 && version[1] == 10) {
// this is gnome 3.26.1, so we install the Gnome extension
Tray.gtkGnomeWorkaround = true;
LegacyExtension.install(trayName);
}
else if (version[0] >= 18) {
// ubuntu 18.04 doesn't need the extension BUT does need a logout-login (or gnome-shell restart) for it to work
// we copy over a config file so we know if we have already restarted the shell or shown the warning. A logout-login will also work.
DummyFile.install();
}
return selectType(TrayType.AppIndicator);
}
logger.error("GNOME shell detected, but UNKNOWN type. This should never happen. Falling back to GtkStatusIcon. " +
"Please create an issue with as many details as possible.");
return selectType(TrayType.Gtk);
}
case KDE: {
// kde 5.8+ is "high DPI", so we need to adjust the scale. Image resize will do that
double plasmaVersion = OS.DesktopEnv.INSTANCE.getPlasmaVersion();
if (DEBUG) {
logger.debug("KDE Plasma Version: {}", plasmaVersion);
}
if (plasmaVersion == 0.0) {
// this shouldn't ever happen!
logger.error("KDE Plasma detected, but UNDEFINED shell version. This should never happen. Falling back to GtkStatusIcon. " +
"Please create an issue with as many details as possible.");
return selectType(TrayType.Gtk);
}
if (plasmaVersion <= 5.5) {
// older versions use GtkStatusIcon
return selectType(TrayType.Gtk);
} else {
// newer versions use appindicator, but the user MIGHT have to install libappindicator
return selectType(TrayType.AppIndicator);
}
}
case Unity: {
// Ubuntu Unity is a weird combination. It's "Gnome", but it's not "Gnome Shell".
return selectType(TrayType.AppIndicator);
}
case Unity7: {
// Ubuntu Unity7 (17.04, which has MIR) is a weird combination. It's "Gnome", but it's not "Gnome Shell".
return selectType(TrayType.AppIndicator);
}
case XFCE: {
// 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
// so far, it is OK to use GtkStatusIcon on XFCE <-> XFCE4 inclusive
return selectType(TrayType.Gtk);
}
case LXDE: {
return selectType(TrayType.Gtk);
}
case MATE: {
return selectType(TrayType.Gtk);
}
case Pantheon: {
// elementaryOS. It only supports appindicator (not gtkstatusicon)
// http://bazaar.launchpad.net/~wingpanel-devs/wingpanel/trunk/view/head:/sample/SampleIndicator.vala
// in version 5.0+, they REMOVED support for appindicators. You can add it back via
// see: https://git.dorkbox.com/dorkbox/elementary-indicators
// 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.
return selectType(TrayType.AppIndicator);
}
case ChromeOS:
// ChromeOS cannot use the swing tray (ChromeOS is not supported!), nor AppIndicaitor/GtkStatusIcon, as those
// libraries do not exist on ChromeOS. Additionally, Java cannot load external libraries unless they are in /bin,
// BECAUSE of the `noexec` bit set. If JNA is moved into /bin, and the JNA library is specified to load from that
// location, we can use JNA.
return null;
}
// Try to autodetect if we can use app indicators (or if we need to fallback to GTK indicators)
BufferedReader bin = null;
try {
// the ONLY guaranteed way to determine if indicator-application-service is running (and thus, using app-indicator),
// is to look through all /proc/<pid>/status, and first line should be Name:\tindicator-appli
File proc = new File("/proc");
File[] listFiles = proc.listFiles();
if (listFiles != null) {
for (File procs : listFiles) {
String name = procs.getName();
if (!Character.isDigit(name.charAt(0))) {
continue;
}
File status = new File(procs, "status");
if (!status.canRead()) {
continue;
}
String line = FileUtil.INSTANCE.readFirstLine(status);
if (line != null && line.contains("indicator-app")) {
// make sure we can also load the library (it might be the wrong version)
return selectType(TrayType.AppIndicator);
}
}
}
} catch (Throwable e) {
if (DEBUG) {
logger.error("Error detecting gnome version", e);
}
}
if (OS.INSTANCE.isLinux()) {
// now just blanket query what we are to guess...
if (OS.Linux.INSTANCE.isUbuntu()) {
return selectType(TrayType.AppIndicator);
}
else if (OS.Linux.INSTANCE.isFedora()) {
return selectType(TrayType.AppIndicator);
} else {
// AppIndicators are now the "default" for most linux distro's.
return selectType(TrayType.AppIndicator);
}
}
}
throw new RuntimeException("This OS is not supported. Please create an issue with the details from `SystemTray.DEBUG=true;`");
}
public static
Runnable getShutdownHook(final String trayName) {
return ()->{
// check if we have been removed or not (when we stop via SystemTray.remove(), we don't want to run the EventDispatch again)
synchronized (traySingletons) {
if (traySingletons.containsKey(trayName)) {
// we haven't been removed by anything else
// we have to make sure we shutdown on our own thread (and not the JavaFX/SWT/AWT/etc thread)
EventDispatch.runLater(()->{
synchronized (traySingletons) {
// Only perform this action ONCE!
SystemTray systemTray = traySingletons.remove(trayName);
if (systemTray != null) {
systemTray.shutdown();
}
}
});
}
}
};
}
public static
void setInstance(final String trayName, final SystemTray systemTray) {
// must add ourselves under the specified tray name for retrieval later
// earlier on inside SystemTray initialization, if the tray-name already exists, THAT tray will be returned (instead of a
// new one getting created)
synchronized (traySingletons) {
traySingletons.put(trayName, systemTray);
}
}
public static
void removeSystemTrayHook(final String trayName) {
synchronized (traySingletons) {
traySingletons.remove(trayName);
}
}
public static
boolean hasOtherTrays() {
synchronized (traySingletons) {
return !traySingletons.isEmpty();
}
}
public static
SystemTray getInstance(final String trayName) {
synchronized (traySingletons) {
return traySingletons.get(trayName);
}
}
}