Skip to content

Commit 2a328d0

Browse files
committed
feat: Add screen.getDisplays() to help with multi-monitor automation.
1 parent 2de732b commit 2a328d0

9 files changed

Lines changed: 440 additions & 39 deletions

File tree

index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ export interface ScreenPoint {
1010
y: number
1111
}
1212

13+
export interface Display {
14+
id: number
15+
x: number
16+
y: number
17+
width: number
18+
height: number
19+
isMain: boolean
20+
}
21+
1322
export interface BitmapSearchOptions {
1423
x?: number
1524
y?: number
@@ -85,6 +94,7 @@ export function scrollMouse(x: number, y: number) : void
8594
export function getMousePos(): { x: number, y: number }
8695
export function getPixelColor(x: number, y: number): string
8796
export function getScreenSize(): { width: number, height: number }
97+
export function getDisplays(): Display[]
8898

8999
export var screen: Screen
90100
export var image: ImageModule

src/mouse.c

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,9 @@ static double crude_hypot(double x, double y)
371371
return ((M_SQRT2 - 1.0) * small) + big;
372372
}
373373

374-
bool smoothlyMoveMouse(MMPoint endPoint,double speed)
374+
bool smoothlyMoveMouse(MMSignedPoint endPoint,double speed)
375375
{
376376
MMSignedPoint pos = getMousePos();
377-
MMSize screenSize = getMainDisplaySize();
378377
double velo_x = 0.0, velo_y = 0.0;
379378
double distance;
380379

@@ -390,14 +389,8 @@ bool smoothlyMoveMouse(MMPoint endPoint,double speed)
390389
velo_x /= veloDistance;
391390
velo_y /= veloDistance;
392391

393-
pos.x += floor(velo_x + 0.5);
394-
pos.y += floor(velo_y + 0.5);
395-
396-
/* Make sure we are in the screen boundaries!
397-
* (Strange things will happen if we are not.) */
398-
if (pos.x >= (int32_t)screenSize.width || pos.y >= (int32_t)screenSize.height) {
399-
return false;
400-
}
392+
pos.x += (int32_t)floor(velo_x + 0.5);
393+
pos.y += (int32_t)floor(velo_y + 0.5);
401394

402395
moveMouse(pos);
403396

src/mouse.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ void dragMouse(MMSignedPoint point, const MMMouseButton button);
7979
*
8080
* Returns false if unsuccessful (i.e. a point was hit that is outside of the
8181
* screen boundaries), or true if successful. */
82-
bool smoothlyMoveMouse(MMPoint point,double speed);
82+
bool smoothlyMoveMouse(MMSignedPoint point,double speed);
8383

8484
/* Returns the coordinates of the mouse on the current screen. */
8585
MMSignedPoint getMousePos(void);

src/robotjs.cc

Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@ Napi::Value moveMouseSmoothWrapper(const Napi::CallbackInfo& info)
141141
Napi::Error::New(env, "Invalid number of arguments.").ThrowAsJavaScriptException();
142142
return env.Null();
143143
}
144-
size_t x = static_cast<size_t>(info[0].ToNumber().Int32Value());
145-
size_t y = static_cast<size_t>(info[1].ToNumber().Int32Value());
144+
int32_t x = info[0].ToNumber().Int32Value();
145+
int32_t y = info[1].ToNumber().Int32Value();
146146

147-
MMPoint point;
148-
point = MMPointMake(x, y);
147+
MMSignedPoint point;
148+
point = MMSignedPointMake(x, y);
149149
if (info.Length() == 3)
150150
{
151-
size_t speed = static_cast<size_t>(info[2].ToNumber().Int32Value());
151+
double speed = info[2].ToNumber().DoubleValue();
152152
smoothlyMoveMouse(point, speed);
153153
}
154154
else
@@ -785,6 +785,75 @@ static bool parseSizeT(Napi::Env env, Napi::Value value, const char *name, size_
785785
return true;
786786
}
787787

