diff --git a/src/dorkbox/systemTray/Checkbox.java b/src/dorkbox/systemTray/Checkbox.java index 9a27836..dbceedb 100644 --- a/src/dorkbox/systemTray/Checkbox.java +++ b/src/dorkbox/systemTray/Checkbox.java @@ -16,13 +16,166 @@ package dorkbox.systemTray; +import java.awt.event.ActionListener; + +import dorkbox.systemTray.util.MenuCheckboxHook; + /** * This represents a common menu-checkbox entry, that is cross platform in nature */ public -interface Checkbox extends Entry { +class Checkbox extends Entry { + private volatile boolean isChecked = false; + private volatile String text; + private volatile ActionListener callback; + + private volatile boolean enabled = true; + private volatile char mnemonicKey; + + public + Checkbox() { + this(null, null); + } + + public + Checkbox(final String text) { + this(text, null); + } + + public + Checkbox(final String text, final ActionListener callback) { + this.text = text; + this.callback = callback; + } + /** - * @return true if this checkbox is selected, false if not + * @param hook the platform specific implementation for all actions for this type + * @param parent the parent of this menu, null if the parent is the system tray + * @param systemTray the system tray (which is the object that sits in the system tray) */ - boolean getState(); + public synchronized + void bind(final MenuCheckboxHook hook, final Menu parent, final SystemTray systemTray) { + super.bind(hook, parent, systemTray); + + hook.setEnabled(this); + hook.setText(this); + hook.setCallback(this); + hook.setShortcut(this); + hook.setChecked(this); + } + + + /** + * Sets the checked status for this entry + * + * @param checked true to show the checkbox, false to hide it + */ + public + void setChecked(boolean checked) { + this.isChecked = checked; + + if (hook != null) { + ((MenuCheckboxHook) hook).setChecked(this); + } + } + + /** + * @return true if this checkbox is selected, false if not. + */ + public final + boolean getChecked() { + return isChecked; + } + + /** + * Gets the callback assigned to this menu entry + */ + public + ActionListener getCallback() { + return callback; + } + + /** + * @return true if this item is enabled, or false if it is disabled. + */ + public + boolean getEnabled() { + return this.enabled; + } + + /** + * Enables, or disables the entry. + */ + public + void setEnabled(final boolean enabled) { + this.enabled = enabled; + + if (hook != null) { + ((MenuCheckboxHook) hook).setEnabled(this); + } + } + + /** + * @return the text label that the menu entry has assigned + */ + public final + String getText() { + return text; + } + + /** + * Specifies the new text to set for a menu entry + * + * @param text the new text to set + */ + public + void setText(final String text) { + this.text = text; + + if (hook != null) { + ((MenuCheckboxHook) hook).setText(this); + } + } + + /** + * Sets a callback for a menu entry. This is the action that occurs when one clicks the menu entry + * + * @param callback the callback to set. If null, the callback is safely removed. + */ + public + void setCallback(final ActionListener callback) { + this.callback = callback; + if (hook != null) { + ((MenuCheckboxHook) hook).setCallback(this); + } + } + + /** + * Gets the shortcut key for this menu entry (Mnemonic) which is what menu entry uses to be "selected" via the keyboard while the + * menu is displayed. + * + * Mnemonics are case-insensitive, and if the character defined by the mnemonic is found within the text, the first occurrence + * of it will be underlined. + */ + public + char getShortcut() { + return this.mnemonicKey; + } + + /** + * Sets a menu entry shortcut key (Mnemonic) so that menu entry can be "selected" via the keyboard while the menu is displayed. + * + * Mnemonics are case-insensitive, and if the character defined by the mnemonic is found within the text, the first occurrence + * of it will be underlined. + * + * @param key this is the key to set as the mnemonic + */ + public + void setShortcut(final char key) { + this.mnemonicKey = key; + + if (hook != null) { + ((MenuCheckboxHook) hook).setShortcut(this); + } + } } diff --git a/src/dorkbox/systemTray/Entry.java b/src/dorkbox/systemTray/Entry.java index b15c645..4176ff2 100644 --- a/src/dorkbox/systemTray/Entry.java +++ b/src/dorkbox/systemTray/Entry.java @@ -16,102 +16,99 @@ package dorkbox.systemTray; -import java.awt.event.ActionListener; -import java.io.File; -import java.io.InputStream; -import java.net.URL; +import java.util.concurrent.atomic.AtomicInteger; + +import dorkbox.systemTray.util.EntryHook; /** * This represents a common menu-entry, that is cross platform in nature */ +@SuppressWarnings({"unused", "SameParameterValue"}) public -interface Entry { +class Entry { + private static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(0); + private final int id = Entry.MENU_ID_COUNTER.getAndIncrement(); + + private Menu parent; + private SystemTray systemTray; + + protected volatile EntryHook hook; + + public + Entry() { + } + + // methods for hooking into the system tray, menu's, and entries. + // called internally when an entry/menu is attached /** - * @return the menu that contains this menu entry + * @param hook the platform specific implementation for all actions for this type + * @param parent the parent of this menu, null if the parent is the system tray + * @param systemTray the system tray (which is the object that sits in the system tray) */ - Menu getParent(); + public synchronized + void bind(final EntryHook hook, final Menu parent, final SystemTray systemTray) { + this.parent = parent; + this.systemTray = systemTray; + + this.hook = hook; + } + + // END methods for hooking into the system tray, menu's, and entries. + /** - * Enables, or disables the entry. + * @return the parent menu (of this entry or menu) or null if we are the root menu */ - void setEnabled(final boolean enabled); + public final synchronized + Menu getParent() { + return this.parent; + } /** - * @return the text label that the menu entry has assigned + * @return the system tray that this menu is ultimately attached to */ - String getText(); - - /** - * Specifies the new text to set for a menu entry - * - * @param newText the new text to set - */ - void setText(String newText); - - /** - * Specifies the new image to set for a menu entry, NULL to delete the image - * - * @param imageFile the file of the image to use or null - */ - void setImage(File imageFile); - - /** - * Specifies the new image to set for a menu entry, NULL to delete the image - * - * @param imagePath the full path of the image to use or null - */ - void setImage(String imagePath); - - /** - * Specifies the new image to set for a menu entry, NULL to delete the image - * - * @param imageUrl the URL of the image to use or null - */ - void setImage(URL imageUrl); - - /** - * Specifies the new image to set for a menu entry, NULL to delete the image - * - * @param cacheName the name to use for lookup in the cache for the imageStream - * @param imageStream the InputStream of the image to use - */ - void setImage(String cacheName, InputStream imageStream); - - /** - * Specifies the new image to set for a menu entry, NULL to delete the image - * - * This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is - * also NOT RECOMMENDED, but is provided for simplicity. - * - * @param imageStream the InputStream of the image to use - */ - void setImage(InputStream imageStream); - - /** - * @return true if this menu entry has an image assigned to it, or is just text. - */ - boolean hasImage(); - - /** - * Sets a callback for a menu entry. This is the action that occurs when one clicks the menu entry - * - * @param callback the callback to set. If null, the callback is safely removed. - */ - void setCallback(ActionListener callback); - - /** - * Sets a menu entry shortcut key (Mnemonic) so that menu entry can be "selected" via the keyboard while the menu is displayed. - * - * Mnemonics are case-insensitive, and if the character defined by the mnemonic is found within the text, the first occurrence - * of it will be underlined. - * - * @param key this is the key to set as the mnemonic - */ - void setShortcut(char key); + public final synchronized + SystemTray getSystemTray() { + return this.systemTray; + } /** * Removes this menu entry from the menu and releases all system resources associated with this menu entry */ - void remove(); + public synchronized + void remove() { + if (hook != null) { + hook.remove(); + + this.parent = null; + this.systemTray = null; + hook = null; + } + } + + + @Override + public final + int hashCode() { + return id; + } + + + @Override + public final + boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + Entry other = (Entry) obj; + return this.id == other.id; + } } diff --git a/src/dorkbox/systemTray/Menu.java b/src/dorkbox/systemTray/Menu.java index 185b514..315f218 100644 --- a/src/dorkbox/systemTray/Menu.java +++ b/src/dorkbox/systemTray/Menu.java @@ -15,169 +15,106 @@ */ package dorkbox.systemTray; +import java.awt.Image; import java.awt.event.ActionListener; +import java.io.File; import java.io.InputStream; import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import dorkbox.systemTray.util.MenuHook; +import dorkbox.systemTray.util.Status; /** * Represents a cross-platform menu that is displayed by the tray-icon or as a sub-menu */ +@SuppressWarnings("unused") public -interface Menu extends Entry { +class Menu extends MenuItem { + protected final List menuEntries = new ArrayList(); + + public + Menu() { + } + + public + Menu(final String text) { + super(text); + } + + public + Menu(final String text, final ActionListener callback) { + super(text, callback); + } + + public + Menu(final String text, final String imagePath) { + super(text, imagePath); + } + + public + Menu(final String text, final File imageFile) { + super(text, imageFile); + } + + public + Menu(final String text, final URL imageUrl) { + super(text, imageUrl); + } + + public + Menu(final String text, final InputStream imageStream) { + super(text, imageStream); + } + + public + Menu(final String text, final Image image) { + super(text, image); + } + + public + Menu(final String text, final String imagePath, final ActionListener callback) { + super(text, imagePath, callback); + } + + public + Menu(final String text, final File imageFile, final ActionListener callback) { + super(text, imageFile, callback); + } + + public + Menu(final String text, final URL imageUrl, final ActionListener callback) { + super(text, imageUrl, callback); + } + + public + Menu(final String text, final InputStream imageStream, final ActionListener callback) { + super(text, imageStream, callback); + } + + public + Menu(final String text, final Image image, final ActionListener callback) { + super(text, image, callback); + } /** - * @return the parent menu (of this menu) or null if we are the root menu + * @param hook the platform specific implementation for all actions for this type + * @param parent the parent of this menu, null if the parent is the system tray + * @param systemTray the system tray (which is the object that sits in the system tray) */ - Menu getParent(); - - /** - * @return the system tray that this menu is ultimately attached to - */ - SystemTray getSystemTray(); - - /** - * 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) - */ - void addSeparator(); - - - /** - * This removes al menu entries from this menu - */ - void removeAll(); - - /** - * 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 - */ - Entry get(final String menuText); - - /** - * Gets the first menu entry or sub-menu, ignoring status and separators - */ - Entry getFirst(); - - /** - * Gets the last menu entry or sub-menu, ignoring status and separators - */ - Entry getLast(); - - /** - * Gets the menu entry or sub-menu for a specified index (zero-index), ignoring status and separators - * - * @param menuIndex the menu entry index to use to retrieve the menu entry. - */ - Entry get(final int menuIndex); - - - - /** - * Adds a menu entry 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 - */ - Entry addEntry(String menuText, ActionListener callback); - - /** - * Adds a menu entry 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 - */ - Entry addEntry(String menuText, String imagePath, ActionListener callback); - - /** - * Adds a menu entry 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 - */ - Entry addEntry(String menuText, URL imageUrl, ActionListener callback); - - /** - * Adds a menu entry 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 - */ - Entry addEntry(String menuText, String cacheName, InputStream imageStream, ActionListener callback); - - /** - * Adds a menu entry 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 - */ - Entry addEntry(String menuText, InputStream imageStream, ActionListener callback); - - - - /** - * Adds a check-box menu entry with text - * - * @param menuText string of the text you want to appear - * @param callback callback that will be executed when this menu entry is clicked - */ - Checkbox addCheckbox(String menuText, ActionListener callback); - - - - /** - * Adds a sub-menu entry with text (no image) - * - * @param menuText string of the text you want to appear - */ - Menu addMenu(String menuText); - - /** - * Adds a sub-menu entry 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 - */ - Menu addMenu(String menuText, String imagePath); - - /** - * Adds a sub-menu entry 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 - */ - Menu addMenu(String menuText, URL imageUrl); - - /** - * Adds a sub-menu entry 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 - */ - Menu addMenu(String menuText, String cacheName, InputStream imageStream); - - /** - * Adds a sub-menu entry 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 - */ - Menu addMenu(String menuText, InputStream imageStream); + public synchronized + void bind(final MenuHook hook, final Menu parent, final SystemTray systemTray) { + super.bind(hook, parent, systemTray); + synchronized (menuEntries) { + for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { + final Entry menuEntry = menuEntries.get(i); + hook.add(this, menuEntry, i); + } + } + } /** * Adds a swing widget as a menu entry. @@ -185,27 +122,149 @@ interface Menu extends Entry { * @param widget the JComponent that is to be added as an entry */ // TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. -// Entry addWidget(JComponent widget); +// Entry add(JComponent widget); + /** + * Adds a menu entry, separator, or sub-menu to this menu + */ + public final + T add(final T entry) { + return add(entry, -1); + } + + /** + * Adds a menu entry, separator, or sub-menu to this menu. + */ + public final + T add(final T entry, int index) { + synchronized (menuEntries) { + if (index == -1) { + menuEntries.add(entry); + } else { + if (!menuEntries.isEmpty() && menuEntries.get(0) instanceof Status) { + // the "status" menu entry is ALWAYS first + index++; + } + menuEntries.add(index, entry); + } + } + + if (hook != null) { + ((MenuHook) hook).add(this, entry, index); + } + + return entry; + } + + /** + * Gets the first menu entry or sub-menu, ignoring status and separators + */ + public final + Entry getFirst() { + return get(0); + } + + /** + * Gets the last menu entry or sub-menu, ignoring status and separators + */ + public final + Entry getLast() { + // Must be wrapped in a synchronized block for object visibility + synchronized (menuEntries) { + if (!menuEntries.isEmpty()) { + Entry entry; + for (int i = menuEntries.size()-1; i >= 0; i--) { + entry = menuEntries.get(i); + + if (!(entry instanceof Separator || entry instanceof Status)) { + return entry; + } + } + } + } + + return null; + } + + /** + * Gets the menu entry or sub-menu for a specified index (zero-index), ignoring status and separators + * + * @param menuIndex the menu entry index to use to retrieve the menu entry. + */ + public final + Entry get(final int menuIndex) { + if (menuIndex < 0) { + return null; + } + + // Must be wrapped in a synchronized block for object visibility + synchronized (menuEntries) { + if (!menuEntries.isEmpty()) { + int count = 0; + for (Entry entry : menuEntries) { + if (entry instanceof Separator || entry instanceof Status) { + continue; + } + + if (count == menuIndex) { + return entry; + } + + count++; + } + } + } + + return null; + } /** * This removes a menu entry from the dropdown menu. * * @param entry This is the menu entry to remove */ - void remove(final Entry entry); + public final + void remove(final Entry entry) { + // null is passed in when a sub-menu is removing itself from us (because they have already called "remove" and have also + // removed themselves from the menuEntries) + if (entry != null) { + synchronized (menuEntries) { + for (Iterator iterator = menuEntries.iterator(); iterator.hasNext(); ) { + final Entry entry__ = iterator.next(); + if (entry__ == entry) { + iterator.remove(); + entry.remove(); + break; + } + } + } + } + } /** - * This removes a sub-menu entry from the dropdown menu. - * - * @param menu This is the menu entry to remove + * This removes all menu entries from this menu */ - void remove(final Menu menu); + public final + void removeAll() { + synchronized (menuEntries) { + // have to make copy because we are deleting all of them, and sub-menus remove themselves from parents + ArrayList menuEntriesCopy = new ArrayList(this.menuEntries); + for (Entry entry : menuEntriesCopy) { + entry.remove(); + } + menuEntries.clear(); + } + } + /** - * This removes a menu entry or sub-menu (via the text label) from the dropdown menu. - * - * @param menuText This is the label for the menu entry or sub-menu to remove + * This removes all menu entries from this menu AND this menu from it's parent */ - void remove(final String menuText); + @Override + public synchronized + void remove() { + removeAll(); + + super.remove(); + } } diff --git a/src/dorkbox/systemTray/MenuItem.java b/src/dorkbox/systemTray/MenuItem.java new file mode 100644 index 0000000..d285ec0 --- /dev/null +++ b/src/dorkbox/systemTray/MenuItem.java @@ -0,0 +1,375 @@ +/* + * Copyright 2015 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; + +import java.awt.Image; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.InputStream; +import java.net.URL; + +import dorkbox.systemTray.util.ImageUtils; +import dorkbox.systemTray.util.MenuItemHook; + +/** + * This represents a common menu-entry, that is cross platform in nature + */ +@SuppressWarnings({"unused", "SameParameterValue", "WeakerAccess"}) +public +class MenuItem extends Entry { + private volatile String text; + private volatile File imageFile; + private volatile ActionListener callback; + + // default enabled is always true + private volatile boolean enabled = true; + private volatile char mnemonicKey; + + public + MenuItem() { + this(null, null, null, false); + } + + public + MenuItem(final String text) { + this(text, null, null, false); + } + + public + MenuItem(final String text, final ActionListener callback) { + this(text, null, callback, false); + } + + public + MenuItem(final String text, final String imagePath) { + this(text, imagePath, null); + } + + public + MenuItem(final String text, final File imageFile) { + this(text, imageFile, null); + } + + public + MenuItem(final String text, final URL imageUrl) { + this(text, imageUrl, null); + } + + public + MenuItem(final String text, final InputStream imageStream) { + this(text, imageStream, null); + } + + public + MenuItem(final String text, final Image image) { + this(text, image, null); + } + + public + MenuItem(final String text, final String imagePath, final ActionListener callback) { + this(text, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath, true), callback, false); + } + + public + MenuItem(final String text, final File imageFile, final ActionListener callback) { + this(text, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile, true), callback, false); + } + + public + MenuItem(final String text, final URL imageUrl, final ActionListener callback) { + this(text, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl, true), callback, false); + } + + public + MenuItem(final String text, final InputStream imageStream, final ActionListener callback) { + this(text, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream, true), callback, false); + } + + public + MenuItem(final String text, final Image image, final ActionListener callback) { + this(text, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, image, true), callback, false); + } + + // the last parameter (unused) is there so the signature is different + private + MenuItem(final String text, final File imageFile, final ActionListener callback, final boolean unused) { + this.text = text; + this.imageFile = imageFile; + this.callback = callback; + } + + /** + * @param hook the platform specific implementation for all actions for this type + * @param parent the parent of this menu, null if the parent is the system tray + * @param systemTray the system tray (which is the object that sits in the system tray) + */ + public synchronized + void bind(final MenuItemHook hook, final Menu parent, final SystemTray systemTray) { + super.bind(hook, parent, systemTray); + + hook.setImage(this); + hook.setEnabled(this); + hook.setText(this); + hook.setCallback(this); + hook.setShortcut(this); + } + + private + void setImage_(final File imageFile) { + this.imageFile = imageFile; + + if (hook != null) { + ((MenuItemHook) hook).setImage(this); + } + } + + /** + * Gets the File (which is the only cross-platform solution) that is assigned to this menu entry. + *

+ * This file can also be a cached file, depending on how the image was assigned to this entry. + */ + public + File getImage() { + return imageFile; + } + + /** + * Gets the callback assigned to this menu entry + */ + public + ActionListener getCallback() { + return callback; + } + + /** + * @return true if this item is enabled, or false if it is disabled. + */ + public + boolean getEnabled() { + return this.enabled; + } + + /** + * Enables, or disables the entry. + */ + public + void setEnabled(final boolean enabled) { + this.enabled = enabled; + + if (hook != null) { + ((MenuItemHook) hook).setEnabled(this); + } + } + + /** + * @return the text label that the menu entry has assigned + */ + public final + String getText() { + return text; + } + + /** + * Specifies the new text to set for a menu entry + * + * @param text the new text to set + */ + public + void setText(final String text) { + this.text = text; + + if (hook != null) { + ((MenuItemHook) hook).setText(this); + } + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image. + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param imageFile the file of the image to use or null + */ + public + void setImage(final File imageFile) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile, true)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image. + * + * @param imageFile the file of the image to use or null + * @param cacheImage true to cache the image (only if the image is resized as necessary) + */ + public + void setImage(final File imageFile, final boolean cacheImage) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile, cacheImage)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param imagePath the full path of the image to use or null + */ + public + void setImage(final String imagePath) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath, true)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + * + * @param imagePath the full path of the image to use or null + * @param cacheImage true to cache the image (only if the image is resized as necessary) + */ + public + void setImage(final String imagePath, final boolean cacheImage) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath, cacheImage)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param imageUrl the URL of the image to use or null + */ + public + void setImage(final URL imageUrl) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl, true)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + * + * @param imageUrl the URL of the image to use or null + * @param cacheImage true to cache the image (only if the image is resized as necessary) + */ + public + void setImage(final URL imageUrl, final boolean cacheImage) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl, cacheImage)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param imageStream the InputStream of the image to use + */ + public + void setImage(final InputStream imageStream) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream, true)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + * + * @param imageStream the InputStream of the image to use + * @param cacheImage true to cache the image (only if the image is resized as necessary) + */ + public + void setImage(final InputStream imageStream, final boolean cacheImage) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream, cacheImage)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param image the image of the image to use + */ + public + void setImage(final Image image) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, image, true)); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + * + * @param image the image of the image to use + * @param cacheImage true to cache the image (only if the image is resized as necessary) + */ + public + void setImage(final Image image, final boolean cacheImage) { + setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, image, cacheImage)); + } + + /** + * @return true if this menu entry has an image assigned to it, or is just text. + */ + public + boolean hasImage() {return imageFile != null;} + + /** + * Sets a callback for a menu entry. This is the action that occurs when one clicks the menu entry + * + * @param callback the callback to set. If null, the callback is safely removed. + */ + public + void setCallback(final ActionListener callback) { + this.callback = callback; + + if (hook != null) { + ((MenuItemHook) hook).setCallback(this); + } + } + + /** + * Gets the shortcut key for this menu entry (Mnemonic) which is what menu entry uses to be "selected" via the keyboard while the + * menu is displayed. + * + * Mnemonics are case-insensitive, and if the character defined by the mnemonic is found within the text, the first occurrence + * of it will be underlined. + */ + public + char getShortcut() { + return this.mnemonicKey; + } + + /** + * Sets a menu entry shortcut key (Mnemonic) so that menu entry can be "selected" via the keyboard while the menu is displayed. + * + * Mnemonics are case-insensitive, and if the character defined by the mnemonic is found within the text, the first occurrence + * of it will be underlined. + * + * @param key this is the key to set as the mnemonic + */ + public + void setShortcut(final char key) { + this.mnemonicKey = key; + + if (hook != null) { + ((MenuItemHook) hook).setShortcut(this); + } + } + + @Override + public synchronized + void remove() { + if (hook != null) { + setImage_(null); + setText(null); + setCallback(null); + } + + super.remove(); + } +} diff --git a/src/dorkbox/systemTray/Separator.java b/src/dorkbox/systemTray/Separator.java index e03b54b..c51f3e8 100644 --- a/src/dorkbox/systemTray/Separator.java +++ b/src/dorkbox/systemTray/Separator.java @@ -17,8 +17,27 @@ package dorkbox.systemTray; /** - * This represents a common menu-spacer entry, that is cross platform in nature + * This represents a common menu-spacer entry, that is cross platform in nature. + *

+ * When menu entries are removed, any menu spacer that ends up at the top/bottom of the menu will also be removed. + *

+ * For example: + *

 {@code
+ * Original     Entry3 deleted     Result
+ *
+ *                 
+ * Entry1           Entry1         Entry1
+ * Entry2      ->   Entry2    ->   Entry2
+ *          (deleted)
+ * Entry3           (deleted)
+ *
+ * }
*/ public -interface Separator { +class Separator extends Entry { + + public + Separator() { + super(); + } } diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index dbc1a2f..f5bd871 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -17,7 +17,7 @@ package dorkbox.systemTray; import java.awt.GraphicsEnvironment; import java.awt.HeadlessException; -import java.awt.event.ActionListener; +import java.awt.Image; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; @@ -58,8 +58,8 @@ import dorkbox.util.process.ShellProcessBuilder; * Factory and base-class for system tray implementations. */ @SuppressWarnings({"unused", "Duplicates", "DanglingJavadoc", "WeakerAccess"}) -public -class SystemTray implements Menu { +public final +class SystemTray { public static final Logger logger = LoggerFactory.getLogger(SystemTray.class); public enum TrayType { @@ -111,7 +111,7 @@ class SystemTray implements Menu { *

* This is an advanced feature, and it is recommended to leave at AutoDetect. */ - public static TrayType FORCE_TRAY_TYPE = TrayType.AutoDetect; + public static TrayType FORCE_TRAY_TYPE = TrayType.Swing; @Property /** @@ -130,7 +130,7 @@ class SystemTray implements Menu { private static volatile SystemTray systemTray = null; - private static volatile Menu systemTrayMenu = null; + private static volatile Tray systemTrayMenu = null; public final static boolean isJavaFxLoaded; public final static boolean isSwtLoaded; @@ -163,7 +163,7 @@ class SystemTray implements Menu { } private static - Class selectType(final boolean useNativeMenus, final TrayType trayType) throws Exception { + Class selectType(final boolean useNativeMenus, final TrayType trayType) throws Exception { if (trayType == TrayType.GtkStatusIcon) { if (useNativeMenus) { return _GtkStatusIconNativeTray.class; @@ -192,7 +192,7 @@ class SystemTray implements Menu { } private static - Class selectTypeQuietly(final boolean useNativeMenus, final TrayType trayType) { + Class selectTypeQuietly(final boolean useNativeMenus, final TrayType trayType) { try { return selectType(useNativeMenus, trayType); } catch (Throwable t) { @@ -225,7 +225,7 @@ class SystemTray implements Menu { throw new HeadlessException(); } - Class trayType = null; + Class trayType = null; if (DEBUG) { logger.debug("OS: {}", System.getProperty("os.name")); @@ -512,7 +512,7 @@ class SystemTray implements Menu { systemTrayMenu = null; } else { - final AtomicReference

reference = new AtomicReference(); + final AtomicReference reference = new AtomicReference(); /* * appIndicator/gtk require strings (which is the path) @@ -562,12 +562,17 @@ class SystemTray implements Menu { if (isJavaFxLoaded || isSwtLoaded || (OS.isLinux() && NativeUI.class.isAssignableFrom(trayType) && trayType != _AwtTray.class)) { try { - reference.set((Menu) trayType.getConstructors()[0].newInstance(systemTray)); + reference.set((Tray) trayType.getConstructors()[0].newInstance(systemTray)); logger.info("Successfully Loaded: {}", trayType.getSimpleName()); } catch (Exception e) { logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e); } } else { + if (trayType == _AwtTray.class) { + // ensure awt toolkit is initialized. + java.awt.Toolkit.getDefaultToolkit(); + } + // have to construct swing stuff inside the swing EDT final Class finalTrayType = trayType; SwingUtil.invokeAndWait(new Runnable() { @@ -575,7 +580,7 @@ class SystemTray implements Menu { public void run() { try { - reference.set((Menu) finalTrayType.getConstructors()[0].newInstance(systemTray)); + reference.set((Tray) finalTrayType.getConstructors()[0].newInstance(systemTray)); logger.info("Successfully Loaded: {}", finalTrayType.getSimpleName()); } catch (Exception e) { logger.error("Unable to create tray type: '" + finalTrayType.getSimpleName() + "'", e); @@ -681,25 +686,11 @@ class SystemTray implements Menu { public void shutdown() { // this will call "dispatchAndWait()" behind the scenes, so it is thread-safe - final Menu menu = systemTrayMenu; - if (menu instanceof _AppIndicatorTray) { - ((_AppIndicatorTray) menu).shutdown(); - } - else if (menu instanceof _AppIndicatorNativeTray) { - ((_AppIndicatorNativeTray) menu).shutdown(); - } - else if (menu instanceof _GtkStatusIconTray) { - ((_GtkStatusIconTray) menu).shutdown(); - } - else if (menu instanceof _GtkStatusIconNativeTray) { - ((_GtkStatusIconNativeTray) menu).shutdown(); - } - else if (menu instanceof _AwtTray) { - ((_AwtTray) menu).shutdown(); - } - else if (menu instanceof _SwingTray) { - ((_SwingTray) menu).shutdown(); + final Menu tray = systemTrayMenu; + if (tray != null) { + tray.remove(); } + systemTrayMenu = null; } @@ -708,29 +699,12 @@ class SystemTray implements Menu { */ public String getStatus() { - final Menu menu = systemTrayMenu; + final Tray tray = systemTrayMenu; + if (tray != null) { + return tray.getStatus(); + } - if (menu instanceof _AppIndicatorTray) { - return ((_AppIndicatorTray) menu).getStatus(); - } - else if (menu instanceof _AppIndicatorNativeTray) { - return ((_AppIndicatorNativeTray) menu).getStatus(); - } - else if (menu instanceof _GtkStatusIconTray) { - return ((_GtkStatusIconTray) menu).getStatus(); - } - else if (menu instanceof _GtkStatusIconNativeTray) { - return ((_GtkStatusIconNativeTray) menu).getStatus(); - } - else if (menu instanceof _AwtTray) { - return ((_AwtTray) menu).getStatus(); - } - else if (menu instanceof _SwingTray) { - return ((_SwingTray) menu).getStatus(); - } - else { - return ""; - } + return ""; } /** @@ -740,40 +714,10 @@ class SystemTray implements Menu { */ public void setStatus(String statusText) { - final Menu menu = systemTrayMenu; - - if (menu instanceof _AppIndicatorTray) { - ((_AppIndicatorTray) menu).setStatus(statusText); + final Tray tray = systemTrayMenu; + if (tray != null) { + tray.setStatus(statusText); } - else if (menu instanceof _AppIndicatorNativeTray) { - ((_AppIndicatorNativeTray) menu).setStatus(statusText); - } - else if (menu instanceof _GtkStatusIconTray) { - ((_GtkStatusIconTray) menu).setStatus(statusText); - } - else if (menu instanceof _GtkStatusIconNativeTray) { - ((_GtkStatusIconNativeTray) menu).setStatus(statusText); - } - else if (menu instanceof _AwtTray) { - ((_AwtTray) menu).setStatus(statusText); - } - else if (menu instanceof _SwingTray) { - ((_SwingTray) menu).setStatus(statusText); - } - } - - /** - * @return the parent menu (of this menu) or null if we are the root menu - */ - public - Menu getParent() { - return null; - } - - @Override - public - SystemTray getSystemTray() { - return this; } /** @@ -784,391 +728,166 @@ class SystemTray implements Menu { return systemTrayMenu; } - /** - * 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 addSeparator() { - systemTrayMenu.addSeparator(); - } - /** * Shows (if hidden), or hides (if showing) the system tray. */ - @Override public void setEnabled(final boolean enabled) { - systemTrayMenu.setEnabled(enabled); + final Menu menu = systemTrayMenu; + if (menu != null) { + menu.setEnabled(enabled); + } } /** - * Does nothing. You cannot get the text for the system tray + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param imageFile the file of the image to use or null */ - @Override public - String getText() { - return ""; - } - - /** - * Does nothing. You cannot set the text for the system tray - */ - @Override - public - void setText(final String newText) { - // NO OP. + void setImage(final File imageFile) { + setImage(imageFile, true); } /** * Specifies the new image to set for a menu entry, NULL to delete the image * * @param imageFile the file of the image to use or null + * @param cacheImage true to cache the image (only if the image is resized as necessary) */ - @Override public - void setImage(final File imageFile) { + void setImage(final File imageFile, final boolean cacheImage) { if (imageFile == null) { throw new NullPointerException("imageFile cannot be null!"); } - systemTrayMenu.setImage(imageFile); + final Menu menu = systemTrayMenu; + if (menu != null) { + menu.setImage(imageFile, cacheImage); + } + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param imagePath the full path of the image to use or null + */ + public + void setImage(final String imagePath) { + setImage(imagePath, true); } /** * Specifies the new image to set for a menu entry, NULL to delete the image * * @param imagePath the full path of the image to use or null + * @param cacheImage true to cache the image (only if the image is resized as necessary) */ - @Override public - void setImage(final String imagePath) { + void setImage(final String imagePath, final boolean cacheImage) { if (imagePath == null) { throw new NullPointerException("imagePath cannot be null!"); } - systemTrayMenu.setImage(imagePath); + final Menu menu = systemTrayMenu; + if (menu != null) { + menu.setImage(imagePath, cacheImage); + } + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param imageUrl the URL of the image to use or null + */ + public + void setImage(final URL imageUrl) { + setImage(imageUrl, true); } /** * Specifies the new image to set for a menu entry, NULL to delete the image * * @param imageUrl the URL of the image to use or null + * @param cacheImage true to cache the image (only if the image is resized as necessary) */ - @Override public - void setImage(final URL imageUrl) { + void setImage(final URL imageUrl, final boolean cacheImage) { if (imageUrl == null) { throw new NullPointerException("imageUrl cannot be null!"); } - systemTrayMenu.setImage(imageUrl); - } - - /** - * Specifies the new image to set for a menu entry, NULL to delete the image - * - * @param cacheName the name to use for lookup in the cache for the imageStream - * @param imageStream the InputStream of the image to use - */ - @Override - public - void setImage(final String cacheName, final InputStream imageStream) { - if (cacheName == null) { - setImage(imageStream); - } else { - if (imageStream == null) { - throw new NullPointerException("imageStream cannot be null!"); - } - - systemTrayMenu.setImage(cacheName, imageStream); + final Menu menu = systemTrayMenu; + if (menu != null) { + menu.setImage(imageUrl); } } /** * Specifies the new image to set for a menu entry, NULL to delete the image - * - * This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is - * also NOT RECOMMENDED, but is provided for simplicity. + *

+ * This method will cache the image if it needs to be resized to fit. * * @param imageStream the InputStream of the image to use */ - @Override public void setImage(final InputStream imageStream) { + setImage(imageStream, true); + } + + /** + * Specifies the new image to set for a menu entry, NULL to delete the image + * + * @param imageStream the InputStream of the image to use + * @param cacheImage true to cache the image (only if the image is resized as necessary) + */ + public + void setImage(final InputStream imageStream, final boolean cacheImage) { if (imageStream == null) { throw new NullPointerException("imageStream cannot be null!"); } - systemTrayMenu.setImage(imageStream); + final Menu menu = systemTrayMenu; + if (menu != null) { + menu.setImage(imageStream); + } } /** - * By default, we always have an image for the system tray - */ - @Override - public - boolean hasImage() { - return true; - } - - /** - * Does nothing. The system tray cannot have a callback when opened - */ - @Override - public - void setCallback(final ActionListener callback) { - // NO OP. - } - - /** - * Does nothing. The system tray cannot be opened via a shortcut key - */ - @Override - public - void setShortcut(final char key) { - // NO OP. - } - - /** - * Removes the system tray. This is the same as calling `shutdown()` + * Specifies the new image to set for a menu entry, NULL to delete the image + *

+ * This method will cache the image if it needs to be resized to fit. + * + * @param image the image of the image to use */ public - void remove() { - shutdown(); + void setImage(final Image image) { + setImage(image, true); } /** - * Gets the menu entry for a specified text + * Specifies the new image to set for a menu entry, NULL to delete the image * - * @param menuText the menu entry text to use to find the menu entry. The first result found is returned - */ - public final - Entry get(final String menuText) { - return systemTrayMenu.get(menuText); - } - - /** - * Gets the first menu entry, ignoring status and spacers - */ - public final - Entry getFirst() { - return systemTrayMenu.getFirst(); - } - - /** - * Gets the last menu entry, ignoring status and spacers - */ - public final - Entry getLast() { - return systemTrayMenu.getLast(); - } - - /** - * Gets the menu entry for a specified index (zero-index), ignoring status and spacers + * @param image the image of the image to use + * @param cacheImage true to cache the image (only if the image is resized as necessary) * - * @param menuIndex the menu entry index to use to retrieve the menu entry. - */ - public final - Entry get(final int menuIndex) { - return systemTrayMenu.get(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 - Entry addEntry(String menuText, ActionListener callback) { - return addEntry(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 - Entry addEntry(String menuText, String imagePath, ActionListener callback) { - return systemTrayMenu.addEntry(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 - Entry addEntry(String menuText, URL imageUrl, ActionListener callback) { - return systemTrayMenu.addEntry(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 - Entry addEntry(String menuText, String cacheName, InputStream imageStream, ActionListener callback) { - return systemTrayMenu.addEntry(menuText, cacheName, imageStream, callback); - } + void setImage(final Image image, final boolean cacheImage) { + if (image == null) { + throw new NullPointerException("image cannot be null!"); + } - /** - * 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 - Entry addEntry(String menuText, InputStream imageStream, ActionListener callback) { - return systemTrayMenu.addEntry(menuText, imageStream, callback); - } - - - /** - * Adds a check-box menu entry to the tray icon with text - * - * @param menuText string of the text you want to appear - * @param callback callback that will be executed when this menu entry is clicked - */ - @Override - public - Checkbox addCheckbox(final String menuText, final ActionListener callback) { - return systemTrayMenu.addCheckbox(menuText, callback); - } - - - - /** - * Adds a sub-menu entry with text (no image) - * - * @param menuText string of the text you want to appear - */ - public - Menu addMenu(String menuText) { - return addMenu(menuText, (String) null); - } - - /** - * Adds a sub-menu entry 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 - */ - public - Menu addMenu(String menuText, String imagePath) { - return systemTrayMenu.addMenu(menuText, imagePath); - } - - /** - * Adds a sub-menu entry 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 - */ - public - Menu addMenu(String menuText, URL imageUrl) { - return systemTrayMenu.addMenu(menuText, imageUrl); - } - - /** - * Adds a sub-menu entry 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 - */ - public - Menu addMenu(String menuText, String cacheName, InputStream imageStream) { - return systemTrayMenu.addMenu(menuText, cacheName, imageStream); - } - - /** - * Adds a sub-menu entry 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 - */ - public - Menu addMenu(String menuText, InputStream imageStream) { - return systemTrayMenu.addMenu(menuText, imageStream); - } - - /** - * Adds a swing widget as a menu entry. - * - * @param widget the JComponent that is to be added as an entry - */ -// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. -// @Override -// public -// Entry addWidget(final JComponent widget) { -// return systemTrayMenu.addWidget(widget); -// } - - - - - /** - * This removes a menu entry from the dropdown menu. - * - * @param entry This is the menu entry to remove - */ - public final - void remove(final Entry entry) { - systemTrayMenu.remove(entry); - } - - /** - * This removes a sub-menu entry from the dropdown menu. - * - * @param menu This is the menu entry to remove - */ - @Override - public final - void remove(final Menu menu) { - systemTrayMenu.remove(menu); - } - - /** - * This removes al menu entries from this menu - */ - @Override - public final - void removeAll() { - systemTrayMenu.removeAll(); - } - - /** - * 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 remove(final String menuText) { - systemTrayMenu.remove(menuText); + final Menu menu = systemTrayMenu; + if (menu != null) { + menu.setImage(image, cacheImage); + } } } diff --git a/src/dorkbox/systemTray/Tray.java b/src/dorkbox/systemTray/Tray.java new file mode 100644 index 0000000..2a8f149 --- /dev/null +++ b/src/dorkbox/systemTray/Tray.java @@ -0,0 +1,68 @@ +package dorkbox.systemTray; + +import dorkbox.systemTray.util.Status; + +/** + * + */ +public +class Tray extends Menu { + + // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) + // they ALSO do not support tooltips, so we cater to the lowest common denominator + // trayIcon.setToolTip("app name"); + + private volatile String statusText; + + public + Tray() { + super(); + } + + /** + * Gets the 'status' string assigned to the system tray + */ + public final + String getStatus() { + return statusText; + } + + /** + * 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 final + void setStatus(final String statusText) { + this.statusText = statusText; + + synchronized (menuEntries) { + // status is ALWAYS at 0 index... + Entry menuEntry = null; + if (!menuEntries.isEmpty()) { + menuEntry = menuEntries.get(0); + } + + if (menuEntry instanceof Status) { + // set the text or delete... + + if (statusText == null) { + // delete + remove(menuEntry); + } + else { + // set text + ((Status) menuEntry).setText(statusText); + } + } else { + // create a new one + Status status = new Status(); + status.setText(statusText); + + // status is ALWAYS at 0 index... + // also calls the hook to add it, so we don't need anything special + add(status, 0); + } + } + } +} diff --git a/src/dorkbox/systemTray/jna/linux/Gtk.java b/src/dorkbox/systemTray/jna/linux/Gtk.java index 5a45a8f..de2bc00 100644 --- a/src/dorkbox/systemTray/jna/linux/Gtk.java +++ b/src/dorkbox/systemTray/jna/linux/Gtk.java @@ -59,6 +59,8 @@ class Gtk { private static final int TIMEOUT = 2; + private static final Object dispatchLock = new Object[0]; + // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0 | grep gtk // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 | grep gtk @@ -324,9 +326,12 @@ class Gtk { void run() { isDispatch = true; - runnable.run(); + try { + runnable.run(); + } finally { + isDispatch = false; + } - isDispatch = false; } }); } @@ -343,24 +348,27 @@ class Gtk { public int callback(final Pointer data) { synchronized (gtkCallbacks) { - gtkCallbacks.removeFirst();// now that we've 'handled' it, we can remove it from our callback list + gtkCallbacks.removeFirst(); // now that we've 'handled' it, we can remove it from our callback list + + isDispatch = true; + + try { + runnable.run(); + } finally { + isDispatch = false; + return Gtk.FALSE; // don't want to call this again + } } - - isDispatch = true; - - runnable.run(); - - isDispatch = false; - return Gtk.FALSE; // don't want to call this again } }; synchronized (gtkCallbacks) { gtkCallbacks.offer(callback); // prevent GC from collecting this object before it can be called + + // the correct way to do it. Add with a slightly higher value + gdk_threads_add_idle_full(100, callback, null, null); } - // the correct way to do it. Add with a slightly higher value - gdk_threads_add_idle_full(100, callback, null, null); } } } @@ -427,11 +435,9 @@ class Gtk { try { callback.actionPerformed(new ActionEvent(menuEntry, ActionEvent.ACTION_PERFORMED, "")); - } catch (Throwable throwable) { - SystemTray.logger.error("Error calling menu entry {} click event.", menuEntry.getText(), throwable); + } finally { + Gtk.isDispatch = false; } - - Gtk.isDispatch = false; } /** @@ -472,11 +478,11 @@ class Gtk { public static native Pointer gtk_image_menu_item_new_with_mnemonic(String label); public static native Pointer gtk_check_menu_item_new_with_mnemonic (String label); - public static native boolean gtk_check_menu_item_get_active (Pointer check_menu_item); + public static native void gtk_check_menu_item_set_active (Pointer check_menu_item, boolean isChecked); public static native void gtk_image_menu_item_set_image(Pointer image_menu_item, Pointer image); - public static native void gtk_image_menu_item_set_always_show_image(Pointer menu_item, int forceShow); + public static native void gtk_image_menu_item_set_always_show_image(Pointer menu_item, boolean forceShow); public static native Pointer gtk_status_icon_new(); @@ -499,7 +505,7 @@ class Gtk { public static native void gtk_menu_shell_deactivate(Pointer menu_shell, Pointer child); - public static native void gtk_widget_set_sensitive(Pointer widget, int sensitive); + public static native void gtk_widget_set_sensitive(Pointer widget, boolean sensitive); public static native void gtk_container_remove(Pointer menu, Pointer subItem); diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntry.java b/src/dorkbox/systemTray/nativeUI/AwtEntry.java index b46b240..2e583f7 100644 --- a/src/dorkbox/systemTray/nativeUI/AwtEntry.java +++ b/src/dorkbox/systemTray/nativeUI/AwtEntry.java @@ -23,16 +23,12 @@ import java.io.InputStream; import java.net.URL; import dorkbox.systemTray.Entry; -import dorkbox.systemTray.Menu; import dorkbox.systemTray.swingUI.SwingUI; -import dorkbox.systemTray.util.ImageUtils; -import dorkbox.systemTray.util.MenuBase; import dorkbox.systemTray.util.SystemTrayFixes; +import dorkbox.util.SwingUtil; abstract -class AwtEntry implements Entry, SwingUI { - private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement(); - +class AwtEntry extends Entry implements SwingUI { private final AwtMenu parent; final MenuItem _native; @@ -47,11 +43,10 @@ class AwtEntry implements Entry, SwingUI { parent._native.add(menuItem); } - @Override - public - Menu getParent() { - return parent; - } +// public +// Menu getParent() { +// return parent; +// } /** * must always be called in the EDT thread @@ -68,20 +63,18 @@ class AwtEntry implements Entry, SwingUI { /** * Enables, or disables the sub-menu entry. */ - @Override public void setEnabled(final boolean enabled) { _native.setEnabled(enabled); } - @Override public void setShortcut(final char key) { if (!(_native instanceof PopupMenu)) { // yikes... final int vKey = SystemTrayFixes.getVirtualKey(key); - parent.dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -91,18 +84,16 @@ class AwtEntry implements Entry, SwingUI { } } - @Override public String getText() { return text; } - @Override public void setText(final String newText) { this.text = newText; - parent.dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -111,65 +102,59 @@ class AwtEntry implements Entry, SwingUI { }); } - @Override public void setImage(final File imageFile) { if (imageFile == null) { setImage_(null); } else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile)); +// setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile)); } } - @Override public final void setImage(final String imagePath) { if (imagePath == null) { setImage_(null); } else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath)); +// setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath)); } } - @Override public final void setImage(final URL imageUrl) { if (imageUrl == null) { setImage_(null); } else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl)); +// setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl)); } } - @Override public final void setImage(final String cacheName, final InputStream imageStream) { if (imageStream == null) { setImage_(null); } else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream)); +// setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream)); } } - @Override public final void setImage(final InputStream imageStream) { if (imageStream == null) { setImage_(null); } else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream)); +// setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream)); } } - @Override public final void remove() { - parent.dispatchAndWait(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -182,28 +167,4 @@ class AwtEntry implements Entry, SwingUI { // called when this item is removed. Necessary to cleanup/remove itself abstract void removePrivate(); - - @Override - public final - int hashCode() { - return id; - } - - - @Override - public final - boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - - AwtEntry other = (AwtEntry) obj; - return this.id == other.id; - } } diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntryCheckbox.java b/src/dorkbox/systemTray/nativeUI/AwtEntryCheckbox.java deleted file mode 100644 index 3697950..0000000 --- a/src/dorkbox/systemTray/nativeUI/AwtEntryCheckbox.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.nativeUI; - -import java.awt.CheckboxMenuItem; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; - -import dorkbox.systemTray.Checkbox; -import dorkbox.systemTray.SystemTray; - -class AwtEntryCheckbox extends AwtEntry implements Checkbox { - - private final ActionListener swingCallback; - - private volatile ActionListener callback; - - // this is ALWAYS called on the EDT. - AwtEntryCheckbox(final AwtMenu parent, final ActionListener callback) { - super(parent, new java.awt.CheckboxMenuItem()); - this.callback = callback; - - if (callback != null) { - _native.setEnabled(true); - swingCallback = new ActionListener() { - @Override - public - void actionPerformed(ActionEvent e) { - // we want it to run on the EDT - handle(); - } - }; - - _native.addActionListener(swingCallback); - } else { - _native.setEnabled(false); - swingCallback = null; - } - } - - /** - * @return true if this checkbox is selected, false if not - */ - public - boolean getState() { - return ((CheckboxMenuItem) _native).getState(); - } - - @Override - public - void setCallback(final ActionListener callback) { - this.callback = callback; - } - - private - void handle() { - ActionListener cb = this.callback; - if (cb != null) { - try { - cb.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")); - } catch (Throwable throwable) { - SystemTray.logger.error("Error calling menu entry {} click event.", getText(), throwable); - } - } - } - - // always called in the EDT - @Override - void renderText(final String text) { - _native.setLabel(text); - } - - - // not supported! - @Override - public - boolean hasImage() { - return false; - } - - // not supported! - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { - _native.removeActionListener(swingCallback); - } -} diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntryItem.java b/src/dorkbox/systemTray/nativeUI/AwtEntryItem.java deleted file mode 100644 index 70d7183..0000000 --- a/src/dorkbox/systemTray/nativeUI/AwtEntryItem.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.nativeUI; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; - -import dorkbox.systemTray.SystemTray; - -class AwtEntryItem extends AwtEntry { - - private final ActionListener swingCallback; - - private volatile ActionListener callback; - - // this is ALWAYS called on the EDT. - AwtEntryItem(final AwtMenu parent, final ActionListener callback) { - super(parent, new java.awt.MenuItem()); - this.callback = callback; - - - if (callback != null) { - _native.setEnabled(true); - swingCallback = new ActionListener() { - @Override - public - void actionPerformed(ActionEvent e) { - // we want it to run on the EDT - handle(); - } - }; - - _native.addActionListener(swingCallback); - } else { - _native.setEnabled(false); - swingCallback = null; - } - } - - @Override - public - void setCallback(final ActionListener callback) { - this.callback = callback; - } - - private - void handle() { - ActionListener cb = this.callback; - if (cb != null) { - try { - cb.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")); - } catch (Throwable throwable) { - SystemTray.logger.error("Error calling menu entry {} click event.", getText(), throwable); - } - } - } - - // always called in the EDT - @Override - void renderText(final String text) { - _native.setLabel(text); - } - - - // not supported! - @Override - public - boolean hasImage() { - return false; - } - - // not supported! - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { - _native.removeActionListener(swingCallback); - } -} diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java b/src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java deleted file mode 100644 index 764cafc..0000000 --- a/src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.nativeUI; - -import static java.awt.Font.DIALOG; - -import java.awt.Font; -import java.awt.MenuItem; -import java.awt.event.ActionListener; -import java.io.File; - -import dorkbox.systemTray.Status; - -class AwtEntryStatus extends AwtEntry implements Status { - - // this is ALWAYS called on the EDT. - AwtEntryStatus(final AwtMenu parent, final String label) { - super(parent, new MenuItem()); - setText(label); - } - - // called in the EDT thread - @Override - void renderText(final String text) { - Font font = _native.getFont(); - if (font == null) { - font = new Font(DIALOG, Font.BOLD, 12); // the default font used for dialogs. - } else { - font = font.deriveFont(Font.BOLD); - } - - _native.setFont(font); - _native.setLabel(text); - - // this makes sure it can't be selected - _native.setEnabled(false); - } - - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { - } - - @Override - public - void setShortcut(final char key) { - } - - @Override - public - boolean hasImage() { - return false; - } - - @Override - public - void setCallback(final ActionListener callback) { - } -} diff --git a/src/dorkbox/systemTray/nativeUI/AwtMenu.java b/src/dorkbox/systemTray/nativeUI/AwtMenu.java index a150567..5a0ce05 100644 --- a/src/dorkbox/systemTray/nativeUI/AwtMenu.java +++ b/src/dorkbox/systemTray/nativeUI/AwtMenu.java @@ -18,319 +18,133 @@ package dorkbox.systemTray.nativeUI; import java.awt.MenuShortcut; import java.awt.PopupMenu; -import java.awt.event.ActionListener; -import java.io.File; -import java.util.concurrent.atomic.AtomicReference; import dorkbox.systemTray.Checkbox; import dorkbox.systemTray.Entry; import dorkbox.systemTray.Menu; -import dorkbox.systemTray.Status; -import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.util.MenuBase; +import dorkbox.systemTray.MenuItem; +import dorkbox.systemTray.Separator; +import dorkbox.systemTray.util.MenuHook; +import dorkbox.systemTray.util.Status; import dorkbox.systemTray.util.SystemTrayFixes; import dorkbox.util.SwingUtil; // this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both @SuppressWarnings("ForLoopReplaceableByForEach") -class AwtMenu extends MenuBase implements NativeUI { +class AwtMenu implements MenuHook { - // sub-menu = java.awt.Menu - // systemtray = java.awt.PopupMenu volatile java.awt.Menu _native; + private final AwtMenu parent; - // this have to be volatile, because they can be changed from any thread - private volatile String text; + // This is NOT a copy constructor! + @SuppressWarnings("IncompleteCopyConstructor") + AwtMenu(final AwtMenu parent) { + this.parent = parent; - /** - * Called in the EDT - * - * @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 - * @param _native the native element that represents this menu - */ - AwtMenu(final SystemTray systemTray, final Menu parent, final java.awt.Menu _native) { - super(systemTray, parent); - this._native = _native; - } - - @Override - protected final - void dispatch(final Runnable runnable) { - // this will properly check if we are running on the EDT - SwingUtil.invokeLater(runnable); - } - - @Override - protected final - void dispatchAndWait(final Runnable runnable) { - // this will properly check if we are running on the EDT - try { - SwingUtil.invokeAndWait(runnable); - } catch (Exception e) { - SystemTray.logger.error("Error processing event on the dispatch thread.", e); + if (parent == null) { + this._native = new PopupMenu(); + } + else { + this._native = new java.awt.Menu(); + parent._native.add(this._native); } } - // always called in the EDT - protected final - void renderText(final String text) { - _native.setLabel(text); - } - - @Override - public final - String getText() { - return text; - } - - @Override - public final - void setText(final String newText) { - text = newText; - dispatch(new Runnable() { - @Override - public - void run() { - renderText(newText); - } - }); - } - - /** - * Will add a new menu entry - * NOT ALWAYS CALLED ON EDT - */ - protected final - Entry addEntry_(final String menuText, final File imagePath, final ActionListener callback) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - final AtomicReference value = new AtomicReference(); - - // must always be called on the EDT - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - Entry entry = entry = new AwtEntryItem(AwtMenu.this, callback); - entry.setText(menuText); - entry.setImage(imagePath); - - menuEntries.add(entry); - value.set(entry); - } - } - }); - - return value.get(); - } - - /** - * Will add a new checkbox menu entry - * NOT ALWAYS CALLED ON DISPATCH - */ - @Override - protected - Checkbox addCheckbox_(final String menuText, final ActionListener callback) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - final AtomicReference value = new AtomicReference(); - - // must always be called on the EDT - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - Entry entry = new AwtEntryCheckbox(AwtMenu.this, callback); - entry.setText(menuText); - - menuEntries.add(entry); - value.set((Checkbox) entry); - } - } - }); - - return value.get(); - } - - - /** - * Will add a new sub-menu entry - * NOT ALWAYS CALLED ON EDT - */ - protected final - Menu addMenu_(final String menuText, final File imagePath) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - final AtomicReference

value = new AtomicReference(); - - // must always be called on the EDT - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - Entry entry = new AwtMenu(getSystemTray(), AwtMenu.this, new java.awt.Menu()); - _native.add(((AwtMenu) entry)._native); // have to add it to our native item separately - - entry.setText(menuText); - entry.setImage(imagePath); - - menuEntries.add(entry); - value.set((Menu) entry); - } - } - }); - - return value.get(); - } - - - - // public here so that Swing/Gtk/AppIndicator can override this - public - void setImage_(final File imageFile) { - // not supported! - } - - // not supported! @Override public - boolean hasImage() { - return false; + void add(final Menu parentMenu, final Entry entry, final int index) { + // must always be called on the EDT + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (entry instanceof Menu) { + AwtMenu swingMenu = new AwtMenu(AwtMenu.this); + ((Menu) entry).bind(swingMenu, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Separator) { + AwtMenuItemSeparator item = new AwtMenuItemSeparator(AwtMenu.this); + entry.bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Checkbox) { + AwtMenuItemCheckbox item = new AwtMenuItemCheckbox(AwtMenu.this); + ((Checkbox) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Status) { + AwtMenuItemStatus item = new AwtMenuItemStatus(AwtMenu.this); + ((Status) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof MenuItem) { + AwtMenuItem item = new AwtMenuItem(AwtMenu.this); + ((MenuItem) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + } + }); } - // public here so that Swing/Gtk/AppIndicator can override this @Override public - void setEnabled(final boolean enabled) { - dispatch(new Runnable() { - @Override - public - void run() { - _native.setEnabled(enabled); - } - }); + void setImage(final MenuItem menuItem) { + // no op. You can't have images in an awt menu } - - /** - * NOT ALWAYS CALLED ON EDT - */ @Override - public final - void addSeparator() { - dispatch(new Runnable() { + public + void setEnabled(final MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { - synchronized (menuEntries) { - synchronized (menuEntries) { - Entry entry = new AwtEntrySeparator(AwtMenu.this); - menuEntries.add(entry); - } - } - } - }); - } - -// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. -// public -// Entry addWidget(final JComponent widget) { -// if (widget == null) { -// throw new NullPointerException("Widget cannot be null"); -// } -// -// final AtomicReference value = new AtomicReference(); -// -// dispatchAndWait(new Runnable() { -// @Override -// public -// void run() { -// synchronized (menuEntries) { -// // must always be called on the EDT -// Entry entry = new SwingEntryWidget(SwingMenu.this, widget); -// value.set(entry); -// menuEntries.add(entry); -// } -// } -// }); -// -// return value.get(); -// } - - - // public here so that Swing/Gtk/AppIndicator can access this - public final - void setStatus(final String statusText) { - final AwtMenu _this = this; - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - // status is ALWAYS at 0 index... - AwtEntry menuEntry = null; - if (!menuEntries.isEmpty()) { - menuEntry = (AwtEntry) menuEntries.get(0); - } - - if (menuEntry instanceof Status) { - // set the text or delete... - - if (statusText == null) { - // delete - remove(menuEntry); - } - else { - // set text - menuEntry.setText(statusText); - } - - } else { - // create a new one - menuEntry = new AwtEntryStatus(_this, statusText); - // status is ALWAYS at 0 index... - menuEntries.add(0, menuEntry); - } - } + _native.setEnabled(menuItem.getEnabled()); } }); } @Override - public final - void setShortcut(final char key) { - if (!(_native instanceof PopupMenu)) { - // yikes... - final int vKey = SystemTrayFixes.getVirtualKey(key); - - dispatch(new Runnable() { - @Override - public - void run() { - _native.setShortcut(new MenuShortcut(vKey)); - } - }); - } + public + void setText(final MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setLabel(menuItem.getText()); + } + }); } @Override - public final + public + void setCallback(final MenuItem menuItem) { + // can't have a callback for menus! + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(menuItem.getShortcut()); + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setShortcut(new MenuShortcut(vKey)); + } + }); + } + + @Override + public void remove() { - dispatchAndWait(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { - AwtMenu parent = (AwtMenu) getParent(); + _native.removeAll(); + _native.deleteShortcut(); + _native.setEnabled(false); + _native.removeNotify(); + if (parent != null) { parent._native.remove(_native); } diff --git a/src/dorkbox/systemTray/nativeUI/AwtMenuItem.java b/src/dorkbox/systemTray/nativeUI/AwtMenuItem.java new file mode 100644 index 0000000..bbf1137 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/AwtMenuItem.java @@ -0,0 +1,139 @@ +/* + * 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.nativeUI; + +import java.awt.MenuItem; +import java.awt.MenuShortcut; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.util.MenuItemHook; +import dorkbox.systemTray.util.SystemTrayFixes; +import dorkbox.util.SwingUtil; + +class AwtMenuItem implements MenuItemHook { + + private final AwtMenu parent; + private final MenuItem _native = new java.awt.MenuItem(); + + private volatile ActionListener swingCallback; + + // this is ALWAYS called on the EDT. + AwtMenuItem(final AwtMenu parent) { + this.parent = parent; + parent._native.add(_native); + } + + @Override + public + void setImage(final dorkbox.systemTray.MenuItem menuItem) { + // no op. (awt menus cannot show images) + } + + @Override + public + void setEnabled(final dorkbox.systemTray.MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setEnabled(menuItem.getEnabled()); + } + }); + } + + @Override + public + void setText(final dorkbox.systemTray.MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setLabel(menuItem.getText()); + } + }); + } + + @SuppressWarnings("Duplicates") + @Override + public + void setCallback(final dorkbox.systemTray.MenuItem menuItem) { + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + } + + if (menuItem.getCallback() != null) { + swingCallback = new ActionListener() { + @Override + public + void actionPerformed(ActionEvent e) { + // we want it to run on the EDT, but with our own action event info (so it is consistent across all platforms) + ActionListener cb = menuItem.getCallback(); + if (cb != null) { + try { + cb.actionPerformed(new ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, "")); + } catch (Throwable throwable) { + SystemTray.logger.error("Error calling menu entry {} click event.", menuItem.getText(), throwable); + } + } + } + }; + + _native.addActionListener(swingCallback); + } + else { + swingCallback = null; + } + } + + @Override + public + void setShortcut(final dorkbox.systemTray.MenuItem menuItem) { + char shortcut = menuItem.getShortcut(); + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(shortcut); + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setShortcut(new MenuShortcut(vKey)); + } + }); + } + + @Override + public + void remove() { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.deleteShortcut(); + _native.setEnabled(false); + + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + swingCallback = null; + } + parent._native.remove(_native); + + _native.removeNotify(); + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/AwtMenuItemCheckbox.java b/src/dorkbox/systemTray/nativeUI/AwtMenuItemCheckbox.java new file mode 100644 index 0000000..4d80f24 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/AwtMenuItemCheckbox.java @@ -0,0 +1,146 @@ +/* + * 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.nativeUI; + +import java.awt.MenuShortcut; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import dorkbox.systemTray.Checkbox; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.util.MenuCheckboxHook; +import dorkbox.systemTray.util.SystemTrayFixes; +import dorkbox.util.SwingUtil; + +class AwtMenuItemCheckbox implements MenuCheckboxHook { + + private final AwtMenu parent; + private final java.awt.CheckboxMenuItem _native = new java.awt.CheckboxMenuItem(); + + private volatile ActionListener swingCallback; + + + // this is ALWAYS called on the EDT. + AwtMenuItemCheckbox(final AwtMenu parent) { + this.parent = parent; + } + + @Override + public + void setEnabled(final Checkbox menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setEnabled(menuItem.getEnabled()); + } + }); + } + + @Override + public + void setText(final Checkbox menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setLabel(menuItem.getText()); + } + }); + } + + @SuppressWarnings("Duplicates") + @Override + public + void setCallback(final Checkbox menuItem) { + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + } + + if (menuItem.getCallback() != null) { + swingCallback = new ActionListener() { + @Override + public + void actionPerformed(ActionEvent e) { + // we want it to run on the EDT, but with our own action event info (so it is consistent across all platforms) + ActionListener cb = menuItem.getCallback(); + if (cb != null) { + try { + cb.actionPerformed(new ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, "")); + } catch (Throwable throwable) { + SystemTray.logger.error("Error calling menu entry {} click event.", menuItem.getText(), throwable); + } + } + } + }; + + _native.addActionListener(swingCallback); + } + else { + swingCallback = null; + } + } + + @Override + public + void setShortcut(final Checkbox menuItem) { + char shortcut = menuItem.getShortcut(); + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(shortcut); + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setShortcut(new MenuShortcut(vKey)); + } + }); + } + + @Override + public + void setChecked(final Checkbox checkbox) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setState(checkbox.getChecked()); + } + }); + } + + @SuppressWarnings("Duplicates") + @Override + public + void remove() { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.deleteShortcut(); + _native.setEnabled(false); + + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + swingCallback = null; + } + parent._native.remove(_native); + + _native.removeNotify(); + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java b/src/dorkbox/systemTray/nativeUI/AwtMenuItemSeparator.java similarity index 53% rename from src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java rename to src/dorkbox/systemTray/nativeUI/AwtMenuItemSeparator.java index c6addf3..141c9af 100644 --- a/src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java +++ b/src/dorkbox/systemTray/nativeUI/AwtMenuItemSeparator.java @@ -16,42 +16,30 @@ package dorkbox.systemTray.nativeUI; import java.awt.MenuItem; -import java.awt.event.ActionListener; -import java.io.File; -class AwtEntrySeparator extends AwtEntry implements dorkbox.systemTray.Separator { +import dorkbox.systemTray.util.EntryHook; +import dorkbox.util.SwingUtil; + +class AwtMenuItemSeparator implements EntryHook { + + private final AwtMenu parent; + private final MenuItem _native = new MenuItem("-"); + // this is ALWAYS called on the EDT. - AwtEntrySeparator(final AwtMenu parent) { - super(parent, new MenuItem("-")); - } - - // called in the EDT thread - @Override - void renderText(final String text) { - } - - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { + AwtMenuItemSeparator(final AwtMenu parent) { + this.parent = parent; } @Override public - void setShortcut(final char key) { - } - - @Override - public - boolean hasImage() { - return false; - } - - @Override - public - void setCallback(final ActionListener callback) { + void remove() { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + parent._native.remove(_native); + } + }); } } diff --git a/src/dorkbox/systemTray/nativeUI/AwtMenuItemStatus.java b/src/dorkbox/systemTray/nativeUI/AwtMenuItemStatus.java new file mode 100644 index 0000000..cda6c3b --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/AwtMenuItemStatus.java @@ -0,0 +1,74 @@ +/* + * 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.nativeUI; + +import static java.awt.Font.DIALOG; + +import java.awt.Font; +import java.awt.MenuItem; + +import dorkbox.systemTray.util.MenuStatusHook; +import dorkbox.systemTray.util.Status; +import dorkbox.util.SwingUtil; + +class AwtMenuItemStatus implements MenuStatusHook { + + private final AwtMenu parent; + private final MenuItem _native = new MenuItem(); + + AwtMenuItemStatus(final AwtMenu parent) { + this.parent = parent; + + // status is ALWAYS at 0 index... + parent._native.insert(_native, 0); + } + + @Override + public + void setText(final Status menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + Font font = _native.getFont(); + if (font == null) { + font = new Font(DIALOG, Font.BOLD, 12); // the default font used for dialogs. + } + else { + font = font.deriveFont(Font.BOLD); + } + + _native.setFont(font); + _native.setLabel(menuItem.getText()); + + // this makes sure it can't be selected + _native.setEnabled(false); + } + }); + } + + @Override + public + void remove() { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + parent._native.remove(_native); + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntry.java b/src/dorkbox/systemTray/nativeUI/GtkEntry.java deleted file mode 100644 index 1a255bc..0000000 --- a/src/dorkbox/systemTray/nativeUI/GtkEntry.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * 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.nativeUI; - -import java.io.File; -import java.io.InputStream; -import java.net.URL; - -import com.sun.jna.Pointer; - -import dorkbox.systemTray.Entry; -import dorkbox.systemTray.Menu; -import dorkbox.systemTray.jna.linux.Gtk; -import dorkbox.systemTray.util.ImageUtils; - -abstract -class GtkEntry implements Entry { - private final int id = GtkMenu.MENU_ID_COUNTER.getAndIncrement(); - - private final GtkMenu parent; - final Pointer _native; - - // this have to be volatile, because they can be changed from any thread - private volatile String text; - - /** - * 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 - */ - GtkEntry(final GtkMenu parent, final Pointer menuItem) { - this.parent = parent; - this._native = menuItem; - } - - public - Menu getParent() { - return parent; - } - - /** - * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images - * - * always called on the DISPATCH thread - */ - abstract - void setSpacerImage(final boolean everyoneElseHasImages); - - /** - * must always be called in the GTK thread - */ - abstract - void renderText(final String text); - - /** - * must always be called in the GTK thread - */ - abstract - void setImage_(final File imageFile); - - /** - * must always be called in the GTK thread - * called when this item is removed. Necessary to cleanup/remove itself - */ - abstract - void removePrivate(); - - /** - * Enables, or disables the sub-menu entry. - */ - @Override - public - void setEnabled(final boolean enabled) { - if (enabled) { - Gtk.gtk_widget_set_sensitive(_native, Gtk.TRUE); - } else { - Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE); - } - } - - @Override - public - void setShortcut(final char key) { - } - - @Override - public - String getText() { - return text; - } - - @Override - public final - void setText(final String newText) { - text = newText; - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - renderText(text); - } - }); - } - - @Override - public - void setImage(final File imageFile) { - if (imageFile == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile)); - } - } - - @Override - public final - void setImage(final String imagePath) { - if (imagePath == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath)); - } - } - - @Override - public final - void setImage(final URL imageUrl) { - if (imageUrl == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl)); - } - } - - @Override - public final - void setImage(final String cacheName, final InputStream imageStream) { - if (imageStream == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream)); - } - } - - @Override - public final - void setImage(final InputStream imageStream) { - if (imageStream == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream)); - } - } - - // a child will always remove itself from the parent. - @Override - public final - void remove() { - parent.dispatchAndWait(new Runnable() { - @Override - public - void run() { - Gtk.gtk_container_remove(parent._native, _native); - Gtk.gtk_menu_shell_deactivate(parent._native, _native); - - removePrivate(); - - Gtk.gtk_widget_destroy(_native); - - // have to rebuild the menu now... - parent.deleteMenu(); - parent.createMenu(); - } - }); - } - - @Override - public final - int hashCode() { - return id; - } - - - @Override - public final - boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - - GtkEntry other = (GtkEntry) obj; - return this.id == other.id; - } -} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntryCheckbox.java b/src/dorkbox/systemTray/nativeUI/GtkEntryCheckbox.java deleted file mode 100644 index bed76d9..0000000 --- a/src/dorkbox/systemTray/nativeUI/GtkEntryCheckbox.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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.nativeUI; - -import java.awt.event.ActionListener; -import java.io.File; - -import com.sun.jna.NativeLong; -import com.sun.jna.Pointer; - -import dorkbox.systemTray.Checkbox; -import dorkbox.systemTray.jna.linux.GCallback; -import dorkbox.systemTray.jna.linux.Gobject; -import dorkbox.systemTray.jna.linux.Gtk; -import dorkbox.systemTray.util.ImageUtils; - -class GtkEntryCheckbox extends GtkEntry implements GCallback, Checkbox { - private static File transparentIcon = null; - - @SuppressWarnings({"FieldCanBeLocal", "unused"}) - private final NativeLong nativeLong; - - // these have to be volatile, because they can be changed from any thread - private volatile ActionListener callback; - private volatile Pointer image; - - // The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then! - // AppIndicators will only show if you use the keyboard to navigate - // GtkStatusIconTray will show on mouse+keyboard movement - private volatile char mnemonicKey = 0; - - /** - * 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 - */ - GtkEntryCheckbox(final GtkMenu parent, final ActionListener callback) { - super(parent, Gtk.gtk_check_menu_item_new_with_mnemonic("")); - this.callback = callback; - - // cannot be done in a static initializer, because the tray icon size might not yet have been determined - if (transparentIcon == null) { - transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); - } - - if (callback != null) { - 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; - } - } - - @Override - public - void setShortcut(final char key) { - this.mnemonicKey = Character.toLowerCase(key); - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - renderText(getText()); - } - }); - } - - /** - * @return true if this checkbox is selected, false if not - */ - public - boolean getState() { - return Gtk.gtk_check_menu_item_get_active(_native); - } - - @Override - public - void setCallback(final ActionListener callback) { - this.callback = callback; - } - - // called by native code - @Override - public - int callback(final Pointer instance, final Pointer data) { - final ActionListener cb = this.callback; - if (cb != null) { - Gtk.proxyClick(GtkEntryCheckbox.this, cb); - } - - return Gtk.TRUE; - } - - @Override - public - boolean hasImage() { - return true; - } - - /** - * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images. - * This is primarily only with AppIndicators, although not always. - *

- * called on the DISPATCH thread - */ - void setSpacerImage(final boolean everyoneElseHasImages) { -// if (true) { -// // we have a legit icon, so there is nothing else we can do. -// return; -// } -// -// if (image != null) { -// Gtk.gtk_widget_destroy(image); -// image = null; -// Gtk.gtk_widget_show_all(_native); -// } -// -// if (everyoneElseHasImages) { -// image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath()); -// 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(_native, Gtk.TRUE); -// } -// -// Gtk.gtk_widget_show_all(_native); - } - - /** - * must always be called in the GTK thread - */ - void renderText(String text) { - if (this.mnemonicKey != 0) { - // they are CASE INSENSITIVE! - int i = text.toLowerCase() - .indexOf(this.mnemonicKey); - - if (i >= 0) { - text = text.substring(0, i) + "_" + text.substring(i); - } - } - - Gtk.gtk_menu_item_set_label(_native, text); - Gtk.gtk_widget_show_all(_native); - } - - void setImage_(final File imageFile) { - } - - void removePrivate() { - callback = null; - - if (image != null) { - Gtk.gtk_widget_destroy(image); - image = null; - } - } -} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntryItem.java b/src/dorkbox/systemTray/nativeUI/GtkEntryItem.java deleted file mode 100644 index 735ab16..0000000 --- a/src/dorkbox/systemTray/nativeUI/GtkEntryItem.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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.nativeUI; - -import java.awt.event.ActionListener; -import java.io.File; - -import com.sun.jna.NativeLong; -import com.sun.jna.Pointer; - -import dorkbox.systemTray.jna.linux.GCallback; -import dorkbox.systemTray.jna.linux.Gobject; -import dorkbox.systemTray.jna.linux.Gtk; -import dorkbox.systemTray.util.ImageUtils; - -class GtkEntryItem extends GtkEntry implements GCallback { - private static File transparentIcon = null; - - @SuppressWarnings({"FieldCanBeLocal", "unused"}) - private final NativeLong nativeLong; - - // these have to be volatile, because they can be changed from any thread - private volatile ActionListener callback; - private volatile Pointer image; - - // these are necessary BECAUSE GTK menus look funky as hell when there are some menu entries WITH icons and some WITHOUT - protected volatile boolean hasLegitIcon = true; - - // The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then! - // AppIndicators will only show if you use the keyboard to navigate - // GtkStatusIconTray will show on mouse+keyboard movement - private volatile char mnemonicKey = 0; - - /** - * 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 - */ - GtkEntryItem(final GtkMenu parent, final ActionListener callback) { - super(parent, Gtk.gtk_image_menu_item_new_with_mnemonic("")); - this.callback = callback; - - // cannot be done in a static initializer, because the tray icon size might not yet have been determined - if (transparentIcon == null) { - transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); - } - - if (callback != null) { - 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; - } - } - - @Override - public - void setShortcut(final char key) { - this.mnemonicKey = Character.toLowerCase(key); - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - renderText(getText()); - } - }); - } - - @Override - public - void setCallback(final ActionListener callback) { - this.callback = callback; - } - - // called by native code - @Override - public - int callback(final Pointer instance, final Pointer data) { - final ActionListener cb = this.callback; - if (cb != null) { - Gtk.proxyClick(GtkEntryItem.this, cb); - } - - return Gtk.TRUE; - } - - @Override - public - boolean hasImage() { - return hasLegitIcon; - } - - /** - * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images. - * This is primarily only with AppIndicators, although not always. - *

- * called on the DISPATCH thread - */ - void setSpacerImage(final boolean everyoneElseHasImages) { - if (hasLegitIcon) { - // we have a legit icon, so there is nothing else we can do. - return; - } - - if (image != null) { - Gtk.gtk_widget_destroy(image); - image = null; - Gtk.gtk_widget_show_all(_native); - } - - if (everyoneElseHasImages) { - image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath()); - 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(_native, Gtk.TRUE); - } - - Gtk.gtk_widget_show_all(_native); - } - - /** - * must always be called in the GTK thread - */ - void renderText(String text) { - if (this.mnemonicKey != 0) { - // they are CASE INSENSITIVE! - int i = text.toLowerCase() - .indexOf(this.mnemonicKey); - - if (i >= 0) { - text = text.substring(0, i) + "_" + text.substring(i); - } - } - - 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. - // see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/ - // see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25 - void setImage_(final File imageFile) { - hasLegitIcon = imageFile != null; - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - if (image != null) { - Gtk.gtk_widget_destroy(image); - image = null; - 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(_native, image); - - // must always re-set always-show after setting the image - Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE); - } - - Gtk.gtk_widget_show_all(_native); - } - }); - } - - void removePrivate() { - callback = null; - - if (image != null) { - Gtk.gtk_widget_destroy(image); - image = null; - } - } -} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java b/src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java deleted file mode 100644 index 19cf8fb..0000000 --- a/src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.nativeUI; - -import java.awt.event.ActionListener; -import java.io.File; - -import dorkbox.systemTray.Separator; -import dorkbox.systemTray.jna.linux.Gtk; - -class GtkEntrySeparator extends GtkEntry implements Separator { - - /** - * 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 - */ - GtkEntrySeparator(final GtkMenu parent) { - super(parent, Gtk.gtk_separator_menu_item_new()); - } - - @Override - void setSpacerImage(final boolean everyoneElseHasImages) { - } - - // called in the GTK thread - @Override - void renderText(final String text) { - } - - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { - } - - @Override - public - boolean hasImage() { - return false; - } - - @Override - public - void setCallback(final ActionListener callback) { - } - - @Override - public - void setEnabled(final boolean enabled) { - } -} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java b/src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java deleted file mode 100644 index e6a25dc..0000000 --- a/src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.nativeUI; - -import java.awt.event.ActionListener; - -import dorkbox.systemTray.jna.linux.Gtk; - -// you might wonder WHY this extends MenuEntryItem -- the reason is that an AppIndicator "status" will be offset from everyone else, -// where a GtkStatusIconTray + SwingUI will have everything lined up. (with or without icons). This is to normalize how it looks -class GtkEntryStatus extends GtkEntryItem { - - /** - * 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 - */ - GtkEntryStatus(final GtkMenu parent, final String text) { - super(parent, null); - // need that extra space so it matches windows/mac - hasLegitIcon = false; - setText(text); - } - - // called in the GTK thread - @Override - void renderText(final String text) { - // AppIndicator strips out markup text. - // https://mail.gnome.org/archives/commits-list/2016-March/msg05444.html - - Gtk.gtk_menu_item_set_label(_native, text); - Gtk.gtk_widget_show_all(_native); - - Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE); - } - - @Override - public - void setCallback(final ActionListener callback) { - } - - @Override - public - void setEnabled(final boolean enabled) { - } -} diff --git a/src/dorkbox/systemTray/nativeUI/GtkMenu.java b/src/dorkbox/systemTray/nativeUI/GtkMenu.java index 8b979fc..ac18ba8 100644 --- a/src/dorkbox/systemTray/nativeUI/GtkMenu.java +++ b/src/dorkbox/systemTray/nativeUI/GtkMenu.java @@ -16,42 +16,61 @@ package dorkbox.systemTray.nativeUI; -import java.awt.event.ActionListener; -import java.io.File; import java.util.ArrayList; -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicReference; +import java.util.LinkedList; +import java.util.List; import com.sun.jna.Pointer; import dorkbox.systemTray.Checkbox; import dorkbox.systemTray.Entry; import dorkbox.systemTray.Menu; -import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.jna.linux.Gobject; +import dorkbox.systemTray.MenuItem; +import dorkbox.systemTray.Separator; import dorkbox.systemTray.jna.linux.Gtk; -import dorkbox.systemTray.util.MenuBase; +import dorkbox.systemTray.util.MenuHook; +import dorkbox.systemTray.util.Status; -class GtkMenu extends MenuBase implements NativeUI { - // menu entry that this menu is attached to. Will be NULL when it's the system tray - private final GtkEntryItem menuEntry; +class GtkMenu extends GtkMenuBaseItem implements MenuHook { + // this is a list (that mirrors the actual list) BECAUSE we have to create/delete the entire menu in GTK every time something is changed + private final List menuEntries = new LinkedList(); - // must ONLY be created at the end of delete! - volatile Pointer _native; + private final GtkMenu parent; + volatile Pointer _nativeMenu; // must ONLY be created at the end of delete! + + private final Pointer _nativeEntry; // is what is added to the parent menu, if we are NOT on the system tray + private volatile Pointer image; + + // The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then! + // AppIndicators will only show if you use the keyboard to navigate + // GtkStatusIconTray will show on mouse+keyboard movement + private volatile char mnemonicKey = 0; // have to make sure no other methods can call obliterate, delete, or create menu once it's already started - private boolean obliterateInProgress = false; + private volatile boolean obliterateInProgress = false; - // called on dispatch - GtkMenu(final SystemTray systemTray, final GtkMenu parent) { - super(systemTray, parent); + // This is NOT a copy constructor! + @SuppressWarnings("IncompleteCopyConstructor") + GtkMenu(final GtkMenu parent) { + this.parent = parent; if (parent != null) { - this.menuEntry = new GtkEntryItem(parent, null); - // by default, no callback on a menu entry means it's DISABLED. we have to undo that, because we don't have a callback for menus - menuEntry.setEnabled(true); + _nativeEntry = Gtk.gtk_image_menu_item_new_with_mnemonic(""); // is what is added to the parent menu } else { - this.menuEntry = null; + _nativeEntry = null; + } + } + + GtkMenu getParent() { + return parent; + } + + private + void add(final GtkMenuBaseItem item, final int index) { + if (index > 0) { + menuEntries.add(index, item); + } else { + menuEntries.add(item); } } @@ -63,323 +82,53 @@ class GtkMenu extends MenuBase implements NativeUI { // only needed for AppIndicator } - /** - * Will add a new menu entry - * NOT ALWAYS CALLED ON DISPATCH - */ - protected - Entry addEntry_(final String menuText, final File imagePath, final ActionListener 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"); - } - - // have to wait for the value - final AtomicReference value = new AtomicReference(); - - // must always be called on DISPATCH - dispatchAndWait(new Runnable() { - @Override - public - void run() { - 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(); - - Entry menuEntry = new GtkEntryItem(GtkMenu.this, callback); - menuEntry.setText(menuText); - menuEntry.setImage(imagePath); - - menuEntries.add(menuEntry); - value.set(menuEntry); - - createMenu(); - } - } - }); - - return value.get(); - } - - /** - * Will add a new checkbox menu entry - * NOT ALWAYS CALLED ON DISPATCH - */ - @Override - protected - Checkbox addCheckbox_(final String menuText, final ActionListener callback) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - final AtomicReference value = new AtomicReference(); - - // must always be called on DISPATCH - dispatchAndWait(new Runnable() { - @Override - public - void run() { - 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(); - - Entry entry = new GtkEntryCheckbox(GtkMenu.this, callback); - entry.setText(menuText); - - menuEntries.add(entry); - value.set((Checkbox) entry); - - createMenu(); - } - } - }); - - return value.get(); - } - - - /** - * Will add a new menu entry - * NOT ALWAYS CALLED ON DISPATCH - */ - protected - Menu addMenu_(final String menuText, final File imagePath) { - // 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"); - } - - final AtomicReference

value = new AtomicReference(); - - // must always be called on DISPATCH - dispatchAndWait(new Runnable() { - @Override - public - void run() { - 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(); - - GtkMenu subMenu = new GtkMenu(getSystemTray(), GtkMenu.this); - subMenu.setText(menuText); - subMenu.setImage(imagePath); - - menuEntries.add(subMenu); - value.set(subMenu); - - createMenu(); - } - } - }); - - return value.get(); - } - - - - /** - * Necessary to guarantee all updates occur on the dispatch thread - */ - protected - void dispatch(final Runnable runnable) { - Gtk.dispatch(runnable); - } - - /** - * Necessary to guarantee all updates occur on the dispatch thread - */ - protected - void dispatchAndWait(final Runnable runnable) { - Gtk.dispatchAndWait(runnable); - } - - public - void shutdown() { - dispatch(new Runnable() { - @Override - public - void run() { - obliterateMenu(); - } - }); - - // does not need to be called on the dispatch (it does that) - Gtk.shutdownGui(); - } - - // public here so that Swing/Gtk/AppIndicator can access this - public final - void setStatus(final String statusText) { - 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) { - // status is ALWAYS at 0 index... - GtkEntry menuEntry = null; - if (!menuEntries.isEmpty()) { - menuEntry = (GtkEntry) menuEntries.get(0); - } - - if (menuEntry instanceof GtkEntryStatus) { - // always delete... - remove(menuEntry); - } - - // 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(); - - if (menuEntry == null) { - menuEntry = new GtkEntryStatus(GtkMenu.this, statusText); - // status is ALWAYS at 0 index... - menuEntries.add(0, menuEntry); - } - else if (menuEntry instanceof GtkEntryStatus) { - // change the text? - if (statusText != null) { - menuEntry = new GtkEntryStatus(GtkMenu.this, statusText); - menuEntries.add(0, menuEntry); - } - } - - createMenu(); - } - } - }); - } - - // public here so that Swing/Gtk/AppIndicator can override this - @Override - public - boolean hasImage() { - return menuEntry.hasImage(); - } - - - // public here so that Swing/Gtk/AppIndicator can override this - @Override - protected - void setImage_(final File imageFile) { - menuEntry.setImage_(imageFile); - } - - // public here so that Swing/Gtk/AppIndicator can override this - @Override - public - void setEnabled(final boolean enabled) { - if (enabled) { - Gtk.gtk_widget_set_sensitive(menuEntry._native, Gtk.TRUE); - } else { - Gtk.gtk_widget_set_sensitive(menuEntry._native, Gtk.FALSE); - } - } - - @Override - public - String getText() { - return menuEntry.getText(); - } - - @Override - public - void setText(final String newText) { - menuEntry.setText(newText); - } - - @Override - public final - void addSeparator() { - 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(); - - GtkEntry menuEntry = new GtkEntrySeparator(GtkMenu.this); - menuEntries.add(menuEntry); - - createMenu(); - } - } - }); - } - - @Override - public final - void setShortcut(final char key) { - menuEntry.setShortcut(key); - } // 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. */ + private void deleteMenu() { if (obliterateInProgress) { return; } - if (_native != null) { + if (_nativeMenu != null) { // have to remove all other menu entries synchronized (menuEntries) { for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { - final Entry menuEntry__ = menuEntries.get(i); - if (menuEntry__ instanceof GtkEntry) { - GtkEntry entry = (GtkEntry) menuEntry__; - - Gobject.g_object_force_floating(entry._native); - Gtk.gtk_container_remove(_native, entry._native); - } - else if (menuEntry__ instanceof GtkMenu) { - GtkMenu subMenu = (GtkMenu) menuEntry__; - - Gobject.g_object_force_floating(subMenu.menuEntry._native); - Gtk.gtk_container_remove(_native, subMenu.menuEntry._native); - } + final GtkMenuBaseItem menuEntry__ = menuEntries.get(i); + menuEntry__.onDeleteMenu(_nativeMenu); } - Gtk.gtk_widget_destroy(_native); + Gtk.gtk_widget_destroy(_nativeMenu); } } - if (getParent() != null) { - ((GtkMenu) getParent()).deleteMenu(); + if (parent != null) { + parent.deleteMenu(); } // makes a new one - _native = Gtk.gtk_menu_new(); + _nativeMenu = Gtk.gtk_menu_new(); // binds sub-menu to entry (if it exists! it does not for the root menu) - if (menuEntry != null) { - Gtk.gtk_menu_item_set_submenu(menuEntry._native, _native); + if (parent != null) { + Gtk.gtk_menu_item_set_submenu(_nativeEntry, _nativeMenu); } } // 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. + private void createMenu() { if (obliterateInProgress) { return; } - if (getParent() != null) { - ((GtkMenu) getParent()).createMenu(); + if (parent != null) { + parent.createMenu(); } boolean hasImages = false; @@ -387,30 +136,17 @@ class GtkMenu extends MenuBase implements NativeUI { // now add back other menu entries synchronized (menuEntries) { for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { - final Entry menuEntry__ = menuEntries.get(i); + final GtkMenuBaseItem menuEntry__ = menuEntries.get(i); hasImages |= menuEntry__.hasImage(); } for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { - final Entry menuEntry__ = menuEntries.get(i); // the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images - if (menuEntry__ instanceof GtkEntry) { - GtkEntry entry = (GtkEntry) menuEntry__; - entry.setSpacerImage(hasImages); + final GtkMenuBaseItem menuEntry__ = menuEntries.get(i); + menuEntry__.onCreateMenu(_nativeMenu, 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, entry._native); - Gobject.g_object_ref_sink(entry._native); // undoes "floating" - Gtk.gtk_widget_show_all(entry._native); // necessary to guarantee widget is visible - } - else if (menuEntry__ instanceof GtkMenu) { + if (menuEntry__ instanceof GtkMenu) { GtkMenu subMenu = (GtkMenu) menuEntry__; - - // will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu' - Gtk.gtk_menu_shell_append(this._native, subMenu.menuEntry._native); - Gobject.g_object_ref_sink(subMenu.menuEntry._native); // undoes "floating" - Gtk.gtk_widget_show_all(subMenu.menuEntry._native); // necessary to guarantee widget is visible - if (subMenu.getParent() != GtkMenu.this) { // we don't want to "createMenu" on our sub-menu that is assigned to us directly, as they are already doing it subMenu.createMenu(); @@ -418,8 +154,8 @@ class GtkMenu extends MenuBase implements NativeUI { } } - onMenuAdded(_native); - Gtk.gtk_widget_show_all(_native); // necessary to guarantee widget is visible (doesn't always show_all for all children) + onMenuAdded(_nativeMenu); + Gtk.gtk_widget_show_all(_nativeMenu); // necessary to guarantee widget is visible (doesn't always show_all for all children) } } @@ -430,60 +166,228 @@ class GtkMenu extends MenuBase implements NativeUI { */ private void obliterateMenu() { - if (_native != null && !obliterateInProgress) { + if (_nativeMenu != null && !obliterateInProgress) { obliterateInProgress = true; // have to remove all other menu entries synchronized (menuEntries) { // a copy is made because sub-menus remove themselves from parents when .remove() is called. If we don't // do this, errors will be had because indices don't line up anymore. - ArrayList menuEntriesCopy = new ArrayList(this.menuEntries); + ArrayList menuEntriesCopy = new ArrayList(this.menuEntries); for (int i = 0, menuEntriesSize = menuEntriesCopy.size(); i < menuEntriesSize; i++) { - final Entry menuEntry__ = menuEntriesCopy.get(i); + final GtkMenuBaseItem menuEntry__ = menuEntriesCopy.get(i); menuEntry__.remove(); } this.menuEntries.clear(); menuEntriesCopy.clear(); - Gtk.gtk_widget_destroy(_native); + Gtk.gtk_widget_destroy(_nativeMenu); + _nativeMenu = null; } obliterateInProgress = false; } } + @Override + public + void add(final Menu parentMenu, final Entry entry, final int index) { + // must always be called on the GTK dispatch. This must be dispatchAndWait + Gtk.dispatchAndWait(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. + deleteMenu(); + + if (entry instanceof Menu) { + // 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 + + GtkMenu item = new GtkMenu(GtkMenu.this); + add(item, index); + ((Menu) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Separator) { + GtkMenuItemSeparator item = new GtkMenuItemSeparator(GtkMenu.this); + add(item, index); + entry.bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Checkbox) { + GtkMenuItemCheckbox item = new GtkMenuItemCheckbox(GtkMenu.this); + add(item, index); + ((Checkbox) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Status) { + GtkMenuItemStatus item = new GtkMenuItemStatus(GtkMenu.this); + add(item, index); + ((Status) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof MenuItem) { + GtkMenuItem item = new GtkMenuItem(GtkMenu.this); + add(item, index); + ((MenuItem) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + + createMenu(); + } + }); + } + + + // NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted. + // see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/ + // see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25 + @SuppressWarnings("Duplicates") + @Override + public + void setImage(final MenuItem menuItem) { + // is overridden by system tray + setLegitImage(menuItem.getImage() != null); + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + Gtk.gtk_widget_show_all(_nativeEntry); + } + + if (menuItem.getImage() != null) { + image = Gtk.gtk_image_new_from_file(menuItem.getImage() + .getAbsolutePath()); + Gtk.gtk_image_menu_item_set_image(_nativeEntry, image); + + // must always re-set always-show after setting the image + Gtk.gtk_image_menu_item_set_always_show_image(_nativeEntry, true); + } + + Gtk.gtk_widget_show_all(_nativeEntry); + } + }); + } + + @Override + public + void setEnabled(final MenuItem menuItem) { + // is overridden by system tray + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_widget_set_sensitive(_nativeEntry, menuItem.getEnabled()); + } + }); + } + + @SuppressWarnings("Duplicates") + @Override + public + void setText(final MenuItem menuItem) { + // is overridden by system tray + final String textWithMnemonic; + + if (mnemonicKey != 0) { + String text = menuItem.getText(); + + if (text != null) { + // they are CASE INSENSITIVE! + int i = text.toLowerCase() + .indexOf(mnemonicKey); + + if (i >= 0) { + textWithMnemonic = text.substring(0, i) + "_" + text.substring(i); + } + else { + textWithMnemonic = menuItem.getText(); + } + } else { + textWithMnemonic = null; + } + } + else { + textWithMnemonic = menuItem.getText(); + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_menu_item_set_label(_nativeEntry, textWithMnemonic); + Gtk.gtk_widget_show_all(_nativeEntry); + } + }); + } + + @Override + public + void setCallback(final MenuItem menuItem) { + // can't have a callback for menus! + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + this.mnemonicKey = Character.toLowerCase(menuItem.getShortcut()); + + setText(menuItem); + } + + + @Override + void onDeleteMenu(final Pointer parentNative) { + if (parent != null) { + onDeleteMenu(parentNative, _nativeEntry); + } + } + + @Override + void onCreateMenu(final Pointer parentNative, final boolean hasImagesInMenu) { + if (parent != null) { + onCreateMenu(parentNative, _nativeEntry, hasImagesInMenu); + } + } + + // called when a child removes itself from the parent menu. Does not work for sub-menus + public + void remove(final GtkMenuBaseItem item) { + synchronized (menuEntries) { + menuEntries.remove(item); + } + + // have to rebuild the menu now... + deleteMenu(); + createMenu(); + } // a child will always remove itself from the parent. @Override public void remove() { - dispatchAndWait(new Runnable() { + Gtk.dispatchAndWait(new Runnable() { @Override public void run() { - GtkMenu parent = (GtkMenu) getParent(); + GtkMenu parent = getParent(); - // have to remove from the parent.menuEntries first - for (Iterator iterator = parent.menuEntries.iterator(); iterator.hasNext(); ) { - final Entry entry = iterator.next(); - if (entry == GtkMenu.this) { - iterator.remove(); - break; + if (parent != null) { + // have to remove from the parent.menuEntries first + synchronized (parent.menuEntries) { + parent.menuEntries.remove(GtkMenu.this); } } - // cleans up the menu -// parent.remove__(null); - // delete all of the children of this submenu (must happen before the menuEntry is removed) obliterateMenu(); - // remove the gtk entry item from our parent menu NATIVE components - // NOTE: this will rebuild the parent menu - if (menuEntry != null) { - menuEntry.remove(); - } else { + if (parent != null) { + // remove the gtk entry item from our menu NATIVE components + Gtk.gtk_menu_item_set_submenu(_nativeEntry, null); + // have to rebuild the menu now... parent.deleteMenu(); parent.createMenu(); diff --git a/src/dorkbox/systemTray/nativeUI/GtkMenuBaseItem.java b/src/dorkbox/systemTray/nativeUI/GtkMenuBaseItem.java new file mode 100644 index 0000000..5eadc34 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkMenuBaseItem.java @@ -0,0 +1,118 @@ +/* + * 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.nativeUI; + +import java.io.File; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.jna.linux.Gobject; +import dorkbox.systemTray.jna.linux.Gtk; +import dorkbox.systemTray.util.EntryHook; +import dorkbox.systemTray.util.ImageUtils; + +abstract +class GtkMenuBaseItem implements EntryHook { + private static File transparentIcon = null; + // these are necessary BECAUSE GTK menus look funky as hell when there are some menu entries WITH icons and some WITHOUT + private volatile boolean hasLegitImage = true; + + // these have to be volatile, because they can be changed from any thread + private volatile Pointer spacerImage; + + GtkMenuBaseItem() { + // cannot be done in a static initializer, because the tray icon size might not yet have been determined + if (transparentIcon == null) { + transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); + } + } + + public + boolean hasImage() { + return hasLegitImage; + } + + public + void setLegitImage(boolean isLegit) { + hasLegitImage = isLegit; + } + + /** + * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images. + * This is primarily only with AppIndicators, although not always. + *

+ * called on the DISPATCH thread + */ + public + void setSpacerImage(final Pointer _native, final boolean everyoneElseHasImages) { + if (hasLegitImage) { + // we have a legit icon, so there is nothing else we can do. + return; + } + + if (spacerImage != null) { + Gtk.gtk_widget_destroy(spacerImage); + spacerImage = null; + Gtk.gtk_widget_show_all(_native); + } + + if (everyoneElseHasImages) { + spacerImage = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath()); + Gtk.gtk_image_menu_item_set_image(_native, spacerImage); + + // must always re-set always-show after setting the image + Gtk.gtk_image_menu_item_set_always_show_image(_native, true); + } + + Gtk.gtk_widget_show_all(_native); + } + + // 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. + abstract void onDeleteMenu(final Pointer parentNative); + abstract void onCreateMenu(final Pointer parentNative, final boolean hasImagesInMenu); + + // always on dispatch + void onDeleteMenu(final Pointer parentNative, final Pointer _native) { + Gobject.g_object_force_floating(_native); // makes it a floating reference + Gtk.gtk_container_remove(parentNative, _native); + } + + // always on dispatch + void onCreateMenu(final Pointer parentNative, final Pointer _native, final boolean hasImagesInMenu) { + setSpacerImage(_native, hasImagesInMenu); + + // will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu' + Gtk.gtk_menu_shell_append(parentNative, _native); + Gobject.g_object_ref_sink(_native); // undoes "floating" + Gtk.gtk_widget_show_all(_native); // necessary to guarantee widget is visible + } + + @Override + public + void remove() { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + if (spacerImage != null) { + Gtk.gtk_widget_destroy(spacerImage); + spacerImage = null; + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkMenuItem.java b/src/dorkbox/systemTray/nativeUI/GtkMenuItem.java new file mode 100644 index 0000000..242af59 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkMenuItem.java @@ -0,0 +1,206 @@ +/* + * 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.nativeUI; + +import java.awt.event.ActionListener; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +import dorkbox.systemTray.MenuItem; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.jna.linux.GCallback; +import dorkbox.systemTray.jna.linux.Gobject; +import dorkbox.systemTray.jna.linux.Gtk; +import dorkbox.systemTray.util.MenuItemHook; + +class GtkMenuItem extends GtkMenuBaseItem implements MenuItemHook, GCallback { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final NativeLong nativeLong; + + private final GtkMenu parent; + protected final Pointer _native = Gtk.gtk_image_menu_item_new_with_mnemonic(""); + + // these have to be volatile, because they can be changed from any thread + private volatile MenuItem menuItemForActionCallback; + private volatile Pointer image; + + // The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then! + // AppIndicators will only show if you use the keyboard to navigate + // GtkStatusIconTray will show on mouse+keyboard movement + private volatile char mnemonicKey = 0; + + /** + * 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 + */ + GtkMenuItem(final GtkMenu parent) { + this.parent = parent; + nativeLong = Gobject.g_signal_connect_object(_native, "activate", this, null, 0); + } + + + // called by native code + @Override + public + int callback(final Pointer instance, final Pointer data) { + if (menuItemForActionCallback != null) { + final ActionListener cb = menuItemForActionCallback.getCallback(); + if (cb != null) { + try { + Gtk.proxyClick(menuItemForActionCallback, cb); + } catch (Exception e) { + SystemTray.logger.error("Error calling menu entry {} click event.", menuItemForActionCallback.getText(), e); + } + } + } + + return Gtk.TRUE; + } + + // NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted. + // see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/ + // see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25 + @SuppressWarnings("Duplicates") + @Override + public + void setImage(final MenuItem menuItem) { + setLegitImage(menuItem.getImage() != null); + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + Gtk.gtk_widget_show_all(_native); + } + + if (menuItem.getImage() != null) { + image = Gtk.gtk_image_new_from_file(menuItem.getImage() + .getAbsolutePath()); + 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(_native, true); + } + + Gtk.gtk_widget_show_all(_native); + } + }); + } + + @Override + public + void setEnabled(final MenuItem menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_widget_set_sensitive(_native, menuItem.getEnabled()); + } + }); + } + + @SuppressWarnings("Duplicates") + @Override + public + void setText(final MenuItem menuItem) { + final String textWithMnemonic; + + if (mnemonicKey != 0) { + String text = menuItem.getText(); + + if (text != null) { + // they are CASE INSENSITIVE! + int i = text.toLowerCase() + .indexOf(mnemonicKey); + + if (i >= 0) { + textWithMnemonic = text.substring(0, i) + "_" + text.substring(i); + } + else { + textWithMnemonic = menuItem.getText(); + } + } else { + textWithMnemonic = null; + } + } + else { + textWithMnemonic = menuItem.getText(); + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_menu_item_set_label(_native, textWithMnemonic); + Gtk.gtk_widget_show_all(_native); + } + }); + } + + @Override + public + void setCallback(final MenuItem menuItem) { + this.menuItemForActionCallback = menuItem; + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + this.mnemonicKey = Character.toLowerCase(menuItem.getShortcut()); + + setText(menuItem); + } + + @Override + void onDeleteMenu(final Pointer parentNative) { + onDeleteMenu(parentNative, _native); + } + + @Override + void onCreateMenu(final Pointer parentNative, final boolean hasImagesInMenu) { + onCreateMenu(parentNative, _native, hasImagesInMenu); + } + + @SuppressWarnings("Duplicates") + @Override + public + void remove() { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_container_remove(parent._nativeMenu, _native); + Gtk.gtk_menu_shell_deactivate(parent._nativeMenu, _native); + + GtkMenuItem.super.remove(); + + menuItemForActionCallback = null; + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + } + + Gtk.gtk_widget_destroy(_native); + + parent.remove(GtkMenuItem.this); + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkMenuItemCheckbox.java b/src/dorkbox/systemTray/nativeUI/GtkMenuItemCheckbox.java new file mode 100644 index 0000000..4f5a731 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkMenuItemCheckbox.java @@ -0,0 +1,200 @@ +/* + * 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.nativeUI; + +import java.awt.event.ActionListener; +import java.io.File; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +import dorkbox.systemTray.Checkbox; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.jna.linux.GCallback; +import dorkbox.systemTray.jna.linux.Gobject; +import dorkbox.systemTray.jna.linux.Gtk; +import dorkbox.systemTray.util.ImageUtils; +import dorkbox.systemTray.util.MenuCheckboxHook; + +class GtkMenuItemCheckbox extends GtkMenuBaseItem implements MenuCheckboxHook, GCallback { + private static File transparentIcon = null; + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final NativeLong nativeLong; + + private final GtkMenu parent; + private final Pointer _native = Gtk.gtk_check_menu_item_new_with_mnemonic(""); + + + // these have to be volatile, because they can be changed from any thread + private volatile Checkbox menuItem; + private volatile Pointer image; + + // The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then! + // AppIndicators will only show if you use the keyboard to navigate + // GtkStatusIconTray will show on mouse+keyboard movement + private volatile char mnemonicKey = 0; + + /** + * 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 + */ + GtkMenuItemCheckbox(final GtkMenu parent) { + this.parent = parent; + + // cannot be done in a static initializer, because the tray icon size might not yet have been determined + if (transparentIcon == null) { + transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); + } + + nativeLong = Gobject.g_signal_connect_object(_native, "activate", this, null, 0); + } + + // called by native code + @Override + public + int callback(final Pointer instance, final Pointer data) { + if (menuItem != null) { + final ActionListener cb = menuItem.getCallback(); + if (cb != null) { + try { + Gtk.proxyClick(menuItem, cb); + } catch (Exception e) { + SystemTray.logger.error("Error calling menu entry checkbox {} click event.", menuItem.getText(), e); + } + } + } + + return Gtk.TRUE; + } + + public + boolean hasImage() { + return true; + } + + public + void setSpacerImage(final Pointer _native, final boolean everyoneElseHasImages) { + // no op + } + + @Override + public + void setEnabled(final Checkbox menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_widget_set_sensitive(_native, menuItem.getEnabled()); + } + }); + } + + @Override + public + void setText(final Checkbox menuItem) { + final String textWithMnemonic; + + if (mnemonicKey != 0) { + String text = menuItem.getText(); + + // they are CASE INSENSITIVE! + int i = text.toLowerCase() + .indexOf(mnemonicKey); + + if (i >= 0) { + textWithMnemonic = text.substring(0, i) + "_" + text.substring(i); + } + else { + textWithMnemonic = menuItem.getText(); + } + } + else { + textWithMnemonic = menuItem.getText(); + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_menu_item_set_label(_native, textWithMnemonic); + Gtk.gtk_widget_show_all(_native); + } + }); + } + + @Override + public + void setCallback(final Checkbox menuItem) { + this.menuItem = menuItem; + } + + @Override + public + void setChecked(final Checkbox checkbox) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_check_menu_item_set_active(_native, checkbox.getChecked()); + } + }); + } + + @Override + public + void setShortcut(final Checkbox menuItem) { + this.mnemonicKey = Character.toLowerCase(menuItem.getShortcut()); + + setText(menuItem); + } + + @Override + void onDeleteMenu(final Pointer parentNative) { + onDeleteMenu(parentNative, _native); + } + + @Override + void onCreateMenu(final Pointer parentNative, final boolean hasImagesInMenu) { + onCreateMenu(parentNative, _native, hasImagesInMenu); + } + + @SuppressWarnings("Duplicates") + @Override + public + void remove() { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_container_remove(parent._nativeMenu, _native); + Gtk.gtk_menu_shell_deactivate(parent._nativeMenu, _native); + + GtkMenuItemCheckbox.super.remove(); + + menuItem = null; + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + } + + Gtk.gtk_widget_destroy(_native); + + parent.remove(GtkMenuItemCheckbox.this); + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkMenuItemSeparator.java b/src/dorkbox/systemTray/nativeUI/GtkMenuItemSeparator.java new file mode 100644 index 0000000..1f190f4 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkMenuItemSeparator.java @@ -0,0 +1,74 @@ +/* + * 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.nativeUI; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.jna.linux.Gtk; +import dorkbox.systemTray.util.EntryHook; + +class GtkMenuItemSeparator extends GtkMenuBaseItem implements EntryHook { + + private final GtkMenu parent; + private final Pointer _native = Gtk.gtk_separator_menu_item_new(); + + + /** + * 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 + */ + GtkMenuItemSeparator(final GtkMenu parent) { + this.parent = parent; + } + + @SuppressWarnings("Duplicates") + @Override + public + void remove() { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_container_remove(parent._nativeMenu, _native); + Gtk.gtk_menu_shell_deactivate(parent._nativeMenu, _native); + + Gtk.gtk_widget_destroy(_native); + + parent.remove(GtkMenuItemSeparator.this); + } + }); + } + + public + boolean hasImage() { + return false; + } + + public + void setSpacerImage(final Pointer _native, final boolean everyoneElseHasImages) { + // no op + } + + @Override + void onDeleteMenu(final Pointer parentNative) { + onDeleteMenu(parentNative, _native); + } + + @Override + void onCreateMenu(final Pointer parentNative, final boolean hasImagesInMenu) { + onCreateMenu(parentNative, _native, hasImagesInMenu); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkMenuItemStatus.java b/src/dorkbox/systemTray/nativeUI/GtkMenuItemStatus.java new file mode 100644 index 0000000..b51c02a --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkMenuItemStatus.java @@ -0,0 +1,90 @@ +/* + * 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.nativeUI; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.jna.linux.Gtk; +import dorkbox.systemTray.util.MenuStatusHook; +import dorkbox.systemTray.util.Status; + +// you might wonder WHY this extends MenuEntryItem -- the reason is that an AppIndicator "status" will be offset from everyone else, +// where a GtkStatusIconTray + SwingUI will have everything lined up. (with or without icons). This is to normalize how it looks +class GtkMenuItemStatus extends GtkMenuBaseItem implements MenuStatusHook { + + private final GtkMenu parent; + private final Pointer _native = Gtk.gtk_image_menu_item_new_with_mnemonic(""); + + /** + * 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 + */ + GtkMenuItemStatus(final GtkMenu parent) { + super(); + this.parent = parent; + + // need that extra space so it matches windows/mac + setLegitImage(false); + } + + @Override + public + void setText(final Status menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // AppIndicator strips out markup text. + // https://mail.gnome.org/archives/commits-list/2016-March/msg05444.html + + Gtk.gtk_menu_item_set_label(_native, menuItem.getText()); + Gtk.gtk_widget_show_all(_native); + + Gtk.gtk_widget_set_sensitive(_native, false); + } + }); + } + + @SuppressWarnings("Duplicates") + @Override + public + void remove() { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_container_remove(parent._nativeMenu, _native); + Gtk.gtk_menu_shell_deactivate(parent._nativeMenu, _native); + + GtkMenuItemStatus.super.remove(); + + Gtk.gtk_widget_destroy(_native); + + parent.remove(GtkMenuItemStatus.this); + } + }); + } + + @Override + void onDeleteMenu(final Pointer parentNative) { + onDeleteMenu(parentNative, _native); + } + + @Override + void onCreateMenu(final Pointer parentNative, final boolean hasImagesInMenu) { + onCreateMenu(parentNative, _native, hasImagesInMenu); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/NativeUI.java b/src/dorkbox/systemTray/nativeUI/NativeUI.java index 7b5b0af..a222030 100644 --- a/src/dorkbox/systemTray/nativeUI/NativeUI.java +++ b/src/dorkbox/systemTray/nativeUI/NativeUI.java @@ -15,6 +15,7 @@ */ package dorkbox.systemTray.nativeUI; + /** * Represents a System Tray or menu, that will have it's menu rendered via the native subsystem. *

diff --git a/src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java b/src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java index 3105c89..fd8a73c 100644 --- a/src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java +++ b/src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java @@ -20,7 +20,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import com.sun.jna.Pointer; +import dorkbox.systemTray.MenuItem; import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.Tray; import dorkbox.systemTray.jna.linux.AppIndicator; import dorkbox.systemTray.jna.linux.AppIndicatorInstanceStruct; import dorkbox.systemTray.jna.linux.Gobject; @@ -74,7 +76,7 @@ import dorkbox.systemTray.util.ImageUtils; */ @SuppressWarnings("Duplicates") public -class _AppIndicatorNativeTray extends GtkMenu { +class _AppIndicatorNativeTray extends Tray implements NativeUI { private volatile AppIndicatorInstanceStruct appIndicator; private boolean isActive = false; @@ -83,6 +85,8 @@ class _AppIndicatorNativeTray extends GtkMenu { // is the system tray visible or not. private volatile boolean visible = true; + private volatile File image; + // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) // they ALSO do not support tooltips, so we cater to the lowest common denominator @@ -90,10 +94,106 @@ class _AppIndicatorNativeTray extends GtkMenu { public _AppIndicatorNativeTray(final SystemTray systemTray) { - super(systemTray, null); + super(); Gtk.startGui(); + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + final GtkMenu gtkMenu = new GtkMenu(null) { + /** + * MUST BE AFTER THE ITEM IS ADDED/CHANGED from the menu + */ + protected final + void onMenuAdded(final Pointer menu) { + // see: https://code.launchpad.net/~mterry/libappindicator/fix-menu-leak/+merge/53247 + AppIndicator.app_indicator_set_menu(appIndicator, menu); + } + + @Override + public + void setEnabled(final MenuItem menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + boolean enabled = menuItem.getEnabled(); + + if (visible && !enabled) { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE); + visible = false; + } + else if (!visible && enabled) { + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + visible = true; + } + } + }); + } + + @Override + public + void setImage(final MenuItem menuItem) { + image = menuItem.getImage(); + if (image == null) { + return; + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + AppIndicator.app_indicator_set_icon(appIndicator, image.getAbsolutePath()); + + if (!isActive) { + isActive = true; + + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + } + } + }); + } + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) + if (!shuttingDown.getAndSet(true)) { + // must happen asap, so our hook properly notices we are in shutdown mode + final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; + appIndicator = null; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); + Pointer p = savedAppIndicator.getPointer(); + Gobject.g_object_unref(p); + } + }); + + super.remove(); + + // does not need to be called on the dispatch (it does that) + Gtk.shutdownGui(); + } + } + }; + Gtk.dispatch(new Runnable() { @Override public @@ -106,80 +206,13 @@ class _AppIndicatorNativeTray extends GtkMenu { }); Gtk.waitForStartup(); - } - public final - void shutdown() { - if (!shuttingDown.getAndSet(true)) { - // must happen asap, so our hook properly notices we are in shutdown mode - final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; - appIndicator = null; - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - // STATUS_PASSIVE hides the indicator - AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); - Pointer p = savedAppIndicator.getPointer(); - Gobject.g_object_unref(p); - } - }); - - super.shutdown(); - } + bind(gtkMenu, null, systemTray); } @Override public final boolean hasImage() { - return true; - } - - @Override - public final - void setImage_(final File imageFile) { - dispatch(new Runnable() { - @Override - public - void run() { - AppIndicator.app_indicator_set_icon(appIndicator, imageFile.getAbsolutePath()); - - if (!isActive) { - isActive = true; - - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); - } - } - }); - } - - @Override - public final - void setEnabled(final boolean setEnabled) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - if (visible && !setEnabled) { - // STATUS_PASSIVE hides the indicator - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE); - visible = false; - } - else if (!visible && setEnabled) { - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); - visible = true; - } - } - }); - } - - /** - * MUST BE AFTER THE ITEM IS ADDED/CHANGED from the menu - */ - protected final - void onMenuAdded(final Pointer menu) { - // see: https://code.launchpad.net/~mterry/libappindicator/fix-menu-leak/+merge/53247 - AppIndicator.app_indicator_set_menu(appIndicator, menu); + return image != null; } } diff --git a/src/dorkbox/systemTray/nativeUI/_AwtTray.java b/src/dorkbox/systemTray/nativeUI/_AwtTray.java index cae1522..f17398d 100644 --- a/src/dorkbox/systemTray/nativeUI/_AwtTray.java +++ b/src/dorkbox/systemTray/nativeUI/_AwtTray.java @@ -24,20 +24,25 @@ import java.io.File; import javax.swing.ImageIcon; +import dorkbox.systemTray.MenuItem; +import dorkbox.systemTray.Tray; import dorkbox.util.OS; +import dorkbox.util.SwingUtil; /** * Class for handling all system tray interaction, via AWT. Pretty much EXCLUSIVELY for on MacOS, because that is the only time this - * looks good + * looks good and works correctly. * * It doesn't work well on linux. See bugs: * http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936 * http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521 * https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028 + * + * Also, on linux, this WILL NOT CLOSE properly -- there is a frame handle that keeps the JVM open */ @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"}) public -class _AwtTray extends AwtMenu { +class _AwtTray extends Tray implements NativeUI { private volatile SystemTray tray; private volatile TrayIcon trayIcon; @@ -50,7 +55,7 @@ class _AwtTray extends AwtMenu { // Called in the EDT public _AwtTray(final dorkbox.systemTray.SystemTray systemTray) { - super(systemTray, null, new PopupMenu()); + super(); if (!SystemTray.isSupported()) { throw new RuntimeException("System Tray is not supported in this configuration! Please write an issue and include your OS " + @@ -58,111 +63,130 @@ class _AwtTray extends AwtMenu { } _AwtTray.this.tray = SystemTray.getSystemTray(); - } - public - void shutdown() { - dispatchAndWait(new Runnable() { + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + final AwtMenu awtMenu = new AwtMenu(null) { @Override public - void run() { - removeAll(); - remove(); - - tray.remove(trayIcon); - } - }); - } - - public - void setImage_(final File iconFile) { - dispatch(new Runnable() { - @Override - public - void run() { - // stupid java won't scale it right away, so we have to do this twice to get the correct size - final Image trayImage = new ImageIcon(iconFile.getAbsolutePath()).getImage(); - trayImage.flush(); - - if (trayIcon == null) { - // here we init. everything - trayIcon = new TrayIcon(trayImage); - - // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) - // they ALSO do not support tooltips, so we cater to the lowest common denominator - // trayIcon.setToolTip("app name"); - - trayIcon.setPopupMenu((PopupMenu) _native); - - try { - tray.add(trayIcon); - } catch (AWTException e) { - dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e); - } - } else { - trayIcon.setImage(trayImage); - } - } - }); - } - - @SuppressWarnings("Duplicates") - public - void setEnabled(final boolean setEnabled) { - if (OS.isMacOsX()) { - if (keepAliveThread != null) { - synchronized (keepAliveLock) { - keepAliveLock.notifyAll(); - } - } - keepAliveThread = null; - - if (visible && !setEnabled) { - // THIS WILL NOT keep the app running, so we use a "keep-alive" thread so this behavior is THE SAME across - // all platforms. This was only noticed on MacOS (where the app would quit after calling setEnabled(false); - keepAliveThread = new Thread(new Runnable() { + void setEnabled(final MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { - synchronized (keepAliveLock) { - keepAliveLock.notifyAll(); + boolean enabled = menuItem.getEnabled(); + if (OS.isMacOsX()) { + if (keepAliveThread != null) { + synchronized (keepAliveLock) { + keepAliveLock.notifyAll(); + } + } + keepAliveThread = null; + + if (visible && !enabled) { + // THIS WILL NOT keep the app running, so we use a "keep-alive" thread so this behavior is THE SAME across + // all platforms. This was only noticed on MacOS (where the app would quit after calling setEnabled(false); + keepAliveThread = new Thread(new Runnable() { + @Override + public + void run() { + synchronized (keepAliveLock) { + keepAliveLock.notifyAll(); + + try { + keepAliveLock.wait(); + } catch (InterruptedException ignored) { + } + } + } + }, "TrayKeepAliveThread"); + keepAliveThread.start(); + } + } + + if (visible && !enabled) { + tray.remove(trayIcon); + visible = false; + } + else if (!visible && enabled) { try { - keepAliveLock.wait(); - } catch (InterruptedException ignored) { + tray.add(trayIcon); + visible = true; + } catch (AWTException e) { + dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray", e); } } } - }, "KeepAliveThread"); - keepAliveThread.start(); + }); } - synchronized (keepAliveLock) { - try { - keepAliveLock.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - dispatch(new Runnable() { @Override public - void run() { - if (visible && !setEnabled) { - tray.remove(trayIcon); - visible = false; + void setImage(final MenuItem menuItem) { + final File image = menuItem.getImage(); + if (image == null) { + return; } - else if (!visible && setEnabled) { - try { - tray.add(trayIcon); - visible = true; - } catch (AWTException e) { - dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray"); + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + // stupid java won't scale it right away, so we have to do this twice to get the correct size + final Image trayImage = new ImageIcon(image.getAbsolutePath()).getImage(); + trayImage.flush(); + + if (trayIcon == null) { + // here we init. everything + trayIcon = new TrayIcon(trayImage); + + trayIcon.setPopupMenu((PopupMenu) _native); + + try { + tray.add(trayIcon); + } catch (AWTException e) { + dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e); + } + } else { + trayIcon.setImage(trayImage); + } } - } + }); } - }); + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (trayIcon != null) { + trayIcon.setPopupMenu(null); + tray.remove(trayIcon); + trayIcon = null; + } + + tray = null; + } + }); + + super.remove(); + } + }; + + bind(awtMenu, null, systemTray); } } diff --git a/src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java b/src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java index 9ca4574..67f9f26 100644 --- a/src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java +++ b/src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java @@ -23,7 +23,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import com.sun.jna.NativeLong; import com.sun.jna.Pointer; +import dorkbox.systemTray.MenuItem; import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.Tray; import dorkbox.systemTray.jna.linux.GEventCallback; import dorkbox.systemTray.jna.linux.GdkEventButton; import dorkbox.systemTray.jna.linux.Gobject; @@ -36,7 +38,7 @@ import dorkbox.systemTray.jna.linux.Gtk; */ @SuppressWarnings("Duplicates") public -class _GtkStatusIconNativeTray extends GtkMenu { +class _GtkStatusIconNativeTray extends Tray implements NativeUI { private volatile Pointer trayIcon; // http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c @@ -52,18 +54,99 @@ class _GtkStatusIconNativeTray extends GtkMenu { // is the system tray visible or not. private volatile boolean visible = true; + private volatile File image; // called on the EDT public _GtkStatusIconNativeTray(final SystemTray systemTray) { - super(systemTray, null); - - // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) - // they ALSO do not support tooltips, so we cater to the lowest common denominator - // trayIcon.setToolTip("app name"); + super(); Gtk.startGui(); + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + final GtkMenu gtkMenu = new GtkMenu(null) { + @Override + public + void setEnabled(final MenuItem menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + boolean enabled = menuItem.getEnabled(); + + if (visible && !enabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, enabled); + visible = false; + } + else if (!visible && enabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, enabled); + visible = true; + } + } + }); + } + + @Override + public + void setImage(final MenuItem menuItem) { + image = menuItem.getImage(); + if (image == null) { + return; + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_status_icon_set_from_file(trayIcon, image.getAbsolutePath()); + + if (!isActive) { + isActive = true; + Gtk.gtk_status_icon_set_visible(trayIcon, true); + } + } + }); + } + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) + if (!shuttingDown.getAndSet(true)) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // this hides the indicator + Gtk.gtk_status_icon_set_visible(trayIcon, false); + Gobject.g_object_unref(trayIcon); + + // mark for GC + trayIcon = null; + gtkCallbacks.clear(); + } + }); + + super.remove(); + + // does not need to be called on the dispatch (it does that) + Gtk.shutdownGui(); + } + } + }; + Gtk.dispatch(new Runnable() { @Override public @@ -77,7 +160,8 @@ class _GtkStatusIconNativeTray extends GtkMenu { // show the swing menu on the EDT // BUTTON_PRESS only (any mouse click) if (event.type == 4) { - Gtk.gtk_menu_popup(_native, null, null, Gtk.gtk_status_icon_position_menu, trayIcon, 0, event.time); + Gtk.gtk_menu_popup(gtkMenu._nativeMenu, null, null, Gtk.gtk_status_icon_position_menu, + trayIcon, 0, event.time); } } }; @@ -117,70 +201,13 @@ class _GtkStatusIconNativeTray extends GtkMenu { } } }); - } - - @SuppressWarnings("FieldRepeatedlyAccessedInMethod") - public final - void shutdown() { - if (!shuttingDown.getAndSet(true)) { - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - // this hides the indicator - Gtk.gtk_status_icon_set_visible(trayIcon, false); - Gobject.g_object_unref(trayIcon); - - // mark for GC - trayIcon = null; - gtkCallbacks.clear(); - } - }); - - super.shutdown(); - } + bind(gtkMenu, null, systemTray); } @Override public final boolean hasImage() { - return true; - } - - @Override - public final - void setImage_(final File iconFile) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - Gtk.gtk_status_icon_set_from_file(trayIcon, iconFile.getAbsolutePath()); - - if (!isActive) { - isActive = true; - Gtk.gtk_status_icon_set_visible(trayIcon, true); - } - } - }); - } - - @Override - public final - void setEnabled(final boolean setEnabled) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - if (visible && !setEnabled) { - Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled); - visible = false; - } else if (!visible && setEnabled) { - Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled); - visible = true; - } - } - }); + return image != null; } } diff --git a/src/dorkbox/systemTray/swingUI/SwingMenu.java b/src/dorkbox/systemTray/swingUI/SwingMenu.java index 3356389..abac76e 100644 --- a/src/dorkbox/systemTray/swingUI/SwingMenu.java +++ b/src/dorkbox/systemTray/swingUI/SwingMenu.java @@ -36,10 +36,8 @@ import dorkbox.util.SwingUtil; @SuppressWarnings("ForLoopReplaceableByForEach") class SwingMenu implements MenuHook { - private final SwingMenu parent; final JComponent _native; - - private volatile boolean hasLegitIcon = false; + private final SwingMenu parent; // This is NOT a copy constructor! @SuppressWarnings("IncompleteCopyConstructor") @@ -55,25 +53,36 @@ class SwingMenu implements MenuHook { } } - protected final - void dispatch(final Runnable runnable) { - // this will properly check if we are running on the EDT - SwingUtil.invokeLater(runnable); - } - - protected final - void dispatchAndWait(final Runnable runnable) { - // this will properly check if we are running on the EDT - try { - SwingUtil.invokeAndWait(runnable); - } catch (Exception e) { - SystemTray.logger.error("Error processing event on the dispatch thread.", e); - } - } - + @Override public - boolean hasImage() { - return hasLegitIcon; + void add(final Menu parentMenu, final Entry entry, final int index) { + // must always be called on the EDT + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (entry instanceof Menu) { + SwingMenu swingMenu = new SwingMenu(SwingMenu.this); + ((Menu) entry).bind(swingMenu, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Separator) { + SwingMenuItemSeparator item = new SwingMenuItemSeparator(SwingMenu.this); + entry.bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Checkbox) { + SwingMenuItemCheckbox item = new SwingMenuItemCheckbox(SwingMenu.this); + ((Checkbox) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Status) { + SwingMenuItemStatus item = new SwingMenuItemStatus(SwingMenu.this); + ((Status) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof MenuItem) { + SwingMenuItem item = new SwingMenuItem(SwingMenu.this); + ((MenuItem) entry).bind(item, parentMenu, parentMenu.getSystemTray()); + } + } + }); } // is overridden in tray impl @@ -81,9 +90,8 @@ class SwingMenu implements MenuHook { public void setImage(final MenuItem menuItem) { final File imageFile = menuItem.getImage(); - hasLegitIcon = imageFile != null; - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -102,7 +110,7 @@ class SwingMenu implements MenuHook { @Override public void setEnabled(final MenuItem menuItem) { - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -116,7 +124,7 @@ class SwingMenu implements MenuHook { @Override public void setText(final MenuItem menuItem) { - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -139,7 +147,7 @@ class SwingMenu implements MenuHook { // yikes... final int vKey = SystemTrayFixes.getVirtualKey(shortcut); - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -148,45 +156,13 @@ class SwingMenu implements MenuHook { }); } - @Override - public - void add(final Menu parentMenu, final Entry entry, final int index) { - // must always be called on the EDT - dispatch(new Runnable() { - @Override - public - void run() { - if (entry instanceof Menu) { - SwingMenu swingMenu = new SwingMenu(SwingMenu.this); - ((Menu) entry).bind(swingMenu, parentMenu, parentMenu.getSystemTray()); - } - else if (entry instanceof Separator) { - SwingMenuItemSeparator swingEntrySeparator = new SwingMenuItemSeparator(SwingMenu.this); - entry.bind(swingEntrySeparator, parentMenu, parentMenu.getSystemTray()); - } - else if (entry instanceof Checkbox) { - SwingMenuItemCheckbox swingEntryCheckbox = new SwingMenuItemCheckbox(SwingMenu.this); - ((Checkbox) entry).bind(swingEntryCheckbox, parentMenu, parentMenu.getSystemTray()); - } - else if (entry instanceof Status) { - SwingMenuItemStatus swingEntryStatus = new SwingMenuItemStatus(SwingMenu.this); - ((Status) entry).bind(swingEntryStatus, parentMenu, parentMenu.getSystemTray()); - } - else if (entry instanceof MenuItem) { - SwingMenuItem swingMenuItem = new SwingMenuItem(SwingMenu.this); - ((MenuItem) entry).bind(swingMenuItem, parentMenu, parentMenu.getSystemTray()); - } - } - }); - } - /** - * This removes all menu entries from this menu AND this menu from it's parent + * This removes all menu entries from this menu AND this menu from it's parent */ @Override public synchronized void remove() { - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -195,11 +171,49 @@ class SwingMenu implements MenuHook { if (parent != null) { parent._native.remove(_native); - } else { + } + else { // have to dispose of the tray popup hidden frame, otherwise the app will never close (because this will hold it open) ((TrayPopup) _native).close(); } } }); } + + + // NOT ALWAYS CALLED ON EDT + protected + void remove__(final Object menuEntry) { + try { +// synchronized (menuEntries) { +// // null is passed in when a sub-menu is removing itself from us (because they have already called "remove" and have also +// // removed themselves from the menuEntries) +// if (menuEntry != null) { +// for (Iterator iterator = menuEntries.iterator(); iterator.hasNext(); ) { +// final Entry entry = iterator.next(); +// if (entry == menuEntry) { +// iterator.remove(); +// entry.remove(); +// break; +// } +// } +// } +// +// // now check to see if a spacer is at the top/bottom of the list (and remove it if so. This is a recursive function. +// if (!menuEntries.isEmpty()) { +// if (menuEntries.get(0) instanceof dorkbox.systemTray.Separator) { +// remove(menuEntries.get(0)); +// } +// } +// // now check to see if a spacer is at the top/bottom of the list (and remove it if so. This is a recursive function. +// if (!menuEntries.isEmpty()) { +// if (menuEntries.get(menuEntries.size()-1) instanceof dorkbox.systemTray.Separator) { +// remove(menuEntries.get(menuEntries.size() - 1)); +// } +// } +// } + } catch (Exception e) { + SystemTray.logger.error("Error removing entry from menu.", e); + } + } } diff --git a/src/dorkbox/systemTray/swingUI/SwingMenuItem.java b/src/dorkbox/systemTray/swingUI/SwingMenuItem.java index 3383983..ff32a9b 100644 --- a/src/dorkbox/systemTray/swingUI/SwingMenuItem.java +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItem.java @@ -33,7 +33,6 @@ class SwingMenuItem implements MenuItemHook { private final SwingMenu parent; private final JMenuItem _native = new AdjustedJMenuItem(); - private volatile boolean hasLegitIcon = false; private volatile ActionListener swingCallback; // this is ALWAYS called on the EDT. @@ -42,16 +41,10 @@ class SwingMenuItem implements MenuItemHook { parent._native.add(_native); } - public - boolean hasImage() { - return hasLegitIcon; - } - @Override public void setImage(final MenuItem menuItem) { final File imageFile = menuItem.getImage(); - hasLegitIcon = imageFile != null; SwingUtil.invokeLater(new Runnable() { @Override @@ -92,6 +85,7 @@ class SwingMenuItem implements MenuItemHook { }); } + @SuppressWarnings("Duplicates") @Override public void setCallback(final MenuItem menuItem) { @@ -100,7 +94,6 @@ class SwingMenuItem implements MenuItemHook { } if (menuItem.getCallback() != null) { - _native.setEnabled(true); swingCallback = new ActionListener() { @Override public @@ -120,7 +113,6 @@ class SwingMenuItem implements MenuItemHook { _native.addActionListener(swingCallback); } else { - _native.setEnabled(false); swingCallback = null; } } diff --git a/src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java b/src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java index 3ce13d6..41c665c 100644 --- a/src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java @@ -56,13 +56,6 @@ class SwingMenuItemCheckbox implements MenuCheckboxHook { } } - // checkbox image is always present - public - boolean hasImage() { - return true; - } - - @Override public void setEnabled(final Checkbox menuItem) { diff --git a/src/dorkbox/systemTray/swingUI/SwingMenuItemSeparator.java b/src/dorkbox/systemTray/swingUI/SwingMenuItemSeparator.java index 3afa4b0..cfe736d 100644 --- a/src/dorkbox/systemTray/swingUI/SwingMenuItemSeparator.java +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItemSeparator.java @@ -31,11 +31,6 @@ class SwingMenuItemSeparator implements EntryHook { parent._native.add(_native); } - public - boolean hasImage() { - return false; - } - @Override public void remove() { diff --git a/src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java b/src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java index 1630f58..6cfd156 100644 --- a/src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java @@ -36,12 +36,6 @@ class SwingMenuItemStatus implements MenuStatusHook { parent._native.add(_native, 0); } - public - boolean hasImage() { - return false; - } - - @Override public void setText(final Status menuItem) { diff --git a/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java b/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java index 63f54c1..f7627ea 100644 --- a/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java +++ b/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java @@ -86,7 +86,7 @@ public class _AppIndicatorTray extends Tray implements SwingUI { private volatile AppIndicatorInstanceStruct appIndicator; private boolean isActive = false; - private final Runnable popupRunnable; + private volatile Runnable popupRunnable; // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) private AtomicBoolean shuttingDown = new AtomicBoolean(); @@ -113,142 +113,6 @@ class _AppIndicatorTray extends Tray implements SwingUI { _AppIndicatorTray(final SystemTray systemTray) { super(); - // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. - final SwingMenu swingMenu = new SwingMenu(null) { - @Override - public - void setEnabled(final MenuItem menuItem) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - boolean enabled = menuItem.getEnabled(); - - if (visible && !enabled) { - // STATUS_PASSIVE hides the indicator - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE); - visible = false; - } - else if (!visible && enabled) { - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); - visible = true; - } - } - }); - } - - @Override - public - void setImage(final MenuItem menuItem) { - image = menuItem.getImage(); - if (image == null) { - return; - } - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - AppIndicator.app_indicator_set_icon(appIndicator, image.getAbsolutePath()); - - if (!isActive) { - isActive = true; - - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); - - // now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set - hookMenuOpen(); - } - } - }); - - - // needs to be on EDT - dispatch(new Runnable() { - @Override - public - void run() { - ((TrayPopup) _native).setTitleBarImage(image); - } - }); - } - - @Override - public - void setText(final MenuItem menuItem) { - // no op - } - - @Override - public - void setShortcut(final MenuItem menuItem) { - // no op - } - - @Override - public - void remove() { - if (!shuttingDown.getAndSet(true)) { - // must happen asap, so our hook properly notices we are in shutdown mode - final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; - appIndicator = null; - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - // STATUS_PASSIVE hides the indicator - AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); - Pointer p = savedAppIndicator.getPointer(); - Gobject.g_object_unref(p); - } - }); - - // does not need to be called on the dispatch (it does that) - Gtk.shutdownGui(); - - super.remove(); - } - } - }; - - - TrayPopup popupMenu = (TrayPopup) swingMenu._native; - popupMenu.pack(); - popupMenu.setFocusable(true); - popupMenu.setOnHideRunnable(new Runnable() { - @Override - public - void run() { - if (appIndicator == null) { - // if we are shutting down, don't hook the menu again - return; - } - - // Such ugly hacks to get AppIndicator support properly working. This is so horrible I am ashamed. - Gtk.dispatchAndWait(new Runnable() { - @Override - public - void run() { - createAppIndicatorMenu(); - hookMenuOpen(); - } - }); - } - }); - - popupRunnable = new Runnable() { - @Override - public - void run() { - Point point = MouseInfo.getPointerInfo() - .getLocation(); - - TrayPopup popupMenu = (TrayPopup) swingMenu._native; - popupMenu.doShow(point, SystemTray.DEFAULT_TRAY_SIZE); - } - }; - Gtk.startGui(); Gtk.dispatch(new Runnable() { @@ -266,7 +130,148 @@ class _AppIndicatorTray extends Tray implements SwingUI { Gtk.waitForStartup(); - bind(swingMenu, null, systemTray); + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + final SwingMenu swingMenu = new SwingMenu(null) { + @Override + public + void setEnabled(final MenuItem menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + boolean enabled = menuItem.getEnabled(); + + if (visible && !enabled) { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE); + visible = false; + } + else if (!visible && enabled) { + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + visible = true; + } + } + }); + } + + @Override + public + void setImage(final MenuItem menuItem) { + image = menuItem.getImage(); + if (image == null) { + return; + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + AppIndicator.app_indicator_set_icon(appIndicator, image.getAbsolutePath()); + + if (!isActive) { + isActive = true; + + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + + // now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set + hookMenuOpen(); + } + } + }); + + + // needs to be on EDT + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + ((TrayPopup) _native).setTitleBarImage(image); + } + }); + } + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + if (!shuttingDown.getAndSet(true)) { + // must happen asap, so our hook properly notices we are in shutdown mode + final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; + appIndicator = null; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); + Pointer p = savedAppIndicator.getPointer(); + Gobject.g_object_unref(p); + } + }); + + // does not need to be called on the dispatch (it does that) + Gtk.shutdownGui(); + + super.remove(); + } + } + }; + + TrayPopup popupMenu = (TrayPopup) swingMenu._native; + popupMenu.pack(); + popupMenu.setFocusable(true); + popupMenu.setOnHideRunnable(new Runnable() { + @Override + public + void run() { + if (appIndicator == null) { + // if we are shutting down, don't hook the menu again + return; + } + + // Such ugly hacks to get AppIndicator support properly working. This is so horrible I am ashamed. + Gtk.dispatchAndWait(new Runnable() { + @Override + public + void run() { + createAppIndicatorMenu(); + hookMenuOpen(); + } + }); + } + }); + + popupRunnable = new Runnable() { + @Override + public + void run() { + Point point = MouseInfo.getPointerInfo() + .getLocation(); + + TrayPopup popupMenu = (TrayPopup) swingMenu._native; + popupMenu.doShow(point, SystemTray.DEFAULT_TRAY_SIZE); + } + }; + + bind(swingMenu, null, systemTray); + } + }); } private diff --git a/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java b/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java index 2356737..2c81933 100644 --- a/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java +++ b/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java @@ -34,6 +34,7 @@ import dorkbox.systemTray.jna.linux.GEventCallback; import dorkbox.systemTray.jna.linux.GdkEventButton; import dorkbox.systemTray.jna.linux.Gobject; import dorkbox.systemTray.jna.linux.Gtk; +import dorkbox.util.SwingUtil; /** * Class for handling all system tray interactions via GTK. @@ -59,130 +60,20 @@ class _GtkStatusIconTray extends Tray implements SwingUI { // is the system tray visible or not. private volatile boolean visible = true; private volatile File image; + private volatile Runnable popupRunnable; // called on the EDT public _GtkStatusIconTray(final SystemTray systemTray) { super(); - // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. - final SwingMenu swingMenu = new SwingMenu(null) { - @Override - public - void setEnabled(final MenuItem menuItem) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - boolean enabled = menuItem.getEnabled(); - - if (visible && !enabled) { - Gtk.gtk_status_icon_set_visible(trayIcon, enabled); - visible = false; - } - else if (!visible && enabled) { - Gtk.gtk_status_icon_set_visible(trayIcon, enabled); - visible = true; - } - } - }); - } - - @Override - public - void setImage(final MenuItem menuItem) { - image = menuItem.getImage(); - if (image == null) { - return; - } - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - Gtk.gtk_status_icon_set_from_file(trayIcon, image.getAbsolutePath()); - - if (!isActive) { - isActive = true; - Gtk.gtk_status_icon_set_visible(trayIcon, true); - } - } - }); - - // needs to be on EDT - dispatch(new Runnable() { - @Override - public - void run() { - ((TrayPopup) _native).setTitleBarImage(image); - } - }); - } - - @Override - public - void setText(final MenuItem menuItem) { - // no op - } - - @Override - public - void setShortcut(final MenuItem menuItem) { - // no op - } - - @Override - public - void remove() { - // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) - if (!shuttingDown.getAndSet(true)) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - // this hides the indicator - Gtk.gtk_status_icon_set_visible(trayIcon, false); - Gobject.g_object_unref(trayIcon); - - // mark for GC - trayIcon = null; - gtkCallbacks.clear(); - } - }); - - // does not need to be called on the dispatch (it does that) - Gtk.shutdownGui(); - - super.remove(); - } - } - }; - - - JPopupMenu popupMenu = (JPopupMenu) swingMenu._native; - popupMenu.pack(); - popupMenu.setFocusable(true); - - final Runnable popupRunnable = new Runnable() { - @Override - public - void run() { - Point point = MouseInfo.getPointerInfo() - .getLocation(); - - TrayPopup popupMenu = (TrayPopup) swingMenu._native; - popupMenu.doShow(point, 0); - } - }; - Gtk.startGui(); Gtk.dispatch(new Runnable() { @Override public void run() { - final Pointer trayIcon_ = Gtk.gtk_status_icon_new(); - trayIcon = trayIcon_; + trayIcon = Gtk.gtk_status_icon_new(); final GEventCallback gtkCallback = new GEventCallback() { @Override @@ -192,7 +83,7 @@ class _GtkStatusIconTray extends Tray implements SwingUI { // BUTTON_PRESS only (any mouse click) if (event.type == 4) { // show the swing menu on the EDT - swingMenu.dispatch(popupRunnable); + SwingUtil.invokeLater(popupRunnable); } } }; @@ -233,7 +124,124 @@ class _GtkStatusIconTray extends Tray implements SwingUI { } }); - bind(swingMenu, null, systemTray); + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + final SwingMenu swingMenu = new SwingMenu(null) { + @Override + public + void setEnabled(final MenuItem menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + boolean enabled = menuItem.getEnabled(); + + if (visible && !enabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, enabled); + visible = false; + } + else if (!visible && enabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, enabled); + visible = true; + } + } + }); + } + + @Override + public + void setImage(final MenuItem menuItem) { + image = menuItem.getImage(); + if (image == null) { + return; + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_status_icon_set_from_file(trayIcon, image.getAbsolutePath()); + + if (!isActive) { + isActive = true; + Gtk.gtk_status_icon_set_visible(trayIcon, true); + } + } + }); + + // needs to be on EDT + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + ((TrayPopup) _native).setTitleBarImage(image); + } + }); + } + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) + if (!shuttingDown.getAndSet(true)) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // this hides the indicator + Gtk.gtk_status_icon_set_visible(trayIcon, false); + Gobject.g_object_unref(trayIcon); + + // mark for GC + trayIcon = null; + gtkCallbacks.clear(); + } + }); + + // does not need to be called on the dispatch (it does that) + Gtk.shutdownGui(); + + super.remove(); + } + } + }; + + + JPopupMenu popupMenu = (JPopupMenu) swingMenu._native; + popupMenu.pack(); + popupMenu.setFocusable(true); + + popupRunnable = new Runnable() { + @Override + public + void run() { + Point point = MouseInfo.getPointerInfo() + .getLocation(); + + TrayPopup popupMenu = (TrayPopup) swingMenu._native; + popupMenu.doShow(point, 0); + } + }; + + + bind(swingMenu, null, systemTray); + } + }); } @Override diff --git a/src/dorkbox/systemTray/swingUI/_SwingTray.java b/src/dorkbox/systemTray/swingUI/_SwingTray.java index 0fc493e..ec88286 100644 --- a/src/dorkbox/systemTray/swingUI/_SwingTray.java +++ b/src/dorkbox/systemTray/swingUI/_SwingTray.java @@ -28,6 +28,7 @@ import javax.swing.JPopupMenu; import dorkbox.systemTray.MenuItem; import dorkbox.systemTray.Tray; +import dorkbox.util.SwingUtil; /** * Class for handling all system tray interaction, via Swing. @@ -63,7 +64,7 @@ class _SwingTray extends Tray implements SwingUI { @Override public void setEnabled(final MenuItem menuItem) { - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -93,7 +94,7 @@ class _SwingTray extends Tray implements SwingUI { return; } - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -151,7 +152,7 @@ class _SwingTray extends Tray implements SwingUI { @Override public void remove() { - dispatch(new Runnable() { + SwingUtil.invokeLater(new Runnable() { @Override public void run() { @@ -168,7 +169,6 @@ class _SwingTray extends Tray implements SwingUI { } }; - bind(swingMenu, null, systemTray); } diff --git a/src/dorkbox/systemTray/util/MenuBase.java b/src/dorkbox/systemTray/util/MenuBase.java index b064d08..dfad421 100644 --- a/src/dorkbox/systemTray/util/MenuBase.java +++ b/src/dorkbox/systemTray/util/MenuBase.java @@ -16,11 +16,7 @@ package dorkbox.systemTray.util; -import java.util.Iterator; - -import dorkbox.systemTray.Entry; import dorkbox.systemTray.Menu; -import dorkbox.systemTray.SystemTray; // this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both @SuppressWarnings("ForLoopReplaceableByForEach") @@ -202,41 +198,7 @@ class MenuBase extends Menu { // } // } - // NOT ALWAYS CALLED ON EDT - protected - void remove__(final Object menuEntry) { - try { - synchronized (menuEntries) { - // null is passed in when a sub-menu is removing itself from us (because they have already called "remove" and have also - // removed themselves from the menuEntries) - if (menuEntry != null) { - for (Iterator iterator = menuEntries.iterator(); iterator.hasNext(); ) { - final Entry entry = iterator.next(); - if (entry == menuEntry) { - iterator.remove(); - entry.remove(); - break; - } - } - } - // now check to see if a spacer is at the top/bottom of the list (and remove it if so. This is a recursive function. - if (!menuEntries.isEmpty()) { - if (menuEntries.get(0) instanceof dorkbox.systemTray.Separator) { - remove(menuEntries.get(0)); - } - } - // now check to see if a spacer is at the top/bottom of the list (and remove it if so. This is a recursive function. - if (!menuEntries.isEmpty()) { - if (menuEntries.get(menuEntries.size()-1) instanceof dorkbox.systemTray.Separator) { - remove(menuEntries.get(menuEntries.size() - 1)); - } - } - } - } catch (Exception e) { - SystemTray.logger.error("Error removing entry from menu.", e); - } - } // /** // * This removes a menu entry or sub-menu (via the text label) from the dropdown menu.