Skip to content

Commit 901f5aa

Browse files
authored
add focus & resize window functions (#164)
* 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
1 parent fa2b880 commit 901f5aa

File tree

7 files changed

+344
-14
lines changed

7 files changed

+344
-14
lines changed

Diff for: index.d.ts

+18
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,22 @@ export function getActiveWindow(): number;
5252
export function getWindowRect(handle: number): Rect;
5353
export function getWindowTitle(handle: number): string;
5454

55+
/**
56+
* Sets the focus to a specific window using its handle.
57+
*
58+
* @param {number} handle - The handle ID of the window to be focused.
59+
* @returns {void}
60+
*/
61+
export function focusWindow(handle: number): void
62+
63+
/**
64+
* Resizes a window by its handle to the given width and height.
65+
* The window is moved to the x & y coordinates if specified.
66+
*
67+
* @param {number} handle - The handle ID of the window to be resized.
68+
* @param {Rect} rect - The new size of the window.
69+
* @returns {void}
70+
*/
71+
export function resizeWindow(handle: number, rect: Rect): void
72+
5573
export const screen: Screen;

Diff for: permissionCheck.js

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ try {
5757
"getActiveWindow",
5858
"getWindowRect",
5959
"getWindowTitle",
60+
"focusWindow",
61+
"resizeWindow"
6062
];
6163
const screenCaptureAccess = [
6264
"getWindowTitle",

Diff for: src/linux/window_manager.cc

+35
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,38 @@ MMRect getWindowRect(const WindowHandle windowHandle) {
6868
}
6969
return windowRect;
7070
}
71+
72+
bool focusWindow(const WindowHandle windowHandle) {
73+
Display* display = XGetMainDisplay();
74+
if (display != NULL && windowHandle >= 0) {
75+
// Try to set the window to the foreground
76+
XSetInputFocus(display, windowHandle, RevertToParent, CurrentTime);
77+
XRaiseWindow(display, windowHandle);
78+
XFlush(display);
79+
80+
return true;
81+
}
82+
return false;
83+
}
84+
85+
bool resizeWindow(const WindowHandle windowHandle, const MMRect& rect) {
86+
Display* display = XGetMainDisplay();
87+
if (display != NULL && windowHandle >= 0) {
88+
XWindowChanges changes;
89+
90+
//size
91+
changes.width = rect.size.width;
92+
changes.height = rect.size.height;
93+
94+
//origin
95+
changes.x = rect.origin.x;
96+
changes.y = rect.origin.y;
97+
98+
// Resize and move the window
99+
XConfigureWindow(display, windowHandle, CWX | CWY | CWWidth | CWHeight, &changes);
100+
XFlush(display);
101+
102+
return true;
103+
}
104+
return false;
105+
}

Diff for: src/macos/window_manager.mm

+191-14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
#include "../window_manager.h"
2+
#import <AppKit/AppKit.h>
3+
#import <AppKit/NSAccessibility.h>
4+
#import <ApplicationServices/ApplicationServices.h>
15
#include <CoreGraphics/CGWindow.h>
26
#import <Foundation/Foundation.h>
3-
#import <AppKit/AppKit.h>
4-
#include "../window_manager.h"
57

6-
NSDictionary* getWindowInfo(int64_t windowHandle) {
7-
CGWindowListOption listOptions = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
8-
CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
8+
NSDictionary *getWindowInfo(int64_t windowHandle) {
9+
CGWindowListOption listOptions =
10+
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
11+
CFArrayRef windowList =
12+
CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
913

1014
for (NSDictionary *info in (NSArray *)windowList) {
1115
NSNumber *windowNumber = info[(id)kCGWindowNumber];
@@ -25,14 +29,17 @@
2529
}
2630

2731
WindowHandle getActiveWindow() {
28-
CGWindowListOption listOptions = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
29-
CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
32+
CGWindowListOption listOptions =
33+
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
34+
CFArrayRef windowList =
35+
CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
3036

3137
for (NSDictionary *info in (NSArray *)windowList) {
3238
NSNumber *ownerPid = info[(id)kCGWindowOwnerPID];
3339
NSNumber *windowNumber = info[(id)kCGWindowNumber];
3440

35-
auto app = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]];
41+
auto app = [NSRunningApplication
42+
runningApplicationWithProcessIdentifier:[ownerPid intValue]];
3643

3744
if (![app isActive]) {
3845
continue;
@@ -49,16 +56,19 @@ WindowHandle getActiveWindow() {
4956
}
5057

5158
std::vector<WindowHandle> getWindows() {
52-
CGWindowListOption listOptions = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
53-
CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
59+
CGWindowListOption listOptions =
60+
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
61+
CFArrayRef windowList =
62+
CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
5463

5564
std::vector<WindowHandle> windowHandles;
5665

5766
for (NSDictionary *info in (NSArray *)windowList) {
5867
NSNumber *ownerPid = info[(id)kCGWindowOwnerPID];
5968
NSNumber *windowNumber = info[(id)kCGWindowNumber];
6069

61-
auto app = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]];
70+
auto app = [NSRunningApplication
71+
runningApplicationWithProcessIdentifier:[ownerPid intValue]];
6272
auto path = app ? [app.bundleURL.path UTF8String] : "";
6373

6474
if (app && path != "") {
@@ -77,8 +87,10 @@ MMRect getWindowRect(const WindowHandle windowHandle) {
7787
auto windowInfo = getWindowInfo(windowHandle);
7888
if (windowInfo != nullptr && windowHandle >= 0) {
7989
CGRect windowRect;
80-
if (CGRectMakeWithDictionaryRepresentation((CFDictionaryRef)windowInfo[(id)kCGWindowBounds], &windowRect)) {
81-
return MMRectMake(windowRect.origin.x, windowRect.origin.y, windowRect.size.width, windowRect.size.height);
90+
if (CGRectMakeWithDictionaryRepresentation(
91+
(CFDictionaryRef)windowInfo[(id)kCGWindowBounds], &windowRect)) {
92+
return MMRectMake(windowRect.origin.x, windowRect.origin.y,
93+
windowRect.size.width, windowRect.size.height);
8294
}
8395
}
8496
return MMRectMake(0, 0, 0, 0);
@@ -88,7 +100,172 @@ MMRect getWindowRect(const WindowHandle windowHandle) {
88100
auto windowInfo = getWindowInfo(windowHandle);
89101
if (windowInfo != nullptr && windowHandle >= 0) {
90102
NSString *windowName = windowInfo[(id)kCGWindowName];
91-
return std::string([windowName UTF8String], [windowName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
103+
return std::string(
104+
[windowName UTF8String],
105+
[windowName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
92106
}
93107
return "";
94108
}
109+
110+
/**
111+
* Focuses on the window provided via its handle.
112+
*
113+
* This function collects a list of on-screen windows and matches the
114+
* windowHandle with their window numbers. If found, the corresponding
115+
* application is brought to foreground. The function then uses accessibility
116+
* APIs to specifically focus the target window using its title.
117+
*
118+
* @param windowHandle Handle to the window that needs to be focused.
119+
*
120+
* @return bool If the function executes without any errors, it returns true.
121+
* If it can't retrieve window information or windowHandle is
122+
* invalid, it returns false.
123+
*/
124+
bool focusWindow(const WindowHandle windowHandle) {
125+
126+
// Collect list of on-screen windows
127+
CGWindowListOption listOptions =
128+
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements;
129+
CFArrayRef windowList =
130+
CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
131+
bool activated = false;
132+
133+
// Look for matching window and bring application to foreground
134+
for (NSDictionary *info in (NSArray *)windowList) {
135+
NSNumber *ownerPid = info[(id)kCGWindowOwnerPID];
136+
NSNumber *windowNumber = info[(id)kCGWindowNumber];
137+
if ([windowNumber intValue] == windowHandle) {
138+
NSRunningApplication *app = [NSRunningApplication
139+
runningApplicationWithProcessIdentifier:[ownerPid intValue]];
140+
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
141+
activated = true;
142+
}
143+
}
144+
145+
// Clean up window list
146+
if (windowList) {
147+
CFRelease(windowList);
148+
}
149+
150+
// Retrieve window info
151+
NSDictionary *windowInfo = getWindowInfo(windowHandle);
152+
if (windowInfo == nullptr || windowHandle < 0) {
153+
// NSLog(@"Could not find window info for window handle %lld", windowHandle);
154+
return false;
155+
}
156+
157+
// Create application object for accessibility
158+
pid_t pid = [[windowInfo objectForKey:(id)kCGWindowOwnerPID] intValue];
159+
AXUIElementRef app = AXUIElementCreateApplication(pid);
160+
161+
// Get target window title
162+
NSString *targetWindowTitle = [windowInfo objectForKey:(id)kCGWindowName];
163+
164+
CFArrayRef windowArray;
165+
AXError error = AXUIElementCopyAttributeValue(app, kAXWindowsAttribute,
166+
(CFTypeRef *)&windowArray);
167+
168+
// Iterate through windows to find target and bring it to front
169+
if (error == kAXErrorSuccess) {
170+
CFIndex count = CFArrayGetCount(windowArray);
171+
for (CFIndex i = 0; i < count; i++) {
172+
AXUIElementRef window =
173+
(AXUIElementRef)CFArrayGetValueAtIndex(windowArray, i);
174+
175+
CFTypeRef windowTitle;
176+
AXUIElementCopyAttributeValue(window, kAXTitleAttribute, &windowTitle);
177+
if (windowTitle && CFGetTypeID(windowTitle) == CFStringGetTypeID()) {
178+
NSString *title = (__bridge NSString *)windowTitle;
179+
if ([title isEqualToString:targetWindowTitle]) {
180+
AXError error = AXUIElementPerformAction(window, kAXRaiseAction);
181+
if (error == kAXErrorSuccess) {
182+
// NSLog(@"Successfully brought the window to front.");
183+
} else {
184+
// NSLog(@"Failed to bring the window to front.");
185+
// NSLog(@"AXUIElementSetAttributeValue error: %d", error);
186+
}
187+
break;
188+
}
189+
}
190+
191+
// Clean up window title
192+
if (windowTitle) {
193+
CFRelease(windowTitle);
194+
}
195+
}
196+
197+
// Clean up window array
198+
CFRelease(windowArray);
199+
} else {
200+
// NSLog(@"Failed to retrieve the window array.");
201+
}
202+
203+
// Clean up application object
204+
CFRelease(app);
205+
206+
// Successfully executed
207+
return true;
208+
}
209+
210+
/**
211+
* Resizes and repositions the window provided via its handle to the specified rectangle.
212+
*
213+
* This function retrieves window information using the provided window handle, then uses
214+
* macOS Accessibility APIs to resize and reposition the window to fit within the provided
215+
* rectangle dimensions and location.
216+
*
217+
* @param windowHandle Handle to the window that needs to be resized.
218+
* @param rect The rectangle area to which the window should be resized and repositioned.
219+
*
220+
* @return bool If the function executes without any errors and successfully resizes the
221+
* window, it returns true. If it can't retrieve window information or
222+
* windowHandle is invalid, or the window resizing operation fails, it returns false.
223+
*/
224+
bool resizeWindow(const WindowHandle windowHandle, const MMRect rect) {
225+
226+
// Retrieve window info
227+
NSDictionary *windowInfo = getWindowInfo(windowHandle);
228+
if (windowInfo == nullptr || windowHandle < 0) {
229+
// NSLog(@"Could not find window info for window handle %lld", windowHandle);
230+
return false;
231+
}
232+
233+
// Create application object for accessibility
234+
pid_t pid = [[windowInfo objectForKey:(id)kCGWindowOwnerPID] intValue];
235+
AXUIElementRef app = AXUIElementCreateApplication(pid);
236+
AXUIElementRef window;
237+
238+
AXError error = AXUIElementCopyAttributeValue(app, kAXFocusedWindowAttribute,
239+
(CFTypeRef *)&window);
240+
241+
// If no error occurred, proceed with the resize and reposition operations
242+
if (error == kAXErrorSuccess) {
243+
244+
// Create AXValue objects for position and size
245+
AXValueRef positionValue = AXValueCreate((AXValueType)kAXValueCGPointType,
246+
(const void *)&rect.origin);
247+
CGSize size = CGSizeMake(rect.size.width, rect.size.height);
248+
AXValueRef sizeValue =
249+
AXValueCreate((AXValueType)kAXValueCGSizeType, (const void *)&size);
250+
251+
// Set new position and size
252+
AXUIElementSetAttributeValue(window, kAXPositionAttribute, positionValue);
253+
AXUIElementSetAttributeValue(window, kAXSizeAttribute, sizeValue);
254+
255+
// Clean up AXValue and AXUIElement objects
256+
CFRelease(positionValue);
257+
CFRelease(sizeValue);
258+
CFRelease(window);
259+
CFRelease(app);
260+
261+
// Return true to indicate successful resize
262+
return true;
263+
} else {
264+
// NSLog(@"Could not resize window with window handle %lld", windowHandle);
265+
CFRelease(app);
266+
return false;
267+
}
268+
269+
return YES;
270+
}
271+

Diff for: src/main.cc

+45
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,49 @@ Napi::String _getWindowTitle(const Napi::CallbackInfo &info) {
646646
return Napi::String::New(env, getWindowTitle(windowHandle));
647647
}
648648

649+
Napi::Boolean _focusWindow(const Napi::CallbackInfo &info) {
650+
Napi::Env env = info.Env();
651+
652+
WindowHandle windowHandle = info[0].As<Napi::Number>().Int64Value();
653+
654+
bool result = focusWindow(windowHandle);
655+
656+
return Napi::Boolean::New(env, result);
657+
}
658+
659+
Napi::Boolean _resizeWindow(const Napi::CallbackInfo& info) {
660+
Napi::Env env = info.Env();
661+
662+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsObject()) {
663+
Napi::TypeError::New(env, "Invalid arguments. Expected handle (number) and rect (object).").ThrowAsJavaScriptException();
664+
return Napi::Boolean::New(env, false);
665+
}
666+
667+
WindowHandle windowHandle = info[0].As<Napi::Number>().Int64Value();
668+
MMRect windowRect = getWindowRect(windowHandle);
669+
Napi::Object rectObj = info[1].As<Napi::Object>();
670+
671+
if (!rectObj.Has("x") || !rectObj.Has("y") || !rectObj.Has("width") || !rectObj.Has("height")) {
672+
Napi::TypeError::New(env, "Invalid rect object. Must have 'x', 'y', 'width', and 'height' properties.").ThrowAsJavaScriptException();
673+
return Napi::Boolean::New(env, false);
674+
}
675+
676+
int64_t x = rectObj.Get("x").As<Napi::Number>().Int64Value();
677+
int64_t y = rectObj.Get("y").As<Napi::Number>().Int64Value();
678+
int64_t width = rectObj.Get("width").As<Napi::Number>().Int64Value();
679+
int64_t height = rectObj.Get("height").As<Napi::Number>().Int64Value();
680+
681+
windowRect.origin.x = x;
682+
windowRect.origin.y = y;
683+
windowRect.size.width = width;
684+
windowRect.size.height = height;
685+
686+
bool resizeResult = resizeWindow(windowHandle, windowRect);
687+
688+
return Napi::Boolean::New(env, resizeResult);
689+
}
690+
691+
649692
Napi::Object _captureScreen(const Napi::CallbackInfo &info) {
650693
Napi::Env env = info.Env();
651694

@@ -727,6 +770,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
727770
exports.Set(Napi::String::New(env, "getActiveWindow"), Napi::Function::New(env, _getActiveWindow));
728771
exports.Set(Napi::String::New(env, "getWindowRect"), Napi::Function::New(env, _getWindowRect));
729772
exports.Set(Napi::String::New(env, "getWindowTitle"), Napi::Function::New(env, _getWindowTitle));
773+
exports.Set(Napi::String::New(env, "focusWindow"), Napi::Function::New(env, _focusWindow));
774+
exports.Set(Napi::String::New(env, "resizeWindow"), Napi::Function::New(env, _resizeWindow));
730775
exports.Set(Napi::String::New(env, "captureScreen"), Napi::Function::New(env, _captureScreen));
731776
exports.Set(Napi::String::New(env, "getXDisplayName"), Napi::Function::New(env, _getXDisplayName));
732777
exports.Set(Napi::String::New(env, "setXDisplayName"), Napi::Function::New(env, _setXDisplayName));

0 commit comments

Comments
 (0)