From e56ed0631455b06fbe4ed597e8b8de4c9cec02b2 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 23 Jan 2015 02:52:09 +0100 Subject: [PATCH] Updated readme to include various options. Fixed GTK tray (not appindicator). Cleaned up code. Cleaned up positioning. Added MUCH better detection of app-indicator for linux. --- LICENSE | 2 +- README.md | 9 +++ src/dorkbox/util/tray/SystemTray.java | 67 ++++++++++++++-- .../util/tray/SystemTrayMenuPopup.java | 36 +++++++-- .../util/tray/linux/AppIndicatorTray.java | 3 +- .../util/tray/linux/GtkSystemTray.java | 80 ++++++++++++------- .../util/tray/swing/SwingSystemTray.java | 21 +---- 7 files changed, 155 insertions(+), 63 deletions(-) diff --git a/LICENSE b/LICENSE index ad768bd7..b3b1cdb3 100644 --- a/LICENSE +++ b/LICENSE @@ -19,5 +19,5 @@ - SLF4J - MIT License - http://www.slf4j.org/ + http://www.slf4j.org Copyright 2004-2008, QOS.ch diff --git a/README.md b/README.md index e2b72b9b..5c966363 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,16 @@ There are a number of problems on Linux with the Swing (and SWT) system-tray ico This is for cross-platform use, specifically - linux 32/64, mac 32/64, and windows 32/64. Java 6+ +``` +To customize the delay (for hiding the popup) when the cursor is "moused out" of the + popup menu, change the value of 'SystemTrayMenuPopup.hidePopupDelay' +Not all system tray icons are the same size (default is 22px), so to properly scale the icon + to fit, change the value of 'SystemTray.TRAY_SIZE' + +You might want to specify the root location of the icons used (to make it easier when + specifying icons), change the value of 'SystemTray.ICON_PATH' +``` ``` Note: This library does NOT use SWT for system-tray support, only for the purpose of lessening the jar dependencies. Changing it to be SWT-based is not be diff --git a/src/dorkbox/util/tray/SystemTray.java b/src/dorkbox/util/tray/SystemTray.java index 7a395bf2..328f9e2f 100644 --- a/src/dorkbox/util/tray/SystemTray.java +++ b/src/dorkbox/util/tray/SystemTray.java @@ -15,8 +15,10 @@ */ package dorkbox.util.tray; +import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -52,9 +54,9 @@ public abstract class SystemTray { protected static final Logger logger = LoggerFactory.getLogger(SystemTray.class); /** - * Size of the icon + * Size of the icon WHEN IT'S IN THE TRAY */ - public static int ICON_SIZE = 22; + public static int TRAY_SIZE = 22; /** * Location of the icon @@ -67,13 +69,62 @@ public abstract class SystemTray { static { if (OS.isLinux()) { GtkSupport.init(); - String getenv = System.getenv("XDG_CURRENT_DESKTOP"); - if (getenv != null && (getenv.equals("Unity") || getenv.equals("KDE"))) { - if (GtkSupport.isSupported) { - trayType = AppIndicatorTray.class; + if (GtkSupport.isSupported) { + // quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least + String getenv = System.getenv("XDG_CURRENT_DESKTOP"); + if (getenv != null && getenv.equals("Unity")) { + try { + trayType = AppIndicatorTray.class; + } catch (Exception ignored) { + } } - } else { - if (GtkSupport.isSupported) { + + if (trayType == null) { + 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//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; + } + + try { + bin = new BufferedReader(new FileReader(status)); + String readLine = bin.readLine(); + if (readLine != null && readLine.contains("indicator-app")) { + trayType = AppIndicatorTray.class; + break; + } + } finally { + if (bin != null) { + bin.close(); + bin = null; + } + } + } + } + } catch (Exception ignored) { + } finally { + if (bin != null) { + try { + bin.close(); + } catch (IOException ignored) { + } + } + } + } + + if (trayType == null) { trayType = GtkSystemTray.class; } } diff --git a/src/dorkbox/util/tray/SystemTrayMenuPopup.java b/src/dorkbox/util/tray/SystemTrayMenuPopup.java index cbf10a5d..0f95f202 100644 --- a/src/dorkbox/util/tray/SystemTrayMenuPopup.java +++ b/src/dorkbox/util/tray/SystemTrayMenuPopup.java @@ -14,10 +14,17 @@ import dorkbox.util.SwingUtil; public class SystemTrayMenuPopup extends JPopupMenu { private static final long serialVersionUID = 1L; + /** Allows you to customize the delay (for hiding the popup) when the cursor is "moused out" of the popup menu */ + public static long hidePopupDelay = 1000L; + private DelayTimer timer; + protected boolean mouseStillOnMenu; +// private JDialog hiddenDialog; + public SystemTrayMenuPopup() { super(); + setFocusable(true); this.timer = new DelayTimer("PopupMenuHider", true, new DelayTimer.Callback() { @Override @@ -48,18 +55,35 @@ public class SystemTrayMenuPopup extends JPopupMenu { SystemTrayMenuPopup.this.timer.delay(SystemTrayMenuPopup.this.timer.getDelay()); } }); - } + // Does not work correctly on linux. a window in the taskbar shows up. + /* Initialize the hidden dialog as a headless, titleless dialog window */ +// this.hiddenDialog = new JDialog((Frame)null); +// this.hiddenDialog.setEnabled(false); +// this.hiddenDialog.setUndecorated(true); +// +// this.hiddenDialog.setSize(5, 5); +// /* Add the window focus listener to the hidden dialog */ +// this.hiddenDialog.addWindowFocusListener(new WindowFocusListener () { +// @Override +// public void windowLostFocus (WindowEvent we ) { +// SystemTrayMenuPopup.this.setVisible(false); +// } +// @Override +// public void windowGainedFocus (WindowEvent we) {} +// }); + } @Override - public void setVisible(boolean b) { + public void setVisible(boolean makeVisible) { this.timer.cancel(); - if (b) { - // if the mouse isn't inside the popup in 5 seconds, close the popup - this.timer.delay(5000L); + if (makeVisible) { + // if the mouse isn't inside the popup in x seconds, close the popup + this.timer.delay(hidePopupDelay); } - super.setVisible(b); +// this.hiddenDialog.setVisible(makeVisible); + super.setVisible(makeVisible); } } diff --git a/src/dorkbox/util/tray/linux/AppIndicatorTray.java b/src/dorkbox/util/tray/linux/AppIndicatorTray.java index 01a6eb0a..4c8c37ad 100644 --- a/src/dorkbox/util/tray/linux/AppIndicatorTray.java +++ b/src/dorkbox/util/tray/linux/AppIndicatorTray.java @@ -25,6 +25,7 @@ import com.sun.jna.Pointer; import dorkbox.util.jna.linux.AppIndicator; import dorkbox.util.jna.linux.Gobject; import dorkbox.util.jna.linux.Gtk; +import dorkbox.util.jna.linux.GtkSupport; import dorkbox.util.tray.SystemTray; import dorkbox.util.tray.SystemTrayMenuAction; @@ -127,7 +128,7 @@ public class AppIndicatorTray extends SystemTray { } this.connectionStatusItem = null; - libgtk.gtk_main_quit(); + GtkSupport.shutdownGTK(); libgtk.gdk_threads_leave(); super.removeTray(); diff --git a/src/dorkbox/util/tray/linux/GtkSystemTray.java b/src/dorkbox/util/tray/linux/GtkSystemTray.java index 6c983612..7a9c2722 100644 --- a/src/dorkbox/util/tray/linux/GtkSystemTray.java +++ b/src/dorkbox/util/tray/linux/GtkSystemTray.java @@ -16,6 +16,8 @@ package dorkbox.util.tray.linux; import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; @@ -31,6 +33,7 @@ import dorkbox.util.SwingUtil; import dorkbox.util.jna.linux.Gobject; import dorkbox.util.jna.linux.Gtk; import dorkbox.util.jna.linux.Gtk.GdkEventButton; +import dorkbox.util.jna.linux.GtkSupport; import dorkbox.util.tray.SystemTray; import dorkbox.util.tray.SystemTrayMenuAction; import dorkbox.util.tray.SystemTrayMenuPopup; @@ -53,55 +56,80 @@ public class GtkSystemTray extends SystemTray { // need to hang on to these to prevent gc private final List widgets = new ArrayList(4); + private Gobject.GEventCallback gtkCallback; public GtkSystemTray() { } @Override public void createTray(String iconName) { + SwingUtil.invokeAndWait(new Runnable() { + @Override + public void run() { + GtkSystemTray.this.jmenu = new SystemTrayMenuPopup(); + } + }); + libgtk.gdk_threads_enter(); + this.trayIcon = libgtk.gtk_status_icon_new(); libgtk.gtk_status_icon_set_from_file(this.trayIcon, iconPath(iconName)); libgtk.gtk_status_icon_set_tooltip(this.trayIcon, this.appName); libgtk.gtk_status_icon_set_visible(this.trayIcon, true); - Gobject.GEventCallback gtkCallback = new Gobject.GEventCallback() { + // have to make this a field, to prevent GC on this object + this.gtkCallback = new Gobject.GEventCallback() { @Override - public void callback(Pointer instance, final GdkEventButton event) { - // BUTTON_PRESS only + public void callback(Pointer system_tray, final GdkEventButton event) { + // BUTTON_PRESS only (any mouse click) if (event.type == 4) { SwingUtil.invokeLater(new Runnable() { @Override public void run() { + // test this using cinnamon (which still uses status icon) + if (GtkSystemTray.this.jmenu.isVisible()) { GtkSystemTray.this.jmenu.setVisible(false); } else { - int iconX = (int) (event.x_root - event.x); - int iconY = (int) (event.y_root - event.y); - // System.err.println("x: " + iconX + " y: " + iconY); - // System.err.println("x1: " + event.x_root + " y1: " + event.y_root); // relative to SCREEN - // System.err.println("x2: " + event.x + " y2: " + event.y); // relative to WINDOW - Dimension size = GtkSystemTray.this.jmenu.getPreferredSize(); - // do we open at top-right or top-left? - // we ASSUME monitor size is greater than 640x480 AND that our tray icon is IN THE CORNER SOMEWHERE + int x = (int) event.x_root; + int y = (int) event.y_root; - // always put the menu in the middle - iconX -= size.width / 2; + Point point = new Point(x, y); + Rectangle bounds = SwingUtil.getScreenBoundsAt(point); - // y = 2 -> top - // y = 1068 -> bottom - if (iconY > 240) { - iconY -= size.height; + if (y < bounds.y) { + y = bounds.y; + } else if (y + size.height > bounds.y + bounds.height) { + // our menu cannot have the top-edge snap to the mouse + // so we make the bottom-edge snap to the mouse + y -= size.height; // snap to edge of mouse + } + + if (x < bounds.x) { + x = bounds.x; + } else if (x + size.width > bounds.x + bounds.width) { + // our menu cannot have the left-edge snap to the mouse + // so we make the right-edge snap to the mouse + x -= size.width; // snap to edge of mouse + } + + // SMALL problem, is that on linux, the popup is BEHIND the tray bar! + // to solve the problem, we anchor the popup above (or below) the tray bar + int distanceToEdgeOfTray = (int) event.y; + // System.err.println(" distance: " + distanceToEdgeOfTray); + // we are at the top of the screen + if (y < 100) { + y += distanceToEdgeOfTray + 4; } else { - // have to account for the icon - iconY += ICON_SIZE; + y -= distanceToEdgeOfTray + 4; } GtkSystemTray.this.jmenu.setInvoker(GtkSystemTray.this.jmenu); - GtkSystemTray.this.jmenu.setLocation(iconX, iconY); + GtkSystemTray.this.jmenu.setLocation(x, y); GtkSystemTray.this.jmenu.setVisible(true); + GtkSystemTray.this.jmenu.requestFocus(); } } }); @@ -109,14 +137,8 @@ public class GtkSystemTray extends SystemTray { } }; // all the clicks. This is because native menu popups are a pain to figure out, so we cheat and use some java bits to do the popup - libgobject.g_signal_connect_data(this.trayIcon, "button_press_event", gtkCallback, null, null, 0); + libgobject.g_signal_connect_data(this.trayIcon, "button_press_event", this.gtkCallback, null, null, 0); libgtk.gdk_threads_leave(); - SwingUtil.invokeAndWait(new Runnable() { - @Override - public void run() { - GtkSystemTray.this.jmenu = new SystemTrayMenuPopup(); - } - }); this.active = true; } @@ -148,7 +170,7 @@ public class GtkSystemTray extends SystemTray { this.jmenu = null; this.connectionStatusItem = null; - libgtk.gtk_main_quit(); + GtkSupport.shutdownGTK(); libgtk.gdk_threads_leave(); super.removeTray(); @@ -194,6 +216,8 @@ public class GtkSystemTray extends SystemTray { menuEntry.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { +// SystemTrayMenuPopup source = (SystemTrayMenuPopup) ((JMenuItem)e.getSource()).getParent(); + GtkSystemTray.this.callbackExecutor.execute(new Runnable() { @Override public void run() { diff --git a/src/dorkbox/util/tray/swing/SwingSystemTray.java b/src/dorkbox/util/tray/swing/SwingSystemTray.java index 93e9aaaf..477c13fa 100644 --- a/src/dorkbox/util/tray/swing/SwingSystemTray.java +++ b/src/dorkbox/util/tray/swing/SwingSystemTray.java @@ -75,7 +75,7 @@ public class SwingSystemTray extends dorkbox.util.tray.SystemTray { public void run() { SwingSystemTray.this.tray = SystemTray.getSystemTray(); if (SwingSystemTray.this.tray == null) { - logger.warn("The system tray is not available"); + logger.error("The system tray is not available"); } else { SwingSystemTray.this.jmenu = new SystemTrayMenuPopup(); @@ -91,9 +91,6 @@ public class SwingSystemTray extends dorkbox.util.tray.SystemTray { Point point = e.getPoint(); Rectangle bounds = SwingUtil.getScreenBoundsAt(point); - // linux gtk was ICON_SIZE+4 - int PADDING = ICON_SIZE/2; - int x = point.x; int y = point.y; @@ -113,20 +110,6 @@ public class SwingSystemTray extends dorkbox.util.tray.SystemTray { x -= size.width; // snap to edge of mouse } -// if (x + size.width > bounds.x + bounds.width) { -// // always put the menu in the middle -// x = bounds.x + bounds.width - size.width; -// } -// if (y + size.height > bounds.y + bounds.height) { -// y = bounds.y + bounds.height - size.height - PADDING; -// } - - // do we open at top-right or top-left? - // we ASSUME monitor size is greater than 640x480 AND that our tray icon is IN THE CORNER SOMEWHERE - - // always put the menu in the horiz. middle -// x -= size.width / 4; - SwingSystemTray.this.jmenu.setInvoker(SwingSystemTray.this.jmenu); SwingSystemTray.this.jmenu.setLocation(x, y); SwingSystemTray.this.jmenu.setVisible(true); @@ -148,7 +131,7 @@ public class SwingSystemTray extends dorkbox.util.tray.SystemTray { Image newImage(String name) { String iconPath = iconPath(name); - return new ImageIcon(iconPath).getImage().getScaledInstance(ICON_SIZE, ICON_SIZE, Image.SCALE_SMOOTH); + return new ImageIcon(iconPath).getImage().getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH); } @Override