Skip to content

Commit 8c06dfa

Browse files
committed
Replace JNA with JNI for macOS native bridge
Migrate all macOS JNA (Java Native Access) bindings to JNI (Java Native Interface) to reduce dependency surface on macOS. JNA remains for Windows/Linux. - Create MacTrayBridge.m: Objective-C JNI bridge (~420 lines) with callback trampolines, GlobalRef lifecycle management, and JAWT support - Create MacNativeBridge.kt: Kotlin JNI binding with auto library loading - Add 7 new Swift @_cdecl functions for dock, space, and mouse APIs - Rewrite MacTrayManager to use opaque Long handles instead of JNA Structures - Rewrite MacOsWindowManager to use native bridge instead of ObjC runtime - Simplify MacOutsideClickWatcher (remove ApplicationServices JNA binding) - Update build.sh to compile ObjC bridge with JNI headers - Delete MacTrayLoader.kt (no longer needed)
1 parent 0b8e842 commit 8c06dfa

13 files changed

Lines changed: 1044 additions & 552 deletions

File tree

maclib/MacTrayBridge.m

Lines changed: 631 additions & 0 deletions
Large diffs are not rendered by default.

maclib/build.sh

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,69 @@
33
# Exit on error
44
set -e
55

6-
echo "Building MacTray library..."
6+
echo "Building MacTray library (Swift + JNI bridge)..."
77

88
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
99
OUTPUT_DIR="${NATIVE_LIBS_OUTPUT_DIR:-$SCRIPT_DIR/../src/commonMain/resources}"
1010
echo "Output dir for mac is: $OUTPUT_DIR"
1111

12-
echo "Building for ARM64 (Apple Silicon)..."
12+
# Detect JAVA_HOME
13+
if [ -z "$JAVA_HOME" ]; then
14+
JAVA_HOME=$(/usr/libexec/java_home 2>/dev/null || true)
15+
fi
16+
if [ -z "$JAVA_HOME" ] || [ ! -d "$JAVA_HOME" ]; then
17+
echo "ERROR: JAVA_HOME not found. Install a JDK or set JAVA_HOME."
18+
exit 1
19+
fi
20+
echo "Using JAVA_HOME: $JAVA_HOME"
21+
22+
JNI_INCLUDE="$JAVA_HOME/include"
23+
JNI_INCLUDE_DARWIN="$JAVA_HOME/include/darwin"
24+
25+
if [ ! -f "$JNI_INCLUDE/jni.h" ]; then
26+
echo "ERROR: jni.h not found at $JNI_INCLUDE/jni.h"
27+
exit 1
28+
fi
1329

1430
mkdir -p "$OUTPUT_DIR/darwin-aarch64"
1531
mkdir -p "$OUTPUT_DIR/darwin-x86-64"
1632

17-
swiftc -emit-library -o "$OUTPUT_DIR/darwin-aarch64/libMacTray.dylib" \
18-
-module-name MacTray \
19-
-swift-version 5 \
20-
-target arm64-apple-macosx11.0 \
21-
-O -whole-module-optimization \
22-
-framework Foundation \
23-
-framework Cocoa \
24-
-Xlinker -rpath -Xlinker @executable_path/../Frameworks \
25-
-Xlinker -rpath -Xlinker @loader_path/Frameworks \
26-
tray.swift
27-
28-
echo "Building for x86_64 (Intel)..."
29-
30-
swiftc -emit-library -o "$OUTPUT_DIR/darwin-x86-64/libMacTray.dylib" \
31-
-module-name MacTray \
32-
-swift-version 5 \
33-
-target x86_64-apple-macosx11.0 \
34-
-O -whole-module-optimization \
35-
-framework Foundation \
36-
-framework Cocoa \
37-
-Xlinker -rpath -Xlinker @executable_path/../Frameworks \
38-
-Xlinker -rpath -Xlinker @loader_path/Frameworks \
39-
tray.swift
33+
build_arch() {
34+
local ARCH=$1
35+
local TARGET=$2
36+
local OUT_DIR=$3
37+
38+
echo "Building for $ARCH..."
39+
40+
# 1. Compile the JNI bridge (Objective-C) -> bridge.o
41+
clang -c -o "$SCRIPT_DIR/bridge_${ARCH}.o" \
42+
-arch "$ARCH" \
43+
-mmacosx-version-min=11.0 \
44+
-I "$JNI_INCLUDE" \
45+
-I "$JNI_INCLUDE_DARWIN" \
46+
-I "$SCRIPT_DIR" \
47+
-fobjc-arc \
48+
"$SCRIPT_DIR/MacTrayBridge.m"
49+
50+
# 2. Compile Swift + link with the bridge object -> dylib
51+
swiftc -emit-library -o "$OUT_DIR/libMacTray.dylib" \
52+
-module-name MacTray \
53+
-swift-version 5 \
54+
-target "$TARGET" \
55+
-O -whole-module-optimization \
56+
-framework Foundation \
57+
-framework Cocoa \
58+
-framework ApplicationServices \
59+
-Xlinker -rpath -Xlinker @executable_path/../Frameworks \
60+
-Xlinker -rpath -Xlinker @loader_path/Frameworks \
61+
"$SCRIPT_DIR/tray.swift" \
62+
"$SCRIPT_DIR/bridge_${ARCH}.o"
63+
64+
# Clean up intermediate object
65+
rm -f "$SCRIPT_DIR/bridge_${ARCH}.o"
66+
}
67+
68+
build_arch "arm64" "arm64-apple-macosx11.0" "$OUTPUT_DIR/darwin-aarch64"
69+
build_arch "x86_64" "x86_64-apple-macosx11.0" "$OUTPUT_DIR/darwin-x86-64"
4070

