Code polish/refactor. Removed GTK menu stuff

This commit is contained in:
nathan 2016-10-09 22:40:28 +02:00
parent c2881e54fd
commit 2e9201f692
25 changed files with 369 additions and 1860 deletions

View File

@ -28,12 +28,12 @@ import java.net.URL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dorkbox.systemTray.linux.AppIndicatorTray;
import dorkbox.systemTray.linux.GnomeShellExtension;
import dorkbox.systemTray.linux.GtkSystemTray;
import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.swing.SwingSystemTray;
import dorkbox.systemTray.swing._AppIndicatorTray;
import dorkbox.systemTray.swing._GtkStatusIconTray;
import dorkbox.systemTray.swing._SwingTray;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.Swt;
@ -42,6 +42,7 @@ import dorkbox.util.CacheUtil;
import dorkbox.util.IO;
import dorkbox.util.OS;
import dorkbox.util.Property;
import dorkbox.util.SwingUtil;
import dorkbox.util.process.ShellProcessBuilder;
@ -104,7 +105,7 @@ class SystemTray extends Menu {
* <p>
* This is an advanced feature, and it is recommended to leave at 0.
*/
public static int FORCE_TRAY_TYPE = 0;
public static int FORCE_TRAY_TYPE = 2;
@Property
/**
@ -280,19 +281,19 @@ class SystemTray extends Menu {
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
try {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
} catch (Throwable e1) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e1);
logger.error("Cannot initialize _GtkStatusIconTray", e1);
}
}
}
else if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_APP_INDICATOR) {
try {
trayType = AppIndicatorTray.class;
trayType = _AppIndicatorTray.class;
} catch (Throwable e1) {
if (DEBUG) {
logger.error("Cannot initialize AppIndicatorTray", e1);
logger.error("Cannot initialize _AppIndicatorTray", e1);
}
}
}
@ -335,16 +336,26 @@ class SystemTray extends Menu {
if (DEBUG) {
logger.debug("Currently using the '{}' desktop", XDG);
// Properties properties = System.getProperties();
// for (Map.Entry<Object, Object> entry : properties.entrySet()) {
// logger.debug(entry.getKey() + " : " + entry.getValue());
// }
}
// must always be set in case of forced tray types
if ("kde".equalsIgnoreCase(XDG)) {
isKDE = true;
}
if (trayType == null) {
if ("unity".equalsIgnoreCase(XDG)) {
try {
trayType = AppIndicatorTray.class;
trayType = _AppIndicatorTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize AppIndicatorTray", e);
logger.error("Cannot initialize _AppIndicatorTray", e);
}
}
}
@ -353,75 +364,31 @@ class SystemTray extends Menu {
// 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
// XFCE4 is OK to use appindicator, <XFCE4 we use GTKStatusIcon. God i wish there was an easy way to do this.
boolean isNewXFCE = false;
// so far, it is OK to use GtkStatusIcon on XFCE <-> XFCE4 inclusive
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
PrintStream outputStream = new PrintStream(byteArrayOutputStream);
// ps aux | grep [x]fce
final ShellProcessBuilder shell = new ShellProcessBuilder(outputStream);
shell.setExecutable("ps");
shell.addArgument("aux");
shell.start();
String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
// should last us the next 20 years or so. XFCE development is glacially slow.
isNewXFCE = output.contains("/xfce4/") || output.contains("/xfce5/") ||
output.contains("/xfce6/") || output.contains("/xfce7/");
} catch (Throwable e) {
trayType = _GtkStatusIconTray.class;
} catch (Throwable e1) {
if (DEBUG) {
logger.error("Cannot detect what version of XFCE is running", e);
}
}
if (DEBUG) {
logger.error("Is 'new' version of XFCE? {}", isNewXFCE);
}
if (isNewXFCE) {
try {
trayType = AppIndicatorTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize AppIndicatorTray", e);
}
// we can fail on AppIndicator, so this is the fallback
try {
trayType = GtkSystemTray.class;
} catch (Throwable e1) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e1);
}
}
}
} else {
try {
trayType = GtkSystemTray.class;
} catch (Throwable e1) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e1);
}
logger.error("Cannot initialize _GtkStatusIconTray", e1);
}
}
}
else if ("lxde".equalsIgnoreCase(XDG)) {
try {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e);
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
}
else if ("kde".equalsIgnoreCase(XDG)) {
isKDE = true;
// kde (at least, plasma 5.5.6) requires appindicator
try {
trayType = AppIndicatorTray.class;
trayType = _AppIndicatorTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize AppIndicatorTray", e);
logger.error("Cannot initialize _AppIndicatorTray", e);
}
}
}
@ -435,34 +402,35 @@ class SystemTray extends Menu {
if ("cinnamon".equalsIgnoreCase(GDM)) {
try {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e);
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
}
else if ("gnome-classic".equalsIgnoreCase(GDM)) {
try {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e);
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
}
else if ("gnome-fallback".equalsIgnoreCase(GDM)) {
try {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e);
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
}
}
else if ("ubuntu".equalsIgnoreCase(GDM)) {
// have to install the gnome extension AND customize the restart command
trayType = null;
// unity panel service??
GnomeShellExtension.SHELL_RESTART_COMMAND = "unity --replace &";
}
}
@ -495,7 +463,7 @@ class SystemTray extends Menu {
GnomeShellExtension.install(output);
// we might be running gnome-shell, we MIGHT NOT. If we are forced to be app-indicator or swing, don't do this.
if (trayType == null) {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
}
}
} catch (Throwable e) {
@ -533,7 +501,7 @@ class SystemTray extends Menu {
if (readLine != null && readLine.contains("indicator-app")) {
// make sure we can also load the library (it might be the wrong version)
try {
trayType = AppIndicatorTray.class;
trayType = _AppIndicatorTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("AppIndicator support detected, but unable to load the library. Falling back to GTK", e);
@ -558,7 +526,7 @@ class SystemTray extends Menu {
// fallback...
if (trayType == null) {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " +
"configuration");
}
@ -575,7 +543,7 @@ class SystemTray extends Menu {
if (trayType == null && java.awt.SystemTray.isSupported()) {
try {
java.awt.SystemTray.getSystemTray();
trayType = SwingSystemTray.class;
trayType = _SwingTray.class;
} catch (Throwable e) {
if (DEBUG) {
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.", e);
@ -591,7 +559,7 @@ class SystemTray extends Menu {
systemTrayMenu = null;
}
else {
Menu menu_ = null;
final Menu[] menuReference = new Menu[1];
/*
* appIndicator/gtk require strings (which is the path)
@ -604,17 +572,17 @@ class SystemTray extends Menu {
try {
if (OS.isLinux() &&
trayType == AppIndicatorTray.class &&
trayType == _AppIndicatorTray.class &&
Gtk.isGtk2 &&
AppIndicator.isVersion3) {
try {
trayType = GtkSystemTray.class;
trayType = _GtkStatusIconTray.class;
logger.warn("AppIndicator3 detected with GTK2, falling back to GTK2 system tray type. " +
"Please install libappindicator1 OR GTK3, for example: 'sudo apt-get install libappindicator1'");
} catch (Throwable e) {
if (DEBUG) {
logger.error("Cannot initialize GtkSystemTray", e);
logger.error("Cannot initialize _GtkStatusIconTray", e);
}
logger.error("AppIndicator3 detected with GTK2 and unable to fallback to using GTK2 system tray type." +
"AppIndicator3 requires GTK3 to be fully functional, and while this will work -- " +
@ -623,14 +591,29 @@ class SystemTray extends Menu {
}
}
menu_ = (Menu) trayType.getConstructors()[0].newInstance(systemTray);
// need to set this
Gtk.isKDE = isKDE;
logger.info("Successfully Loaded: {}", trayType.getSimpleName());
// have to construct swing stuff inside the swing EDT
// this is the safest way to do this.
final Class<? extends Menu> finalTrayType = trayType;
SwingUtil.invokeAndWait(new Runnable() {
@Override
public
void run() {
try {
menuReference[0] = (Menu) finalTrayType.getConstructors()[0].newInstance(systemTray);
logger.info("Successfully Loaded: {}", finalTrayType.getSimpleName());
} catch (Exception e) {
logger.error("Unable to create tray type: '" + finalTrayType.getSimpleName() + "'", e);
}
}
});
} catch (Exception e) {
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
}
systemTrayMenu = menu_;
systemTrayMenu = menuReference[0];
// These install a shutdown hook in JavaFX/SWT, so that when the main window is closed -- the system tray is ALSO closed.
@ -698,14 +681,14 @@ class SystemTray extends Menu {
void shutdown() {
final Menu menu = systemTrayMenu;
if (menu instanceof AppIndicatorTray) {
((AppIndicatorTray) menu).shutdown();
if (menu instanceof _AppIndicatorTray) {
((_AppIndicatorTray) menu).shutdown();
}
else if (menu instanceof GtkSystemTray) {
((GtkSystemTray) menu).shutdown();
else if (menu instanceof _GtkStatusIconTray) {
((_GtkStatusIconTray) menu).shutdown();
} else {
// swing
((SwingSystemTray) menu).shutdown();
((_SwingTray) menu).shutdown();
}
}
@ -715,14 +698,14 @@ class SystemTray extends Menu {
public
String getStatus() {
final Menu menu = systemTrayMenu;
if (menu instanceof AppIndicatorTray) {
return ((AppIndicatorTray) menu).getStatus();
if (menu instanceof _AppIndicatorTray) {
return ((_AppIndicatorTray) menu).getStatus();
}
else if (menu instanceof GtkSystemTray) {
return ((GtkSystemTray) menu).getStatus();
else if (menu instanceof _GtkStatusIconTray) {
return ((_GtkStatusIconTray) menu).getStatus();
} else {
// swing
return ((SwingSystemTray) menu).getStatus();
return ((_SwingTray) menu).getStatus();
}
}
@ -734,28 +717,28 @@ class SystemTray extends Menu {
public
void setStatus(String statusText) {
final Menu menu = systemTrayMenu;
if (menu instanceof AppIndicatorTray) {
((AppIndicatorTray) menu).setStatus(statusText);
if (menu instanceof _AppIndicatorTray) {
((_AppIndicatorTray) menu).setStatus(statusText);
}
else if (menu instanceof GtkSystemTray) {
((GtkSystemTray) menu).setStatus(statusText);
else if (menu instanceof _GtkStatusIconTray) {
((_GtkStatusIconTray) menu).setStatus(statusText);
} else {
// swing
((SwingSystemTray) menu).setStatus(statusText);
((_SwingTray) menu).setStatus(statusText);
}
}
protected
void setImage_(File iconPath) {
final Menu menu = systemTrayMenu;
if (menu instanceof AppIndicatorTray) {
((AppIndicatorTray) menu).setImage_(iconPath);
if (menu instanceof _AppIndicatorTray) {
((_AppIndicatorTray) menu).setImage_(iconPath);
}
else if (menu instanceof GtkSystemTray) {
((GtkSystemTray) menu).setImage_(iconPath);
else if (menu instanceof _GtkStatusIconTray) {
((_GtkStatusIconTray) menu).setImage_(iconPath);
} else {
// swing (windows/mac)
((SwingSystemTray) menu).setImage_(iconPath);
((_SwingTray) menu).setImage_(iconPath);
}
}

View File

@ -1,132 +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.linux;
import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
import com.sun.jna.Pointer;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.AppIndicatorInstanceStruct;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
/**
* Class for handling all system tray interactions.
* <p/>
* specialization for using app indicators in ubuntu unity
* <p/>
* Derived from
* Lantern: https://github.com/getlantern/lantern/ Apache 2.0 License Copyright 2010 Brave New Software Project, Inc.
*
* AppIndicators DO NOT support anything other than PLAIN gtk-menus (because of how they use dbus)
* (so no tooltips AND no custom widgets)
*
* http://unity.ubuntu.com/projects/appindicators/
*
* http://askubuntu.com/questions/16431/putting-an-arbitrary-gtk-widget-into-an-appindicator-indicator
*
* If we re-implement appindicators dbus functionality, we could POTENTIALLY do whatever we want...
*
* https://developer.ubuntu.com/api/devel/ubuntu-12.04/c/dbusmenugtk/index.html
* https://launchpad.net/ido
* http://bazaar.launchpad.net/~canonical-dx-team/ido/trunk/view/head:/src/idoentrymenuitem.c
* http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files
*/
public
class AppIndicatorTray extends GtkTypeSystemTray {
private AppIndicatorInstanceStruct appIndicator;
private boolean isActive = false;
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
private AtomicBoolean shuttingDown = new AtomicBoolean();
public
AppIndicatorTray(final SystemTray systemTray) {
super(systemTray);
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
throw new IllegalArgumentException("Unable to start AppIndicator if 'SystemTray.FORCE_TRAY_TYPE' is set to GtkStatusIcon");
}
ImageUtils.determineIconSize();
Gtk.startGui();
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();
}
@Override
public
void shutdown() {
if (!shuttingDown.getAndSet(true)) {
dispatch(new Runnable() {
@Override
public
void run() {
// STATUS_PASSIVE hides the indicator
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE);
Pointer p = appIndicator.getPointer();
Gobject.g_object_unref(p);
appIndicator = null;
}
});
super.shutdown();
}
}
public
void setImage_(final File iconFile) {
dispatch(new Runnable() {
@Override
public
void run() {
AppIndicator.app_indicator_set_icon(appIndicator, iconFile.getAbsolutePath());
if (!isActive) {
isActive = true;
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE);
}
}
});
}
/**
* MUST BE AFTER THE ITEM IS ADDED/CHANGED from the menu
*/
protected
void onMenuAdded(final Pointer menu) {
// see: https://code.launchpad.net/~mterry/libappindicator/fix-menu-leak/+merge/53247
AppIndicator.app_indicator_set_menu(appIndicator, menu);
}
}

