소스 검색

window tracker code

milan jurkulák 2 년 전
부모
커밋
7321ed798f

+ 53 - 8
src/main/kotlin/eu/mjdev/desktop/data/App.kt

@@ -6,6 +6,10 @@ import androidx.compose.ui.graphics.Color
 import eu.mjdev.desktop.helpers.exception.EmptyException.Companion.EmptyException
 import eu.mjdev.desktop.provider.DesktopProvider
 import java.io.File
+import kotlin.jvm.optionals.getOrNull
+
+val Process.command: String?
+    get() = info().command().getOrNull()
 
 @Suppress("unused", "MemberVisibilityCanBePrivate", "HasPlatformType")
 class App(
@@ -47,16 +51,33 @@ class App(
     var isStarted: Boolean = false
 
     // todo :  replace all params with what needed
-    val cleanExec
-        get() = exec.replace("%u", "").replace("%U", "")
+    val cmd
+        get() = exec.replace("%u", "").replace("%U", "").trim()
+
+    val command
+        get() = process?.command
+
+    val pids: List<Long>
+        get() = getProcessPids(process?.toHandle())
 
     val runningProcesses
         get() = ProcessHandle.allProcesses()
 
+    fun getProcessPids(p: ProcessHandle?): List<Long> {
+        val pids = mutableListOf<Long>()
+        if (p != null) {
+            pids.add(p.pid())
+            p.children().forEach { pch ->
+                pids.addAll(getProcessPids(pch))
+            }
+        }
+        return pids
+    }
+
     fun start() = runCatching {
         triggerStart()
-        println("Starting app: $name [$cleanExec]")
-        ProcessBuilder("/bin/bash", "-c", cleanExec).start().also {
+        println("Starting app: $name [$cmd]")
+        ProcessBuilder(cmd).start().also {
             process = it
         }.apply {
             onExit().thenRun {
@@ -100,20 +121,44 @@ class App(
         return this
     }
 
+    fun isWindowFocus(api: DesktopProvider): Boolean =
+        api.windowsTracker.isWindowActive(pids)
+
     fun requestWindowFocus(api: DesktopProvider) {
-        api.windowsTracker.getWindowByPid(process?.pid())?.toFront() ?: closeWindow(api)
+        val windows = api.windowsTracker.getWindowsByPids(pids)
+        if (windows.isNotEmpty()) {
+            println("Got windows:${windows.size}")
+            windows.forEach { w -> w.toFront() }
+        } else {
+            closeWindow(api)
+        }
     }
 
     fun minimizeWindow(api: DesktopProvider) {
-        api.windowsTracker.getWindowByPid(process?.pid())?.minimize()
+        val windows = api.windowsTracker.getWindowsByPids(pids)
+        if (windows.isNotEmpty()) {
+            windows.forEach { w -> w.minimize() }
+        } else {
+            closeWindow(api)
+        }
     }
 
     fun maximizeWindow(api: DesktopProvider) {
-        api.windowsTracker.getWindowByPid(process?.pid())?.maximize()
+        val windows = api.windowsTracker.getWindowsByPids(pids)
+        if (windows.isNotEmpty()) {
+            windows.forEach { w -> w.maximize() }
+        } else {
+            closeWindow(api)
+        }
     }
 
     fun closeWindow(api: DesktopProvider) {
-        api.windowsTracker.getWindowByPid(process?.pid())?.close() ?: exit()
+        val windows = api.windowsTracker.getWindowsByPids(pids)
+        if (windows.isNotEmpty()) {
+            windows.forEach { w -> w.close() }
+        } else {
+            exit()
+        }
     }
 
     fun exit() {

+ 12 - 3
src/main/kotlin/eu/mjdev/desktop/extensions/Modifier.kt

@@ -197,9 +197,18 @@ object Modifier {
         val blur: Dp,
         val spread: Dp,
     ) : ModifierNodeElement<DropShadowNode>() {
-        override fun create() = DropShadowNode(shape, color, offsetX, offsetY, blur, spread)
-
-        override fun update(node: DropShadowNode) {
+        override fun create() = DropShadowNode(
+            shape,
+            color,
+            offsetX,
+            offsetY,
+            blur,
+            spread
+        )
+
+        override fun update(
+            node: DropShadowNode
+        ) {
             node.shape = shape
             node.color = color
             node.offsetX = offsetX

+ 456 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/managers/WindowsManager.kt

@@ -0,0 +1,456 @@
+package eu.mjdev.desktop.helpers.managers
+
+import com.sun.jna.Library
+import com.sun.jna.Native
+import com.sun.jna.NativeLong
+import com.sun.jna.Pointer
+import com.sun.jna.platform.unix.X11
+import com.sun.jna.platform.unix.X11.*
+import com.sun.jna.ptr.IntByReference
+import com.sun.jna.ptr.NativeLongByReference
+import com.sun.jna.ptr.PointerByReference
+
+@Suppress(
+    "unused", "MemberVisibilityCanBePrivate", "FunctionName", "LocalVariableName", "SameParameterValue",
+    "DEPRECATION", "UNUSED_PARAMETER"
+)
+class WindowsManager {
+    private val x11: X11 by lazy {
+        INSTANCE
+    }
+    private val x11Ext by lazy {
+        Native.loadLibrary("X11", X11Ext::class.java)
+    }
+    private val xmu by lazy {
+        Native.loadLibrary("Xmu", Xmu::class.java)
+    }
+    private val display: Display by lazy { x11.XOpenDisplay(null) }
+    private val rootWindow: Window
+        get() = x11.XDefaultRootWindow(display)
+
+    val allSystemWindows: List<SystemWindow>
+        get() = getAllWindows(rootWindow, 0)
+
+    @Suppress("DEPRECATED_IDENTITY_EQUALS")
+    private fun getAllWindows(root: Window, depth: Int): List<SystemWindow> {
+        val windowList = mutableListOf<SystemWindow>()
+        val windowRef = WindowByReference()
+        val parentRef = WindowByReference()
+        val childrenRef = PointerByReference()
+        val childCountRef = IntByReference()
+        x11.XQueryTree(display, root, windowRef, parentRef, childrenRef, childCountRef)
+        if (childrenRef.value == null) return emptyList()
+        val ids: LongArray
+        if (Native.LONG_SIZE === java.lang.Long.BYTES) {
+            ids = childrenRef.value.getLongArray(0, childCountRef.value)
+        } else if (Native.LONG_SIZE === Integer.BYTES) {
+            val intIds: IntArray = childrenRef.value.getIntArray(0, childCountRef.value)
+            ids = LongArray(intIds.size)
+            for (i in intIds.indices) {
+                ids[i] = intIds[i].toLong()
+            }
+        } else return emptyList()
+        for (id in ids) {
+            if (id == 0L) continue
+            Window(id).also { window ->
+                windowList.add(
+                    SystemWindow(
+                        this,
+                        id,
+                        getWindowName(window) ?: "",
+                        getWindowPid(window) ?: 0L,
+                        getWindowClass(window) ?: "",
+                        getWindowDesktop(window) ?: 0L
+                    )
+                )
+                getAllWindows(window, depth + 1)
+            }
+        }
+        return windowList
+    }
+
+    fun getWindowsByPids(
+        pids: List<Long>
+    ): List<IWindow> = allSystemWindows.filter { pids.contains(it.pid) }.distinctBy { it.id }
+
+    fun getWindowPid(
+        window: Window
+    ) = get_property_as_long(
+        window,
+        XA_CARDINAL,
+        "_NET_WM_PID"
+    )
+
+    fun getWindowName(
+        window: Window
+    ) = get_property_as_utf8_string(
+        window,
+        x11.XInternAtom(display, "UTF8_STRING", false),
+        "_NET_WM_NAME"
+    )
+
+    fun getWindowClass(
+        window: Window
+    ) = get_property_as_utf8_string(
+        window,
+        x11.XInternAtom(display, "UTF8_STRING", false),
+        "WM_CLASS"
+    )
+
+    fun getWindowDesktop(
+        window: Window
+    ) = get_property_as_long(
+        window,
+        XA_CARDINAL,
+        "_NET_SHOWING_DESKTOP"
+    )
+
+    fun minimizeWindow(window: Window): Boolean =
+        x11Ext.XIconifyWindow(display, window, x11.XDefaultScreen(display)) == TRUE
+
+    fun maximizeWindow(window: Window) {
+        // todo
+    }
+
+    fun activateWindow(
+        window: Window,
+        switchDesktop: Boolean = true
+    ) = activate_window(window, switchDesktop)
+
+    fun closeWindow(
+        window: Window
+    ) = client_msg(window, "_NET_CLOSE_WINDOW", 0, 0, 0, 0, 0)
+
+    fun isWindowActive(
+        pids: List<Long>
+    ): Boolean = get_active_window()?.let { aw ->
+        getWindowsByPids(pids).any {
+            it.window.toLong() == aw.toLong()
+        }
+    } ?: false
+
+    private fun g_free(
+        pointer: Pointer?
+    ) {
+        if (pointer != null) x11.XFree(pointer)
+    }
+
+    private fun get_property_as_long(
+        window: Window,
+        xa_prop_type: Atom,
+        prop_name: String
+    ): Long? = get_property_as_long(window, xa_prop_type, prop_name, null)
+
+    private fun get_property_as_long(
+        window: Window,
+        xa_prop_type: Atom,
+        prop_name: String,
+        size: NativeLongByReference?
+    ): Long? {
+        var longProp: Long? = null
+        val prop = get_property(window, xa_prop_type, prop_name, size)
+        if (prop != null) {
+            longProp = prop.getLong(0)
+//            g_free(prop)
+        }
+        return longProp
+    }
+
+    private fun get_property_as_int(
+        display: Display,
+        win: Window,
+        xa_prop_type: Atom,
+        prop_name: String
+    ): Int? {
+        var intProp: Int? = null
+        val prop = get_property(win, xa_prop_type, prop_name, null)
+        if (prop != null) {
+            intProp = prop.getInt(0)
+//            g_free(prop)
+        }
+        return intProp
+    }
+
+    private fun g_strdup(
+        pointer: Pointer
+    ): String {
+        val value = pointer.getString(0)
+        // g_free(pointer);
+        return value
+    }
+
+    private fun g_locale_to_utf8(
+        pointer: Pointer
+    ): String = g_strdup(pointer)
+
+    private fun get_property_as_utf8_string(
+        window: Window,
+        xa_prop_type: Atom,
+        prop_name: String
+    ): String? {
+        var strProp: String? = null
+        val prop = get_property(window, xa_prop_type, prop_name, null)
+        if (prop != null) {
+            strProp = g_locale_to_utf8(prop)
+//            g_free(prop)
+        }
+        return strProp
+    }
+
+    private fun get_property_as_window(
+        window: Window,
+        xa_prop_type: Atom,
+        prop_name: String
+    ): Window? {
+        var ret: Window? = null
+        val prop = get_property(window, xa_prop_type, prop_name, null)
+        if (prop != null) {
+            ret = Window(prop.getLong(0))
+//            g_free(prop)
+        }
+        return ret
+    }
+
+    fun get_property(
+        window: Window?,
+        xa_prop_type: Atom,
+        prop_name: String?,
+        size: NativeLongByReference?
+    ): Pointer? {
+        val xa_ret_type = AtomByReference()
+        val ret_format = IntByReference()
+        val ret_n_items = NativeLongByReference()
+        val ret_bytes_after = NativeLongByReference()
+        val ret_prop = PointerByReference()
+        val xa_prop_name = x11.XInternAtom(display, prop_name, false)
+        if (x11.XGetWindowProperty(
+                display,
+                window,
+                xa_prop_name,
+                NativeLong(0),
+                NativeLong((MAX_PROPERTY_VALUE_LEN / 4).toLong()),
+                false,
+                xa_prop_type,
+                xa_ret_type,
+                ret_format,
+                ret_n_items,
+                ret_bytes_after,
+                ret_prop
+            ) != Success
+        ) {
+            return null
+        }
+        if ((xa_ret_type.value == null) || (xa_ret_type.value.toLong() != xa_prop_type.toLong())
+        ) {
+//            g_free(ret_prop.pointer)
+            return null
+        }
+        if (size != null) {
+            var tmp_size = ((ret_format.value / 8) * ret_n_items.value.toLong())
+            if (ret_format.value == 32) {
+                tmp_size *= (NativeLong.SIZE / 4).toLong()
+            }
+            size.value = NativeLong(tmp_size)
+        }
+        return ret_prop.value
+    }
+
+    fun client_msg(
+        window: Window,
+        msg: String,
+        data0: Long,
+        data1: Long,
+        data2: Long,
+        data3: Long,
+        data4: Long
+    ): Boolean {
+        val mask = NativeLong((SubstructureRedirectMask or SubstructureNotifyMask).toLong())
+        val xclient = XClientMessageEvent()
+        xclient.type = ClientMessage
+        xclient.serial = NativeLong(0)
+        xclient.send_event = TRUE
+        xclient.message_type = x11.XInternAtom(display, msg, false)
+        xclient.window = window
+        xclient.format = 32
+        xclient.data.setType(Array<NativeLong>::class.java)
+        xclient.data.l[0] = NativeLong(data0)
+        xclient.data.l[1] = NativeLong(data1)
+        xclient.data.l[2] = NativeLong(data2)
+        xclient.data.l[3] = NativeLong(data3)
+        xclient.data.l[4] = NativeLong(data4)
+        val event = XEvent()
+        event.setTypedValue(xclient)
+        return if (x11.XSendEvent(
+                display,
+                x11.XDefaultRootWindow(display),
+                FALSE,
+                mask,
+                event
+            ) != FALSE
+        ) EXIT_SUCCESS else EXIT_FAILURE
+    }
+
+    fun activate_window(
+        window: Window,
+        switch_desktop: Boolean
+    ): Boolean {
+        var desktop: Int? = get_property_as_int(display, window, XA_CARDINAL, "_NET_WM_DESKTOP")
+        if (desktop == null) {
+            desktop = get_property_as_int(display, window, XA_CARDINAL, "_WIN_WORKSPACE")
+        }
+        if (switch_desktop && (desktop != null)) {
+            client_msg(
+                x11.XDefaultRootWindow(display),
+                "_NET_CURRENT_DESKTOP",
+                desktop.toLong(),
+                0,
+                0,
+                0,
+                0
+            )
+        }
+        client_msg(window, "_NET_ACTIVE_WINDOW", 0, 0, 0, 0, 0)
+        x11.XMapRaised(display, window)
+        return EXIT_SUCCESS
+    }
+
+    fun get_active_window(): Window? = get_property_as_window(
+        x11.XDefaultRootWindow(display),
+        XA_WINDOW,
+        "_NET_ACTIVE_WINDOW"
+    )
+
+    fun dispose() {
+        x11.XCloseDisplay(display)
+    }
+
+    companion object {
+        const val MAX_PROPERTY_VALUE_LEN = 4096
+        const val TRUE: Int = 1
+        const val FALSE: Int = 0
+        const val EXIT_SUCCESS: Boolean = true
+        const val EXIT_FAILURE: Boolean = false
+    }
+
+    class SystemWindow(
+        val windowTracker: WindowsManager,
+        val id: Long,
+        val name: String,
+        val pid: Long,
+        val windowClass: String,
+        val desktop: Long,
+        override val window: Window = Window(id)
+    ) : IWindow {
+        override fun toBack() {
+            // todo
+        }
+
+        override fun toFront() {
+            windowTracker.activateWindow(window)
+        }
+
+        override fun minimize() {
+            windowTracker.minimizeWindow(window)
+        }
+
+        override fun maximize() {
+            windowTracker.maximizeWindow(window)
+        }
+
+        override fun close() {
+            windowTracker.closeWindow(window)
+        }
+
+        override fun toString(): String {
+            return "WID : $id  PID: $pid NAME: $name"
+        }
+    }
+
+    interface IWindow {
+        val window: Window
+
+        fun toBack() {}
+        fun toFront() {}
+        fun minimize() {}
+        fun maximize() {}
+        fun close() {}
+    }
+
+    private interface Xmu : Library {
+        fun XmuClientWindow(display: Display?, win: Window?): Window?
+    }
+
+    private interface X11Ext : Library {
+        fun XMoveWindow(
+            display: Display?,
+            window: Window?,
+            x: Long,
+            y: Long
+        )
+
+        fun XResizeWindow(
+            display: Display?,
+            window: Window?,
+            width: Long,
+            height: Long
+        )
+
+        fun XMoveResizeWindow(
+            display: Display?,
+            window: Window?,
+            x: Long,
+            y: Long,
+            width: Long,
+            height: Long
+        )
+
+        fun XCreateFontCursor(
+            display: Display?,
+            shape: Long
+        ): Cursor?
+
+        fun XGrabPointer(
+            display: Display?,
+            grab_window: Window?,
+            owner_events: Int,
+            event_mask: NativeLong?,
+            pointer_mode: Int,
+            keyboard_mode: Int,
+            confine_to: Window?,
+            cursor: Cursor?,
+            time: Int
+        ): Int
+
+        fun XAllowEvents(
+            display: Display?,
+            event_mode: Int,
+            time: Int
+        ): Int
+
+        fun XUngrabPointer(
+            display: Display?,
+            time: Int
+        ): Int
+
+        fun XmuClientWindow(
+            display: Display?,
+            window: Window?
+        ): Window?
+
+        fun XGetIconName(
+            display: Display?,
+            window: Window?,
+            icon_name_return: PointerByReference?
+        ): Int
+
+        fun XIconifyWindow(
+            display: Display?,
+            window: Window?,
+            screen: Int
+        ): Int
+
+        companion object {
+            const val XC_CROSS_HAIR: Int = 34
+        }
+    }
+}

+ 5 - 1
src/main/kotlin/eu/mjdev/desktop/provider/AppsProvider.kt

@@ -145,7 +145,11 @@ class AppsProvider(
         scope.launch(Dispatchers.IO) {
             if (app.isRunning) {
                 // todo menu to switch
-                app.requestWindowFocus(api)
+                if (app.isWindowFocus(api)) {
+                    app.minimizeWindow(api)
+                } else {
+                    app.requestWindowFocus(api)
+                }
             } else {
                 app.onStarting {
                     favoriteApps.invalidate()

+ 3 - 2
src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

@@ -13,8 +13,8 @@ import eu.mjdev.desktop.helpers.adb.AdbDiscover.Companion.adbDevicesHandler
 import eu.mjdev.desktop.helpers.managers.ConnectivityManager
 import eu.mjdev.desktop.helpers.managers.FileSystemWatcher
 import eu.mjdev.desktop.helpers.managers.KCEFHelper
+import eu.mjdev.desktop.helpers.managers.WindowsManager
 import eu.mjdev.desktop.provider.AIProvider.AiPluginGemini
-import eu.mjdev.desktop.windows.WindowsTracker
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import java.awt.GraphicsDevice
@@ -31,7 +31,7 @@ class DesktopProvider(
 //    val scriptManager: ScriptEngineManager = ScriptEngineManager(),
     val kcefHelper: KCEFHelper = KCEFHelper(scope),
     val aiProvider: AIProvider = AIProvider(scope, AiPluginGemini(scope)),
-    val windowsTracker: WindowsTracker = WindowsTracker()
+    val windowsTracker: WindowsManager = WindowsManager()
 ) {
     private val __currentUser: User by lazy { User.load() }
     private val _currentUser: MutableState<User> = mutableStateOf(__currentUser)
@@ -83,6 +83,7 @@ class DesktopProvider(
     fun dispose() {
         kcefHelper.dispose()
         mounts.dispose()
+        windowsTracker.dispose()
     }
 
 //    fun runScript(script: String): Any =

+ 0 - 115
src/main/kotlin/eu/mjdev/desktop/windows/WindowsTracker.kt

@@ -1,115 +0,0 @@
-package eu.mjdev.desktop.windows
-
-import com.sun.jna.Native
-import com.sun.jna.platform.unix.X11
-import com.sun.jna.platform.unix.X11.*
-import com.sun.jna.ptr.IntByReference
-import com.sun.jna.ptr.PointerByReference
-
-@Suppress("unused")
-class WindowsTracker {
-    private val x11: X11 = X11.INSTANCE
-    private val display: Display = x11.XOpenDisplay(null)
-    private val rootWindow: Window
-        get() = x11.XDefaultRootWindow(display)
-    val allWindows: List<SystemWindow>
-        get() = recurse(rootWindow, 0)
-
-    fun getWindowByPid(
-        pid: Long?
-    ): IWindow? {
-        allWindows.forEachIndexed { idx, wn ->
-            println("$idx, ${wn.name}")
-        }
-        return if (pid == null) null else null
-    }
-
-    @Suppress("DEPRECATED_IDENTITY_EQUALS")
-    private fun recurse(root: Window, depth: Int): List<SystemWindow> {
-        val windowList = mutableListOf<SystemWindow>()
-        val windowRef = WindowByReference()
-        val parentRef = WindowByReference()
-        val childrenRef = PointerByReference()
-        val childCountRef = IntByReference()
-        x11.XQueryTree(display, root, windowRef, parentRef, childrenRef, childCountRef)
-        if (childrenRef.value == null) return emptyList()
-        val ids: LongArray
-        if (Native.LONG_SIZE === java.lang.Long.BYTES) {
-            ids = childrenRef.value.getLongArray(0, childCountRef.value)
-        } else if (Native.LONG_SIZE === Integer.BYTES) {
-            val intIds: IntArray = childrenRef.value.getIntArray(0, childCountRef.value)
-            ids = LongArray(intIds.size)
-            for (i in intIds.indices) {
-                ids[i] = intIds[i].toLong()
-            }
-        } else return emptyList()
-//            throw IllegalStateException("Unexpected size for Native.LONG_SIZE" + Native.LONG_SIZE)
-        for (id in ids) {
-            if (id == 0L) continue
-            val window = Window(id)
-            val name = XTextProperty()
-            val pids = mutableListOf<Long>()
-            // todo
-            x11.XGetWMName(display, window, name)
-//            x11.XGetWindowAttributes()
-//            x11.XGetGeometry()
-//            x11.XGetWMHints()
-//            x11.XGetVisualInfo()
-//            x11.XGetWindowProperty()
-//            x11.XGetKeyboardMapping()
-//            x11.XGetModifierMapping()
-//            x11.XGetKeyboardControl()
-            windowList.add(
-                SystemWindow(
-                    this,
-                    id,
-                    name.value ?: "",
-                    pids
-                )
-            )
-            // todo error free memory
-//            try {
-//                x11.XFree(name.getPointer())
-//            } catch (t: Throwable) {
-//                t.printStackTrace()
-//            }
-            recurse(window, depth + 1)
-        }
-        return windowList
-    }
-
-    class SystemWindow(
-        val windowTracker: WindowsTracker,
-        val id: Long,
-        val name: String,
-        val pids: List<Long>
-    ) : IWindow {
-        override fun toBack() {
-            // todo
-        }
-
-        override fun toFront() {
-            // todo
-        }
-
-        override fun minimize() {
-            // todo
-        }
-
-        override fun maximize() {
-            // todo
-        }
-
-        override fun close() {
-            // todo
-        }
-    }
-
-    interface IWindow {
-        fun toBack() {}
-        fun toFront() {}
-        fun minimize() {}
-        fun maximize() {}
-        fun close() {}
-    }
-}