Skip to content

Commit fc851cc

Browse files
authored
feat(desktop): control macOS dock icon visibility (#1053)
1 parent 5b0f46c commit fc851cc

File tree

10 files changed

+159
-14
lines changed

10 files changed

+159
-14
lines changed

.github/workflows/build_native_libraries.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ jobs:
2323
- name: Checkout repository
2424
uses: actions/checkout@v4
2525

26-
- name: Setup
27-
uses: ./.github/actions/setup
26+
- name: Set up Java
27+
uses: actions/setup-java@v4
28+
with:
29+
distribution: 'temurin'
30+
java-version: '17'
2831

2932
- name: Install dependencies (Linux)
3033
if: runner.os == 'Linux'

buildSrc/src/main/kotlin/TaskRegistration.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,10 @@ private fun Project.registerDesktopTasks() {
5050
dependsOn("setupSparkle")
5151
workingDir = file("src/desktopMain")
5252
commandLine = listOf("make", "all")
53-
doFirst {
54-
println("🔨 Building native libraries...")
55-
}
56-
doLast {
57-
println("✅ Native libraries built successfully")
58-
}
53+
54+
inputs.files(fileTree("src/desktopMain/c") {
55+
include("*.m", "*.c")
56+
})
5957
}
6058

6159
tasks.register("cleanLibrary", Exec::class) {

composeApp/build.gradle.kts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,6 @@ compose.desktop {
385385
entitlementsFile.set(project.file("OONIProbe.entitlements"))
386386
infoPlist {
387387
extraKeysRawXml = """
388-
<key>LSUIElement</key>
389-
<string>true</string>
390388
<key>CFBundleURLTypes</key>
391389
<array>
392390
<dict>

composeApp/src/desktopMain/Makefile

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
# Makefile for NetworkTypeFinder and UpdateBridge JNI Bridges
1+
# Makefile for NetworkTypeFinder, UpdateBridge, and MacDockVisibility JNI Bridges
22

33
# Variables
44
COMPILER = clang
55
NETWORK_FILES = c/NetworkTypeFinder.m
66
UPDATE_FILES_MAC = c/UpdateBridge.c c/SparkeBridge.m
77
UPDATE_FILES_WIN = c/UpdateBridge.c c/WinSparkleBridge.c
8+
MACDOCK_FILES = c/MacDockVisibility.m
89
NETWORK_LIBRARY_NAME = networktypefinder
910
UPDATE_LIBRARY_NAME = updatebridge
11+
MACDOCK_LIBRARY_NAME = macdockvisibility
1012
NETWORK_LIBRARY_FILE_MAC = lib$(NETWORK_LIBRARY_NAME).dylib
1113
NETWORK_LIBRARY_FILE_LINUX = lib$(NETWORK_LIBRARY_NAME).so
1214
UPDATE_LIBRARY_FILE_MAC = lib$(UPDATE_LIBRARY_NAME).dylib
1315
UPDATE_LIBRARY_FILE_WIN = $(UPDATE_LIBRARY_NAME).dll
16+
MACDOCK_LIBRARY_FILE_MAC = lib$(MACDOCK_LIBRARY_NAME).dylib
1417
JAVA_HOME ?= $(shell java -XshowSettings:properties -version 2>&1 > /dev/null | grep 'java.home' | awk '{print $$3}')
1518
RESOURCES_BASE_DIR = resources
1619

@@ -64,13 +67,22 @@ macos:
6467
# Fix Sparkle framework reference to use @loader_path
6568
install_name_tool -change "@rpath/Sparkle.framework/Versions/B/Sparkle" "@loader_path/Sparkle.framework/Versions/B/Sparkle" $(RESOURCES_BASE_DIR)/macos/$(UPDATE_LIBRARY_FILE_MAC)
6669
@echo "UpdateBridge library created at $(RESOURCES_BASE_DIR)/macos/$(UPDATE_LIBRARY_FILE_MAC)"
70+
@echo "Compiling MacDockVisibility for macOS..."
71+
@mkdir -p $(RESOURCES_BASE_DIR)/macos
72+
$(COMPILER) -dynamiclib -o $(RESOURCES_BASE_DIR)/macos/$(MACDOCK_LIBRARY_FILE_MAC) $(MACDOCK_FILES) \
73+
-framework Cocoa \
74+
-framework Foundation \
75+
-I$(JAVA_HOME)/include \
76+
-I$(JAVA_HOME)/include/darwin
77+
@echo "MacDockVisibility library created at $(RESOURCES_BASE_DIR)/macos/$(MACDOCK_LIBRARY_FILE_MAC)"
6778

6879
linux:
6980
@echo "Compiling NetworkTypeFinder for Linux..."
7081
@mkdir -p $(RESOURCES_BASE_DIR)/linux
7182
$(COMPILER) -shared -fPIC -o $(RESOURCES_BASE_DIR)/linux/$(NETWORK_LIBRARY_FILE_LINUX) $(NETWORK_FILES) -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux
7283
@echo "NetworkTypeFinder library created at $(RESOURCES_BASE_DIR)/linux/$(NETWORK_LIBRARY_FILE_LINUX)"
7384
@echo "UpdateBridge not supported on Linux"
85+
@echo "MacDockVisibility not supported on Linux"
7486

7587
windows:
7688
@echo "Compiling NetworkTypeFinder for Windows..."
@@ -80,13 +92,15 @@ windows:
8092
@echo "Compiling UpdateBridge for Windows..."
8193
$(COMPILER) -shared -m64 -o $(RESOURCES_BASE_DIR)/windows/$(UPDATE_LIBRARY_FILE_WIN) $(UPDATE_FILES_WIN) '-I$(JAVA_HOME)\\include' '-I$(JAVA_HOME)\\include\\win32'
8294
@echo "UpdateBridge library created at $(RESOURCES_BASE_DIR)/windows/$(UPDATE_LIBRARY_FILE_WIN)"
95+
@echo "MacDockVisibility not supported on Windows"
8396

8497
# Install the library to system path
8598
install: all
8699
@echo "Installing libraries to system path..."
87100
ifeq ($(PLATFORM_TARGET),macos)
88101
cp $(RESOURCES_BASE_DIR)/macos/$(NETWORK_LIBRARY_FILE_MAC) $(HOME)/Library/Java/Extensions
89102
cp $(RESOURCES_BASE_DIR)/macos/$(UPDATE_LIBRARY_FILE_MAC) $(HOME)/Library/Java/Extensions
103+
cp $(RESOURCES_BASE_DIR)/macos/$(MACDOCK_LIBRARY_FILE_MAC) $(HOME)/Library/Java/Extensions
90104
else ifeq ($(PLATFORM_TARGET),linux)
91105
sudo cp $(RESOURCES_BASE_DIR)/linux/$(NETWORK_LIBRARY_FILE_LINUX) /usr/lib/
92106
else ifeq ($(PLATFORM_TARGET),windows)
@@ -112,17 +126,18 @@ clean:
112126
@echo "Clean complete"
113127

114128
help:
115-
@echo "NetworkTypeFinder and UpdateBridge JNI Bridges Makefile"
129+
@echo "NetworkTypeFinder, UpdateBridge, and MacDockVisibility JNI Bridges Makefile"
116130
@echo ""
117131
@echo "Targets:"
118-
@echo " all (default): Compile both libraries for the current platform"
132+
@echo " all (default): Compile all libraries for the current platform"
119133
@echo " install: Install the libraries to system path for the current platform"
120134
@echo " clean: Clean build artifacts"
121135
@echo " help: Show this help message"
122136
@echo ""
123137
@echo "Libraries built:"
124138
@echo " NetworkTypeFinder: Network type detection"
125139
@echo " UpdateBridge: Sparkle (macOS) / WinSparkle (Windows) integration"
140+
@echo " MacDockVisibility: macOS dock icon visibility control"
126141
@echo ""
127142
@echo "Example usage:"
128143
@echo " make all # Compile the libraries for your platform"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include <jni.h>
2+
#import <Cocoa/Cocoa.h>
3+
4+
// JNI function to show the app in dock (NSApp.setActivationPolicy(.regular))
5+
JNIEXPORT void JNICALL Java_org_ooni_probe_shared_MacDockVisibility_showInDockNative
6+
(JNIEnv *env, jclass cls) {
7+
@autoreleasepool {
8+
dispatch_async(dispatch_get_main_queue(), ^{
9+
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
10+
NSLog(@"MacDockVisibility: Set activation policy to Regular (show in dock)");
11+
});
12+
}
13+
}
14+
15+
// JNI function to remove the app from dock (NSApp.setActivationPolicy(.accessory))
16+
JNIEXPORT void JNICALL Java_org_ooni_probe_shared_MacDockVisibility_removeFromDockNative
17+
(JNIEnv *env, jclass cls) {
18+
@autoreleasepool {
19+
dispatch_async(dispatch_get_main_queue(), ^{
20+
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
21+
NSLog(@"MacDockVisibility: Set activation policy to Accessory (remove from dock)");
22+
});
23+
}
24+
}

composeApp/src/desktopMain/kotlin/org/ooni/probe/Main.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import org.ooni.probe.shared.DeepLinkParser
4949
import org.ooni.probe.shared.DesktopOS
5050
import org.ooni.probe.shared.createUpdateManager
5151
import org.ooni.probe.shared.InstanceManager
52+
import org.ooni.probe.shared.MacDockVisibility
5253
import org.ooni.probe.shared.Platform
5354
import org.ooni.probe.shared.UpdateState
5455
import org.ooni.probe.update.DesktopUpdateController
@@ -85,6 +86,9 @@ fun main(args: Array<String>) {
8586
// Register shutdown callback for update installation when applicable
8687
updateController.registerShutdownHandler(this)
8788
var isWindowVisible by remember { mutableStateOf(!autoLaunch.isStartedViaAutostart()) }
89+
90+
// Set initial dock visibility based on window visibility
91+
MacDockVisibility.setDockIconVisible(isWindowVisible)
8892
val trayIcon = trayIcon()
8993
val deepLink by deepLinkFlow.collectAsState(null)
9094
val runBackgroundState by dependencies.runBackgroundStateManager
@@ -97,6 +101,7 @@ fun main(args: Array<String>) {
97101

98102
fun showWindow() {
99103
isWindowVisible = true
104+
MacDockVisibility.showDockIcon()
100105
if (Desktop.isDesktopSupported() &&
101106
Desktop
102107
.getDesktop()
@@ -107,7 +112,10 @@ fun main(args: Array<String>) {
107112
}
108113

109114
Window(
110-
onCloseRequest = { isWindowVisible = false },
115+
onCloseRequest = {
116+
isWindowVisible = false
117+
MacDockVisibility.hideDockIcon()
118+
},
111119
visible = isWindowVisible,
112120
icon = painterResource(Res.drawable.ooni_colored_logo),
113121
title = stringResource(Res.string.app_name),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package org.ooni.probe.shared
2+
3+
import co.touchlab.kermit.Logger
4+
import org.ooni.probe.platform
5+
import org.ooni.shared.loadNativeLibrary
6+
7+
/**
8+
* Utility for controlling macOS dock icon visibility using JNI.
9+
* Calls native macOS functions through NSApp.setActivationPolicy.
10+
*/
11+
object MacDockVisibility {
12+
private var isNativeLibraryLoaded = false
13+
14+
init {
15+
if (isMacOS()) {
16+
loadNativeLibrary()
17+
}
18+
}
19+
20+
/**
21+
* Shows the dock icon (makes app visible in dock)
22+
* Calls native: NSApp.setActivationPolicy(.regular)
23+
*/
24+
fun showDockIcon() {
25+
if (!isMacOS()) {
26+
Logger.d("MacDockVisibility: Not on macOS, skipping dock icon show")
27+
return
28+
}
29+
30+
if (!isNativeLibraryLoaded) {
31+
Logger.w("MacDockVisibility: Native library not loaded, cannot show dock icon")
32+
return
33+
}
34+
35+
try {
36+
showInDockNative()
37+
Logger.d("MacDockVisibility: Successfully showed dock icon via JNI")
38+
} catch (e: Exception) {
39+
Logger.w("MacDockVisibility: Failed to show dock icon via JNI", e)
40+
}
41+
}
42+
43+
/**
44+
* Hides the dock icon (makes app invisible in dock)
45+
* Calls native: NSApp.setActivationPolicy(.accessory)
46+
*/
47+
fun hideDockIcon() {
48+
if (!isMacOS()) {
49+
Logger.d("MacDockVisibility: Not on macOS, skipping dock icon hide")
50+
return
51+
}
52+
53+
if (!isNativeLibraryLoaded) {
54+
Logger.w("MacDockVisibility: Native library not loaded, cannot hide dock icon")
55+
return
56+
}
57+
58+
try {
59+
removeFromDockNative()
60+
Logger.d("MacDockVisibility: Successfully hid dock icon via JNI")
61+
} catch (e: Exception) {
62+
Logger.w("MacDockVisibility: Failed to hide dock icon via JNI", e)
63+
}
64+
}
65+
66+
/**
67+
* Sets the dock icon visibility based on window visibility
68+
*/
69+
fun setDockIconVisible(visible: Boolean) {
70+
if (visible) {
71+
showDockIcon()
72+
} else {
73+
hideDockIcon()
74+
}
75+
}
76+
77+
private fun isMacOS(): Boolean = platform.os == DesktopOS.Mac
78+
79+
private fun loadNativeLibrary() {
80+
try {
81+
isNativeLibraryLoaded = loadNativeLibrary("macdockvisibility")
82+
if (isNativeLibraryLoaded) {
83+
Logger.d("MacDockVisibility: Successfully loaded native library")
84+
} else {
85+
Logger.w("MacDockVisibility: Failed to load native library")
86+
}
87+
} catch (e: Exception) {
88+
Logger.w("MacDockVisibility: Exception while loading native library", e)
89+
isNativeLibraryLoaded = false
90+
}
91+
}
92+
93+
// JNI native method declarations
94+
@JvmStatic
95+
private external fun showInDockNative()
96+
97+
@JvmStatic
98+
private external fun removeFromDockNative()
99+
}
Binary file not shown.
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)