Skip to content

Add MacDockToggler to hide/show Dock icon at runtime with focus handling #710

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 16, 2025

Conversation

amirroid
Copy link
Contributor

@amirroid amirroid commented Jul 13, 2025

Hi! To completely hide the icon, if it already exists in the Dock, you need to manually right-click it and select "Remove from Dock".

Related to #592

@amir1376
Copy link
Owner

Hi @amirroid thank you for this PR. I'm not very familiar with the macOS ecosystem, so I have a few questions:

  • What happens if a user pins the app to the dock (if its like Windows taskbar)? Will it disappear?
  • Is hiding the dock icon a common practice among macOS apps?
  • Is keeping it always active acceptable among macOS users, or should we make this behavior optional (e.g., via a setting)?
  • Could you share some screenshots to help visualize the difference?

Thanks!

@amirroid
Copy link
Contributor Author

Hi @amirroid thank you for this PR. I'm not very familiar with the macOS ecosystem, so I have a few questions:

  • What happens if a user pins the app to the dock (if its like Windows taskbar)? Will it disappear?
  • Is hiding the dock icon a common practice among macOS apps?
  • Is keeping it always active acceptable among macOS users, or should we make this behavior optional (e.g., via a setting)?
  • Could you share some screenshots to help visualize the difference?

Thanks!

Hi,
If the user pins the app, when the app opens and the tray is active, the icon stays in the dock but it doesn’t show the “app is open” state (meaning the little dot under the icon that indicates it’s running doesn’t appear).
I’ve seen some apps do exactly this.
I don’t think hiding the dock icon is a bad thing because there’s the system tray anyway, and the user can also choose to run it without the tray and with the icon if they want.

One more thing:
If the app was installed before and already appears in the dock as a recent app, the first case I mentioned happens.

@amir1376 amir1376 requested a review from Copilot July 15, 2025 06:34
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a new MacDockToggler implementation to hide and show the macOS Dock icon at runtime (with focus handling), replaces the old activator binding with a unified FoundationLibrary interface, wires the toggler into the UI, and updates the app packaging to run as an LSUIElement app.

  • Introduce FoundationLibrary interface and remove the old Foundation file.
  • Implement MacDockToggler (formerly “Hider”) using JNA’s NativeLibrary.
  • Hook PlatformDockToggler.hide()/show() into the Compose UI system tray logic.
  • Add LSUIElement key in infoPlist to enable Dock-less execution.

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
desktop/shared/src/main/kotlin/ir/amirab/util/desktop/utils/mac/FoundationLibrary.kt New unified JNA interface for Objective-C runtime calls
desktop/shared/src/main/kotlin/ir/amirab/util/desktop/dock/mac/MacDockHider.kt Implement MacDockToggler object (file/name mismatch)
desktop/shared/src/main/kotlin/ir/amirab/util/desktop/activator/mac/MacAppActivator.kt Swap old Foundation for new FoundationLibrary
desktop/shared/src/main/kotlin/ir/amirab/util/desktop/activator/mac/Foundation.kt Removed obsolete Foundation interface file
desktop/shared/src/main/kotlin/ir/amirab/util/desktop/PlatformDockHider.kt Define PlatformDockToggler but file is named “Hider”
desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt Call PlatformDockToggler.hide()/show() via LaunchedEffect
desktop/app/build.gradle.kts Add LSUIElement key in infoPlist for Dock-less mode
Comments suppressed due to low confidence (3)

desktop/shared/src/main/kotlin/ir/amirab/util/desktop/dock/mac/MacDockHider.kt:1

  • The filename MacDockHider.kt does not match the object MacDockToggler defined inside. Consider renaming the file to MacDockToggler.kt for consistency.
package ir.amirab.util.desktop.dock.mac

desktop/shared/src/main/kotlin/ir/amirab/util/desktop/PlatformDockHider.kt:1

  • This file defines PlatformDockToggler but is named PlatformDockHider.kt. Rename it to PlatformDockToggler.kt to avoid confusion.
package ir.amirab.util.desktop

