Fininshed round 2 of API changes (native/swing UI finished)

This commit is contained in:
nathan 2016-10-23 23:27:13 +02:00
parent d665f29f28
commit daff5a8e48
41 changed files with 3127 additions and 2949 deletions

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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<Entry> menuEntries = new ArrayList<Entry>();
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
*
* <Status> <Status> <Status>
* Entry1 Entry1 Entry1
* Entry2 -> Entry2 -> Entry2
* <Spacer> (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 extends Entry> T add(final T entry) {
return add(entry, -1);
}
/**
* Adds a menu entry, separator, or sub-menu to this menu.
*/
public final
<T extends Entry> 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<Entry> 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<Entry> menuEntriesCopy = new ArrayList<Entry>(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();
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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
* <p>
* 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
* <p>
* 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
* <p>
* 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
* <p>
* 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();
}
}

View File

@ -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.
* <p>
* When menu entries are removed, any menu spacer that ends up at the top/bottom of the menu will also be removed.
* <p>
* For example:
* <pre> {@code
* Original Entry3 deleted Result
*
* <Status> <Status> <Status>
* Entry1 Entry1 Entry1
* Entry2 -> Entry2 -> Entry2
* <Spacer> (deleted)
* Entry3 (deleted)
*
* }</pre>
*/
public
interface Separator {
class Separator extends Entry {
public
Separator() {
super();
}
}

View File

@ -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 {
* <p>
* 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<? extends Menu> selectType(final boolean useNativeMenus, final TrayType trayType) throws Exception {
Class<? extends Tray> 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<? extends Menu> selectTypeQuietly(final boolean useNativeMenus, final TrayType trayType) {
Class<? extends Tray> 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<? extends Menu> trayType = null;
Class<? extends Tray> trayType = null;
if (DEBUG) {
logger.debug("OS: {}", System.getProperty("os.name"));
@ -512,7 +512,7 @@ class SystemTray implements Menu {
systemTrayMenu = null;
}
else {
final AtomicReference<Menu> reference = new AtomicReference<Menu>();
final AtomicReference<Tray> reference = new AtomicReference<Tray>();
/*
* 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<? extends Menu> 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
*
* <Status> <Status> <Status>
* Entry1 Entry1 Entry1
* Entry2 -> Entry2 -> Entry2
* <Spacer> (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
* <p>
* 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
* <p>
* 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
*<p>
* 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.
* <p>
* 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
* <p>
* 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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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) {
}
}

View File

@ -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<Entry> value = new AtomicReference<Entry>();
// 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<Checkbox> value = new AtomicReference<Checkbox>();
// 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<Menu> value = new AtomicReference<Menu>();
// 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<Entry> value = new AtomicReference<Entry>();
//
// 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);
}

View File

@ -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();
}
});
}
}

View File

@ -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();
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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;
}
}

View File

@ -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.
* <p>
* 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;
}
}
}

View File

@ -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.
* <p>
* 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;
}
}
}

View File

@ -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) {
}
}

View File

@ -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) {
}
}

View File

@ -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<GtkMenuBaseItem> menuEntries = new LinkedList<GtkMenuBaseItem>();
// 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<Entry> value = new AtomicReference<Entry>();
// 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<Checkbox> value = new AtomicReference<Checkbox>();
// 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<Menu> value = new AtomicReference<Menu>();
// 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<Entry> menuEntriesCopy = new ArrayList<Entry>(this.menuEntries);
ArrayList<GtkMenuBaseItem> menuEntriesCopy = new ArrayList<GtkMenuBaseItem>(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<Entry> 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();

View File

@ -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.
* <p>
* 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;
}
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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.
* <p>

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<Entry> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -31,11 +31,6 @@ class SwingMenuItemSeparator implements EntryHook {
parent._native.add(_native);
}
public
boolean hasImage() {
return false;
}
@Override
public
void remove() {

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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<Entry> 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.