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:
parent
6c0884b4e5
commit
724019615a
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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())) {
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue