2021-01-31 00:36:44 +01:00
/ *
* 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 ;
2021-01-31 19:27:41 +01:00
import java.util.HashMap ;
import java.util.Map ;
2021-01-31 00:36:44 +01:00
import dorkbox.os.OS ;
2021-01-31 19:27:41 +01:00
import dorkbox.systemTray.SystemTray ;
2021-01-31 00:36:44 +01:00
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 ;
2022-12-27 15:47:29 +01:00
import dorkbox.systemTray.ui.osx._OsxAwtTray ;
2021-01-31 00:36:44 +01:00
import dorkbox.systemTray.ui.swing._SwingTray ;
import dorkbox.systemTray.ui.swing._WindowsNativeTray ;
2022-05-17 23:18:38 +02:00
import dorkbox.util.FileUtil ;
2021-01-31 00:36:44 +01:00
/ * *
* Auto - Detection of system tray type , with basic conversion utilities
* /
public
class AutoDetectTrayType {
2021-01-31 19:27:41 +01:00
// 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 < > ( ) ;
2021-01-31 00:36:44 +01:00
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 ) {
2022-12-27 15:47:29 +01:00
return _OsxAwtTray . class ;
2021-01-31 00:36:44 +01:00
}
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 ;
}
2022-12-27 15:47:29 +01:00
else if ( trayClass = = _OsxAwtTray . class ) {
2021-01-31 00:36:44 +01:00
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 ;
2022-12-27 15:47:29 +01:00
case Osx : return tray = = _OsxAwtTray . class ;
2021-01-31 00:36:44 +01:00
case Awt : return tray = = _AwtTray . class ;
}
return false ;
}
/ * *
* @return what the default " autodetect " tray type should be
* /
@SuppressWarnings ( " DuplicateBranchesInSwitch " )
public static
2021-01-31 19:27:41 +01:00
Class < ? extends Tray > get ( final String trayName ) {
2022-03-07 22:25:13 +01:00
if ( OS . INSTANCE . isWindows ( ) ) {
2021-01-31 00:36:44 +01:00
return selectType ( TrayType . WindowsNative ) ;
}
2022-03-07 22:25:13 +01:00
else if ( OS . INSTANCE . isMacOsX ( ) ) {
2022-12-27 15:47:29 +01:00
// macOS can ONLY use AWT if you want it to follow the L&F of the OS. It is the default.
2021-01-31 00:36:44 +01:00
return selectType ( TrayType . Osx ) ;
}
2022-03-07 22:25:13 +01:00
else if ( ( OS . INSTANCE . isLinux ( ) | | OS . INSTANCE . isUnix ( ) ) ) {
2021-01-31 00:36:44 +01:00
// 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
2022-03-07 22:25:13 +01:00
OS . DesktopEnv . Env de = OS . DesktopEnv . INSTANCE . getEnv ( ) ;
2021-01-31 00:36:44 +01:00
if ( DEBUG ) {
2022-03-07 22:25:13 +01:00
logger . debug ( " Currently using the '{}' desktop environment " + OS . INSTANCE . getLINE_SEPARATOR ( ) + OS . Linux . INSTANCE . getInfo ( ) , de ) ;
2021-01-31 00:36:44 +01:00
}
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
2022-03-07 22:25:13 +01:00
if ( OS . Linux . INSTANCE . isPop ( ) ) {
2021-01-31 00:36:44 +01:00
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.
2022-03-07 22:25:13 +01:00
if ( OS . Linux . INSTANCE . isUbuntu ( ) ) {
2021-01-31 00:36:44 +01:00
// 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
2022-03-07 22:25:13 +01:00
String gnomeVersion = OS . DesktopEnv . INSTANCE . getGnomeVersion ( ) ;
2021-01-31 00:36:44 +01:00
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 ;
2021-01-31 19:27:41 +01:00
LegacyExtension . install ( trayName ) ;
2021-01-31 00:36:44 +01:00
// 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 {
2021-03-20 21:03:42 +01:00
logger . error ( " GNOME shell detected, but UNSUPPORTED shell version ( " + gnomeVersion + " ). Falling back to GtkStatusIcon. " +
2021-01-31 00:36:44 +01:00
" 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 ) ;
}
2021-12-13 23:44:59 +01:00
else if ( " awesome " . equalsIgnoreCase ( GDM ) ) {
return selectType ( TrayType . Gtk ) ;
}
2021-01-31 00:36:44 +01:00
else if ( " ubuntu " . equalsIgnoreCase ( GDM ) ) {
// NOTE: popOS can also get here. It will also version check (since it's ubuntu-like)
2022-03-07 22:25:13 +01:00
int [ ] version = OS . Linux . INSTANCE . getUbuntuVersion ( ) ;
2021-01-31 00:36:44 +01:00
// 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 ;
2021-01-31 19:27:41 +01:00
LegacyExtension . install ( trayName ) ;
2021-01-31 00:36:44 +01:00
}
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
2022-03-07 22:25:13 +01:00
double plasmaVersion = OS . DesktopEnv . INSTANCE . getPlasmaVersion ( ) ;
2021-01-31 00:36:44 +01:00
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 ;
}
2022-05-17 23:18:38 +02:00
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 ) ;
2021-01-31 00:36:44 +01:00
}
}
}
} catch ( Throwable e ) {
if ( DEBUG ) {
logger . error ( " Error detecting gnome version " , e ) ;
}
}
2022-03-07 22:25:13 +01:00
if ( OS . INSTANCE . isLinux ( ) ) {
2021-01-31 00:36:44 +01:00
// now just blanket query what we are to guess...
2022-03-07 22:25:13 +01:00
if ( OS . Linux . INSTANCE . isUbuntu ( ) ) {
2021-01-31 00:36:44 +01:00
return selectType ( TrayType . AppIndicator ) ;
}
2022-03-07 22:25:13 +01:00
else if ( OS . Linux . INSTANCE . isFedora ( ) ) {
2021-01-31 00:36:44 +01:00
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;` " ) ;
}
2021-01-31 19:27:41 +01:00
2021-03-29 00:16:11 +02:00
public static
Runnable getShutdownHook ( final String trayName ) {
return ( ) - > {
2021-05-02 12:26:01 +02:00
// check if we have been removed or not (when we stop via SystemTray.remove(), we don't want to run the EventDispatch again)
2021-03-29 00:16:11 +02:00
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 ( ) ;
}
}
} ) ;
}
}
} ;
}
2021-01-31 19:27:41 +01:00
public static
2021-03-29 00:16:11 +02:00
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 ) ;
}
}
2021-01-31 19:27:41 +01:00
public static
void removeSystemTrayHook ( final String trayName ) {
synchronized ( traySingletons ) {
traySingletons . remove ( trayName ) ;
}
}
2021-01-31 23:42:21 +01:00
public static
boolean hasOtherTrays ( ) {
synchronized ( traySingletons ) {
return ! traySingletons . isEmpty ( ) ;
}
}
2021-01-31 23:45:48 +01:00
public static
SystemTray getInstance ( final String trayName ) {
synchronized ( traySingletons ) {
return traySingletons . get ( trayName ) ;
}
}
2021-01-31 00:36:44 +01:00
}