View File

@ -1,218 +0,0 @@
/*
* Copyright 2014 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.linux;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import com.sun.jna.Pointer;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
abstract
class GtkEntry implements MenuEntry {
private final int id = Menu.MENU_ID_COUNTER.getAndIncrement();
private final GtkMenu parent;
final Pointer _native;
// this have to be volatile, because they can be changed from any thread
private volatile String text;
/**
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
*/
GtkEntry(final GtkMenu parent, final Pointer menuItem) {
this.parent = parent;
this._native = menuItem;
}
public
Menu getParent() {
return parent;
}
/**
* the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images
*
* always called on the DISPATCH thread
*/
abstract
void setSpacerImage(final boolean everyoneElseHasImages);
/**
* must always be called in the GTK thread
*/
abstract
void renderText(final String text);
/**
* must always be called in the GTK thread
*/
abstract
void setImage_(final File imageFile);
/**
* must always be called in the GTK thread
* called when this item is removed. Necessary to cleanup/remove itself
*/
abstract
void removePrivate();
/**
* Enables, or disables the sub-menu entry.
*/
@Override
public
void setEnabled(final boolean enabled) {
if (enabled) {
Gtk.gtk_widget_set_sensitive(_native, Gtk.TRUE);
} else {
Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE);
}
}
@Override
public
void setShortcut(final char key) {
}
@Override
public
String getText() {
return text;
}
@Override
public final
void setText(final String newText) {
text = newText;
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
renderText(text);
}
});
}
@Override
public
void setImage(final File imageFile) {
if (imageFile == null) {
setImage_(null);
}
else {
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile));
}
}
@Override
public final
void setImage(final String imagePath) {
if (imagePath == null) {
setImage_(null);
}
else {
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath));
}
}
@Override
public final
void setImage(final URL imageUrl) {
if (imageUrl == null) {
setImage_(null);
}
else {
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl));
}
}
@Override
public final
void setImage(final String cacheName, final InputStream imageStream) {
if (imageStream == null) {
setImage_(null);
}
else {
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream));
}
}
@Override
public final
void setImage(final InputStream imageStream) {
if (imageStream == null) {
setImage_(null);
}
else {
setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream));
}
}
// a child will always remove itself from the parent.
@Override
public final
void remove() {
parent.dispatchAndWait(new Runnable() {
@Override
public
void run() {
Gtk.gtk_container_remove(parent._native, _native);
Gtk.gtk_menu_shell_deactivate(parent._native, _native);
removePrivate();
Gtk.gtk_widget_destroy(_native);
// have to rebuild the menu now...
parent.deleteMenu();
parent.createMenu();
}
});
}
@Override
public final
int hashCode() {
return id;
}
@Override
public final
boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
GtkEntry other = (GtkEntry) obj;
return this.id == other.id;
}
}

