From dd50b1487b18ace628eb4a52b2e7a0c4273eb10d Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Fri, 6 May 2016 13:45:30 +0000 Subject: [PATCH] DTG: Implement second TrayManager menu implementation in Swing. Use Swing for non-Windows menus because AWT looks terrible on Linux and the button handling there is almost impossible to fix TODO: test on Mac --- .../i2p/desktopgui/ExternalTrayManager.java | 37 +++- .../i2p/desktopgui/InternalTrayManager.java | 163 +++++++++++++++++- .../src/net/i2p/desktopgui/Main.java | 10 +- .../src/net/i2p/desktopgui/TrayManager.java | 134 +++++++++++--- 4 files changed, 303 insertions(+), 41 deletions(-) diff --git a/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java index a18d16b223..5c0c0a1cc7 100644 --- a/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java +++ b/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java @@ -6,6 +6,8 @@ import java.awt.TrayIcon; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; import javax.swing.SwingWorker; import net.i2p.I2PAppContext; @@ -20,20 +22,17 @@ import net.i2p.desktopgui.router.RouterManager; */ class ExternalTrayManager extends TrayManager { - public ExternalTrayManager(I2PAppContext ctx, Main main) { - super(ctx, main); + public ExternalTrayManager(I2PAppContext ctx, Main main, boolean useSwing) { + super(ctx, main, useSwing); } - @Override public PopupMenu getMainMenu() { PopupMenu popup = new PopupMenu(); MenuItem startItem = new MenuItem(_t("Start I2P")); startItem.addActionListener(new ActionListener() { - @Override public void actionPerformed(ActionEvent arg0) { new SwingWorker<Object, Object>() { - @Override protected Object doInBackground() throws Exception { RouterManager.start(); @@ -48,10 +47,36 @@ class ExternalTrayManager extends TrayManager { //since that risks killing the I2P process as well. tray.remove(trayIcon); } + }.execute(); + } + }); + popup.add(startItem); + return popup; + } + + public JPopupMenu getSwingMainMenu() { + JPopupMenu popup = new JPopupMenu(); + JMenuItem startItem = new JMenuItem(_t("Start I2P")); + startItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + RouterManager.start(); + return null; + } + @Override + protected void done() { + trayIcon.displayMessage(_t("Starting"), _t("I2P is starting!"), TrayIcon.MessageType.INFO); + //Hide the tray icon. + //We cannot stop the desktopgui program entirely, + //since that risks killing the I2P process as well. + tray.remove(trayIcon); + } }.execute(); } - }); popup.add(startItem); return popup; diff --git a/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java index 8c49559b8d..bf7d7c947b 100644 --- a/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java +++ b/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java @@ -5,6 +5,9 @@ import java.awt.PopupMenu; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; import javax.swing.SwingWorker; import net.i2p.desktopgui.router.RouterManager; @@ -23,14 +26,14 @@ class InternalTrayManager extends TrayManager { private final RouterContext _context; private final Log log; private MenuItem _restartItem, _stopItem, _cancelItem; + private JMenuItem _jrestartItem, _jstopItem, _jcancelItem; - public InternalTrayManager(RouterContext ctx, Main main) { - super(ctx, main); + public InternalTrayManager(RouterContext ctx, Main main, boolean useSwing) { + super(ctx, main, useSwing); _context = ctx; log = ctx.logManager().getLog(InternalTrayManager.class); } - @Override public PopupMenu getMainMenu() { PopupMenu popup = new PopupMenu(); @@ -171,6 +174,146 @@ class InternalTrayManager extends TrayManager { return popup; } + public JPopupMenu getSwingMainMenu() { + JPopupMenu popup = new JPopupMenu(); + + JMenuItem browserLauncher = new JMenuItem(_t("Launch I2P Browser")); + browserLauncher.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + return null; + } + + @Override + protected void done() { + try { + I2PDesktop.browse("http://localhost:7657"); + } catch (BrowseException e1) { + log.log(Log.WARN, "Failed to open browser!", e1); + } + } + }.execute(); + } + }); + + JMenu desktopguiConfigurationLauncher = new JMenu(_t("Configure I2P System Tray")); + JMenuItem configSubmenu = new JMenuItem(_t("Disable")); + configSubmenu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + configureDesktopgui(false); + return null; + } + }.execute(); + } + }); + + final JMenuItem restartItem; + if (_context.hasWrapper()) { + restartItem = new JMenuItem(_t("Restart I2P")); + restartItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + RouterManager.restartGracefully(_context); + return null; + } + }.execute(); + } + }); + } else { + restartItem = null; + } + + final JMenuItem stopItem = new JMenuItem(_t("Stop I2P")); + stopItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + RouterManager.shutDownGracefully(_context); + return null; + } + }.execute(); + } + }); + + final JMenuItem restartItem2; + if (_context.hasWrapper()) { + restartItem2 = new JMenuItem(_t("Restart I2P Immediately")); + restartItem2.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + RouterManager.restart(_context); + return null; + } + }.execute(); + } + }); + } else { + restartItem2 = null; + } + + final JMenuItem stopItem2 = new JMenuItem(_t("Stop I2P Immediately")); + stopItem2.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + RouterManager.shutDown(_context); + return null; + } + }.execute(); + } + }); + + final JMenuItem cancelItem = new JMenuItem(_t("Cancel I2P Shutdown")); + cancelItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + RouterManager.cancelShutdown(_context); + return null; + } + }.execute(); + } + }); + + popup.add(browserLauncher); + popup.addSeparator(); + desktopguiConfigurationLauncher.add(configSubmenu); + popup.add(desktopguiConfigurationLauncher); + popup.addSeparator(); + if (_context.hasWrapper()) + popup.add(restartItem); + popup.add(stopItem); + if (_context.hasWrapper()) + popup.add(restartItem2); + popup.add(stopItem2); + popup.add(cancelItem); + + _jrestartItem = restartItem; + _jstopItem = stopItem; + _jcancelItem = cancelItem; + + return popup; + } + /** * Update the menu * @since 0.9.26 @@ -179,15 +322,23 @@ class InternalTrayManager extends TrayManager { boolean x = RouterManager.isShutdownInProgress(_context); if (_restartItem != null) _restartItem.setEnabled(!x); - _stopItem.setEnabled(!x); - _cancelItem.setEnabled(x); + if (_stopItem != null) + _stopItem.setEnabled(!x); + if (_cancelItem != null) + _cancelItem.setEnabled(x); + if (_jrestartItem != null) + _jrestartItem.setEnabled(!x); + if (_jstopItem != null) + _jstopItem.setEnabled(!x); + if (_jcancelItem != null) + _jcancelItem.setEnabled(x); } /** * @since 0.9.26 from removed gui/DesktopguiConfigurationFrame */ private void configureDesktopgui(boolean enable) { - String property = "desktopgui.enabled"; + String property = Main.PROP_ENABLE; String value = Boolean.toString(enable); try { diff --git a/apps/desktopgui/src/net/i2p/desktopgui/Main.java b/apps/desktopgui/src/net/i2p/desktopgui/Main.java index 76e5f2c573..f7626f3aef 100644 --- a/apps/desktopgui/src/net/i2p/desktopgui/Main.java +++ b/apps/desktopgui/src/net/i2p/desktopgui/Main.java @@ -15,6 +15,7 @@ import net.i2p.desktopgui.util.*; import net.i2p.router.RouterContext; import net.i2p.router.app.RouterApp; import net.i2p.util.Log; +import net.i2p.util.SystemVersion; import net.i2p.util.Translate; import net.i2p.util.I2PProperties.I2PPropertyCallback; @@ -29,6 +30,8 @@ public class Main implements RouterApp { private final Log log; private ClientAppState _state = UNINITIALIZED; private TrayManager _trayManager; + public static final String PROP_ENABLE = "desktopgui.enabled"; + private static final String PROP_SWING = "desktopgui.swing"; /** * @since 0.9.26 @@ -60,10 +63,11 @@ public class Main implements RouterApp { */ private synchronized void startUp() throws Exception { final TrayManager trayManager; + boolean useSwing = _appContext.getProperty(PROP_SWING, !SystemVersion.isWindows()); if (_context != null) - trayManager = new InternalTrayManager(_context, this); + trayManager = new InternalTrayManager(_context, this, useSwing); else - trayManager = new ExternalTrayManager(_appContext, this); + trayManager = new ExternalTrayManager(_appContext, this, useSwing); trayManager.startManager(); _trayManager = trayManager; changeState(RUNNING); @@ -72,14 +76,12 @@ public class Main implements RouterApp { if (_context != null) { _context.addPropertyCallback(new I2PPropertyCallback() { - @Override public void propertyChanged(String arg0, String arg1) { if(arg0.equals(Translate.PROP_LANG)) { trayManager.languageChanged(); } } - }); } } diff --git a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java index 1982dba61b..f5ff8a4053 100644 --- a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java +++ b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java @@ -1,15 +1,26 @@ package net.i2p.desktopgui; import java.awt.AWTException; +import java.awt.Dimension; +import java.awt.Font; import java.awt.Image; import java.awt.PopupMenu; import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.net.URL; +import javax.swing.JFrame; +import javax.swing.JPopupMenu; +import javax.swing.event.MenuKeyEvent; +import javax.swing.event.MenuKeyListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; + import net.i2p.I2PAppContext; import net.i2p.desktopgui.i18n.DesktopguiTranslator; import net.i2p.util.SystemVersion; @@ -21,6 +32,7 @@ abstract class TrayManager { protected final I2PAppContext _appContext; protected final Main _main; + protected final boolean _useSwing; ///The tray area, or null if unsupported protected SystemTray tray; ///Our tray icon, or null if unsupported @@ -29,37 +41,99 @@ abstract class TrayManager { /** * Instantiate tray manager. */ - protected TrayManager(I2PAppContext ctx, Main main) { + protected TrayManager(I2PAppContext ctx, Main main, boolean useSwing) { _appContext = ctx; _main = main; + _useSwing = useSwing; } /** * Add the tray icon to the system tray and start everything up. */ - public synchronized void startManager() throws AWTException { - if(SystemTray.isSupported()) { - // TODO figure out how to get menu to pop up on left-click - // left-click does nothing by default - // MouseListener, MouseEvent, ... - tray = SystemTray.getSystemTray(); - // Windows typically has tooltips; Linux (at least Ubuntu) doesn't - String tooltip = SystemVersion.isWindows() ? _t("I2P: Right-click for menu") : null; - trayIcon = new TrayIcon(getTrayImage(), tooltip, getMainMenu()); - trayIcon.setImageAutoSize(true); //Resize image to fit the system tray - tray.add(trayIcon); - // 16x16 on Windows, 24x24 on Linux, but that will probably vary - //System.out.println("Tray icon size is " + trayIcon.getSize()); - trayIcon.addMouseListener(new MouseListener() { - public void mouseClicked(MouseEvent m) { updateMenu(); } - public void mouseEntered(MouseEvent m) { updateMenu(); } - public void mouseExited(MouseEvent m) { updateMenu(); } - public void mousePressed(MouseEvent m) { updateMenu(); } - public void mouseReleased(MouseEvent m) { updateMenu(); } - }); - } else { + public synchronized void startManager() throws AWTException { + if (!SystemTray.isSupported()) throw new AWTException("SystemTray not supported"); - } + tray = SystemTray.getSystemTray(); + // Windows typically has tooltips; Linux (at least Ubuntu) doesn't + String tooltip = SystemVersion.isWindows() ? _t("I2P: Right-click for menu") : null; + TrayIcon ti; + if (_useSwing) + ti = getSwingTrayIcon(tooltip); + else + ti = getAWTTrayIcon(tooltip); + ti.setImageAutoSize(true); //Resize image to fit the system tray + tray.add(ti); + trayIcon = ti; + } + + private TrayIcon getAWTTrayIcon(String tooltip) throws AWTException { + PopupMenu menu = getMainMenu(); + if (!SystemVersion.isWindows()) + menu.setFont(new Font("Arial", Font.BOLD, 14)); + TrayIcon ti = new TrayIcon(getTrayImage(), tooltip, menu); + ti.addMouseListener(new MouseListener() { + public void mouseClicked(MouseEvent m) {} + public void mouseEntered(MouseEvent m) {} + public void mouseExited(MouseEvent m) {} + public void mousePressed(MouseEvent m) { updateMenu(); } + public void mouseReleased(MouseEvent m) { updateMenu(); } + }); + return ti; + } + + private TrayIcon getSwingTrayIcon(String tooltip) throws AWTException { + // A JPopupMenu by itself is hard to get rid of, + // so we hang it off a zero-size, undecorated JFrame. + // http://stackoverflow.com/questions/1498789/jpopupmenu-behavior + // http://stackoverflow.com/questions/2581314/how-do-you-hide-a-swing-popup-when-you-click-somewhere-else + final JFrame frame = new JFrame(); + // http://stackoverflow.com/questions/2011601/jframe-without-frame-border-maximum-button-minimum-button-and-frame-icon + frame.setUndecorated(true); + frame.setMinimumSize(new Dimension(0, 0)); + frame.setSize(0, 0); + final JPopupMenu menu = getSwingMainMenu(); + menu.setFocusable(true); + frame.add(menu); + TrayIcon ti = new TrayIcon(getTrayImage(), tooltip, null); + ti.addMouseListener(new MouseListener() { + public void mouseClicked(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mousePressed(MouseEvent e) { handle(e); } + public void mouseReleased(MouseEvent e) { handle(e); } + private void handle(MouseEvent e) { + // http://stackoverflow.com/questions/17258250/changing-the-laf-of-a-popupmenu-for-a-trayicon-in-java + // menu visible check is failsafe, for when menu gets cancelled + if (!frame.isVisible() || !menu.isVisible()) { + frame.setLocation(e.getX(), e.getY()); + frame.setVisible(true); + menu.show(frame, 0, 0); + } + updateMenu(); + } + }); + menu.addPopupMenuListener(new PopupMenuListener() { + public void popupMenuCanceled(PopupMenuEvent e) { frame.setVisible(false); } + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} + public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} + }); + // this is to make it go away when we click elsewhere + // doesn't do anything + menu.addFocusListener(new FocusListener() { + public void focusGained(FocusEvent e) {} + public void focusLost(FocusEvent e) { frame.setVisible(false); } + }); + // this is to make it go away when we hit escape + // doesn't do anything + menu.addMenuKeyListener(new MenuKeyListener() { + public void menuKeyPressed(MenuKeyEvent e) {} + public void menuKeyReleased(MenuKeyEvent e) {} + public void menuKeyTyped(MenuKeyEvent e) { + if (e.getKeyChar() == (char) 0x1b) + frame.setVisible(false); + } + }); + return ti; } /** @@ -76,8 +150,11 @@ abstract class TrayManager { } public synchronized void languageChanged() { - if (trayIcon != null) - trayIcon.setPopupMenu(getMainMenu()); + if (trayIcon != null) { + if (!_useSwing) + trayIcon.setPopupMenu(getMainMenu()); + // else TODO + } } /** @@ -86,6 +163,13 @@ abstract class TrayManager { */ protected abstract PopupMenu getMainMenu(); + /** + * Build a popup menu, adding callbacks to the different items. + * @return popup menu + * @since 0.9.26 + */ + protected abstract JPopupMenu getSwingMainMenu(); + /** * Update the menu * @since 0.9.26 -- GitLab