diff --git a/src/main/kotlin/controllers/DesktopController.kt b/src/main/kotlin/controllers/DesktopController.kt index 056676e..8987836 100644 --- a/src/main/kotlin/controllers/DesktopController.kt +++ b/src/main/kotlin/controllers/DesktopController.kt @@ -84,8 +84,12 @@ interface DesktopController { } } -interface OSProxy{ +interface OSProxy { fun getActiveWindowName(): String + + fun enumWindowNames(): ArrayList + + fun setForegroundWindowByName(name: String) } /** @@ -99,7 +103,7 @@ interface OSProxy{ */ class WindowsDesktopController : DesktopController, OSProxy { - companion object{ + companion object { /** * Converts a native byte buffer to a String. * @@ -119,7 +123,7 @@ class WindowsDesktopController : DesktopController, OSProxy { * @param byteBuffer Byte array containing text from a native call * @return The native text as a String */ - private fun nativeByteBufferToString(byteBuffer: ByteArray): String{ + private fun nativeByteBufferToString(byteBuffer: ByteArray): String { // I guess this prunes anything that isn't on the ascii table? val wText = Native.toString(byteBuffer).trim { it <= ' ' } return wText @@ -229,13 +233,99 @@ class WindowsDesktopController : DesktopController, OSProxy { */ fun GetWindowTextA(hWnd: Pointer?, lpString: ByteArray?, nMaxCount: Int): Int + /** + * Gets the handle of the foreground window. + * + * Calls the Win32 API [User32.GetForegroundWindow] to retrieve the window handle + * of the current foreground window - the one the user is currently interacting with. + * + * @return The window handle (HWND) of the current foreground window, or null if none. + */ fun GetForegroundWindow(): Pointer? + /** + * Sets the foreground window. + * + * Calls the Win32 API [User32.SetForegroundWindow] to bring the window with the given + * handle [hWnd] to the foreground. + * + * @param hWnd The native window handle (HWND) of the window to activate. + * @return True if successful, false otherwise. + */ + fun SetForegroundWindow(hWnd: Pointer?): Boolean + + /** + * Loads the User32.dll library and gets an instance of the [User32] interface. + * + * This uses JNA to dynamically load the user32.dll library at runtime. It casts + * the result to the [User32] interface to provide access to the Windows API + * functions defined there. + * + * The [User32] instance is stored in [INSTANCE] to be used throughout the class + * for calling Windows APIs. + * + * For example: + * + * ``` + * val user32 = User32.INSTANCE + * + * user32.EnumWindows(...) // Call Windows API + * ``` + * + * @see User32 + */ companion object { + /** + * Loads the User32.dll library and gets an instance of the [User32] interface. + * + * This uses JNA to dynamically load the user32.dll library at runtime. It casts + * the result to the [User32] interface to provide access to the Windows API + * functions defined there. + * + * The [User32] instance is stored in [INSTANCE] to be used throughout the class + * for calling Windows APIs. + * + * For example: + * + * ``` + * val user32 = User32.INSTANCE + * + * user32.EnumWindows(...) // Call Windows API + * ``` + * + * @see User32 + */ val INSTANCE = Native.load("user32", User32::class.java) as User32 } + } + /** + * Callback class used for enumerating windows. + * + * This implements the [User32.WNDENUMPROC] interface required by the + * [User32.EnumWindows] API. An instance of this class is passed to + * [User32.EnumWindows] to receive callbacks for each window. + * + * @param cb The callback function to invoke for each window. It should return + * true to continue enumeration or false to stop. + */ + internal class WindowEnumCallback(val cb: (Pointer?, Pointer?) -> Boolean) : User32.WNDENUMPROC { + + /** + * Called by [User32.EnumWindows] for each window. + * + * Implements the callback method required by [User32.WNDENUMPROC]. + * This simply invokes the provided [cb] callback and returns its result. + * + * @param hWnd The window handle being enumerated. + * @param userDataPtr A pointer to user data passed to [User32.EnumWindows]. + * @return The return value from the [cb] callback. + */ + override fun callback(hWnd: Pointer?, userDataPtr: Pointer?): Boolean { + return cb(hWnd, userDataPtr) + } + } /** @@ -256,10 +346,93 @@ class WindowsDesktopController : DesktopController, OSProxy { */ override fun getActiveWindowName(): String { val user32 = User32.INSTANCE - val textBuffer = ByteArray(512) - - user32.GetWindowTextA(user32.GetForegroundWindow(), textBuffer, 512) - - return nativeByteBufferToString(textBuffer) + val foregroundWindowHwnd = user32.GetForegroundWindow() + return getWindowName(foregroundWindowHwnd) } + + /** + * Gets the title/name of the window for the given handle. + * + * This calls the Win32 API [User32.GetWindowTextA] to retrieve the title + * text for the window referenced by [hWnd]. + * + * It allocates a [windowTitleBuffer] byte array to hold the result. This is + * passed to [User32.GetWindowTextA] to be populated. + * + * The buffer is then converted to a [String] via [nativeByteBufferToString]. + * + * @param hWnd The native window handle to get the title for. + * @return The window title text as a [String]. + */ + private fun getWindowName(hWnd: Pointer?): String { + val maxTitleLength = 512 + val user32 = User32.INSTANCE + val windowTitleBuffer = ByteArray(maxTitleLength) + + user32.GetWindowTextA(hWnd, windowTitleBuffer, windowTitleBuffer.size) + + return nativeByteBufferToString(windowTitleBuffer) + } + + /** + * Enumerates all open window names on the desktop. + * + * Calls the Win32 API [User32.EnumWindows] to iterate through all current open windows. + * For each window handle, it retrieves the window name using [getWindowName] + * and adds it to a list if the name is not blank. We filter out blank window names because that particular information + * is useless for any reason other than counting how many open windows there are. If we actually need that information, + * we can simply acquire a list of all HWND references. + * + * @return An [ArrayList] containing the name of each open window. + */ + override fun enumWindowNames(): ArrayList { + val user32 = User32.INSTANCE + val windowNames = ArrayList() + + val iterationCallback = WindowEnumCallback { hWnd, _ -> + val windowName = getWindowName(hWnd) + if (windowName.isNotBlank()) { + windowNames.add(windowName) + } + true + } + + user32.EnumWindows(iterationCallback, null) + return windowNames + } + + + /** + * Sets the foreground window by name on Windows. + * + * This calls the Win32 API [User32.EnumWindows] to iterate through all + * top-level windows, compares their name to the given [name], and calls + * [User32.SetForegroundWindow] on the matching window to bring it to the foreground. + * + * @param name The window name to search for and activate. + */ + override fun setForegroundWindowByName(name: String) { + val user32 = User32.INSTANCE + + val iterationCallback = WindowEnumCallback { hWnd, _ -> + val windowName = getWindowName(hWnd) + + if (windowName.isBlank()) { + //if the window is blank, tell the system that we should continue + true + } else { + //otherwise, we need to check the window name + if (windowName == name) { + user32.SetForegroundWindow(hWnd) + //we found a match, so tell the system that we don't need to proceed any further + false + } else { + true + } + } + } + + user32.EnumWindows(iterationCallback, null) + } + } \ No newline at end of file diff --git a/src/test/kotlin/controllers/DesktopControllerTest.kt b/src/test/kotlin/controllers/DesktopControllerTest.kt index 422b7f3..f9ce130 100644 --- a/src/test/kotlin/controllers/DesktopControllerTest.kt +++ b/src/test/kotlin/controllers/DesktopControllerTest.kt @@ -64,6 +64,6 @@ class DesktopControllerTest { @Test fun devTest(){ val c = WindowsDesktopController() - println("TEXT: ${ c.getActiveWindowName()}") + c.setForegroundWindowByName("RuneScape") } } \ No newline at end of file