View File

@ -1,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.linux;
import java.io.File;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.GCallback;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.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 SystemTrayMenuAction 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 SystemTrayMenuAction 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 SystemTrayMenuAction callback) {
this.callback = callback;
}
// called by native code
@Override
public
int callback(final Pointer instance, final Pointer data) {
final SystemTrayMenuAction cb = this.callback;
if (cb != null) {
Gtk.proxyClick(getParent(), GtkEntryItem.this, cb);
}
return Gtk.TRUE;
}
@Override
public
boolean hasImage() {
return hasLegitIcon;
}
/**
* the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images.
* This is primarily only with AppIndicators, although not always.
* <p>
* called on the DISPATCH thread
*/
void setSpacerImage(final boolean everyoneElseHasImages) {
if (hasLegitIcon) {
// we have a legit icon, so there is nothing else we can do.
return;
}
if (image != null) {
Gtk.gtk_widget_destroy(image);
image = null;
Gtk.gtk_widget_show_all(_native);
}
if (everyoneElseHasImages) {
image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath());
Gtk.gtk_image_menu_item_set_image(_native, image);
// must always re-set always-show after setting the image
Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE);
}
Gtk.gtk_widget_show_all(_native);
}
/**
* must always be called in the GTK thread
*/
void renderText(String text) {
if (this.mnemonicKey != 0) {
// they are CASE INSENSITIVE!
int i = text.toLowerCase()
.indexOf(this.mnemonicKey);
if (i >= 0) {
text = text.substring(0, i) + "_" + text.substring(i);
}
}
Gtk.gtk_menu_item_set_label(_native, text);
Gtk.gtk_widget_show_all(_native);
}
// NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted.
// see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/
// see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25
void setImage_(final File imageFile) {
hasLegitIcon = imageFile != null;
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
if (image != null) {
Gtk.gtk_widget_destroy(image);
image = null;
Gtk.gtk_widget_show_all(_native);
}
if (imageFile != null) {
image = Gtk.gtk_image_new_from_file(imageFile.getAbsolutePath());
Gtk.gtk_image_menu_item_set_image(_native, image);
// must always re-set always-show after setting the image
Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE);
}
Gtk.gtk_widget_show_all(_native);
}
});
}
void removePrivate() {
callback = null;
if (image != null) {
Gtk.gtk_widget_destroy(image);
image = null;
}
}
}

View File

@ -1,66 +0,0 @@
/*
* Copyright 2014 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.linux;
import java.io.File;
import dorkbox.systemTray.MenuSpacer;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.Gtk;
class GtkEntrySeparator extends GtkEntry implements MenuSpacer {
/**
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
*/
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 SystemTrayMenuAction callback) {
}
@Override
public
void setEnabled(final boolean enabled) {
}
}

View File

@ -1,57 +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.linux;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.Gtk;
// you might wonder WHY this extends MenuEntryItem -- the reason is that an AppIndicator "status" will be offset from everyone else,
// where a GtkStatusIconTray + SwingTray 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 SystemTrayMenuAction callback) {
}
@Override
public
void setEnabled(final boolean enabled) {
}
}

View File

