diff --git a/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java b/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java new file mode 100644 index 0000000..4feb9c0 --- /dev/null +++ b/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java @@ -0,0 +1,144 @@ +/* + * Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jdesktop.swinghelper.tray; + +import javax.swing.*; +import javax.swing.event.PopupMenuListener; +import javax.swing.event.PopupMenuEvent; +import java.awt.*; +import java.awt.geom.Ellipse2D; +import java.awt.image.BufferedImage; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; + +public class JXTrayIcon extends TrayIcon { + private JPopupMenu menu; + private static JDialog dialog; + static { + dialog = new JDialog((Frame) null); + dialog.setUndecorated(true); + dialog.setAlwaysOnTop(true); + } + + private static PopupMenuListener popupListener = new PopupMenuListener() { + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + } + + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + dialog.setVisible(false); + } + + public void popupMenuCanceled(PopupMenuEvent e) { + dialog.setVisible(false); + } + }; + + + public JXTrayIcon(Image image) { + super(image); + addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + showJPopupMenu(e); + } + + public void mouseReleased(MouseEvent e) { + showJPopupMenu(e); + } + }); + } + + protected void showJPopupMenu(MouseEvent e) { + if (menu != null) { + Dimension size = menu.getPreferredSize(); + showJPopupMenu(e.getX(), e.getY() - size.height); + } + } + + protected void showJPopupMenu(int x, int y) { + dialog.setLocation(x, y); + dialog.setVisible(true); + menu.show(dialog.getContentPane(), 0, 0); + // popup works only for focused windows + dialog.toFront(); + } + + public JPopupMenu getJPopupMenu() { + return menu; + } + + public void setJPopupMenu(JPopupMenu menu) { + if (this.menu != null) { + this.menu.removePopupMenuListener(popupListener); + } + this.menu = menu; + menu.addPopupMenuListener(popupListener); + } + + private static void createGui() { + JXTrayIcon tray = new JXTrayIcon(createImage()); + tray.setJPopupMenu(createJPopupMenu()); + try { + SystemTray.getSystemTray().add(tray); + } catch (AWTException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) throws Exception { + + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + createGui(); + } + }); + } + + static Image createImage() { + BufferedImage i = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = (Graphics2D) i.getGraphics(); + g2.setColor(Color.RED); + g2.fill(new Ellipse2D.Float(0, 0, i.getWidth(), i.getHeight())); + g2.dispose(); + return i; + } + + static JPopupMenu createJPopupMenu() { + final JPopupMenu m = new JPopupMenu(); + m.add(new JMenuItem("Item 1")); + m.add(new JMenuItem("Item 2")); + JMenu submenu = new JMenu("Submenu"); + submenu.add(new JMenuItem("item 1")); + submenu.add(new JMenuItem("item 2")); + submenu.add(new JMenuItem("item 3")); + m.add(submenu); + JMenuItem exitItem = new JMenuItem("Exit"); + exitItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + m.add(exitItem); + return m; + } +} diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index 38e5d9a..daec196 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.jdesktop.swinghelper.tray.JXTrayIcon; import qz.auth.Certificate; import qz.deploy.DeployUtilities; import qz.deploy.WindowsDeploy; @@ -58,11 +59,7 @@ public class TrayManager { // The cached icons private final IconCache iconCache; - // Time in millis before the popup menu disappears - final int POPUP_TIMEOUT = 2000; - - // Custom swing pop-up menu - AutoHidePopupTray tray; + JXTrayIcon tray; private ConfirmDialog confirmDialog; private GatewayDialog gatewayDialog; @@ -105,15 +102,27 @@ public TrayManager() { shortcutCreator = DeployUtilities.getSystemShortcutCreator(); shortcutCreator.setShortcutName(Constants.ABOUT_TITLE); - // Initialize a custom Swing system tray that hides after a timeout - tray = new AutoHidePopupTray(POPUP_TIMEOUT); - tray.setToolTipText(name); + SystemUtilities.setSystemLookAndFeel(); + + if (SystemTray.isSupported()) { + // Accommodate some OS-specific tray bugs + tray = SystemUtilities.isLinux() ? new ModernTrayIcon() : new JXTrayIcon(new ImageIcon(new byte[1]).getImage()); - // Iterates over all images denoted by IconCache.getTypes() and caches them - iconCache = new IconCache(tray.getIconSize()); - tray.setImage(iconCache.getImage(IconCache.Icon.DANGER_ICON)); + // Iterates over all images denoted by IconCache.getTypes() and caches them + iconCache = new IconCache(tray.getSize()); + tray.setImage(iconCache.getImage(IconCache.Icon.DANGER_ICON)); + tray.setToolTip(name); + + try { + SystemTray.getSystemTray().add(tray); + } catch (AWTException awt) { + trayLogger.log(Level.SEVERE, "Could not attach tray", awt); + } + } else { + iconCache = new IconCache(); + } - // Linux spcecific tasks + // OS-specific tasks if (SystemUtilities.isLinux()) { // Fix the tray icon to look proper on Ubuntu UbuntuUtilities.fixTrayIcons(iconCache); @@ -131,32 +140,7 @@ public TrayManager() { // The ok/cancel dialog confirmDialog = new ConfirmDialog(null, "Please Confirm", iconCache); - addMenuItems(tray); - //tray.displayMessage(name, name + " is running.", Level.INFO); - - if (tray.getTrayIcon()!=null){ - tray.getTrayIcon().addMouseListener(new MouseListener() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - tray.setVisible(false); - aboutListener.actionPerformed(new ActionEvent(e.getSource(),e.getID(),null)); - } - } - - @Override - public void mousePressed(MouseEvent e) {} - - @Override - public void mouseReleased(MouseEvent e) {} - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - }); - } + addMenuItems(); } /** @@ -176,7 +160,9 @@ public void run() { /** * Builds the swing pop-up menu with the specified items */ - private void addMenuItems(JPopupMenu popup) { + private void addMenuItems() { + JPopupMenu popup = new JPopupMenu(); + JMenu advancedMenu = new JMenu("Advanced"); advancedMenu.setMnemonic(KeyEvent.VK_A); advancedMenu.setIcon(iconCache.getIcon(IconCache.Icon.SETTINGS_ICON)); @@ -253,6 +239,8 @@ private void addMenuItems(JPopupMenu popup) { popup.add(startupItem); popup.add(separator); popup.add(exitItem); + + tray.setJPopupMenu(popup); } @@ -391,10 +379,10 @@ private void shortcutToggle(ActionEvent e, DeployUtilities.ToggleType toggleType // Remove shortcut entry if (confirmDialog.prompt("Remove " + name + " from " + toggleType + "?")) { if (!shortcutCreator.removeShortcut(toggleType)) { - tray.displayMessage(name, "Error removing " + toggleType + " entry", Level.SEVERE); + tray.displayMessage(name, "Error removing " + toggleType + " entry", TrayIcon.MessageType.ERROR); checkBoxState = true; // Set our checkbox back to true } else { - tray.displayMessage(name, "Successfully removed " + toggleType + " entry", Level.INFO); + tray.displayMessage(name, "Successfully removed " + toggleType + " entry", TrayIcon.MessageType.INFO); } } else { checkBoxState = true; // Set our checkbox back to true @@ -402,10 +390,10 @@ private void shortcutToggle(ActionEvent e, DeployUtilities.ToggleType toggleType } else { // Add shortcut entry if (!shortcutCreator.createShortcut(toggleType)) { - tray.displayMessage(name, "Error creating " + toggleType + " entry", Level.SEVERE); + tray.displayMessage(name, "Error creating " + toggleType + " entry", TrayIcon.MessageType.ERROR); checkBoxState = false; // Set our checkbox back to false } else { - tray.displayMessage(name, "Successfully added " + toggleType + " entry", Level.INFO); + tray.displayMessage(name, "Successfully added " + toggleType + " entry", TrayIcon.MessageType.INFO); } } @@ -418,7 +406,7 @@ private void shortcutToggle(ActionEvent e, DeployUtilities.ToggleType toggleType * Displays a basic error dialog. */ private void showErrorDialog(String message) { - JOptionPane.showMessageDialog(tray, message, name, JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(null, message, name, JOptionPane.ERROR_MESSAGE); } public boolean showGatewayDialog(final Certificate cert) { @@ -522,7 +510,7 @@ public static String getPorts(Server server) { * Thread safe method for setting an info status message */ public void displayInfoMessage(String text) { - displayMessage(name, text, Level.INFO); + displayMessage(name, text, TrayIcon.MessageType.INFO); } /** @@ -530,7 +518,7 @@ public void displayInfoMessage(String text) { * notifications" is checked. */ public void displayFineMessage(String text) { - displayMessage(name, text, Level.FINE); + displayMessage(name, text, TrayIcon.MessageType.NONE); } /** @@ -544,7 +532,7 @@ public void setDefaultIcon() { * Thread safe method for setting the error status message */ public void displayErrorMessage(String text) { - displayMessage(name, text, Level.SEVERE); + displayMessage(name, text, TrayIcon.MessageType.ERROR); } /** @@ -558,7 +546,7 @@ public void setDangerIcon() { * Thread safe method for setting the warning status message */ public void displayWarningMessage(String text) { - displayMessage(name, text, Level.WARNING); + displayMessage(name, text, TrayIcon.MessageType.WARNING); } @@ -577,7 +565,7 @@ private void setIcon(final IconCache.Icon i) { SwingUtilities.invokeLater(new Thread(new Runnable() { @Override public void run() { - tray.setIcon(iconCache.getIcon(i)); + tray.setImage(iconCache.getImage(i)); } })); } @@ -590,21 +578,34 @@ public void run() { * @param text The text body of the tray message * @param level The message type: Level.INFO, .WARN, .SEVERE */ - private void displayMessage(final String caption, final String text, final Level level) { + private void displayMessage(final String caption, final String text, final TrayIcon.MessageType level) { if (tray != null) { SwingUtilities.invokeLater(new Thread(new Runnable() { @Override public void run() { boolean showAllNotifications = prefs.getBoolean(notificationsKey, false); - if (showAllNotifications || (level == Level.INFO || level == Level.SEVERE)) { + if (showAllNotifications || (level == TrayIcon.MessageType.INFO || level == TrayIcon.MessageType.ERROR)) { tray.displayMessage(caption, text, level); } - trayLogger.log(level, "Tray Message: " + text); + trayLogger.log(convertLevel(level), "Tray Message: " + text); } })); } } + public static Level convertLevel(TrayIcon.MessageType level) { + switch (level) { + case NONE: + return Level.FINE; + case ERROR: + return Level.SEVERE; + case WARNING: + return Level.WARNING; + default: + return Level.INFO; + } + } + public void addLogHandler(Logger logger) { if (logHandler == null) { try { diff --git a/src/qz/ui/AutoHidePopupTray.java b/src/qz/ui/AutoHidePopupTray.java deleted file mode 100644 index 3f9fd7d..0000000 --- a/src/qz/ui/AutoHidePopupTray.java +++ /dev/null @@ -1,197 +0,0 @@ -/** - * @author Tres Finocchiaro - * - * Copyright (C) 2013 Tres Finocchiaro, QZ Industries - * - * IMPORTANT: This software is dual-licensed - * - * LGPL 2.1 This is free software. This software and source code are released - * under the "LGPL 2.1 License". A copy of this license should be distributed - * with this software. http://www.gnu.org/licenses/lgpl-2.1.html - * - * QZ INDUSTRIES SOURCE CODE LICENSE This software and source code *may* instead - * be distributed under the "QZ Industries Source Code License", available by - * request ONLY. If source code for this project is to be made proprietary for - * an individual and/or a commercial entity, written permission via a copy of - * the "QZ Industries Source Code License" must be obtained first. If you've - * obtained a copy of the proprietary license, the terms and conditions of the - * license apply only to the licensee identified in the agreement. Only THEN may - * the LGPL 2.1 license be voided. - * - */ - -package qz.ui; - -import javax.swing.*; -import java.awt.Component; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Pop-up Menu which auto-hides after a certain timeout - * @author A. Tres Finocchiaro - */ -public class AutoHidePopupTray extends PopupTray { - // The timeout in milliseconds before this item is hidden - private final int timeout; - - // Attached to all menu items, needed for detecting mouse hover - private final MouseListener globalMouseListener; - - // Stores the current mouse hover status - private boolean mouseOver; - - // Notifies the "hide listener thread" to terminate - private boolean threadActive; - - /** - * Default constructor - * @param timeout The amount of time before this menu hides itself - */ - public AutoHidePopupTray(final int timeout) { - super(); - this.mouseOver = false; - this.threadActive = true; - this.timeout = timeout; - this.globalMouseListener = createGlobalMouseListener(); - setSystemLookAndFeel(); - } - - @Override - public void setVisible(boolean visible) { - super.setVisible(visible); - setMouseOver(true); - if (visible) { - createHideListenerThread(timeout); - } else { - stopHideListenerThreads(); - } - } - - /** - * Sets a flag indicating whether or not the mouse is currently over the menu - * @param mouseOver whether or not the mouse is currently over the menu - */ - private void setMouseOver(boolean mouseOver) { - this.mouseOver = mouseOver; - } - - /** - * Returns whether or not the mouse is currently over the menu - * @return whether or not the mouse is currently over the menu - */ - private boolean getMouseOver() { - return mouseOver; - } - - - /** - * Adds a specified JMenuItem and also attaches a mouse listener which - * is used to detect focus - * @param menuItem The MenuItem to add - * @return The MenuItem argument - */ - @Override - public JMenuItem add(JMenuItem menuItem) { - if (menuItem != null) { menuItem.addMouseListener(globalMouseListener); } - recurseSubMenuItems(menuItem); - return super.add(menuItem); - } - - public void recurseSubMenuItems(Component c) { - if (c instanceof JMenu) { - for (Component component : ((JMenu)c).getMenuComponents()) { - component.addMouseListener(globalMouseListener); - recurseSubMenuItems(component); - } - } - } - - /** - * Adds a specified Component and also attaches a mouse listener which - * is used to detect focus - * @param comp The Component to add - * @return The Component argument - */ - @Override - public Component add(Component comp) { - if (comp != null) { comp.addMouseListener(globalMouseListener); } - return super.add(comp); - } - - - /** - * Creates a single mouse listener which all JMenuItems will share - * @return A new MouseListener object - */ - private MouseListener createGlobalMouseListener() { - return new MouseListener() { - public void mouseClicked(MouseEvent e) {} - public void mousePressed(MouseEvent e) {} - public void mouseReleased(MouseEvent e) {} - public void mouseEntered(MouseEvent e) { setMouseOver(true); } - public void mouseExited(MouseEvent e) { setMouseOver(false); } - }; - } - - /** - * Tells all internal "hide listener threads" to stop - */ - private void stopHideListenerThreads() { - threadActive = false; - } - - - - /** - * Creates a new thread which hides this component after a specified timeout value - * Self-aware of mouse position, this will only be hidden if the mouse isn't - * currently over the component. - * - * Since mouse position outside of a pop-up is impossible to determine on a - * system tray, we use the JMenuItem's enter/exit to set the hide flag instead - * @param timeout The timeout in milliseconds before this component is hidden - */ - private void createHideListenerThread(final int timeout) { - threadActive = true; - new Thread(new Runnable() { - final int STEP = 100; - int counter = 0; - @Override - public void run() { - while (threadActive) { - try { Thread.sleep(STEP); } - catch (InterruptedException e) {} - if (!threadActive) { - break; - } - if (!getMouseOver()) { - counter++; - } else { - counter = 0; - } - if (counter * STEP > timeout) { - SwingUtilities.invokeLater(new Thread(new Runnable() { - public void run() { setVisible(false); } - })); - } - } - } - }).start(); - } - - /** - * Attempts to set the Java Look & Feel to that which matches the Operating - * System - */ - private void setSystemLookAndFeel() { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception e) { - Logger.getLogger(getClass().getName()).log(Level.WARNING, - "Error getting the default look and feel"); - } - } -} diff --git a/src/qz/ui/ModernTrayIcon.java b/src/qz/ui/ModernTrayIcon.java new file mode 100644 index 0000000..989e3dc --- /dev/null +++ b/src/qz/ui/ModernTrayIcon.java @@ -0,0 +1,146 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2013 Tres Finocchiaro, QZ Industries + * + * IMPORTANT: This software is dual-licensed + * + * LGPL 2.1 This is free software. This software and source code are released + * under the "LGPL 2.1 License". A copy of this license should be distributed + * with this software. http://www.gnu.org/licenses/lgpl-2.1.html + * + * QZ INDUSTRIES SOURCE CODE LICENSE This software and source code *may* instead + * be distributed under the "QZ Industries Source Code License", available by + * request ONLY. If source code for this project is to be made proprietary for + * an individual and/or a commercial entity, written permission via a copy of + * the "QZ Industries Source Code License" must be obtained first. If you've + * obtained a copy of the proprietary license, the terms and conditions of the + * license apply only to the licensee identified in the agreement. Only THEN may + * the LGPL 2.1 license be voided. + * + */ + +package qz.ui; + +import org.jdesktop.swinghelper.tray.JXTrayIcon; +import qz.utils.SystemUtilities; + +import javax.swing.*; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import java.awt.*; +import java.awt.event.AWTEventListener; +import java.awt.event.MouseEvent; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +/** + * @author A. Tres Finocchiaro + */ +public class ModernTrayIcon extends JXTrayIcon { + private JFrame invisibleFrame; + private JPopupMenu popup; + private int x = 0, y = 0; + + public ModernTrayIcon() { + super(new ImageIcon(new byte[1]).getImage()); + } + + public ModernTrayIcon(Image image) { + super(image); + } + + public ModernTrayIcon(Image image, String tooltip) { + super(image); + setToolTip(tooltip); + } + + public ModernTrayIcon(Image image, String tooltip, JPopupMenu popup) { + super(image); + setToolTip(tooltip); + setJPopupMenu(popup); + } + + @Override + public void setJPopupMenu(final JPopupMenu popup) { + this.popup = popup; + + invisibleFrame = new JFrame(); + invisibleFrame.setAlwaysOnTop(true); + invisibleFrame.setUndecorated(true); + invisibleFrame.setBackground(Color.BLACK); + invisibleFrame.setSize(0, 0); + invisibleFrame.pack(); + + popup.addPopupMenuListener(new PopupMenuListener() { + @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} + @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { invisibleFrame.setVisible(false); } + @Override public void popupMenuCanceled(PopupMenuEvent e) { invisibleFrame.setVisible(false); } + }); + + invisibleFrame.addWindowListener(new WindowListener() { + @Override + public void windowActivated(WindowEvent we) { + // X11 bug + if (SystemUtilities.isFedora()) { + popup.show(invisibleFrame, 0, 0); + } else { + popup.setInvoker(invisibleFrame); + popup.setVisible(true); + popup.setLocation(x, SystemUtilities.isWindows() ? y - popup.getHeight() : y); + } + popup.requestFocus(); + } + + @Override public void windowOpened(WindowEvent we) {} + @Override public void windowClosing(WindowEvent we) {} + @Override public void windowClosed(WindowEvent we) {} + @Override public void windowIconified(WindowEvent we) {} + @Override public void windowDeiconified(WindowEvent we) {} + @Override public void windowDeactivated(WindowEvent we) {} + }); + + addTrayListener(); + } + + @Override + public JPopupMenu getJPopupMenu() { + return popup; + } + + /** + * Functional equivalent of a MouseAdapter, but accommodates an edge-case in Gnome3 where the tray + * icon cannot listen on mouse events. + */ + private void addTrayListener() { + Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { + @Override + public void eventDispatched(AWTEvent e) { + Point p = isTrayEvent(e); + if (p != null) { + x = p.x; y = p.y; + invisibleFrame.setVisible(true); + invisibleFrame.requestFocus(); + } + } + }, MouseEvent.MOUSE_EVENT_MASK); + } + + /** + * Determines if TrayIcon event is detected + * @param e An AWTEvent + * @return A Point on the screen which the tray event occurred, or null if none is found + */ + private static Point isTrayEvent(AWTEvent e) { + if (e instanceof MouseEvent) { + MouseEvent me = (MouseEvent)e; + + if (me.getID() == MouseEvent.MOUSE_RELEASED && me.getSource() != null) { + if (me.getSource().getClass().getName().contains("TrayIcon")) { + return me.getLocationOnScreen(); + } + } + } + return null; + } +} diff --git a/src/qz/ui/PopupTray.java b/src/qz/ui/PopupTray.java deleted file mode 100644 index 509bcba..0000000 --- a/src/qz/ui/PopupTray.java +++ /dev/null @@ -1,284 +0,0 @@ -/** - * @author Tres Finocchiaro - * - * Copyright (C) 2013 Tres Finocchiaro, QZ Industries - * - * IMPORTANT: This software is dual-licensed - * - * LGPL 2.1 This is free software. This software and source code are released - * under the "LGPL 2.1 License". A copy of this license should be distributed - * with this software. http://www.gnu.org/licenses/lgpl-2.1.html - * - * QZ INDUSTRIES SOURCE CODE LICENSE This software and source code *may* instead - * be distributed under the "QZ Industries Source Code License", available by - * request ONLY. If source code for this project is to be made proprietary for - * an individual and/or a commercial entity, written permission via a copy of - * the "QZ Industries Source Code License" must be obtained first. If you've - * obtained a copy of the proprietary license, the terms and conditions of the - * license apply only to the licensee identified in the agreement. Only THEN may - * the LGPL 2.1 license be voided. - * - */ - -package qz.ui; - -import qz.utils.SystemUtilities; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.AWTEventListener; -import java.awt.event.MouseEvent; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Creates a more robust System Tray menu by leveraging a JPopupMenu. - * Java currently only supports an AWT PopupMenu, so this class is - * a work-around for that limitation. - * - * @author A. Tres Finocchiaro - */ -public class PopupTray extends JPopupMenu { - private final SystemTray systemTray; - private final TrayIcon trayIcon; - - /** - * Constructs a JPopupMenu attached to the system's SystemTray with - * the specified imageIcon and toolTipText - * @param imageIcon The icon to display in the System Tray - * @param toolTipText The text to display during tray mouse-over - */ - public PopupTray(ImageIcon imageIcon, String toolTipText) { - super(); - trayIcon = createTrayIcon(); - systemTray = getSystemTray(); - attachSystemTray(trayIcon); - setIcon(imageIcon); - setToolTipText(toolTipText); - } - - /** - * Constructs a JPopupMenu attached to the system's SystemTray with - * the specified image and toolTipText - * @param image The image to display in the System Tray - * @param toolTipText The text to display during tray mouse-over - */ - public PopupTray(Image image, String toolTipText) { - super(toolTipText); - trayIcon = createTrayIcon(); - systemTray = getSystemTray(); - attachSystemTray(trayIcon); - setImage(image); - setToolTipText(toolTipText); - } - - /** - * Constructs a JPopupMenu attached to the system's SystemTray with - * the specified toolTipText - * @param toolTipText The text to display during tray mouse-over - */ - public PopupTray(String toolTipText) { - super(toolTipText); - trayIcon = createTrayIcon(); - systemTray = getSystemTray(); - attachSystemTray(trayIcon); - setToolTipText(toolTipText); - } - - /** - * Constructs a JPopupMenu attached to the system's SystemTray with - * no icon and no toolTipText - */ - public PopupTray() { - super(); - trayIcon = createTrayIcon(); - systemTray = getSystemTray(); - attachSystemTray(trayIcon); - } - - /** - * Sets the TrayIcon image based on the Image specified - * @param image The image to set the TrayIcon to - */ - public final void setImage(Image image) { - if (image != null && trayIcon != null) { - trayIcon.setImage(image); - } - } - - /** - * Sets the toolTipText for the underlying TrayIcon instance, rather than - * that of the JPopupMenu - * @param toolTipText tool text to set - */ - @Override - public final void setToolTipText(String toolTipText) { - if (trayIcon != null){ - trayIcon.setToolTip(toolTipText); - } - } - - /** - * Returns the underlying TrayIcon for this object - * @return The underlying TrayIcon object - */ - public TrayIcon getTrayIcon() { - return trayIcon; - } - - /** - * Returns the width of the underlying TrayIcon - * @return Width measurement in pixels - */ - public int getTrayWidth() { - return (trayIcon != null)?(int)trayIcon.getSize().getWidth():1; - } - - /** - * Returns the height of the underlying TrayIcon - * @return Height measurement in pixels - */ - public int getTrayHeight() { - return (trayIcon != null)?(int)trayIcon.getSize().getHeight():1; - } - - /** - * Returns the size of the TrayIcons - * @return Height measurement in pixels - */ - public Dimension getIconSize() { - if (trayIcon != null){ - return trayIcon.getSize(); - } - return new Dimension(1,1); - } - - /** - * Sets the TrayIcon image based on the ImageIcon specified - * @param imageIcon The ImageIcon to set the TrayIcon to - */ - public final void setIcon(ImageIcon imageIcon) { - if (imageIcon != null && trayIcon != null) { - trayIcon.setImage(imageIcon.getImage()); - } - } - - /** - * Creates a custom tray icon with this JPopupMenu attached - * @return The AWT TrayIcon associated with this JPopupMenu - */ - private TrayIcon createTrayIcon() { - // Create an empty tray icon - if (SystemTray.isSupported()){ - TrayIcon newTrayIcon = new TrayIcon(new ImageIcon(new byte[1]).getImage()); - addTrayListener(); - newTrayIcon.setImageAutoSize(true); - return newTrayIcon; - } - return null; - } - - /** - * Functional equivalent of a MouseAdapter, but accommodates an edge-cases in Gnome3 where the tray - * icon cannot listen on mouse events. - * - * Since there's no component to listen on, show popup only when - * MouseEvent.getComponent() is null - */ - private void addTrayListener() { - Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { - @Override - public void eventDispatched(AWTEvent e) { - if (isTrayEvent(e)) { - MouseEvent me = (MouseEvent)e; - // X11 bug - if (SystemUtilities.isFedora()) { - show(me.getComponent(),0,0); - } else { - setInvoker(PopupTray.this); - setVisible(true); - setLocation(me.getX(), me.getY() - (SystemUtilities.isWindows() ? getHeight() : 0)); - } - } - } - }, MouseEvent.MOUSE_EVENT_MASK); - } - - private boolean isTrayEvent(AWTEvent e) { - if (e instanceof MouseEvent) { - MouseEvent me = (MouseEvent)e; - if (me.getID() == MouseEvent.MOUSE_RELEASED && me.getSource() != null) { - return me.getSource().getClass().getName().contains("TrayIcon"); - } - } - return false; - } - - - /** - * Convenience method for getTrayIcon().displayMessage(...). - * Displays a pop-up message near the tray icon. The message will - * disappear after a time or if the user clicks on it. - * @param caption the caption displayed above the text, usually in - * bold; may be null - * @param text the text displayed for the particular message; may be - * null - * @param level The Logger message severity, shouldn't be - * null - */ - public void displayMessage(String caption, String text, Level level) { - if (trayIcon != null){ - trayIcon.displayMessage(caption, text, convert(level)); - } - } - - - /** - * Attaches the specified TrayIcon to the SystemTray - * @param trayIcon The TrayIcon to attach - */ - private void attachSystemTray(TrayIcon trayIcon) { - try { - if (getSystemTray() != null) { - getSystemTray().add(trayIcon); - } - } catch (AWTException e) { - Logger.getLogger(getClass().getName()).log(Level.WARNING, - "Tray menu could not be added: {0}", e.getLocalizedMessage()); - if (SystemUtilities.isLinux()) { - Logger.getLogger(getClass().getName()).log(Level.WARNING, - "Ubuntu users: If the tray icon does not appear try running the following:\n\n" + - " $ gsettings set com.canonical.Unity.Panel systray-whitelist \"['all']\"\n\n" + - "... then log out and log back in to restart Unity."); - } - } - } - - /** - * Returns the SystemTray, or null if none exists - * @return The SystemTray, or null if none exists - */ - private SystemTray getSystemTray() { - if (SystemTray.isSupported()) { - if (systemTray == null) { - return SystemTray.getSystemTray(); - } - return systemTray; - } else { - Logger.getLogger(getClass().getName()).log(Level.SEVERE, - "System tray is not supported on this platform"); - return null; - } - } - - public static TrayIcon.MessageType convert(Level level) { - if (level.equals(Level.SEVERE)) { - return TrayIcon.MessageType.ERROR; - } else if (level.equals(Level.WARNING)) { - return TrayIcon.MessageType.WARNING; - } else if (level.equals(Level.INFO)) { - return TrayIcon.MessageType.INFO; - } - return TrayIcon.MessageType.NONE; - } -} diff --git a/src/qz/utils/SystemUtilities.java b/src/qz/utils/SystemUtilities.java index af51ba9..3678c1d 100644 --- a/src/qz/utils/SystemUtilities.java +++ b/src/qz/utils/SystemUtilities.java @@ -24,7 +24,10 @@ import qz.common.Constants; +import javax.swing.*; import java.io.File; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Utility class for OS detection functions. @@ -170,5 +173,15 @@ public static String getUname() { } return uname; } - + + public static boolean setSystemLookAndFeel() { + try { + UIManager.getDefaults().put("Button.showMnemonics", Boolean.TRUE); + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + return true; + } catch (Exception e) { + Logger.getLogger(SystemUtilities.class.getName()).log(Level.WARNING, "Error getting the default look and feel"); + } + return false; + } }