788+
static bool parseInt32(Napi::Env env, Napi::Value value, const char *name, int32_t *result)
789+
{
790+
if (!value.IsNumber()) {
791+
Napi::Error::New(env, std::string(name) + " must be a number.")
792+
.ThrowAsJavaScriptException();
793+
return false;
794+
}
795+
796+
double number = value.As<Napi::Number>().DoubleValue();
797+
if (number < static_cast<double>(INT32_MIN) ||
798+
number > static_cast<double>(INT32_MAX)) {
799+
Napi::Error::New(env, std::string(name) + " is outside the supported range.")
800+
.ThrowAsJavaScriptException();
801+
return false;
802+
}
803+
804+
*result = static_cast<int32_t>(number);
805+
return true;
806+
}
807+
808+
static bool rectFitsOnDisplay(MMDisplay display, int32_t x, int32_t y,
809+
size_t width, size_t height, MMRect *localRect)
810+
{
811+
if (width == 0 || height == 0 ||
812+
width > static_cast<size_t>(INT64_MAX) ||
813+
height > static_cast<size_t>(INT64_MAX)) {
814+
return false;
815+
}
816+
817+
const int64_t left = x;
818+
const int64_t top = y;
819+
const int64_t right = left + static_cast<int64_t>(width);
820+
const int64_t bottom = top + static_cast<int64_t>(height);
821+
const int64_t displayLeft = display.x;
822+
const int64_t displayTop = display.y;
823+
const int64_t displayRight = displayLeft + static_cast<int64_t>(display.width);
824+
const int64_t displayBottom = displayTop + static_cast<int64_t>(display.height);
825+
826+
if (left < displayLeft || top < displayTop ||
827+
right > displayRight || bottom > displayBottom) {
828+
return false;
829+
}
830+
831+
if (localRect != NULL) {
832+
*localRect = MMRectMake(static_cast<size_t>(left - displayLeft),
833+
static_cast<size_t>(top - displayTop),
834+
width,
835+
height);
836+
}
837+
838+
return true;
839+
}
840+
841+
static bool findDisplayForRect(const MMDisplay *displayList, size_t displayCount,
842+
int32_t x, int32_t y, size_t width, size_t height,
843+
MMDisplay *display, MMRect *localRect)
844+
{
845+
for (size_t index = 0; index < displayCount; ++index) {
846+
if (rectFitsOnDisplay(displayList[index], x, y, width, height, localRect)) {
847+
if (display != NULL) {
848+
*display = displayList[index];
849+
}
850+
return true;
851+
}
852+
}
853+
854+
return false;
855+
}
856+
788857
static bool parseToleranceOption(Napi::Env env, Napi::Object options, float *tolerance)
789858
{
790859
if (!options.Has("tolerance")) {
@@ -1132,6 +1201,35 @@ Napi::Value getScreenSizeWrapper(const Napi::CallbackInfo& info)
11321201
return obj;
11331202
}
11341203

1204+
Napi::Value getDisplaysWrapper(const Napi::CallbackInfo& info)
1205+
{
1206+
Napi::Env env = info.Env();
1207+
size_t displayCount = 0;
1208+
MMDisplay *displayList = getDisplayList(&displayCount);
1209+
1210+
if (displayList == NULL || displayCount == 0) {
1211+
Napi::Error::New(env, "Failed to enumerate active displays.")
1212+
.ThrowAsJavaScriptException();
1213+
return env.Null();
1214+
}
1215+
1216+
Napi::Array displays = Napi::Array::New(env, displayCount);
1217+
1218+
for (size_t index = 0; index < displayCount; ++index) {
1219+
Napi::Object display = Napi::Object::New(env);
1220+
display.Set("id", Napi::Number::New(env, displayList[index].id));
1221+
display.Set("x", Napi::Number::New(env, displayList[index].x));
1222+
display.Set("y", Napi::Number::New(env, displayList[index].y));
1223+
display.Set("width", Napi::Number::New(env, displayList[index].width));
1224+
display.Set("height", Napi::Number::New(env, displayList[index].height));
1225+
display.Set("isMain", Napi::Boolean::New(env, displayList[index].isMain));
1226+
displays.Set(index, display);
1227+
}
1228+
1229+
destroyDisplayList(displayList);
1230+
return displays;
1231+
}
1232+
11351233
Napi::Value getXDisplayNameWrapper(const Napi::CallbackInfo& info)
11361234
{
11371235
Napi::Env env = info.Env();
@@ -1162,24 +1260,26 @@ Napi::Value setXDisplayNameWrapper(const Napi::CallbackInfo& info)
11621260
Napi::Value captureScreenWrapper(const Napi::CallbackInfo& info)
11631261
{
11641262
Napi::Env env = info.Env();
1165-
size_t x = 0;
1166-
size_t y = 0;
1263+
int32_t x = 0;
1264+
int32_t y = 0;
11671265
size_t w;
11681266
size_t h;
1169-
MMSize displaySize = getMainDisplaySize();
1267+
bool hasBounds = false;
11701268

11711269
//If user has provided screen coords, use them!
11721270
if (info.Length() == 4)
11731271
{
1174-
if (!parseSizeT(env, info[0], "x", &x) ||
1175-
!parseSizeT(env, info[1], "y", &y) ||
1272+
hasBounds = true;
1273+
if (!parseInt32(env, info[0], "x", &x) ||
1274+
!parseInt32(env, info[1], "y", &y) ||
11761275
!parseSizeT(env, info[2], "width", &w) ||
11771276
!parseSizeT(env, info[3], "height", &h)) {
11781277
return env.Null();
11791278
}
11801279
}
11811280
else if (info.Length() == 0)
11821281
{
1282+
MMSize displaySize = getMainDisplaySize();
11831283
//We're getting the full screen.
11841284
w = displaySize.width;
11851285
h = displaySize.height;
@@ -1190,15 +1290,39 @@ Napi::Value captureScreenWrapper(const Napi::CallbackInfo& info)
11901290
return env.Null();
11911291
}
11921292

1193-
if (w == 0 || h == 0 ||
1194-
x > displaySize.width || y > displaySize.height ||
1195-
w > displaySize.width - x || h > displaySize.height - y) {
1196-
Napi::Error::New(env, "Requested capture rect is outside the main screen's dimensions.")
1293+
if (w == 0 || h == 0) {
1294+
Napi::Error::New(env, "Requested capture rect is outside the active displays' dimensions.")
11971295
.ThrowAsJavaScriptException();
11981296
return env.Null();
11991297
}
12001298

1201-
MMBitmapRef bitmap = copyMMBitmapFromDisplayInRect(MMRectMake(x, y, w, h));
1299+
MMBitmapRef bitmap = NULL;
1300+
1301+
if (hasBounds) {
1302+
size_t displayCount = 0;
1303+
MMDisplay *displayList = getDisplayList(&displayCount);
1304+
MMDisplay display;
1305+
MMRect localRect;
1306+
1307+
if (displayList == NULL || displayCount == 0) {
1308+
Napi::Error::New(env, "Failed to enumerate active displays.")
1309+
.ThrowAsJavaScriptException();
1310+
return env.Null();
1311+
}
1312+
1313+
if (!findDisplayForRect(displayList, displayCount, x, y, w, h, &display, &localRect)) {
1314+
destroyDisplayList(displayList);
1315+
Napi::Error::New(env, "Requested capture rect is outside the active displays' dimensions.")
1316+
.ThrowAsJavaScriptException();
1317+
return env.Null();
1318+
}
1319+
1320+
bitmap = copyMMBitmapFromDisplayInRectOnDisplay(display, localRect);
1321+
destroyDisplayList(displayList);
1322+
} else {
1323+
bitmap = copyMMBitmapFromDisplayInRect(MMRectMake(0, 0, w, h));
1324+
}
1325+
12021326
if (bitmap == NULL) {
12031327
Napi::Error::New(env, "Failed to capture the requested screen rect.")
12041328
.ThrowAsJavaScriptException();
@@ -1657,6 +1781,9 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports)
16571781
exports.Set(Napi::String::New(env, "getScreenSize"),
16581782
Napi::Function::New(env, getScreenSizeWrapper));
16591783

1784+
exports.Set(Napi::String::New(env, "getDisplays"),
1785+
Napi::Function::New(env, getDisplaysWrapper));
1786+
16601787
exports.Set(Napi::String::New(env, "captureScreen"),
16611788
Napi::Function::New(env, captureScreenWrapper));
16621789

0 commit comments

Comments
 (0)