diff --git a/ant/unix/unix-launcher.sh.in b/ant/unix/unix-launcher.sh.in index 772907293..0ae629d77 100644 --- a/ant/unix/unix-launcher.sh.in +++ b/ant/unix/unix-launcher.sh.in @@ -96,7 +96,7 @@ if command -v java &>/dev/null; then else prefix="../../" # back two directories, e.g. postinstall fi - java $LAUNCH_OPTS -Xdock:name="$ABOUT_TITLE" -Xdock:icon="$ICON_PATH" -jar -Dapple.awt.UIElement="true" "${prefix}$PROPS_FILE.jar" -NSRequiresAquaSystemAppearance False "$@" + java $LAUNCH_OPTS -Xdock:name="$ABOUT_TITLE" -Xdock:icon="$ICON_PATH" -jar -Dapple.awt.UIElement="true" -Dapple.awt.enableTemplateImages="true" "${prefix}$PROPS_FILE.jar" -NSRequiresAquaSystemAppearance False "$@" else java $LAUNCH_OPTS -jar "$PROPS_FILE.jar" "$@" fi diff --git a/src/org/dyorgio/jna/platform/mac/ActionCallback.java b/src/org/dyorgio/jna/platform/mac/ActionCallback.java new file mode 100644 index 000000000..5a3a5ec39 --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/ActionCallback.java @@ -0,0 +1,96 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.Callback; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import java.util.HashMap; + +/** + * + * @author dyorgio + */ +@SuppressWarnings("Convert2Lambda") +public final class ActionCallback extends NSObject { + + private static final NativeLong actionCallbackClass = Foundation.INSTANCE.objc_allocateClassPair(objectClass, ActionCallback.class.getSimpleName(), 0); + private static final Pointer actionCallbackSel = Foundation.INSTANCE.sel_registerName("actionCallback"); + private static final Pointer setTargetSel = Foundation.INSTANCE.sel_registerName("setTarget:"); + private static final Pointer setActionSel = Foundation.INSTANCE.sel_registerName("setAction:"); + private static final Callback registerActionCallback; + + static { + startNativeAppMainThread(); + registerActionCallback = new Callback() { + @SuppressWarnings("unused") + public void callback(Pointer self, Pointer selector) { + if (selector.equals(actionCallbackSel)) { + ActionCallback action; + + synchronized (callbackMap) { + action = callbackMap.get(Pointer.nativeValue(self)); + } + + if (action != null) { + action.runnable.run(); + } + } + } + }; + + if (!Foundation.INSTANCE.class_addMethod(actionCallbackClass, + actionCallbackSel, registerActionCallback, "v@:")) { + throw new RuntimeException("Error initializing ActionCallback as a objective C class"); + } + + Foundation.INSTANCE.objc_registerClassPair(actionCallbackClass); + } + + private static final HashMap callbackMap = new HashMap(); + + private final Runnable runnable; + + @SuppressWarnings("LeakingThisInConstructor") + public ActionCallback(Runnable callable) { + super(Foundation.INSTANCE.class_createInstance(actionCallbackClass, 0)); + this.runnable = callable; + synchronized (callbackMap) { + callbackMap.put(getId().longValue(), this); + } + } + + @Override + public void release() { + synchronized (callbackMap) { + callbackMap.remove(getId().longValue()); + } + super.release(); + } + + public void installActionOnNSControl(NativeLong nsControl) { + Foundation.INSTANCE.objc_msgSend(nsControl, setTargetSel, id); + Foundation.INSTANCE.objc_msgSend(nsControl, setActionSel, actionCallbackSel); + } +} diff --git a/src/org/dyorgio/jna/platform/mac/Foundation.java b/src/org/dyorgio/jna/platform/mac/Foundation.java new file mode 100644 index 000000000..e7f3c5e66 --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/Foundation.java @@ -0,0 +1,57 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +/** + * + * @author dyorgio + */ +public interface Foundation extends Library { + + public static final Foundation INSTANCE = Native.load("Foundation", Foundation.class); + + NativeLong class_getInstanceVariable(NativeLong classPointer, String name); + + NativeLong object_getIvar(NativeLong target, NativeLong ivar); + + NativeLong objc_getClass(String className); + + NativeLong objc_allocateClassPair(NativeLong superClass, String name, long extraBytes); + + void objc_registerClassPair(NativeLong clazz); + + NativeLong class_createInstance(NativeLong clazz, int extraBytes); + + boolean class_addMethod(NativeLong clazz, Pointer selector, Callback callback, String types); + + NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Object... args); + + Pointer sel_registerName(String selectorName); +} diff --git a/src/org/dyorgio/jna/platform/mac/FoundationUtil.java b/src/org/dyorgio/jna/platform/mac/FoundationUtil.java new file mode 100644 index 000000000..b97aceccd --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/FoundationUtil.java @@ -0,0 +1,88 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * + * @author dyorgio + */ +public final class FoundationUtil { + + private static final Foundation FOUNDATION = Foundation.INSTANCE; + + public static final NativeLong NULL = new NativeLong(0l); + + private FoundationUtil() { + } + + public static boolean isNull(NativeLong id) { + return NULL.equals(id); + } + + public static boolean isNull(NSObject object) { + return NULL.equals(object.id); + } + + public static boolean isFalse(NativeLong id) { + return NULL.equals(id); + } + + public static boolean isTrue(NativeLong id) { + return !NULL.equals(id); + } + + public static NativeLong invoke(NativeLong id, String selector, Object... args) { + return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector), args); + } + + public static NativeLong invoke(NativeLong id, Pointer selectorPointer, Object... args) { + return FOUNDATION.objc_msgSend(id, selectorPointer, args); + } + + public static void runOnMainThreadAndWait(Runnable runnable) throws InterruptedException, ExecutionException { + runOnMainThread(runnable, true); + } + + public static FutureTask runOnMainThread(Runnable runnable, boolean waitUntilDone) { + FutureTask futureTask = new FutureTask(runnable, null); + FutureTaskCallback.performOnMainThread(futureTask, waitUntilDone); + return futureTask; + } + + public static T callOnMainThreadAndWait(Callable callable) throws InterruptedException, ExecutionException { + return callOnMainThread(callable, true).get(); + } + + public static FutureTask callOnMainThread(Callable callable, boolean waitUntilDone) { + FutureTask futureTask = new FutureTask(callable); + FutureTaskCallback.performOnMainThread(futureTask, waitUntilDone); + return futureTask; + } +} diff --git a/src/org/dyorgio/jna/platform/mac/FutureTaskCallback.java b/src/org/dyorgio/jna/platform/mac/FutureTaskCallback.java new file mode 100644 index 000000000..bb2f08fef --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/FutureTaskCallback.java @@ -0,0 +1,94 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.Callback; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import java.util.HashMap; +import java.util.concurrent.FutureTask; + +/** + * + * @author dyorgio + */ +@SuppressWarnings("Convert2Lambda") +class FutureTaskCallback extends NSObject { + + private static final NativeLong futureTaskCallbackClass = Foundation.INSTANCE.objc_allocateClassPair(objectClass, FutureTaskCallback.class.getSimpleName(), 0); + private static final Pointer futureTaskCallbackSel = Foundation.INSTANCE.sel_registerName("futureTaskCallback"); + private static final Callback registerFutureTaskCallback; + + static { + startNativeAppMainThread(); + registerFutureTaskCallback = new Callback() { + @SuppressWarnings("unused") + public void callback(Pointer self, Pointer selector) { + if (selector.equals(futureTaskCallbackSel)) { + FutureTaskCallback action; + + synchronized (callbackMap) { + action = callbackMap.remove(Pointer.nativeValue(self)); + } + + if (action != null) { + action.callable.run(); + } + } + } + }; + + if (!Foundation.INSTANCE.class_addMethod(futureTaskCallbackClass, + futureTaskCallbackSel, registerFutureTaskCallback, "v@:")) { + throw new RuntimeException("Error initializing FutureTaskCallback as a objective C class"); + } + + Foundation.INSTANCE.objc_registerClassPair(futureTaskCallbackClass); + } + + private static final HashMap callbackMap = new HashMap(); + + private final FutureTask callable; + + @SuppressWarnings("LeakingThisInConstructor") + private FutureTaskCallback(FutureTask callable) { + super(Foundation.INSTANCE.class_createInstance(futureTaskCallbackClass, 0)); + this.callable = callable; + synchronized (callbackMap) { + callbackMap.put(getId().longValue(), this); + } + } + + @Override + public void release() { + synchronized (callbackMap) { + callbackMap.remove(getId().longValue()); + } + super.release(); + } + + static void performOnMainThread(FutureTask futureTask, boolean waitUntilDone) { + new FutureTaskCallback(futureTask).performSelectorOnMainThread(futureTaskCallbackSel, null, waitUntilDone); + } +} diff --git a/src/org/dyorgio/jna/platform/mac/NSDictionary.java b/src/org/dyorgio/jna/platform/mac/NSDictionary.java new file mode 100644 index 000000000..87f2b0244 --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/NSDictionary.java @@ -0,0 +1,50 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +/** + * + * @author dyorgio + */ +public class NSDictionary extends NSObject { + + private static final NativeLong dictionaryClass = Foundation.INSTANCE.objc_getClass("NSDictionary"); + private static final Pointer dictionaryWithContentsOfFileSel = Foundation.INSTANCE.sel_registerName("dictionaryWithContentsOfFile:"); + private static final Pointer objectForKeySel = Foundation.INSTANCE.sel_registerName("objectForKey:"); + + public NSDictionary(NativeLong id) { + super(id); + } + + public static NSDictionary dictionaryWithContentsOfFile(NSString file) { + return new NSDictionary(Foundation.INSTANCE.objc_msgSend(dictionaryClass, dictionaryWithContentsOfFileSel, file.id)); + } + + public NSObject objectForKey(NSObject key) { + return new NSString(FoundationUtil.invoke(id, objectForKeySel, key.id)); + } +} diff --git a/src/org/dyorgio/jna/platform/mac/NSObject.java b/src/org/dyorgio/jna/platform/mac/NSObject.java new file mode 100644 index 000000000..ed8c51318 --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/NSObject.java @@ -0,0 +1,96 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +import java.awt.*; +import java.lang.reflect.InvocationTargetException; +import javax.swing.SwingUtilities; + +/** + * + * @author dyorgio + */ +public class NSObject { + + static final NativeLong objectClass = Foundation.INSTANCE.objc_getClass("NSObject"); + protected static final Pointer allocSel = Foundation.INSTANCE.sel_registerName("alloc"); + protected static final Pointer initSel = Foundation.INSTANCE.sel_registerName("init"); + protected static final Pointer releaseSel = Foundation.INSTANCE.sel_registerName("release"); + protected static final Pointer performSelectorOnMainThread$withObject$waitUntilDoneSel + = Foundation.INSTANCE.sel_registerName("performSelectorOnMainThread:withObject:waitUntilDone:"); + + final NativeLong id; + + public NSObject(NativeLong id) { + this.id = id; + } + + public final NativeLong getId() { + return id; + } + + public void release() { + Foundation.INSTANCE.objc_msgSend(id, releaseSel); + } + + @Override + @SuppressWarnings("FinalizeDeclaration") + protected void finalize() throws Throwable { + release(); + super.finalize(); + } + + public void performSelectorOnMainThread(Pointer selector, NativeLong object, boolean waitUntilDone) { + Foundation.INSTANCE.objc_msgSend(id, // + NSObject.performSelectorOnMainThread$withObject$waitUntilDoneSel, // + selector, object, waitUntilDone); + } + + static volatile boolean initialized = false; + + static void startNativeAppMainThread() { + if (!initialized) { + synchronized (NSObject.objectClass) { + if (!initialized) { + try { + if(EventQueue.isDispatchThread()) { + Toolkit.getDefaultToolkit(); + } else { + SwingUtilities.invokeAndWait(() -> Toolkit.getDefaultToolkit()); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } catch (InvocationTargetException ex) { + // ignore + } finally { + initialized = true; + } + } + } + } + } +} diff --git a/src/org/dyorgio/jna/platform/mac/NSString.java b/src/org/dyorgio/jna/platform/mac/NSString.java new file mode 100644 index 000000000..969661589 --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/NSString.java @@ -0,0 +1,74 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import com.sun.jna.platform.mac.CoreFoundation; +import java.nio.charset.Charset; + +/** + * + * @author dyorgio + */ +public class NSString extends NSObject { + + public static final Charset UTF_16LE_CHARSET = Charset.forName("UTF-16LE"); + + private static final NativeLong stringCls = Foundation.INSTANCE.objc_getClass("NSString"); + private static final Pointer stringSel = Foundation.INSTANCE.sel_registerName("string"); + private static final Pointer initWithBytesLengthEncodingSel = Foundation.INSTANCE.sel_registerName("initWithBytes:length:encoding:"); + private static final long NSUTF16LittleEndianStringEncoding = 0x94000100; + + public NSString(String string) { + this(fromJavaString(string)); + } + + public NSString(NativeLong id) { + super(id); + } + + @Override + public String toString() { + if (FoundationUtil.isNull(this)) { + return null; + } + CoreFoundation.CFStringRef cfString = new CoreFoundation.CFStringRef(new Pointer(id.longValue())); + try { + return CoreFoundation.INSTANCE.CFStringGetLength(cfString).intValue() > 0 ? cfString.stringValue() : ""; + } finally { + cfString.release(); + } + } + + private static NativeLong fromJavaString(String s) { + if (s.isEmpty()) { + return Foundation.INSTANCE.objc_msgSend(stringCls, stringSel); + } + + byte[] utf16Bytes = s.getBytes(UTF_16LE_CHARSET); + return Foundation.INSTANCE.objc_msgSend(Foundation.INSTANCE.objc_msgSend(stringCls, allocSel), + initWithBytesLengthEncodingSel, utf16Bytes, utf16Bytes.length, NSUTF16LittleEndianStringEncoding); + } +} diff --git a/src/org/dyorgio/jna/platform/mac/NSUserDefaults.java b/src/org/dyorgio/jna/platform/mac/NSUserDefaults.java new file mode 100644 index 000000000..0cdbea117 --- /dev/null +++ b/src/org/dyorgio/jna/platform/mac/NSUserDefaults.java @@ -0,0 +1,50 @@ +/* + * The MIT License + * + * Copyright 2020 dyorgio. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.dyorgio.jna.platform.mac; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +/** + * + * @author dyorgio + */ +public class NSUserDefaults extends NSObject { + + private static final NativeLong userDefaultsClass = Foundation.INSTANCE.objc_getClass("NSUserDefaults"); + private static final Pointer standardUserDefaultsdSel = Foundation.INSTANCE.sel_registerName("standardUserDefaults"); + private static final Pointer stringForKeySel = Foundation.INSTANCE.sel_registerName("stringForKey:"); + + public NSUserDefaults(NativeLong id) { + super(id); + } + + public static NSUserDefaults standard() { + return new NSUserDefaults(Foundation.INSTANCE.objc_msgSend(userDefaultsClass, standardUserDefaultsdSel)); + } + + public NSString stringForKey(NSString key) { + return new NSString(FoundationUtil.invoke(id, stringForKeySel, key.id)); + } +} diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index 3ce682c42..5ed23bb2a 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -163,7 +163,7 @@ public TrayManager(boolean isHeadless) { darkDesktopMode = SystemUtilities.isDarkDesktop(); darkTaskbarMode = SystemUtilities.isDarkTaskbar(); iconCache.fixTrayIcons(darkTaskbarMode); - refreshIcon(); + refreshIcon(null); SwingUtilities.invokeLater(() -> { SystemUtilities.setSystemLookAndFeel(); for(Component c : componentList) { @@ -552,7 +552,12 @@ public void displayInfoMessage(String text) { * Thread safe method for setting the default icon */ public void setDefaultIcon() { - setIcon(IconCache.Icon.DEFAULT_ICON); + // Workaround for JDK-8252015 + if(SystemUtilities.isMac() && Constants.MASK_TRAY_SUPPORTED && !MacUtilities.jdkSupportsTemplateIcon()) { + setIcon(IconCache.Icon.DEFAULT_ICON, () -> MacUtilities.toggleTemplateIcon(tray.tray())); + } else { + setIcon(IconCache.Icon.DEFAULT_ICON); + } } /** Thread safe method for setting the error status message */ @@ -576,15 +581,24 @@ public void setWarningIcon() { } /** Thread safe method for setting the specified icon */ - private void setIcon(final IconCache.Icon i) { + private void setIcon(final IconCache.Icon i, Runnable whenDone) { if (tray != null && i != shownIcon) { shownIcon = i; - refreshIcon(); + refreshIcon(whenDone); } } - public void refreshIcon() { - SwingUtilities.invokeLater(() -> tray.setImage(iconCache.getImage(shownIcon, tray.getSize()))); + private void setIcon(final IconCache.Icon i) { + setIcon(i, null); + } + + public void refreshIcon(final Runnable whenDone) { + SwingUtilities.invokeLater(() -> { + tray.setImage(iconCache.getImage(shownIcon, tray.getSize())); + if(whenDone != null) { + whenDone.run(); + } + }); } /** diff --git a/src/qz/ui/component/IconCache.java b/src/qz/ui/component/IconCache.java index 795442626..9bce86016 100644 --- a/src/qz/ui/component/IconCache.java +++ b/src/qz/ui/component/IconCache.java @@ -283,8 +283,8 @@ public void fixTrayIcons(boolean darkTaskbar) { for(IconCache.Icon i : IconCache.getTypes()) { // See also JXTrayIcon.getSize() if (i.isTrayIcon() && SystemUtilities.isMac()) { - // Prevent padding from happening twice on WARNING_ICON, DANGER_ICON - if (!i.padded || i == Icon.DEFAULT_ICON) { + // Prevent padding from happening twice + if (!i.padded) { padIcon(i, 25); } } diff --git a/src/qz/ui/tray/ClassicTrayIcon.java b/src/qz/ui/tray/ClassicTrayIcon.java index b066f11e6..7690f46a6 100644 --- a/src/qz/ui/tray/ClassicTrayIcon.java +++ b/src/qz/ui/tray/ClassicTrayIcon.java @@ -30,6 +30,7 @@ public void setJPopupMenu(JPopupMenu source) { final PopupMenu popup = new PopupMenu(); setPopupMenu(popup); wrapAll(popup, source.getComponents()); + popup.addNotify(); } /** diff --git a/src/qz/utils/MacUtilities.java b/src/qz/utils/MacUtilities.java index 675c271ab..4513ceed2 100644 --- a/src/qz/utils/MacUtilities.java +++ b/src/qz/utils/MacUtilities.java @@ -14,13 +14,22 @@ import com.github.zafarkhaja.semver.Version; import com.sun.jna.Library; import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import org.dyorgio.jna.platform.mac.ActionCallback; +import org.dyorgio.jna.platform.mac.Foundation; +import org.dyorgio.jna.platform.mac.FoundationUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qz.common.Constants; import qz.common.TrayManager; import qz.ui.component.IconCache; +import javax.swing.*; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -36,6 +45,9 @@ public class MacUtilities { private static Dialog aboutDialog; private static TrayManager trayManager; private static String bundleId; + private static Integer pid; + private static Boolean jdkSupportsTemplateIcon; + private static boolean templateIconForced = false; public static void showAboutDialog() { if (aboutDialog != null) { aboutDialog.setVisible(true); } @@ -107,7 +119,7 @@ public static void registerQuitHandler(TrayManager trayManager) { * Runs a shell command to determine if "Dark" desktop theme is enabled * @return true if enabled, false if not */ - public static boolean isDarkMode() { + public static boolean isDarkDesktop() { return !ShellUtilities.execute(new String[] { "defaults", "read", "-g", "AppleInterfaceStyle" }, new String[] { "Dark" }, true, true).isEmpty(); } @@ -134,12 +146,16 @@ public static int getScaleFactor() { } public static int getProcessID() { - try { - return CLibrary.INSTANCE.getpid(); - } catch(UnsatisfiedLinkError | NoClassDefFoundError e) { - log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1."); + if(pid == null) { + try { + pid = CLibrary.INSTANCE.getpid(); + } + catch(UnsatisfiedLinkError | NoClassDefFoundError e) { + log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1."); + pid = -1; + } } - return -1; + return pid; } private interface CLibrary extends Library { @@ -147,4 +163,90 @@ private interface CLibrary extends Library { int getpid (); } + /** + * Checks for presence of JDK-8252015 using reflection + */ + public static boolean jdkSupportsTemplateIcon() { + if(jdkSupportsTemplateIcon == null) { + try { + // before JDK-8252015: setNativeImage(long, long, boolean) + // after JDK-8252015: setNativeImage(long, long, boolean, boolean) + Class.forName("sun.lwawt.macosx.CTrayIcon").getDeclaredMethod("setNativeImage", long.class, long.class, boolean.class, boolean.class); + jdkSupportsTemplateIcon = true; + } + catch(ClassNotFoundException | NoSuchMethodException ignore) { + jdkSupportsTemplateIcon = false; + } + } + return jdkSupportsTemplateIcon; + } + + public static void toggleTemplateIcon(TrayIcon icon) { + // Check if icon has a menu + if (icon.getPopupMenu() == null) { + throw new IllegalStateException("PopupMenu needs to be set on TrayIcon first"); + } + // Check if icon is on SystemTray + if (icon.getImage() == null) { + throw new IllegalStateException("TrayIcon needs to be added on SystemTray first"); + } + // Check if icon is on SystemTray + if (!Arrays.asList(SystemTray.getSystemTray().getTrayIcons()).contains(icon)) { + throw new IllegalStateException("TrayIcon needs to be added on SystemTray first"); + } + + // Prevent second invocation; causes icon to disappear + if(templateIconForced) { + return; + } else { + templateIconForced = true; + } + + try { + Field ptrField = Class.forName("sun.lwawt.macosx.CFRetainedResource").getDeclaredField("ptr"); + ptrField.setAccessible(true); + + Field field = TrayIcon.class.getDeclaredField("peer"); + field.setAccessible(true); + long cTrayIconAddress = ptrField.getLong(field.get(icon)); + + long cPopupMenuAddressTmp = 0; + if (icon.getPopupMenu() != null) { + field = MenuComponent.class.getDeclaredField("peer"); + field.setAccessible(true); + cPopupMenuAddressTmp = ptrField.getLong(field.get(icon.getPopupMenu())); + } + final long cPopupMenuAddress = cPopupMenuAddressTmp; + + final NativeLong statusItem = FoundationUtil.invoke(new NativeLong(cTrayIconAddress), "theItem"); + NativeLong awtView = FoundationUtil.invoke(statusItem, "view"); + final NativeLong image = Foundation.INSTANCE.object_getIvar(awtView, Foundation.INSTANCE.class_getInstanceVariable(FoundationUtil.invoke(awtView, "class"), "image")); + FoundationUtil.invoke(image, "setTemplate:", true); + FoundationUtil.runOnMainThreadAndWait(() -> { + FoundationUtil.invoke(statusItem, "setView:", (Object) null); + NativeLong target; + if (SystemUtilities.getOSVersion().greaterThanOrEqualTo(Version.forIntegers(10, 10))) { + target = FoundationUtil.invoke(statusItem, "button"); + } else { + target = statusItem; + } + FoundationUtil.invoke(target, "setImage:", image); + //FoundationUtil.invoke(statusItem, "setLength:", length); + + if (cPopupMenuAddress != 0) { + FoundationUtil.invoke(statusItem, "setMenu:", FoundationUtil.invoke(new NativeLong(cPopupMenuAddress), "menu")); + } else { + new ActionCallback(() -> { + final ActionListener[] listeners = icon.getActionListeners(); + final int now = (int) System.currentTimeMillis(); + for (int i = 0; i < listeners.length; i++) { + final int iF = i; + SwingUtilities.invokeLater(() -> listeners[iF].actionPerformed(new ActionEvent(icon, now + iF, null))); + } + }).installActionOnNSControl(target); + } + }); + } catch (Throwable ignore) {} + } + } diff --git a/src/qz/utils/SystemUtilities.java b/src/qz/utils/SystemUtilities.java index e58d4e717..540aabf1a 100644 --- a/src/qz/utils/SystemUtilities.java +++ b/src/qz/utils/SystemUtilities.java @@ -317,11 +317,14 @@ public static boolean isDarkTaskbar() { public static boolean isDarkTaskbar(boolean recheck) { if(darkTaskbar == null || recheck) { - if (!isWindows()) { - // Mac and Linux don't differentiate; return the cached darkDesktop value - darkTaskbar = isDarkDesktop(); - } else { + if (isWindows()) { darkTaskbar = WindowsUtilities.isDarkTaskbar(); + } else if(isMac()) { + // Ignore, we'll set the template flag using JNA + darkTaskbar = false; + } else { + // Linux doesn't differentiate; return the cached darkDesktop value + darkTaskbar = isDarkDesktop(); } } return darkTaskbar.booleanValue(); @@ -335,7 +338,7 @@ public static boolean isDarkDesktop(boolean recheck) { if (darkDesktop == null || recheck) { // Check for Dark Mode on MacOS if (isMac()) { - darkDesktop = MacUtilities.isDarkMode(); + darkDesktop = MacUtilities.isDarkDesktop(); } else if (isWindows()) { darkDesktop = WindowsUtilities.isDarkDesktop(); } else { @@ -353,7 +356,8 @@ public static void adjustThemeColors() { public static boolean prefersMaskTrayIcon() { if (Constants.MASK_TRAY_SUPPORTED) { if (SystemUtilities.isMac()) { - return true; + // Assume a pid of -1 is a broken JNA + return MacUtilities.getProcessID() != -1; } else if (SystemUtilities.isWindows() && SystemUtilities.getOSVersion().getMajorVersion() >= 10) { return true; }