From ab513fb0e90d5a3efdfdd789d64ef649255c3810 Mon Sep 17 00:00:00 2001 From: JFronny Date: Sat, 20 Jul 2024 22:54:25 +0200 Subject: [PATCH] feat: semi-working state --- .../jfronny/globalmenu/GlobalMenuService.kt | 61 ++++++++++++--- .../jfronny/globalmenu/proxy/DbusmenuImpl.kt | 14 +++- .../gitlab/jfronny/globalmenu/proxy/Menu.kt | 1 + .../jfronny/globalmenu/proxy/SwingMenu.kt | 74 +++++++++++++++++-- .../globalmenu/proxy/SwingMenuHolder.kt | 21 ++++-- .../jfronny/globalmenu/proxy/SwingRootMenu.kt | 23 +++++- 6 files changed, 165 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt index 7b799b6..7825c02 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt @@ -1,38 +1,77 @@ package io.gitlab.jfronny.globalmenu import com.canonical.appmenu.Registrar +import com.intellij.ide.IdeEventQueue +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.impl.ActionMenu import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationActivationListener +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.IdeFrame import com.intellij.openapi.wm.impl.IdeFrameImpl import com.intellij.openapi.wm.impl.ProjectFrameHelper +import com.intellij.platform.ide.menu.IdeJMenuBar import io.gitlab.jfronny.globalmenu.proxy.DbusmenuImpl import io.gitlab.jfronny.globalmenu.proxy.SwingMenuHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking import org.freedesktop.dbus.DBusPath +import org.freedesktop.dbus.connections.impl.DBusConnection import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder import org.freedesktop.dbus.types.UInt32 +import java.awt.Window +import java.awt.event.KeyEvent import javax.swing.JMenuBar +import javax.swing.SwingUtilities class GlobalMenuService(private val app: Application) : ApplicationActivationListener { override fun applicationActivated(ideFrame: IdeFrame) { super.applicationActivated(ideFrame) if (!GlobalMenu.Native.isSupported) return - ideFrame.project?.let { project -> - println(ideFrame.javaClass) - if (ideFrame is ProjectFrameHelper) { - visualize(ideFrame.rootPane.jMenuBar, ideFrame.rootPane.peer) - } else if (ideFrame is IdeFrameImpl) { - visualize(ideFrame.jMenuBar, ideFrame.peer) - } - GlobalMenu.Log.warn("Activated: ${project.name}") + if (ideFrame is ProjectFrameHelper) { + visualize(ideFrame.rootPane.jMenuBar, ideFrame.rootPane.peer) + } else if (ideFrame is IdeFrameImpl) { + visualize(ideFrame.jMenuBar, ideFrame.peer) } } - fun visualize(menu: JMenuBar, peer: Peer) { - val conn = DBusConnectionBuilder.forSessionBus().build() + override fun applicationDeactivated(ideFrame: IdeFrame) { + lastMenu?.let { Disposer.dispose(it) } + } + + private var lastMenu: Disposable? = null + private var connection: DBusConnection? = null + + fun visualize(menu: JMenuBar, peer: Peer) = app.invokeLater { + lastMenu?.let { Disposer.dispose(it) } + lastMenu = Disposer.newDisposable() + + val conn = connection ?: DBusConnectionBuilder.forSessionBus().build() + connection = conn + + val menuHolder = SwingMenuHolder(menu, "DBusMenuRoot") + if (menu is IdeJMenuBar) { + menu.addUpdateGlobalMenuRootsListener { + menuHolder.update(menu.rootMenuItems) + } + menu.updateMenuActions(true) + } +// IdeEventQueue.getInstance().addDispatcher({ e -> +// if (e !is KeyEvent) false +// else if (!e.isAltDown) false +// else { +// val src = e.component +// val wndParent = if (src is Window) src else SwingUtilities.windowForComponent(src) +// val eventChar = e.keyChar.uppercaseChar() +// +// +// true +// } +// }, lastMenu!!) val windowPtr = peer.nativePtr - val menu = DbusmenuImpl(windowPtr, SwingMenuHolder(menu, "DBusMenuRoot")) + val menu = DbusmenuImpl(windowPtr, menuHolder) conn.unExportObject(menu.objectPath) conn.exportObject(menu) 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 6b58ce7..91c8c73 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/DbusmenuImpl.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/DbusmenuImpl.kt @@ -2,6 +2,7 @@ package io.gitlab.jfronny.globalmenu.proxy import com.canonical.* import io.gitlab.jfronny.globalmenu.DPair +import io.gitlab.jfronny.globalmenu.GlobalMenu import org.freedesktop.dbus.types.UInt32 import org.freedesktop.dbus.types.Variant @@ -65,9 +66,16 @@ class DbusmenuImpl(windowId: Long, private val menuHolder: MenuHolder) : Dbusmen return properties } - override fun Event(id: Int, eventId: String?, data: Variant<*>?, timestamp: UInt32?) { - if ("clicked".endsWith(eventId!!)) { //TODO this seems off - menuHolder.find(id)?.onEvent() + override fun Event(id: Int, eventId: String, data: Variant<*>?, timestamp: UInt32?) { + GlobalMenu.Log.warn("Event $eventId for menu $id (${menuHolder.find(id)})") + try { + when (eventId) { + "clicked" -> menuHolder.find(id)?.onEvent() + "opened" -> menuHolder.find(id)?.update() + } + } catch (e: Exception) { + GlobalMenu.Log.error("Failed to handle event $eventId for menu $id", e) + throw e } } diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt index f5ebb91..f9ce387 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/Menu.kt @@ -12,4 +12,5 @@ interface Menu { val toggleState: Int val children: List? fun onEvent() + fun update() } \ 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 index 83481f5..1ed112d 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenu.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenu.kt @@ -1,6 +1,11 @@ package io.gitlab.jfronny.globalmenu.proxy +import com.intellij.openapi.actionSystem.impl.ActionMenu +import com.intellij.openapi.actionSystem.impl.ActionMenuItem +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import io.gitlab.jfronny.globalmenu.GlobalMenu +import kotlinx.coroutines.* import org.apache.commons.io.output.ByteArrayOutputStream import java.awt.event.ActionEvent import java.awt.event.InputEvent @@ -13,6 +18,7 @@ import javax.swing.JCheckBoxMenuItem import javax.swing.JMenu import javax.swing.JMenuItem import javax.swing.JRadioButtonMenuItem +import javax.swing.JSeparator class SwingMenu(private val menuItem: JMenuItem?, private val holder: SwingMenuHolder) : Menu { override val id = holder.getId(menuItem) @@ -64,20 +70,76 @@ class SwingMenu(private val menuItem: JMenuItem?, private val holder: SwingMenuH } override val toggleType: String? get() = when (menuItem) { + is ActionMenuItem -> { + //TODO handle action items + if (menuItem.isToggleable) "checkmark" + else null + } 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 + private var _children: List? = null + override val children: List? get() = _children 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 + ApplicationManager.getApplication().invokeLater { + for (it in menuItem.actionListeners) it.actionPerformed(event) + } + menuItem.doClick() + GlobalMenu.Log.warn("Event $event for menu $id (${menuItem.javaClass})") + when (menuItem) { + is JCheckBoxMenuItem -> menuItem.isSelected = !menuItem.isSelected + is JRadioButtonMenuItem -> menuItem.isSelected = true + } + } + + override fun update() { + try { + if (menuItem is ActionMenu) { + runBlocking { + launch(Dispatchers.EDT) { + menuItem.removeAll() + menuItem.isSelected = true + menuItem.fillMenu() + syncChildren(2) + } + } + } + } catch (e: Exception) { + GlobalMenu.Log.error("Failed to update menu", e) + throw e + } + } + + fun syncChildren(deepness: Int) { + GlobalMenu.Log.warn("Syncing children for $label") + _children = when (menuItem) { + is ActionMenu -> { + val ch = mutableListOf() + for (each in menuItem.popupMenu.components) { + if (each == null) continue + if (each is JSeparator) { + ch.add(SwingMenu(null, holder)) + continue + } + if (each !is JMenuItem) continue + val cmi = SwingMenu(each, holder) + if (deepness > 1 && each is ActionMenu) { + each.removeAll() + each.isSelected = true + each.fillMenu() + cmi.syncChildren(deepness - 1) + } + ch.add(cmi) + } + ch + } + is JMenu -> (0 until menuItem.itemCount).map { SwingMenu(menuItem.getItem(it), holder) } + else -> null + } } companion object { diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt index ffbbceb..b379ac9 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingMenuHolder.kt @@ -1,5 +1,7 @@ package io.gitlab.jfronny.globalmenu.proxy +import com.intellij.openapi.actionSystem.impl.ActionMenu +import com.intellij.openapi.actionSystem.impl.ActionMenuItem import javax.swing.JMenuBar import javax.swing.JMenuItem @@ -12,15 +14,20 @@ class SwingMenuHolder(bar: JMenuBar, menuName: String): MenuHolder { } 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 parent.children?.let { children -> + for (child in children) { + if (child.id == menuId) return child + val found = find(child, menuId) + if (found != null) return found + } + null } - return null } - fun getId(menuItem: JMenuItem?): Int { - return System.identityHashCode(menuItem) + fun getId(menuItem: JMenuItem?): Int = when (menuItem) { + is ActionMenu -> menuItem.anAction.toString().hashCode() + is ActionMenuItem -> menuItem.anAction.toString().hashCode() + else -> System.identityHashCode(menuItem) } + fun update(items: List) = root.update(items) } \ 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 index 906d898..f34c2af 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingRootMenu.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/proxy/SwingRootMenu.kt @@ -1,8 +1,9 @@ package io.gitlab.jfronny.globalmenu.proxy +import com.intellij.openapi.actionSystem.impl.ActionMenu import javax.swing.JMenuItem -class SwingRootMenu(private val menuItems: List?, private val name: String, private val holder: SwingMenuHolder): Menu { +class SwingRootMenu(private var 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 @@ -12,8 +13,26 @@ class SwingRootMenu(private val menuItems: List?, private val name: S 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) } + private var _children: List? = null + override val children: List? get() = _children override fun onEvent() { } + + override fun update() { + syncChildren() + } + + fun update(items: List) { + menuItems = items + syncChildren() + } + + init { + syncChildren() + } + + private fun syncChildren() { + _children = menuItems?.map { SwingMenu(it, holder).apply { syncChildren(1) } } + } }