From 65e7c669b5f6cf592246af46fa204089e193ea30 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 24 Jul 2017 14:32:58 +0200 Subject: [PATCH] Added Menu Entry tooltip support. Emits warning on first load of tooltips (as they are not supported by all OS configurations). This warning can be suppressed. Misc fix for loading swing with GTK. --- src/dorkbox/systemTray/MenuItem.java | 45 ++++++++ src/dorkbox/systemTray/SystemTray.java | 101 +++++++++--------- src/dorkbox/systemTray/Tray.java | 26 ----- src/dorkbox/systemTray/peer/MenuItemPeer.java | 2 + src/dorkbox/systemTray/ui/awt/AwtMenu.java | 12 +++ .../systemTray/ui/awt/AwtMenuItem.java | 7 ++ src/dorkbox/systemTray/ui/awt/_AwtTray.java | 50 ++++----- src/dorkbox/systemTray/ui/gtk/GtkMenu.java | 6 ++ .../systemTray/ui/gtk/GtkMenuItem.java | 14 +++ .../ui/gtk/_AppIndicatorNativeTray.java | 12 +-- .../ui/gtk/_GtkStatusIconNativeTray.java | 38 ++++--- .../systemTray/ui/swing/SwingMenu.java | 6 ++ .../systemTray/ui/swing/SwingMenuItem.java | 12 +++ .../systemTray/ui/swing/_SwingTray.java | 46 ++++---- test/dorkbox/TestTray.java | 2 + test/dorkbox/TestTrayJavaFX.java | 2 + test/dorkbox/TestTraySwt.java | 2 + 17 files changed, 238 insertions(+), 145 deletions(-) diff --git a/src/dorkbox/systemTray/MenuItem.java b/src/dorkbox/systemTray/MenuItem.java index a0bc7be..29737bb 100644 --- a/src/dorkbox/systemTray/MenuItem.java +++ b/src/dorkbox/systemTray/MenuItem.java @@ -33,6 +33,8 @@ import dorkbox.util.SwingUtil; @SuppressWarnings({"unused", "SameParameterValue", "WeakerAccess"}) public class MenuItem extends Entry { + private static boolean alreadyEmittedTooltipWarning = false; + private volatile String text; private volatile File imageFile; private volatile ActionListener callback; @@ -40,6 +42,7 @@ class MenuItem extends Entry { // default enabled is always true private volatile boolean enabled = true; private volatile char mnemonicKey; + private volatile String tooltip; public MenuItem() { @@ -133,6 +136,7 @@ class MenuItem extends Entry { peer.setText(this); peer.setCallback(this); peer.setShortcut(this); + peer.setTooltip(this); } protected @@ -343,6 +347,47 @@ class MenuItem extends Entry { } } + /** + * Specifies the tooltip text, usually this is used to brand the SystemTray icon with your product's name, or to provide extra + * information during mouse-over for menu entries. + *

+ * NOTE: Maximum length is 64 characters long, and it is not supported on all Operating Systems and Desktop Environments. + *

+ * For more details on Linux see https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12. + * + * @param tooltipText the text to use as a mouse-over tooltip for the tray icon or menu entry, null to remove. + */ + public + void setTooltip(final String tooltipText) { + if (tooltipText != null) { + // this is a safety precaution, since the behavior of really long text is undefined. + if (tooltipText.length() > 64) { + throw new RuntimeException("Tooltip text cannot be longer than 64 characters."); + } + + if (!alreadyEmittedTooltipWarning) { + alreadyEmittedTooltipWarning = true; + SystemTray.logger.warn("Tooltips are not consistent across all platforms and tray types."); + } + } + + this.tooltip = tooltipText; + + if (peer != null) { + ((MenuItemPeer) peer).setTooltip(this); + } + } + + /** + * Gets the mouse-over tooltip for the meme entry. + * + * NOTE: This is not consistent across all platforms and tray types. + */ + public + String getTooltip() { + return this.tooltip; + } + @Override public void remove() { diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index 96d158d..89faa7b 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -675,7 +675,7 @@ class SystemTray { logger.debug("Forced tray type: {}", FORCE_TRAY_TYPE.name()); } logger.debug("Force GTK2: {}", FORCE_GTK2); - logger.debug("Prefer GTK3: {}", FORCE_GTK2); + logger.debug("Prefer GTK3: {}", PREFER_GTK3); } // Note: AppIndicators DO NOT support tooltips. We could try to create one, by creating a GTK widget and attaching it on @@ -777,8 +777,9 @@ class SystemTray { // at this point, the tray type is what it should be. If there are failures or special cases, all types will fall back to // Swing. - if (isNix && !isTrayType(trayType, TrayType.Swing)) { + if (isNix) { // linux/unix need access to GTK, so load it up before the tray is loaded! + // Swing gets the image size info VIA gtk, so this is important as well. GtkEventDispatch.startGui(FORCE_GTK2, PREFER_GTK3, DEBUG); GtkEventDispatch.waitForEventsToComplete(); @@ -788,65 +789,65 @@ class SystemTray { logger.debug("Is the system already running GTK? {}", Gtk.alreadyRunningGTK); } - // NOTE: appindicator1 -> GTk2, appindicator3 -> GTK3. - // appindicator3 doesn't support menu icons via GTK2!! - if (!Gtk.isLoaded) { - trayType = selectTypeQuietly(TrayType.Swing); - - logger.error("Unable to initialize GTK! Something is severely wrong! Using the Swing Tray type instead."); - } - else if (OSUtil.Linux.isArch()) { - // arch linux is fun! - - if (isTrayType(trayType, TrayType.AppIndicator) && !AppIndicator.isLoaded) { - // appindicators - - // requires the install of libappindicator which is GTK2 (as of 25DEC2016) - // requires the install of libappindicator3 which is GTK3 (as of 25DEC2016) + if (!isTrayType(trayType, TrayType.Swing)) { +// NOTE: appindicator1 -> GTk2, appindicator3 -> GTK3. + // appindicator3 doesn't support menu icons via GTK2!! + if (!Gtk.isLoaded) { trayType = selectTypeQuietly(TrayType.Swing); - if (Gtk.isGtk2) { - logger.warn("Unable to initialize AppIndicator for Arch linux, it requires GTK2! " + - "Please install libappindicator, for example: 'sudo pacman -S libappindicator'. " + + logger.error("Unable to initialize GTK! Something is severely wrong! Using the Swing Tray type instead."); + } + else if (OSUtil.Linux.isArch()) { + // arch linux is fun! + + if (isTrayType(trayType, TrayType.AppIndicator) && !AppIndicator.isLoaded) { + // appindicators + + // requires the install of libappindicator which is GTK2 (as of 25DEC2016) + // requires the install of libappindicator3 which is GTK3 (as of 25DEC2016) + trayType = selectTypeQuietly(TrayType.Swing); + + if (Gtk.isGtk2) { + logger.warn("Unable to initialize AppIndicator for Arch linux, it requires GTK2! " + + "Please install libappindicator, for example: 'sudo pacman -S libappindicator'. " + + "Using the Swing Tray type instead."); + } else { + logger.error("Unable to initialize AppIndicator for Arch linux, it requires GTK3! " + + "Please install libappindicator3, for example: 'sudo pacman -S libappindicator3'. " + + "Using the Swing Tray type instead."); // GTK3 + } + } else if (isTrayType(trayType, TrayType.GtkStatusIcon)) { + if (!Extension.isInstalled()) { + // Automatically install the extension for everyone except Arch. They are bonkers. + Extension.ENABLE_EXTENSION_INSTALL = false; + SystemTray.logger.info("You may need a work-around for showing the SystemTray icon - we suggest installing the " + + "the [Top Icons] plugin (https://extensions.gnome.org/extension/1031/topicons/) which moves " + + "icons from the *notification drawer* (it is normally collapsed) at the bottom left corner " + + "of the screen to the menu panel next to the clock."); + } + } + } + else if (isTrayType(trayType, TrayType.AppIndicator)) { + if (Gtk.isGtk2 && AppIndicator.isVersion3) { + trayType = selectTypeQuietly(TrayType.Swing); + + 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'. " + "Using the Swing Tray type instead."); - } else { - logger.error("Unable to initialize AppIndicator for Arch linux, it requires GTK3! " + - "Please install libappindicator3, for example: 'sudo pacman -S libappindicator3'. " + - "Using the Swing Tray type instead."); // GTK3 + } - } else if (isTrayType(trayType, TrayType.GtkStatusIcon)) { - if (!Extension.isInstalled()) { - // Automatically install the extension for everyone except Arch. They are bonkers. - Extension.ENABLE_EXTENSION_INSTALL = false; - SystemTray.logger.info("You may need a work-around for showing the SystemTray icon - we suggest installing the " + - "the [Top Icons] plugin (https://extensions.gnome.org/extension/1031/topicons/) which moves " + - "icons from the *notification drawer* (it is normally collapsed) at the bottom left corner " + - "of the screen to the menu panel next to the clock."); + else if (!AppIndicator.isLoaded) { + // YIKES. Try to fallback to GtkStatusIndicator, since AppIndicator couldn't load. + trayType = selectTypeQuietly(TrayType.Swing); + + logger.warn("Unable to initialize the AppIndicator correctly. Using the Swing Tray type instead."); } } } - else if (isTrayType(trayType, TrayType.AppIndicator)) { - if (Gtk.isGtk2 && AppIndicator.isVersion3) { - trayType = selectTypeQuietly(TrayType.Swing); - - 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'. " + - "Using the Swing Tray type instead."); - - } - else if (!AppIndicator.isLoaded) { - // YIKES. Try to fallback to GtkStatusIndicator, since AppIndicator couldn't load. - trayType = selectTypeQuietly(TrayType.Swing); - - logger.warn("Unable to initialize the AppIndicator correctly. Using the Swing Tray type instead."); - } - } } - - // have to make adjustments BEFORE the tray/menu image size calculations if (AUTO_FIX_INCONSISTENCIES && isTrayType(trayType, TrayType.Swing) && SystemTray.SWING_UI == null) { if (isNix) { diff --git a/src/dorkbox/systemTray/Tray.java b/src/dorkbox/systemTray/Tray.java index 834b156..f0d36af 100644 --- a/src/dorkbox/systemTray/Tray.java +++ b/src/dorkbox/systemTray/Tray.java @@ -90,32 +90,6 @@ class Tray extends Menu { } } - // method that is meant to be overridden by the tray implementations - protected - void setTooltip_(final String tooltipText) { - // default is NO OP - } - - /** - * Specifies the tooltip text, usually this is used to brand the SystemTray icon with your product's name. - *

- * The maximum length is 64 characters long, and it is not supported on all Operating Systems and Desktop - * Environments. - *

- * For more details on Linux see https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12. - * - * @param tooltipText the text to use as tooltip for the tray icon, null to remove - */ - final - void setTooltip(final String tooltipText) { - // this is a safety precaution, since the behavior of really long text is undefined. - if (tooltipText.length() > 64) { - throw new RuntimeException("Tooltip text cannot be longer than 64 characters."); - } - - setTooltip_(tooltipText); - } - /** * Specifies the new image to set for the tray icon. *

diff --git a/src/dorkbox/systemTray/peer/MenuItemPeer.java b/src/dorkbox/systemTray/peer/MenuItemPeer.java index aa45c0d..db57442 100644 --- a/src/dorkbox/systemTray/peer/MenuItemPeer.java +++ b/src/dorkbox/systemTray/peer/MenuItemPeer.java @@ -31,4 +31,6 @@ interface MenuItemPeer extends EntryPeer { void setCallback(MenuItem menuItem); void setShortcut(MenuItem menuItem); + + void setTooltip(MenuItem menuItem); } diff --git a/src/dorkbox/systemTray/ui/awt/AwtMenu.java b/src/dorkbox/systemTray/ui/awt/AwtMenu.java index d9e112a..df7d527 100644 --- a/src/dorkbox/systemTray/ui/awt/AwtMenu.java +++ b/src/dorkbox/systemTray/ui/awt/AwtMenu.java @@ -136,6 +136,18 @@ class AwtMenu implements MenuPeer { }); } + @Override + public + void setTooltip(final MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setLabel(menuItem.getTooltip()); + } + }); + } + @Override public void remove() { diff --git a/src/dorkbox/systemTray/ui/awt/AwtMenuItem.java b/src/dorkbox/systemTray/ui/awt/AwtMenuItem.java index 99018c7..384a8eb 100644 --- a/src/dorkbox/systemTray/ui/awt/AwtMenuItem.java +++ b/src/dorkbox/systemTray/ui/awt/AwtMenuItem.java @@ -19,6 +19,7 @@ import java.awt.MenuShortcut; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import dorkbox.systemTray.MenuItem; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.peer.MenuItemPeer; import dorkbox.util.SwingUtil; @@ -114,6 +115,12 @@ class AwtMenuItem implements MenuItemPeer { }); } + @Override + public + void setTooltip(final MenuItem menuItem) { + // no op. (awt menus cannot show tooltips) + } + @SuppressWarnings("Duplicates") @Override public diff --git a/src/dorkbox/systemTray/ui/awt/_AwtTray.java b/src/dorkbox/systemTray/ui/awt/_AwtTray.java index 673e8dd..883233c 100644 --- a/src/dorkbox/systemTray/ui/awt/_AwtTray.java +++ b/src/dorkbox/systemTray/ui/awt/_AwtTray.java @@ -182,6 +182,31 @@ class _AwtTray extends Tray { // no op } + @Override + public + void setTooltip(final MenuItem menuItem) { + final String text = menuItem.getTooltip(); + + if (tooltipText != null && tooltipText.equals(text) || + tooltipText == null && text != null) { + return; + } + + tooltipText = text; + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + // don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we + // want to make sure keep the tooltip text the same as before. + if (trayIcon != null) { + trayIcon.setToolTip(text); + } + } + }); + } + @Override public void remove() { @@ -209,31 +234,6 @@ class _AwtTray extends Tray { bind(awtMenu, null, systemTray); } - @Override - protected - void setTooltip_(final String tooltipText) { - if (this.tooltipText.equals(tooltipText)){ - return; - } - this.tooltipText = tooltipText; - - SwingUtil.invokeLater(new Runnable() { - @Override - public - void run() { - if (tray == null) { - tray = SystemTray.getSystemTray(); - } - - // don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we - // want to make sure keep the tooltip text the same as before. - if (trayIcon != null) { - trayIcon.setToolTip(tooltipText); - } - } - }); - } - @Override public boolean hasImage() { diff --git a/src/dorkbox/systemTray/ui/gtk/GtkMenu.java b/src/dorkbox/systemTray/ui/gtk/GtkMenu.java index 9098756..308e74d 100644 --- a/src/dorkbox/systemTray/ui/gtk/GtkMenu.java +++ b/src/dorkbox/systemTray/ui/gtk/GtkMenu.java @@ -346,6 +346,12 @@ class GtkMenu extends GtkBaseMenuItem implements MenuPeer { setText(menuItem); } + @Override + public + void setTooltip(final MenuItem menuItem) { + + } + /** * called when a child removes itself from the parent menu. Does not work for sub-menus * diff --git a/src/dorkbox/systemTray/ui/gtk/GtkMenuItem.java b/src/dorkbox/systemTray/ui/gtk/GtkMenuItem.java index 8e76ec9..a23a9b7 100644 --- a/src/dorkbox/systemTray/ui/gtk/GtkMenuItem.java +++ b/src/dorkbox/systemTray/ui/gtk/GtkMenuItem.java @@ -167,6 +167,20 @@ class GtkMenuItem extends GtkBaseMenuItem implements MenuItemPeer, GCallback { setText(menuItem); } + @Override + public + void setTooltip(final MenuItem menuItem) { + GtkEventDispatch.dispatch(new Runnable() { + @Override + public + void run() { + // NOTE: this will not work for AppIndicator tray types! + // null will remove the tooltip + Gtk2.gtk_widget_set_tooltip_text(_native, menuItem.getTooltip()); + } + }); + } + @SuppressWarnings("Duplicates") @Override public diff --git a/src/dorkbox/systemTray/ui/gtk/_AppIndicatorNativeTray.java b/src/dorkbox/systemTray/ui/gtk/_AppIndicatorNativeTray.java index 920a15f..95bb737 100644 --- a/src/dorkbox/systemTray/ui/gtk/_AppIndicatorNativeTray.java +++ b/src/dorkbox/systemTray/ui/gtk/_AppIndicatorNativeTray.java @@ -186,6 +186,12 @@ class _AppIndicatorNativeTray extends Tray { // no op. } + @Override + public + void setTooltip(final MenuItem menuItem) { + // no op. see https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12 + } + @Override public void remove() { @@ -231,12 +237,6 @@ class _AppIndicatorNativeTray extends Tray { bind(gtkMenu, null, systemTray); } - @Override - protected - void setTooltip_(final String tooltipText) { - // https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12 - } - @Override public boolean hasImage() { diff --git a/src/dorkbox/systemTray/ui/gtk/_GtkStatusIconNativeTray.java b/src/dorkbox/systemTray/ui/gtk/_GtkStatusIconNativeTray.java index 213c6af..ee4b95f 100644 --- a/src/dorkbox/systemTray/ui/gtk/_GtkStatusIconNativeTray.java +++ b/src/dorkbox/systemTray/ui/gtk/_GtkStatusIconNativeTray.java @@ -123,6 +123,27 @@ class _GtkStatusIconNativeTray extends Tray { // no op } + @Override + public + void setTooltip(final MenuItem menuItem) { + final String text = menuItem.getTooltip(); + + if (tooltipText != null && tooltipText.equals(text) || + tooltipText == null && text != null) { + return; + } + + tooltipText = text; + + GtkEventDispatch.dispatch(new Runnable() { + @Override + public + void run() { + Gtk2.gtk_status_icon_set_tooltip_text(trayIcon, text); + } + }); + } + @Override public void remove() { @@ -208,23 +229,6 @@ class _GtkStatusIconNativeTray extends Tray { } } - @Override - protected - void setTooltip_(final String tooltipText) { - if (this.tooltipText.equals(tooltipText)){ - return; - } - this.tooltipText = tooltipText; - - GtkEventDispatch.dispatch(new Runnable() { - @Override - public - void run() { - Gtk2.gtk_status_icon_set_tooltip_text(trayIcon, tooltipText); - } - }); - } - @Override public boolean hasImage() { diff --git a/src/dorkbox/systemTray/ui/swing/SwingMenu.java b/src/dorkbox/systemTray/ui/swing/SwingMenu.java index 6d4ece1..59350ea 100644 --- a/src/dorkbox/systemTray/ui/swing/SwingMenu.java +++ b/src/dorkbox/systemTray/ui/swing/SwingMenu.java @@ -173,6 +173,12 @@ class SwingMenu implements MenuPeer { }); } + @Override + public + void setTooltip(final MenuItem menuItem) { + + } + /** * This removes all menu entries from this menu AND this menu from it's parent */ diff --git a/src/dorkbox/systemTray/ui/swing/SwingMenuItem.java b/src/dorkbox/systemTray/ui/swing/SwingMenuItem.java index eac0de2..1e771f5 100644 --- a/src/dorkbox/systemTray/ui/swing/SwingMenuItem.java +++ b/src/dorkbox/systemTray/ui/swing/SwingMenuItem.java @@ -166,6 +166,18 @@ class SwingMenuItem implements MenuItemPeer { }); } + @Override + public + void setTooltip(final MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setToolTipText(menuItem.getTooltip()); + } + }); + } + @Override public void remove() { diff --git a/src/dorkbox/systemTray/ui/swing/_SwingTray.java b/src/dorkbox/systemTray/ui/swing/_SwingTray.java index 2e6374f..0255957 100644 --- a/src/dorkbox/systemTray/ui/swing/_SwingTray.java +++ b/src/dorkbox/systemTray/ui/swing/_SwingTray.java @@ -167,6 +167,31 @@ class _SwingTray extends Tray { // no op } + @Override + public + void setTooltip(final MenuItem menuItem) { + final String text = menuItem.getTooltip(); + + if (tooltipText != null && tooltipText.equals(text) || + tooltipText == null && text != null) { + return; + } + + tooltipText = text; + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + // don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we + // want to make sure keep the tooltip text the same as before. + if (trayIcon != null) { + trayIcon.setToolTip(text); + } + } + }); + } + @Override public void remove() { @@ -199,27 +224,6 @@ class _SwingTray extends Tray { bind(swingMenu, null, systemTray); } - @Override - protected - void setTooltip_(final String tooltipText) { - if (this.tooltipText.equals(tooltipText)){ - return; - } - this.tooltipText = tooltipText; - - SwingUtil.invokeLater(new Runnable() { - @Override - public - void run() { - // don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we - // want to make sure keep the tooltip text the same as before. - if (trayIcon != null) { - trayIcon.setToolTip(tooltipText); - } - } - }); - } - @Override public boolean hasImage() { diff --git a/test/dorkbox/TestTray.java b/test/dorkbox/TestTray.java index 4c567eb..b28ff41 100644 --- a/test/dorkbox/TestTray.java +++ b/test/dorkbox/TestTray.java @@ -102,12 +102,14 @@ class TestTray { entry.setCallback(callbackGray); entry.setImage(BLACK_MAIL); entry.setText("Delete Mail"); + entry.setTooltip(null); // remove the tooltip // systemTray.remove(menuEntry); } }); greenEntry.setImage(GREEN_MAIL); // case does not matter greenEntry.setShortcut('G'); + greenEntry.setTooltip("This means you have green mail!"); mainMenu.add(greenEntry); diff --git a/test/dorkbox/TestTrayJavaFX.java b/test/dorkbox/TestTrayJavaFX.java index 4680a21..de3c643 100644 --- a/test/dorkbox/TestTrayJavaFX.java +++ b/test/dorkbox/TestTrayJavaFX.java @@ -164,12 +164,14 @@ class TestTrayJavaFX { entry.setCallback(callbackGray); entry.setImage(BLACK_MAIL); entry.setText("Delete Mail"); + entry.setTooltip(null); // remove the tooltip // systemTray.remove(menuEntry); } }); greenEntry.setImage(GREEN_MAIL); // case does not matter greenEntry.setShortcut('G'); + greenEntry.setTooltip("This means you have green mail!"); mainMenu.add(greenEntry); diff --git a/test/dorkbox/TestTraySwt.java b/test/dorkbox/TestTraySwt.java index 3aecbd6..75b9141 100644 --- a/test/dorkbox/TestTraySwt.java +++ b/test/dorkbox/TestTraySwt.java @@ -118,12 +118,14 @@ class TestTraySwt { entry.setCallback(callbackGray); entry.setImage(BLACK_MAIL); entry.setText("Delete Mail"); + entry.setTooltip(null); // remove the tooltip // systemTray.remove(menuEntry); } }); greenEntry.setImage(GREEN_MAIL); // case does not matter greenEntry.setShortcut('G'); + greenEntry.setTooltip("This means you have green mail!"); mainMenu.add(greenEntry);