From bf3cb8de118678d1aabedcc7d778b88233c33708 Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 29 Sep 2016 01:44:11 +0200 Subject: [PATCH] Refactored out Menu object (from the SystemTray object). Converted SystemTray into a crossPlatform proxy. WIP submenus --- src/dorkbox/systemTray/Menu.java | 55 ++++- src/dorkbox/systemTray/MenuEntry.java | 6 + src/dorkbox/systemTray/SystemTray.java | 223 ++++++++++++++++-- .../systemTray/SystemTrayMenuAction.java | 5 +- .../systemTray/linux/AppIndicatorTray.java | 8 +- src/dorkbox/systemTray/linux/GtkMenu.java | 201 ++++++++++++++++ .../systemTray/linux/GtkMenuEntry.java | 28 ++- .../systemTray/linux/GtkMenuEntryItem.java | 31 +-- .../systemTray/linux/GtkMenuEntrySpacer.java | 4 +- .../systemTray/linux/GtkMenuEntryStatus.java | 8 +- .../systemTray/linux/GtkSystemTray.java | 9 +- .../systemTray/linux/GtkTypeSystemTray.java | 179 +------------- src/dorkbox/systemTray/linux/jna/Gtk.java | 11 +- .../systemTray/swing/AdjustedJMenuItem.java | 32 +++ src/dorkbox/systemTray/swing/SwingMenu.java | 105 +++++++++ .../systemTray/swing/SwingMenuEntry.java | 24 +- .../systemTray/swing/SwingMenuEntryItem.java | 58 ++--- .../swing/SwingMenuEntrySpacer.java | 5 +- .../swing/SwingMenuEntryStatus.java | 12 +- .../systemTray/swing/SwingSystemTray.java | 98 +------- src/dorkbox/systemTray/util/ImageUtils.java | 3 + test/dorkbox/TestTray.java | 10 +- test/dorkbox/TestTrayJavaFX.java | 9 +- test/dorkbox/TestTraySwt.java | 9 +- 24 files changed, 735 insertions(+), 398 deletions(-) create mode 100644 src/dorkbox/systemTray/linux/GtkMenu.java create mode 100644 src/dorkbox/systemTray/swing/AdjustedJMenuItem.java create mode 100644 src/dorkbox/systemTray/swing/SwingMenu.java diff --git a/src/dorkbox/systemTray/Menu.java b/src/dorkbox/systemTray/Menu.java index 115d025..11d18ca 100644 --- a/src/dorkbox/systemTray/Menu.java +++ b/src/dorkbox/systemTray/Menu.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import dorkbox.systemTray.util.ImageUtils; @@ -34,8 +35,40 @@ import dorkbox.systemTray.util.ImageUtils; @SuppressWarnings({"WeakerAccess", "unused"}) public class Menu { + public static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); + private final int id = Menu.MENU_ID_COUNTER.getAndIncrement(); + protected final java.util.List menuEntries = new ArrayList(); + private final SystemTray systemTray; + private final Menu parent; + + /** + * @param systemTray the system tray (which is the object that sits in the system tray) + * @param parent the parent of this menu, null if the parent is the system tray + */ + public + Menu(final SystemTray systemTray, final Menu parent) { + this.systemTray = systemTray; + this.parent = parent; + } + + /** + * @return the parent menu (of this menu) or null if we are the root menu + */ + public + Menu getParent() { + return parent; + } + + /** + * @return the system tray that this menu is ultimately attached to + */ + public + SystemTray getSystemTray() { + return systemTray; + } + /** * Adds a spacer to the dropdown menu. When menu entries are removed, any menu spacer that ends up at the top/bottom of the drop-down * menu, will also be removed. For example: @@ -71,7 +104,7 @@ class Menu { * * @param menuText the menu entry text to use to find the menu entry. The first result found is returned */ - public final + public MenuEntry getMenuEntry(final String menuText) { if (menuText == null || menuText.isEmpty()) { return null; @@ -95,7 +128,7 @@ class Menu { /** * Gets the first menu entry, ignoring status and spacers */ - public final + public MenuEntry getFirstMenuEntry() { return getMenuEntry(0); } @@ -103,7 +136,7 @@ class Menu { /** * Gets the last menu entry, ignoring status and spacers */ - public final + public MenuEntry getLastMenuEntry() { // Must be wrapped in a synchronized block for object visibility synchronized (menuEntries) { @@ -127,7 +160,7 @@ class Menu { * * @param menuIndex the menu entry index to use to retrieve the menu entry. */ - public final + public MenuEntry getMenuEntry(final int menuIndex) { if (menuIndex < 0) { return null; @@ -162,7 +195,7 @@ class Menu { * @param menuText string of the text you want to appear * @param callback callback that will be executed when this menu entry is clicked */ - public final + public void addMenuEntry(String menuText, SystemTrayMenuAction callback) { addMenuEntry(menuText, (String) null, callback); } @@ -174,7 +207,7 @@ class Menu { * @param imagePath the image (full path required) to use. If null, no image will be used * @param callback callback that will be executed when this menu entry is clicked */ - public final + public void addMenuEntry(String menuText, String imagePath, SystemTrayMenuAction callback) { if (imagePath == null) { addMenuEntry_(menuText, null, callback); @@ -191,7 +224,7 @@ class Menu { * @param imageUrl the URL of the image to use. If null, no image will be used * @param callback callback that will be executed when this menu entry is clicked */ - public final + public void addMenuEntry(String menuText, URL imageUrl, SystemTrayMenuAction callback) { if (imageUrl == null) { addMenuEntry_(menuText, null, callback); @@ -209,7 +242,7 @@ class Menu { * @param imageStream the InputStream of the image to use. If null, no image will be used * @param callback callback that will be executed when this menu entry is clicked */ - public final + public void addMenuEntry(String menuText, String cacheName, InputStream imageStream, SystemTrayMenuAction callback) { if (imageStream == null) { addMenuEntry_(menuText, null, callback); @@ -226,7 +259,7 @@ class Menu { * @param imageStream the InputStream of the image to use. If null, no image will be used * @param callback callback that will be executed when this menu entry is clicked */ - public final + public void addMenuEntry(String menuText, InputStream imageStream, SystemTrayMenuAction callback) { if (imageStream == null) { addMenuEntry_(menuText, null, callback); @@ -241,7 +274,7 @@ class Menu { * * @param menuEntry This is the menu entry to remove */ - public final + public void removeMenuEntry(final MenuEntry menuEntry) { if (menuEntry == null) { throw new NullPointerException("No menu entry exists for menuEntry"); @@ -311,7 +344,7 @@ class Menu { * * @param menuText This is the label for the menu entry to remove */ - public final + public void removeMenuEntry(final String menuText) { // have to wait for the value final CountDownLatch countDownLatch = new CountDownLatch(1); diff --git a/src/dorkbox/systemTray/MenuEntry.java b/src/dorkbox/systemTray/MenuEntry.java index 712533e..496122a 100644 --- a/src/dorkbox/systemTray/MenuEntry.java +++ b/src/dorkbox/systemTray/MenuEntry.java @@ -25,6 +25,12 @@ import java.net.URL; */ public interface MenuEntry { + + /** + * @return the menu that contains this menu entry + */ + Menu getParent(); + /** * @return the text label that the menu entry has assigned */ diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index a71d61f..c6d02c0 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -49,7 +49,7 @@ import dorkbox.util.process.ShellProcessBuilder; * Factory and base-class for system tray implementations. */ @SuppressWarnings({"unused", "Duplicates", "DanglingJavadoc", "WeakerAccess"}) -public abstract +public class SystemTray extends Menu { public static final Logger logger = LoggerFactory.getLogger(SystemTray.class); @@ -121,6 +121,7 @@ class SystemTray extends Menu { private static volatile SystemTray systemTray = null; + private static volatile Menu systemTrayMenu = null; public final static boolean isJavaFxLoaded; public final static boolean isSwtLoaded; @@ -157,13 +158,15 @@ class SystemTray extends Menu { return; } + systemTray = new SystemTray(); + // no tray in a headless environment if (GraphicsEnvironment.isHeadless()) { logger.error("Cannot use the SystemTray in a headless environment"); throw new HeadlessException(); } - Class trayType = null; + Class trayType = null; boolean isKDE = false; @@ -577,10 +580,10 @@ class SystemTray extends Menu { if (trayType == null) { // unsupported tray logger.error("Unable to discover what tray implementation to use!"); - systemTray = null; + systemTrayMenu = null; } else { - SystemTray systemTray_ = null; + Menu menu_ = null; /* * appIndicator/gtk require strings (which is the path) @@ -612,14 +615,14 @@ class SystemTray extends Menu { } } - systemTray_ = (SystemTray) trayType.getConstructors()[0].newInstance(); + menu_ = (Menu) trayType.getConstructors()[0].newInstance(systemTray); logger.info("Successfully Loaded: {}", trayType.getSimpleName()); } catch (Exception e) { logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e); } - systemTray = systemTray_; + systemTrayMenu = menu_; // These install a shutdown hook in JavaFX/SWT, so that when the main window is closed -- the system tray is ALSO closed. @@ -671,36 +674,82 @@ class SystemTray extends Menu { * be granted in order to get the {@code SystemTray} instance. Otherwise this will return null. */ public static - SystemTray getSystemTray() { + SystemTray get() { init(); return systemTray; } - protected + private SystemTray() { + super(null, null); } - public abstract - void shutdown(); + public + void shutdown() { + final Menu menu = systemTrayMenu; + + if (menu instanceof AppIndicatorTray) { + ((AppIndicatorTray) menu).shutdown(); + } + else if (menu instanceof GtkSystemTray) { + ((GtkSystemTray) menu).shutdown(); + } else { + // swing + ((SwingSystemTray) menu).shutdown(); + } + } /** * Gets the 'status' string assigned to the system tray */ - public abstract - String getStatus(); + public + String getStatus() { + final Menu menu = systemTrayMenu; + if (menu instanceof AppIndicatorTray) { + return ((AppIndicatorTray) menu).getStatus(); + } + else if (menu instanceof GtkSystemTray) { + return ((GtkSystemTray) menu).getStatus(); + } else { + // swing + return ((SwingSystemTray) menu).getStatus(); + } + } /** * Sets a 'status' string at the first position in the popup menu. This 'status' string appears as a disabled menu entry. * * @param statusText the text you want displayed, null if you want to remove the 'status' string */ - public abstract - void setStatus(String statusText); + public + void setStatus(String statusText) { + final Menu menu = systemTrayMenu; + if (menu instanceof AppIndicatorTray) { + ((AppIndicatorTray) menu).setStatus(statusText); + } + else if (menu instanceof GtkSystemTray) { + ((GtkSystemTray) menu).setStatus(statusText); + } else { + // swing + ((SwingSystemTray) menu).setStatus(statusText); + } + } - protected abstract - void setIcon_(File iconPath); + protected + void setIcon_(File iconPath) { + final Menu menu = systemTrayMenu; + if (menu instanceof AppIndicatorTray) { + ((AppIndicatorTray) menu).setIcon_(iconPath); + } + else if (menu instanceof GtkSystemTray) { + ((GtkSystemTray) menu).setIcon_(iconPath); + } else { + // swing + ((SwingSystemTray) menu).setIcon_(iconPath); + } + } /** * Changes the tray icon used. @@ -754,5 +803,147 @@ class SystemTray extends Menu { void setIcon(InputStream imageStream) { setIcon_(ImageUtils.resizeAndCache(ImageUtils.TRAY_SIZE, imageStream)); } + + /** + * @return the parent menu (of this menu) or null if we are the root menu + */ + public + Menu getParent() { + return null; + } + + /** + * Adds a spacer to the dropdown menu. When menu entries are removed, any menu spacer that ends up at the top/bottom of the drop-down + * menu, will also be removed. For example: + * + * Original Entry3 deleted Result + * + * + * Entry1 Entry1 Entry1 + * Entry2 -> Entry2 -> Entry2 + * (deleted) + * Entry3 (deleted) + */ + public + void addMenuSpacer() { + systemTrayMenu.addMenuSpacer(); + } + + /** + * Gets the menu entry for a specified text + * + * @param menuText the menu entry text to use to find the menu entry. The first result found is returned + */ + public final + MenuEntry getMenuEntry(final String menuText) { + return systemTrayMenu.getMenuEntry(menuText); + } + + /** + * Gets the first menu entry, ignoring status and spacers + */ + public final + MenuEntry getFirstMenuEntry() { + return systemTrayMenu.getFirstMenuEntry(); + } + + /** + * Gets the last menu entry, ignoring status and spacers + */ + public final + MenuEntry getLastMenuEntry() { + return systemTrayMenu.getLastMenuEntry(); + } + + /** + * Gets the menu entry for a specified index (zero-index), ignoring status and spacers + * + * @param menuIndex the menu entry index to use to retrieve the menu entry. + */ + public final + MenuEntry getMenuEntry(final int menuIndex) { + return systemTrayMenu.getMenuEntry(menuIndex); + } + + /** + * Adds a menu entry to the tray icon with text (no image) + * + * @param menuText string of the text you want to appear + * @param callback callback that will be executed when this menu entry is clicked + */ + public final + void addMenuEntry(String menuText, SystemTrayMenuAction callback) { + addMenuEntry(menuText, (String) null, callback); + } + + /** + * Adds a menu entry to the tray icon with text + image + * + * @param menuText string of the text you want to appear + * @param imagePath the image (full path required) to use. If null, no image will be used + * @param callback callback that will be executed when this menu entry is clicked + */ + public final + void addMenuEntry(String menuText, String imagePath, SystemTrayMenuAction callback) { + systemTrayMenu.addMenuEntry(menuText, imagePath, callback); + } + + /** + * Adds a menu entry to the tray icon with text + image + * + * @param menuText string of the text you want to appear + * @param imageUrl the URL of the image to use. If null, no image will be used + * @param callback callback that will be executed when this menu entry is clicked + */ + public final + void addMenuEntry(String menuText, URL imageUrl, SystemTrayMenuAction callback) { + systemTrayMenu.addMenuEntry(menuText, imageUrl, callback); + } + + /** + * Adds a menu entry to the tray icon with text + image + * + * @param menuText string of the text you want to appear + * @param cacheName @param cacheName the name to use for lookup in the cache for the imageStream + * @param imageStream the InputStream of the image to use. If null, no image will be used + * @param callback callback that will be executed when this menu entry is clicked + */ + public + void addMenuEntry(String menuText, String cacheName, InputStream imageStream, SystemTrayMenuAction callback) { + systemTrayMenu.addMenuEntry(menuText, cacheName, imageStream, callback); + } + + /** + * Adds a menu entry to the tray icon with text + image + * + * @param menuText string of the text you want to appear + * @param imageStream the InputStream of the image to use. If null, no image will be used + * @param callback callback that will be executed when this menu entry is clicked + */ + public final + void addMenuEntry(String menuText, InputStream imageStream, SystemTrayMenuAction callback) { + systemTrayMenu.addMenuEntry(menuText, imageStream, callback); + } + + /** + * This removes a menu entry from the dropdown menu. + * + * @param menuEntry This is the menu entry to remove + */ + public final + void removeMenuEntry(final MenuEntry menuEntry) { + systemTrayMenu.removeMenuEntry(menuEntry); + } + + + /** + * This removes a menu entry (via the text label) from the dropdown menu. + * + * @param menuText This is the label for the menu entry to remove + */ + public final + void removeMenuEntry(final String menuText) { + systemTrayMenu.removeMenuEntry(menuText); + } } diff --git a/src/dorkbox/systemTray/SystemTrayMenuAction.java b/src/dorkbox/systemTray/SystemTrayMenuAction.java index e5c6730..4001524 100644 --- a/src/dorkbox/systemTray/SystemTrayMenuAction.java +++ b/src/dorkbox/systemTray/SystemTrayMenuAction.java @@ -19,10 +19,11 @@ public interface SystemTrayMenuAction { /** * This method will ALWAYS be called in the correct context, either in the swing EDT (if it's swing based), or in a separate thread - * (GTK/AppIndicator based). + * (GtkStatusIcon/AppIndicator based). * * @param systemTray this is the parent, system tray object + * @param parentMenu this is the parent menu of this menu entry * @param menuEntry this is the menu entry that was clicked */ - void onClick(SystemTray systemTray, final MenuEntry menuEntry); + void onClick(SystemTray systemTray, Menu parentMenu, final MenuEntry menuEntry); } diff --git a/src/dorkbox/systemTray/linux/AppIndicatorTray.java b/src/dorkbox/systemTray/linux/AppIndicatorTray.java index 8af94c9..fd4363f 100644 --- a/src/dorkbox/systemTray/linux/AppIndicatorTray.java +++ b/src/dorkbox/systemTray/linux/AppIndicatorTray.java @@ -58,8 +58,9 @@ class AppIndicatorTray extends GtkTypeSystemTray { private AtomicBoolean shuttingDown = new AtomicBoolean(); public - AppIndicatorTray() { - super(); + AppIndicatorTray(final SystemTray systemTray) { + super(systemTray); + if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) { // if we force GTK type system tray, don't attempt to load AppIndicator libs throw new IllegalArgumentException("Unable to start AppIndicator if 'SystemTray.FORCE_TRAY_TYPE' is set to GtkStatusIcon"); @@ -103,8 +104,7 @@ class AppIndicatorTray extends GtkTypeSystemTray { } } - @Override - protected + public void setIcon_(final File iconFile) { dispatch(new Runnable() { @Override diff --git a/src/dorkbox/systemTray/linux/GtkMenu.java b/src/dorkbox/systemTray/linux/GtkMenu.java new file mode 100644 index 0000000..de714df --- /dev/null +++ b/src/dorkbox/systemTray/linux/GtkMenu.java @@ -0,0 +1,201 @@ +/* + * Copyright 2014 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.systemTray.linux; + +import java.io.File; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.MenuEntry; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.SystemTrayMenuAction; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; + +class GtkMenu extends Menu { + volatile Pointer _native; + + // called on dispatch + GtkMenu(SystemTray systemTray, GtkMenu parentMenu) { + super(systemTray, parentMenu); +// this._native = Gtk.gtk_menu_new(); + } + + + /** + * Necessary to guarantee all updates occur on the dispatch thread + */ + @Override + protected + void dispatch(final Runnable runnable) { + Gtk.dispatch(runnable); + } + + public + void shutdown() { + dispatch(new Runnable() { + @Override + public + void run() { + obliterateMenu(); + + Gtk.shutdownGui(); + } + }); + } + + @Override + public + void addMenuSpacer() { + dispatch(new Runnable() { + @Override + public + void run() { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + synchronized (menuEntries) { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + deleteMenu(); + + GtkMenuEntry menuEntry = new GtkMenuEntrySpacer(GtkMenu.this); + menuEntries.add(menuEntry); + + createMenu(); + } + } + }); + } + + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + /** + * Deletes the menu, and unreferences everything in it. ALSO recreates ONLY the menu object. + */ + void deleteMenu() { + if (_native != null) { + // have to remove all other menu entries + synchronized (menuEntries) { + for (int i = 0; i < menuEntries.size(); i++) { + GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); + + Gobject.g_object_force_floating(menuEntry__._native); + Gtk.gtk_container_remove(_native, menuEntry__._native); + } + + Gtk.gtk_widget_destroy(_native); + } + } + + // makes a new one + _native = Gtk.gtk_menu_new(); + } + + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + void createMenu() { + boolean hasImages = false; + + // now add back other menu entries + synchronized (menuEntries) { + for (int i = 0; i < menuEntries.size(); i++) { + MenuEntry menuEntry__ = menuEntries.get(i); + hasImages |= menuEntry__.hasImage(); + } + + + for (int i = 0; i < menuEntries.size(); i++) { + GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); + // the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images + menuEntry__.setSpacerImage(hasImages); + + // will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu' + Gtk.gtk_menu_shell_append(this._native, menuEntry__._native); + Gobject.g_object_ref_sink(menuEntry__._native); // undoes "floating" + } + + onMenuAdded(_native); + Gtk.gtk_widget_show_all(_native); + } + } + + /** + * Called inside the gdk_threads block + */ + void onMenuAdded(final Pointer menu) { + // only needed for AppIndicator + } + + /** + * Completely obliterates the menu, no possible way to reconstruct it. + */ + private + void obliterateMenu() { + if (_native != null) { + // have to remove all other menu entries + synchronized (menuEntries) { + for (int i = 0; i < menuEntries.size(); i++) { + GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); + menuEntry__.removePrivate(); + } + menuEntries.clear(); + + Gtk.gtk_widget_destroy(_native); + } + } + } + + + /** + * Will add a new menu entry, or update one if it already exists + */ + protected + void addMenuEntry_(final String menuText, final File imagePath, final SystemTrayMenuAction callback) { + // some implementations of appindicator, do NOT like having a menu added, which has no menu items yet. + // see: https://bugs.launchpad.net/glipper/+bug/1203888 + + if (menuText == null) { + throw new NullPointerException("Menu text cannot be null"); + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + MenuEntry menuEntry = getMenuEntry(menuText); + if (menuEntry == null) { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + deleteMenu(); + + menuEntry = new GtkMenuEntryItem(GtkMenu.this, menuText, imagePath, callback); + menuEntries.add(menuEntry); + + if (menuText.equals("AAAAAAAA")) { +// GtkMenu subMenu = new GtkMenu(); +// subMenu.addMenuEntry("asdasdasd", null, null, null); +// Gtk.gtk_menu_item_set_submenu(((GtkMenuEntryItem) menuEntry).nativeMenuItem, subMenu.nativeMenu); + } + + createMenu(); + } + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntry.java b/src/dorkbox/systemTray/linux/GtkMenuEntry.java index 90dbb9d..4b7b0c2 100644 --- a/src/dorkbox/systemTray/linux/GtkMenuEntry.java +++ b/src/dorkbox/systemTray/linux/GtkMenuEntry.java @@ -21,16 +21,17 @@ import java.net.URL; import com.sun.jna.Pointer; +import dorkbox.systemTray.Menu; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.linux.jna.Gtk; import dorkbox.systemTray.util.ImageUtils; abstract class GtkMenuEntry implements MenuEntry { - private final int id = GtkTypeSystemTray.MENU_ID_COUNTER.getAndIncrement(); + private final int id = Menu.MENU_ID_COUNTER.getAndIncrement(); - final Pointer menuItem; - final GtkTypeSystemTray systemTray; + private final GtkMenu parentMenu; + final Pointer _native; // this have to be volatile, because they can be changed from any thread private volatile String text; @@ -39,9 +40,14 @@ class GtkMenuEntry implements MenuEntry { * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref */ - GtkMenuEntry(Pointer menuItem, final GtkTypeSystemTray systemTray) { - this.systemTray = systemTray; - this.menuItem = menuItem; + GtkMenuEntry(final GtkMenu parentMenu, final Pointer menuItem) { + this.parentMenu = parentMenu; + this._native = menuItem; + } + + public + Menu getParent() { + return parentMenu; } /** @@ -142,16 +148,16 @@ class GtkMenuEntry implements MenuEntry { */ public final void remove() { - Gtk.gtk_container_remove(systemTray.getMenu(), menuItem); - Gtk.gtk_menu_shell_deactivate(systemTray.getMenu(), menuItem); + Gtk.gtk_container_remove(parentMenu._native, _native); + Gtk.gtk_menu_shell_deactivate(parentMenu._native, _native); removePrivate(); - Gtk.gtk_widget_destroy(menuItem); + Gtk.gtk_widget_destroy(_native); // have to rebuild the menu now... - systemTray.deleteMenu(); - systemTray.createMenu(); + parentMenu.deleteMenu(); + parentMenu.createMenu(); } // called when this item is removed. Necessary to cleanup/remove itself diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntryItem.java b/src/dorkbox/systemTray/linux/GtkMenuEntryItem.java index 3eae259..530e8c7 100644 --- a/src/dorkbox/systemTray/linux/GtkMenuEntryItem.java +++ b/src/dorkbox/systemTray/linux/GtkMenuEntryItem.java @@ -28,6 +28,7 @@ import dorkbox.systemTray.util.ImageUtils; class GtkMenuEntryItem extends GtkMenuEntry implements GCallback { private static File transparentIcon = null; + @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final NativeLong nativeLong; @@ -42,8 +43,8 @@ class GtkMenuEntryItem extends GtkMenuEntry implements GCallback { * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref */ - GtkMenuEntryItem(final String label, final File image, final SystemTrayMenuAction callback, final GtkTypeSystemTray systemTray) { - super(Gtk.gtk_image_menu_item_new_with_label(""), systemTray); + GtkMenuEntryItem(final GtkMenu parentMenu, final String label, final File image, final SystemTrayMenuAction callback) { + super(parentMenu, Gtk.gtk_image_menu_item_new_with_label("")); this.callback = callback; setText(label); @@ -54,9 +55,11 @@ class GtkMenuEntryItem extends GtkMenuEntry implements GCallback { setImage_(image); if (callback != null) { - nativeLong = Gobject.g_signal_connect_object(menuItem, "activate", this, null, 0); + Gtk.gtk_widget_set_sensitive(_native, Gtk.TRUE); + nativeLong = Gobject.g_signal_connect_object(_native, "activate", this, null, 0); } else { + Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE); nativeLong = null; } } @@ -73,7 +76,7 @@ class GtkMenuEntryItem extends GtkMenuEntry implements GCallback { int callback(final Pointer instance, final Pointer data) { final SystemTrayMenuAction cb = this.callback; if (cb != null) { - Gtk.proxyClick(cb, systemTray, GtkMenuEntryItem.this); + Gtk.proxyClick(getParent(), GtkMenuEntryItem.this, cb); } return Gtk.TRUE; @@ -100,26 +103,26 @@ class GtkMenuEntryItem extends GtkMenuEntry implements GCallback { if (image != null) { Gtk.gtk_widget_destroy(image); image = null; - Gtk.gtk_widget_show_all(menuItem); + Gtk.gtk_widget_show_all(_native); } if (everyoneElseHasImages) { image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath()); - Gtk.gtk_image_menu_item_set_image(menuItem, image); + Gtk.gtk_image_menu_item_set_image(_native, image); // must always re-set always-show after setting the image - Gtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); + Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE); } - Gtk.gtk_widget_show_all(menuItem); + Gtk.gtk_widget_show_all(_native); } /** * must always be called in the GTK thread */ void renderText(final String text) { - Gtk.gtk_menu_item_set_label(menuItem, text); - Gtk.gtk_widget_show_all(menuItem); + Gtk.gtk_menu_item_set_label(_native, text); + Gtk.gtk_widget_show_all(_native); } // NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted. @@ -135,18 +138,18 @@ class GtkMenuEntryItem extends GtkMenuEntry implements GCallback { if (image != null) { Gtk.gtk_widget_destroy(image); image = null; - Gtk.gtk_widget_show_all(menuItem); + Gtk.gtk_widget_show_all(_native); } if (imageFile != null) { image = Gtk.gtk_image_new_from_file(imageFile.getAbsolutePath()); - Gtk.gtk_image_menu_item_set_image(menuItem, image); + Gtk.gtk_image_menu_item_set_image(_native, image); // must always re-set always-show after setting the image - Gtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); + Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE); } - Gtk.gtk_widget_show_all(menuItem); + Gtk.gtk_widget_show_all(_native); } }); } diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntrySpacer.java b/src/dorkbox/systemTray/linux/GtkMenuEntrySpacer.java index 6fd0e0c..a328708 100644 --- a/src/dorkbox/systemTray/linux/GtkMenuEntrySpacer.java +++ b/src/dorkbox/systemTray/linux/GtkMenuEntrySpacer.java @@ -27,8 +27,8 @@ class GtkMenuEntrySpacer extends GtkMenuEntry implements MenuSpacer { * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref */ - GtkMenuEntrySpacer(final GtkTypeSystemTray parent) { - super(Gtk.gtk_separator_menu_item_new(), parent); + GtkMenuEntrySpacer(final GtkMenu parent) { + super(parent, Gtk.gtk_separator_menu_item_new()); } @Override diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntryStatus.java b/src/dorkbox/systemTray/linux/GtkMenuEntryStatus.java index 0da3b79..a8c23a8 100644 --- a/src/dorkbox/systemTray/linux/GtkMenuEntryStatus.java +++ b/src/dorkbox/systemTray/linux/GtkMenuEntryStatus.java @@ -29,8 +29,8 @@ class GtkMenuEntryStatus extends GtkMenuEntryItem { * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref */ - GtkMenuEntryStatus(final String label, final GtkTypeSystemTray parent) { - super(label, null, null, parent); + GtkMenuEntryStatus(final GtkMenu parentMenu, final String label) { + super(parentMenu, label, null, null); } // called in the GTK thread @@ -39,13 +39,13 @@ class GtkMenuEntryStatus extends GtkMenuEntryItem { // evil hacks abound... // https://developer.gnome.org/pango/stable/PangoMarkupFormat.html - Pointer label = Gtk.gtk_bin_get_child(menuItem); + Pointer label = Gtk.gtk_bin_get_child(_native); Gtk.gtk_label_set_use_markup(label, Gtk.TRUE); Pointer markup = Gobject.g_markup_printf_escaped("%s", text); Gtk.gtk_label_set_markup(label, markup); Gobject.g_free(markup); - Gtk.gtk_widget_set_sensitive(menuItem, Gtk.FALSE); + Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE); } @Override diff --git a/src/dorkbox/systemTray/linux/GtkSystemTray.java b/src/dorkbox/systemTray/linux/GtkSystemTray.java index 3b3dee5..2693521 100644 --- a/src/dorkbox/systemTray/linux/GtkSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkSystemTray.java @@ -51,8 +51,8 @@ class GtkSystemTray extends GtkTypeSystemTray { private volatile boolean isActive = false; public - GtkSystemTray() { - super(); + GtkSystemTray(final SystemTray systemTray) { + super(systemTray); if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_APP_INDICATOR) { // if we force GTK type system tray, don't attempt to load AppIndicator libs throw new IllegalArgumentException("Unable to start GtkStatusIcon if 'SystemTray.FORCE_TRAY_TYPE' is set to AppIndicator"); @@ -74,7 +74,7 @@ class GtkSystemTray extends GtkTypeSystemTray { void callback(Pointer notUsed, final GdkEventButton event) { // BUTTON_PRESS only (any mouse click) if (event.type == 4) { - Gtk.gtk_menu_popup(getMenu(), null, null, Gtk.gtk_status_icon_position_menu, trayIcon, 0, event.time); + Gtk.gtk_menu_popup(_native, null, null, Gtk.gtk_status_icon_position_menu, trayIcon, 0, event.time); } } }; @@ -140,8 +140,7 @@ class GtkSystemTray extends GtkTypeSystemTray { } } - @Override - protected + public void setIcon_(final File iconFile) { dispatch(new Runnable() { @Override diff --git a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java index cc168c2..c6158e9 100644 --- a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java @@ -16,48 +16,20 @@ package dorkbox.systemTray.linux; -import java.io.File; -import java.util.concurrent.atomic.AtomicInteger; - -import com.sun.jna.Pointer; - import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.SystemTrayMenuAction; -import dorkbox.systemTray.linux.jna.Gobject; -import dorkbox.systemTray.linux.jna.Gtk; /** * Derived from * Lantern: https://github.com/getlantern/lantern/ Apache 2.0 License Copyright 2010 Brave New Software Project, Inc. */ -public abstract -class GtkTypeSystemTray extends SystemTray { - static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); +abstract +class GtkTypeSystemTray extends GtkMenu { - private volatile Pointer menu; - - @Override - protected - void dispatch(final Runnable runnable) { - Gtk.dispatch(runnable); + GtkTypeSystemTray(final SystemTray systemTray) { + super(systemTray, null); } - @Override - public - void shutdown() { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - obliterateMenu(); - - Gtk.shutdownGui(); - } - }); - } - - @Override public String getStatus() { synchronized (menuEntries) { @@ -70,10 +42,9 @@ class GtkTypeSystemTray extends SystemTray { return null; } - @Override public void setStatus(final String statusText) { - Gtk.dispatch(new Runnable() { + dispatch(new Runnable() { @Override public void run() { @@ -96,13 +67,13 @@ class GtkTypeSystemTray extends SystemTray { deleteMenu(); if (menuEntry == null) { - menuEntry = new GtkMenuEntryStatus(statusText, GtkTypeSystemTray.this); + menuEntry = new GtkMenuEntryStatus( GtkTypeSystemTray.this, statusText); // status is ALWAYS at 0 index... menuEntries.add(0, menuEntry); } else if (menuEntry instanceof GtkMenuEntryStatus) { // change the text? if (statusText != null) { - menuEntry = new GtkMenuEntryStatus(statusText, GtkTypeSystemTray.this); + menuEntry = new GtkMenuEntryStatus( GtkTypeSystemTray.this, statusText); menuEntries.add(0, menuEntry); } } @@ -112,140 +83,4 @@ class GtkTypeSystemTray extends SystemTray { } }); } - - @Override - public - void addMenuSpacer() { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. - // To work around this issue, we destroy then recreate the menu every time something is changed. - synchronized (menuEntries) { - // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. - // To work around this issue, we destroy then recreate the menu every time something is changed. - deleteMenu(); - - GtkMenuEntry menuEntry = new GtkMenuEntrySpacer(GtkTypeSystemTray.this); - menuEntries.add(menuEntry); - - createMenu(); - } - } - }); - } - - // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. - // To work around this issue, we destroy then recreate the menu every time something is changed. - /** - * Deletes the menu, and unreferences everything in it. ALSO recreates ONLY the menu object. - */ - void deleteMenu() { - if (menu != null) { - // have to remove all other menu entries - synchronized (menuEntries) { - for (int i = 0; i < menuEntries.size(); i++) { - GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); - - Gobject.g_object_force_floating(menuEntry__.menuItem); - Gtk.gtk_container_remove(menu, menuEntry__.menuItem); - } - - Gtk.gtk_widget_destroy(menu); - } - } - - // makes a new one - menu = Gtk.gtk_menu_new(); - } - - // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. - // To work around this issue, we destroy then recreate the menu every time something is changed. - void createMenu() { - boolean hasImages = false; - - // now add back other menu entries - synchronized (menuEntries) { - for (int i = 0; i < menuEntries.size(); i++) { - MenuEntry menuEntry__ = menuEntries.get(i); - hasImages |= menuEntry__.hasImage(); - } - - - for (int i = 0; i < menuEntries.size(); i++) { - GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); - // the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images - menuEntry__.setSpacerImage(hasImages); - - // will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu' - Gtk.gtk_menu_shell_append(this.menu, menuEntry__.menuItem); - Gobject.g_object_ref_sink(menuEntry__.menuItem); // undoes "floating" - } - - onMenuAdded(menu); - Gtk.gtk_widget_show_all(menu); - } - } - - /** - * Called inside the gdk_threads block - */ - void onMenuAdded(final Pointer menu) { - // only needed for AppIndicator - } - - /** - * Completely obliterates the menu, no possible way to reconstruct it. - */ - private - void obliterateMenu() { - if (menu != null) { - // have to remove all other menu entries - synchronized (menuEntries) { - for (int i = 0; i < menuEntries.size(); i++) { - GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); - menuEntry__.removePrivate(); - } - menuEntries.clear(); - - Gtk.gtk_widget_destroy(menu); - } - } - } - - protected - Pointer getMenu() { - return menu; - } - - protected - void addMenuEntry_(final String menuText, final File imagePath, final SystemTrayMenuAction callback) { - // some implementations of appindicator, do NOT like having a menu added, which has no menu items yet. - // see: https://bugs.launchpad.net/glipper/+bug/1203888 - - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - MenuEntry menuEntry = getMenuEntry(menuText); - if (menuEntry == null) { - // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. - // To work around this issue, we destroy then recreate the menu every time something is changed. - deleteMenu(); - - menuEntry = new GtkMenuEntryItem(menuText, imagePath, callback, GtkTypeSystemTray.this); - menuEntries.add(menuEntry); - - createMenu(); - } - } - } - }); - } } diff --git a/src/dorkbox/systemTray/linux/jna/Gtk.java b/src/dorkbox/systemTray/linux/jna/Gtk.java index 6c8c049..33725f4 100644 --- a/src/dorkbox/systemTray/linux/jna/Gtk.java +++ b/src/dorkbox/systemTray/linux/jna/Gtk.java @@ -24,10 +24,10 @@ import java.util.concurrent.TimeUnit; import com.sun.jna.Function; import com.sun.jna.Pointer; +import dorkbox.systemTray.Menu; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTrayMenuAction; -import dorkbox.systemTray.linux.GtkTypeSystemTray; import dorkbox.systemTray.util.JavaFX; import dorkbox.systemTray.util.Swt; @@ -383,11 +383,11 @@ class Gtk { * @param callback will never be null. */ public static - void proxyClick(final SystemTrayMenuAction callback, final GtkTypeSystemTray parent, final MenuEntry menuEntry) { + void proxyClick(final Menu parentMenu, final MenuEntry menuEntry, final SystemTrayMenuAction callback) { Gtk.isDispatch = true; try { - callback.onClick(parent, menuEntry); + callback.onClick(parentMenu.getSystemTray(), parentMenu, menuEntry); } catch (Throwable throwable) { SystemTray.logger.error("Error calling menu entry {} click event.", menuEntry.getText(), throwable); } @@ -419,11 +419,10 @@ class Gtk { private static native void gtk_main_quit(); - - public static native Pointer gtk_menu_new(); + public static native Pointer gtk_menu_item_set_submenu(Pointer menuEntry, Pointer menu); + - public static native Pointer gtk_menu_item_new_with_label(String label); public static native Pointer gtk_separator_menu_item_new(); diff --git a/src/dorkbox/systemTray/swing/AdjustedJMenuItem.java b/src/dorkbox/systemTray/swing/AdjustedJMenuItem.java new file mode 100644 index 0000000..b0cbed6 --- /dev/null +++ b/src/dorkbox/systemTray/swing/AdjustedJMenuItem.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.systemTray.swing; + +import java.awt.Insets; + +import javax.swing.JMenuItem; + +class AdjustedJMenuItem extends JMenuItem { + @Override + public + Insets getMargin() { + Insets margin = super.getMargin(); + if (margin != null) { + margin.set(2, -2, 2, 4); + } + return margin; + } +} diff --git a/src/dorkbox/systemTray/swing/SwingMenu.java b/src/dorkbox/systemTray/swing/SwingMenu.java new file mode 100644 index 0000000..c28e8c2 --- /dev/null +++ b/src/dorkbox/systemTray/swing/SwingMenu.java @@ -0,0 +1,105 @@ +/* + * Copyright 2014 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.systemTray.swing; + + +import java.io.File; + +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.MenuEntry; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.SystemTrayMenuAction; +import dorkbox.util.SwingUtil; + +public +class SwingMenu extends Menu { + + volatile SwingSystemTrayMenuPopup _native; + + /** + * @param systemTray + * the system tray (which is the object that sits in the system tray) + * @param parent + * the parent of this menu, null if the parent is the system tray + */ + public + SwingMenu(final SystemTray systemTray, final Menu parent) { + super(systemTray, parent); + + SwingUtil.invokeAndWait(new Runnable() { + @Override + public + void run() { + _native = new SwingSystemTrayMenuPopup(); + } + }); + } + + protected + void dispatch(final Runnable runnable) { + // this will properly check if we are running on the EDT + SwingUtil.invokeLater(runnable); + } + + @Override + public + void addMenuSpacer() { + dispatch(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + synchronized (menuEntries) { + MenuEntry menuEntry = new SwingMenuEntrySpacer(SwingMenu.this); + menuEntries.add(menuEntry); + } + } + } + }); + } + + /** + * Will add a new menu entry, or update one if it already exists + */ + protected + void addMenuEntry_(final String menuText, final File imagePath, final SystemTrayMenuAction callback) { + if (menuText == null) { + throw new NullPointerException("Menu text cannot be null"); + } + + dispatch(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + MenuEntry menuEntry = getMenuEntry(menuText); + + if (menuEntry != null) { + throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'"); + } + else { + // must always be called on the EDT + menuEntry = new SwingMenuEntryItem(SwingMenu.this, callback); + menuEntry.setText(menuText); + menuEntry.setImage(imagePath); + + menuEntries.add(menuEntry); + } + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntry.java b/src/dorkbox/systemTray/swing/SwingMenuEntry.java index 545a536..0dd10d3 100644 --- a/src/dorkbox/systemTray/swing/SwingMenuEntry.java +++ b/src/dorkbox/systemTray/swing/SwingMenuEntry.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package dorkbox.systemTray.swing; import java.io.File; @@ -22,26 +21,33 @@ import java.net.URL; import javax.swing.JComponent; +import dorkbox.systemTray.Menu; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.util.ImageUtils; import dorkbox.util.SwingUtil; abstract class SwingMenuEntry implements MenuEntry { - private final int id = SwingSystemTray.MENU_ID_COUNTER.getAndIncrement(); + private final int id = Menu.MENU_ID_COUNTER.getAndIncrement(); - final SwingSystemTray systemTray; - final JComponent menuItem; + private final SwingMenu parentMenu; + final JComponent _native; // this have to be volatile, because they can be changed from any thread private volatile String text; // this is ALWAYS called on the EDT. - SwingMenuEntry(JComponent menuItem, final SwingSystemTray systemTray) { - this.menuItem = menuItem; - this.systemTray = systemTray; + SwingMenuEntry(final SwingMenu parentMenu, final JComponent menuItem) { + this.parentMenu = parentMenu; + this._native = menuItem; - systemTray.getMenu().add(menuItem); + parentMenu._native.add(menuItem); + } + + @Override + public + Menu getParent() { + return parentMenu; } /** @@ -137,7 +143,7 @@ class SwingMenuEntry implements MenuEntry { public void run() { removePrivate(); - systemTray.getMenu().remove(menuItem); + parentMenu._native.remove(_native); } }); } diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntryItem.java b/src/dorkbox/systemTray/swing/SwingMenuEntryItem.java index d62c4f0..30a9111 100644 --- a/src/dorkbox/systemTray/swing/SwingMenuEntryItem.java +++ b/src/dorkbox/systemTray/swing/SwingMenuEntryItem.java @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package dorkbox.systemTray.swing; -import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; @@ -27,43 +25,35 @@ import javax.swing.JMenuItem; import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.util.SwingUtil; -public class SwingMenuEntryItem extends SwingMenuEntry { - private static - class AdjustedJMenuItem extends JMenuItem { - @Override - public - Insets getMargin() { - Insets margin = super.getMargin(); - if (margin != null) { - margin.set(2, -2, 2, 4); - } - return margin; - } - } - private final ActionListener swingCallback; private volatile boolean hasLegitIcon = false; private volatile SystemTrayMenuAction callback; - public // this is ALWAYS called on the EDT. - SwingMenuEntryItem(final SystemTrayMenuAction callback, final SwingSystemTray systemTray) { - super(new AdjustedJMenuItem(), systemTray); - + SwingMenuEntryItem(final SwingMenu parentMenu, final SystemTrayMenuAction callback) { + super(parentMenu, new AdjustedJMenuItem()); this.callback = callback; - swingCallback = new ActionListener() { - @Override - public - void actionPerformed(ActionEvent e) { - // we want it to run on the EDT - handle(); - } - }; - ((JMenuItem) menuItem).addActionListener(swingCallback); + + if (callback != null) { + _native.setEnabled(true); + swingCallback = new ActionListener() { + @Override + public + void actionPerformed(ActionEvent e) { + // we want it to run on the EDT + handle(); + } + }; + + ((JMenuItem) _native).addActionListener(swingCallback); + } else { + _native.setEnabled(false); + swingCallback = null; + } } @Override @@ -75,7 +65,7 @@ class SwingMenuEntryItem extends SwingMenuEntry { private void handle() { if (callback != null) { - callback.onClick(systemTray, this); + callback.onClick(getParent().getSystemTray(), getParent(), this); } } @@ -87,13 +77,13 @@ class SwingMenuEntryItem extends SwingMenuEntry { @Override void removePrivate() { - ((JMenuItem) menuItem).removeActionListener(swingCallback); + ((JMenuItem) _native).removeActionListener(swingCallback); } // always called in the EDT @Override void renderText(final String text) { - ((JMenuItem) menuItem).setText(text); + ((JMenuItem) _native).setText(text); } @Override @@ -106,10 +96,10 @@ class SwingMenuEntryItem extends SwingMenuEntry { void run() { if (imageFile != null) { ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath()); - ((JMenuItem) menuItem).setIcon(origIcon); + ((JMenuItem) _native).setIcon(origIcon); } else { - ((JMenuItem) menuItem).setIcon(null); + ((JMenuItem) _native).setIcon(null); } } }); diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntrySpacer.java b/src/dorkbox/systemTray/swing/SwingMenuEntrySpacer.java index 30b31c1..59d9acc 100644 --- a/src/dorkbox/systemTray/swing/SwingMenuEntrySpacer.java +++ b/src/dorkbox/systemTray/swing/SwingMenuEntrySpacer.java @@ -25,8 +25,8 @@ import dorkbox.systemTray.SystemTrayMenuAction; class SwingMenuEntrySpacer extends SwingMenuEntry implements MenuSpacer { // this is ALWAYS called on the EDT. - SwingMenuEntrySpacer(final SwingSystemTray systemTray) { - super(new JSeparator(JSeparator.HORIZONTAL), systemTray); + SwingMenuEntrySpacer(final SwingMenu parentMenu) { + super(parentMenu, new JSeparator(JSeparator.HORIZONTAL)); } // called in the EDT thread @@ -51,6 +51,5 @@ class SwingMenuEntrySpacer extends SwingMenuEntry implements MenuSpacer { @Override public void setCallback(final SystemTrayMenuAction callback) { - } } diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntryStatus.java b/src/dorkbox/systemTray/swing/SwingMenuEntryStatus.java index d96644b..e675616 100644 --- a/src/dorkbox/systemTray/swing/SwingMenuEntryStatus.java +++ b/src/dorkbox/systemTray/swing/SwingMenuEntryStatus.java @@ -26,20 +26,20 @@ import dorkbox.systemTray.SystemTrayMenuAction; class SwingMenuEntryStatus extends SwingMenuEntry implements MenuStatus { // this is ALWAYS called on the EDT. - SwingMenuEntryStatus(final String label, final SwingSystemTray systemTray) { - super(new JMenuItem(), systemTray); + SwingMenuEntryStatus(final SwingMenu parentMenu, final String label) { + super(parentMenu, new JMenuItem()); setText(label); } // called in the EDT thread @Override void renderText(final String text) { - ((JMenuItem) menuItem).setText(text); - Font font = menuItem.getFont(); + ((JMenuItem) _native).setText(text); + Font font = _native.getFont(); Font font1 = font.deriveFont(Font.BOLD); - menuItem.setFont(font1); + _native.setFont(font1); - menuItem.setEnabled(false); + _native.setEnabled(false); } @Override diff --git a/src/dorkbox/systemTray/swing/SwingSystemTray.java b/src/dorkbox/systemTray/swing/SwingSystemTray.java index 432af34..b4b7bee 100644 --- a/src/dorkbox/systemTray/swing/SwingSystemTray.java +++ b/src/dorkbox/systemTray/swing/SwingSystemTray.java @@ -25,12 +25,10 @@ import java.awt.TrayIcon; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; -import java.util.concurrent.atomic.AtomicInteger; import javax.swing.ImageIcon; import dorkbox.systemTray.MenuEntry; -import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.util.ImageUtils; import dorkbox.util.ScreenUtil; import dorkbox.util.SwingUtil; @@ -45,11 +43,7 @@ import dorkbox.util.SwingUtil; */ @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"}) public -class SwingSystemTray extends dorkbox.systemTray.SystemTray { - static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); - - volatile SwingSystemTrayMenuPopup menu; - +class SwingSystemTray extends SwingMenu { volatile SystemTray tray; volatile TrayIcon trayIcon; @@ -59,8 +53,8 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { * Creates a new system tray handler class. */ public - SwingSystemTray() { - super(); + SwingSystemTray(final dorkbox.systemTray.SystemTray systemTray) { + super(systemTray, null); ImageUtils.determineIconSize(dorkbox.systemTray.SystemTray.TYPE_SWING); @@ -69,15 +63,10 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { public void run() { SwingSystemTray.this.tray = SystemTray.getSystemTray(); - menu = new SwingSystemTrayMenuPopup(); - if (SwingSystemTray.this.tray == null) { - logger.error("The system tray is not available"); - } } }); } - @Override public void shutdown() { SwingUtil.invokeAndWait(new Runnable() { @@ -96,12 +85,6 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { }); } - protected - SwingSystemTrayMenuPopup getMenu() { - return menu; - } - - @Override public String getStatus() { synchronized (menuEntries) { @@ -114,13 +97,6 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { return null; } - protected - void dispatch(final Runnable runnable) { - // this will properly check if we are running on the EDT - SwingUtil.invokeLater(runnable); - } - - @Override public void setStatus(final String statusText) { dispatch(new Runnable() { @@ -148,7 +124,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { } else { // create a new one - menuEntry = new SwingMenuEntryStatus(statusText, SwingSystemTray.this); + menuEntry = new SwingMenuEntryStatus(SwingSystemTray.this, statusText); // status is ALWAYS at 0 index... menuEntries.add(0, menuEntry); } @@ -157,27 +133,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { }); } - - @Override public - void addMenuSpacer() { - dispatch(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - synchronized (menuEntries) { - MenuEntry menuEntry = new SwingMenuEntrySpacer(SwingSystemTray.this); - menuEntries.add(menuEntry); - } - } - } - }); - } - - - @Override - protected void setIcon_(final File iconFile) { dispatch(new Runnable() { @Override @@ -201,7 +157,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { @Override public void mousePressed(MouseEvent e) { - Dimension size = menu.getPreferredSize(); + Dimension size = _native.getPreferredSize(); Point point = e.getPoint(); Rectangle bounds = ScreenUtil.getScreenBoundsAt(point); @@ -229,18 +185,18 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { // voodoo to get this to popup to have the correct parent // from: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6285881 - menu.setInvoker(menu); - menu.setLocation(x, y); - menu.setVisible(true); - menu.setFocusable(true); - menu.requestFocusInWindow(); + _native.setInvoker(_native); + _native.setLocation(x, y); + _native.setVisible(true); + _native.setFocusable(true); + _native.requestFocusInWindow(); } }); try { SwingSystemTray.this.tray.add(trayIcon); } catch (AWTException e) { - logger.error("TrayIcon could not be added.", e); + dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e); } } else { trayIcon.setImage(trayImage); @@ -248,36 +204,4 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { } }); } - - /** - * Will add a new menu entry, or update one if it already exists - */ - protected - void addMenuEntry_(final String menuText, final File imagePath, final SystemTrayMenuAction callback) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - dispatch(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - MenuEntry menuEntry = getMenuEntry(menuText); - - if (menuEntry != null) { - throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'"); - } - else { - // must always be called on the EDT - menuEntry = new SwingMenuEntryItem(callback, SwingSystemTray.this); - menuEntry.setText(menuText); - menuEntry.setImage(imagePath); - - menuEntries.add(menuEntry); - } - } - } - }); - } } diff --git a/src/dorkbox/systemTray/util/ImageUtils.java b/src/dorkbox/systemTray/util/ImageUtils.java index fdfdb79..423bf3c 100644 --- a/src/dorkbox/systemTray/util/ImageUtils.java +++ b/src/dorkbox/systemTray/util/ImageUtils.java @@ -217,6 +217,9 @@ class ImageUtils { return newFile; } + // make sure the directory exists + newFile.getParentFile().mkdirs(); + BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = image.createGraphics(); g2d.setColor(new Color(0,0,0,0)); diff --git a/test/dorkbox/TestTray.java b/test/dorkbox/TestTray.java index 6ad7c79..36bc9bc 100644 --- a/test/dorkbox/TestTray.java +++ b/test/dorkbox/TestTray.java @@ -18,6 +18,7 @@ package dorkbox; import java.net.URL; +import dorkbox.systemTray.Menu; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTrayMenuAction; @@ -44,7 +45,7 @@ class TestTray { public TestTray() { - this.systemTray = SystemTray.getSystemTray(); + this.systemTray = SystemTray.get(); if (systemTray == null) { throw new RuntimeException("Unable to load SystemTray!"); } @@ -55,7 +56,7 @@ class TestTray { callbackGreen = new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.setStatus("Some Mail!"); systemTray.setIcon(GREEN_MAIL); @@ -69,7 +70,7 @@ class TestTray { callbackGray = new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.setStatus(null); systemTray.setIcon(BLACK_MAIL); @@ -82,11 +83,12 @@ class TestTray { this.systemTray.addMenuEntry("Green Mail", GREEN_MAIL, callbackGreen); this.systemTray.addMenuSpacer(); + this.systemTray.addMenuEntry("AAAAAAAA", LT_GRAY_MAIL, null); systemTray.addMenuEntry("Quit", new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.shutdown(); //System.exit(0); not necessary if all non-daemon threads have stopped. } diff --git a/test/dorkbox/TestTrayJavaFX.java b/test/dorkbox/TestTrayJavaFX.java index 45e7941..51b822d 100644 --- a/test/dorkbox/TestTrayJavaFX.java +++ b/test/dorkbox/TestTrayJavaFX.java @@ -18,6 +18,7 @@ package dorkbox; import java.net.URL; +import dorkbox.systemTray.Menu; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTrayMenuAction; @@ -77,7 +78,7 @@ class TestTrayJavaFX extends Application { primaryStage.show(); - this.systemTray = SystemTray.getSystemTray(); + this.systemTray = SystemTray.get(); if (systemTray == null) { throw new RuntimeException("Unable to load SystemTray!"); } @@ -89,7 +90,7 @@ class TestTrayJavaFX extends Application { callbackGreen = new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.setIcon(GREEN_MAIL); systemTray.setStatus("Some Mail!"); @@ -103,7 +104,7 @@ class TestTrayJavaFX extends Application { callbackGray = new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.setStatus(null); systemTray.setIcon(BLACK_MAIL); @@ -120,7 +121,7 @@ class TestTrayJavaFX extends Application { systemTray.addMenuEntry("Quit", new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.shutdown(); Platform.exit(); // necessary to close javaFx //System.exit(0); not necessary if all non-daemon threads have stopped. diff --git a/test/dorkbox/TestTraySwt.java b/test/dorkbox/TestTraySwt.java index 044a029..8656e7f 100644 --- a/test/dorkbox/TestTraySwt.java +++ b/test/dorkbox/TestTraySwt.java @@ -23,6 +23,7 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; +import dorkbox.systemTray.Menu; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTrayMenuAction; @@ -61,7 +62,7 @@ class TestTraySwt { helloWorldTest.pack(); - this.systemTray = SystemTray.getSystemTray(); + this.systemTray = SystemTray.get(); if (systemTray == null) { throw new RuntimeException("Unable to load SystemTray!"); } @@ -73,7 +74,7 @@ class TestTraySwt { callbackGreen = new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final dorkbox.systemTray.Menu parentMenu, final MenuEntry menuEntry) { systemTray.setStatus("Some Mail!"); systemTray.setIcon(GREEN_MAIL); @@ -87,7 +88,7 @@ class TestTraySwt { callbackGray = new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.setStatus(null); systemTray.setIcon(BLACK_MAIL); @@ -104,7 +105,7 @@ class TestTraySwt { systemTray.addMenuEntry("Quit", new SystemTrayMenuAction() { @Override public - void onClick(final SystemTray systemTray, final MenuEntry menuEntry) { + void onClick(final SystemTray systemTray, final Menu parentMenu, final MenuEntry menuEntry) { systemTray.shutdown(); display.asyncExec(new Runnable() {