@ -1,483 +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.linux;
import static dorkbox.systemTray.SystemTray.TIMEOUT;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import com.sun.jna.Pointer;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
@SuppressWarnings("ForLoopReplaceableByForEach")
class GtkMenu extends Menu {
// menu entry that this menu is attached to. Will be NULL when it's the system tray
private final GtkEntryItem menuEntry;
// must ONLY be created at the end of delete!
volatile Pointer _native;
// have to make sure no other methods can call obliterate, delete, or create menu once it's already started
private boolean obliterateInProgress = false;
// called on dispatch
GtkMenu(final SystemTray systemTray, final GtkMenu parent) {
super(systemTray, 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);
} else {
this.menuEntry = null;
}
}
/**
* Necessary to guarantee all updates occur on the dispatch thread
*/
@Override
protected
void dispatch(final Runnable runnable) {
Gtk.dispatch(runnable);
}
/**
* Necessary to guarantee all updates occur on the dispatch thread
*/
@Override
protected
void dispatchAndWait(final Runnable runnable) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
try {
runnable.run();
} finally {
countDownLatch.countDown();
}
}
});
// this is slightly different than how swing does it. We have a timeout here so that we can make sure that updates on the GUI
// thread occur in REASONABLE time-frames, and alert the user if not.
try {
if (!countDownLatch.await(TIMEOUT, TimeUnit.SECONDS)) {
if (SystemTray.DEBUG) {
SystemTray.logger.error("Event dispatch queue took longer than " + TIMEOUT + " seconds to complete. Please adjust " +
"`SystemTray.TIMEOUT` to a value which better suites your environment.");
} else {
throw new RuntimeException("Event dispatch queue took longer than " + TIMEOUT + " seconds to complete. Please adjust " +
"`SystemTray.TIMEOUT` to a value which better suites your environment.");
}
}
} catch (InterruptedException e) {
SystemTray.logger.error("Error waiting for dispatch to complete.", new Exception());
}
}
public
void shutdown() {
dispatchAndWait(new Runnable() {
@Override
public
void run() {
obliterateMenu();
Gtk.shutdownGui();
}
});
}
/**
* Enables, or disables the sub-menu entry.
*/
@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
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
void setShortcut(final char key) {
menuEntry.setShortcut(key);
}
@Override
public
String getText() {
return menuEntry.getText();
}
@Override
public
void setText(final String newText) {
menuEntry.setText(newText);
}
@Override
public
void setImage(final File imageFile) {
menuEntry.setImage(imageFile);
}
@Override
public
void setImage(final String imagePath) {
menuEntry.setImage(imagePath);
}
@Override
public
void setImage(final URL imageUrl) {
menuEntry.setImage(imageUrl);
}
@Override
public
void setImage(final String cacheName, final InputStream imageStream) {
menuEntry.setImage(cacheName, imageStream);
}
@Override
public
void setImage(final InputStream imageStream) {
menuEntry.setImage(imageStream);
}
@Override
public
boolean hasImage() {
return menuEntry.hasImage();
}
// NO OP.
@Override
public
void setCallback(final SystemTrayMenuAction callback) {
}
/**
* Called inside the gdk_threads block
*/
protected
void onMenuAdded(final Pointer menu) {
// only needed for AppIndicator
}
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
// To work around this issue, we destroy then recreate the menu every time something is changed.
/**
* Deletes the menu, and unreferences everything in it. ALSO recreates ONLY the menu object.
*/
void deleteMenu() {
if (obliterateInProgress) {
return;
}
if (_native != null) {
// have to remove all other menu entries
synchronized (menuEntries) {
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
final MenuEntry menuEntry__ = menuEntries.get(i);
if (menuEntry__ instanceof GtkEntry) {
GtkEntry entry = (GtkEntry) menuEntry__;
Gobject.g_object_force_floating(entry._native);
Gtk.gtk_container_remove(_native, entry._native);
}
else if (menuEntry__ instanceof GtkMenu) {
GtkMenu subMenu = (GtkMenu) menuEntry__;
Gobject.g_object_force_floating(subMenu.menuEntry._native);
Gtk.gtk_container_remove(_native, subMenu.menuEntry._native);
}
}
Gtk.gtk_widget_destroy(_native);
}
}
if (getParent() != null) {
((GtkMenu) getParent()).deleteMenu();
}
// makes a new one
_native = 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);
}
}
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
// To work around this issue, we destroy then recreate the menu every time something is changed.
void createMenu() {
if (obliterateInProgress) {
return;
}
if (getParent() != null) {
((GtkMenu) getParent()).createMenu();
}
boolean hasImages = false;
// now add back other menu entries
synchronized (menuEntries) {
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
final MenuEntry menuEntry__ = menuEntries.get(i);
hasImages |= menuEntry__.hasImage();
}
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
final MenuEntry 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);
// 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"
}
else 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"
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();
}
}
}
onMenuAdded(_native);
Gtk.gtk_widget_show_all(_native);
}
}
/**
* must be called on the dispatch thread
*
* Completely obliterates the menu, no possible way to reconstruct it.
*/
private
void obliterateMenu() {
if (_native != 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<MenuEntry> menuEntriesCopy = new ArrayList<MenuEntry>(this.menuEntries);
for (int i = 0, menuEntriesSize = menuEntriesCopy.size(); i < menuEntriesSize; i++) {
final MenuEntry menuEntry__ = menuEntriesCopy.get(i);
menuEntry__.remove();
}
this.menuEntries.clear();
menuEntriesCopy.clear();
Gtk.gtk_widget_destroy(_native);
}
obliterateInProgress = false;
}
}
/**
* Will add a new menu entry, or update one if it already exists
*/
protected
MenuEntry addEntry_(final String menuText, final File imagePath, final SystemTrayMenuAction callback) {
// some implementations of appindicator, do NOT like having a menu added, which has no menu items yet.
// see: https://bugs.launchpad.net/glipper/+bug/1203888
if (menuText == null) {
throw new NullPointerException("Menu text cannot be null");
}
// have to wait for the value
final AtomicReference<MenuEntry> value = new AtomicReference<MenuEntry>();
dispatchAndWait(new Runnable() {
@Override
public
void run() {
synchronized (menuEntries) {
MenuEntry menuEntry = get(menuText);
if (menuEntry == null) {
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
// To work around this issue, we destroy then recreate the menu every time something is changed.
deleteMenu();
menuEntry = new GtkEntryItem(GtkMenu.this, callback);
menuEntry.setText(menuText);
menuEntry.setImage(imagePath);
menuEntries.add(menuEntry);
createMenu();
} else if (menuEntry instanceof GtkEntryItem) {
menuEntry.setText(menuText);
menuEntry.setImage(imagePath);
}
value.set(menuEntry);
}
}
});
return value.get();
}
/**
* Will add a new menu entry, or update one if it already exists
*/
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>();
dispatchAndWait(new Runnable() {
@Override
public
void run() {
synchronized (menuEntries) {
MenuEntry menuEntry = get(menuText);
if (menuEntry == null) {
// some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator.
// To work around this issue, we destroy then recreate the menu every time something is changed.
deleteMenu();
GtkMenu subMenu = new GtkMenu(getSystemTray(), GtkMenu.this);
subMenu.setText(menuText);
subMenu.setImage(imagePath);
menuEntries.add(subMenu);
value.set(subMenu);
createMenu();
} else if (menuEntry instanceof GtkMenu) {
menuEntry.setText(menuText);
menuEntry.setImage(imagePath);
value.set(((GtkMenu) menuEntry));
}
}
}
});
return value.get();
}
// a child will always remove itself from the parent.
@Override
public
void remove() {
dispatchAndWait(new Runnable() {
@Override
public
void run() {
GtkMenu parent = (GtkMenu) getParent();
// have to remove from the parent.menuEntries first
for (Iterator<MenuEntry> iterator = parent.menuEntries.iterator(); iterator.hasNext(); ) {
final MenuEntry entry = iterator.next();
if (entry == GtkMenu.this) {
iterator.remove();
break;
}
}
// 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 {
// have to rebuild the menu now...
parent.deleteMenu();
parent.createMenu();
}
}
});
}
}

View File

@ -1,87 +0,0 @@
/*
* 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.linux;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
/**
* Derived from
* Lantern: https://github.com/getlantern/lantern/ Apache 2.0 License Copyright 2010 Brave New Software Project, Inc.
*/
abstract
class GtkTypeSystemTray extends GtkMenu {
GtkTypeSystemTray(final SystemTray systemTray) {
super(systemTray, null);
}
public
String getStatus() {
synchronized (menuEntries) {
MenuEntry menuEntry = menuEntries.get(0);
if (menuEntry instanceof GtkEntryStatus) {
return menuEntry.getText();
}
}
return null;
}
public
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(GtkTypeSystemTray.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(GtkTypeSystemTray.this, statusText);
menuEntries.add(0, menuEntry);
}
}
createMenu();
}
}
});
}
}

View File

