Skip to content

add focus & resize window functions #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 14, 2023
18 changes: 18 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 2 additions & 0 deletions permissionCheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ try {
"getActiveWindow",
"getWindowRect",
"getWindowTitle",
"focusWindow",
"resizeWindow"
];
const screenCaptureAccess = [
"getWindowTitle",
Expand Down
35 changes: 35 additions & 0 deletions src/linux/window_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
205 changes: 191 additions & 14 deletions src/macos/window_manager.mm
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#include "../window_manager.h"
#import <AppKit/AppKit.h>
#import <AppKit/NSAccessibility.h>
#import <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CGWindow.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#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];
Expand All @@ -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;
Expand All @@ -49,16 +56,19 @@ WindowHandle getActiveWindow() {
}

std::vector<WindowHandle> getWindows() {
CGWindowListOption listOptions = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
CGWindowListOption listOptions =
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
CFArrayRef windowList =
CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);

std::vector<WindowHandle> windowHandles;

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]];
auto path = app ? [app.bundleURL.path UTF8String] : "";

if (app && path != "") {
Expand All @@ -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);
Expand All @@ -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;
}

45 changes: 45 additions & 0 deletions src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,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<Napi::Number>().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<Napi::Number>().Int64Value();
MMRect windowRect = getWindowRect(windowHandle);
Napi::Object rectObj = info[1].As<Napi::Object>();

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<Napi::Number>().Int64Value();
int64_t y = rectObj.Get("y").As<Napi::Number>().Int64Value();
int64_t width = rectObj.Get("width").As<Napi::Number>().Int64Value();
int64_t height = rectObj.Get("height").As<Napi::Number>().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();

Expand Down Expand Up @@ -725,6 +768,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));
Expand Down
Loading