From 5e1b580cb6a9cb37cadc5e72a8f0e7fb7dc38061 Mon Sep 17 00:00:00 2001 From: oc7o Date: Sun, 24 May 2026 16:04:30 +0200 Subject: [PATCH 1/6] Add native TrayMenuController implementation for macOS --- .gitignore | 3 + ..._macos_tray_MacTrayMenuController_Native.h | 71 +++++++ src/main/java/module-info.java | 5 + .../macos/tray/MacTrayMenuController.java | 91 +++++++++ .../Integrations.xcodeproj/project.pbxproj | 4 + ..._macos_tray_MacTrayMenuController_Native.m | 182 ++++++++++++++++++ 6 files changed, 356 insertions(+) create mode 100644 src/main/headers/org_cryptomator_macos_tray_MacTrayMenuController_Native.h create mode 100644 src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java create mode 100644 src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m diff --git a/.gitignore b/.gitignore index b35bb5e..190649d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ pom.xml.versionsBackup .idea/compiler.xml .idea/jarRepositories.xml *.iml + +# MacOS +.DS_Store \ No newline at end of file diff --git a/src/main/headers/org_cryptomator_macos_tray_MacTrayMenuController_Native.h b/src/main/headers/org_cryptomator_macos_tray_MacTrayMenuController_Native.h new file mode 100644 index 0000000..62b411c --- /dev/null +++ b/src/main/headers/org_cryptomator_macos_tray_MacTrayMenuController_Native.h @@ -0,0 +1,71 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class org_cryptomator_macos_tray_MacTrayMenuController_Native */ + +#ifndef _Included_org_cryptomator_macos_tray_MacTrayMenuController_Native +#define _Included_org_cryptomator_macos_tray_MacTrayMenuController_Native +#ifdef __cplusplus +extern "C" { +#endif +#undef org_cryptomator_macos_tray_MacTrayMenuController_Native_ROOT_MENU +#define org_cryptomator_macos_tray_MacTrayMenuController_Native_ROOT_MENU 0LL +/* + * Class: org_cryptomator_macos_tray_MacTrayMenuController_Native + * Method: showTrayIcon + * Signature: ([BLjava/lang/String;Ljava/lang/Runnable;)V + */ +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_showTrayIcon + (JNIEnv *, jobject, jbyteArray, jstring, jobject); + +/* + * Class: org_cryptomator_macos_tray_MacTrayMenuController_Native + * Method: updateTrayIcon + * Signature: ([B)V + */ +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_updateTrayIcon + (JNIEnv *, jobject, jbyteArray); + +/* + * Class: org_cryptomator_macos_tray_MacTrayMenuController_Native + * Method: clearMenu + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_clearMenu + (JNIEnv *, jobject); + +/* + * Class: org_cryptomator_macos_tray_MacTrayMenuController_Native + * Method: addActionItem + * Signature: (JLjava/lang/String;ZLjava/lang/Runnable;)V + */ +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addActionItem + (JNIEnv *, jobject, jlong, jstring, jboolean, jobject); + +/* + * Class: org_cryptomator_macos_tray_MacTrayMenuController_Native + * Method: addSeparator + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addSeparator + (JNIEnv *, jobject, jlong); + +/* + * Class: org_cryptomator_macos_tray_MacTrayMenuController_Native + * Method: addSubMenuItem + * Signature: (JLjava/lang/String;)J + */ +JNIEXPORT jlong JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addSubMenuItem + (JNIEnv *, jobject, jlong, jstring); + +/* + * Class: org_cryptomator_macos_tray_MacTrayMenuController_Native + * Method: setBeforeOpenMenuListener + * Signature: (Ljava/lang/Runnable;)V + */ +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_setBeforeOpenMenuListener + (JNIEnv *, jobject, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 25c4ab3..fe760e3 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -2,6 +2,7 @@ import org.cryptomator.integrations.keychain.KeychainAccessProvider; import org.cryptomator.integrations.revealpath.RevealPathService; import org.cryptomator.integrations.tray.TrayIntegrationProvider; +import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.integrations.update.UpdateMechanism; import org.cryptomator.macos.autostart.MacAutoStartProvider; @@ -9,6 +10,7 @@ import org.cryptomator.macos.keychain.TouchIdKeychainAccess; import org.cryptomator.macos.revealpath.OpenCmdRevealPathService; import org.cryptomator.macos.tray.MacTrayIntegrationProvider; +import org.cryptomator.macos.tray.MacTrayMenuController; import org.cryptomator.macos.uiappearance.MacUiAppearanceProvider; import org.cryptomator.macos.update.DmgUpdateMechanism; @@ -16,10 +18,13 @@ requires org.cryptomator.integrations.api; requires org.slf4j; + opens org.cryptomator.macos.tray to org.cryptomator.integrations.api; + provides AutoStartProvider with MacAutoStartProvider; provides KeychainAccessProvider with MacSystemKeychainAccess, TouchIdKeychainAccess; provides RevealPathService with OpenCmdRevealPathService; provides TrayIntegrationProvider with MacTrayIntegrationProvider; + provides TrayMenuController with MacTrayMenuController; provides UiAppearanceProvider with MacUiAppearanceProvider; provides UpdateMechanism with DmgUpdateMechanism; } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java b/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java new file mode 100644 index 0000000..6381927 --- /dev/null +++ b/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java @@ -0,0 +1,91 @@ +package org.cryptomator.macos.tray; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.tray.ActionItem; +import org.cryptomator.integrations.tray.SeparatorItem; +import org.cryptomator.integrations.tray.SubMenuItem; +import org.cryptomator.integrations.tray.TrayIconLoader; +import org.cryptomator.integrations.tray.TrayMenuController; +import org.cryptomator.integrations.tray.TrayMenuException; +import org.cryptomator.integrations.tray.TrayMenuItem; +import org.cryptomator.macos.common.NativeLibLoader; + +import java.util.List; +import java.util.function.Consumer; + +@Priority(1000) +@OperatingSystem(OperatingSystem.Value.MAC) +@CheckAvailability +public class MacTrayMenuController implements TrayMenuController { + + @CheckAvailability + public static boolean isAvailable() { + return MacTrayMenuController.class.getResource("/libIntegrations.dylib") != null; + } + + @Override + public void showTrayIcon(Consumer iconLoader, Runnable defaultAction, String tooltip) throws TrayMenuException { + Native.INSTANCE.showTrayIcon(loadPng(iconLoader), tooltip, defaultAction); + } + + @Override + public void updateTrayIcon(Consumer iconLoader) { + Native.INSTANCE.updateTrayIcon(loadPng(iconLoader)); + } + + @Override + public void updateTrayMenu(List items) { + Native.INSTANCE.clearMenu(); + buildMenu(Native.ROOT_MENU, items); + } + + @Override + public void onBeforeOpenMenu(Runnable listener) { + Native.INSTANCE.setBeforeOpenMenuListener(listener); + } + + private static byte[] loadPng(Consumer iconLoader) { + byte[][] holder = {null}; + iconLoader.accept((TrayIconLoader.PngData) data -> holder[0] = data); + return holder[0]; + } + + private void buildMenu(long menuHandle, List items) { + for (var item : items) { + switch (item) { + case ActionItem a -> Native.INSTANCE.addActionItem(menuHandle, a.title(), a.enabled(), a.action()); + case SeparatorItem ignored -> Native.INSTANCE.addSeparator(menuHandle); + case SubMenuItem s -> { + long submenuHandle = Native.INSTANCE.addSubMenuItem(menuHandle, s.title()); + buildMenu(submenuHandle, s.items()); + } + } + } + } + + private static final class Native { + + static final long ROOT_MENU = 0L; + static final Native INSTANCE = new Native(); + + private Native() { + NativeLibLoader.loadLib(); + } + + native void showTrayIcon(byte[] pngData, String tooltip, Runnable defaultAction); + + native void updateTrayIcon(byte[] pngData); + + native void clearMenu(); + + native void addActionItem(long menuHandle, String title, boolean enabled, Runnable action); + + native void addSeparator(long menuHandle); + + native long addSubMenuItem(long menuHandle, String title); + + native void setBeforeOpenMenuListener(Runnable listener); + } +} diff --git a/src/main/native/Integrations.xcodeproj/project.pbxproj b/src/main/native/Integrations.xcodeproj/project.pbxproj index a1c46cb..23a3751 100644 --- a/src/main/native/Integrations.xcodeproj/project.pbxproj +++ b/src/main/native/Integrations.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 06B9771B2FB9FC7200949F2E /* org_cryptomator_macos_tray_MacTrayMenuController_Native.m in Sources */ = {isa = PBXBuildFile; fileRef = 06B9771A2FB9FC7200949F2E /* org_cryptomator_macos_tray_MacTrayMenuController_Native.m */; }; 74BEF1852C0384FB006731AC /* SKYLaunchService.m in Sources */ = {isa = PBXBuildFile; fileRef = 74BEF1842C0384FB006731AC /* SKYLaunchService.m */; }; 74BEF1862C0384FB006731AC /* SKYLaunchService.h in Headers */ = {isa = PBXBuildFile; fileRef = 74BEF1832C0384FB006731AC /* SKYLaunchService.h */; }; 74CF2E7B254C295A006266D6 /* org_cryptomator_macos_autostart_MacLaunchServices_Native.m in Sources */ = {isa = PBXBuildFile; fileRef = 74CF2E7A254C295A006266D6 /* org_cryptomator_macos_autostart_MacLaunchServices_Native.m */; }; @@ -21,6 +22,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 06B9771A2FB9FC7200949F2E /* org_cryptomator_macos_tray_MacTrayMenuController_Native.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = org_cryptomator_macos_tray_MacTrayMenuController_Native.m; sourceTree = ""; }; 74BEF1832C0384FB006731AC /* SKYLaunchService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SKYLaunchService.h; sourceTree = ""; }; 74BEF1842C0384FB006731AC /* SKYLaunchService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SKYLaunchService.m; sourceTree = ""; }; 74CF2E7A254C295A006266D6 /* org_cryptomator_macos_autostart_MacLaunchServices_Native.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = org_cryptomator_macos_autostart_MacLaunchServices_Native.m; sourceTree = ""; }; @@ -49,6 +51,7 @@ 9E0819AE1D748ECC000C89E6 = { isa = PBXGroup; children = ( + 06B9771A2FB9FC7200949F2E /* org_cryptomator_macos_tray_MacTrayMenuController_Native.m */, 74CF2E7A254C295A006266D6 /* org_cryptomator_macos_autostart_MacLaunchServices_Native.m */, 9E0819C71D75CABC000C89E6 /* org_cryptomator_macos_keychain_MacKeychain_Native.m */, 9EACB1B325557865000F3214 /* org_cryptomator_macos_tray_ActivationPolicy_Native.m */, @@ -145,6 +148,7 @@ 74BEF1852C0384FB006731AC /* SKYLaunchService.m in Sources */, 9EACB1B425557865000F3214 /* org_cryptomator_macos_tray_ActivationPolicy_Native.m in Sources */, 74CF2E7B254C295A006266D6 /* org_cryptomator_macos_autostart_MacLaunchServices_Native.m in Sources */, + 06B9771B2FB9FC7200949F2E /* org_cryptomator_macos_tray_MacTrayMenuController_Native.m in Sources */, 9E21340C255429CA006EA872 /* SKYAppearanceObserver.m in Sources */, 9E21340025542735006EA872 /* org_cryptomator_macos_uiappearance_AppAppearance_Native.m in Sources */, 9E21341225542ECE006EA872 /* SKYAppearanceNotifier.m in Sources */, diff --git a/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m new file mode 100644 index 0000000..cb5b9cd --- /dev/null +++ b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m @@ -0,0 +1,182 @@ +// +// org_cryptomator_macos_tray_MacTrayMenuController_Native.m +// + +#import +#include + +static JavaVM *gJvm = NULL; +static NSStatusItem *gStatusItem = nil; +static NSMenu *gRootMenu = nil; + +// NSMenuItem.target is a weak reference, so action handlers are retained here +// for the menu's lifetime and released in clearMenu(). +static NSMutableArray *gActionHandlers = nil; + +static JNIEnv *getEnv(void) { + JNIEnv *env = NULL; + (*gJvm)->AttachCurrentThread(gJvm, (void **)&env, NULL); + return env; +} + +static void callRunnable(JNIEnv *env, jobject runnable) { + jclass cls = (*env)->FindClass(env, "java/lang/Runnable"); + jmethodID run = (*env)->GetMethodID(env, cls, "run", "()V"); + (*env)->CallVoidMethod(env, runnable, run); + (*env)->DeleteLocalRef(env, cls); +} + +@interface SKYActionHandler : NSObject +- (instancetype)initWithAction:(jobject)action; +- (void)fire:(id)sender; +@end + +@implementation SKYActionHandler { + jobject _action; +} + +- (instancetype)initWithAction:(jobject)action { + self = [super init]; + JNIEnv *env = getEnv(); + _action = (*env)->NewGlobalRef(env, action); + return self; +} + +- (void)dealloc { + JNIEnv *env = getEnv(); + (*env)->DeleteGlobalRef(env, _action); +} + +- (void)fire:(id)sender { + JNIEnv *env = getEnv(); + callRunnable(env, _action); +} + +@end + +@interface SKYMenuDelegate : NSObject +@property (nonatomic) jobject beforeOpenListener; +@end + +@implementation SKYMenuDelegate + +- (void)menuWillOpen:(NSMenu *)menu { + if (self.beforeOpenListener != NULL) { + JNIEnv *env = getEnv(); + callRunnable(env, self.beforeOpenListener); + } +} + +@end + +static SKYMenuDelegate *gMenuDelegate = nil; + +// Menu handles crossing the JNI boundary are raw NSMenu pointers cast to jlong; +// 0 (ROOT_MENU) denotes the status item's root menu. +static NSMenu *menuFromHandle(jlong handle) { + if (handle == 0L) { + return gRootMenu; + } + return (__bridge NSMenu *)(void *)(intptr_t)handle; +} + +JNIEXPORT jint JNI_OnLoad(JavaVM *jvm, void *reserved) { + gJvm = jvm; + gActionHandlers = [NSMutableArray array]; + return JNI_VERSION_1_8; +} + +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_showTrayIcon +(JNIEnv *env, jobject obj, jbyteArray pngData, jstring tooltip, jobject defaultAction) { + jsize len = (*env)->GetArrayLength(env, pngData); + jbyte *bytes = (*env)->GetByteArrayElements(env, pngData, NULL); + NSData *imageData = [NSData dataWithBytes:bytes length:len]; + (*env)->ReleaseByteArrayElements(env, pngData, bytes, JNI_ABORT); + + const char *tooltipChars = (*env)->GetStringUTFChars(env, tooltip, NULL); + NSString *tooltipStr = [NSString stringWithUTF8String:tooltipChars]; + (*env)->ReleaseStringUTFChars(env, tooltip, tooltipChars); + + gMenuDelegate = [[SKYMenuDelegate alloc] init]; + + NSImage *image = [[NSImage alloc] initWithData:imageData]; + image.size = NSMakeSize(18, 18); + image.template = YES; + + gRootMenu = [[NSMenu alloc] init]; + [gRootMenu setAutoenablesItems:NO]; + gRootMenu.delegate = gMenuDelegate; + + gStatusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + gStatusItem.button.image = image; + gStatusItem.button.toolTip = tooltipStr; + gStatusItem.menu = gRootMenu; +} + +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_updateTrayIcon +(JNIEnv *env, jobject obj, jbyteArray pngData) { + jsize len = (*env)->GetArrayLength(env, pngData); + jbyte *bytes = (*env)->GetByteArrayElements(env, pngData, NULL); + NSData *imageData = [NSData dataWithBytes:bytes length:len]; + (*env)->ReleaseByteArrayElements(env, pngData, bytes, JNI_ABORT); + + NSImage *image = [[NSImage alloc] initWithData:imageData]; + image.size = NSMakeSize(18, 18); + image.template = YES; + gStatusItem.button.image = image; +} + +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_clearMenu +(JNIEnv *env, jobject obj) { + [gRootMenu removeAllItems]; + [gActionHandlers removeAllObjects]; +} + +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addActionItem +(JNIEnv *env, jobject obj, jlong menuHandle, jstring title, jboolean enabled, jobject action) { + const char *titleChars = (*env)->GetStringUTFChars(env, title, NULL); + NSString *titleStr = [NSString stringWithUTF8String:titleChars]; + (*env)->ReleaseStringUTFChars(env, title, titleChars); + + SKYActionHandler *handler = [[SKYActionHandler alloc] initWithAction:action]; + [gActionHandlers addObject:handler]; + + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:titleStr + action:@selector(fire:) + keyEquivalent:@""]; + item.target = handler; + item.enabled = (BOOL)enabled; + [menuFromHandle(menuHandle) addItem:item]; +} + +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addSeparator +(JNIEnv *env, jobject obj, jlong menuHandle) { + [menuFromHandle(menuHandle) addItem:[NSMenuItem separatorItem]]; +} + +JNIEXPORT jlong JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addSubMenuItem +(JNIEnv *env, jobject obj, jlong menuHandle, jstring title) { + const char *titleChars = (*env)->GetStringUTFChars(env, title, NULL); + NSString *titleStr = [NSString stringWithUTF8String:titleChars]; + (*env)->ReleaseStringUTFChars(env, title, titleChars); + + NSMenu *submenu = [[NSMenu alloc] initWithTitle:titleStr]; + [submenu setAutoenablesItems:NO]; + submenu.delegate = gMenuDelegate; + + NSMenuItem *parentItem = [[NSMenuItem alloc] initWithTitle:titleStr + action:NULL + keyEquivalent:@""]; + parentItem.submenu = submenu; + [menuFromHandle(menuHandle) addItem:parentItem]; + + return (jlong)(intptr_t)(__bridge void *)submenu; +} + +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_setBeforeOpenMenuListener +(JNIEnv *env, jobject obj, jobject listener) { + if (gMenuDelegate.beforeOpenListener != NULL) { + (*env)->DeleteGlobalRef(env, gMenuDelegate.beforeOpenListener); + } + gMenuDelegate.beforeOpenListener = (*env)->NewGlobalRef(env, listener); +} From 0b468072ae45b5d04a00b3bbaeb252cacc0d7db8 Mon Sep 17 00:00:00 2001 From: 0x70 <104923372+oc7o@users.noreply.github.com> Date: Sun, 24 May 2026 17:22:31 +0200 Subject: [PATCH 2/6] Add null guard to loadPng function Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../java/org/cryptomator/macos/tray/MacTrayMenuController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java b/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java index 6381927..a2057f7 100644 --- a/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java +++ b/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java @@ -49,6 +49,9 @@ public void onBeforeOpenMenu(Runnable listener) { private static byte[] loadPng(Consumer iconLoader) { byte[][] holder = {null}; iconLoader.accept((TrayIconLoader.PngData) data -> holder[0] = data); + if (holder[0] == null) { + throw new IllegalStateException("Icon loader did not provide PNG data"); + } return holder[0]; } From 7d0af148a790233e06a3be09337565f40dec1fad Mon Sep 17 00:00:00 2001 From: oc7o Date: Sun, 24 May 2026 17:28:57 +0200 Subject: [PATCH 3/6] Add comment explaining why the defaultAction parameter is unused --- .../org_cryptomator_macos_tray_MacTrayMenuController_Native.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m index cb5b9cd..b7a45ae 100644 --- a/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m +++ b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m @@ -88,6 +88,8 @@ JNIEXPORT jint JNI_OnLoad(JavaVM *jvm, void *reserved) { JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_showTrayIcon (JNIEnv *env, jobject obj, jbyteArray pngData, jstring tooltip, jobject defaultAction) { + // defaultAction is intentionally unused: this status item always presents its + // menu on click (see gStatusItem.menu below) jsize len = (*env)->GetArrayLength(env, pngData); jbyte *bytes = (*env)->GetByteArrayElements(env, pngData, NULL); NSData *imageData = [NSData dataWithBytes:bytes length:len]; From 54dddd3dd590be8174aebc568da874ea3c55809b Mon Sep 17 00:00:00 2001 From: 0x70 <104923372+oc7o@users.noreply.github.com> Date: Sun, 24 May 2026 17:33:51 +0200 Subject: [PATCH 4/6] Log error to stderr for better debugging Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../org_cryptomator_macos_tray_MacTrayMenuController_Native.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m index b7a45ae..5f056d5 100644 --- a/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m +++ b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m @@ -24,6 +24,10 @@ static void callRunnable(JNIEnv *env, jobject runnable) { jmethodID run = (*env)->GetMethodID(env, cls, "run", "()V"); (*env)->CallVoidMethod(env, runnable, run); (*env)->DeleteLocalRef(env, cls); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); // Log to stderr for debugging + (*env)->ExceptionClear(env); + } } @interface SKYActionHandler : NSObject From cd9c43519041fc7416be3fa7ecc3bcb6c6ec4d89 Mon Sep 17 00:00:00 2001 From: oc7o Date: Wed, 27 May 2026 06:29:06 +0200 Subject: [PATCH 5/6] removed obsolete availability check as suggested in PR --- src/main/java/module-info.java | 2 -- .../org/cryptomator/macos/tray/MacTrayMenuController.java | 7 ------- 2 files changed, 9 deletions(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index fe760e3..dc2b4e7 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -18,8 +18,6 @@ requires org.cryptomator.integrations.api; requires org.slf4j; - opens org.cryptomator.macos.tray to org.cryptomator.integrations.api; - provides AutoStartProvider with MacAutoStartProvider; provides KeychainAccessProvider with MacSystemKeychainAccess, TouchIdKeychainAccess; provides RevealPathService with OpenCmdRevealPathService; diff --git a/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java b/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java index a2057f7..a8f16de 100644 --- a/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java +++ b/src/main/java/org/cryptomator/macos/tray/MacTrayMenuController.java @@ -1,6 +1,5 @@ package org.cryptomator.macos.tray; -import org.cryptomator.integrations.common.CheckAvailability; import org.cryptomator.integrations.common.OperatingSystem; import org.cryptomator.integrations.common.Priority; import org.cryptomator.integrations.tray.ActionItem; @@ -17,14 +16,8 @@ @Priority(1000) @OperatingSystem(OperatingSystem.Value.MAC) -@CheckAvailability public class MacTrayMenuController implements TrayMenuController { - @CheckAvailability - public static boolean isAvailable() { - return MacTrayMenuController.class.getResource("/libIntegrations.dylib") != null; - } - @Override public void showTrayIcon(Consumer iconLoader, Runnable defaultAction, String tooltip) throws TrayMenuException { Native.INSTANCE.showTrayIcon(loadPng(iconLoader), tooltip, defaultAction); From bc5403995be7918ddf1f2da5b152e9ae63d4444f Mon Sep 17 00:00:00 2001 From: oc7o Date: Sat, 15 Aug 2026 12:27:10 +0200 Subject: [PATCH 6/6] autoreleasepool, thread safety, global Runnable, and minor fixes --- ..._macos_tray_MacTrayMenuController_Native.m | 302 ++++++++++++------ 1 file changed, 198 insertions(+), 104 deletions(-) diff --git a/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m index 5f056d5..485918c 100644 --- a/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m +++ b/src/main/native/org_cryptomator_macos_tray_MacTrayMenuController_Native.m @@ -4,30 +4,29 @@ #import #include +#include "org_cryptomator_macos_tray_MacTrayMenuController_Native.h" static JavaVM *gJvm = NULL; static NSStatusItem *gStatusItem = nil; static NSMenu *gRootMenu = nil; -// NSMenuItem.target is a weak reference, so action handlers are retained here -// for the menu's lifetime and released in clearMenu(). -static NSMutableArray *gActionHandlers = nil; +static jobject gBeforeOpenListener = NULL; + +// Cached java.lang.Runnable.run() method ID for invoking Java callbacks. +static jmethodID gRunnableRun = NULL; static JNIEnv *getEnv(void) { - JNIEnv *env = NULL; - (*gJvm)->AttachCurrentThread(gJvm, (void **)&env, NULL); - return env; + JNIEnv *env = NULL; + (*gJvm)->AttachCurrentThread(gJvm, (void **)&env, NULL); + return env; } static void callRunnable(JNIEnv *env, jobject runnable) { - jclass cls = (*env)->FindClass(env, "java/lang/Runnable"); - jmethodID run = (*env)->GetMethodID(env, cls, "run", "()V"); - (*env)->CallVoidMethod(env, runnable, run); - (*env)->DeleteLocalRef(env, cls); - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); // Log to stderr for debugging - (*env)->ExceptionClear(env); - } + (*env)->CallVoidMethod(env, runnable, gRunnableRun); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); // Log to stderr for debugging + (*env)->ExceptionClear(env); + } } @interface SKYActionHandler : NSObject @@ -36,153 +35,248 @@ - (void)fire:(id)sender; @end @implementation SKYActionHandler { - jobject _action; + jobject _action; } - (instancetype)initWithAction:(jobject)action { - self = [super init]; - JNIEnv *env = getEnv(); - _action = (*env)->NewGlobalRef(env, action); - return self; + self = [super init]; + _action = action; // global ref owned by this handler + return self; } - (void)dealloc { - JNIEnv *env = getEnv(); - (*env)->DeleteGlobalRef(env, _action); + JNIEnv *env = getEnv(); + (*env)->DeleteGlobalRef(env, _action); } - (void)fire:(id)sender { - JNIEnv *env = getEnv(); - callRunnable(env, _action); + JNIEnv *env = getEnv(); + callRunnable(env, _action); } @end @interface SKYMenuDelegate : NSObject -@property (nonatomic) jobject beforeOpenListener; @end @implementation SKYMenuDelegate - (void)menuWillOpen:(NSMenu *)menu { - if (self.beforeOpenListener != NULL) { - JNIEnv *env = getEnv(); - callRunnable(env, self.beforeOpenListener); - } + // Only the root tray menu shall trigger the callback, not submenus. + if (menu != gRootMenu) { + return; + } + if (gBeforeOpenListener != NULL) { + JNIEnv *env = getEnv(); + callRunnable(env, gBeforeOpenListener); + } } @end +// NSMenuItem.target is a weak reference, so action handlers are retained here +// for the menu's lifetime and released in clearMenu(). +static NSMutableArray *gActionHandlers = nil; + +// Single delegate created during JNI_OnLoad so its lifecycle is independent of +// the call order of showTrayIcon and setBeforeOpenMenuListener. static SKYMenuDelegate *gMenuDelegate = nil; // Menu handles crossing the JNI boundary are raw NSMenu pointers cast to jlong; // 0 (ROOT_MENU) denotes the status item's root menu. static NSMenu *menuFromHandle(jlong handle) { - if (handle == 0L) { - return gRootMenu; - } - return (__bridge NSMenu *)(void *)(intptr_t)handle; + if (handle == 0L) { + return gRootMenu; + } + return (__bridge NSMenu *)(void *)(intptr_t)handle; } JNIEXPORT jint JNI_OnLoad(JavaVM *jvm, void *reserved) { - gJvm = jvm; - gActionHandlers = [NSMutableArray array]; - return JNI_VERSION_1_8; + @autoreleasepool { + gJvm = jvm; + gActionHandlers = [NSMutableArray array]; + gMenuDelegate = [[SKYMenuDelegate alloc] init]; + + JNIEnv *env; + if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_8) == JNI_OK) { + jclass runnableClass = (*env)->FindClass(env, "java/lang/Runnable"); + gRunnableRun = (*env)->GetMethodID(env, runnableClass, "run", "()V"); + (*env)->DeleteLocalRef(env, runnableClass); + } + } + return JNI_VERSION_1_8; } -JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_showTrayIcon -(JNIEnv *env, jobject obj, jbyteArray pngData, jstring tooltip, jobject defaultAction) { - // defaultAction is intentionally unused: this status item always presents its - // menu on click (see gStatusItem.menu below) - jsize len = (*env)->GetArrayLength(env, pngData); - jbyte *bytes = (*env)->GetByteArrayElements(env, pngData, NULL); - NSData *imageData = [NSData dataWithBytes:bytes length:len]; - (*env)->ReleaseByteArrayElements(env, pngData, bytes, JNI_ABORT); - - const char *tooltipChars = (*env)->GetStringUTFChars(env, tooltip, NULL); - NSString *tooltipStr = [NSString stringWithUTF8String:tooltipChars]; - (*env)->ReleaseStringUTFChars(env, tooltip, tooltipChars); - - gMenuDelegate = [[SKYMenuDelegate alloc] init]; - - NSImage *image = [[NSImage alloc] initWithData:imageData]; - image.size = NSMakeSize(18, 18); - image.template = YES; +JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved) { + @autoreleasepool { + if (gBeforeOpenListener != NULL) { + JNIEnv *env = getEnv(); + (*env)->DeleteGlobalRef(env, gBeforeOpenListener); + gBeforeOpenListener = NULL; + } + gMenuDelegate = nil; + gActionHandlers = nil; + gJvm = NULL; + } +} - gRootMenu = [[NSMenu alloc] init]; - [gRootMenu setAutoenablesItems:NO]; - gRootMenu.delegate = gMenuDelegate; +static void runOnMainThread(void (^block)(void)) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} - gStatusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - gStatusItem.button.image = image; - gStatusItem.button.toolTip = tooltipStr; - gStatusItem.menu = gRootMenu; +JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_showTrayIcon +(JNIEnv *env, jobject obj, jbyteArray pngData, jstring tooltip, jobject defaultAction) { + // defaultAction is intentionally unused: this status item always presents its + // menu on click (see gStatusItem.menu below) + (void)defaultAction; + + @autoreleasepool { + jsize len = (*env)->GetArrayLength(env, pngData); + jbyte *bytes = (*env)->GetByteArrayElements(env, pngData, NULL); + NSData *imageData = [NSData dataWithBytes:bytes length:len]; + (*env)->ReleaseByteArrayElements(env, pngData, bytes, JNI_ABORT); + + const char *tooltipChars = (*env)->GetStringUTFChars(env, tooltip, NULL); + NSString *tooltipStr = [NSString stringWithUTF8String:tooltipChars]; + (*env)->ReleaseStringUTFChars(env, tooltip, tooltipChars); + + runOnMainThread((dispatch_block_t)^(void) { + @autoreleasepool { + if (gStatusItem != nil) { + [[NSStatusBar systemStatusBar] removeStatusItem:gStatusItem]; + gStatusItem = nil; + } + + [gRootMenu removeAllItems]; + [gActionHandlers removeAllObjects]; + + gRootMenu = [[NSMenu alloc] init]; + [gRootMenu setAutoenablesItems:NO]; + gRootMenu.delegate = gMenuDelegate; + + NSImage *image = [[NSImage alloc] initWithData:imageData]; + image.size = NSMakeSize(18, 18); + image.template = YES; + + gStatusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + gStatusItem.button.image = image; + gStatusItem.button.toolTip = tooltipStr; + gStatusItem.menu = gRootMenu; + } + }); + } } JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_updateTrayIcon (JNIEnv *env, jobject obj, jbyteArray pngData) { - jsize len = (*env)->GetArrayLength(env, pngData); - jbyte *bytes = (*env)->GetByteArrayElements(env, pngData, NULL); - NSData *imageData = [NSData dataWithBytes:bytes length:len]; - (*env)->ReleaseByteArrayElements(env, pngData, bytes, JNI_ABORT); - - NSImage *image = [[NSImage alloc] initWithData:imageData]; - image.size = NSMakeSize(18, 18); - image.template = YES; - gStatusItem.button.image = image; + @autoreleasepool { + jsize len = (*env)->GetArrayLength(env, pngData); + jbyte *bytes = (*env)->GetByteArrayElements(env, pngData, NULL); + NSData *imageData = [NSData dataWithBytes:bytes length:len]; + (*env)->ReleaseByteArrayElements(env, pngData, bytes, JNI_ABORT); + + runOnMainThread((dispatch_block_t)^(void) { + @autoreleasepool { + NSImage *image = [[NSImage alloc] initWithData:imageData]; + image.size = NSMakeSize(18, 18); + image.template = YES; + gStatusItem.button.image = image; + } + }); + } } JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_clearMenu (JNIEnv *env, jobject obj) { - [gRootMenu removeAllItems]; - [gActionHandlers removeAllObjects]; + runOnMainThread((dispatch_block_t)^(void) { + @autoreleasepool { + [gRootMenu removeAllItems]; + [gActionHandlers removeAllObjects]; + } + }); } JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addActionItem (JNIEnv *env, jobject obj, jlong menuHandle, jstring title, jboolean enabled, jobject action) { - const char *titleChars = (*env)->GetStringUTFChars(env, title, NULL); - NSString *titleStr = [NSString stringWithUTF8String:titleChars]; - (*env)->ReleaseStringUTFChars(env, title, titleChars); - - SKYActionHandler *handler = [[SKYActionHandler alloc] initWithAction:action]; - [gActionHandlers addObject:handler]; - - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:titleStr - action:@selector(fire:) - keyEquivalent:@""]; - item.target = handler; - item.enabled = (BOOL)enabled; - [menuFromHandle(menuHandle) addItem:item]; + @autoreleasepool { + const char *titleChars = (*env)->GetStringUTFChars(env, title, NULL); + NSString *titleStr = [NSString stringWithUTF8String:titleChars]; + (*env)->ReleaseStringUTFChars(env, title, titleChars); + + jobject actionRef = (*env)->NewGlobalRef(env, action); + + runOnMainThread((dispatch_block_t)^(void) { + @autoreleasepool { + SKYActionHandler *handler = [[SKYActionHandler alloc] initWithAction:actionRef]; + [gActionHandlers addObject:handler]; + + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:titleStr + action:@selector(fire:) + keyEquivalent:@""]; + item.target = handler; + item.enabled = (BOOL)enabled; + [menuFromHandle(menuHandle) addItem:item]; + } + }); + } } JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addSeparator (JNIEnv *env, jobject obj, jlong menuHandle) { - [menuFromHandle(menuHandle) addItem:[NSMenuItem separatorItem]]; + runOnMainThread((dispatch_block_t)^(void) { + @autoreleasepool { + [menuFromHandle(menuHandle) addItem:[NSMenuItem separatorItem]]; + } + }); } JNIEXPORT jlong JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_addSubMenuItem (JNIEnv *env, jobject obj, jlong menuHandle, jstring title) { - const char *titleChars = (*env)->GetStringUTFChars(env, title, NULL); - NSString *titleStr = [NSString stringWithUTF8String:titleChars]; - (*env)->ReleaseStringUTFChars(env, title, titleChars); - - NSMenu *submenu = [[NSMenu alloc] initWithTitle:titleStr]; - [submenu setAutoenablesItems:NO]; - submenu.delegate = gMenuDelegate; - - NSMenuItem *parentItem = [[NSMenuItem alloc] initWithTitle:titleStr - action:NULL - keyEquivalent:@""]; - parentItem.submenu = submenu; - [menuFromHandle(menuHandle) addItem:parentItem]; - - return (jlong)(intptr_t)(__bridge void *)submenu; + __block jlong result = 0L; + + @autoreleasepool { + const char *titleChars = (*env)->GetStringUTFChars(env, title, NULL); + NSString *titleStr = [NSString stringWithUTF8String:titleChars]; + (*env)->ReleaseStringUTFChars(env, title, titleChars); + + runOnMainThread((dispatch_block_t)^(void) { + @autoreleasepool { + NSMenu *submenu = [[NSMenu alloc] initWithTitle:titleStr]; + [submenu setAutoenablesItems:NO]; + // Submenus share the delegate, but menuWillOpen: ignores non-root menus. + submenu.delegate = gMenuDelegate; + + NSMenuItem *parentItem = [[NSMenuItem alloc] initWithTitle:titleStr + action:NULL + keyEquivalent:@""]; + parentItem.submenu = submenu; + [menuFromHandle(menuHandle) addItem:parentItem]; + + result = (jlong)(intptr_t)(__bridge void *)submenu; + } + }); + } + + return result; } JNIEXPORT void JNICALL Java_org_cryptomator_macos_tray_MacTrayMenuController_00024Native_setBeforeOpenMenuListener (JNIEnv *env, jobject obj, jobject listener) { - if (gMenuDelegate.beforeOpenListener != NULL) { - (*env)->DeleteGlobalRef(env, gMenuDelegate.beforeOpenListener); - } - gMenuDelegate.beforeOpenListener = (*env)->NewGlobalRef(env, listener); + @autoreleasepool { + jobject listenerRef = (*env)->NewGlobalRef(env, listener); + + runOnMainThread((dispatch_block_t)^(void) { + @autoreleasepool { + if (gBeforeOpenListener != NULL) { + JNIEnv *mainEnv = getEnv(); + (*mainEnv)->DeleteGlobalRef(mainEnv, gBeforeOpenListener); + } + gBeforeOpenListener = listenerRef; + } + }); + } }