4171
echo "Build completed successfully."

maclib/tray.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ TRAY_API const char *tray_get_status_item_region_for(struct tray *tray);
8282
/* macOS: pre-rendered appearance icons for instant light/dark switching */
8383
TRAY_API void tray_set_icons_for_appearance(struct tray *tray, const char *light_icon, const char *dark_icon);
8484

85+
/* macOS: space behavior for all windows */
86+
TRAY_API void tray_set_windows_move_to_active_space(void);
87+
88+
/* macOS: dock visibility */
89+
TRAY_API int tray_show_in_dock(void);
90+
TRAY_API int tray_hide_from_dock(void);
91+
92+
/* macOS: floating window / Space management */
93+
TRAY_API int tray_is_floating_window_on_active_space(void);
94+
TRAY_API int tray_bring_floating_window_to_front(void);
95+
TRAY_API int tray_set_move_to_active_space_for_view(void *nsViewPtr);
96+
TRAY_API int tray_is_on_active_space_for_view(void *nsViewPtr);
97+
98+
/* macOS: mouse button state */
99+
TRAY_API int tray_get_mouse_button_state(int button);
100+
85101
#ifdef __cplusplus
86102
} /* extern "C" */
87103
#endif

maclib/tray.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,4 +563,95 @@ public func tray_set_windows_move_to_active_space() {
563563
for window in NSApp.windows {
564564
window.collectionBehavior.insert(.moveToActiveSpace)
565565
}
566+
}
567+
568+
// MARK: - Dock visibility
569+
570+
@_cdecl("tray_show_in_dock")
571+
public func tray_show_in_dock() -> Int32 {
572+
let doWork = { () -> Int32 in
573+
NSApp.setActivationPolicy(.regular)
574+
return 0
575+
}
576+
if Thread.isMainThread { return doWork() }
577+
return DispatchQueue.main.sync { doWork() }
578+
}
579+
580+
@_cdecl("tray_hide_from_dock")
581+
public func tray_hide_from_dock() -> Int32 {
582+
let doWork = { () -> Int32 in
583+
NSApp.setActivationPolicy(.accessory)
584+
return 0
585+
}
586+
if Thread.isMainThread { return doWork() }
587+
return DispatchQueue.main.sync { doWork() }
588+
}
589+
590+
// MARK: - Floating window / Space management
591+
592+
@_cdecl("tray_is_floating_window_on_active_space")
593+
public func tray_is_floating_window_on_active_space() -> Int32 {
594+
let doWork = { () -> Int32 in
595+
for window in NSApp.windows {
596+
if window.level.rawValue > 0 {
597+
return window.isOnActiveSpace ? 1 : 0
598+
}
599+
}
600+
return 1 // No floating window found, assume on active Space
601+
}
602+
if Thread.isMainThread { return doWork() }
603+
return DispatchQueue.main.sync { doWork() }
604+
}
605+
606+
@_cdecl("tray_bring_floating_window_to_front")
607+
public func tray_bring_floating_window_to_front() -> Int32 {
608+
let doWork = { () -> Int32 in
609+
NSApp.activate(ignoringOtherApps: true)
610+
for window in NSApp.windows {
611+
if window.level.rawValue > 0 {
612+
window.makeKeyAndOrderFront(nil)
613+
return 0
614+
}
615+
}
616+
return -1 // No floating window found
617+
}
618+
if Thread.isMainThread { return doWork() }
619+
return DispatchQueue.main.sync { doWork() }
620+
}
621+
622+
@_cdecl("tray_set_move_to_active_space_for_view")
623+
public func tray_set_move_to_active_space_for_view(_ nsViewPtr: UnsafeMutableRawPointer?) -> Int32 {
624+
let doWork = { () -> Int32 in
625+
guard let viewPtr = nsViewPtr else { return -1 }
626+
let nsView = Unmanaged<NSView>.fromOpaque(viewPtr).takeUnretainedValue()
627+
guard let nsWindow = nsView.window else { return -1 }
628+
var behavior = nsWindow.collectionBehavior
629+
behavior.remove(.canJoinAllSpaces)
630+
behavior.insert(.moveToActiveSpace)
631+
nsWindow.collectionBehavior = behavior
632+
return 0
633+
}
634+
if Thread.isMainThread { return doWork() }
635+
return DispatchQueue.main.sync { doWork() }
636+
}
637+
638+
@_cdecl("tray_is_on_active_space_for_view")
639+
public func tray_is_on_active_space_for_view(_ nsViewPtr: UnsafeMutableRawPointer?) -> Int32 {
640+
let doWork = { () -> Int32 in
641+
guard let viewPtr = nsViewPtr else { return 1 } // fail-open
642+
let nsView = Unmanaged<NSView>.fromOpaque(viewPtr).takeUnretainedValue()
643+
guard let nsWindow = nsView.window else { return 1 }
644+
return nsWindow.isOnActiveSpace ? 1 : 0
645+
}
646+
if Thread.isMainThread { return doWork() }
647+
return DispatchQueue.main.sync { doWork() }
648+
}
649+
650+
// MARK: - Mouse button state
651+
652+
@_cdecl("tray_get_mouse_button_state")
653+
public func tray_get_mouse_button_state(_ button: Int32) -> Int32 {
654+
let cgButton = CGMouseButton(rawValue: UInt32(button))!
655+
let state = CGEventSource.buttonState(.combinedSessionState, button: cgButton)
656+
return state ? 1 : 0
566657
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.kdroid.composetray.lib.mac
2+
3+
import com.kdroid.composetray.utils.extractToTempIfDifferent
4+
import java.io.File
5+
6+
/**
7+
* JNI bridge to the native macOS tray library (libMacTray.dylib).
8+
* Replaces the previous JNA direct-mapping approach.
9+
* All methods are static JNI calls into MacTrayBridge.m.
10+
*/
11+
internal object MacNativeBridge {
12+
13+
init {
14+
loadNativeLibrary()
15+
}
16+
17+
private fun loadNativeLibrary() {
18+
val arch = System.getProperty("os.arch") ?: "aarch64"
19+
val resourceDir = when {
20+
arch.contains("aarch64") || arch.contains("arm64") -> "darwin-aarch64"
21+
else -> "darwin-x86-64"
22+
}
23+
val resourcePath = "$resourceDir/libMacTray.dylib"
24+
25+
// Try to find the dylib on the classpath (inside a JAR or on disk)
26+
val url = MacNativeBridge::class.java.classLoader?.getResource(resourcePath)
27+
if (url != null) {
28+
val protocol = url.protocol
29+
if (protocol == "jar") {
30+
// Extract from JAR to a temp file
31+
val tempFile = extractToTempIfDifferent(url.toString())
32+
if (tempFile != null) {
33+
System.load(tempFile.absolutePath)
34+
return
35+
}
36+
} else if (protocol == "file") {
37+
// Direct file on disk (development mode)
38+
val file = File(url.toURI())
39+
if (file.exists()) {
40+
System.load(file.absolutePath)
41+
return
42+
}
43+
}
44+
}
45+
46+
// Fallback: let the JVM find it on java.library.path
47+
System.loadLibrary("MacTray")
48+
}
49+
50+
// ── Tray lifecycle ──────────────────────────────────────────────────
51+
52+
@JvmStatic external fun nativeCreateTray(iconPath: String, tooltip: String): Long
53+
@JvmStatic external fun nativeFreeTray(handle: Long)
54+
@JvmStatic external fun nativeSetTrayIcon(handle: Long, iconPath: String)
55+
@JvmStatic external fun nativeSetTrayTooltip(handle: Long, tooltip: String)
56+
@JvmStatic external fun nativeSetTrayCallback(handle: Long, callback: Runnable?)
57+
@JvmStatic external fun nativeSetTrayMenu(trayHandle: Long, menuHandle: Long)
58+
@JvmStatic external fun nativeClearTrayMenu(trayHandle: Long)
59+
@JvmStatic external fun nativeInitTray(handle: Long): Int
60+
@JvmStatic external fun nativeLoopTray(blocking: Int): Int
61+
@JvmStatic external fun nativeUpdateTray(handle: Long)
62+
@JvmStatic external fun nativeDisposeTray(handle: Long)
63+
@JvmStatic external fun nativeExitTray()
64+
65+
// ── Menu items ──────────────────────────────────────────────────────
66+
67+
@JvmStatic external fun nativeCreateMenuItems(count: Int): Long
68+
@JvmStatic external fun nativeSetMenuItem(
69+
menuHandle: Long, index: Int,
70+
text: String, iconPath: String?,
71+
disabled: Int, checked: Int
72+
)
73+
@JvmStatic external fun nativeSetMenuItemCallback(menuHandle: Long, index: Int, callback: Runnable?)
74+
@JvmStatic external fun nativeSetMenuItemSubmenu(menuHandle: Long, index: Int, submenuHandle: Long)
75+
@JvmStatic external fun nativeFreeMenuItems(menuHandle: Long, count: Int)
76+
77+
// ── Theme ───────────────────────────────────────────────────────────
78+
79+
@JvmStatic external fun nativeSetThemeCallback(callback: ThemeChangeCallback?)
80+
@JvmStatic external fun nativeIsMenuDark(): Int
81+
82+
// ── Position ────────────────────────────────────────────────────────
83+
84+
/** Writes [x, y] into outXY. Returns 1 if precise, 0 if fallback. */
85+
@JvmStatic external fun nativeGetStatusItemPosition(outXY: IntArray): Int
86+
@JvmStatic external fun nativeGetStatusItemRegion(): String
87+
@JvmStatic external fun nativeGetStatusItemPositionFor(handle: Long, outXY: IntArray): Int
88+
@JvmStatic external fun nativeGetStatusItemRegionFor(handle: Long): String
89+
90+
// ── Appearance ──────────────────────────────────────────────────────
91+
92+
@JvmStatic external fun nativeSetIconsForAppearance(handle: Long, lightIcon: String, darkIcon: String)
93+
94+
// ── Window management ───────────────────────────────────────────────
95+
96+
@JvmStatic external fun nativeShowInDock(): Int
97+
@JvmStatic external fun nativeHideFromDock(): Int
98+
@JvmStatic external fun nativeSetMoveToActiveSpace()
99+
@JvmStatic external fun nativeSetMoveToActiveSpaceForWindow(viewPtr: Long): Int
100+
@JvmStatic external fun nativeIsFloatingWindowOnActiveSpace(): Int
101+
@JvmStatic external fun nativeBringFloatingWindowToFront(): Int
102+
@JvmStatic external fun nativeIsOnActiveSpaceForView(viewPtr: Long): Int
103+
104+
// ── Mouse ───────────────────────────────────────────────────────────
105+
106+
@JvmStatic external fun nativeGetMouseButtonState(button: Int): Int
107+
108+
// ── JAWT ────────────────────────────────────────────────────────────
109+
110+
/** Returns the NSView pointer for an AWT component, or 0 on failure. */
111+
@JvmStatic external fun nativeGetAWTViewPtr(awtComponent: Any): Long
112+
113+
// ── Callback interface ──────────────────────────────────────────────
114+
115+
interface ThemeChangeCallback {
116+
fun onThemeChanged(isDark: Int)
117+
}
118+
}
Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
package com.kdroid.composetray.lib.mac
22

3-
import com.sun.jna.Native
43
import java.util.function.Consumer
54
import java.util.concurrent.ConcurrentHashMap
65
import java.util.concurrent.Executors
7-
import com.kdroid.composetray.lib.mac.MacTrayManager.MacTrayLibrary
8-
9-
// Removed kermit Logger import and usage
10-
// private val logger = Logger.withTag("MacOSMenuBarThemeDetector")
116

127
object MacOSMenuBarThemeDetector {
138

14-
private val trayLib: MacTrayLibrary = MacTrayLoader.lib
15-
169
private val listeners: MutableSet<Consumer<Boolean>> = ConcurrentHashMap.newKeySet()
1710

1811
private val callbackExecutor = Executors.newSingleThreadExecutor { r ->
1912
Thread(r, "MacOS MenuBar Theme Detector Thread").apply { isDaemon = true }
2013
}
2114

22-
private val themeChangedCallback = object : MacTrayManager.ThemeCallback {
23-
override fun invoke(isDark: Int) {
15+
private val themeChangedCallback = object : MacNativeBridge.ThemeChangeCallback {
16+
override fun onThemeChanged(isDark: Int) {
2417
callbackExecutor.execute {
2518
val dark = isDark != 0
2619
notifyListeners(dark)
@@ -29,11 +22,11 @@ object MacOSMenuBarThemeDetector {
2922
}
3023

3124
init {
32-
trayLib.tray_set_theme_callback(themeChangedCallback)
25+
MacNativeBridge.nativeSetThemeCallback(themeChangedCallback)
3326
}
3427

3528
fun isDark(): Boolean {
36-
return trayLib.tray_is_menu_dark() != 0
29+
return MacNativeBridge.nativeIsMenuDark() != 0
3730
}
3831

3932
fun registerListener(listener: Consumer<Boolean>) {
@@ -49,4 +42,4 @@ object MacOSMenuBarThemeDetector {
4942
private fun notifyListeners(isDark: Boolean) {
5043
listeners.forEach { it.accept(isDark) }
5144
}
52-
}
45+
}

0 commit comments

Comments
 (0)