From 9a813fbbaf7347d602e59b046ba9eba3cce00e19 Mon Sep 17 00:00:00 2001 From: JFronny Date: Sat, 20 Jul 2024 20:25:13 +0200 Subject: [PATCH] feat: get the first menu to show --- build.gradle.kts | 29 ++++-- settings.gradle.kts | 2 +- .../io/gitlab/jfronny/globalmenu/DPair.java | 30 ++++++ .../jfronny/globalmenu/GlobalMenuService.kt | 24 ++--- .../io/gitlab/jfronny/globalmenu/Reflect.kt | 10 +- .../jfronny/globalmenu/proxy/DbusmenuImpl.kt | 91 +++++++++++-------- .../gitlab/jfronny/globalmenu/proxy/Menu.kt | 15 +++ .../jfronny/globalmenu/proxy/MenuHolder.kt | 5 + .../jfronny/globalmenu/proxy/SwingMenu.kt | 88 ++++++++++++++++++ .../globalmenu/proxy/SwingMenuHolder.kt | 26 ++++++ .../jfronny/globalmenu/proxy/SwingRootMenu.kt | 19 ++++ 11 files changed, 273 insertions(+), 66 deletions(-) create mode 100644 src/main/java/io/gitlab/jfronny/globalmenu/DPair.java create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/MenuHolder.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenu.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingRootMenu.kt diff --git a/build.gradle.kts b/build.gradle.kts index bde2e25..238140d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,8 @@ dependencies { } extraResources(project(mapOf("path" to ":native", "configuration" to "results"))) implementation("io.gitlab.jfronny:commons-unsafe:2.0.0-SNAPSHOT") - implementation("com.github.hypfvieh:dbus-java-core:5.0.0") +// implementation("com.github.hypfvieh:dbus-java-core:5.0.0") +// implementation("com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0") } val copyExtraResources by tasks.creating(Copy::class) { @@ -66,25 +67,26 @@ abstract class InterfaceGenerateTask : DefaultTask() { false, introspectionData, objectPath.get(), - busName.get(), - null, - true + busName.get() ) val analyze = generator.analyze(true)!! if (analyze.isEmpty()) throw IllegalStateException("No interfaces found") @OptIn(ExperimentalPathApi::class) output.deleteRecursively() output.createDirectories() - val regex = Regex("List, ([^_\\n]+)>") - val memory = LinkedHashMap() + val illegalStruct = Regex("List, ([^_\\n]+)>") + val illegalTuple = Regex("public ([A-Za-z]+Tuple) ") + val fieldPattern = Regex("@Position\\(\\d+\\)\\r?\\n +private (.+) [a-zA-Z]+;") + val structMemory = LinkedHashMap() + val tupleMemory = LinkedHashMap() for (entry in analyze) { - if (entry.key.path.equals("/.java")) continue // Skip incorrectly generated file + if (entry.key.path.equals("/.java") || entry.key.path.endsWith("Tuple.java")) continue // Skip incorrectly generated file val pth = output.resolve(entry.key.path.trimStart('/')) pth.createParentDirs() // Fix the incorrect generic type - Files.writeString(pth, entry.value.replace(regex) { match -> - memory.computeIfAbsent(match.groups[1]!!.value) { type -> - val name = "Struct${memory.size + 1}" + Files.writeString(pth, entry.value.replace(illegalStruct) { match -> + structMemory.computeIfAbsent(match.groups[1]!!.value) { type -> + val name = "Struct${structMemory.size + 1}" Files.writeString(output.resolve("com/canonical").resolve("$name.java"), """ package com.canonical; @@ -107,6 +109,13 @@ abstract class InterfaceGenerateTask : DefaultTask() { """.trimIndent()) name } + }.replace(illegalTuple) { match -> + tupleMemory.computeIfAbsent(match.groups[1]!!.value) { type -> + val impl = analyze[analyze.keys.first { it.path.contains(type) }]!! + val found = fieldPattern.findAll(impl).toList() + if (found.size != 2) throw IllegalStateException("Tuple must have exactly two fields") + "io.gitlab.jfronny.globalmenu.DPair<${found[0].groups[1]!!.value}, ${found[1].groups[1]!!.value}>" + } }) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index ffc87c7..b7efa14 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,7 +10,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.github.hypfvieh:dbus-java-utils:5.0.0") + classpath("com.github.hypfvieh:dbus-java-utils:4.3.2") } } diff --git a/src/main/java/io/gitlab/jfronny/globalmenu/DPair.java b/src/main/java/io/gitlab/jfronny/globalmenu/DPair.java new file mode 100644 index 0000000..06db20c --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/globalmenu/DPair.java @@ -0,0 +1,30 @@ +package io.gitlab.jfronny.globalmenu; + +import org.freedesktop.dbus.Tuple; +import org.freedesktop.dbus.annotations.Position; + +public class DPair extends Tuple { + @Position(0) private A a; + @Position(1) private B b; + + public DPair(A a, B b) { + this.a = a; + this.b = b; + } + + public void setA(A a) { + this.a = a; + } + + public A getA() { + return a; + } + + public void setB(B b) { + this.b = b; + } + + public B getB() { + return b; + } +} diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt index 3b5db97..7b799b6 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt @@ -1,6 +1,5 @@ package io.gitlab.jfronny.globalmenu -import com.canonical.Dbusmenu import com.canonical.appmenu.Registrar import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationActivationListener @@ -8,6 +7,7 @@ import com.intellij.openapi.wm.IdeFrame import com.intellij.openapi.wm.impl.IdeFrameImpl import com.intellij.openapi.wm.impl.ProjectFrameHelper import io.gitlab.jfronny.globalmenu.proxy.DbusmenuImpl +import io.gitlab.jfronny.globalmenu.proxy.SwingMenuHolder import org.freedesktop.dbus.DBusPath import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder import org.freedesktop.dbus.types.UInt32 @@ -20,8 +20,6 @@ class GlobalMenuService(private val app: Application) : ApplicationActivationLis ideFrame.project?.let { project -> println(ideFrame.javaClass) if (ideFrame is ProjectFrameHelper) { -// ideFrame.frame.jMenuBar = JMenuBar() -// ideFrame.rootPane.jMenuBar visualize(ideFrame.rootPane.jMenuBar, ideFrame.rootPane.peer) } else if (ideFrame is IdeFrameImpl) { visualize(ideFrame.jMenuBar, ideFrame.peer) @@ -31,29 +29,21 @@ class GlobalMenuService(private val app: Application) : ApplicationActivationLis } fun visualize(menu: JMenuBar, peer: Peer) { - GlobalMenu.Log.warn("Using peer: $peer") - //TODO send to compositor - for (i in 0 until menu.menuCount) { - val submenu = menu.getMenu(i) - for (j in 0 until submenu.itemCount) { - val component = submenu.getItem(j) - GlobalMenu.Log.warn("${submenu.text}.${component?.text}") - } - } val conn = DBusConnectionBuilder.forSessionBus().build() - val menu: Dbusmenu = DbusmenuImpl() - // firefox seems to use mObjectPath(nsPrintfCString("/com/canonical/menu/%u", sID++)) - conn.exportObject("/com/canonical/dbusmenu", menu) + val windowPtr = peer.nativePtr + val menu = DbusmenuImpl(windowPtr, SwingMenuHolder(menu, "DBusMenuRoot")) + conn.unExportObject(menu.objectPath) + conn.exportObject(menu) if (peer is WLPeer) { peer.performLocked { - val ptr = GlobalMenu.Native.create(peer.nativePtr) + val ptr = GlobalMenu.Native.create(windowPtr) GlobalMenu.Native.setAddress(ptr, conn.uniqueName, menu.objectPath) } } else { val registrar = conn.getRemoteObject("org.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar", Registrar::class.java) - registrar.RegisterWindow(UInt32(peer.nativePtr), DBusPath(menu.objectPath)) + registrar.RegisterWindow(UInt32(windowPtr), DBusPath(menu.objectPath)) } } } \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/Reflect.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/Reflect.kt index bccaaa2..60f6bf5 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/Reflect.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/Reflect.kt @@ -19,11 +19,14 @@ sealed interface Peer { val nativePtr: Long } fun Peer(peer: Any): Peer { - if (X11Peer.componentPeerClass.isInstance(peer)) return X11Peer(peer) - if (WLPeer.componentPeerClass.isInstance(peer)) return WLPeer(peer) + if (peer.javaClass.name.contains("X11")) return X11Peer(peer) + if (peer.javaClass.name.contains("WL")) return WLPeer(peer) throw IllegalArgumentException("Unknown peer type: ${peer.javaClass}") } class X11Peer(private val inner: Any) : Peer { + init { + componentPeerClass.cast(inner) + } override val nativePtr: Long get() = getPtrMethod(inner) companion object { @@ -32,6 +35,9 @@ class X11Peer(private val inner: Any) : Peer { } } class WLPeer(private val inner: Any) : Peer { + init { + componentPeerClass.cast(inner) + } override val nativePtr: Long get() = nativePtrField.getLong(inner) fun performLocked(runnable: Runnable) { performLockedMethod(inner, runnable) diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/DbusmenuImpl.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/DbusmenuImpl.kt index c1ab4e9..6b58ce7 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/DbusmenuImpl.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/DbusmenuImpl.kt @@ -1,58 +1,77 @@ package io.gitlab.jfronny.globalmenu.proxy import com.canonical.* +import io.gitlab.jfronny.globalmenu.DPair import org.freedesktop.dbus.types.UInt32 import org.freedesktop.dbus.types.Variant -class DbusmenuImpl : Dbusmenu { - override fun getObjectPath(): String { - TODO("Not yet implemented") +class DbusmenuImpl(windowId: Long, private val menuHolder: MenuHolder) : Dbusmenu { + private val menuPath: String = "/com/canonical/menu0x${windowId.toString(16)}" + override fun getObjectPath(): String = menuPath +// override fun getVersion(): UInt32 = UInt32(3) +// override fun getTextDirection(): String = "none" +// override fun getStatus(): String = "normal" +// +// override fun getIconThemePath(): Dbusmenu.PropertyIconThemePathType = +// object : Dbusmenu.PropertyIconThemePathType, List by listOf() {} + + override fun GetLayout(parentId: Int, recursionDepth: Int, propertyNames: MutableList?): DPair { + return DPair( + UInt32(parentId.toLong()), + getLayout(parentId, recursionDepth, propertyNames, menuHolder.find(parentId)!!) + ) } - override fun getVersion(): UInt32 { - TODO("Not yet implemented") - } + private fun getLayout(parentId: Int, recursionDepth: Int, propertyNames: MutableList?, menu: Menu): GetLayoutStruct { + val properties = readProperties(menu) + val children = mutableListOf>() - override fun getTextDirection(): String { - TODO("Not yet implemented") - } + menu.children?.let { + for (sm in it) { + children.add(Variant(getLayout(parentId, recursionDepth, propertyNames, sm))) + } + if (it.isNotEmpty()) properties["children-display"] = Variant("submenu") + } - override fun getStatus(): String { - TODO("Not yet implemented") - } - - override fun getIconThemePath(): Dbusmenu.PropertyIconThemePathType { - TODO("Not yet implemented") - } - - override fun GetLayout(parentId: Int, recursionDepth: Int, propertyNames: MutableList?): GetLayoutTuple { - TODO("Not yet implemented") + return GetLayoutStruct(menu.id, properties, children) } override fun GetGroupProperties( - ids: MutableList?, + ids: MutableList, propertyNames: MutableList? - ): MutableList { - TODO("Not yet implemented") + ): MutableList = mutableListOf().apply { + ids.forEach { id -> + menuHolder.find(id)?.let { menu -> + add(GetGroupPropertiesStruct(id, readProperties(menu))) + } + } } - override fun GetProperty(id: Int, name: String?): Variant<*> { - TODO("Not yet implemented") + override fun GetProperty(id: Int, name: String?): Variant<*>? = menuHolder.find(id)?.let { menu -> readProperties(menu)[name] } + + private fun readProperties(menu: Menu): MutableMap> { + if (menu.isSeparator) return mutableMapOf("type" to Variant("separator")) + val properties = mutableMapOf>() + properties["type"] = Variant("standard") + properties["label"] = Variant(menu.label) + properties["visible"] = Variant(menu.isVisible) + properties["enabled"] = Variant(menu.isEnabled) + if (!menu.shortcut.isNullOrEmpty()) properties["shortcut"] = Variant(menu.shortcut) + if (!menu.toggleType.isNullOrEmpty()) { + properties["toggle-type"] = Variant(menu.toggleType) + properties["toggle-state"] = Variant(menu.toggleState) + } + if (menu.iconData?.isNotEmpty() == true) properties["icon-data"] = Variant(menu.iconData) + return properties } override fun Event(id: Int, eventId: String?, data: Variant<*>?, timestamp: UInt32?) { - TODO("Not yet implemented") + if ("clicked".endsWith(eventId!!)) { //TODO this seems off + menuHolder.find(id)?.onEvent() + } } - override fun EventGroup(events: MutableList?): MutableList { - TODO("Not yet implemented") - } - - override fun AboutToShow(id: Int): Boolean { - TODO("Not yet implemented") - } - - override fun AboutToShowGroup(ids: MutableList?): AboutToShowGroupTuple { - TODO("Not yet implemented") - } + override fun EventGroup(events: MutableList?): MutableList? = null // not needed? + override fun AboutToShow(id: Int): Boolean = true // not needed? + override fun AboutToShowGroup(ids: MutableList?): DPair, MutableList>? = null // not needed? } diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt new file mode 100644 index 0000000..f5ebb91 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt @@ -0,0 +1,15 @@ +package io.gitlab.jfronny.globalmenu.proxy + +interface Menu { + val id: Int + val isSeparator: Boolean + val label: String + val isEnabled: Boolean + val isVisible: Boolean + val iconData: ByteArray? + val shortcut: Array? + val toggleType: String? + val toggleState: Int + val children: List? + fun onEvent() +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/MenuHolder.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/MenuHolder.kt new file mode 100644 index 0000000..2e83452 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/MenuHolder.kt @@ -0,0 +1,5 @@ +package io.gitlab.jfronny.globalmenu.proxy + +interface MenuHolder { + fun find(menuId: Int): Menu? +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenu.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenu.kt new file mode 100644 index 0000000..83481f5 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenu.kt @@ -0,0 +1,88 @@ +package io.gitlab.jfronny.globalmenu.proxy + +import io.gitlab.jfronny.globalmenu.GlobalMenu +import org.apache.commons.io.output.ByteArrayOutputStream +import java.awt.event.ActionEvent +import java.awt.event.InputEvent +import java.awt.event.KeyEvent +import java.awt.image.BufferedImage +import java.lang.reflect.Modifier +import java.util.* +import javax.imageio.ImageIO +import javax.swing.JCheckBoxMenuItem +import javax.swing.JMenu +import javax.swing.JMenuItem +import javax.swing.JRadioButtonMenuItem + +class SwingMenu(private val menuItem: JMenuItem?, private val holder: SwingMenuHolder) : Menu { + override val id = holder.getId(menuItem) + override val isSeparator: Boolean get() = menuItem == null + override val label: String get() = menuItem?.text ?: "" + override val isEnabled: Boolean get() = menuItem?.isEnabled ?: false + override val isVisible: Boolean get() = menuItem?.isVisible ?: false + override val iconData: ByteArray? get() = menuItem?.icon?.let { icon -> + val width = icon.iconWidth + val height = icon.iconHeight + + val bufferedImage = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + + bufferedImage.createGraphics().apply { + icon.paintIcon(menuItem, this, 0, 0) + dispose() + } + + try { + ByteArrayOutputStream().use { stream -> + ImageIO.write(bufferedImage, "png", stream) + stream.toByteArray() + } + } catch (e: Exception) { + GlobalMenu.Log.error("Failed to convert icon to byte array", e) + null + } + } + + override val shortcut: Array? get() = menuItem?.accelerator?.let { ks -> + val s = getModifiersText(ks.modifiers) + val vk = keyEvents[ks.keyCode] ?: "UNKNOWN" + val l = mutableListOf() + val st = StringTokenizer(s) + while (st.hasMoreTokens()) l.add(st.nextToken()) + l.add(vk) + l.toTypedArray() + } + + private fun getModifiersText(modifiers: Int): String = buildString { + if (modifiers and InputEvent.SHIFT_DOWN_MASK != 0) append("Shift ") + if (modifiers and InputEvent.CTRL_DOWN_MASK != 0) append("Ctrl ") + if (modifiers and InputEvent.META_DOWN_MASK != 0) append("Meta ") + if (modifiers and InputEvent.ALT_DOWN_MASK != 0) append("Alt ") + if (modifiers and InputEvent.ALT_GRAPH_DOWN_MASK != 0) append("AltGraph ") + if (modifiers and InputEvent.BUTTON1_DOWN_MASK != 0) append("Button1 ") + if (modifiers and InputEvent.BUTTON2_DOWN_MASK != 0) append("Button2 ") + if (modifiers and InputEvent.BUTTON3_DOWN_MASK != 0) append("Button3 ") + } + + override val toggleType: String? get() = when (menuItem) { + is JRadioButtonMenuItem -> "radio" + is JCheckBoxMenuItem -> "checkmark" + else -> null + } + override val toggleState: Int get() = if (toggleType?.isNotEmpty() == true) if (menuItem!!.isSelected) 1 else 0 else -1 + override val children: List? get() = if (menuItem is JMenu) { + (0 until menuItem.itemCount).map { SwingMenu(menuItem.getItem(it), holder) } + } else null + + override fun onEvent() { + val event = ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, menuItem!!.actionCommand) + for (it in menuItem.actionListeners) it.actionPerformed(event) + if (menuItem is JCheckBoxMenuItem) menuItem.isSelected = !menuItem.isSelected + if (menuItem is JRadioButtonMenuItem) menuItem.isSelected = true + } + + companion object { + val keyEvents: Map = KeyEvent::class.java.fields + .filter { it.modifiers == Modifier.PUBLIC or Modifier.STATIC or Modifier.FINAL && it.name.startsWith("VK_") } + .associate { it.getInt(null) to it.name.substring(3) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt new file mode 100644 index 0000000..ffbbceb --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt @@ -0,0 +1,26 @@ +package io.gitlab.jfronny.globalmenu.proxy + +import javax.swing.JMenuBar +import javax.swing.JMenuItem + +class SwingMenuHolder(bar: JMenuBar, menuName: String): MenuHolder { + private val root = SwingRootMenu((0 until bar.menuCount).map { bar.getMenu(it) }, menuName, this) + + override fun find(menuId: Int): Menu? { + if (menuId == 0) return root + return find(root, menuId) + } + + private fun find(parent: Menu, menuId: Int): Menu? { + parent.children?.forEach { + if (it.id == menuId) return it + val found = find(it, menuId) + if (found != null) return found + } + return null + } + + fun getId(menuItem: JMenuItem?): Int { + return System.identityHashCode(menuItem) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingRootMenu.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingRootMenu.kt new file mode 100644 index 0000000..906d898 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingRootMenu.kt @@ -0,0 +1,19 @@ +package io.gitlab.jfronny.globalmenu.proxy + +import javax.swing.JMenuItem + +class SwingRootMenu(private val menuItems: List?, private val name: String, private val holder: SwingMenuHolder): Menu { + override val id: Int get() = 0 + override val isSeparator: Boolean get() = menuItems == null + override val label: String get() = name + override val isEnabled: Boolean get() = true + override val isVisible: Boolean get() = true + override val iconData: ByteArray? get() = null + override val shortcut: Array? get() = null + override val toggleType: String? get() = null + override val toggleState: Int get() = 0 + override val children: List? get() = menuItems?.map { SwingMenu(it, holder) } + + override fun onEvent() { + } +}