@ -28,7 +28,7 @@ import dorkbox.systemTray.SystemTray;
*
* Direct-mapping, See: https://github.com/java-native-access/jna/blob/master/www/DirectMapping.md
*/
@SuppressWarnings("Duplicates")
@SuppressWarnings({"Duplicates", "SameParameterValue", "DanglingJavadoc"})
public
class AppIndicator {
public static boolean isVersion3 = false;
@ -213,14 +213,14 @@ class AppIndicator {
// See: https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12
public static final int CATEGORY_APPLICATION_STATUS = 0;
public static final int CATEGORY_COMMUNICATIONS = 1;
public static final int CATEGORY_SYSTEM_SERVICES = 2;
public static final int CATEGORY_HARDWARE = 3;
public static final int CATEGORY_OTHER = 4;
// public static final int CATEGORY_COMMUNICATIONS = 1;
// public static final int CATEGORY_SYSTEM_SERVICES = 2;
// public static final int CATEGORY_HARDWARE = 3;
// public static final int CATEGORY_OTHER = 4;
public static final int STATUS_PASSIVE = 0;
public static final int STATUS_ACTIVE = 1;
public static final int STATUS_ATTENTION = 2;
// public static final int STATUS_ATTENTION = 2;
public static native AppIndicatorInstanceStruct app_indicator_new(String id, String icon_name, int category);

View File

@ -1,29 +0,0 @@
/*
* 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.linux.jna;
import com.sun.jna.Callback;
import com.sun.jna.Pointer;
import dorkbox.util.Keep;
@Keep
public
interface GCallback extends Callback {
/**
* @return Gtk.TRUE if we handled this event
*/
int callback(Pointer instance, Pointer data);
}

View File

@ -35,11 +35,7 @@ class Gobject {
public static native void g_object_get(Pointer object, String objectName, PointerByReference objectVal, Pointer nullValue);
public static native void g_free(Pointer object);
public static native void g_object_unref(Pointer object);
public static native void g_object_force_floating(Pointer object);
public static native void g_object_ref_sink(Pointer object);
public static native NativeLong g_signal_connect_object(Pointer instance, String detailed_signal, Callback c_handler, Pointer object, int connect_flags);
}

View File

@ -24,10 +24,7 @@ import java.util.concurrent.TimeUnit;
import com.sun.jna.Function;
import com.sun.jna.Pointer;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.Swt;
@ -379,23 +376,6 @@ class Gtk {
});
}
/**
* required to properly setup the dispatch flag
* @param callback will never be null.
*/
public static
void proxyClick(final Menu parent, final MenuEntry menuEntry, final SystemTrayMenuAction callback) {
Gtk.isDispatch = true;
try {
callback.onClick(parent.getSystemTray(), parent, menuEntry);
} catch (Throwable throwable) {
SystemTray.logger.error("Error calling menu entry {} click event.", menuEntry.getText(), throwable);
}
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
* enough that we just pass 'null' as the second argument, therefore, we don't have to define that parameter here.
@ -421,22 +401,10 @@ class Gtk {
public static native Pointer gtk_menu_new();
public static native Pointer gtk_menu_item_set_submenu(Pointer menuEntry, Pointer menu);
public static native Pointer gtk_separator_menu_item_new();
// to create a menu entry WITH an icon.
public static native Pointer gtk_image_new_from_file(String iconPath);
// uses '_' to define which key is the mnemonic
public static native Pointer gtk_image_menu_item_new_with_mnemonic(String label);
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 Pointer gtk_status_icon_new();
public static native void gtk_status_icon_set_from_file(Pointer widget, String label);
@ -450,18 +418,8 @@ class Gtk {
public static native void gtk_status_icon_set_name(Pointer widget, String name);
public static native void gtk_menu_popup(Pointer menu, Pointer widget, Pointer bla, Function func, Pointer data, int button, int time);
public static native void gtk_menu_item_set_label(Pointer menu_item, String label);
public static native void gtk_menu_shell_append(Pointer menu_shell, Pointer child);
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_container_remove(Pointer menu, Pointer subItem);
public static native void gtk_widget_show_all(Pointer widget);
public static native void gtk_widget_destroy(Pointer widget);

View File

@ -22,7 +22,6 @@ import javax.swing.JPopupMenu;
import javax.swing.border.EmptyBorder;
class AdjustedJMenu extends JMenu {
AdjustedJMenu() {
}

View File

@ -30,7 +30,7 @@ import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.SwingUtil;
abstract
class SwingEntry implements MenuEntry {
class Entry implements MenuEntry {
private final int id = Menu.MENU_ID_COUNTER.getAndIncrement();
private final SwingMenu parent;
@ -40,7 +40,7 @@ class SwingEntry implements MenuEntry {
private volatile String text;
// this is ALWAYS called on the EDT.
SwingEntry(final SwingMenu parent, final JComponent menuItem) {
Entry(final SwingMenu parent, final JComponent menuItem) {
this.parent = parent;
this._native = menuItem;
@ -204,7 +204,7 @@ class SwingEntry implements MenuEntry {
return false;
}
SwingEntry other = (SwingEntry) obj;
Entry other = (Entry) obj;
return this.id == other.id;
}

View File

@ -25,7 +25,7 @@ import javax.swing.JMenuItem;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.util.SwingUtil;
class SwingEntryItem extends SwingEntry {
class EntryItem extends Entry {
private final ActionListener swingCallback;
@ -33,7 +33,7 @@ class SwingEntryItem extends SwingEntry {
private volatile SystemTrayMenuAction callback;
// this is ALWAYS called on the EDT.
SwingEntryItem(final SwingMenu parent, final SystemTrayMenuAction callback) {
EntryItem(final SwingMenu parent, final SystemTrayMenuAction callback) {
super(parent, new AdjustedJMenuItem());
this.callback = callback;

View File

@ -22,10 +22,10 @@ import javax.swing.JSeparator;
import dorkbox.systemTray.MenuSpacer;
import dorkbox.systemTray.SystemTrayMenuAction;
class SwingEntrySeparator extends SwingEntry implements MenuSpacer {
class EntrySeparator extends Entry implements MenuSpacer {
// this is ALWAYS called on the EDT.
SwingEntrySeparator(final SwingMenu parent) {
EntrySeparator(final SwingMenu parent) {
super(parent, new JSeparator(JSeparator.HORIZONTAL));
}

View File

@ -22,10 +22,10 @@ import javax.swing.JMenuItem;
import dorkbox.systemTray.MenuStatus;
import dorkbox.systemTray.SystemTrayMenuAction;
class SwingEntryStatus extends SwingEntry implements MenuStatus {
class EntryStatus extends Entry implements MenuStatus {
// this is ALWAYS called on the EDT.
SwingEntryStatus(final SwingMenu parent, final String label) {
EntryStatus(final SwingMenu parent, final String label) {
super(parent, new JMenuItem());
setText(label);
}

View File

@ -1,3 +1,18 @@
/*
* Copyright 2016 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
import javax.swing.JComponent;
@ -7,20 +22,12 @@ import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.util.ImageUtils;
/**
*
*/
public abstract
class SwingGenericTray extends SwingMenu {
class GenericTray extends SwingMenu {
/**
* Called in the EDT
*
* @param systemTray
* the system tray (which is the object that sits in the system tray)
* @param parent
* @param _native
*/
SwingGenericTray(final SystemTray systemTray, final Menu parent, final JComponent _native) {
GenericTray(final SystemTray systemTray, final Menu parent, final JComponent _native) {
super(systemTray, parent, _native);
ImageUtils.determineIconSize();
@ -30,7 +37,7 @@ class SwingGenericTray extends SwingMenu {
String getStatus() {
synchronized (menuEntries) {
MenuEntry menuEntry = menuEntries.get(0);
if (menuEntry instanceof SwingEntryStatus) {
if (menuEntry instanceof EntryStatus) {
return menuEntry.getText();
}
}
@ -47,12 +54,12 @@ class SwingGenericTray extends SwingMenu {
void run() {
synchronized (menuEntries) {
// status is ALWAYS at 0 index...
SwingEntry menuEntry = null;
Entry menuEntry = null;
if (!menuEntries.isEmpty()) {
menuEntry = (SwingEntry) menuEntries.get(0);
menuEntry = (Entry) menuEntries.get(0);
}
if (menuEntry instanceof SwingEntryStatus) {
if (menuEntry instanceof EntryStatus) {
// set the text or delete...
if (statusText == null) {
@ -66,7 +73,7 @@ class SwingGenericTray extends SwingMenu {
} else {
// create a new one
menuEntry = new SwingEntryStatus(_this, statusText);
menuEntry = new EntryStatus(_this, statusText);
// status is ALWAYS at 0 index...
menuEntries.add(0, menuEntry);
}

View File

@ -1,242 +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.swing;
import static dorkbox.systemTray.SystemTray.TIMEOUT;
import java.awt.Dimension;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JPopupMenu;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.linux.jna.GEventCallback;
import dorkbox.systemTray.linux.jna.GdkEventButton;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.util.ScreenUtil;
/**
* Class for handling all system tray interactions via GTK.
* <p/>
* This is the "old" way to do it, and does not work with some desktop environments. This is a hybrid class, because we want to show the
* swing menu popup INSTEAD of GTK menu popups. The "golden standard" is our swing menu popup, since we have 100% control over it.
*/
public
class GtkStatusIconTray extends SwingGenericTray {
private volatile Pointer trayIcon;
// http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c
// https://github.com/djdeath/glib/blob/master/gobject/gobject.c
// have to save these in a field to prevent GC on the objects (since they go out-of-scope from java)
private final List<Object> gtkCallbacks = new ArrayList<Object>();
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
private AtomicBoolean shuttingDown = new AtomicBoolean();
private volatile boolean isActive = false;
// called on the EDT
public
GtkStatusIconTray(final SystemTray systemTray) {
super(systemTray, null, new SwingSystemTrayMenuPopup());
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_APP_INDICATOR) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
throw new IllegalArgumentException("Unable to start GtkStatusIcon if 'SystemTray.FORCE_TRAY_TYPE' is set to AppIndicator");
}
JPopupMenu popupMenu = (JPopupMenu) _native;
popupMenu.pack();
popupMenu.setFocusable(true);
final Runnable popupRunnable = new Runnable() {
@Override
public
void run() {
Dimension size = _native.getPreferredSize();
Point point = MouseInfo.getPointerInfo()
.getLocation();
Rectangle bounds = ScreenUtil.getScreenBoundsAt(point);
int x = point.x;
int y = point.y;
if (y < bounds.y) {
y = bounds.y;
}
else if (y + size.height > bounds.y + bounds.height) {
// our menu cannot have the top-edge snap to the mouse
// so we make the bottom-edge snap to the mouse
y -= size.height; // snap to edge of mouse
}
if (x < bounds.x) {
x = bounds.x;
}
else if (x + size.width > bounds.x + bounds.width) {
// our menu cannot have the left-edge snap to the mouse
// so we make the right-edge snap to the mouse
x -= size.width; // snap to edge of mouse
}
SwingSystemTrayMenuPopup popupMenu = (SwingSystemTrayMenuPopup) _native;
popupMenu.doShow(x, y);
}
};
// appindicators DO NOT support anything other than PLAIN gtk-menus
// they ALSO do not support tooltips, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName);
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
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");
}
}
});
}
@SuppressWarnings("FieldRepeatedlyAccessedInMethod")
public
void shutdown() {
if (!shuttingDown.getAndSet(true)) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
try {
// this hides the indicator
Gtk.gtk_status_icon_set_visible(trayIcon, false);
Gobject.g_object_unref(trayIcon);
// mark for GC
trayIcon = null;
gtkCallbacks.clear();
} finally {
countDownLatch.countDown();
}
}
});
// this is slightly different than how swing does it. We have a timeout here so that we can make sure that updates on the GUI
// thread occur in REASONABLE time-frames, and alert the user if not.
try {
if (!countDownLatch.await(TIMEOUT, TimeUnit.SECONDS)) {
SystemTray.logger.error("Event dispatch queue took longer than " + TIMEOUT + " seconds to shutdown. Please adjust " +
"`SystemTray.TIMEOUT` to a value which better suites your environment.");
}
} catch (InterruptedException e) {
SystemTray.logger.error("Error waiting for shutdown dispatch to complete.", new Exception());
}
Gtk.shutdownGui();
// uses EDT
super.remove();
}
}
public
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);
}
}
});
dispatch(new Runnable() {
@Override
public
void run() {
((SwingSystemTrayMenuPopup) _native).setTitleBarImage(iconFile);
}
});
}
}

View File

@ -16,7 +16,7 @@
package dorkbox.systemTray.swing;
import static dorkbox.systemTray.swing.SwingEntry.getVkKey;
import static dorkbox.systemTray.swing.Entry.getVkKey;
import java.io.File;
import java.io.InputStream;
@ -38,7 +38,7 @@ import dorkbox.util.SwingUtil;
class SwingMenu extends Menu {
// sub-menu = AdjustedJMenu
// systemtray = SwingSystemTrayMenuPopup
// systemtray = TrayPopup
volatile JComponent _native;
// this have to be volatile, because they can be changed from any thread
@ -84,7 +84,7 @@ class SwingMenu extends Menu {
void run() {
synchronized (menuEntries) {
synchronized (menuEntries) {
MenuEntry menuEntry = new SwingEntrySeparator(SwingMenu.this);
MenuEntry menuEntry = new EntrySeparator(SwingMenu.this);
menuEntries.add(menuEntry);
}
}
@ -121,12 +121,12 @@ class SwingMenu extends Menu {
if (menuEntry == null) {
// must always be called on the EDT
menuEntry = new SwingEntryItem(SwingMenu.this, callback);
menuEntry = new EntryItem(SwingMenu.this, callback);
menuEntry.setText(menuText);
menuEntry.setImage(imagePath);
menuEntries.add(menuEntry);
} else if (menuEntry instanceof SwingEntryItem) {
} else if (menuEntry instanceof EntryItem) {
menuEntry.setText(menuText);
menuEntry.setImage(imagePath);
}
@ -317,8 +317,8 @@ class SwingMenu extends Menu {
public
void run() {
_native.setVisible(false);
if (_native instanceof SwingSystemTrayMenuPopup) {
((SwingSystemTrayMenuPopup) _native).close();
if (_native instanceof TrayPopup) {
((TrayPopup) _native).close();
}
SwingMenu parent = (SwingMenu) getParent();

View File

@ -15,8 +15,11 @@
*/
package dorkbox.systemTray.swing;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.io.File;
@ -33,23 +36,23 @@ import javax.swing.event.PopupMenuListener;
import dorkbox.systemTray.SystemTray;
import dorkbox.util.OS;
import dorkbox.util.ScreenUtil;
/**
* This custom popup is required if we want to be able to show images on the menu,
*
* This is our "golden standard" since we have 100% control over it.
*/
public
class SwingSystemTrayMenuPopup extends JPopupMenu {
class TrayPopup extends JPopupMenu {
private static final long serialVersionUID = 1L;
// NOTE: we can use the "hidden dialog" focus window trick...
private JDialog hiddenDialog;
private volatile File iconFile;
private volatile Runnable runnable;
@SuppressWarnings("unchecked")
public
SwingSystemTrayMenuPopup() {
TrayPopup() {
super();
setFocusable(true);
// setBorder(new BorderUIResource.EmptyBorderUIResource(0, 0, 0, 0)); // borderUI resource border type will get changed!
@ -106,7 +109,7 @@ class SwingSystemTrayMenuPopup extends JPopupMenu {
hiddenDialog.addWindowFocusListener(new WindowFocusListener() {
@Override
public void windowLostFocus (WindowEvent we ) {
SwingSystemTrayMenuPopup.this.setVisible(false);
TrayPopup.this.setVisible(false);
}
@Override
public void windowGainedFocus (WindowEvent we) {
@ -133,8 +136,62 @@ class SwingSystemTrayMenuPopup extends JPopupMenu {
}
}
void setOnHideRunnable(final Runnable runnable) {
this.runnable = runnable;
}
@Override
public
void doShow(final int x, final int y) {
void setVisible(final boolean b) {
if (!b) {
Runnable r = this.runnable;
if (r != null) {
r.run();
}
}
super.setVisible(b);
}
void close() {
hiddenDialog.setVisible(false);
hiddenDialog.dispatchEvent(new WindowEvent(hiddenDialog, WindowEvent.WINDOW_CLOSING));
}
void doShow(final Point point, final int offset) {
Dimension size = getPreferredSize();
Rectangle bounds = ScreenUtil.getScreenBoundsAt(point);
int x = point.x;
int y = point.y;
if (y < bounds.y) {
y = bounds.y;
}
else if (y + size.height > bounds.y + bounds.height) {
// our menu cannot have the top-edge snap to the mouse
// so we make the bottom-edge snap to the mouse
y -= size.height; // snap to edge of mouse
}
if (x < bounds.x) {
x = bounds.x;
x -= offset; // display over the stupid appindicator menu (which has to show, this is a major hack!)
}
else if (x + size.width > bounds.x + bounds.width) {
// our menu cannot have the left-edge snap to the mouse so we make the right-edge snap to the mouse
x -= size.width; // snap right edge of menu to mouse
x += offset; // display over the stupid appindicator menu (which has to show, this is a major hack!)
} else {
x -= offset; // display over the stupid appindicator menu (which has to show, this is a major hack!)
}
System.err.println("SHOWING POPUP @" + x + "," + y);
// critical to get the keyboard listeners working for the popup menu
setInvoker(hiddenDialog.getContentPane());
@ -146,8 +203,5 @@ class SwingSystemTrayMenuPopup extends JPopupMenu {
requestFocusInWindow();
}
void close() {
hiddenDialog.setVisible(false);
hiddenDialog.dispatchEvent(new WindowEvent(hiddenDialog, WindowEvent.WINDOW_CLOSING));
}
}

View File

@ -15,19 +15,11 @@
*/
package dorkbox.systemTray.swing;
import static dorkbox.systemTray.SystemTray.TIMEOUT;
import java.awt.Dimension;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JPopupMenu;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.PointerByReference;
@ -40,7 +32,6 @@ import dorkbox.systemTray.linux.jna.GdkEventButton;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
/**
@ -89,7 +80,7 @@ import dorkbox.util.SwingUtil;
* http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files
*/
public
class AppIndicatorTray extends SwingGenericTray {
class _AppIndicatorTray extends GenericTray {
private AppIndicatorInstanceStruct appIndicator;
private boolean isActive = false;
@ -102,56 +93,21 @@ class AppIndicatorTray extends SwingGenericTray {
private final Runnable popupRunnable;
public
AppIndicatorTray(final SystemTray systemTray) {
super(systemTray,null, new SwingSystemTrayMenuPopup());
_AppIndicatorTray(final SystemTray systemTray) {
super(systemTray,null, new TrayPopup());
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
throw new IllegalArgumentException("Unable to start AppIndicator if 'SystemTray.FORCE_TRAY_TYPE' is set to GtkStatusIcon");
}
JPopupMenu popupMenu = (JPopupMenu) _native;
TrayPopup popupMenu = (TrayPopup) _native;
popupMenu.pack();
popupMenu.setFocusable(true);
popupRunnable = new Runnable() {
popupMenu.setOnHideRunnable(new Runnable() {
@Override
public
void run() {
Dimension size = _native.getPreferredSize();
Point point = MouseInfo.getPointerInfo()
.getLocation();
Rectangle bounds = ScreenUtil.getScreenBoundsAt(point);
int x = point.x;
int y = point.y;
if (y < bounds.y) {
y = bounds.y;
}
else if (y + size.height > bounds.y + bounds.height) {
// our menu cannot have the top-edge snap to the mouse
// so we make the bottom-edge snap to the mouse
y -= size.height; // snap to edge of mouse
}
if (x < bounds.x) {
x = bounds.x;
x -= 32; // display over the stupid appindicator menu (which has to show, this is a major hack!)
}
else if (x + size.width > bounds.x + bounds.width) {
// our menu cannot have the left-edge snap to the mouse
// so we make the right-edge snap to the mouse
x -= size.width; // snap to edge of mouse
x += 32; // display over the stupid appindicator menu (which has to show, this is a major hack!)
}
SwingSystemTrayMenuPopup popupMenu = (SwingSystemTrayMenuPopup) _native;
popupMenu.doShow(x, y);
// Such ugly hacks to get AppIndicator support properly working. This is so horrible I am ashamed.
Gtk.dispatch(new Runnable() {
@Override
@ -162,11 +118,23 @@ class AppIndicatorTray extends SwingGenericTray {
}
});
}
});
popupRunnable = new Runnable() {
@Override
public
void run() {
Point point = MouseInfo.getPointerInfo()
.getLocation();
TrayPopup popupMenu = (TrayPopup) _native;
popupMenu.doShow(point, SystemTray.DEFAULT_TRAY_SIZE);
}
};
// appindicators DO NOT support anything other than PLAIN gtk-menus
// they ALSO do not support tooltips, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName);
// trayIcon.setToolTip(_SwingTray.this.appName);
Gtk.startGui();
@ -219,36 +187,20 @@ class AppIndicatorTray extends SwingGenericTray {
public
void shutdown() {
if (!shuttingDown.getAndSet(true)) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
try {
// STATUS_PASSIVE hides the indicator
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE);
Pointer p = appIndicator.getPointer();
Gobject.g_object_unref(p);
// STATUS_PASSIVE hides the indicator
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE);
Pointer p = appIndicator.getPointer();
Gobject.g_object_unref(p);
appIndicator = null;
} finally {
countDownLatch.countDown();
}
appIndicator = null;
}
});
// this is slightly different than how swing does it. We have a timeout here so that we can make sure that updates on the GUI
// thread occur in REASONABLE time-frames, and alert the user if not.
try {
if (!countDownLatch.await(TIMEOUT, TimeUnit.SECONDS)) {
SystemTray.logger.error("Event dispatch queue took longer than " + TIMEOUT + " seconds to shutdown. Please adjust " +
"`SystemTray.TIMEOUT` to a value which better suites your environment.");
}
} catch (InterruptedException e) {
SystemTray.logger.error("Error waiting for shutdown dispatch to complete.", new Exception());
}
Gtk.shutdownGui();
// uses EDT
@ -284,7 +236,7 @@ class AppIndicatorTray extends SwingGenericTray {
@Override
public
void run() {
((SwingSystemTrayMenuPopup) _native).setTitleBarImage(imageFile);
((TrayPopup) _native).setTitleBarImage(imageFile);
}
});
}

View File

@ -13,13 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.linux;
package dorkbox.systemTray.swing;
import java.awt.MouseInfo;
import java.awt.Point;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JPopupMenu;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
@ -28,15 +32,15 @@ import dorkbox.systemTray.linux.jna.GEventCallback;
import dorkbox.systemTray.linux.jna.GdkEventButton;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
/**
* Class for handling all system tray interactions via GTK.
* <p/>
* This is the "old" way to do it, and does not work with some desktop environments.
* This is the "old" way to do it, and does not work with some desktop environments. This is a hybrid class, because we want to show the
* swing menu popup INSTEAD of GTK menu popups. The "golden standard" is our swing menu popup, since we have 100% control over it.
*/
public
class GtkSystemTray extends GtkTypeSystemTray {
class _GtkStatusIconTray extends GenericTray {
private volatile Pointer trayIcon;
// http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c
@ -50,18 +54,38 @@ class GtkSystemTray extends GtkTypeSystemTray {
private volatile boolean isActive = false;
// called on the EDT
public
GtkSystemTray(final SystemTray systemTray) {
super(systemTray);
_GtkStatusIconTray(final SystemTray systemTray) {
super(systemTray, null, new TrayPopup());
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_APP_INDICATOR) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
throw new IllegalArgumentException("Unable to start GtkStatusIcon if 'SystemTray.FORCE_TRAY_TYPE' is set to AppIndicator");
}
ImageUtils.determineIconSize();
JPopupMenu popupMenu = (JPopupMenu) _native;
popupMenu.pack();
popupMenu.setFocusable(true);
final Runnable popupRunnable = new Runnable() {
@Override
public
void run() {
Point point = MouseInfo.getPointerInfo()
.getLocation();
TrayPopup popupMenu = (TrayPopup) _native;
popupMenu.doShow(point, 0);
}
};
// appindicators DO NOT support anything other than PLAIN gtk-menus
// they ALSO do not support tooltips, so we cater to the lowest common denominator
// trayIcon.setToolTip(_SwingTray.this.appName);
Gtk.startGui();
dispatch(new Runnable() {
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
@ -72,14 +96,16 @@ class GtkSystemTray extends GtkTypeSystemTray {
@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) {
Gtk.gtk_menu_popup(_native, null, null, Gtk.gtk_status_icon_position_menu, trayIcon, 0, event.time);
// show the swing menu on the EDT
dispatch(popupRunnable);
}
}
};
final NativeLong button_press_event = Gobject.g_signal_connect_object(trayIcon, "button_press_event", gtkCallback,
null, 0);
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);
@ -90,7 +116,7 @@ class GtkSystemTray extends GtkTypeSystemTray {
Gtk.waitForStartup();
// we have to be able to set our title, otherwise the gnome-shell extension WILL NOT work
dispatch(new Runnable() {
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
@ -118,11 +144,11 @@ class GtkSystemTray extends GtkTypeSystemTray {
@SuppressWarnings("FieldRepeatedlyAccessedInMethod")
@Override
public
void shutdown() {
if (!shuttingDown.getAndSet(true)) {
dispatchAndWait(new Runnable() {
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
@ -136,13 +162,16 @@ class GtkSystemTray extends GtkTypeSystemTray {
}
});
super.shutdown();
Gtk.shutdownGui();
// uses EDT
super.remove();
}
}
public
void setImage_(final File iconFile) {
dispatch(new Runnable() {
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
@ -154,5 +183,13 @@ class GtkSystemTray extends GtkTypeSystemTray {
}
}
});
dispatch(new Runnable() {
@Override
public
void run() {
((TrayPopup) _native).setTitleBarImage(iconFile);
}
});
}
}

View File

@ -16,10 +16,7 @@
package dorkbox.systemTray.swing;
import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.MouseAdapter;
@ -30,7 +27,6 @@ import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import dorkbox.systemTray.MenuEntry;
import dorkbox.util.ScreenUtil;
/**
* Class for handling all system tray interaction, via SWING.
@ -42,16 +38,16 @@ import dorkbox.util.ScreenUtil;
*/
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"})
public
class SwingSystemTray extends SwingGenericTray {
class _SwingTray extends GenericTray {
volatile SystemTray tray;
volatile TrayIcon trayIcon;
// Called in the EDT
public
SwingSystemTray(final dorkbox.systemTray.SystemTray systemTray) {
super(systemTray, null, new SwingSystemTrayMenuPopup());
_SwingTray(final dorkbox.systemTray.SystemTray systemTray) {
super(systemTray, null, new TrayPopup());
SwingSystemTray.this.tray = SystemTray.getSystemTray();
_SwingTray.this.tray = SystemTray.getSystemTray();
}
public
@ -94,40 +90,14 @@ class SwingSystemTray extends SwingGenericTray {
// appindicators DO NOT support anything other than PLAIN gtk-menus
// they ALSO do not support tooltips, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName);
// trayIcon.setToolTip(_SwingTray.this.appName);
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public
void mousePressed(MouseEvent e) {
Dimension size = _native.getPreferredSize();
Point point = e.getPoint();
Rectangle bounds = ScreenUtil.getScreenBoundsAt(point);
int x = point.x;
int y = point.y;
if (y < bounds.y) {
y = bounds.y;
}
else if (y + size.height > bounds.y + bounds.height) {
// our menu cannot have the top-edge snap to the mouse
// so we make the bottom-edge snap to the mouse
y -= size.height; // snap to edge of mouse
}
if (x < bounds.x) {
x = bounds.x;
}
else if (x + size.width > bounds.x + bounds.width) {
// our menu cannot have the left-edge snap to the mouse
// so we make the right-edge snap to the mouse
x -= size.width; // snap to edge of mouse
}
SwingSystemTrayMenuPopup popupMenu = (SwingSystemTrayMenuPopup) _native;
popupMenu.doShow(x, y);
TrayPopup popupMenu = (TrayPopup) _native;
popupMenu.doShow(e.getPoint(), 0);
}
});
@ -140,7 +110,7 @@ class SwingSystemTray extends SwingGenericTray {
trayIcon.setImage(trayImage);
}
((SwingSystemTrayMenuPopup) _native).setTitleBarImage(iconFile);
((TrayPopup) _native).setTitleBarImage(iconFile);
}
});
}

View File

@ -36,6 +36,7 @@ import javax.imageio.stream.ImageInputStream;
import javax.swing.ImageIcon;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.util.CacheUtil;
import dorkbox.util.FileUtil;
import dorkbox.util.LocationResolver;
@ -89,6 +90,11 @@ class ImageUtils {
}
}
// 1 = 16
// 2 = 32
// 4 = 64
// 8 = 128
if (windowsVersion.startsWith("5.1")) {
// Windows XP 5.1.2600
scalingFactor = 1;
@ -139,46 +145,97 @@ class ImageUtils {
} else if (OS.isLinux()) {
// GtkStatusIcon will USUALLY automatically scale the icon
// AppIndicator MIGHT scale the icon (depends on the OS)
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
PrintStream outputStream = new PrintStream(byteArrayOutputStream);
// gsettings get org.gnome.desktop.interface scaling-factor
final ShellProcessBuilder shellVersion = new ShellProcessBuilder(outputStream);
shellVersion.setExecutable("gsettings");
shellVersion.addArgument("get");
shellVersion.addArgument("org.gnome.desktop.interface");
shellVersion.addArgument("scaling-factor");
shellVersion.start();
String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
// KDE is bonkers.
if (Gtk.isKDE) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
PrintStream outputStream = new PrintStream(byteArrayOutputStream);
if (!output.isEmpty()) {
if (SystemTray.DEBUG) {
SystemTray.logger.debug("Checking scaling factor for GTK environment, should start with 'uint32', value: '{}'", output);
// plasma-desktop -v
// plasmashell --version
final ShellProcessBuilder shellVersion = new ShellProcessBuilder(outputStream);
shellVersion.setExecutable("plasmashell");
shellVersion.addArgument("--version");
shellVersion.start();
String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
if (!output.isEmpty()) {
if (SystemTray.DEBUG) {
SystemTray.logger.debug("Checking plasma KDE environment, should start with 'plasmashell', value: '{}'", output);
}
// DEFAULT icon size is 16. KDE is bananas on what they did with tray icon scale
// should be: plasmashell 5.6.5 or something
String s = "plasmashell ";
if (output.contains(s)) {
String value = output.substring(output.indexOf(s) + s.length(), output.length() - 1);
// 1 = 16
// 2 = 32
// 4 = 64
// 8 = 128
if (value.startsWith("4")) {
scalingFactor = 2;
} else if (value.startsWith("5")) {
scalingFactor = 8; // it is insane how large the icon is
} else {
// assume very low version of plasmashell, default 32
scalingFactor = 2;
}
}
}
// DEFAULT icon size is 16. HiDpi changes this scale, so we should use it as well.
// should be: uint32 0 or something
if (output.contains("uint32")) {
String value = output.substring(output.indexOf("uint")+7, output.length()-1);
scalingFactor = Integer.parseInt(value);
// 0 is disabled (no scaling)
// 1 is enabled (default scale)
// 2 is 2x scale
// 3 is 3x scale
// etc
// A setting of 2, 3, etc, which is all you can do with scaling-factor
// To enable HiDPI, use gsettings:
// gsettings set org.gnome.desktop.interface scaling-factor 2
} catch (Throwable e) {
if (SystemTray.DEBUG) {
SystemTray.logger.error("Cannot check plasmashell version", e);
}
}
} catch (Throwable e) {
if (SystemTray.DEBUG) {
SystemTray.logger.error("Cannot check scaling factor", e);
} else {
// it's a GTK environment or something
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
PrintStream outputStream = new PrintStream(byteArrayOutputStream);
// gsettings get org.gnome.desktop.interface scaling-factor
final ShellProcessBuilder shellVersion = new ShellProcessBuilder(outputStream);
shellVersion.setExecutable("gsettings");
shellVersion.addArgument("get");
shellVersion.addArgument("org.gnome.desktop.interface");
shellVersion.addArgument("scaling-factor");
shellVersion.start();
String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
if (!output.isEmpty()) {
if (SystemTray.DEBUG) {
SystemTray.logger.debug("Checking scaling factor for GTK environment, should start with 'uint32', value: '{}'", output);
}
// DEFAULT icon size is 16. HiDpi changes this scale, so we should use it as well.
// should be: uint32 0 or something
if (output.contains("uint32")) {
String value = output.substring(output.indexOf("uint")+7, output.length()-1);
scalingFactor = Integer.parseInt(value);
// 0 is disabled (no scaling)
// 1 is enabled (default scale)
// 2 is 2x scale
// 3 is 3x scale
// etc
// A setting of 2, 3, etc, which is all you can do with scaling-factor
// To enable HiDPI, use gsettings:
// gsettings set org.gnome.desktop.interface scaling-factor 2
}
}
} catch (Throwable e) {
if (SystemTray.DEBUG) {
SystemTray.logger.error("Cannot check scaling factor", e);
}
}
}
} else if (OS.isMacOsX()) {
@ -222,6 +279,7 @@ class ImageUtils {
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static
File getTransparentImage(final int size) {
File newFile = new File(TEMP_DIR, size + "_empty.png").getAbsoluteFile();
@ -243,6 +301,7 @@ class ImageUtils {
return newFile;
}
@SuppressWarnings("WeakerAccess")
public static
BufferedImage getTransparentImageAsImage(final int size) {
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);