package controllers.windows import com.sun.jna.Native import com.sun.jna.Pointer import com.sun.jna.Structure import com.sun.jna.Structure.FieldOrder import controllers.MousePointerObserver import controllers.OSProxy import java.awt.Rectangle /** * Windows implementation of [MousePointerObserver]. * * This class provides methods to interact with the desktop on Windows * by calling Win32 APIs. * * It implements the [MousePointerObserver] interface to provide desktop * functionality like getting the mouse pointer location on Windows. */ interface WindowsOSProxy : MousePointerObserver, OSProxy { companion object { /** * Converts a native byte buffer to a String. * * This takes a byte array [byteBuffer] containing text from a native Win32 call, * converts it to a String using JNA, and trims whitespace characters. * * Usage example: * * ``` * val buffer = ByteArray(256) * GetWindowTextA(hwnd, buffer, buffer.size) // Win32 call * * val windowTitle = nativeByteBufferToString(buffer) * println(windowTitle) // Print title string * ``` * * @param byteBuffer Byte array containing text from a native call * @return The native text as a String */ private fun nativeByteBufferToString(byteBuffer: ByteArray): String { return Native.toString(byteBuffer).trim { it <= ' ' } } } /** * 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. */ 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) } } /** * Mirror of the Windows RECT struct used for window coordinates. */ @FieldOrder("left", "top", "right", "bottom") public class WinRect : Structure() { /** The x coordinate of the left edge */ @JvmField var left: Int = 0 /** The y coordinate of the top edge */ @JvmField var top: Int = 0 /** The x coordinate of the right edge */ @JvmField var right: Int = 0 /** The y coordinate of the bottom edge */ @JvmField var bottom: Int = 0 } /** * Gets the title of the active/foreground window. * * This calls Win32 APIs to get the handle of the foreground window, * then gets its title text. * * Usage example: * * ``` * val activeWindowName = getActiveWindowName() * * println(activeWindowName) // Prints foreground window title * ``` * * @return The title text of the current foreground window. */ override fun getActiveWindowName(): String { val user32 = User32.INSTANCE val foregroundWindowHwnd = user32.GetForegroundWindow() return getWindowName(foregroundWindowHwnd) } fun getWindowHandleByName(name: String): Pointer? { val user32 = User32.INSTANCE var ptr: Pointer? = null val enumFunction = WindowEnumCallback { hwnd, _ -> val wName = getWindowName(hwnd) if (name == wName) { ptr = hwnd false } else { true } } user32.EnumWindows(enumFunction, null) return ptr } /** * 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) } /** * Gets the screen bounds of the current foreground window. * * Calls the Win32 API [User32.GetForegroundWindow] to get the HWND of the * foreground window. * * Then calls [User32.GetWindowRect] to populate a [WinRect] struct with the * window bounds. * * The rectangle coordinates are converted to a [Rectangle] and returned. This allows us to keep the implementation * generic and constrained within the java std library. * In the future we may want to add LinuxOSProxy or DarwinOSProxy. * * @return The outer bounding rectangle of the foreground window in screen coordinates, * or an empty [Rectangle] if it failed. */ override fun getForegroundWindowBounds(): Rectangle? { val user32 = User32.INSTANCE val hWnd = user32.GetForegroundWindow() val rect = getRectFromWindowHandle(user32, hWnd) return if (rect != null) { //we need to make the calls to get the info to correct for the dpi scaling val secondarySuccess = correctWinRectForDpi(user32, hWnd!!, rect.pointer) if (secondarySuccess) { //the correctWinRectForDpi function doesn't have access to the underlying stuct in order to read it, so // we have to call this rect.read() Rectangle(rect.top, rect.left, (rect.right - rect.left), (rect.bottom - rect.top)) } else { return null } } else { null } } private fun getRectFromWindowHandle(user32: User32, hWnd: Pointer?): WinRect? { //we have to provide the system a native struct in order to hold the results of our request val rect = WinRect() val success = user32.GetWindowRect(hWnd, rect.pointer) return if (success) { //the values are stuck down in memory, so we have to read these back out in order to proceed rect.read() rect } else { null } } private fun correctWinRectForDpi(user32: User32, hWnd: Pointer, lpRect: Pointer): Boolean { val user32 = User32.INSTANCE val dwStyle = user32.GetWindowLongA(hWnd, User32.GWL_STYLE) val bMenu = true val dwExStyle = user32.GetWindowLongA(hWnd, User32.GWL_EXSTYLE) val dpi = user32.GetDpiForWindow(hWnd) return user32.AdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi) } }