feat: initial experimentation

This commit is contained in:
Johannes Frohnmeyer 2024-07-19 21:25:04 +02:00
commit 9c7ca4aaa1
Signed by: Johannes
GPG Key ID: E76429612C2929F4
17 changed files with 522 additions and 0 deletions

40
.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Plugin" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log"/>
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value=""/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="runIde"/>
</list>
</option>
<option name="vmOptions" value=""/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2"/>
</configuration>
</component>

3
README.md Normal file
View File

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

76
build.gradle.kts Normal file
View File

@ -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>(RunToolTask::class.java) {
@get:InputFile abstract val inputFile: RegularFileProperty
@get:OutputFile abstract val outputFile: RegularFileProperty
}
tasks {
// Set the JVM compatibility versions
withType<JavaCompile> {
sourceCompatibility = "21"
targetCompatibility = "21"
}
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
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")
}
}

6
gradle.properties Normal file
View File

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

78
native/build.gradle.kts Normal file
View File

@ -0,0 +1,78 @@
import org.gradle.internal.jvm.Jvm
plugins {
`cpp-library`
}
group = rootProject.group
version = rootProject.version
abstract class RunToolTask : AbstractExecTask<RunToolTask>(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)
}
}
}

View File

@ -0,0 +1,67 @@
#include <jni.h>
#include <stdio.h>
#include <string.h>
#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);
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="appmenu">
<copyright><![CDATA[
SPDX-FileCopyrightText: 2017 David Edmundson
SPDX-License-Identifier: LGPL-2.1-or-later
]]></copyright>
<interface name="org_kde_kwin_appmenu_manager" version="1">
<description summary="appmenu dbus address interface">
This interface allows a client to link a window (or wl_surface) to an com.canonical.dbusmenu
interface registered on DBus.
</description>
<request name="create">
<arg name="id" type="new_id" interface="org_kde_kwin_appmenu"/>
<arg name="surface" type="object" interface="wl_surface"/>
</request>
</interface>
<interface name="org_kde_kwin_appmenu" version="1">
<description summary="appmenu dbus address interface">
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.
</description>
<request name="set_address">
<description summary="initialise or update the location of the AppMenu interface">
Set or update the service name and object path.
Strings should be formatted in Latin-1 matching the relevant DBus specifications.
</description>
<arg name="service_name" type="string" />
<arg name="object_path" type="string" />
</request>
<request name="release" type="destructor">
<description summary="release the appmenu object"/>
</request>
</interface>
</protocol>

10
settings.gradle.kts Normal file
View File

@ -0,0 +1,10 @@
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "globalmenu"
include("native")

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
package io.gitlab.jfronny.globalmenu
import com.intellij.ide.AppLifecycleListener
class InitializationComponent : AppLifecycleListener {
override fun appFrameCreated(commandLineArgs: MutableList<String>) {
GlobalMenu.Native.init(getDisplayPtr())
}
}

View File

@ -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 <T, R> Function<T, R>.unchecked: Function<Any, R> get() = Function { it: Any -> apply(it as T) }
val <T1, T2> BiConsumer<T1, T2>.unchecked1: BiConsumer<Any, T2> get() = BiConsumer { t1: Any, t2: T2 -> accept(t1 as T1, t2) }
operator fun Runnable.invoke() = run()
operator fun <T> Supplier<T>.invoke() = get()
operator fun <T, R> Function<T, R>.invoke(t: T): R = apply(t)
operator fun <T1, T2, R> BiFunction<T1, T2, R>.invoke(t1: T1, t2: T2): R = apply(t1, t2)
operator fun <T1, T2> BiConsumer<T1, T2>.invoke(t1: T1, t2: T2) = accept(t1, t2)

View File

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

View File

@ -0,0 +1,32 @@
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin>
<!-- Unique identifier of the plugin. It should be FQN. It cannot be changed between the plugin versions. -->
<id>io.gitlab.jfronny.globalmenu</id>
<!-- Public plugin name should be written in Title Case.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-name -->
<name>Global Menu</name>
<!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. -->
<vendor email="projects.contact@frohnmeyer-wds.de" url="https://jfronny.gitlab.io">JFronny</vendor>
<!-- Description of the plugin displayed on the Plugin Page and IDE Plugin Manager.
Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description -->
<description><![CDATA[
Reenables the global menu on Linux systems.
]]></description>
<!-- Product and plugin compatibility requirements.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
<depends>com.intellij.modules.platform</depends>
<!-- Extension points defined by the plugin.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
<extensions defaultExtensionNs="com.intellij">
</extensions>
<applicationListeners>
<listener class="io.gitlab.jfronny.globalmenu.GlobalMenuService" topic="com.intellij.openapi.application.ApplicationActivationListener"/>
<listener class="io.gitlab.jfronny.globalmenu.InitializationComponent" topic="com.intellij.ide.AppLifecycleListener"/>
</applicationListeners>
</idea-plugin>

View File

@ -0,0 +1,12 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.0845 7.94025V4H24.0203V7.9896H16.029V4H7.91553V7.94025H4V36H16.0044V32.0045C16.0058 30.9457 16.4274 29.9308 17.1766 29.1826C17.9258 28.4345 18.9412 28.0143 20 28.0143C21.0588 28.0143 22.0743 28.4345 22.8234 29.1826C23.5726 29.9308 23.9942 30.9457 23.9956 32.0045V36H36V7.94025H32.0845Z"
fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="2.94192" y1="4.89955" x2="37.7772" y2="39.7345"
gradientUnits="userSpaceOnUse">
<stop offset="0.15937" stop-color="#3BEA62"/>
<stop offset="0.5404" stop-color="#3C99CC"/>
<stop offset="0.93739" stop-color="#6B57FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 818 B