diff --git a/build.gradle.kts b/build.gradle.kts index 47180d5..bde2e25 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,11 @@ +import com.jetbrains.plugin.structure.base.utils.createParentDirs +import org.freedesktop.dbus.utils.generator.InterfaceCodeGenerator +import org.jetbrains.intellij.platform.gradle.utils.asPath +import java.nio.file.Files +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteRecursively + plugins { java kotlin("jvm") version "1.9.24" @@ -23,8 +31,9 @@ dependencies { intellijIdeaCommunity("242.20224.91") instrumentationTools() } - implementation("io.gitlab.jfronny:commons-unsafe:2.0.0-SNAPSHOT") 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") } val copyExtraResources by tasks.creating(Copy::class) { @@ -35,17 +44,96 @@ val copyExtraResources by tasks.creating(Copy::class) { sourceSets { main { resources { - this.srcDir(copyExtraResources) + srcDir(copyExtraResources) + } + java { + srcDir(layout.buildDirectory.dir("generated/dbus/menu")) + srcDir(layout.buildDirectory.dir("generated/dbus/registrar")) } } } -abstract class RunToolTask : AbstractExecTask(RunToolTask::class.java) { +abstract class InterfaceGenerateTask : DefaultTask() { @get:InputFile abstract val inputFile: RegularFileProperty - @get:OutputFile abstract val outputFile: RegularFileProperty + @get:Input abstract val objectPath: Property + @get:Input abstract val busName: Property + @get:OutputDirectory abstract val outputFile: DirectoryProperty + @TaskAction fun generate() { + val input = inputFile.get().asPath + val introspectionData = Files.readString(input) + val output = outputFile.get().asPath + val generator = InterfaceCodeGenerator( + false, + introspectionData, + objectPath.get(), + busName.get(), + null, + true + ) + 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() + for (entry in analyze) { + if (entry.key.path.equals("/.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(output.resolve("com/canonical").resolve("$name.java"), """ + package com.canonical; + + import org.freedesktop.dbus.Struct; + import org.freedesktop.dbus.annotations.Position; + import org.freedesktop.dbus.types.Variant; + + import java.util.List; + import java.util.Map; + + public class $name extends Struct { + @Position(0) public final int a; + @Position(1) public final $type b; + + public $name(int a, $type b) { + this.a = a; + this.b = b; + } + } + """.trimIndent()) + name + } + }) + } + } } tasks { + val generateDbus by registering(InterfaceGenerateTask::class) { + group = "custom" + objectPath = "/" + busName = "" + inputFile = file("src/main/protocols/dbus-menu.xml") + outputFile = layout.buildDirectory.dir("generated/dbus/menu") + } + val generateDbusRegistrar by registering(InterfaceGenerateTask::class) { + group = "custom" + objectPath = "/" + busName = "" + inputFile = file("src/main/protocols/com.canonical.AppMenu.Registrar.xml") + outputFile = layout.buildDirectory.dir("generated/dbus/registrar") + } + compileJava { + dependsOn(generateDbus, generateDbusRegistrar) + } + compileKotlin { + dependsOn(generateDbus, generateDbusRegistrar) + } + // Set the JVM compatibility versions withType { sourceCompatibility = "21" diff --git a/native/build.gradle.kts b/native/build.gradle.kts index e75bc3c..7bff5eb 100644 --- a/native/build.gradle.kts +++ b/native/build.gradle.kts @@ -24,8 +24,8 @@ library { 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")) + compileTask.source.from(fileTree(mapOf("dir" to layout.buildDirectory.dir("generated/wayland"), "include" to "**/*.c"))) + compileTask.includes.from(layout.buildDirectory.dir("generated/wayland")) if (toolChain is VisualCpp) { compileTask.compilerArgs.addAll("/TC") @@ -58,13 +58,13 @@ tasks { val generateAppmenuHeader by registering(RunToolTask::class) { group = "custom" inputFile = file("src/main/protocols/appmenu.xml") - outputFile = layout.buildDirectory.file("generated/appmenu.h") + outputFile = layout.buildDirectory.file("generated/wayland/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") + outputFile = layout.buildDirectory.file("generated/wayland/appmenu.c") commandLine("wayland-scanner", "private-code", inputFile.asFile.get().absolutePath, outputFile.asFile.get().absolutePath) } afterEvaluate { diff --git a/settings.gradle.kts b/settings.gradle.kts index c1de7c0..ffc87c7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,15 @@ pluginManagement { } } +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("com.github.hypfvieh:dbus-java-utils:5.0.0") + } +} + 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 index c74440b..24d44b4 100644 --- a/src/main/java/io/gitlab/jfronny/globalmenu/Native.java +++ b/src/main/java/io/gitlab/jfronny/globalmenu/Native.java @@ -10,17 +10,24 @@ public class Native { public native void destroy(long ptr); public native void setAddress(long ptr, String serviceName, String objectPath); + private static final boolean supported; static { - if (System.getProperty("os.name").toLowerCase().contains("linux")) { + if (!System.getProperty("os.name").toLowerCase().contains("linux")) { + supported = false; + } else { 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) { + supported = false; throw new RuntimeException(e); } - } else { - throw new RuntimeException("Linux is required for the global menu plugin"); + supported = true; } } + + public boolean isSupported() { + return supported; + } } diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt index 2378aee..8b47262 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/GlobalMenuService.kt @@ -10,6 +10,7 @@ import javax.swing.JMenuBar 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) { diff --git a/src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt b/src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt index edc56a8..5fbf00a 100644 --- a/src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt +++ b/src/main/kotlin/io/gitlab/jfronny/globalmenu/InitializationComponent.kt @@ -4,6 +4,8 @@ import com.intellij.ide.AppLifecycleListener class InitializationComponent : AppLifecycleListener { override fun appFrameCreated(commandLineArgs: MutableList) { - GlobalMenu.Native.init(getDisplayPtr()) + if (GlobalMenu.Native.isSupported) { + GlobalMenu.Native.init(getDisplayPtr()) + } } } \ No newline at end of file diff --git a/src/main/protocols/com.canonical.AppMenu.Registrar.xml b/src/main/protocols/com.canonical.AppMenu.Registrar.xml new file mode 100644 index 0000000..1ad2bdd --- /dev/null +++ b/src/main/protocols/com.canonical.AppMenu.Registrar.xml @@ -0,0 +1,56 @@ + + + + + + An interface to register a menu from an application's window to be displayed in another + window. This manages that association between XWindow Window IDs and the dbus + address and object that provides the menu using the dbusmenu dbus interface. + + + + + The XWindow ID of the window + + + The object on the dbus interface implementing the dbusmenu interface + + + + + A method to allow removing a window from the database. Windows will also be removed + when the client drops off DBus so this is not required. It is polite though. And + important for testing. + + + The XWindow ID of the window + + + + Gets the registered menu for a given window ID. + + The XWindow ID of the window to get + + + The address of the connection on DBus (e.g. :1.23 or org.example.service) + + + The path to the object which implements the com.canonical.dbusmenu interface. + + + + diff --git a/src/main/protocols/dbus-menu.xml b/src/main/protocols/dbus-menu.xml new file mode 100644 index 0000000..f93b1b1 --- /dev/null +++ b/src/main/protocols/dbus-menu.xml @@ -0,0 +1,437 @@ + + + + + + + + Name + Type + Description + Default Value + + + type + String + Can be one of: + - "standard": an item which can be clicked to trigger an action or + show another menu + - "separator": a separator + + Vendor specific types can be added by prefixing them with + "x--". + + "standard" + + + label + string + Text of the item, except that: + -# two consecutive underscore characters "__" are displayed as a + single underscore, + -# any remaining underscore characters are not displayed at all, + -# the first of those remaining underscore characters (unless it is + the last character in the string) indicates that the following + character is the access key. + + "" + + + enabled + boolean + Whether the item can be activated or not. + true + + + visible + boolean + True if the item is visible in the menu. + true + + + icon-name + string + Icon name of the item, following the freedesktop.org icon spec. + "" + + + icon-data + binary + PNG data of the icon. + Empty + + + shortcut + array of arrays of strings + The shortcut of the item. Each array represents the key press + in the list of keypresses. Each list of strings contains a list of + modifiers and then the key that is used. The modifier strings + allowed are: "Control", "Alt", "Shift" and "Super". + + - A simple shortcut like Ctrl+S is represented as: + [["Control", "S"]] + - A complex shortcut like Ctrl+Q, Alt+X is represented as: + [["Control", "Q"], ["Alt", "X"]] + Empty + + + toggle-type + string + + If the item can be toggled, this property should be set to: + - "checkmark": Item is an independent togglable item + - "radio": Item is part of a group where only one item can be + toggled at a time + - "": Item cannot be toggled + + "" + + + toggle-state + int + + Describe the current state of a "togglable" item. Can be one of: + - 0 = off + - 1 = on + - anything else = indeterminate + + Note: + The implementation does not itself handle ensuring that only one + item in a radio group is set to "on", or that a group does not have + "on" and "indeterminate" items simultaneously; maintaining this + policy is up to the toolkit wrappers. + + -1 + + + children-display + string + + If the menu item has children this property should be set to + "submenu". + + "" + + + disposition + string + + How the menuitem feels the information it's displaying to the + user should be presented. + - "normal" a standard menu item + - "informative" providing additional information to the user + - "warning" looking at potentially harmful results + - "alert" something bad could potentially happen + + "normal" + + + + Vendor specific properties can be added by prefixing them with + "x--". + ]]> + + + + + Provides the version of the DBusmenu API that this API is + implementing. + + + + + + Represents the way the text direction of the application. This + allows the server to handle mismatches intelligently. For left- + to-right the string is "ltr" for right-to-left it is "rtl". + + + + + + Tells if the menus are in a normal state or they believe that they + could use some attention. Cases for showing them would be if help + were referring to them or they accessors were being highlighted. + This property can have two values: "normal" in almost all cases and + "notice" when they should have a higher priority to be shown. + + + + + + A list of directories that should be used for finding icons using + the icon naming spec. Idealy there should only be one for the icon + theme, but additional ones are often added by applications for + app specific icons. + + + + + + + + Provides the layout and propertiers that are attached to the entries + that are in the layout. It only gives the items that are children + of the item that is specified in @a parentId. It will return all of the + properties or specific ones depending of the value in @a propertyNames. + + The format is recursive, where the second 'v' is in the same format + as the original 'a(ia{sv}av)'. Its content depends on the value + of @a recursionDepth. + + + The ID of the parent node for the layout. For + grabbing the layout from the root node use zero. + + + + The amount of levels of recursion to use. This affects the + content of the second variant array. + - -1: deliver all the items under the @a parentId. + - 0: no recursion, the array will be empty. + - n: array will contains items up to 'n' level depth. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + The revision number of the layout. For matching + with layoutUpdated signals. + + + The layout, as a recursive structure. + + + + + + Returns the list of items which are children of @a parentId. + + + + A list of ids that we should be finding the properties + on. If the list is empty, all menu items should be sent. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + + An array of property values. + An item in this area is represented as a struct following + this format: + @li id unsigned the item id + @li properties map(string => variant) the requested item properties + + + + + + + Get a signal property on a single item. This is not useful if you're + going to implement this interface, it should only be used if you're + debugging via a commandline tool. + + + the id of the item which received the event + + + the name of the property to get + + + the value of the property + + + + + -" + ]]> + + the id of the item which received the event + + + the type of event + + + event-specific data + + + The time that the event occured if available or the time the message was sent if not + + + + + + Used to pass a set of events as a single message for possibily several + different menuitems. This is done to optimize DBus traffic. + + + + An array of all the events that should be passed. This tuple should + match the parameters of the 'Event' signal. Which is roughly: + id, eventID, data and timestamp. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + This is called by the applet to notify the application that it is about + to show the menu under the specified item. + + + + Which menu item represents the parent of the item about to be shown. + + + + + Whether this AboutToShow event should result in the menu being updated. + + + + + + + A function to tell several menus being shown that they are about to + be shown to the user. This is likely only useful for programitc purposes + so while the return values are returned, in general, the singular function + should be used in most user interacation scenarios. + + + + The IDs of the menu items who's submenus are being shown. + + + + + The IDs of the menus that need updates. Note: if no update information + is needed the DBus message should set the no reply flag. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + + Triggered when there are lots of property updates across many items + so they all get grouped into a single dbus message. The format is + the ID of the item with a hashtable of names and values for those + properties. + + + + + + + Triggered by the application to notify display of a layout update, up to + revision + + + The revision of the layout that we're currently on + + + + If the layout update is only of a subtree, this is the + parent item for the entries that have changed. It is zero if + the whole layout should be considered invalid. + + + + + + The server is requesting that all clients displaying this + menu open it to the user. This would be for things like + hotkeys that when the user presses them the menu should + open and display itself to the user. + + + ID of the menu that should be activated + + + The time that the event occured + + + + + + + \ No newline at end of file