diff --git a/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java b/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java index 7d44c5010307086b70df7a29c0fc03435aa66314..e70cbf502e5104841ae1073aa26737d9962b7bdd 100644 --- a/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java +++ b/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java @@ -13,6 +13,9 @@ import net.i2p.I2PAppContext; import net.i2p.app.ClientAppManager; import net.i2p.app.ClientApp; import net.i2p.app.ClientAppState; +import net.i2p.app.MenuCallback; +import net.i2p.app.MenuHandle; +import net.i2p.app.MenuService; import net.i2p.app.NotificationService; import net.i2p.util.Log; import net.i2p.util.SystemVersion; @@ -24,7 +27,7 @@ import net.i2p.util.SystemVersion; * * @since 0.9.54 */ -public class ExternalMain implements ClientApp, NotificationService { +public class ExternalMain implements ClientApp, NotificationService, MenuService { private final I2PAppContext _appContext; private final ClientAppManager _mgr; @@ -60,7 +63,6 @@ public class ExternalMain implements ClientApp, NotificationService { * @throws AWTException on startup error, including systray not supported */ private synchronized void startUp() throws Exception { - final TrayManager trayManager; boolean useSwingDefault = !(SystemVersion.isWindows() || SystemVersion.isMac()); boolean useSwing = _appContext.getProperty(PROP_SWING, useSwingDefault); _trayManager = new ExternalTrayManager(_appContext, useSwing); @@ -200,6 +202,89 @@ public class ExternalMain implements ClientApp, NotificationService { return false; } + /////// MenuService methods + + /** + * Menu will start out shown and enabled, in the root menu + * + * @param message for the menu, translated + * @param callback fired on click + * @return null on error + * @since 0.9.59 + */ + public MenuHandle addMenu(String message, MenuCallback callback) { + return addMenu(message, callback, null); + } + + /** + * Menu will start out enabled, as a submenu + * + * @param message for the menu, translated + * @param callback fired on click + * @param parent the parent menu this will be a submenu of, or null for top level + * @return null on error + * @since 0.9.59 + */ + public MenuHandle addMenu(String message, MenuCallback callback, MenuHandle parent) { + if (_trayManager == null) + return null; + return _trayManager.addMenu(message, callback, parent); + } + + /** + * @since 0.9.59 + */ + public void removeMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.removeMenu(item); + } + + /** + * @since 0.9.59 + */ + public void showMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.showMenu(item); + } + + /** + * @since 0.9.59 + */ + public void hideMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.hideMenu(item); + } + + /** + * @since 0.9.59 + */ + public void enableMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.enableMenu(item); + } + + /** + * @since 0.9.59 + */ + public void disableMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.disableMenu(item); + } + + /** + * @since 0.9.59 + */ + public void updateMenu(String message, MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.updateMenu(message, item); + } + /////// ClientApp methods public synchronized void startup() { diff --git a/apps/desktopgui/src/net/i2p/desktopgui/Main.java b/apps/desktopgui/src/net/i2p/desktopgui/Main.java index 0a7e3f9bc2fb55d7a2ba86a66143574915b10ebc..921efaa055fec03cf996b23bf5c27e269f43d2fc 100644 --- a/apps/desktopgui/src/net/i2p/desktopgui/Main.java +++ b/apps/desktopgui/src/net/i2p/desktopgui/Main.java @@ -17,6 +17,9 @@ import net.i2p.I2PAppContext; import net.i2p.app.ClientAppManager; import net.i2p.app.ClientAppState; import static net.i2p.app.ClientAppState.*; +import net.i2p.app.MenuCallback; +import net.i2p.app.MenuHandle; +import net.i2p.app.MenuService; import net.i2p.app.NotificationService; import net.i2p.desktopgui.router.RouterManager; import net.i2p.router.RouterContext; @@ -29,7 +32,7 @@ import net.i2p.util.I2PProperties.I2PPropertyCallback; /** * The main class of the application. */ -public class Main implements RouterApp, NotificationService { +public class Main implements RouterApp, NotificationService, MenuService { // non-null private final I2PAppContext _appContext; @@ -245,6 +248,89 @@ public class Main implements RouterApp, NotificationService { return false; } + /////// MenuService methods + + /** + * Menu will start out shown and enabled, in the root menu + * + * @param message for the menu, translated + * @param callback fired on click + * @return null on error + * @since 0.9.59 + */ + public MenuHandle addMenu(String message, MenuCallback callback) { + return addMenu(message, callback, null); + } + + /** + * Menu will start out enabled, as a submenu + * + * @param message for the menu, translated + * @param callback fired on click + * @param parent the parent menu this will be a submenu of, or null for top level + * @return null on error + * @since 0.9.59 + */ + public MenuHandle addMenu(String message, MenuCallback callback, MenuHandle parent) { + if (_trayManager == null) + return null; + return _trayManager.addMenu(message, callback, parent); + } + + /** + * @since 0.9.59 + */ + public void removeMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.removeMenu(item); + } + + /** + * @since 0.9.59 + */ + public void showMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.showMenu(item); + } + + /** + * @since 0.9.59 + */ + public void hideMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.hideMenu(item); + } + + /** + * @since 0.9.59 + */ + public void enableMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.enableMenu(item); + } + + /** + * @since 0.9.59 + */ + public void disableMenu(MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.disableMenu(item); + } + + /** + * @since 0.9.59 + */ + public void updateMenu(String message, MenuHandle item) { + if (_trayManager == null) + return; + _trayManager.updateMenu(message, item); + } + /////// ClientApp methods /** @since 0.9.26 */ diff --git a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java index 0261c107d3aea571bc1822099a6093594aa6e66a..a7390d7fb61650afcde059914dde050b44328d77 100644 --- a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java +++ b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java @@ -4,6 +4,7 @@ import java.awt.AWTException; import java.awt.Dimension; import java.awt.Font; import java.awt.Image; +import java.awt.MenuItem; import java.awt.PopupMenu; import java.awt.SystemTray; import java.awt.Toolkit; @@ -16,8 +17,10 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; -import java.awt.MenuItem; import javax.swing.JFrame; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; @@ -28,6 +31,8 @@ import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import net.i2p.I2PAppContext; +import net.i2p.app.MenuCallback; +import net.i2p.app.MenuHandle; import net.i2p.apps.systray.UrlLauncher; import net.i2p.desktopgui.i18n.DesktopguiTranslator; import net.i2p.util.Log; @@ -47,6 +52,9 @@ abstract class TrayManager { protected volatile boolean _showNotifications; protected MenuItem _notificationItem1, _notificationItem2; protected JMenuItem _jnotificationItem1, _jnotificationItem2; + private final AtomicInteger _id = new AtomicInteger(); + private final List<MenuInternal> _menus; + private JPopupMenu _jPopupMenu; private static final String PNG_DIR = "/desktopgui/resources/images/"; private static final String MAC_ICON = "itoopie_black_24.png"; @@ -61,6 +69,7 @@ abstract class TrayManager { protected TrayManager(I2PAppContext ctx, boolean useSwing) { _appContext = ctx; _useSwing = useSwing; + _menus = new ArrayList<MenuInternal>(); } /** @@ -109,6 +118,7 @@ abstract class TrayManager { frame.setMinimumSize(new Dimension(0, 0)); frame.setSize(0, 0); final JPopupMenu menu = getSwingMainMenu(); + _jPopupMenu = menu; menu.setFocusable(true); frame.add(menu); TrayIcon ti = new TrayIcon(getTrayImage(), tooltip, null); @@ -375,6 +385,165 @@ abstract class TrayManager { _jnotificationItem1 = notificationItem1; } + /////// MenuService delegation methods + + /** + * @since 0.9.59 + */ + public MenuHandle addMenu(String message, final MenuCallback callback, MenuHandle p) { + MenuInternal parent = p != null ? (MenuInternal) p : null; + final int id = _id.incrementAndGet(); + final MenuInternal rv; + if (_useSwing) { + final JMenuItem m = new JMenuItem(message); + rv = new MenuInternal(null, m, callback, id); + m.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + rv.cb.clicked(rv); + return null; + } + }.execute(); + } + }); + _jPopupMenu.add(m); + } else { + final MenuItem m = new MenuItem(message); + rv = new MenuInternal(m, null, callback, id); + m.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + new SwingWorker<Object, Object>() { + @Override + protected Object doInBackground() throws Exception { + rv.cb.clicked(rv); + return null; + } + }.execute(); + } + }); + trayIcon.getPopupMenu().add(m); + } + synchronized(_menus) { + _menus.add(rv); + } + updateMenu(); + return rv; + } + + /** + * @since 0.9.59 + */ + public void removeMenu(MenuHandle item) { + MenuInternal mi = (MenuInternal) item; + if (_useSwing) { + _jPopupMenu.remove(mi.jm); + } else { + trayIcon.getPopupMenu().remove(mi.m); + } + updateMenu(); + } + + /** + * @since 0.9.59 + */ + public void showMenu(MenuHandle item) { + MenuInternal mi = (MenuInternal) item; + mi.setVisible(true); + updateMenu(); + } + + /** + * @since 0.9.59 + */ + public void hideMenu(MenuHandle item) { + MenuInternal mi = (MenuInternal) item; + mi.setVisible(false); + updateMenu(); + } + + /** + * @since 0.9.59 + */ + public void enableMenu(MenuHandle item) { + MenuInternal mi = (MenuInternal) item; + mi.setEnabled(true); + updateMenu(); + } + + /** + * @since 0.9.59 + */ + public void disableMenu(MenuHandle item) { + MenuInternal mi = (MenuInternal) item; + mi.setEnabled(false); + updateMenu(); + } + + /** + * @since 0.9.59 + */ + public void updateMenu(String message, MenuHandle item) { + MenuInternal mi = (MenuInternal) item; + mi.setText(message); + updateMenu(); + } + + /////// MenuService internals + + /** + * @since 0.9.59 + */ + private MenuInternal getMenu(int id) { + synchronized(_menus) { + for (MenuInternal mi : _menus) { + if (mi.getID() == id) + return mi; + } + } + return null; + } + + /** + * @since 0.9.59 + */ + private static class MenuInternal implements MenuHandle { + private final MenuItem m; + private final JMenuItem jm; + private final MenuCallback cb; + private final int id; + + public MenuInternal(MenuItem mm, JMenuItem jmm, MenuCallback cbb, int idd) { + m = mm; jm = jmm; cb = cbb; id = idd; + } + + public int getID() { return id; } + + private void setEnabled(boolean yes) { + if (m != null) + m.setEnabled(yes); + else + jm.setEnabled(yes); + } + + private void setVisible(boolean yes) { + if (m != null) + m.setEnabled(yes); + else + jm.setVisible(yes); + } + + private void setText(String text) { + if (m != null) + m.setLabel(text); + else + jm.setText(text); + } + } + protected String _t(String s) { return DesktopguiTranslator._t(_appContext, s); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java b/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java index 4cce5e8fddba34bbde323887b958aded386cd619..180081613853de006029500838b47143b881416b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java +++ b/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java @@ -8,6 +8,8 @@ import java.util.Properties; import org.eclipse.jetty.util.log.Log; import net.i2p.I2PAppContext; +import net.i2p.app.MenuCallback; +import net.i2p.app.MenuHandle; import net.i2p.apps.systray.UrlLauncher; import net.i2p.data.DataHelper; import net.i2p.desktopgui.ExternalMain; @@ -133,9 +135,22 @@ public class RunStandalone { System.setProperty("java.awt.headless", "false"); ExternalMain dtg = new ExternalMain(_context, _context.clientAppManager(), null); dtg.startup(); + try { + Thread.sleep(1000); + } catch (InterruptedException ie) {} + Callback cb = new Callback(); + MenuHandle mh = dtg.addMenu("i2psnark is running", cb); + if (mh == null) + System.out.println("addMenu failed!"); } } catch (Throwable t) { t.printStackTrace(); } } + + private static class Callback implements MenuCallback { + public void clicked(MenuHandle handle) { + System.out.println("Clicked! " + handle.getID()); + } + } } diff --git a/core/java/src/net/i2p/app/MenuCallback.java b/core/java/src/net/i2p/app/MenuCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..a88fcd455b721e255e1a418c16f92f4ec2dee34a --- /dev/null +++ b/core/java/src/net/i2p/app/MenuCallback.java @@ -0,0 +1,16 @@ +package net.i2p.app; + +/** + * The callback when a user clicks a MenuHandle. + * + * @since 0.9.59 + */ +public interface MenuCallback { + + /** + * Called when the user clicks the menu + * + * @param menu the menu handle clicked + */ + public void clicked(MenuHandle menu); +} diff --git a/core/java/src/net/i2p/app/MenuHandle.java b/core/java/src/net/i2p/app/MenuHandle.java new file mode 100644 index 0000000000000000000000000000000000000000..b4a93df2123fd5650310a4fd79ff0a2cf6bccc01 --- /dev/null +++ b/core/java/src/net/i2p/app/MenuHandle.java @@ -0,0 +1,15 @@ +package net.i2p.app; + +/** + * An opaque handle for the menu, returned from MenuService.addMenuHandle() + * + * @since 0.9.59 + */ +public interface MenuHandle { + + /** + * @return a unique identifier for this MenuHandle + */ + public int getID(); + +} diff --git a/core/java/src/net/i2p/app/MenuService.java b/core/java/src/net/i2p/app/MenuService.java new file mode 100644 index 0000000000000000000000000000000000000000..02ccebe038bd52bbc65d6598d1c4dcfe34b0ede5 --- /dev/null +++ b/core/java/src/net/i2p/app/MenuService.java @@ -0,0 +1,55 @@ +package net.i2p.app; + +/** + * A service to provide a menu to users. + * This service is currently provided by desktopgui (when supported and enabled). + * Other applications may support this interface in the future. + * + * This API is independent of any particular UI framework, e.g. AWT or Swing. + * + * Example usage: + * + * <pre> + * ClientAppManager cmgr = _context.clientAppManager(); + * if (cmgr != null) { + * MenuService ms = (MenuService) cmgr.getRegisteredApp("desktopgui"); + * if (ms != null) + * ms.addMenuHandle(_t("foo"), new Callback()); + * } + * </pre> + * + * @since 0.9.59 + */ +public interface MenuService { + + /** + * Menu will start out shown and enabled, in the root menu + * + * @param message for the menu, translated + * @param callback fired on click + * @return null on error + */ + public MenuHandle addMenu(String message, MenuCallback callback); + + /** + * Menu will start out enabled, as a submenu + * + * @param message for the menu, translated + * @param callback fired on click + * @param parent the parent menu this will be a submenu of, or null for top level + * @return null on error + */ + public MenuHandle addMenu(String message, MenuCallback callback, MenuHandle parent); + + public void removeMenu(MenuHandle item); + + public void showMenu(MenuHandle item); + + public void hideMenu(MenuHandle item); + + public void enableMenu(MenuHandle item); + + public void disableMenu(MenuHandle item); + + public void updateMenu(String message, MenuHandle item); +}