Added support for macOS-AWT try type. The OSX native tray type was broken by the big sur (macos11) update.

Updated gradle
This commit is contained in:
Robinson 2022-12-27 15:47:29 +01:00
parent 6c0884b4e5
commit 724019615a
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
19 changed files with 1039 additions and 1084 deletions

View File

@ -71,7 +71,7 @@ import dorkbox.util.SwingUtil;
* </li>
* </ul>
*/
@SuppressWarnings({"unused", "Duplicates", "DanglingJavadoc", "WeakerAccess"})
@SuppressWarnings({"unused", "Duplicates", "WeakerAccess"})
public final
class SystemTray {
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
@ -152,14 +152,14 @@ class SystemTray {
}
/**
* Enables native menus on Windows/Linux/OSX instead of the swing menu. The drawback is that this menu is native, and sometimes
* Enables native menus on Windows/Linux/macOS instead of the swing menu. The drawback is that this menu is native, and sometimes
* native menus looks absolutely HORRID.
* <p>
* This always returns the same instance per JVM (it's a singleton), and on some platforms the system tray may not be
* supported, in which case this will return NULL.
* <p>
* If this is using the Swing SystemTray and a SecurityManager is installed, the AWTPermission {@code accessSystemTray} must
* be granted in order to get the {@code SystemTray} instance. Otherwise this will return null.
* be granted in order to get the {@code SystemTray} instance. Otherwise, this will return null.
*
* If you create MORE than 1 system tray, you should use {{@link SystemTray#get(String)}} instead, and specify a unique name for
* each instance
@ -170,14 +170,14 @@ class SystemTray {
}
/**
* Enables native menus on Windows/Linux/OSX instead of the swing menu. The drawback is that this menu is native, and sometimes
* Enables native menus on Windows/Linux/macOS instead of the swing menu. The drawback is that this menu is native, and sometimes
* native menus looks absolutely HORRID.
* <p>
* This always returns the same instance per JVM (it's a singleton), and on some platforms the system tray may not be
* supported, in which case this will return NULL.
* <p>
* If this is using the Swing SystemTray and a SecurityManager is installed, the AWTPermission {@code accessSystemTray} must
* be granted in order to get the {@code SystemTray} instance. Otherwise this will return null.
* be granted in order to get the {@code SystemTray} instance. Otherwise, this will return null.
*
* @param trayName This is the name assigned to the system tray instance. If you create MORE than 1 system tray,
* you must make sure to use different names (or un-predicable things can happen!).
@ -242,7 +242,7 @@ class SystemTray {
}
else if (isMacOsX) {
if (RenderProvider.isSwt() && FORCE_TRAY_TYPE == TrayType.Swing) {
// cannot mix Swing and SWT on MacOSX (for all versions of java) so we force native menus instead, which work just fine with SWT
// cannot mix Swing and SWT on MacOSX (for all versions of java) so we force ATW menus instead, which work just fine with SWT
// http://mail.openjdk.java.net/pipermail/bsd-port-dev/2008-December/000173.html
if (AUTO_FIX_INCONSISTENCIES) {
logger.warn("Unable to load Swing + SWT (for all versions of Java). Using the AWT Tray type instead.");
@ -265,7 +265,7 @@ class SystemTray {
}
}
else if (isNix) {
// linux/unix can use all of the tray types. AWT looks horrid. GTK versions are really sensitive...
// linux/unix can use all the tray types. AWT looks horrid. GTK versions are really sensitive...
// this checks to see if Swing/SWT/JavaFX has loaded GTK yet, and if so, what version they loaded.
// if swing is used, we have to do some extra checks...
@ -701,8 +701,27 @@ class SystemTray {
logger.debug("Tray menu image size: {}", menuImageSize);
}
if (!RenderProvider.isDefault() && SwingUtilities.isEventDispatchThread()) {
// This WILL NOT WORK. Let the dev know
logger.error("SystemTray initialization for JavaFX or SWT **CAN NOT** occur on the Swing Event Dispatch Thread " +
"(EDT). Something is seriously wrong.");
return null;
}
if (isTrayType(trayType, TrayType.Swing) ||
isTrayType(trayType, TrayType.Awt) ||
isTrayType(trayType, TrayType.Osx) ||
isTrayType(trayType, TrayType.WindowsNative)) {
// ensure AWT toolkit is initialized.
// OSX is based off of AWT now, instead of creating our own dispatch
java.awt.Toolkit.getDefaultToolkit();
}
if (AUTO_FIX_INCONSISTENCIES) {
// this logic has to be before we create the system Tray, but after GTK is started (if applicable)
// this logic has to be before we create the system Tray, but after AWT/GTK is started (if applicable)
if (isWindows && isTrayType(trayType, TrayType.Swing)) {
// we don't permit AWT for windows (it looks absolutely HORRID)
@ -710,7 +729,10 @@ class SystemTray {
// windows hard-codes the image size for AWT/SWING tray types
SystemTrayFixes.fixWindows(trayImageSize);
}
else if (isMacOsX && (isTrayType(trayType, TrayType.Awt) || isTrayType(trayType, TrayType.Swing))) {
else if (isMacOsX && (isTrayType(trayType, TrayType.Awt) ||
isTrayType(trayType, TrayType.Swing) ||
isTrayType(trayType, TrayType.Osx))) {
// macosx doesn't respond to all buttons (but should)
SystemTrayFixes.fixMacOS();
}
@ -723,18 +745,7 @@ class SystemTray {
if (!RenderProvider.isDefault() && SwingUtilities.isEventDispatchThread()) {
// This WILL NOT WORK. Let the dev know
logger.error("SystemTray initialization for JavaFX or SWT **CAN NOT** occur on the Swing Event Dispatch Thread " +
"(EDT). Something is seriously wrong.");
return null;
}
if (isTrayType(trayType, TrayType.Swing) || isTrayType(trayType, TrayType.Awt) || isTrayType(trayType, TrayType.WindowsNative)) {
// ensure AWT toolkit is initialized.
java.awt.Toolkit.getDefaultToolkit();
}
// initialize the tray icon height
// this is during init, so we can statically access this everywhere else. Multiple instances of this will always have the same value
@ -746,7 +757,7 @@ class SystemTray {
// guarantee that we are running on the event dispatch. It doesn't matter if we are "double-looping" on the EventDispatch,
// so extra checks are unnecessary.
// we have to make sure we shutdown on our own thread (and not the JavaFX/SWT/AWT/etc thread)
// we have to make sure we shut down on our own thread (and not the JavaFX/SWT/AWT/etc thread)
EventDispatch.runLater(()->{
// must remove ourselves from the init() map (since we want to be able to access things)
AutoDetectTrayType.removeSystemTrayHook(trayName);
@ -758,7 +769,7 @@ class SystemTray {
});
};
// the cache name **MUST** be combined with the currently logged in user, otherwise permissions get screwed up
// the cache name **MUST** be combined with the currently logged-in user, otherwise permissions get screwed up
// when there is more than 1 user logged in at the same time!
CacheUtil cache = new CacheUtil(trayName + "Cache" + "_" + System.getProperty("user.name"));
ImageResizeUtil imageResizeUtil = new ImageResizeUtil(cache);
@ -769,9 +780,9 @@ class SystemTray {
// javaFX and SWT **CAN NOT** start on the EDT!!
// linux + GTK/AppIndicator + windows-native menus must not start on the EDT!
// AWT/Swing must be constructed on the EDT however...
// AWT + Swing + AWT-macOS must be constructed on the EDT however...
if (RenderProvider.isDefault() &&
(isTrayType(trayType, TrayType.Swing) || isTrayType(trayType, TrayType.Awt))) {
(isTrayType(trayType, TrayType.Swing) || isTrayType(trayType, TrayType.Awt) || isTrayType(trayType, TrayType.Osx))) {
// have to construct swing stuff inside the swing EDT
final Class<? extends Menu> finalTrayType = trayType;
SwingUtil.invokeAndWait(()->{
@ -833,7 +844,7 @@ class SystemTray {
*/
public
void shutdown() {
// this will shutdown and do what it needs to. The onRemoveEvent cleans up.
// this will shut down and do what it needs to. The onRemoveEvent cleans up.
menu.remove();
}

View File

@ -0,0 +1,177 @@
/*
* Copyright 2022 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.ui.osx;
import java.awt.Image;
import java.awt.MenuShortcut;
import java.awt.PopupMenu;
import java.io.File;
import javax.swing.ImageIcon;
import dorkbox.systemTray.Checkbox;
import dorkbox.systemTray.Entry;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.Separator;
import dorkbox.systemTray.Status;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.peer.MenuPeer;
import dorkbox.systemTray.util.AwtAccessor;
import dorkbox.util.SwingUtil;
// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both
class AwtOsxMenu implements MenuPeer {
volatile java.awt.Menu _native;
private final AwtOsxMenu parent;
// we cannot access the peer object NORMALLY, so we use tricks (via looking at the osx source code)
// peerObj will be null for the TrayImpl!
private final Object peerObj;
// This is NOT a copy constructor!
@SuppressWarnings("IncompleteCopyConstructor")
AwtOsxMenu(final AwtOsxMenu parent) {
this.parent = parent;
// are we a menu or a sub-menu?
if (parent == null) {
this._native = new PopupMenu();
}
else {
this._native = new java.awt.Menu();
parent._native.add(this._native);
}
peerObj = AwtAccessor.getPeer(_native);
}
@Override
public
void add(final Menu parentMenu, final Entry entry, final int index) {
// must always be called on the EDT
SwingUtil.invokeAndWaitQuietly(()->{
if (entry instanceof Menu) {
AwtOsxMenu menu = new AwtOsxMenu(AwtOsxMenu.this);
((Menu) entry).bind(menu, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof Separator) {
AwtOsxMenuItemSeparator item = new AwtOsxMenuItemSeparator(AwtOsxMenu.this);
entry.bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof Checkbox) {
AwtOsxMenuItemCheckbox item = new AwtOsxMenuItemCheckbox(AwtOsxMenu.this);
((Checkbox) entry).bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof Status) {
AwtOsxMenuItemStatus item = new AwtOsxMenuItemStatus(AwtOsxMenu.this);
((Status) entry).bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof MenuItem) {
AwtOsxMenuItem item = new AwtOsxMenuItem(AwtOsxMenu.this);
((MenuItem) entry).bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
});
}
// is overridden in tray impl
@SuppressWarnings("DuplicatedCode")
@Override
public
void setImage(final MenuItem menuItem) {
// lucky for us, macOS AWT menu items CAN show images, but it takes a bit of magic.
// peerObj will be null for the TrayImpl!
File imageFile = menuItem.getImage();
if (peerObj != null && imageFile != null) {
Image image = new ImageIcon(imageFile.getAbsolutePath()).getImage();
SwingUtil.invokeLater(()-> {
try {
AwtAccessor.setImage(peerObj, image);
} catch (Exception e) {
SystemTray.logger.error("Unable to setImage for awt-osx menus.", e);
}
});
}
}
// is overridden in tray impl
@Override
public
void setEnabled(final MenuItem menuItem) {
SwingUtil.invokeLater(()->_native.setEnabled(menuItem.getEnabled()));
}
// is overridden in tray impl
@Override
public
void setText(final MenuItem menuItem) {
SwingUtil.invokeLater(()->_native.setLabel(menuItem.getText()));
}
@Override
public
void setCallback(final MenuItem menuItem) {
// can't have a callback for menus!
}
// is overridden in tray impl
@Override
public
void setShortcut(final MenuItem menuItem) {
// Will return 0 as the vKey if it's not set (which will remove the shortcut)
final int vKey = SwingUtil.getVirtualKey(menuItem.getShortcut());
SwingUtil.invokeLater(()->_native.setShortcut(new MenuShortcut(vKey)));
}
@SuppressWarnings("DuplicatedCode")
// is overridden in tray impl
@Override
public
void setTooltip(final MenuItem menuItem) {
// lucky for us, macOS AWT menu items CAN show tooltips, but it takes a bit of magic.
// peerObj will be null for the TrayImpl!
String tooltipText = menuItem.getTooltip();
if (peerObj != null && tooltipText != null) {
SwingUtil.invokeLater(()-> {
try {
AwtAccessor.setToolTipText(peerObj, tooltipText);
} catch (Exception e) {
SystemTray.logger.error("Unable to setTooltip for awt-osx menus.", e);
}
});
}
}
@Override
public
void remove() {
SwingUtil.invokeLater(()->{
_native.removeAll();
_native.deleteShortcut();
_native.setEnabled(false);
_native.removeNotify();
if (parent != null) {
parent._native.remove(_native);
}
});
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright 2022 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.ui.osx;
import java.awt.Image;
import java.awt.MenuShortcut;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.ImageIcon;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.peer.MenuItemPeer;
import dorkbox.systemTray.util.AwtAccessor;
import dorkbox.systemTray.util.EventDispatch;
import dorkbox.util.SwingUtil;
class AwtOsxMenuItem implements MenuItemPeer {
private final AwtOsxMenu parent;
private final java.awt.MenuItem _native = new java.awt.MenuItem();
private volatile ActionListener callback;
// we cannot access the peer object NORMALLY, so we use tricks (via looking at the osx source code)
private final Object peerObj;
// this is ALWAYS called on the EDT.
AwtOsxMenuItem(final AwtOsxMenu parent) {
this.parent = parent;
parent._native.add(_native);
peerObj = AwtAccessor.getPeer(_native);
}
@SuppressWarnings("DuplicatedCode")
@Override
public
void setImage(final MenuItem menuItem) {
// lucky for us, macOS AWT menu items CAN show images, but it takes a bit of magic.
File imageFile = menuItem.getImage();
if (peerObj != null && imageFile != null) {
Image image = new ImageIcon(imageFile.getAbsolutePath()).getImage();
SwingUtil.invokeLater(()-> {
try {
AwtAccessor.setImage(peerObj, image);
} catch (Exception e) {
SystemTray.logger.error("Unable to setImage for awt-osx menus.", e);
}
});
}
}
@Override
public
void setEnabled(final MenuItem menuItem) {
SwingUtil.invokeLater(()->_native.setEnabled(menuItem.getEnabled()));
}
@Override
public
void setText(final MenuItem menuItem) {
SwingUtil.invokeLater(()->_native.setLabel(menuItem.getText()));
}
@SuppressWarnings("Duplicates")
@Override
public
void setCallback(final MenuItem menuItem) {
if (callback != null) {
_native.removeActionListener(callback);
}
callback = menuItem.getCallback(); // can be set to null
if (callback != null) {
callback = new ActionListener() {
final ActionListener cb = menuItem.getCallback();
@Override
public
void actionPerformed(ActionEvent e) {
// we want it to run on our own with our own action event info (so it is consistent across all platforms)
EventDispatch.runLater(()->{
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(callback);
}
}
@Override
public
void setShortcut(final MenuItem menuItem) {
// Will return 0 as the vKey if it's not set (which will remove the shortcut)
final int vKey = SwingUtil.getVirtualKey(menuItem.getShortcut());
SwingUtil.invokeLater(()->_native.setShortcut(new MenuShortcut(vKey)));
}
@SuppressWarnings("DuplicatedCode")
@Override
public
void setTooltip(final MenuItem menuItem) {
// lucky for us, macOS AWT menu items CAN show tooltips, but it takes a bit of magic.
String tooltipText = menuItem.getTooltip();
if (peerObj != null && tooltipText != null) {
SwingUtil.invokeLater(()-> {
try {
AwtAccessor.setToolTipText(peerObj, tooltipText);
} catch (Exception e) {
SystemTray.logger.error("Unable to setTooltip for awt-osx menus.", e);
}
});
}
}
@SuppressWarnings("Duplicates")
@Override
public
void remove() {
SwingUtil.invokeLater(()->{
_native.deleteShortcut();
_native.setEnabled(false);
if (callback != null) {
_native.removeActionListener(callback);
callback = null;
}
parent._native.remove(_native);
_native.removeNotify();
});
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright 2022 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.ui.osx;
import java.awt.MenuShortcut;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import dorkbox.systemTray.Checkbox;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.peer.CheckboxPeer;
import dorkbox.systemTray.util.AwtAccessor;
import dorkbox.systemTray.util.EventDispatch;
import dorkbox.util.SwingUtil;
class AwtOsxMenuItemCheckbox implements CheckboxPeer {
private final AwtOsxMenu parent;
private final java.awt.CheckboxMenuItem _native = new java.awt.CheckboxMenuItem();
// these have to be volatile, because they can be changed from any thread
private volatile ItemListener callback;
private volatile boolean isChecked = false;
// we cannot access the peer object NORMALLY, so we use tricks (via looking at the osx source code)
private final Object peerObj;
// this is ALWAYS called on the EDT.
AwtOsxMenuItemCheckbox(final AwtOsxMenu parent) {
this.parent = parent;
parent._native.add(_native);
peerObj = AwtAccessor.getPeer(_native);
}
@Override
public
void setEnabled(final Checkbox menuItem) {
SwingUtil.invokeLater(()->_native.setEnabled(menuItem.getEnabled()));
}
@Override
public
void setText(final Checkbox menuItem) {
SwingUtil.invokeLater(()->_native.setLabel(menuItem.getText()));
}
@SuppressWarnings("Duplicates")
@Override
public
void setCallback(final Checkbox menuItem) {
// of critical note: AWT only works with ItemListener -- but we use ActionListener for everything, so here we make things compatible
if (callback != null) {
_native.removeItemListener(callback);
}
ActionListener callback = menuItem.getCallback(); // can be set to null
if (callback != null) {
this.callback = new ItemListener() {
final ActionListener cb = menuItem.getCallback();
@Override
public
void itemStateChanged(final ItemEvent e) {
// this will run on the EDT, since we are calling it from the EDT
menuItem.setChecked(!isChecked);
// we want it to run on our own with our own action event info (so it is consistent across all platforms)
EventDispatch.runLater(()->{
try {
cb.actionPerformed(new ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, ""));
} catch (Throwable throwable) {
SystemTray.logger.error("Error calling menu checkbox entry {} click event.", menuItem.getText(), throwable);
}
});
}
};
_native.addItemListener(this.callback);
}
}
@Override
public
void setShortcut(final Checkbox menuItem) {
// Will return 0 as the vKey if it's not set (which will remove the shortcut)
final int vKey = SwingUtil.getVirtualKey(menuItem.getShortcut());
SwingUtil.invokeLater(()->_native.setShortcut(new MenuShortcut(vKey)));
}
@SuppressWarnings("DuplicatedCode")
@Override
public
void setTooltip(final Checkbox menuItem) {
// lucky for us, macOS AWT menu items CAN show tooltips, but it takes a bit of magic.
String tooltipText = menuItem.getTooltip();
if (peerObj != null && tooltipText != null) {
SwingUtil.invokeLater(()-> {
try {
AwtAccessor.setToolTipText(peerObj, tooltipText);
} catch (Exception e) {
SystemTray.logger.error("Unable to setTooltip for awt-osx menus.", e);
}
});
}
}
@Override
public
void setChecked(final Checkbox menuItem) {
boolean checked = menuItem.getChecked();
// only dispatch if it's actually different
if (checked != this.isChecked) {
this.isChecked = checked;
SwingUtil.invokeLater(()->_native.setState(isChecked));
}
}
@SuppressWarnings("Duplicates")
@Override
public
void remove() {
SwingUtil.invokeLater(()->{
_native.deleteShortcut();
_native.setEnabled(false);
if (callback != null) {
_native.removeItemListener(callback);
callback = null;
}
parent._native.remove(_native);
_native.removeNotify();
});
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 dorkbox, llc
* Copyright 2022 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,22 +15,25 @@
*/
package dorkbox.systemTray.ui.osx;
import dorkbox.jna.macos.cocoa.NSMenuItem;
import dorkbox.systemTray.peer.EntryPeer;
import dorkbox.util.SwingUtil;
class OsxMenuItemSeparator implements EntryPeer {
class AwtOsxMenuItemSeparator implements EntryPeer {
private final NSMenuItem _native = NSMenuItem.separatorItem();
private final OsxMenu parent;
private final AwtOsxMenu parent;
private final java.awt.MenuItem _native = new java.awt.MenuItem("-");
OsxMenuItemSeparator(final OsxMenu parent) {
// this is ALWAYS called on the EDT.
AwtOsxMenuItemSeparator(final AwtOsxMenu parent) {
this.parent = parent;
parent.addItem(_native);
parent._native.add(_native);
}
@Override
public
void remove() {
parent.removeItem(_native);
SwingUtil.invokeLater(()->parent._native.remove(_native));
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2022 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.ui.osx;
import static java.awt.Font.DIALOG;
import java.awt.Font;
import java.awt.MenuItem;
import dorkbox.systemTray.Status;
import dorkbox.systemTray.peer.StatusPeer;
import dorkbox.util.SwingUtil;
class AwtOsxMenuItemStatus implements StatusPeer {
private final AwtOsxMenu parent;
private final MenuItem _native = new MenuItem();
AwtOsxMenuItemStatus(final AwtOsxMenu parent) {
this.parent = parent;
// status is ALWAYS at 0 index...
parent._native.insert(_native, 0);
}
@Override
public
void setText(final Status menuItem) {
SwingUtil.invokeLater(()->{
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(()->parent._native.remove(_native));
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright 2021 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.ui.osx;
import java.awt.image.BufferedImage;
import dorkbox.jna.macos.cocoa.NSImage;
import dorkbox.jna.macos.cocoa.NSInteger;
import dorkbox.jna.macos.cocoa.NSMenuItem;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.peer.EntryPeer;
import dorkbox.systemTray.util.SizeAndScalingUtil;
import dorkbox.util.ImageUtil;
abstract
class OsxBaseMenuItem implements EntryPeer {
// these are necessary BECAUSE OSX menus look funky when there are some menu entries WITH icons and some WITHOUT
private static NSImage transparentIcon = null;
/**
* @param menuImageSize this is the largest size of an image used in a JMenuItem, before the size of the JMenuItem is forced to be larger
*/
static NSImage getTransparentIcon(int menuImageSize) {
if (transparentIcon == null) {
NSImage transparentIcon_;
try {
final BufferedImage image = ImageUtil.createImageAsBufferedImage(3, menuImageSize, null);
transparentIcon_ = new NSImage(ImageUtil.toBytes(image));
} catch (Exception e) {
transparentIcon_ = null;
SystemTray.logger.error("Error creating transparent image.", e);
}
transparentIcon = transparentIcon_;
}
return transparentIcon;
}
// the native OSX components
protected final OsxMenu parent;
protected final NSMenuItem _native = new NSMenuItem();
// to prevent GC
@SuppressWarnings("FieldCanBeLocal")
private final NSInteger indentationLevel = new NSInteger(1);
OsxBaseMenuItem(final OsxMenu parent) {
this.parent = parent;
// this is to provide reasonable spacing for the menu item, otherwise it looks weird
_native.setIndentationLevel(indentationLevel);
_native.setImage(getTransparentIcon(SizeAndScalingUtil.TRAY_MENU_SIZE));
parent.addItem(_native);
}
@Override
public
void remove() {
_native.setImage(null);
if (parent != null) {
parent.removeItem(_native);
}
}
}

View File

@ -1,90 +0,0 @@
/*
* Copyright 2021 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.ui.osx;
import java.util.HashMap;
import com.sun.jna.Callback;
import com.sun.jna.Pointer;
import dorkbox.jna.macos.cocoa.NSObject;
import dorkbox.jna.macos.cocoa.OsxClickCallback;
import dorkbox.jna.macos.foundation.ObjectiveC;
//@formatter:off
class OsxClickAction extends NSObject {
// NOTE: The order in this file is CRITICAL to the behavior of this class. Changing the order of anything here will BREAK functionality!
private static final Pointer registerObjectClass = ObjectiveC.objc_allocateClassPair(NSObject.objectClass, OsxClickAction.class.getSimpleName(), 0);
private static final Pointer registerActionSelector = ObjectiveC.sel_registerName("action");
static final Pointer action;
static {
Callback registerClickAction = new Callback() {
@SuppressWarnings("unused")
public
void callback(Pointer self, Pointer selector) {
if (selector.equals(registerActionSelector)) {
OsxClickAction action;
synchronized (clickMap){
action = clickMap.get(Pointer.nativeValue(self));
}
if (action != null){
action.callback.click();
}
}
}
};
if (!ObjectiveC.class_addMethod(registerObjectClass, registerActionSelector, registerClickAction, "v@:")) {
throw new RuntimeException("Error initializing click action as a objective C class");
}
ObjectiveC.objc_registerClassPair(registerObjectClass);
action = ObjectiveC.sel_getUid("action");
}
private static final Pointer objectClass = ObjectiveC.objc_lookUpClass(OsxClickAction.class.getSimpleName());
private static final HashMap<Long, OsxClickAction> clickMap = new HashMap<Long, OsxClickAction>();
private final OsxClickCallback callback;
OsxClickAction(OsxClickCallback callback) {
super(ObjectiveC.class_createInstance(objectClass, 0));
synchronized (clickMap){
clickMap.put(asPointer(), this);
}
this.callback = callback;
}
void remove() {
clickMap.remove(asPointer());
}
@Override protected
void finalize() throws Throwable {
synchronized (clickMap){
clickMap.remove(asPointer());
}
super.finalize();
}
}

View File

@ -1,190 +0,0 @@
/*
* Copyright 2021 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.ui.osx;
import dorkbox.jna.macos.cocoa.NSCellStateValue;
import dorkbox.jna.macos.cocoa.NSImage;
import dorkbox.jna.macos.cocoa.NSInteger;
import dorkbox.jna.macos.cocoa.NSMenu;
import dorkbox.jna.macos.cocoa.NSMenuItem;
import dorkbox.jna.macos.cocoa.NSString;
import dorkbox.systemTray.Checkbox;
import dorkbox.systemTray.Entry;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.Separator;
import dorkbox.systemTray.Status;
import dorkbox.systemTray.peer.MenuPeer;
import dorkbox.systemTray.util.SizeAndScalingUtil;
class OsxMenu implements MenuPeer {
// the native OSX components
protected final OsxMenu parent;
protected final NSMenuItem _native = new NSMenuItem();
volatile NSMenu _nativeMenu;
// to prevent GC
private volatile NSImage image;
private NSString tooltip;
private NSString title;
private NSString keyEquivalent;
@SuppressWarnings("FieldCanBeLocal")
private final NSInteger indentationLevel = new NSInteger(1);
// called by the system tray constructors
// This is NOT a copy constructor!
OsxMenu() {
this(null);
}
OsxMenu(final OsxMenu parent) {
this.parent = parent;
_nativeMenu = new NSMenu();
if (parent != null) {
_native.setSubmenu(_nativeMenu);
parent.addItem(_native);
// this is to provide reasonable spacing for the menu item, otherwise it looks weird
_native.setIndentationLevel(indentationLevel);
_native.setImage(OsxBaseMenuItem.getTransparentIcon(SizeAndScalingUtil.TRAY_MENU_SIZE));
}
}
@Override
public
void add(final Menu parentMenu, final Entry entry, final int index) {
if (entry instanceof Menu) {
OsxMenu menu = new OsxMenu(OsxMenu.this);
((Menu) entry).bind(menu, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof Separator) {
OsxMenuItemSeparator item = new OsxMenuItemSeparator(OsxMenu.this);
entry.bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof Checkbox) {
OsxMenuItemCheckbox item = new OsxMenuItemCheckbox(OsxMenu.this);
((Checkbox) entry).bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof Status) {
OsxMenuItemStatus item = new OsxMenuItemStatus(OsxMenu.this);
((Status) entry).bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
else if (entry instanceof MenuItem) {
OsxMenuItem item = new OsxMenuItem(OsxMenu.this);
((MenuItem) entry).bind(item, parentMenu, parentMenu.getImageResizeUtil());
}
}
@SuppressWarnings("Duplicates")
@Override
public
void setImage(final MenuItem menuItem) {
if (menuItem.getImage() != null) {
_native.setState(NSCellStateValue.NSOnState);
image = new NSImage(menuItem.getImage());
_native.setOnStateImage(image);
}
else {
_native.setState(NSCellStateValue.NSOffState);
_native.setOnStateImage(null);
}
}
@Override
public
void setEnabled(final MenuItem menuItem) {
_native.setEnabled(menuItem.getEnabled());
}
@Override
public
void setText(final MenuItem menuItem) {
String text = menuItem.getText();
if (text == null || text.isEmpty()) {
title = null;
}
else {
title = new NSString(text);
}
_native.setTitle(title);
}
@Override
public
void setCallback(final MenuItem menuItem) {
// no op
}
@Override
public
void setShortcut(final MenuItem menuItem) {
char shortcut = menuItem.getShortcut();
if (shortcut != 0) {
keyEquivalent = new NSString(Character.toString(shortcut).toLowerCase());
} else {
keyEquivalent = new NSString("");
}
_native.setKeyEquivalent(keyEquivalent);
}
@Override
public
void setTooltip(final MenuItem menuItem) {
String tooltip = menuItem.getTooltip();
if (tooltip == null || tooltip.isEmpty()) {
this.tooltip = null;
}
else {
this.tooltip = new NSString(tooltip);
}
_native.setToolTip(this.tooltip);
}
@Override
public
void remove() {
if (parent != null) {
parent.removeItem(_native);
}
title = null;
tooltip = null;
keyEquivalent = null;
image = null;
_native.setImage(null);
_native.setTarget(null);
_native.setAction(null);
}
// to make native add/remove easier for children
void addItem(final NSMenuItem item) {
_nativeMenu.addItem(item);
}
void removeItem(final NSMenuItem item) {
_nativeMenu.removeItem(item);
}
}

View File

@ -1,172 +0,0 @@
/*
* Copyright 2021 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.ui.osx;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import dorkbox.jna.macos.cocoa.NSCellStateValue;
import dorkbox.jna.macos.cocoa.NSImage;
import dorkbox.jna.macos.cocoa.NSString;
import dorkbox.jna.macos.cocoa.OsxClickCallback;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.peer.MenuItemPeer;
import dorkbox.systemTray.util.EventDispatch;
class OsxMenuItem extends OsxBaseMenuItem implements MenuItemPeer, OsxClickCallback {
// these have to be volatile, because they can be changed from any thread
private volatile ActionListener callback;
// to prevent GC
private final OsxClickAction clickAction;
private volatile NSImage image;
private NSString tooltip;
private NSString title;
private NSString keyEquivalent;
OsxMenuItem(final OsxMenu parent) {
super(parent);
clickAction = new OsxClickAction(this);
_native.setTarget(clickAction);
_native.setAction(OsxClickAction.action);
}
@Override
public
void click() {
ActionListener callback = this.callback;
if (callback != null) {
callback.actionPerformed(null);
}
}
@SuppressWarnings("Duplicates")
@Override
public
void setImage(final MenuItem menuItem) {
if (menuItem.getImage() != null) {
_native.setState(NSCellStateValue.NSOnState);
image = new NSImage(menuItem.getImage());
_native.setOnStateImage(image);
}
else {
_native.setState(NSCellStateValue.NSOffState);
_native.setOnStateImage(null);
}
}
@Override
public
void setEnabled(final MenuItem menuItem) {
_native.setEnabled(menuItem.getEnabled());
}
@Override
public
void setText(final MenuItem menuItem) {
String text = menuItem.getText();
if (text == null || text.isEmpty()) {
title = null;
}
else {
title = new NSString(text);
}
_native.setTitle(title);
}
@SuppressWarnings("Duplicates")
@Override
public
void setCallback(final MenuItem menuItem) {
callback = menuItem.getCallback(); // can be set to null
if (callback != null) {
callback = new ActionListener() {
final ActionListener cb = menuItem.getCallback();
@Override
public
void actionPerformed(ActionEvent e) {
// we want it to run on our own with our own action event info (so it is consistent across all platforms)
EventDispatch.runLater(()->{
try {
cb.actionPerformed(new ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, ""));
} catch (Throwable throwable) {
SystemTray.logger.error("Error calling menu entry {} click event.", menuItem.getText(), throwable);
}
});
}
};
}
}
/**
* NOTE:
* OSX can have upper + lower case shortcuts, so we force lowercase because Linux/windows do not have uppercase.
* Additionally, we cater to the lowest common denominator (as much as is reasonable), so lower-case for everyone.
*/
@Override
public
void setShortcut(final MenuItem menuItem) {
char shortcut = menuItem.getShortcut();
if (shortcut != 0) {
keyEquivalent = new NSString(Character.toString(shortcut).toLowerCase());
} else {
keyEquivalent = new NSString("");
}
_native.setKeyEquivalent(keyEquivalent);
}
@Override
public
void setTooltip(final MenuItem menuItem) {
String tooltip = menuItem.getTooltip();
if (tooltip == null || tooltip.isEmpty()) {
this.tooltip = null;
}
else {
this.tooltip = new NSString(tooltip);
}
_native.setToolTip(this.tooltip);
}
@Override
public
void remove() {
super.remove();
title = null;
tooltip = null;
keyEquivalent = null;
callback = null;
_native.setTarget(null);
_native.setAction(null);
clickAction.remove();
image = null;
}
}

View File

@ -1,174 +0,0 @@
/*
* Copyright 2021 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.ui.osx;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import dorkbox.jna.macos.cocoa.NSCellStateValue;
import dorkbox.jna.macos.cocoa.NSString;
import dorkbox.jna.macos.cocoa.OsxClickCallback;
import dorkbox.systemTray.Checkbox;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.peer.CheckboxPeer;
import dorkbox.systemTray.util.EventDispatch;
class OsxMenuItemCheckbox extends OsxBaseMenuItem implements CheckboxPeer, OsxClickCallback {
// to prevent GC
private final OsxClickAction clickAction;
private NSString tooltip;
private NSString title;
private NSString keyEquivalent;
// these have to be volatile, because they can be changed from any thread
private volatile ActionListener callback;
private volatile boolean isChecked = false;
OsxMenuItemCheckbox(final OsxMenu parent) {
super(parent);
clickAction = new OsxClickAction(this);
_native.setTarget(clickAction);
_native.setAction(OsxClickAction.action);
}
@Override
public
void click() {
ActionListener callback = this.callback;
if (callback != null) {
callback.actionPerformed(null);
}
}
@Override
public
void setEnabled(final Checkbox menuItem) {
_native.setEnabled(menuItem.getEnabled());
}
@Override
public
void setText(final Checkbox menuItem) {
String text = menuItem.getText();
if (text == null || text.isEmpty()) {
title = null;
}
else {
title = new NSString(text);
}
_native.setTitle(title);
}
@SuppressWarnings("Duplicates")
@Override
public
void setCallback(final Checkbox menuItem) {
callback = menuItem.getCallback(); // can be set to null
if (callback != null) {
callback = new ActionListener() {
final ActionListener cb = menuItem.getCallback();
@Override
public
void actionPerformed(ActionEvent e) {
// This can ALSO recursively call the callback
menuItem.setChecked(!isChecked);
// we want it to run on our own with our own action event info (so it is consistent across all platforms)
EventDispatch.runLater(()->{
try {
cb.actionPerformed(new ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, ""));
} catch (Throwable throwable) {
SystemTray.logger.error("Error calling menu entry {} click event.", menuItem.getText(), throwable);
}
});
}
};
} else {
callback = e->{
// This can ALSO recursively call the callback
menuItem.setChecked(!isChecked);
};
}
}
@Override
public
void setShortcut(final Checkbox menuItem) {
char shortcut = menuItem.getShortcut();
if (shortcut != 0) {
keyEquivalent = new NSString(Character.toString(shortcut).toLowerCase());
} else {
keyEquivalent = new NSString("");
}
_native.setKeyEquivalent(keyEquivalent);
}
@Override
public
void setChecked(final Checkbox menuItem) {
boolean checked = menuItem.getChecked();
// only dispatch if it's actually different
if (checked != this.isChecked) {
this.isChecked = checked;
if (isChecked) {
_native.setState(NSCellStateValue.NSOnState);
}
else {
_native.setState(NSCellStateValue.NSOffState);
}
}
}
@Override
public
void setTooltip(final Checkbox menuItem) {
String tooltip = menuItem.getTooltip();
if (tooltip == null || tooltip.isEmpty()) {
this.tooltip = null;
}
else {
this.tooltip = new NSString(tooltip);
}
_native.setToolTip(this.tooltip);
}
@Override
public
void remove() {
super.remove();
title = null;
tooltip = null;
keyEquivalent = null;
callback = null;
_native.setTarget(null);
_native.setAction(null);
clickAction.remove();
}
}

View File

@ -1,54 +0,0 @@
/*
* Copyright 2021 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.ui.osx;
import dorkbox.jna.macos.cocoa.NSString;
import dorkbox.systemTray.Status;
import dorkbox.systemTray.peer.StatusPeer;
class OsxMenuItemStatus extends OsxBaseMenuItem implements StatusPeer {
// to prevent GC
private NSString title;
OsxMenuItemStatus(final OsxMenu parent) {
super(parent);
_native.setEnabled(false);
}
@Override
public
void setText(final Status menuItem) {
String text = menuItem.getText();
if (text == null || text.isEmpty()) {
title = null;
}
else {
title = new NSString(text);
}
_native.setTitle(title);
}
@Override
public
void remove() {
super.remove();
title = null;
}
}

View File

@ -0,0 +1,255 @@
/*
* Copyright 2022 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.ui.osx;
import java.awt.AWTException;
import java.awt.Image;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.io.File;
import javax.swing.ImageIcon;
import dorkbox.collections.ArrayMap;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.Tray;
import dorkbox.systemTray.util.ImageResizeUtil;
import dorkbox.util.SwingUtil;
/**
* The previous, native access we used to create menus NO LONGER works on any OS beyond Big Sur (macOS 11), and now the *best* way
* to access this (since I do not want to rewrite a LOT of code), is to use AWT hacks to access images + tooltips via reflection. This
* has been possible since jdk8. While I don't like reflection, it is sadly the only way to do this.
*
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/7fcf35286d52/src/macosx/classes/sun/lwawt/macosx/CMenuItem.java
*/
public
class _OsxAwtTray extends Tray {
private volatile SystemTray tray;
private volatile TrayIcon trayIcon;
// is the system tray visible or not.
private volatile boolean visible = false;
private volatile File imageFile;
private volatile String tooltipText = "";
private final Object keepAliveLock = new Object[0];
private volatile Thread keepAliveThread;
// The image resources are cached, so that if someone is trying to create an animation, the image resource is re-used instead of
// constantly created/destroyed -- which over time leads to issues.
// This cache isn't anything fancy, it just lets us reuse what we have. It's cleared on hide(), and it will auto-grow as necessary.
// If someone uses a different file every time, then this will cause problems. An error log is added if a different image is created 100x
private final ArrayMap<String, Image> imageCache = new ArrayMap<>(false, 10);
@SuppressWarnings("unused")
public
_OsxAwtTray(final String trayName, final ImageResizeUtil imageResizeUtil, final Runnable onRemoveEvent) {
super(onRemoveEvent);
if (!SystemTray.isSupported()) {
throw new RuntimeException("System Tray is not supported in this configuration! Please write an issue and include your OS " +
"type and configuration");
}
// we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization.
final AwtOsxMenu awtMenu = new AwtOsxMenu(null) {
@Override
public
void setEnabled(final MenuItem menuItem) {
SwingUtil.invokeLater(()->{
if (tray == null) {
tray = SystemTray.getSystemTray();
}
boolean enabled = menuItem.getEnabled();
if (keepAliveThread != null) {
synchronized (keepAliveLock) {
keepAliveLock.notifyAll();
}
}
keepAliveThread = null;
if (visible && !enabled) {
// THIS WILL NOT keep the app running, so we use a "keep-alive" thread so this behavior is THE SAME across
// all platforms. This was only noticed on macOS (where the app would quit after calling setEnabled(false);
keepAliveThread = new Thread(()->{
synchronized (keepAliveLock) {
keepAliveLock.notifyAll();
try {
keepAliveLock.wait();
} catch (InterruptedException ignored) {
}
}
}, "TrayKeepAliveThread");
keepAliveThread.start();
}
if (visible && !enabled) {
tray.remove(trayIcon);
visible = false;
}
else if (!visible && enabled && trayIcon != null) {
try {
tray.add(trayIcon);
visible = true;
// don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we
// want to make sure keep the tooltip text the same as before.
trayIcon.setToolTip(tooltipText);
} catch (AWTException e) {
dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray", e);
}
}
});
}
@Override
public
void setImage(final MenuItem menuItem) {
imageFile = menuItem.getImage();
SwingUtil.invokeLater(()->{
if (tray == null) {
tray = SystemTray.getSystemTray();
}
final Image trayImage;
if (imageFile != null) {
String path = imageFile.getAbsolutePath();
synchronized (imageCache) {
Image previousImage = imageCache.get(path);
if (previousImage == null) {
previousImage = new ImageIcon(path).getImage();
imageCache.put(path, previousImage);
if (imageCache.size > 120) {
dorkbox.systemTray.SystemTray.logger.error("More than 120 different images used for the SystemTray icon. This will lead to performance issues.");
}
}
trayImage = previousImage;
}
} else {
trayImage = null;
}
if (trayIcon == null) {
if (trayImage == null) {
// we can't do anything!
return;
} else {
trayIcon = new TrayIcon(trayImage);
}
trayIcon.setPopupMenu((PopupMenu) _native);
try {
tray.add(trayIcon);
visible = true;
} catch (AWTException e) {
dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e);
}
} else {
trayIcon.setImage(trayImage);
}
// don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we
// want to make sure keep the tooltip text the same as before.
trayIcon.setToolTip(tooltipText);
});
}
@Override
public
void setText(final MenuItem menuItem) {
// no op.
}
@Override
public
void setShortcut(final MenuItem menuItem) {
// no op
}
@Override
public
void setTooltip(final MenuItem menuItem) {
final String text = menuItem.getTooltip();
if (tooltipText != null && tooltipText.equals(text) ||
tooltipText == null && text != null) {
return;
}
tooltipText = text;
SwingUtil.invokeLater(()->{
// don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we
// want to make sure keep the tooltip text the same as before.
if (trayIcon != null) {
trayIcon.setToolTip(text);
}
});
}
@Override
public
void remove() {
synchronized (imageCache) {
for (final Image value : imageCache.values()) {
value.flush();
}
imageCache.clear();
}
SwingUtil.invokeLater(()->{
if (trayIcon != null) {
trayIcon.setPopupMenu(null);
if (tray != null) {
tray.remove(trayIcon);
}
trayIcon = null;
}
tray = null;
});
super.remove();
// make sure this thread doesn't keep the JVM alive anymore
if (keepAliveThread != null) {
synchronized (keepAliveLock) {
keepAliveLock.notifyAll();
}
}
}
};
bind(awtMenu, null, imageResizeUtil);
}
@Override
public
boolean hasImage() {
return imageFile != null;
}
}

View File

@ -1,197 +0,0 @@
/*
* Copyright 2021 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.ui.osx;
import java.io.File;
import dorkbox.jna.macos.cocoa.NSImage;
import dorkbox.jna.macos.cocoa.NSStatusBar;
import dorkbox.jna.macos.cocoa.NSStatusItem;
import dorkbox.jna.macos.cocoa.NSString;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.Tray;
import dorkbox.systemTray.util.ImageResizeUtil;
/**
*
*/
public
class _OsxNativeTray extends Tray {
// is the system tray visible or not.
private volatile boolean visible = false;
private volatile File imageFile;
private volatile String tooltipText = "";
private final Object keepAliveLock = new Object[0];
@SuppressWarnings("FieldCanBeLocal")
private final Thread keepAliveThread;
// references are ALSO to prevent GC
private final NSStatusBar statusBar;
private volatile NSStatusItem statusItem;
private volatile NSString statusItemTooltip;
private volatile NSImage statusItemImage;
@SuppressWarnings("unused")
public
_OsxNativeTray(final String trayName, final ImageResizeUtil imageResizeUtil, final Runnable onRemoveEvent) {
super(onRemoveEvent);
// THIS WILL NOT keep the app running, so we use a "keep-alive" thread so this behavior is THE SAME across
// all platforms.
keepAliveThread = new Thread(()->{
synchronized (keepAliveLock) {
keepAliveLock.notifyAll();
try {
keepAliveLock.wait();
} catch (InterruptedException ignored) {
}
}
}, "TrayKeepAliveThread");
keepAliveThread.start();
// we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization.
final OsxMenu osxMenu = new OsxMenu() {
@Override
public
void setEnabled(final MenuItem menuItem) {
if (statusItem == null) {
statusItem = statusBar.newStatusItem();
statusItem.setHighlightMode(true);
statusItem.setMenu(this._nativeMenu);
}
boolean enabled = menuItem.getEnabled();
if (visible && !enabled) {
statusBar.removeStatusItem(statusItem);
visible = false;
}
else if (!visible && enabled) {
visible = true;
// don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we
// want to make sure keep the tooltip text the same as before.
statusItem.setToolTip(statusItemTooltip);
statusItem.setImage(statusItemImage);
}
}
@Override
public
void setImage(final MenuItem menuItem) {
imageFile = menuItem.getImage();
if (statusItem == null) {
statusItem = statusBar.newStatusItem();
statusItem.setHighlightMode(true);
statusItem.setMenu(this._nativeMenu);
}
if (imageFile == null) {
statusItemImage = null;
}
else {
statusItemImage = new NSImage(imageFile);
}
statusItem.setImage(statusItemImage);
// don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we
// want to make sure keep the tooltip text the same as before.
statusItem.setToolTip(statusItemTooltip);
}
@Override
public
void setText(final MenuItem menuItem) {
// no op.
}
@Override
public
void setShortcut(final MenuItem menuItem) {
// no op
}
@Override
public
void setTooltip(final MenuItem menuItem) {
if (statusItem == null) {
statusItem = statusBar.newStatusItem();
statusItem.setHighlightMode(true);
statusItem.setMenu(this._nativeMenu);
}
final String text = menuItem.getTooltip();
if (tooltipText != null && tooltipText.equals(text) ||
tooltipText == null && text != null) {
return;
}
if (text == null) {
tooltipText = "";
}
else {
tooltipText = text;
}
statusItemTooltip = new NSString(tooltipText);
// don't want to matter which (setImage/setTooltip/setEnabled) is done first, and if the image/enabled is changed, we
// want to make sure keep the tooltip text the same as before.
statusItem.setImage(statusItemImage);
statusItem.setToolTip(statusItemTooltip);
}
@Override
public
void remove() {
if (statusItem != null) {
statusBar.removeStatusItem(statusItem);
}
statusItem = null;
statusItemTooltip = null;
statusItemImage = null;
// make sure this thread doesn't keep the JVM alive anymore
synchronized (keepAliveLock) {
keepAliveLock.notifyAll();
}
super.remove();
}
};
statusBar = NSStatusBar.systemStatusBar();
bind(osxMenu, null, imageResizeUtil);
}
@Override
public
boolean hasImage() {
return imageFile != null;
}
}

View File

@ -33,7 +33,7 @@ import dorkbox.systemTray.gnomeShell.LegacyExtension;
import dorkbox.systemTray.ui.awt._AwtTray;
import dorkbox.systemTray.ui.gtk._AppIndicatorNativeTray;
import dorkbox.systemTray.ui.gtk._GtkStatusIconNativeTray;
import dorkbox.systemTray.ui.osx._OsxNativeTray;
import dorkbox.systemTray.ui.osx._OsxAwtTray;
import dorkbox.systemTray.ui.swing._SwingTray;
import dorkbox.systemTray.ui.swing._WindowsNativeTray;
import dorkbox.util.FileUtil;
@ -63,7 +63,7 @@ class AutoDetectTrayType {
return _SwingTray.class;
}
else if (trayType == TrayType.Osx) {
return _OsxNativeTray.class;
return _OsxAwtTray.class;
}
else if (trayType == TrayType.Awt) {
return _AwtTray.class;
@ -86,7 +86,7 @@ class AutoDetectTrayType {
else if (trayClass == _SwingTray.class) {
return TrayType.Swing;
}
else if (trayClass == _OsxNativeTray.class) {
else if (trayClass == _OsxAwtTray.class) {
return TrayType.Osx;
}
else if (trayClass == _AwtTray.class) {
@ -103,7 +103,7 @@ class AutoDetectTrayType {
case AppIndicator: return tray == _AppIndicatorNativeTray.class;
case WindowsNative: return tray == _WindowsNativeTray.class;
case Swing: return tray == _SwingTray.class;
case Osx: return tray == _OsxNativeTray.class;
case Osx: return tray == _OsxAwtTray.class;
case Awt: return tray == _AwtTray.class;
}
@ -120,7 +120,7 @@ class AutoDetectTrayType {
return selectType(TrayType.WindowsNative);
}
else if (OS.INSTANCE.isMacOsX()) {
// macos can ONLY use the OSXStatusItem or AWT if you want it to follow the L&F of the OS. It is the default.
// macOS can ONLY use AWT if you want it to follow the L&F of the OS. It is the default.
return selectType(TrayType.Osx);
}
else if ((OS.INSTANCE.isLinux() || OS.INSTANCE.isUnix())) {

View File

@ -0,0 +1,18 @@
package dorkbox.systemTray.util;
import java.awt.Image;
public
class AwtAccessor {
public static Object getPeer(java.awt.MenuComponent nativeComp) {
return null;
}
public static void setImage(final Object peerObj, final Image img) {
}
public static void setToolTipText(final Object peerObj, final String text) {
}
}

View File

@ -17,12 +17,12 @@ package dorkbox.systemTray.util;
import static dorkbox.systemTray.SystemTray.logger;
import java.awt.AWTException;
import java.util.Locale;
import dorkbox.jna.JnaClassUtils;
import dorkbox.jna.ClassUtils;
import dorkbox.os.OS;
import dorkbox.systemTray.SystemTray;
import dorkbox.util.Sys;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
@ -67,6 +67,7 @@ import javassist.bytecode.Opcode;
/**
* Fixes issues with some java runtimes
*/
@SuppressWarnings("JavadocLinkAsPlainText")
public
class SystemTrayFixes {
private static
@ -84,13 +85,9 @@ class SystemTrayFixes {
}
try {
// this is important to use reflection, because if JavaFX is not being used, calling getToolkit() will initialize it...
java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class);
m.setAccessible(true);
ClassLoader cl = ClassLoader.getSystemClassLoader();
// if we are using swing the classes are already created and we cannot fix that if it's already loaded.
return (null != m.invoke(cl, className)) || (null != m.invoke(cl, "java.awt.SystemTray"));
// if we are using swing, the classes are already created and we cannot fix that if it's already loaded.
return ClassUtils.isClassLoaded(cl, className) || ClassUtils.isClassLoaded(cl, "java.awt.SystemTray");
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error detecting if the Swing SystemTray is loaded, unexpected error.", e);
@ -113,9 +110,9 @@ class SystemTrayFixes {
/**
* NOTE: Only for SWING
*
* <p>
* oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that!
*
* <p>
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/windows/native/sun/windows/awt_TrayIcon.cpp
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/windows/classes/sun/awt/windows/WTrayIconPeer.java
*/
@ -241,8 +238,8 @@ class SystemTrayFixes {
}
// whoosh, past the classloader and directly into memory.
JnaClassUtils.defineClass(trayBytes);
JnaClassUtils.defineClass(trayIconBytes);
ClassUtils.defineClass(trayBytes);
ClassUtils.defineClass(trayIconBytes);
if (SystemTray.DEBUG) {
logger.debug("Successfully changed tray icon size to: {}", trayIconSize);
@ -254,44 +251,108 @@ class SystemTrayFixes {
/**
* NOTE: Only for SWING + AWT tray types
*
* MacOS AWT is hardcoded to respond only to left-click for menus, where it should be any mouse button
*
* <p>
* MacOS AWT is hardcoded to respond only to left-click for menus, where it should be ANY mouse button
* <p>
* https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788
* https://bugs.openjdk.java.net/browse/JDK-7158615
*
* <p>
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/macosx/classes/sun/lwawt/macosx/CTrayIcon.java
* <p>
* The previous, native access we used to create menus NO LONGER works on any OS beyond Big Sur (macos 11), and now the *best* way
* to access this (since I do not want to rewrite a LOT of code), is to use AWT hacks to access images + tooltips via reflection. This
* has been possible since jdk8. While I don't like reflection, it is sadly the only way to do this.
* <p>
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/7fcf35286d52/src/macosx/classes/sun/lwawt/macosx/CMenuItem.java
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/7fcf35286d52/src/macosx/native/sun/awt/CTrayIcon.m
*/
public static
void fixMacOS() {
if (!isOracleVM()) {
// not fixing things that are not broken.
return;
}
// ONLY java <= 8
if (OS.INSTANCE.getJavaVersion() > 8) {
// there are problems with java 9+
return;
}
if (isSwingTrayLoaded()) {
// we have to throw a significant error.
throw new RuntimeException("Unable to initialize the AWT System Tray, it has already been created!");
}
ClassPool pool = ClassPool.getDefault();
try {
java.awt.Robot robot = new java.awt.Robot();
robot.mousePress(java.awt.event.InputEvent.BUTTON1_DOWN_MASK);
} catch (AWTException e) {
e.printStackTrace();
// allow non-reflection access to sun.awt.AWTAccessor...getPeer()
{
CtClass dynamicClass = pool.makeClass("java.awt.MenuComponentAccessory");
CtMethod method = CtNewMethod.make(
"public static Object getPeer(java.awt.MenuComponent nativeComp) { " +
// "java.lang.System.err.println(\"Getting peer!\" + sun.awt.AWTAccessor.getMenuComponentAccessor().getPeer(nativeComp));" +
"return sun.awt.AWTAccessor.getMenuComponentAccessor().getPeer(nativeComp);" +
"}", dynamicClass);
dynamicClass.addMethod(method);
// CMenuItem can only PROPERLY be accessed from the java.awt package. Other locations might work within the JVM, but not
// from a library
method = CtNewMethod.make(
"public static void setImage(Object peerObj, java.awt.Image img) { " +
"((sun.lwawt.macosx.CMenuItem)peerObj).setImage(img);" +
"}", dynamicClass);
dynamicClass.addMethod(method);
method = CtNewMethod.make(
"public static void setToolTipText(Object peerObj, String text) { " +
"((sun.lwawt.macosx.CMenuItem)peerObj).setToolTipText(text);" +
"}", dynamicClass);
dynamicClass.addMethod(method);
dynamicClass.setModifiers(dynamicClass.getModifiers() & ~Modifier.STATIC);
final byte[] dynamicClassBytes = dynamicClass.toBytecode();
ClassUtils.defineClass(null, dynamicClassBytes);
}
{
CtClass classFixer = pool.get("dorkbox.systemTray.util.AwtAccessor");
CtMethod ctMethod = classFixer.getDeclaredMethod("getPeer");
ctMethod.setBody("{" +
"return java.awt.MenuComponentAccessory.getPeer($1);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
ctMethod = classFixer.getDeclaredMethod("setImage");
ctMethod.setBody("{" +
"java.awt.MenuComponentAccessory.setImage($1, $2);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
ctMethod = classFixer.getDeclaredMethod("setToolTipText");
ctMethod.setBody("{" +
"java.awt.MenuComponentAccessory.setToolTipText($1, $2);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
final byte[] classFixerBytes = classFixer.toBytecode();
ClassUtils.defineClass(ClassLoader.getSystemClassLoader(), classFixerBytes);
}
if (SystemTray.DEBUG) {
logger.debug("Successfully added images/tooltips to macOS AWT tray menus");
}
} catch (Exception e) {
logger.error("Error adding SystemTray images/tooltips for macOS AWT tray menus.", e);
}
ClassPool pool = ClassPool.getDefault();
byte[] mouseEventBytes;
int mouseDelay = 75;
try {
// must call this otherwise the robot call later on will crash.
new java.awt.Robot();
byte[] mouseEventBytes;
CtClass trayClass = pool.get("sun.lwawt.macosx.CTrayIcon");
// now have to make a new "system tray" (that is null) in order to init/load this class completely
// have to modify the SystemTray.getIconSize as well.
@ -301,19 +362,28 @@ class SystemTrayFixes {
CtField ctField = new CtField(CtClass.intType, "lastButton", trayClass);
trayClass.addField(ctField);
ctField = new CtField(CtClass.intType, "lastX", trayClass);
trayClass.addField(ctField);
ctField = new CtField(CtClass.intType, "lastY", trayClass);
trayClass.addField(ctField);
ctField = new CtField(pool.get("java.awt.Robot"), "robot", trayClass);
trayClass.addField(ctField);
CtMethod ctMethodGet = trayClass.getDeclaredMethod("handleMouseEvent");
String nsEventFQND = "sun.lwawt.macosx.NSEvent";
String nsEventFQND;
String mouseModInfo;
String mousePressEventInfo;
String mouseReleaseEventInfo;
if (OS.INSTANCE.getJavaVersion() <= 8) {
nsEventFQND = "sun.lwawt.macosx.event.NSEvent";
mouseModInfo = "int mouseMods = " + nsEventFQND + ".nsToJavaMouseModifiers(button, event.getModifierFlags());";
mousePressEventInfo = "java.awt.event.MouseEvent mEvent = new java.awt.event.MouseEvent(this.dummyFrame, eventType, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
mouseReleaseEventInfo = "java.awt.event.MouseEvent event7 = new java.awt.event.MouseEvent(this.dummyFrame, 500, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
}
else {
nsEventFQND = "sun.lwawt.macosx.NSEvent";
mouseModInfo = "int mouseMods = " + nsEventFQND + ".nsToJavaModifiers(event.getModifierFlags());";
mousePressEventInfo = "java.awt.event.MouseEvent mEvent = new java.awt.event.MouseEvent(this.dummyFrame, eventType, event0, mouseMods, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
mouseReleaseEventInfo = "java.awt.event.MouseEvent event7 = new java.awt.event.MouseEvent(this.dummyFrame, 500, event0, mouseMods, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
}
ctMethodGet.setBody("{" +
nsEventFQND + " event = $1;" +
@ -324,16 +394,17 @@ class SystemTrayFixes {
"int mouseY = event.getAbsY();" +
// have to intercept to see if it was a button click redirect to preserve what button was used in the event
"if (lastButton == 1 && mouseX == lastX && mouseY == lastY) {" +
// "java.lang.System.err.println(\"Redefining button press to 1\");" +
"if (button > 0 && lastButton == 1) {" +
"int eventType = " + nsEventFQND + ".nsToJavaEventType(event.getType());" +
"if (eventType == 501) {" +
// "java.lang.System.err.println(\"Redefining button press to 1: \" + eventType);" +
"button = 1;" +
"lastButton = -1;" +
"lastX = 0;" +
"lastY = 0;" +
"button = 1;" +
"lastButton = -1;" +
"}" +
"}" +
"if ((button <= 2 || toolKit.areExtraMouseButtonsEnabled()) && button <= toolKit.getNumberOfButtons() - 1) {" +
"if (button > 0 && (button <= 2 || toolKit.areExtraMouseButtonsEnabled()) && button <= toolKit.getNumberOfButtons() - 1) {" +
"int eventType = " + nsEventFQND + ".nsToJavaEventType(event.getType());" +
"int jButton = 0;" +
"int jClickCount = 0;" +
@ -345,14 +416,17 @@ class SystemTrayFixes {
// "java.lang.System.err.println(\"Click \" + jButton + \" event: \" + eventType);" +
"int mouseMods = " + nsEventFQND + ".nsToJavaMouseModifiers(button, event.getModifierFlags());" +
//"int mouseMods = " + nsEventFQND + ".nsToJavaMouseModifiers(button, event.getModifierFlags());" +
mouseModInfo +
// surprisingly, this is false when the popup is showing
"boolean popupTrigger = " + nsEventFQND + ".isPopupTrigger(mouseMods);" +
"int mouseMask = jButton > 0 ? java.awt.event.MouseEvent.getMaskForButton(jButton) : 0;" +
"long event0 = System.currentTimeMillis();" +
"if(eventType == 501) {" +
"if (eventType == 501) {" +
"mouseClickButtons |= mouseMask;" +
"} else if(eventType == 506) {" +
"mouseClickButtons = 0;" +
@ -360,36 +434,37 @@ class SystemTrayFixes {
// have to swallow + re-dispatch events in specific cases. (right click)
"if (eventType == 501 && popupTrigger && button == 1) {" +
"if (eventType == 501 && popupTrigger && button != 0) {" +
// "java.lang.System.err.println(\"Redispatching mouse press. Has popupTrigger \" + " + "popupTrigger + \" event: \" + " + "eventType);" +
// we use Robot to left click where we right clicked, in order to "fool" the native part to show the popup
// For what it's worth, this is the only way to get the native bits to behave.
// For what it's worth, this is the only way to get the native bits to behave (since we cannot access the native parts).
"if (robot == null) {" +
"try {" +
"robot = new java.awt.Robot();" +
"robot.setAutoDelay(40);" +
"robot.setAutoWaitForIdle(true);" +
"} catch (java.awt.AWTException e) {" +
// the delay is necessary for this to work correctly.
"robot.setAutoDelay(10);" +
"robot.setAutoWaitForIdle(false);" +
"} " +
"catch (java.awt.AWTException e) {" +
"e.printStackTrace();" +
"}" +
"}" +
"lastButton = 1;" +
"lastX = mouseX;" +
"lastY = mouseY;" +
// the delay is necessary for this to work correctly. Mouse release is not necessary.
// Mouse release is not necessary.
// this simulates *just enough* of the default behavior so that right click behaves the same as left click.
"int maskButton1 = java.awt.event.InputEvent.getMaskForButton(java.awt.event.MouseEvent.BUTTON1);" +
"robot.mouseMove(mouseX, mouseY);" +
"robot.mousePress(maskButton1);" +
"robot.delay(" + mouseDelay + ");" +
"return;" +
"}" +
"}" +
"java.awt.event.MouseEvent mEvent = new java.awt.event.MouseEvent(this.dummyFrame, eventType, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);" +
//"java.awt.event.MouseEvent mEvent = new java.awt.event.MouseEvent(this.dummyFrame, eventType, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);" +
mousePressEventInfo +
"mEvent.setSource(this.target);" +
"this.postEvent(mEvent);" +
@ -406,7 +481,8 @@ class SystemTrayFixes {
// mouse release
"if (eventType == 502) {" +
"if ((mouseClickButtons & mouseMask) != 0) {" +
"java.awt.event.MouseEvent event7 = new java.awt.event.MouseEvent(this.dummyFrame, 500, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);" +
// "java.awt.event.MouseEvent event7 = new java.awt.event.MouseEvent(this.dummyFrame, 500, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);" +
mouseReleaseEventInfo +
"event7.setSource(this.target);" +
"this.postEvent(event7);" +
@ -418,42 +494,41 @@ class SystemTrayFixes {
"}");
// perform pre-verification for the modified method
ctMethodGet.getMethodInfo().rebuildStackMapForME(trayClass.getClassPool());
ctMethodGet.getMethodInfo().rebuildStackMapForME(pool);
mouseEventBytes = trayClass.toBytecode();
// whoosh, past the classloader and directly into memory.
JnaClassUtils.defineClass(mouseEventBytes);
ClassUtils.defineClass(null, mouseEventBytes);
if (SystemTray.DEBUG) {
logger.debug("Successfully changed mouse trigger for MacOSX");
logger.debug("Successfully changed mouse trigger for macOS AWT tray menus");
}
} catch (Exception e) {
logger.error("Error changing SystemTray mouse trigger for MacOSX.", e);
logger.error("Error changing SystemTray mouse trigger for macOS AWT tray menus.", e);
}
}
/**
* NOTE: ONLY IS FOR SWING TRAY TYPES!
*
* <p>
* Linux/Unix/Solaris use X11 + AWT to add an AWT window to a spot in the notification panel. UNFORTUNATELY, AWT
* components are heavyweight, and DO NOT support transparency -- so one gets a "grey" box as the background of the icon.
*
* <p>
* Spectacularly enough, because this uses X11, it works on any X backend -- regardless of GtkStatusIcon or AppIndicator support. This
* actually provides **more** support than GtkStatusIcons or AppIndicators, since this will ALWAYS work.
*
* <p>
* Additionally, the size of the tray is hard-coded to be 24.
*
* <p>
*
* The down side, is that there is a "grey" box -- so hack around this issue by getting the color of a pixel in the notification area 1
* off the corner, and setting that as the background.
*
* <p>
* It would be better to take a screenshot of the space BEHIND the tray icon, but we can't do that because there is no way to get
* the info BEFORE the AWT is added to the notification area. See comments below for more details.
*
* <p>
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936
*
* <p>
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/solaris/classes/sun/awt/X11/XTrayIconPeer.java
* http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/solaris/classes/sun/awt/X11/XSystemTrayPeer.java
*/
@ -710,11 +785,11 @@ class SystemTrayFixes {
}
// whoosh, past the classloader and directly into memory.
JnaClassUtils.defineClass(runnableBytes);
JnaClassUtils.defineClass(eFrameBytes);
JnaClassUtils.defineClass(iconCanvasBytes);
JnaClassUtils.defineClass(trayIconBytes);
JnaClassUtils.defineClass(trayPeerBytes);
ClassUtils.defineClass(runnableBytes);
ClassUtils.defineClass(eFrameBytes);
ClassUtils.defineClass(iconCanvasBytes);
ClassUtils.defineClass(trayIconBytes);
ClassUtils.defineClass(trayPeerBytes);
if (SystemTray.DEBUG) {
logger.debug("Successfully changed tray icon background color");

View File

@ -3,6 +3,7 @@ module dorkbox.systemtray {
exports dorkbox.systemTray.peer;
exports dorkbox.systemTray.util;
requires transitive dorkbox.collections;
requires transitive dorkbox.executor;
requires transitive dorkbox.updates;
requires transitive dorkbox.utilities;
@ -13,17 +14,9 @@ module dorkbox.systemtray {
requires transitive com.sun.jna;
requires transitive com.sun.jna.platform;
// when running javaFX
// requires static javafx.graphics;
// when running SWT
// 32-bit support was dropped by eclipse since 4.10 (3.108.0 is the oldest that is 32 bit)
// requires static org.eclipse.swt.gtk.linux.x86_64;
// requires static org.eclipse.swt.win32.win32.x86_64;
// requires static org.eclipse.swt.cocoa.macosx.x86_64;
requires transitive java.desktop;
requires kotlin.stdlib;
requires java.base;
requires org.javassist;
}

View File

@ -63,7 +63,9 @@ class TestTray {
public
TestTray() {
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
// SystemTray.FORCE_TRAY_TYPE = SystemTray.TrayType.Swing;
// SystemTray.FORCE_TRAY_TYPE = SystemTray.TrayType.Swing;
// SystemTray.FORCE_TRAY_TYPE = SystemTray.TrayType.Awt;
// SystemTray.FORCE_TRAY_TYPE = SystemTray.TrayType.Osx;
// for test apps, make sure the cache is always reset. These are the ones used, and you should never do this in production.
CacheUtil.clear("SysTrayExample");