Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,34 @@ mavenPublishing {
signAllPublications()
}
}

val publishAllToMavenLocal by tasks.registering {
group = "publishing"
description = "Publishes all library modules to Maven Local."

dependsOn(tasks.named("publishToMavenLocal"))
}

gradle.projectsEvaluated {
publishAllToMavenLocal.configure {
dependsOn(subprojects.mapNotNull { it.tasks.findByName("publishToMavenLocal") })
}
}

tasks.register<Exec>("publishDevToMavenLocal") {
group = "publishing"
description = "Publishes all library modules to Maven Local with version 'dev'."

workingDir = rootDir
// The publish version is resolved from GITHUB_REF at configuration time, so
// re-invoke the build with it set to force version "dev" everywhere.
environment("GITHUB_REF", "refs/tags/vdev")

val gradlew =
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
listOf("cmd", "/c", rootDir.resolve("gradlew.bat").absolutePath)
} else {
listOf(rootDir.resolve("gradlew").absolutePath)
}
commandLine(gradlew + listOf("publishAllToMavenLocal", "--no-configuration-cache"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,36 @@ internal class MacTrayManager(
}
}

// Update a menu item's checked state
// Update a menu item's checked state (including items nested in submenus)
fun updateMenuItemCheckedState(
label: String,
isChecked: Boolean,
) {
lock.withLock {
val index = menuItems.indexOfFirst { it.text == label }
if (index != -1) {
menuItems[index] = menuItems[index].copy(isChecked = isChecked)
val patched = patchCheckedState(menuItems.toList(), label, isChecked)
if (patched != menuItems) {
menuItems.clear()
menuItems.addAll(patched)
// Recreate the menu to reflect changes
recreateMenu()
}
}
}

private fun patchCheckedState(
items: List<MenuItem>,
label: String,
checked: Boolean,
): List<MenuItem> =
items.map { item ->
when {
item.isCheckable && item.text == label -> item.copy(isChecked = checked)
item.subMenuItems.isNotEmpty() ->
item.copy(subMenuItems = patchCheckedState(item.subMenuItems, label, checked))
else -> item
}
}

// Update the tray with new properties and menu items
fun update(
newIconPath: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ internal class WindowsTrayManager(
private val updateQueue = mutableListOf<UpdateRequest>()
private val updateQueueLock = Object()

// Last applied menu structure + pending checked-state patches (processed on the tray thread)
private var currentMenuItems: List<MenuItem> = emptyList()
private val checkedUpdateQueue = mutableListOf<Pair<String, Boolean>>()

companion object {
private fun log(message: String) {
debugln { "[WindowsTrayManager] $message" }
Expand Down Expand Up @@ -75,6 +79,7 @@ internal class WindowsTrayManager(
}

running.set(true)
currentMenuItems = menuItems

// Create coroutine scopes
mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
Expand Down Expand Up @@ -167,6 +172,20 @@ internal class WindowsTrayManager(
}
}

/**
* Patches the checked state of a checkable item (matched by its full menu text) without
* requiring a full rebuild from the caller. Processed on the tray thread.
*/
fun updateMenuItemCheckedState(
text: String,
isChecked: Boolean,
) {
synchronized(updateQueueLock) {
checkedUpdateQueue.add(text to isChecked)
updateQueueLock.notify()
}
}

private fun runMessageLoop() {
log("Entering message loop on tray thread")
var consecutiveErrors = 0
Expand All @@ -188,6 +207,7 @@ internal class WindowsTrayManager(

// Check for pending updates
processUpdateQueue()
processCheckedUpdateQueue()

// Process Windows messages with non-blocking call
val result = WindowsNativeBridge.nativeLoopTray(0)
Expand Down Expand Up @@ -295,12 +315,54 @@ internal class WindowsTrayManager(
}
}

private fun processCheckedUpdateQueue() {
val updates =
synchronized(updateQueueLock) {
if (checkedUpdateQueue.isEmpty()) return
val copy = checkedUpdateQueue.toList()
checkedUpdateQueue.clear()
copy
}

val handle = trayHandle
if (handle == 0L) return

currentMenuItems =
updates.fold(currentMenuItems) { items, (text, checked) ->
patchCheckedState(items, text, checked)
}

log("Applying ${updates.size} checked-state patch(es)")
freeMenuHandles()
setupMenu(handle, currentMenuItems)
try {
WindowsNativeBridge.nativeUpdateTray(handle)
} catch (e: Throwable) {
log("Failed to apply checked-state patch: ${e.message}")
}
}

private fun patchCheckedState(
items: List<MenuItem>,
text: String,
checked: Boolean,
): List<MenuItem> =
items.map { item ->
when {
item.isCheckable && item.text == text -> item.copy(isChecked = checked)
item.subMenuItems.isNotEmpty() ->
item.copy(subMenuItems = patchCheckedState(item.subMenuItems, text, checked))
else -> item
}
}

private fun performUpdate(update: UpdateRequest) {
// Update properties
iconPath = update.iconPath
tooltip = update.tooltip
onLeftClick = update.onLeftClick
onMenuOpened = update.onMenuOpened
currentMenuItems = update.menuItems

val handle = trayHandle
if (handle == 0L) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dev.nucleusframework.composenativetray.utils.IconRenderProperties
import dev.nucleusframework.composenativetray.utils.isMenuBarInDarkMode
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

Expand Down Expand Up @@ -148,24 +149,22 @@ internal class LinuxTrayMenuBuilderImpl(
shortcut: KeyShortcut?,
) {
lock.withLock {
val initialChecked = checked
// Live state: clicking must toggle the current value, not the one captured
// when the menu was built (issue #432).
val liveChecked = AtomicBoolean(checked)

val menuItem =
LinuxTrayManager.MenuItem(
text = label,
isEnabled = isEnabled,
isCheckable = true,
isChecked = initialChecked,
isChecked = checked,
shortcut = shortcut,
onClick = {
lock.withLock {
val currentMenuItem = menuItems.find { it.text == label }
val currentChecked = currentMenuItem?.isChecked ?: initialChecked
val newChecked = !currentChecked

onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(label, newChecked)
}
val newChecked = !liveChecked.get()
liveChecked.set(newChecked)
onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(label, newChecked)
},
)
menuItems.add(menuItem)
Expand All @@ -185,25 +184,21 @@ internal class LinuxTrayMenuBuilderImpl(
lock.withLock {
val iconPath = ComposableIconUtils.renderComposableToPngFile(iconRenderProperties, iconContent)

val initialChecked = checked
val liveChecked = AtomicBoolean(checked)

val menuItem =
LinuxTrayManager.MenuItem(
text = label,
isEnabled = isEnabled,
isCheckable = true,
isChecked = initialChecked,
isChecked = checked,
iconPath = iconPath,
shortcut = shortcut,
onClick = {
lock.withLock {
val currentMenuItem = menuItems.find { it.text == label }
val currentChecked = currentMenuItem?.isChecked ?: initialChecked
val newChecked = !currentChecked

onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(label, newChecked)
}
val newChecked = !liveChecked.get()
liveChecked.set(newChecked)
onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(label, newChecked)
},
)
menuItems.add(menuItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dev.nucleusframework.composenativetray.utils.IconRenderProperties
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

Expand Down Expand Up @@ -148,6 +149,9 @@ internal class MacTrayMenuBuilderImpl(
shortcut: KeyShortcut?,
) {
lock.withLock {
// Live state: clicking must toggle the current value, not the one captured
// when the menu was built (issue #432).
val liveChecked = AtomicBoolean(checked)
val menuItem =
MacTrayManager.MenuItem(
text = label,
Expand All @@ -157,10 +161,10 @@ internal class MacTrayMenuBuilderImpl(
isChecked = checked,
shortcut = shortcut,
onClick = {
lock.withLock {
val newChecked = !checked
onCheckedChange(newChecked)
}
val newChecked = !liveChecked.get()
liveChecked.set(newChecked)
onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(label, newChecked)
},
)
menuItems.add(menuItem)
Expand All @@ -180,6 +184,7 @@ internal class MacTrayMenuBuilderImpl(
lock.withLock {
val iconPath = ComposableIconUtils.renderComposableToPngFile(iconRenderProperties, iconContent)

val liveChecked = AtomicBoolean(checked)
val menuItem =
MacTrayManager.MenuItem(
text = label,
Expand All @@ -189,10 +194,10 @@ internal class MacTrayMenuBuilderImpl(
isChecked = checked,
shortcut = shortcut,
onClick = {
lock.withLock {
val newChecked = !checked
onCheckedChange(newChecked)
}
val newChecked = !liveChecked.get()
liveChecked.set(newChecked)
onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(label, newChecked)
},
)
menuItems.add(menuItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import dev.nucleusframework.composenativetray.utils.IconRenderProperties
import dev.nucleusframework.composenativetray.utils.isMenuBarInDarkMode
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

internal class WindowsTrayMenuBuilderImpl(
private val iconPath: String,
private val tooltip: String = "",
private val onLeftClick: (() -> Unit)?,
private val trayManager: WindowsTrayManager? = null,
) : TrayMenuBuilder {
private val menuItems = mutableListOf<WindowsTrayManager.MenuItem>()
private val lock = ReentrantLock()
Expand Down Expand Up @@ -146,16 +148,22 @@ internal class WindowsTrayMenuBuilderImpl(
shortcut: KeyShortcut?,
) {
lock.withLock {
val text = label.withShortcut(shortcut)
// Live state: clicking must toggle the current value, not the one captured
// when the menu was built (issue #432).
val liveChecked = AtomicBoolean(checked)
val menuItem =
WindowsTrayManager.MenuItem(
text = label.withShortcut(shortcut),
text = text,
iconPath = null,
isEnabled = isEnabled,
isCheckable = true,
isChecked = checked,
onClick = {
val newChecked = !checked
val newChecked = !liveChecked.get()
liveChecked.set(newChecked)
onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(text, newChecked)
},
)
menuItems.add(menuItem)
Expand All @@ -175,16 +183,20 @@ internal class WindowsTrayMenuBuilderImpl(
lock.withLock {
val iconPath = ComposableIconUtils.renderComposableToIcoFile(iconRenderProperties, iconContent)

val text = label.withShortcut(shortcut)
val liveChecked = AtomicBoolean(checked)
val menuItem =
WindowsTrayManager.MenuItem(
text = label.withShortcut(shortcut),
text = text,
iconPath = iconPath,
isEnabled = isEnabled,
isCheckable = true,
isChecked = checked,
onClick = {
val newChecked = !checked
val newChecked = !liveChecked.get()
liveChecked.set(newChecked)
onCheckedChange(newChecked)
trayManager?.updateMenuItemCheckedState(text, newChecked)
},
)
menuItems.add(menuItem)
Expand Down Expand Up @@ -356,6 +368,7 @@ internal class WindowsTrayMenuBuilderImpl(
this.iconPath,
tooltip,
onLeftClick = onLeftClick,
trayManager = trayManager,
).apply(submenuContent)
subMenuItems.addAll(subMenuImpl.menuItems)
}
Expand Down
Loading
Loading