Skip to content

Commit

Permalink
add focus & resize window functions (#164)
Browse files Browse the repository at this point in the history
* add focus & resize window functions

* fix windows rect

* rect object param

* fix focus window implementation on macos

* update focus window method

* "working" implementation of resize on mac

* individual window focusing

* fix windows build

* focus app on macos first for consistency

* add documentation and clean code

* throw errors and return resize result properly

* update resizeWindow rect documentation
  • Loading branch information
ekrenzin authored Jul 14, 2023
1 parent fa2b880 commit 901f5aa
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 14 deletions.
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 @@ -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<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 @@ -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));
Expand Down
Loading

0 comments on commit 901f5aa

Please sign in to comment.