Fininshed round 2 of API changes (native/swing UI finished)
This commit is contained in:
parent
d665f29f28
commit
daff5a8e48
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
375
src/dorkbox/systemTray/MenuItem.java
Normal file
375
src/dorkbox/systemTray/MenuItem.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,30 +699,13 @@ 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 "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a 'status' string at the first position in the popup menu. This 'status' string appears as a disabled menu entry.
|
||||
|
@ -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);
|
||||
final Menu menu = systemTrayMenu;
|
||||
if (menu != null) {
|
||||
menu.setImage(image, cacheImage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
68
src/dorkbox/systemTray/Tray.java
Normal file
68
src/dorkbox/systemTray/Tray.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,10 +326,13 @@ class Gtk {
|
|||
void run() {
|
||||
isDispatch = true;
|
||||
|
||||
try {
|
||||
runnable.run();
|
||||
|
||||
} finally {
|
||||
isDispatch = false;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -343,25 +348,28 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -427,12 +435,10 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This would NORMALLY have a 2nd argument that is a String[] -- however JNA direct-mapping DOES NOT support this. We are lucky
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -18,302 +18,113 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* NOT ALWAYS CALLED ON EDT
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
void addSeparator() {
|
||||
dispatch(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);
|
||||
if (parent == null) {
|
||||
this._native = new PopupMenu();
|
||||
}
|
||||
else {
|
||||
// set text
|
||||
menuEntry.setText(statusText);
|
||||
this._native = new java.awt.Menu();
|
||||
parent._native.add(this._native);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// create a new one
|
||||
menuEntry = new AwtEntryStatus(_this, statusText);
|
||||
// status is ALWAYS at 0 index...
|
||||
menuEntries.add(0, menuEntry);
|
||||
@Override
|
||||
public
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
void setShortcut(final char key) {
|
||||
if (!(_native instanceof PopupMenu)) {
|
||||
// yikes...
|
||||
final int vKey = SystemTrayFixes.getVirtualKey(key);
|
||||
public
|
||||
void setImage(final MenuItem menuItem) {
|
||||
// no op. You can't have images in an awt menu
|
||||
}
|
||||
|
||||
dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void setEnabled(final MenuItem menuItem) {
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
_native.setEnabled(menuItem.getEnabled());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setText(final MenuItem menuItem) {
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
_native.setLabel(menuItem.getText());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
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() {
|
||||
|
@ -321,16 +132,19 @@ class AwtMenu extends MenuBase implements NativeUI {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
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);
|
||||
}
|
||||
|
|
139
src/dorkbox/systemTray/nativeUI/AwtMenuItem.java
Normal file
139
src/dorkbox/systemTray/nativeUI/AwtMenuItem.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
146
src/dorkbox/systemTray/nativeUI/AwtMenuItemCheckbox.java
Normal file
146
src/dorkbox/systemTray/nativeUI/AwtMenuItemCheckbox.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
|
||||
void remove() {
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
boolean hasImage() {
|
||||
return false;
|
||||
void run() {
|
||||
parent._native.remove(_native);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setCallback(final ActionListener callback) {
|
||||
});
|
||||
}
|
||||
}
|
74
src/dorkbox/systemTray/nativeUI/AwtMenuItemStatus.java
Normal file
74
src/dorkbox/systemTray/nativeUI/AwtMenuItemStatus.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
final GtkMenuBaseItem menuEntry__ = menuEntries.get(i);
|
||||
menuEntry__.onDeleteMenu(_nativeMenu);
|
||||
}
|
||||
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);
|
||||
Gtk.gtk_widget_destroy(_nativeMenu);
|
||||
}
|
||||
}
|
||||
|
||||
Gtk.gtk_widget_destroy(_native);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (parent != null) {
|
||||
// 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;
|
||||
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();
|
||||
|
|
118
src/dorkbox/systemTray/nativeUI/GtkMenuBaseItem.java
Normal file
118
src/dorkbox/systemTray/nativeUI/GtkMenuBaseItem.java
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
206
src/dorkbox/systemTray/nativeUI/GtkMenuItem.java
Normal file
206
src/dorkbox/systemTray/nativeUI/GtkMenuItem.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
200
src/dorkbox/systemTray/nativeUI/GtkMenuItemCheckbox.java
Normal file
200
src/dorkbox/systemTray/nativeUI/GtkMenuItemCheckbox.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
74
src/dorkbox/systemTray/nativeUI/GtkMenuItemSeparator.java
Normal file
74
src/dorkbox/systemTray/nativeUI/GtkMenuItemSeparator.java
Normal 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);
|
||||
}
|
||||
}
|
90
src/dorkbox/systemTray/nativeUI/GtkMenuItemStatus.java
Normal file
90
src/dorkbox/systemTray/nativeUI/GtkMenuItemStatus.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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,26 +94,82 @@ 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() {
|
||||
// we initialize with a blank image
|
||||
File image = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE);
|
||||
String id = System.nanoTime() + "DBST";
|
||||
appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS);
|
||||
AppIndicator.app_indicator_set_icon(appIndicator, image.getAbsolutePath());
|
||||
|
||||
if (!isActive) {
|
||||
isActive = true;
|
||||
|
||||
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Gtk.waitForStartup();
|
||||
}
|
||||
|
||||
public final
|
||||
void shutdown() {
|
||||
@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;
|
||||
|
@ -126,60 +186,33 @@ class _AppIndicatorNativeTray extends GtkMenu {
|
|||
}
|
||||
});
|
||||
|
||||
super.shutdown();
|
||||
super.remove();
|
||||
|
||||
// does not need to be called on the dispatch (it does that)
|
||||
Gtk.shutdownGui();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
// we initialize with a blank image
|
||||
File image = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE);
|
||||
String id = System.nanoTime() + "DBST";
|
||||
appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS);
|
||||
}
|
||||
});
|
||||
|
||||
Gtk.waitForStartup();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,57 +63,18 @@ class _AwtTray extends AwtMenu {
|
|||
}
|
||||
|
||||
_AwtTray.this.tray = SystemTray.getSystemTray();
|
||||
}
|
||||
|
||||
// we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization.
|
||||
final AwtMenu awtMenu = new AwtMenu(null) {
|
||||
@Override
|
||||
public
|
||||
void shutdown() {
|
||||
dispatchAndWait(new Runnable() {
|
||||
void setEnabled(final MenuItem menuItem) {
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
removeAll();
|
||||
remove();
|
||||
boolean enabled = menuItem.getEnabled();
|
||||
|
||||
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) {
|
||||
|
@ -117,7 +83,7 @@ class _AwtTray extends AwtMenu {
|
|||
}
|
||||
keepAliveThread = null;
|
||||
|
||||
if (visible && !setEnabled) {
|
||||
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() {
|
||||
|
@ -133,36 +99,94 @@ class _AwtTray extends AwtMenu {
|
|||
}
|
||||
}
|
||||
}
|
||||
}, "KeepAliveThread");
|
||||
}, "TrayKeepAliveThread");
|
||||
keepAliveThread.start();
|
||||
}
|
||||
|
||||
synchronized (keepAliveLock) {
|
||||
try {
|
||||
keepAliveLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
if (visible && !setEnabled) {
|
||||
if (visible && !enabled) {
|
||||
tray.remove(trayIcon);
|
||||
visible = false;
|
||||
}
|
||||
else if (!visible && setEnabled) {
|
||||
else if (!visible && enabled) {
|
||||
try {
|
||||
tray.add(trayIcon);
|
||||
visible = true;
|
||||
} catch (AWTException e) {
|
||||
dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray");
|
||||
dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void setImage(final MenuItem menuItem) {
|
||||
final File image = menuItem.getImage();
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -31,11 +31,6 @@ class SwingMenuItemSeparator implements EntryHook {
|
|||
parent._native.add(_native);
|
||||
}
|
||||
|
||||
public
|
||||
boolean hasImage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void remove() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,6 +113,27 @@ class _AppIndicatorTray extends Tray implements SwingUI {
|
|||
_AppIndicatorTray(final SystemTray systemTray) {
|
||||
super();
|
||||
|
||||
Gtk.startGui();
|
||||
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
// we initialize with a blank image
|
||||
File image = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE);
|
||||
String id = System.nanoTime() + "DBST";
|
||||
appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS);
|
||||
|
||||
createAppIndicatorMenu();
|
||||
}
|
||||
});
|
||||
|
||||
Gtk.waitForStartup();
|
||||
|
||||
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
|
||||
|
@ -164,7 +185,7 @@ class _AppIndicatorTray extends Tray implements SwingUI {
|
|||
|
||||
|
||||
// needs to be on EDT
|
||||
dispatch(new Runnable() {
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
|
@ -212,7 +233,6 @@ class _AppIndicatorTray extends Tray implements SwingUI {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
TrayPopup popupMenu = (TrayPopup) swingMenu._native;
|
||||
popupMenu.pack();
|
||||
popupMenu.setFocusable(true);
|
||||
|
@ -249,24 +269,9 @@ class _AppIndicatorTray extends Tray implements SwingUI {
|
|||
}
|
||||
};
|
||||
|
||||
Gtk.startGui();
|
||||
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
// we initialize with a blank image
|
||||
File image = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE);
|
||||
String id = System.nanoTime() + "DBST";
|
||||
appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS);
|
||||
|
||||
createAppIndicatorMenu();
|
||||
bind(swingMenu, null, systemTray);
|
||||
}
|
||||
});
|
||||
|
||||
Gtk.waitForStartup();
|
||||
|
||||
bind(swingMenu, null, systemTray);
|
||||
}
|
||||
|
||||
private
|
||||
|
|
|
@ -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,13 +60,75 @@ 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();
|
||||
|
||||
Gtk.startGui();
|
||||
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
trayIcon = Gtk.gtk_status_icon_new();
|
||||
|
||||
final GEventCallback gtkCallback = new GEventCallback() {
|
||||
@Override
|
||||
public
|
||||
void callback(Pointer notUsed, final GdkEventButton event) {
|
||||
// show the swing menu on the EDT
|
||||
// BUTTON_PRESS only (any mouse click)
|
||||
if (event.type == 4) {
|
||||
// show the swing menu on the EDT
|
||||
SwingUtil.invokeLater(popupRunnable);
|
||||
}
|
||||
}
|
||||
};
|
||||
final NativeLong button_press_event = Gobject.g_signal_connect_object(trayIcon, "button_press_event",
|
||||
gtkCallback, null, 0);
|
||||
|
||||
// have to do this to prevent GC on these objects
|
||||
gtkCallbacks.add(gtkCallback);
|
||||
gtkCallbacks.add(button_press_event);
|
||||
}
|
||||
});
|
||||
|
||||
Gtk.waitForStartup();
|
||||
|
||||
// we have to be able to set our title, otherwise the gnome-shell extension WILL NOT work
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
// by default, the title/name of the tray icon is "java". We are the only java-based tray icon, so we just use that.
|
||||
// If you change "SystemTray" to something else, make sure to change it in extension.js as well
|
||||
|
||||
// necessary for gnome icon detection/placement because we move tray icons around by title. This is hardcoded
|
||||
// in extension.js, so don't change it
|
||||
Gtk.gtk_status_icon_set_title(trayIcon, "SystemTray");
|
||||
|
||||
// can cause
|
||||
// Gdk-CRITICAL **: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed
|
||||
// Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed
|
||||
|
||||
// ... so, bizzaro things going on here. These errors DO NOT happen if JavaFX is dispatching the events.
|
||||
// BUT this is REQUIRED when running JavaFX. For unknown reasons, the title isn't pushed to GTK, so our
|
||||
// gnome-shell extension cannot see our tray icon -- so naturally, it won't move it to the "top" area and
|
||||
// we appear broken.
|
||||
if (SystemTray.isJavaFxLoaded) {
|
||||
Gtk.gtk_status_icon_set_name(trayIcon, "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
|
||||
|
@ -110,7 +173,7 @@ class _GtkStatusIconTray extends Tray implements SwingUI {
|
|||
});
|
||||
|
||||
// needs to be on EDT
|
||||
dispatch(new Runnable() {
|
||||
SwingUtil.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
|
@ -163,7 +226,7 @@ class _GtkStatusIconTray extends Tray implements SwingUI {
|
|||
popupMenu.pack();
|
||||
popupMenu.setFocusable(true);
|
||||
|
||||
final Runnable popupRunnable = new Runnable() {
|
||||
popupRunnable = new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
|
@ -175,66 +238,11 @@ class _GtkStatusIconTray extends Tray implements SwingUI {
|
|||
}
|
||||
};
|
||||
|
||||
Gtk.startGui();
|
||||
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
final Pointer trayIcon_ = Gtk.gtk_status_icon_new();
|
||||
trayIcon = trayIcon_;
|
||||
|
||||
final GEventCallback gtkCallback = new GEventCallback() {
|
||||
@Override
|
||||
public
|
||||
void callback(Pointer notUsed, final GdkEventButton event) {
|
||||
// show the swing menu on the EDT
|
||||
// BUTTON_PRESS only (any mouse click)
|
||||
if (event.type == 4) {
|
||||
// show the swing menu on the EDT
|
||||
swingMenu.dispatch(popupRunnable);
|
||||
}
|
||||
}
|
||||
};
|
||||
final NativeLong button_press_event = Gobject.g_signal_connect_object(trayIcon, "button_press_event",
|
||||
gtkCallback, null, 0);
|
||||
|
||||
// have to do this to prevent GC on these objects
|
||||
gtkCallbacks.add(gtkCallback);
|
||||
gtkCallbacks.add(button_press_event);
|
||||
}
|
||||
});
|
||||
|
||||
Gtk.waitForStartup();
|
||||
|
||||
// we have to be able to set our title, otherwise the gnome-shell extension WILL NOT work
|
||||
Gtk.dispatch(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
// by default, the title/name of the tray icon is "java". We are the only java-based tray icon, so we just use that.
|
||||
// If you change "SystemTray" to something else, make sure to change it in extension.js as well
|
||||
|
||||
// necessary for gnome icon detection/placement because we move tray icons around by title. This is hardcoded
|
||||
// in extension.js, so don't change it
|
||||
Gtk.gtk_status_icon_set_title(trayIcon, "SystemTray");
|
||||
|
||||
// can cause
|
||||
// Gdk-CRITICAL **: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed
|
||||
// Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed
|
||||
|
||||
// ... so, bizzaro things going on here. These errors DO NOT happen if JavaFX is dispatching the events.
|
||||
// BUT this is REQUIRED when running JavaFX. For unknown reasons, the title isn't pushed to GTK, so our
|
||||
// gnome-shell extension cannot see our tray icon -- so naturally, it won't move it to the "top" area and
|
||||
// we appear broken.
|
||||
if (SystemTray.isJavaFxLoaded) {
|
||||
Gtk.gtk_status_icon_set_name(trayIcon, "SystemTray");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bind(swingMenu, null, systemTray);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user