desktop/shared/src/main/kotlin/ir/amirab/util/desktop/dock/mac/MacDockHider.kt:30

  • [nitpick] The hideAndKeepFocus logic involves OS interop and state changes—consider adding a unit or integration test (with mocking) to verify that it calls the correct selectors.
    private fun hideAndKeepFocus() {

Comment on lines 3 to 20
import com.sun.jna.NativeLibrary
import com.sun.jna.Pointer
import ir.amirab.util.desktop.PlatformDockToggler

object MacDockToggler : PlatformDockToggler {
private val objc: NativeLibrary = NativeLibrary.getInstance("objc")

private val getClass = objc.getFunction("objc_getClass")
private val getSel = objc.getFunction("sel_registerName")
private val msgSend = objc.getFunction("objc_msgSend")

private val nsAppClass: Pointer = getClass.invokePointer(arrayOf("NSApplication"))
private val sharedAppSel: Pointer = getSel.invokePointer(arrayOf("sharedApplication"))
private val setPolicySel: Pointer = getSel.invokePointer(arrayOf("setActivationPolicy:"))

private val nsRunningAppClass: Pointer = getClass.invokePointer(arrayOf("NSRunningApplication"))
private val currentAppSel: Pointer = getSel.invokePointer(arrayOf("currentApplication"))
private val activateSel: Pointer = getSel.invokePointer(arrayOf("activateWithOptions:"))
Copy link
Preview

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] You’re mixing two patterns for JNA (NativeLibrary in MacDockToggler vs. Native.load in MacAppActivator). Consider standardizing on one approach (e.g., always use FoundationLibrary) to reduce duplicated interop code.

Suggested change
import com.sun.jna.NativeLibrary
import com.sun.jna.Pointer
import ir.amirab.util.desktop.PlatformDockToggler
object MacDockToggler : PlatformDockToggler {
private val objc: NativeLibrary = NativeLibrary.getInstance("objc")
private val getClass = objc.getFunction("objc_getClass")
private val getSel = objc.getFunction("sel_registerName")
private val msgSend = objc.getFunction("objc_msgSend")
private val nsAppClass: Pointer = getClass.invokePointer(arrayOf("NSApplication"))
private val sharedAppSel: Pointer = getSel.invokePointer(arrayOf("sharedApplication"))
private val setPolicySel: Pointer = getSel.invokePointer(arrayOf("setActivationPolicy:"))
private val nsRunningAppClass: Pointer = getClass.invokePointer(arrayOf("NSRunningApplication"))
private val currentAppSel: Pointer = getSel.invokePointer(arrayOf("currentApplication"))
private val activateSel: Pointer = getSel.invokePointer(arrayOf("activateWithOptions:"))
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.Library
import ir.amirab.util.desktop.PlatformDockToggler
object MacDockToggler : PlatformDockToggler {
private val foundation: FoundationLibrary = Native.load("objc", FoundationLibrary::class.java)
private val nsAppClass: Pointer = foundation.objc_getClass("NSApplication")
private val sharedAppSel: Pointer = foundation.sel_registerName("sharedApplication")
private val setPolicySel: Pointer = foundation.sel_registerName("setActivationPolicy:")
private val nsAppClass: Pointer = getClass.invokePointer(arrayOf("NSApplication"))
private val sharedAppSel: Pointer = getSel.invokePointer(arrayOf("sharedApplication"))
private val setPolicySel: Pointer = getSel.invokePointer(arrayOf("setActivationPolicy:"))
private val nsRunningAppClass: Pointer = foundation.objc_getClass("NSRunningApplication")
private val currentAppSel: Pointer = foundation.sel_registerName("currentApplication")
private val activateSel: Pointer = foundation.sel_registerName("activateWithOptions:")

Copilot uses AI. Check for mistakes.

@amirroid amirroid requested a review from amir1376 July 15, 2025 21:44
@amir1376 amir1376 merged commit f5b730d into amir1376:master Jul 16, 2025
@amir1376
Copy link
Owner

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants