diff --git a/index.d.ts b/index.d.ts index d24bc9d..ef24a09 100644 --- a/index.d.ts +++ b/index.d.ts @@ -52,4 +52,22 @@ export function getActiveWindow(): number; export function getWindowRect(handle: number): Rect; export function getWindowTitle(handle: number): string; +/** + * Sets the focus to a specific window using its handle. + * + * @param {number} handle - The handle ID of the window to be focused. + * @returns {void} + */ +export function focusWindow(handle: number): void + +/** +* Resizes a window by its handle to the given width and height. +* The window is moved to the x & y coordinates if specified. +* +* @param {number} handle - The handle ID of the window to be resized. +* @param {Rect} rect - The new size of the window. +* @returns {void} +*/ +export function resizeWindow(handle: number, rect: Rect): void + export const screen: Screen; diff --git a/permissionCheck.js b/permissionCheck.js index e0e9fa5..7bd8b27 100644 --- a/permissionCheck.js +++ b/permissionCheck.js @@ -57,6 +57,8 @@ try { "getActiveWindow", "getWindowRect", "getWindowTitle", + "focusWindow", + "resizeWindow" ]; const screenCaptureAccess = [ "getWindowTitle", diff --git a/src/linux/window_manager.cc b/src/linux/window_manager.cc index eebaf9c..1e61970 100644 --- a/src/linux/window_manager.cc +++ b/src/linux/window_manager.cc @@ -68,3 +68,38 @@ MMRect getWindowRect(const WindowHandle windowHandle) { } return windowRect; } + +bool focusWindow(const WindowHandle windowHandle) { + Display* display = XGetMainDisplay(); + if (display != NULL && windowHandle >= 0) { + // Try to set the window to the foreground + XSetInputFocus(display, windowHandle, RevertToParent, CurrentTime); + XRaiseWindow(display, windowHandle); + XFlush(display); + + return true; + } + return false; +} + +bool resizeWindow(const WindowHandle windowHandle, const MMRect& rect) { + Display* display = XGetMainDisplay(); + if (display != NULL && windowHandle >= 0) { + XWindowChanges changes; + + //size + changes.width = rect.size.width; + changes.height = rect.size.height; + + //origin + changes.x = rect.origin.x; + changes.y = rect.origin.y; + + // Resize and move the window + XConfigureWindow(display, windowHandle, CWX | CWY | CWWidth | CWHeight, &changes); + XFlush(display); + + return true; + } + return false; +} \ No newline at end of file diff --git a/src/macos/window_manager.mm b/src/macos/window_manager.mm index 93c6154..66fe6bf 100644 --- a/src/macos/window_manager.mm +++ b/src/macos/window_manager.mm @@ -1,11 +1,15 @@ +#include "../window_manager.h" +#import +#import +#import #include #import -#import -#include "../window_manager.h" -NSDictionary* getWindowInfo(int64_t windowHandle) { - CGWindowListOption listOptions = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; - CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID); +NSDictionary *getWindowInfo(int64_t windowHandle) { + CGWindowListOption listOptions = + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; + CFArrayRef windowList = + CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID); for (NSDictionary *info in (NSArray *)windowList) { NSNumber *windowNumber = info[(id)kCGWindowNumber]; @@ -25,14 +29,17 @@ } WindowHandle getActiveWindow() { - CGWindowListOption listOptions = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; - CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID); + CGWindowListOption listOptions = + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; + CFArrayRef windowList = + CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID); for (NSDictionary *info in (NSArray *)windowList) { NSNumber *ownerPid = info[(id)kCGWindowOwnerPID]; NSNumber *windowNumber = info[(id)kCGWindowNumber]; - auto app = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]]; + auto app = [NSRunningApplication + runningApplicationWithProcessIdentifier:[ownerPid intValue]]; if (![app isActive]) { continue; @@ -49,8 +56,10 @@ WindowHandle getActiveWindow() { } std::vector getWindows() { - CGWindowListOption listOptions = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; - CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID); + CGWindowListOption listOptions = + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; + CFArrayRef windowList = + CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID); std::vector windowHandles; @@ -58,7 +67,8 @@ WindowHandle getActiveWindow() { NSNumber *ownerPid = info[(id)kCGWindowOwnerPID]; NSNumber *windowNumber = info[(id)kCGWindowNumber]; - auto app = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]]; + auto app = [NSRunningApplication + runningApplicationWithProcessIdentifier:[ownerPid intValue]]; auto path = app ? [app.bundleURL.path UTF8String] : ""; if (app && path != "") { @@ -77,8 +87,10 @@ MMRect getWindowRect(const WindowHandle windowHandle) { auto windowInfo = getWindowInfo(windowHandle); if (windowInfo != nullptr && windowHandle >= 0) { CGRect windowRect; - if (CGRectMakeWithDictionaryRepresentation((CFDictionaryRef)windowInfo[(id)kCGWindowBounds], &windowRect)) { - return MMRectMake(windowRect.origin.x, windowRect.origin.y, windowRect.size.width, windowRect.size.height); + if (CGRectMakeWithDictionaryRepresentation( + (CFDictionaryRef)windowInfo[(id)kCGWindowBounds], &windowRect)) { + return MMRectMake(windowRect.origin.x, windowRect.origin.y, + windowRect.size.width, windowRect.size.height); } } return MMRectMake(0, 0, 0, 0); @@ -88,7 +100,172 @@ MMRect getWindowRect(const WindowHandle windowHandle) { auto windowInfo = getWindowInfo(windowHandle); if (windowInfo != nullptr && windowHandle >= 0) { NSString *windowName = windowInfo[(id)kCGWindowName]; - return std::string([windowName UTF8String], [windowName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); + return std::string( + [windowName UTF8String], + [windowName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); } return ""; } + +/** + * Focuses on the window provided via its handle. + * + * This function collects a list of on-screen windows and matches the + * windowHandle with their window numbers. If found, the corresponding + * application is brought to foreground. The function then uses accessibility + * APIs to specifically focus the target window using its title. + * + * @param windowHandle Handle to the window that needs to be focused. + * + * @return bool If the function executes without any errors, it returns true. + * If it can't retrieve window information or windowHandle is + * invalid, it returns false. + */ +bool focusWindow(const WindowHandle windowHandle) { + + // Collect list of on-screen windows + CGWindowListOption listOptions = + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; + CFArrayRef windowList = + CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID); + bool activated = false; + + // Look for matching window and bring application to foreground + for (NSDictionary *info in (NSArray *)windowList) { + NSNumber *ownerPid = info[(id)kCGWindowOwnerPID]; + NSNumber *windowNumber = info[(id)kCGWindowNumber]; + if ([windowNumber intValue] == windowHandle) { + NSRunningApplication *app = [NSRunningApplication + runningApplicationWithProcessIdentifier:[ownerPid intValue]]; + [app activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + activated = true; + } + } + + // Clean up window list + if (windowList) { + CFRelease(windowList); + } + + // Retrieve window info + NSDictionary *windowInfo = getWindowInfo(windowHandle); + if (windowInfo == nullptr || windowHandle < 0) { + // NSLog(@"Could not find window info for window handle %lld", windowHandle); + return false; + } + + // Create application object for accessibility + pid_t pid = [[windowInfo objectForKey:(id)kCGWindowOwnerPID] intValue]; + AXUIElementRef app = AXUIElementCreateApplication(pid); + + // Get target window title + NSString *targetWindowTitle = [windowInfo objectForKey:(id)kCGWindowName]; + + CFArrayRef windowArray; + AXError error = AXUIElementCopyAttributeValue(app, kAXWindowsAttribute, + (CFTypeRef *)&windowArray); + + // Iterate through windows to find target and bring it to front + if (error == kAXErrorSuccess) { + CFIndex count = CFArrayGetCount(windowArray); + for (CFIndex i = 0; i < count; i++) { + AXUIElementRef window = + (AXUIElementRef)CFArrayGetValueAtIndex(windowArray, i); + + CFTypeRef windowTitle; + AXUIElementCopyAttributeValue(window, kAXTitleAttribute, &windowTitle); + if (windowTitle && CFGetTypeID(windowTitle) == CFStringGetTypeID()) { + NSString *title = (__bridge NSString *)windowTitle; + if ([title isEqualToString:targetWindowTitle]) { + AXError error = AXUIElementPerformAction(window, kAXRaiseAction); + if (error == kAXErrorSuccess) { + // NSLog(@"Successfully brought the window to front."); + } else { + // NSLog(@"Failed to bring the window to front."); + // NSLog(@"AXUIElementSetAttributeValue error: %d", error); + } + break; + } + } + + // Clean up window title + if (windowTitle) { + CFRelease(windowTitle); + } + } + + // Clean up window array + CFRelease(windowArray); + } else { + // NSLog(@"Failed to retrieve the window array."); + } + + // Clean up application object + CFRelease(app); + + // Successfully executed + return true; +} + +/** + * Resizes and repositions the window provided via its handle to the specified rectangle. + * + * This function retrieves window information using the provided window handle, then uses + * macOS Accessibility APIs to resize and reposition the window to fit within the provided + * rectangle dimensions and location. + * + * @param windowHandle Handle to the window that needs to be resized. + * @param rect The rectangle area to which the window should be resized and repositioned. + * + * @return bool If the function executes without any errors and successfully resizes the + * window, it returns true. If it can't retrieve window information or + * windowHandle is invalid, or the window resizing operation fails, it returns false. + */ +bool resizeWindow(const WindowHandle windowHandle, const MMRect rect) { + + // Retrieve window info + NSDictionary *windowInfo = getWindowInfo(windowHandle); + if (windowInfo == nullptr || windowHandle < 0) { + // NSLog(@"Could not find window info for window handle %lld", windowHandle); + return false; + } + + // Create application object for accessibility + pid_t pid = [[windowInfo objectForKey:(id)kCGWindowOwnerPID] intValue]; + AXUIElementRef app = AXUIElementCreateApplication(pid); + AXUIElementRef window; + + AXError error = AXUIElementCopyAttributeValue(app, kAXFocusedWindowAttribute, + (CFTypeRef *)&window); + + // If no error occurred, proceed with the resize and reposition operations + if (error == kAXErrorSuccess) { + + // Create AXValue objects for position and size + AXValueRef positionValue = AXValueCreate((AXValueType)kAXValueCGPointType, + (const void *)&rect.origin); + CGSize size = CGSizeMake(rect.size.width, rect.size.height); + AXValueRef sizeValue = + AXValueCreate((AXValueType)kAXValueCGSizeType, (const void *)&size); + + // Set new position and size + AXUIElementSetAttributeValue(window, kAXPositionAttribute, positionValue); + AXUIElementSetAttributeValue(window, kAXSizeAttribute, sizeValue); + + // Clean up AXValue and AXUIElement objects + CFRelease(positionValue); + CFRelease(sizeValue); + CFRelease(window); + CFRelease(app); + + // Return true to indicate successful resize + return true; + } else { + // NSLog(@"Could not resize window with window handle %lld", windowHandle); + CFRelease(app); + return false; + } + + return YES; +} + diff --git a/src/main.cc b/src/main.cc index 9760141..71c53a2 100644 --- a/src/main.cc +++ b/src/main.cc @@ -646,6 +646,49 @@ Napi::String _getWindowTitle(const Napi::CallbackInfo &info) { return Napi::String::New(env, getWindowTitle(windowHandle)); } +Napi::Boolean _focusWindow(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + WindowHandle windowHandle = info[0].As().Int64Value(); + + bool result = focusWindow(windowHandle); + + return Napi::Boolean::New(env, result); +} + +Napi::Boolean _resizeWindow(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsObject()) { + Napi::TypeError::New(env, "Invalid arguments. Expected handle (number) and rect (object).").ThrowAsJavaScriptException(); + return Napi::Boolean::New(env, false); + } + + WindowHandle windowHandle = info[0].As().Int64Value(); + MMRect windowRect = getWindowRect(windowHandle); + Napi::Object rectObj = info[1].As(); + + if (!rectObj.Has("x") || !rectObj.Has("y") || !rectObj.Has("width") || !rectObj.Has("height")) { + Napi::TypeError::New(env, "Invalid rect object. Must have 'x', 'y', 'width', and 'height' properties.").ThrowAsJavaScriptException(); + return Napi::Boolean::New(env, false); + } + + int64_t x = rectObj.Get("x").As().Int64Value(); + int64_t y = rectObj.Get("y").As().Int64Value(); + int64_t width = rectObj.Get("width").As().Int64Value(); + int64_t height = rectObj.Get("height").As().Int64Value(); + + windowRect.origin.x = x; + windowRect.origin.y = y; + windowRect.size.width = width; + windowRect.size.height = height; + + bool resizeResult = resizeWindow(windowHandle, windowRect); + + return Napi::Boolean::New(env, resizeResult); +} + + Napi::Object _captureScreen(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); @@ -727,6 +770,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { exports.Set(Napi::String::New(env, "getActiveWindow"), Napi::Function::New(env, _getActiveWindow)); exports.Set(Napi::String::New(env, "getWindowRect"), Napi::Function::New(env, _getWindowRect)); exports.Set(Napi::String::New(env, "getWindowTitle"), Napi::Function::New(env, _getWindowTitle)); + exports.Set(Napi::String::New(env, "focusWindow"), Napi::Function::New(env, _focusWindow)); + exports.Set(Napi::String::New(env, "resizeWindow"), Napi::Function::New(env, _resizeWindow)); exports.Set(Napi::String::New(env, "captureScreen"), Napi::Function::New(env, _captureScreen)); exports.Set(Napi::String::New(env, "getXDisplayName"), Napi::Function::New(env, _getXDisplayName)); exports.Set(Napi::String::New(env, "setXDisplayName"), Napi::Function::New(env, _setXDisplayName)); diff --git a/src/win32/window_manager.cc b/src/win32/window_manager.cc index 8daa663..868e751 100644 --- a/src/win32/window_manager.cc +++ b/src/win32/window_manager.cc @@ -51,3 +51,33 @@ std::string getWindowTitle(const WindowHandle windowHandle) { } return ""; } + +bool focusWindow(const WindowHandle windowHandle) { + HWND hWnd = reinterpret_cast(windowHandle); + if (IsWindow(hWnd)) { + // Restore the window if it's minimized + if (IsIconic(hWnd)) { + ShowWindow(hWnd, SW_RESTORE); + } + + // Try to set the window to the foreground + return SetForegroundWindow(hWnd); + } + return false; +} + +bool resizeWindow(const WindowHandle windowHandle, const MMRect rect) { + HWND hWnd = reinterpret_cast(windowHandle); + if (IsWindow(hWnd)) { + //size + int width = rect.size.width; + int height = rect.size.height; + + //origin + int x = rect.origin.x; + int y = rect.origin.y; + + return MoveWindow(hWnd, x, y, width, height, TRUE); + } + return false; +} diff --git a/src/window_manager.h b/src/window_manager.h index dd4ec08..dff37bd 100644 --- a/src/window_manager.h +++ b/src/window_manager.h @@ -33,4 +33,27 @@ std::string getWindowTitle(const WindowHandle windowHandle); */ MMRect getWindowRect(const WindowHandle windowHandle); +/** + * `focusWindow` focuses on the window specified by its window handle. + * It brings the specified window to the foreground and gives it input focus. + * The respective window handle may be acquired via `getWindows` or `getActiveWindow`. + * @param windowHandle The window handle of the window to be focused. + * @return Returns a boolean indicating whether the window focus operation was successful. + */ +bool focusWindow(const WindowHandle windowHandle); + +/** + * `resizeWindow` resizes the window specified by its window handle to the given width and height. + * The respective window handle may be acquired via `getWindows` or `getActiveWindow`. + * @param windowHandle The window handle of the window to be resized. + * @param rect The new position and size of the window. + * @param rect.x The new x coordinate of the window's top left corner. + * @param rect.y The new y coordinate of the window's top left corner. + * @param rect.width The new width of the window. + * @param rect.height The new height of the window. + * @return Returns a boolean indicating whether the window resize operation was successful. + */ +bool resizeWindow(const WindowHandle windowHandle, const MMRect rect); + + #endif