Skip to content

Commit 17aeb12

Browse files
authored
Merge pull request #375 from kdroidFilter/refactor/macos-jna-to-jni
Replace JNA with JNI for macOS native bridge
2 parents 0b8e842 + b1961fa commit 17aeb12

13 files changed

Lines changed: 1050 additions & 554 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: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,102 @@ public func tray_set_icons_for_appearance(
560560
/// switching back to the Space where it was originally created.
561561
@_cdecl("tray_set_windows_move_to_active_space")
562562
public func tray_set_windows_move_to_active_space() {
563-
for window in NSApp.windows {
564-
window.collectionBehavior.insert(.moveToActiveSpace)
563+
let doWork = {
564+
for window in NSApp.windows {
565+
window.collectionBehavior.insert(.moveToActiveSpace)
566+
}
565567
}
568+
if Thread.isMainThread { doWork() }
569+
else { DispatchQueue.main.sync { doWork() } }
570+
}
571+
572+
// MARK: - Dock visibility
573+
574+
@_cdecl("tray_show_in_dock")
575+
public func tray_show_in_dock() -> Int32 {
576+
let doWork = { () -> Int32 in
577+
NSApp.setActivationPolicy(.regular)
578+
return 0
579+
}
580+
if Thread.isMainThread { return doWork() }
581+
return DispatchQueue.main.sync { doWork() }
582+
}
583+
584+
@_cdecl("tray_hide_from_dock")
585+
public func tray_hide_from_dock() -> Int32 {
586+
let doWork = { () -> Int32 in
587+
NSApp.setActivationPolicy(.accessory)
588+
return 0
589+
}
590+
if Thread.isMainThread { return doWork() }
591+
return DispatchQueue.main.sync { doWork() }
592+
}
593+
594+
// MARK: - Floating window / Space management
595+
596+
@_cdecl("tray_is_floating_window_on_active_space")
597+
public func tray_is_floating_window_on_active_space() -> Int32 {
598+
let doWork = { () -> Int32 in
599+
for window in NSApp.windows {
600+
if window.level.rawValue > 0 {
601+
return window.isOnActiveSpace ? 1 : 0
602+
}
603+
}
604+
return 1 // No floating window found, assume on active Space
605+
}
606+
if Thread.isMainThread { return doWork() }
607+
return DispatchQueue.main.sync { doWork() }
608+
}
609+
610+
@_cdecl("tray_bring_floating_window_to_front")
611+
public func tray_bring_floating_window_to_front() -> Int32 {
612+
let doWork = { () -> Int32 in
613+
NSApp.activate(ignoringOtherApps: true)
614+
for window in NSApp.windows {
615+
if window.level.rawValue > 0 {
616+
window.makeKeyAndOrderFront(nil)
617+
return 0
618+
}
619+
}
620+
return -1 // No floating window found
621+
}
622+
if Thread.isMainThread { return doWork() }
623+
return DispatchQueue.main.sync { doWork() }
624+
}
625+
626+
@_cdecl("tray_set_move_to_active_space_for_view")
627+
public func tray_set_move_to_active_space_for_view(_ nsViewPtr: UnsafeMutableRawPointer?) -> Int32 {
628+
let doWork = { () -> Int32 in
629+
guard let viewPtr = nsViewPtr else { return -1 }
630+
let nsView = Unmanaged<NSView>.fromOpaque(viewPtr).takeUnretainedValue()
631+
guard let nsWindow = nsView.window else { return -1 }
632+
var behavior = nsWindow.collectionBehavior
633+
behavior.remove(.canJoinAllSpaces)
634+
behavior.insert(.moveToActiveSpace)
635+
nsWindow.collectionBehavior = behavior
636+
return 0
637+
}
638+
if Thread.isMainThread { return doWork() }
639+
return DispatchQueue.main.sync { doWork() }
640+
}
641+
642+
@_cdecl("tray_is_on_active_space_for_view")
643+
public func tray_is_on_active_space_for_view(_ nsViewPtr: UnsafeMutableRawPointer?) -> Int32 {
644+
let doWork = { () -> Int32 in
645+
guard let viewPtr = nsViewPtr else { return 1 } // fail-open
646+
let nsView = Unmanaged<NSView>.fromOpaque(viewPtr).takeUnretainedValue()
647+
guard let nsWindow = nsView.window else { return 1 }
648+
return nsWindow.isOnActiveSpace ? 1 : 0
649+
}
650+
if Thread.isMainThread { return doWork() }
651+
return DispatchQueue.main.sync { doWork() }
652+
}
653+
654+
// MARK: - Mouse button state
655+
656+
@_cdecl("tray_get_mouse_button_state")
657+
public func tray_get_mouse_button_state(_ button: Int32) -> Int32 {
658+
let cgButton = CGMouseButton(rawValue: UInt32(button))!
659+
let state = CGEventSource.buttonState(.combinedSessionState, button: cgButton)
660+
return state ? 1 : 0
566661
}
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)