From 9c7ca4aaa1589f2196a069a0dbcb8fdbc5e0f81d Mon Sep 17 00:00:00 2001 From: JFronny Date: Fri, 19 Jul 2024 21:25:04 +0200 Subject: [PATCH] feat: initial experimentation --- .gitignore | 40 ++++++++++ .run/Run IDE with Plugin.run.xml | 24 ++++++ README.md | 3 + build.gradle.kts | 76 ++++++++++++++++++ gradle.properties | 6 ++ native/build.gradle.kts | 78 +++++++++++++++++++ native/src/main/c/native.c | 67 ++++++++++++++++ native/src/main/protocols/appmenu.xml | 35 +++++++++ settings.gradle.kts | 10 +++ .../io/gitlab/jfronny/globalmenu/Native.java | 26 +++++++ .../gitlab/jfronny/globalmenu/GlobalMenu.kt | 9 +++ .../jfronny/globalmenu/GlobalMenuService.kt | 43 ++++++++++ .../globalmenu/InitializationComponent.kt | 9 +++ .../io/gitlab/jfronny/globalmenu/Lambda.kt | 14 ++++ .../io/gitlab/jfronny/globalmenu/Reflect.kt | 38 +++++++++ src/main/resources/META-INF/plugin.xml | 32 ++++++++ src/main/resources/META-INF/pluginIcon.svg | 12 +++ 17 files changed, 522 insertions(+) create mode 100644 .gitignore create mode 100644 .run/Run IDE with Plugin.run.xml create mode 100644 README.md create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 native/build.gradle.kts create mode 100644 native/src/main/c/native.c create mode 100644 native/src/main/protocols/appmenu.xml create mode 100644 settings.gradle.kts create mode 100644 src/main/java/io/gitlab/jfronny/globalmenu/Native.java create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenu.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/Lambda.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/globalmenu/Reflect.kt create mode 100644 src/main/resources/META-INF/plugin.xml create mode 100644 src/main/resources/META-INF/pluginIcon.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d32f8ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ +.intellijPlatform + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.run/Run IDE with Plugin.run.xml b/.run/Run IDE with Plugin.run.xml new file mode 100644 index 0000000..7747a29 --- /dev/null +++ b/.run/Run IDE with Plugin.run.xml @@ -0,0 +1,24 @@ + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..06e39c8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# References +- [the removal commit](https://github.com/JetBrains/intellij-community/commit/336265215c1ae9bf9fd7f9c23ebfabc8fc810743) +- [a swing menu library](https://github.com/Vitaliy-Yakovchuk/dbusmenu-swing) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..47180d5 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + java + kotlin("jvm") version "1.9.24" + id("org.jetbrains.intellij.platform") version "2.0.0-beta9" +} + +group = "io.gitlab.jfronny" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.frohnmeyer-wds.de/artifacts") + + intellijPlatform { + defaultRepositories() + } +} + +val extraResources by configurations.creating + +dependencies { + intellijPlatform { + intellijIdeaCommunity("242.20224.91") + instrumentationTools() + } + implementation("io.gitlab.jfronny:commons-unsafe:2.0.0-SNAPSHOT") + extraResources(project(mapOf("path" to ":native", "configuration" to "results"))) +} + +val copyExtraResources by tasks.creating(Copy::class) { + from(extraResources) + into(layout.buildDirectory.dir("extraResources")) +} + +sourceSets { + main { + resources { + this.srcDir(copyExtraResources) + } + } +} + +abstract class RunToolTask : AbstractExecTask(RunToolTask::class.java) { + @get:InputFile abstract val inputFile: RegularFileProperty + @get:OutputFile abstract val outputFile: RegularFileProperty +} + +tasks { + // Set the JVM compatibility versions + withType { + sourceCompatibility = "21" + targetCompatibility = "21" + } + withType { + kotlinOptions.jvmTarget = "21" + } + + patchPluginXml { + sinceBuild.set("242") + untilBuild.set("243.*") + } + + signPlugin { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } + + publishPlugin { + token.set(System.getenv("PUBLISH_TOKEN")) + } + + runIde { + this.jvmArgs("-Dawt.toolkit.name=WLToolkit") + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e1c1990 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib +kotlin.stdlib.default.dependency=false +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache=false +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching=true diff --git a/native/build.gradle.kts b/native/build.gradle.kts new file mode 100644 index 0000000..e75bc3c --- /dev/null +++ b/native/build.gradle.kts @@ -0,0 +1,78 @@ +import org.gradle.internal.jvm.Jvm + +plugins { + `cpp-library` +} + +group = rootProject.group +version = rootProject.version + +abstract class RunToolTask : AbstractExecTask(RunToolTask::class.java) { + @get:InputFile abstract val inputFile: RegularFileProperty + @get:OutputFile abstract val outputFile: RegularFileProperty +} + +library { + binaries.configureEach { + val compileTask = compileTask.get() + val dir = "${Jvm.current().javaHome}/include" + compileTask.includes.from(dir) + + val osFamily = targetPlatform.targetMachine.operatingSystemFamily + if (osFamily.isMacOs) compileTask.includes.from("$dir/darwin") + else if (osFamily.isLinux) compileTask.includes.from("$dir/linux") + else if (osFamily.isWindows) compileTask.includes.from("$dir/win32") + + compileTask.source.from(fileTree(mapOf("dir" to "src/main/c", "include" to "**/*.c"))) + compileTask.source.from(fileTree(mapOf("dir" to layout.buildDirectory.dir("generated"), "include" to "**/*.c"))) + compileTask.includes.from(layout.buildDirectory.dir("generated")) + + if (toolChain is VisualCpp) { + compileTask.compilerArgs.addAll("/TC") + } else if (toolChain is GccCompatibleToolChain) { + compileTask.compilerArgs.addAll("-x", "c", "-std=c11") + } + } +} + +val results by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} + +afterEvaluate { +// val jar by tasks.creating(Jar::class) { +// destinationDirectory = layout.buildDirectory +// archiveClassifier.set("native") +// dependsOn() +// from() +// } + artifacts { + add("results", layout.buildDirectory.file("lib/main/release/libnative.so")) { + builtBy(tasks.named("assembleRelease")) + } + } +} + +tasks { + val generateAppmenuHeader by registering(RunToolTask::class) { + group = "custom" + inputFile = file("src/main/protocols/appmenu.xml") + outputFile = layout.buildDirectory.file("generated/appmenu.h") + commandLine("wayland-scanner", "client-header", inputFile.asFile.get().absolutePath, outputFile.asFile.get().absolutePath) + } + val generateAppmenuGlue by registering(RunToolTask::class) { + group = "custom" + inputFile = file("src/main/protocols/appmenu.xml") + outputFile = layout.buildDirectory.file("generated/appmenu.c") + commandLine("wayland-scanner", "private-code", inputFile.asFile.get().absolutePath, outputFile.asFile.get().absolutePath) + } + afterEvaluate { + named("compileDebugCpp") { + dependsOn(generateAppmenuHeader, generateAppmenuGlue) + } + named("compileReleaseCpp") { + dependsOn(generateAppmenuHeader, generateAppmenuGlue) + } + } +} \ No newline at end of file diff --git a/native/src/main/c/native.c b/native/src/main/c/native.c new file mode 100644 index 0000000..f6d0895 --- /dev/null +++ b/native/src/main/c/native.c @@ -0,0 +1,67 @@ +#include +#include +#include +#include "wayland-client-protocol.h" +#include "appmenu.h" + +struct WLFrame { + jobject pad1; + struct wl_surface *wl_surface; + // more stuff follows, but we don't care about it +}; + +struct org_kde_kwin_appmenu_manager *org_kde_kwin_appmenu_manager = NULL; + +static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { + if (strcmp(interface, "org_kde_kwin_appmenu_manager") == 0) { + org_kde_kwin_appmenu_manager = wl_registry_bind(registry, name, &org_kde_kwin_appmenu_manager_interface, 1); + } +} + +static void registry_global_remove(void *data, struct wl_registry *registry, uint32_t name) { + // Do nothing +} + +static const struct wl_registry_listener wl_registry_listener = { + .global = registry_global, + .global_remove = registry_global_remove, +}; + +JNIEXPORT void JNICALL Java_io_gitlab_jfronny_globalmenu_Native_init(JNIEnv *env, jobject obj, jlong ptr) { + struct wl_display *wl_display = (struct wl_display *) ptr; + struct wl_registry *wl_registry = wl_display_get_registry(wl_display); + if (wl_registry == NULL) { + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/RuntimeException"), "Failed to get registry"); + return; + } + + wl_registry_add_listener(wl_registry, &wl_registry_listener, NULL); + if (wl_display_roundtrip(wl_display) < 0) { + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/RuntimeException"), "Failed to roundtrip"); + return; + } +} + +JNIEXPORT jlong JNICALL Java_io_gitlab_jfronny_globalmenu_Native_create(JNIEnv *env, jobject obj, jlong ptr) { + if (org_kde_kwin_appmenu_manager == NULL) { + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/RuntimeException"), "Appmenu manager not initialized"); + return 0; + } + struct WLFrame *frame = (struct WLFrame *) ptr; + return (jlong) (intptr_t) org_kde_kwin_appmenu_manager_create(org_kde_kwin_appmenu_manager, frame->wl_surface); +} + +JNIEXPORT void JNICALL Java_io_gitlab_jfronny_globalmenu_Native_destroy(JNIEnv *env, jobject obj, jlong ptr) { + struct org_kde_kwin_appmenu *frame = (struct org_kde_kwin_appmenu *) ptr; + org_kde_kwin_appmenu_release(frame); + org_kde_kwin_appmenu_destroy(frame); +} + +JNIEXPORT void JNICALL Java_io_gitlab_jfronny_globalmenu_Native_setAddress(JNIEnv *env, jobject obj, jlong ptr, jstring serviceName, jstring objectPath) { + struct org_kde_kwin_appmenu *frame = (struct org_kde_kwin_appmenu *) ptr; + char *service_name = (*env)->GetStringUTFChars(env, serviceName, NULL); + char *object_path = (*env)->GetStringUTFChars(env, objectPath, NULL); + org_kde_kwin_appmenu_set_address(frame, service_name, object_path); + (*env)->ReleaseStringUTFChars(env, serviceName, service_name); + (*env)->ReleaseStringUTFChars(env, objectPath, object_path); +} \ No newline at end of file diff --git a/native/src/main/protocols/appmenu.xml b/native/src/main/protocols/appmenu.xml new file mode 100644 index 0000000..3b92cce --- /dev/null +++ b/native/src/main/protocols/appmenu.xml @@ -0,0 +1,35 @@ + + + + + + This interface allows a client to link a window (or wl_surface) to an com.canonical.dbusmenu + interface registered on DBus. + + + + + + + + + The DBus service name and object path where the appmenu interface is present + The object should be registered on the session bus before sending this request. + If not applicable, clients should remove this object. + + + + Set or update the service name and object path. + Strings should be formatted in Latin-1 matching the relevant DBus specifications. + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..c1de7c0 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "globalmenu" + +include("native") \ No newline at end of file diff --git a/src/main/java/io/gitlab/jfronny/globalmenu/Native.java b/src/main/java/io/gitlab/jfronny/globalmenu/Native.java new file mode 100644 index 0000000..c74440b --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/globalmenu/Native.java @@ -0,0 +1,26 @@ +package io.gitlab.jfronny.globalmenu; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class Native { + public native void init(long displayPtr); + public native long create(long ptr); + public native void destroy(long ptr); + public native void setAddress(long ptr, String serviceName, String objectPath); + + static { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + try (InputStream is = Native.class.getResourceAsStream("/libnative.so")) { + Path path = Files.createTempFile("libnative", ".so"); + Files.copy(is, path, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + System.load(path.toString()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException("Linux is required for the global menu plugin"); + } + } +} diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenu.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenu.kt new file mode 100644 index 0000000..5eea2b9 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenu.kt @@ -0,0 +1,9 @@ +package io.gitlab.jfronny.globalmenu + +import com.intellij.openapi.diagnostic.Logger + +object GlobalMenu { + val Log: Logger = Logger.getInstance(GlobalMenu::class.java) + val Native = Native() + val Cleaner = java.lang.ref.Cleaner.create() +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt new file mode 100644 index 0000000..2378aee --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt @@ -0,0 +1,43 @@ +package io.gitlab.jfronny.globalmenu + +import com.intellij.openapi.application.Application +import com.intellij.openapi.application.ApplicationActivationListener +import com.intellij.openapi.wm.IdeFrame +import com.intellij.openapi.wm.impl.IdeFrameImpl +import com.intellij.openapi.wm.impl.ProjectFrameHelper +import javax.swing.JMenuBar + +class GlobalMenuService(private val app: Application) : ApplicationActivationListener { + override fun applicationActivated(ideFrame: IdeFrame) { + super.applicationActivated(ideFrame) + 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) + } + GlobalMenu.Log.warn("Activated: ${project.name}") + } + } + + 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}") + } + } + peer.performLocked { + val ptr = GlobalMenu.Native.create(peer.nativePtr) +// peer.registerCleaner { +// GlobalMenu.Native.destroy(ptr) +// } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt new file mode 100644 index 0000000..edc56a8 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt @@ -0,0 +1,9 @@ +package io.gitlab.jfronny.globalmenu + +import com.intellij.ide.AppLifecycleListener + +class InitializationComponent : AppLifecycleListener { + override fun appFrameCreated(commandLineArgs: MutableList) { + GlobalMenu.Native.init(getDisplayPtr()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/Lambda.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/Lambda.kt new file mode 100644 index 0000000..bc394a0 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/Lambda.kt @@ -0,0 +1,14 @@ +package io.gitlab.jfronny.globalmenu + +import java.util.function.BiConsumer +import java.util.function.BiFunction +import java.util.function.Function +import java.util.function.Supplier + +val Function.unchecked: Function get() = Function { it: Any -> apply(it as T) } +val BiConsumer.unchecked1: BiConsumer get() = BiConsumer { t1: Any, t2: T2 -> accept(t1 as T1, t2) } +operator fun Runnable.invoke() = run() +operator fun Supplier.invoke() = get() +operator fun Function.invoke(t: T): R = apply(t) +operator fun BiFunction.invoke(t1: T1, t2: T2): R = apply(t1, t2) +operator fun BiConsumer.invoke(t1: T1, t2: T2) = accept(t1, t2) \ 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 new file mode 100644 index 0000000..6d54769 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/Reflect.kt @@ -0,0 +1,38 @@ +package io.gitlab.jfronny.globalmenu + +import io.gitlab.jfronny.commons.unsafe.reflect.Reflect +import io.gitlab.jfronny.commons.unsafe.reflect.impl.CoreReflect +import java.awt.Component +import java.lang.reflect.AccessibleObject +import java.lang.reflect.Field + +private val accessibleSetter = CoreReflect.lookup(AccessibleObject::class.java).findSetter(AccessibleObject::class.java, "override", Boolean::class.java) +private fun setAccessible(field: Field) { + accessibleSetter.invoke(field, true) +} + +private val peerField = Component::class.java.getDeclaredField("peer").apply { isAccessible = true } + +val Component.peer: Peer get() = Peer(peerField.get(this)) + +private val componentPeerClass = Class.forName("sun.awt.wl.WLComponentPeer") +private val performLockedMethod = Reflect.instanceProcedure(componentPeerClass, "performLocked", Runnable::class.java).unchecked1 +private val nativePtrField = componentPeerClass.getDeclaredField("nativePtr").apply { setAccessible(this) } + +class Peer(private val inner: Any) { + val nativePtr: Long get() = nativePtrField.getLong(inner) + fun performLocked(runnable: Runnable) { + performLockedMethod(inner, runnable) + } + fun registerCleaner(runnable: Runnable) { + GlobalMenu.Cleaner.register(this, runnable) + } +} + +private val wlDisplayClass = Class.forName("sun.awt.wl.WLDisplay") +private val getInstanceMethod = Reflect.staticFunction(wlDisplayClass, "getInstance", wlDisplayClass) +private val getDisplayPtrMethod = Reflect.instanceFunction(wlDisplayClass, "getDisplayPtr", Long::class.java).unchecked +fun getDisplayPtr(): Long { + val display = getInstanceMethod() + return getDisplayPtrMethod(display) as Long +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..750d057 --- /dev/null +++ b/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,32 @@ + + + + io.gitlab.jfronny.globalmenu + + + Global Menu + + + JFronny + + + + + + com.intellij.modules.platform + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..dcf6b99 --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file