295 lines
9.8 KiB
Kotlin
295 lines
9.8 KiB
Kotlin
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<String> {
|
|
val user32 = User32.INSTANCE
|
|
val windowNames = ArrayList<String>()
|
|
|
|
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)
|
|
}
|
|
|
|
} |