feat: get the first menu to show

This commit is contained in:
Johannes Frohnmeyer 2024-07-20 20:25:13 +02:00
parent 184ca19652
commit 9a813fbbaf
Signed by: Johannes
GPG Key ID: E76429612C2929F4
11 changed files with 273 additions and 66 deletions

View File

@ -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<org\\.freedesktop\\.dbus\\.Struct<Integer>, ([^_\\n]+)>")
val memory = LinkedHashMap<String, String>()
val illegalStruct = Regex("List<org\\.freedesktop\\.dbus\\.Struct<Integer>, ([^_\\n]+)>")
val illegalTuple = Regex("public ([A-Za-z]+Tuple) ")
val fieldPattern = Regex("@Position\\(\\d+\\)\\r?\\n +private (.+) [a-zA-Z]+;")
val structMemory = LinkedHashMap<String, String>()
val tupleMemory = LinkedHashMap<String, String>()
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}>"
}
})
}
}

View File

@ -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")
}
}

View File

@ -0,0 +1,30 @@
package io.gitlab.jfronny.globalmenu;
import org.freedesktop.dbus.Tuple;
import org.freedesktop.dbus.annotations.Position;
public class DPair<A, B> 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;
}
}

View File

@ -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))
}
}
}

View File

@ -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)

View File

@ -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<String> by listOf() {}
override fun GetLayout(parentId: Int, recursionDepth: Int, propertyNames: MutableList<String>?): DPair<UInt32, GetLayoutStruct> {
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<String>?, menu: Menu): GetLayoutStruct {
val properties = readProperties(menu)
val children = mutableListOf<Variant<*>>()
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<String>?): GetLayoutTuple {
TODO("Not yet implemented")
return GetLayoutStruct(menu.id, properties, children)
}
override fun GetGroupProperties(
ids: MutableList<Int>?,
ids: MutableList<Int>,
propertyNames: MutableList<String>?
): MutableList<GetGroupPropertiesStruct> {
TODO("Not yet implemented")
): MutableList<GetGroupPropertiesStruct> = mutableListOf<GetGroupPropertiesStruct>().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<String, Variant<*>> {
if (menu.isSeparator) return mutableMapOf("type" to Variant("separator"))
val properties = mutableMapOf<String, Variant<*>>()
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<EventGroupStruct>?): MutableList<Int> {
TODO("Not yet implemented")
}
override fun AboutToShow(id: Int): Boolean {
TODO("Not yet implemented")
}
override fun AboutToShowGroup(ids: MutableList<Int>?): AboutToShowGroupTuple {
TODO("Not yet implemented")
}
override fun EventGroup(events: MutableList<EventGroupStruct>?): MutableList<Int>? = null // not needed?
override fun AboutToShow(id: Int): Boolean = true // not needed?
override fun AboutToShowGroup(ids: MutableList<Int>?): DPair<MutableList<Int>, MutableList<Int>>? = null // not needed?
}

View File

@ -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<String>?
val toggleType: String?
val toggleState: Int
val children: List<Menu>?
fun onEvent()
}

View File

@ -0,0 +1,5 @@
package io.gitlab.jfronny.globalmenu.proxy
interface MenuHolder {
fun find(menuId: Int): Menu?
}

View File

@ -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<String>? get() = menuItem?.accelerator?.let { ks ->
val s = getModifiersText(ks.modifiers)
val vk = keyEvents[ks.keyCode] ?: "UNKNOWN"
val l = mutableListOf<String>()
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<Menu>? 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<Int, String> = 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) }
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.globalmenu.proxy
import javax.swing.JMenuItem
class SwingRootMenu(private val menuItems: List<JMenuItem>?, 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<String>? get() = null
override val toggleType: String? get() = null
override val toggleState: Int get() = 0
override val children: List<Menu>? get() = menuItems?.map { SwingMenu(it, holder) }
override fun onEvent() {
}
}