appium-mac2-driver 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/WebDriverAgentMac/IntegrationTests/AMWindowTests.m +427 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMWindowCommands.m +63 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBDebugCommands.m +2 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.h +3 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.m +13 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.h +4 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.m +23 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.h +18 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.m +43 -28
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.h +111 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.m +780 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.h +61 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.m +108 -0
- package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +20 -0
- package/build/lib/commands/native-record-screen.d.ts +2 -0
- package/build/lib/commands/native-record-screen.d.ts.map +1 -1
- package/build/lib/commands/native-record-screen.js.map +1 -1
- package/build/lib/commands/windows.d.ts +22 -0
- package/build/lib/commands/windows.d.ts.map +1 -0
- package/build/lib/commands/windows.js +32 -0
- package/build/lib/commands/windows.js.map +1 -0
- package/build/lib/driver.d.ts +12 -0
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +3 -0
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts +9 -0
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +9 -0
- package/build/lib/execute-method-map.js.map +1 -1
- package/build/lib/types.d.ts +26 -0
- package/build/lib/types.d.ts.map +1 -1
- package/lib/commands/native-record-screen.ts +2 -0
- package/lib/commands/windows.ts +44 -0
- package/lib/driver.ts +4 -0
- package/lib/execute-method-map.ts +9 -0
- package/lib/types.ts +28 -0
- package/node_modules/bare-url/README.md +4 -0
- package/node_modules/bare-url/index.d.ts +3 -0
- package/node_modules/bare-url/index.js +42 -18
- package/node_modules/bare-url/lib/errors.js +4 -0
- package/node_modules/bare-url/package.json +1 -1
- package/node_modules/bare-url/prebuilds/android-arm/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-ia32/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-x64/bare-url.bare +0 -0
- package/package.json +3 -2
- package/scripts/setup-accessibility.mjs +175 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
* you may not use this file except in compliance with the License.
|
|
4
|
+
* See the NOTICE file distributed with this work for additional
|
|
5
|
+
* information regarding copyright ownership.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
#import "AMWindowUtils.h"
|
|
18
|
+
|
|
19
|
+
#import <AppKit/AppKit.h>
|
|
20
|
+
#import <ApplicationServices/ApplicationServices.h>
|
|
21
|
+
|
|
22
|
+
#import "AMGeometryUtils.h"
|
|
23
|
+
#import "AMScreenUtils.h"
|
|
24
|
+
#import "AMSnapshotUtils.h"
|
|
25
|
+
#import "AMXCTAccessibility.h"
|
|
26
|
+
#import "XCUIApplication+AMHelpers.h"
|
|
27
|
+
#import "XCUIElementQuery+AMHelpers.h"
|
|
28
|
+
|
|
29
|
+
NSString *const AMWindowErrorDomain = @"AMWindowErrorDomain";
|
|
30
|
+
NSString *const AMWindowStateNormal = @"normal";
|
|
31
|
+
NSString *const AMWindowStateMinimized = @"minimized";
|
|
32
|
+
NSString *const AMWindowStateMaximized = @"maximized";
|
|
33
|
+
NSString *const AMWindowStateFullscreen = @"fullscreen";
|
|
34
|
+
|
|
35
|
+
static const NSTimeInterval AM_WINDOW_STATE_TIMEOUT = 3.0;
|
|
36
|
+
static NSString *const AM_GEOMETRY_ACCESS_DENIED_MESSAGE = @"Changing the window geometry requires the Accessibility API. "
|
|
37
|
+
@"Grant the Accessibility permission to the WebDriverAgentRunner app and restart it";
|
|
38
|
+
static NSString *const AM_MINIMIZE_BUTTON_ID = @"_XCUI:MinimizeWindow";
|
|
39
|
+
static NSString *const AM_FULLSCREEN_BUTTON_ID = @"_XCUI:FullScreenWindow";
|
|
40
|
+
static NSString *const AM_DOCK_BUNDLE_ID = @"com.apple.dock";
|
|
41
|
+
static NSString *const AM_MINIMIZED_DOCK_ITEM_SUBROLE = @"AXMinimizedWindowDockItem";
|
|
42
|
+
|
|
43
|
+
static NSString *const AM_KEY_TITLE = @"title";
|
|
44
|
+
static NSString *const AM_KEY_RECT = @"rect";
|
|
45
|
+
static NSString *const AM_KEY_MAIN = @"main";
|
|
46
|
+
static NSString *const AM_KEY_FOCUSED = @"focused";
|
|
47
|
+
static NSString *const AM_KEY_MINIMIZED = @"minimized";
|
|
48
|
+
static NSString *const AM_KEY_FULLSCREEN = @"fullscreen";
|
|
49
|
+
|
|
50
|
+
static BOOL gUsesUnprivilegedRoutes = YES;
|
|
51
|
+
|
|
52
|
+
static void AMAssignError(NSError **error, NSError *value)
|
|
53
|
+
{
|
|
54
|
+
if (NULL != error) {
|
|
55
|
+
*error = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The display with the biggest intersection with the rect. Falls back to the main one
|
|
60
|
+
static AMScreenProperties *_Nullable AMDisplayForRect(CGRect rect)
|
|
61
|
+
{
|
|
62
|
+
AMScreenProperties *result = nil;
|
|
63
|
+
CGFloat maxArea = 0;
|
|
64
|
+
for (AMScreenProperties *screen in AMListScreens()) {
|
|
65
|
+
CGRect intersection = CGRectIntersection(screen.bounds, rect);
|
|
66
|
+
CGFloat area = CGRectIsNull(intersection) ? 0 : intersection.size.width * intersection.size.height;
|
|
67
|
+
if (area > maxArea) {
|
|
68
|
+
maxArea = area;
|
|
69
|
+
result = screen;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (nil == result) {
|
|
73
|
+
for (AMScreenProperties *screen in AMListScreens()) {
|
|
74
|
+
if (screen.isMain) {
|
|
75
|
+
return screen;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static id _Nullable AMCopyAXAttribute(AXUIElementRef element, CFStringRef name)
|
|
83
|
+
{
|
|
84
|
+
CFTypeRef value = NULL;
|
|
85
|
+
if (kAXErrorSuccess != AXUIElementCopyAttributeValue(element, name, &value) || NULL == value) {
|
|
86
|
+
return nil;
|
|
87
|
+
}
|
|
88
|
+
return CFBridgingRelease(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static NSNumber *_Nullable AMAXBool(AXUIElementRef element, CFStringRef name)
|
|
92
|
+
{
|
|
93
|
+
id value = AMCopyAXAttribute(element, name);
|
|
94
|
+
return [value isKindOfClass:NSNumber.class] ? value : nil;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static BOOL AMAXSetBool(AXUIElementRef element, CFStringRef name, BOOL value)
|
|
98
|
+
{
|
|
99
|
+
return kAXErrorSuccess == AXUIElementSetAttributeValue(element, name, value ? kCFBooleanTrue : kCFBooleanFalse);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static BOOL AMAXIsSettable(AXUIElementRef element, CFStringRef name)
|
|
103
|
+
{
|
|
104
|
+
Boolean settable = false;
|
|
105
|
+
return kAXErrorSuccess == AXUIElementIsAttributeSettable(element, name, &settable) && settable;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static BOOL AMAXSetGeometry(AXUIElementRef element, CFStringRef name, AXValueType type, const void *geometry)
|
|
109
|
+
{
|
|
110
|
+
AXValueRef value = AXValueCreate(type, geometry);
|
|
111
|
+
if (NULL == value) {
|
|
112
|
+
return NO;
|
|
113
|
+
}
|
|
114
|
+
AXError err = AXUIElementSetAttributeValue(element, name, value);
|
|
115
|
+
CFRelease(value);
|
|
116
|
+
return kAXErrorSuccess == err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static BOOL AMAXReadRect(AXUIElementRef element, CGRect *rect)
|
|
120
|
+
{
|
|
121
|
+
CGPoint origin = CGPointZero;
|
|
122
|
+
CGSize size = CGSizeZero;
|
|
123
|
+
id position = AMCopyAXAttribute(element, kAXPositionAttribute);
|
|
124
|
+
id dimensions = AMCopyAXAttribute(element, kAXSizeAttribute);
|
|
125
|
+
if (nil == position || nil == dimensions
|
|
126
|
+
|| !AXValueGetValue((__bridge AXValueRef)position, kAXValueTypeCGPoint, &origin)
|
|
127
|
+
|| !AXValueGetValue((__bridge AXValueRef)dimensions, kAXValueTypeCGSize, &size)) {
|
|
128
|
+
return NO;
|
|
129
|
+
}
|
|
130
|
+
*rect = CGRectMake(origin.x, origin.y, size.width, size.height);
|
|
131
|
+
return YES;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static BOOL AMWaitUntil(BOOL (^condition)(void))
|
|
135
|
+
{
|
|
136
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:AM_WINDOW_STATE_TIMEOUT];
|
|
137
|
+
while (!condition()) {
|
|
138
|
+
if ([deadline timeIntervalSinceNow] <= 0) {
|
|
139
|
+
return NO;
|
|
140
|
+
}
|
|
141
|
+
[NSThread sleepForTimeInterval:0.1];
|
|
142
|
+
}
|
|
143
|
+
return YES;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static CGPoint AMPointFromAXDict(NSDictionary *dict)
|
|
147
|
+
{
|
|
148
|
+
return CGPointMake([dict[@"X"] doubleValue], [dict[@"Y"] doubleValue]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static CGSize AMSizeFromAXDict(NSDictionary *dict)
|
|
152
|
+
{
|
|
153
|
+
return CGSizeMake([dict[@"Width"] doubleValue], [dict[@"Height"] doubleValue]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
Identifies a window by its XCTest accessibility element. Minimized windows have no
|
|
158
|
+
XCUIElement, because they are not a part of the element hierarchy.
|
|
159
|
+
*/
|
|
160
|
+
@interface AMWindowHandle : NSObject
|
|
161
|
+
@property (nonatomic, nullable) XCUIElement *element;
|
|
162
|
+
@property (nonatomic, nullable) id accessibilityElement;
|
|
163
|
+
@property (nonatomic, copy, nullable) NSString *fallbackTitle;
|
|
164
|
+
@property (nonatomic) CGRect fallbackFrame;
|
|
165
|
+
|
|
166
|
+
/** Takes the ownership of the AX element. Used if XCTest accessibility elements are not available */
|
|
167
|
+
- (void)adoptAXElement:(AXUIElementRef CF_RELEASES_ARGUMENT)ref;
|
|
168
|
+
/** The AX element is owned by the handle. NULL if the Accessibility permission is not granted */
|
|
169
|
+
- (nullable AXUIElementRef)axElement;
|
|
170
|
+
@end
|
|
171
|
+
|
|
172
|
+
@implementation AMWindowHandle {
|
|
173
|
+
AXUIElementRef _axElement;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
- (void)adoptAXElement:(AXUIElementRef CF_RELEASES_ARGUMENT)ref
|
|
177
|
+
{
|
|
178
|
+
if (NULL != _axElement) {
|
|
179
|
+
CFRelease(_axElement);
|
|
180
|
+
}
|
|
181
|
+
_axElement = ref;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
- (nullable AXUIElementRef)axElement
|
|
185
|
+
{
|
|
186
|
+
if (NULL == _axElement && nil != self.accessibilityElement) {
|
|
187
|
+
_axElement = [AMSnapshotUtils createAXElementWithAccessibilityElement:self.accessibilityElement];
|
|
188
|
+
}
|
|
189
|
+
return _axElement;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
- (void)dealloc
|
|
193
|
+
{
|
|
194
|
+
if (NULL != _axElement) {
|
|
195
|
+
CFRelease(_axElement);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@end
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
Reads the window attributes (title, rect, main, focused, minimized, fullscreen). XCTest accessibility
|
|
203
|
+
is used first, because it does not need the Accessibility permission. Returns nil if the window cannot be read.
|
|
204
|
+
*/
|
|
205
|
+
static NSDictionary<NSString *, id> *_Nullable AMReadWindow(AMWindowHandle *handle)
|
|
206
|
+
{
|
|
207
|
+
NSMutableDictionary<NSString *, id> *result = [NSMutableDictionary dictionary];
|
|
208
|
+
if (gUsesUnprivilegedRoutes && nil != handle.accessibilityElement) {
|
|
209
|
+
NSDictionary *attrs = [AMXCTAccessibility attributes:@[@"AXTitle", @"AXPosition", @"AXSize", @"AXMinimized",
|
|
210
|
+
@"AXFullScreen", @"AXMain", @"AXFocused"]
|
|
211
|
+
ofElement:handle.accessibilityElement];
|
|
212
|
+
if (attrs.count > 0) {
|
|
213
|
+
if ([attrs[@"AXTitle"] isKindOfClass:NSString.class]) {
|
|
214
|
+
result[AM_KEY_TITLE] = attrs[@"AXTitle"];
|
|
215
|
+
}
|
|
216
|
+
if ([attrs[@"AXPosition"] isKindOfClass:NSDictionary.class] && [attrs[@"AXSize"] isKindOfClass:NSDictionary.class]) {
|
|
217
|
+
CGPoint origin = AMPointFromAXDict(attrs[@"AXPosition"]);
|
|
218
|
+
CGSize size = AMSizeFromAXDict(attrs[@"AXSize"]);
|
|
219
|
+
result[AM_KEY_RECT] = [NSValue valueWithRect:CGRectMake(origin.x, origin.y, size.width, size.height)];
|
|
220
|
+
}
|
|
221
|
+
NSDictionary *keys = @{AM_KEY_MINIMIZED: @"AXMinimized", AM_KEY_FULLSCREEN: @"AXFullScreen",
|
|
222
|
+
AM_KEY_MAIN: @"AXMain", AM_KEY_FOCUSED: @"AXFocused"};
|
|
223
|
+
for (NSString *key in keys) {
|
|
224
|
+
if ([attrs[keys[key]] isKindOfClass:NSNumber.class]) {
|
|
225
|
+
result[key] = attrs[keys[key]];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result.copy;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
AXUIElementRef ref = [handle axElement];
|
|
232
|
+
if (NULL == ref) {
|
|
233
|
+
return nil;
|
|
234
|
+
}
|
|
235
|
+
id title = AMCopyAXAttribute(ref, kAXTitleAttribute);
|
|
236
|
+
if ([title isKindOfClass:NSString.class]) {
|
|
237
|
+
result[AM_KEY_TITLE] = title;
|
|
238
|
+
}
|
|
239
|
+
CGRect rect;
|
|
240
|
+
if (AMAXReadRect(ref, &rect)) {
|
|
241
|
+
result[AM_KEY_RECT] = [NSValue valueWithRect:rect];
|
|
242
|
+
}
|
|
243
|
+
NSDictionary *keys = @{AM_KEY_MINIMIZED: (__bridge NSString *)kAXMinimizedAttribute, AM_KEY_FULLSCREEN: @"AXFullScreen",
|
|
244
|
+
AM_KEY_MAIN: (__bridge NSString *)kAXMainAttribute, AM_KEY_FOCUSED: (__bridge NSString *)kAXFocusedAttribute};
|
|
245
|
+
for (NSString *key in keys) {
|
|
246
|
+
NSNumber *value = AMAXBool(ref, (__bridge CFStringRef)keys[key]);
|
|
247
|
+
if (nil != value) {
|
|
248
|
+
result[key] = value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return result.copy;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static BOOL AMWindowFlag(AMWindowHandle *handle, NSString *key)
|
|
255
|
+
{
|
|
256
|
+
return [AMReadWindow(handle)[key] boolValue];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static BOOL AMWaitForWindowFlag(AMWindowHandle *handle, NSString *key, BOOL expected)
|
|
260
|
+
{
|
|
261
|
+
return AMWaitUntil(^BOOL{ return AMWindowFlag(handle, key) == expected; });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@implementation AMWindowUtils
|
|
265
|
+
|
|
266
|
+
+ (BOOL)usesUnprivilegedRoutes
|
|
267
|
+
{
|
|
268
|
+
return gUsesUnprivilegedRoutes;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
+ (void)setUsesUnprivilegedRoutes:(BOOL)value
|
|
272
|
+
{
|
|
273
|
+
gUsesUnprivilegedRoutes = value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
+ (NSArray<XCUIElement *> *)windowsOfApplication:(XCUIApplication *)application
|
|
277
|
+
{
|
|
278
|
+
return [application.windows am_allMatches];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
+ (nullable AMWindowHandle *)handleWithWindow:(XCUIElement *)window
|
|
282
|
+
{
|
|
283
|
+
id<XCUIElementSnapshot> snapshot = [window snapshotWithError:nil];
|
|
284
|
+
if (nil == snapshot) {
|
|
285
|
+
return nil;
|
|
286
|
+
}
|
|
287
|
+
AMWindowHandle *handle = [AMWindowHandle new];
|
|
288
|
+
handle.element = window;
|
|
289
|
+
handle.accessibilityElement = [AMXCTAccessibility accessibilityElementWithSnapshot:snapshot];
|
|
290
|
+
handle.fallbackTitle = snapshot.title;
|
|
291
|
+
handle.fallbackFrame = snapshot.frame;
|
|
292
|
+
if (nil == handle.accessibilityElement) {
|
|
293
|
+
// XCTest accessibility elements are not available, so AX has to be used
|
|
294
|
+
[handle adoptAXElement:[AMSnapshotUtils createAXElementWithSnapshot:snapshot]];
|
|
295
|
+
}
|
|
296
|
+
return handle;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
+ (nullable XCUIElement *)mainWindowOfApplication:(XCUIApplication *)application
|
|
300
|
+
{
|
|
301
|
+
NSArray<XCUIElement *> *windows = [self windowsOfApplication:application];
|
|
302
|
+
XCUIElement *focused = nil;
|
|
303
|
+
for (XCUIElement *window in windows) {
|
|
304
|
+
AMWindowHandle *handle = [self handleWithWindow:window];
|
|
305
|
+
NSDictionary *attrs = nil == handle ? nil : AMReadWindow(handle);
|
|
306
|
+
if (nil == attrs) {
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if ([attrs[AM_KEY_MAIN] boolValue]) {
|
|
310
|
+
return window;
|
|
311
|
+
}
|
|
312
|
+
if ([attrs[AM_KEY_FOCUSED] boolValue] && nil == focused) {
|
|
313
|
+
focused = window;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return focused ?: windows.firstObject;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
+ (nullable NSDictionary<NSString *, id> *)infoForWindow:(XCUIElement *)window
|
|
320
|
+
{
|
|
321
|
+
AMWindowHandle *handle = [self handleWithWindow:window];
|
|
322
|
+
return nil == handle ? nil : [self infoForHandle:handle];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
+ (NSDictionary<NSString *, id> *)infoForHandle:(AMWindowHandle *)handle
|
|
326
|
+
{
|
|
327
|
+
NSDictionary *attrs = AMReadWindow(handle);
|
|
328
|
+
NSString *title = attrs[AM_KEY_TITLE] ?: handle.fallbackTitle;
|
|
329
|
+
CGRect frame = nil != attrs[AM_KEY_RECT] ? [attrs[AM_KEY_RECT] rectValue] : handle.fallbackFrame;
|
|
330
|
+
NSNumber *null = (NSNumber *)NSNull.null;
|
|
331
|
+
// Whether the window is movable/resizable can only be asked through the Accessibility API
|
|
332
|
+
AXUIElementRef ref = [handle axElement];
|
|
333
|
+
NSMutableDictionary<NSString *, id> *info = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
334
|
+
@"title": title ?: @"",
|
|
335
|
+
@"isMain": attrs[AM_KEY_MAIN] ?: null,
|
|
336
|
+
@"isFocused": attrs[AM_KEY_FOCUSED] ?: null,
|
|
337
|
+
@"isMinimized": attrs[AM_KEY_MINIMIZED] ?: null,
|
|
338
|
+
@"isFullscreen": attrs[AM_KEY_FULLSCREEN] ?: null,
|
|
339
|
+
@"isMovable": NULL == ref ? null : @(AMAXIsSettable(ref, kAXPositionAttribute)),
|
|
340
|
+
@"isResizable": NULL == ref ? null : @(AMAXIsSettable(ref, kAXSizeAttribute)),
|
|
341
|
+
@"rect": AMCGRectToDict(frame),
|
|
342
|
+
}];
|
|
343
|
+
AMScreenProperties *display = AMDisplayForRect(frame);
|
|
344
|
+
info[@"displayId"] = nil == display ? NSNull.null : @(display.identifier);
|
|
345
|
+
return info.copy;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
+ (nullable NSDictionary<NSString *, id> *)adjustWindow:(XCUIElement *)window
|
|
349
|
+
inApplication:(XCUIApplication *)application
|
|
350
|
+
withArguments:(NSDictionary<NSString *, id> *)arguments
|
|
351
|
+
error:(NSError **)error
|
|
352
|
+
{
|
|
353
|
+
NSString *state = nil;
|
|
354
|
+
NSDictionary<NSString *, NSNumber *> *rect = nil;
|
|
355
|
+
AMScreenProperties *display = nil;
|
|
356
|
+
if (![self parseArguments:arguments state:&state rect:&rect display:&display error:error]) {
|
|
357
|
+
return nil;
|
|
358
|
+
}
|
|
359
|
+
id<XCUIElementSnapshot> snapshot = [window snapshotWithError:nil];
|
|
360
|
+
if (nil == snapshot) {
|
|
361
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorNotFound
|
|
362
|
+
format:@"The window is not available anymore. It might have been closed or minimized"]);
|
|
363
|
+
return nil;
|
|
364
|
+
}
|
|
365
|
+
if (XCUIElementTypeWindow != snapshot.elementType) {
|
|
366
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorInvalidArgument format:@"The element is not a window"]);
|
|
367
|
+
return nil;
|
|
368
|
+
}
|
|
369
|
+
AMWindowHandle *handle = [self handleWithWindow:window];
|
|
370
|
+
return [self adjustHandle:handle application:application state:state rect:rect display:display error:error];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
+ (nullable NSDictionary<NSString *, id> *)adjustMinimizedWindowOfApplication:(XCUIApplication *)application
|
|
374
|
+
withArguments:(NSDictionary<NSString *, id> *)arguments
|
|
375
|
+
error:(NSError **)error
|
|
376
|
+
{
|
|
377
|
+
NSString *state = nil;
|
|
378
|
+
NSDictionary<NSString *, NSNumber *> *rect = nil;
|
|
379
|
+
AMScreenProperties *display = nil;
|
|
380
|
+
if (![self parseArguments:arguments state:&state rect:&rect display:&display error:error]) {
|
|
381
|
+
return nil;
|
|
382
|
+
}
|
|
383
|
+
AMWindowHandle *handle = [self minimizedWindowsOfApplication:application].firstObject;
|
|
384
|
+
if (nil == handle) {
|
|
385
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorNotFound format:@"The application has no minimized windows"]);
|
|
386
|
+
return nil;
|
|
387
|
+
}
|
|
388
|
+
return [self adjustHandle:handle application:application state:state rect:rect display:display error:error];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
+ (NSUInteger)minimizedWindowsCountOfApplication:(XCUIApplication *)application
|
|
392
|
+
{
|
|
393
|
+
return [self minimizedWindowsOfApplication:application].count;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#pragma mark - Private
|
|
397
|
+
|
|
398
|
+
+ (NSError *)windowErrorWithCode:(AMWindowErrorCode)code format:(NSString *)format, ... NS_FORMAT_FUNCTION(2,3)
|
|
399
|
+
{
|
|
400
|
+
va_list args;
|
|
401
|
+
va_start(args, format);
|
|
402
|
+
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
|
|
403
|
+
va_end(args);
|
|
404
|
+
return [NSError errorWithDomain:AMWindowErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: message}];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Validates the arguments and extracts the geometry, the state and the target display
|
|
408
|
+
+ (BOOL)parseArguments:(NSDictionary<NSString *, id> *)arguments
|
|
409
|
+
state:(NSString * _Nullable *)outState
|
|
410
|
+
rect:(NSDictionary<NSString *, NSNumber *> * _Nullable *)outRect
|
|
411
|
+
display:(AMScreenProperties * _Nullable *)outDisplay
|
|
412
|
+
error:(NSError **)error
|
|
413
|
+
{
|
|
414
|
+
NSMutableDictionary<NSString *, NSNumber *> *rect = [NSMutableDictionary dictionary];
|
|
415
|
+
for (NSString *key in @[@"x", @"y", @"width", @"height"]) {
|
|
416
|
+
id value = arguments[key];
|
|
417
|
+
if (nil == value || [value isKindOfClass:NSNull.class]) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (![value isKindOfClass:NSNumber.class]
|
|
421
|
+
|| (([key hasPrefix:@"w"] || [key hasPrefix:@"h"]) && [value doubleValue] <= 0)) {
|
|
422
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorInvalidArgument
|
|
423
|
+
format:@"'%@' must be a valid number, got '%@'", key, value]);
|
|
424
|
+
return NO;
|
|
425
|
+
}
|
|
426
|
+
rect[key] = value;
|
|
427
|
+
}
|
|
428
|
+
id rawDisplayId = arguments[@"displayId"];
|
|
429
|
+
if (nil != rawDisplayId && ![rawDisplayId isKindOfClass:NSNull.class] && ![rawDisplayId isKindOfClass:NSNumber.class]) {
|
|
430
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorInvalidArgument
|
|
431
|
+
format:@"'displayId' must be a number, got '%@'", rawDisplayId]);
|
|
432
|
+
return NO;
|
|
433
|
+
}
|
|
434
|
+
NSNumber *displayId = [rawDisplayId isKindOfClass:NSNumber.class] ? rawDisplayId : nil;
|
|
435
|
+
NSString *state = arguments[@"state"];
|
|
436
|
+
if ([state isKindOfClass:NSNull.class]) {
|
|
437
|
+
state = nil;
|
|
438
|
+
}
|
|
439
|
+
NSArray<NSString *> *states = @[AMWindowStateNormal, AMWindowStateMinimized, AMWindowStateMaximized, AMWindowStateFullscreen];
|
|
440
|
+
if (nil != state && ![states containsObject:state]) {
|
|
441
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorInvalidArgument
|
|
442
|
+
format:@"'state' must be one of: %@. Got '%@'", [states componentsJoinedByString:@", "], state]);
|
|
443
|
+
return NO;
|
|
444
|
+
}
|
|
445
|
+
if (nil == state && 0 == rect.count && nil == displayId) {
|
|
446
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorInvalidArgument
|
|
447
|
+
format:@"At least one of 'state', 'x', 'y', 'width', 'height' or 'displayId' must be provided"]);
|
|
448
|
+
return NO;
|
|
449
|
+
}
|
|
450
|
+
BOOL isGeometryFree = [state isEqualToString:AMWindowStateMinimized] || [state isEqualToString:AMWindowStateFullscreen];
|
|
451
|
+
if ((isGeometryFree && (rect.count > 0 || nil != displayId))
|
|
452
|
+
|| ([state isEqualToString:AMWindowStateMaximized] && rect.count > 0)) {
|
|
453
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorInvalidArgument
|
|
454
|
+
format:@"The '%@' state cannot be combined with the explicit window geometry", state]);
|
|
455
|
+
return NO;
|
|
456
|
+
}
|
|
457
|
+
AMScreenProperties *targetDisplay = nil;
|
|
458
|
+
if (nil != displayId) {
|
|
459
|
+
NSMutableArray<NSNumber *> *available = [NSMutableArray array];
|
|
460
|
+
for (AMScreenProperties *screen in AMListScreens()) {
|
|
461
|
+
[available addObject:@(screen.identifier)];
|
|
462
|
+
if (screen.identifier == displayId.longLongValue) {
|
|
463
|
+
targetDisplay = screen;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (nil == targetDisplay) {
|
|
467
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorInvalidArgument
|
|
468
|
+
format:@"The display '%@' is unknown. Available identifiers: %@",
|
|
469
|
+
displayId, [available componentsJoinedByString:@","]]);
|
|
470
|
+
return NO;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
*outState = state;
|
|
474
|
+
*outRect = rect.copy;
|
|
475
|
+
*outDisplay = targetDisplay;
|
|
476
|
+
return YES;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Minimized windows are not in the XCUIElement hierarchy, but the accessibility application keeps them
|
|
480
|
+
+ (NSArray<AMWindowHandle *> *)minimizedWindowsOfApplication:(XCUIApplication *)application
|
|
481
|
+
{
|
|
482
|
+
NSMutableArray<AMWindowHandle *> *result = [NSMutableArray array];
|
|
483
|
+
if (gUsesUnprivilegedRoutes) {
|
|
484
|
+
id appElement = [AMXCTAccessibility accessibilityElementWithApplication:application];
|
|
485
|
+
NSArray *windows = [AMXCTAccessibility attributes:@[@"AXWindows"] ofElement:appElement][@"AXWindows"];
|
|
486
|
+
if ([windows isKindOfClass:NSArray.class]) {
|
|
487
|
+
for (id element in windows) {
|
|
488
|
+
AMWindowHandle *handle = [AMWindowHandle new];
|
|
489
|
+
handle.accessibilityElement = element;
|
|
490
|
+
if (AMWindowFlag(handle, AM_KEY_MINIMIZED)) {
|
|
491
|
+
[result addObject:handle];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return result.copy;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (!AMSnapshotUtils.isAccessibilityTrusted) {
|
|
498
|
+
return @[];
|
|
499
|
+
}
|
|
500
|
+
NSRunningApplication *runningApp = [NSRunningApplication runningApplicationsWithBundleIdentifier:application.am_bundleID].firstObject;
|
|
501
|
+
if (nil == runningApp) {
|
|
502
|
+
return @[];
|
|
503
|
+
}
|
|
504
|
+
AXUIElementRef appRef = AXUIElementCreateApplication(runningApp.processIdentifier);
|
|
505
|
+
id windows = AMCopyAXAttribute(appRef, kAXWindowsAttribute);
|
|
506
|
+
CFRelease(appRef);
|
|
507
|
+
for (id window in [windows isKindOfClass:NSArray.class] ? windows : @[]) {
|
|
508
|
+
AMWindowHandle *handle = [AMWindowHandle new];
|
|
509
|
+
[handle adoptAXElement:(AXUIElementRef)CFRetain((__bridge CFTypeRef)window)];
|
|
510
|
+
if (AMWindowFlag(handle, AM_KEY_MINIMIZED)) {
|
|
511
|
+
[result addObject:handle];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return result.copy;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// The XCUIElement of the window, which is not available while the window is minimized
|
|
518
|
+
+ (nullable XCUIElement *)elementOfHandle:(AMWindowHandle *)handle application:(XCUIApplication *)application
|
|
519
|
+
{
|
|
520
|
+
if (nil != handle.element && nil != [handle.element snapshotWithError:nil]) {
|
|
521
|
+
return handle.element;
|
|
522
|
+
}
|
|
523
|
+
if (nil == handle.accessibilityElement) {
|
|
524
|
+
return nil;
|
|
525
|
+
}
|
|
526
|
+
for (XCUIElement *window in [self windowsOfApplication:application]) {
|
|
527
|
+
id element = [AMXCTAccessibility accessibilityElementWithSnapshot:[window snapshotWithError:nil]];
|
|
528
|
+
if ([element isEqual:handle.accessibilityElement]) {
|
|
529
|
+
handle.element = window;
|
|
530
|
+
return window;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return nil;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Presses the button of the window without moving the mouse
|
|
537
|
+
+ (BOOL)pressButton:(NSString *)identifier ofHandle:(AMWindowHandle *)handle application:(XCUIApplication *)application
|
|
538
|
+
{
|
|
539
|
+
XCUIElement *window = [self elementOfHandle:handle application:application];
|
|
540
|
+
XCUIElement *button = [[window descendantsMatchingType:XCUIElementTypeButton] matchingIdentifier:identifier].firstMatch;
|
|
541
|
+
id element = nil == window || !button.exists
|
|
542
|
+
? nil
|
|
543
|
+
: [AMXCTAccessibility accessibilityElementWithSnapshot:[button snapshotWithError:nil]];
|
|
544
|
+
return nil != element && [AMXCTAccessibility pressElement:element error:nil];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Restores the window by its Dock item. It is only done if the item can be identified unambiguously
|
|
548
|
+
+ (BOOL)restoreHandleThroughDock:(AMWindowHandle *)handle
|
|
549
|
+
{
|
|
550
|
+
NSString *title = AMReadWindow(handle)[AM_KEY_TITLE] ?: handle.fallbackTitle;
|
|
551
|
+
if (0 == title.length) {
|
|
552
|
+
return NO;
|
|
553
|
+
}
|
|
554
|
+
XCUIApplication *dock = [[XCUIApplication alloc] initWithBundleIdentifier:AM_DOCK_BUNDLE_ID];
|
|
555
|
+
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title == %@", title];
|
|
556
|
+
NSMutableArray<XCUIElement *> *candidates = [NSMutableArray array];
|
|
557
|
+
NSMutableArray *elements = [NSMutableArray array];
|
|
558
|
+
for (XCUIElement *item in [[dock descendantsMatchingType:XCUIElementTypeDockItem] matchingPredicate:predicate].allElementsBoundByIndex) {
|
|
559
|
+
id element = [AMXCTAccessibility accessibilityElementWithSnapshot:[item snapshotWithError:nil]];
|
|
560
|
+
NSDictionary *attrs = [AMXCTAccessibility attributes:@[@"AXSubrole"] ofElement:element];
|
|
561
|
+
if (nil != element && [attrs[@"AXSubrole"] isEqual:AM_MINIMIZED_DOCK_ITEM_SUBROLE]) {
|
|
562
|
+
[candidates addObject:item];
|
|
563
|
+
[elements addObject:element];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (1 != candidates.count) {
|
|
567
|
+
// Another minimized window with the same title could be restored by mistake
|
|
568
|
+
return NO;
|
|
569
|
+
}
|
|
570
|
+
// An auto-hidden Dock item is not hittable, so it is pressed without the mouse
|
|
571
|
+
if (![AMXCTAccessibility pressElement:elements.firstObject error:nil]) {
|
|
572
|
+
if (!candidates.firstObject.hittable) {
|
|
573
|
+
return NO;
|
|
574
|
+
}
|
|
575
|
+
[candidates.firstObject click];
|
|
576
|
+
}
|
|
577
|
+
return AMWaitForWindowFlag(handle, AM_KEY_MINIMIZED, NO);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
+ (BOOL)minimizeHandle:(AMWindowHandle *)handle application:(XCUIApplication *)application
|
|
581
|
+
{
|
|
582
|
+
if (gUsesUnprivilegedRoutes
|
|
583
|
+
&& [self pressButton:AM_MINIMIZE_BUTTON_ID ofHandle:handle application:application]
|
|
584
|
+
&& AMWaitForWindowFlag(handle, AM_KEY_MINIMIZED, YES)) {
|
|
585
|
+
return YES;
|
|
586
|
+
}
|
|
587
|
+
AXUIElementRef ref = [handle axElement];
|
|
588
|
+
return NULL != ref
|
|
589
|
+
&& AMAXSetBool(ref, kAXMinimizedAttribute, YES)
|
|
590
|
+
&& AMWaitForWindowFlag(handle, AM_KEY_MINIMIZED, YES);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
+ (BOOL)restoreHandle:(AMWindowHandle *)handle
|
|
594
|
+
{
|
|
595
|
+
if (gUsesUnprivilegedRoutes && [self restoreHandleThroughDock:handle]) {
|
|
596
|
+
return YES;
|
|
597
|
+
}
|
|
598
|
+
AXUIElementRef ref = [handle axElement];
|
|
599
|
+
return NULL != ref
|
|
600
|
+
&& AMAXSetBool(ref, kAXMinimizedAttribute, NO)
|
|
601
|
+
&& AMWaitForWindowFlag(handle, AM_KEY_MINIMIZED, NO);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
+ (BOOL)enterFullscreenHandle:(AMWindowHandle *)handle application:(XCUIApplication *)application
|
|
605
|
+
{
|
|
606
|
+
if (gUsesUnprivilegedRoutes
|
|
607
|
+
&& [self pressButton:AM_FULLSCREEN_BUTTON_ID ofHandle:handle application:application]
|
|
608
|
+
&& AMWaitForWindowFlag(handle, AM_KEY_FULLSCREEN, YES)) {
|
|
609
|
+
return YES;
|
|
610
|
+
}
|
|
611
|
+
AXUIElementRef ref = [handle axElement];
|
|
612
|
+
return NULL != ref
|
|
613
|
+
&& AMAXSetBool(ref, CFSTR("AXFullScreen"), YES)
|
|
614
|
+
&& AMWaitForWindowFlag(handle, AM_KEY_FULLSCREEN, YES);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
+ (BOOL)focusHandle:(AMWindowHandle *)handle application:(XCUIApplication *)application
|
|
618
|
+
{
|
|
619
|
+
[application activate];
|
|
620
|
+
if (AMWaitForWindowFlag(handle, AM_KEY_FOCUSED, YES)) {
|
|
621
|
+
return YES;
|
|
622
|
+
}
|
|
623
|
+
AXUIElementRef ref = [handle axElement];
|
|
624
|
+
if (NULL == ref) {
|
|
625
|
+
return NO;
|
|
626
|
+
}
|
|
627
|
+
AXUIElementPerformAction(ref, kAXRaiseAction);
|
|
628
|
+
return AMWaitForWindowFlag(handle, AM_KEY_FOCUSED, YES);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
+ (BOOL)leaveFullscreenHandle:(AMWindowHandle *)handle application:(XCUIApplication *)application
|
|
632
|
+
{
|
|
633
|
+
if (gUsesUnprivilegedRoutes) {
|
|
634
|
+
// The window buttons are hidden in the fullscreen mode, so the standard shortcut is used.
|
|
635
|
+
// It toggles the focused window, so the shortcut is skipped if the target cannot be focused
|
|
636
|
+
if ([self focusHandle:handle application:application]) {
|
|
637
|
+
[application typeKey:@"f" modifierFlags:XCUIKeyModifierControl | XCUIKeyModifierCommand];
|
|
638
|
+
if (AMWaitForWindowFlag(handle, AM_KEY_FULLSCREEN, NO)) {
|
|
639
|
+
return YES;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
AXUIElementRef ref = [handle axElement];
|
|
644
|
+
return NULL != ref
|
|
645
|
+
&& AMAXSetBool(ref, CFSTR("AXFullScreen"), NO)
|
|
646
|
+
&& AMWaitForWindowFlag(handle, AM_KEY_FULLSCREEN, NO);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Applies the change and describes the result
|
|
650
|
+
+ (nullable NSDictionary<NSString *, id> *)adjustHandle:(AMWindowHandle *)handle
|
|
651
|
+
application:(XCUIApplication *)application
|
|
652
|
+
state:(nullable NSString *)state
|
|
653
|
+
rect:(NSDictionary<NSString *, NSNumber *> *)rect
|
|
654
|
+
display:(nullable AMScreenProperties *)targetDisplay
|
|
655
|
+
error:(NSError **)error
|
|
656
|
+
{
|
|
657
|
+
NSDictionary *attrs = AMReadWindow(handle);
|
|
658
|
+
if (nil == attrs) {
|
|
659
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure
|
|
660
|
+
format:@"Cannot read the window state. The XCTest accessibility interface is not available. "
|
|
661
|
+
@"Grant the Accessibility permission to the WebDriverAgentRunner app and restart it"]);
|
|
662
|
+
return nil;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
BOOL wantsMinimized = [state isEqualToString:AMWindowStateMinimized];
|
|
666
|
+
BOOL isMinimized = [attrs[AM_KEY_MINIMIZED] boolValue];
|
|
667
|
+
if (isMinimized && wantsMinimized) {
|
|
668
|
+
return [self infoForHandle:handle];
|
|
669
|
+
}
|
|
670
|
+
// Every other change needs a regular window
|
|
671
|
+
if (isMinimized && ![self restoreHandle:handle]) {
|
|
672
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure
|
|
673
|
+
format:@"The minimized window cannot be restored. Make sure there are no other minimized windows "
|
|
674
|
+
@"with the same title or grant the Accessibility permission to the WebDriverAgentRunner app"]);
|
|
675
|
+
return nil;
|
|
676
|
+
}
|
|
677
|
+
BOOL isFullscreen = [AMReadWindow(handle)[AM_KEY_FULLSCREEN] boolValue];
|
|
678
|
+
BOOL wantsFullscreen = [state isEqualToString:AMWindowStateFullscreen];
|
|
679
|
+
if (isFullscreen && !wantsFullscreen && ![self leaveFullscreenHandle:handle application:application]) {
|
|
680
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure
|
|
681
|
+
format:@"The window did not leave the fullscreen mode in %.0f seconds", AM_WINDOW_STATE_TIMEOUT]);
|
|
682
|
+
return nil;
|
|
683
|
+
}
|
|
684
|
+
if (wantsMinimized) {
|
|
685
|
+
if (![self minimizeHandle:handle application:application]) {
|
|
686
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure format:@"The window cannot be minimized"]);
|
|
687
|
+
return nil;
|
|
688
|
+
}
|
|
689
|
+
return [self infoForHandle:handle];
|
|
690
|
+
}
|
|
691
|
+
if (wantsFullscreen) {
|
|
692
|
+
if (!isFullscreen && ![self enterFullscreenHandle:handle application:application]) {
|
|
693
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure format:@"The window cannot be put into the fullscreen mode"]);
|
|
694
|
+
return nil;
|
|
695
|
+
}
|
|
696
|
+
return [self infoForHandle:handle];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if ([state isEqualToString:AMWindowStateMaximized] || rect.count > 0 || nil != targetDisplay) {
|
|
700
|
+
AXUIElementRef ref = [handle axElement];
|
|
701
|
+
if (NULL == ref) {
|
|
702
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure format:@"%@", AM_GEOMETRY_ACCESS_DENIED_MESSAGE]);
|
|
703
|
+
return nil;
|
|
704
|
+
}
|
|
705
|
+
if (![self applyGeometryToAXWindow:ref state:state rect:rect display:targetDisplay error:error]) {
|
|
706
|
+
return nil;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return [self infoForHandle:handle];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
+ (BOOL)applyGeometryToAXWindow:(AXUIElementRef)ref
|
|
713
|
+
state:(nullable NSString *)state
|
|
714
|
+
rect:(NSDictionary<NSString *, NSNumber *> *)rect
|
|
715
|
+
display:(nullable AMScreenProperties *)targetDisplay
|
|
716
|
+
error:(NSError **)error
|
|
717
|
+
{
|
|
718
|
+
CGRect current;
|
|
719
|
+
if (!AMAXReadRect(ref, ¤t)) {
|
|
720
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure format:@"Cannot read the current window rect"]);
|
|
721
|
+
return NO;
|
|
722
|
+
}
|
|
723
|
+
if ([state isEqualToString:AMWindowStateMaximized]) {
|
|
724
|
+
// Fills the visible area of the display, which is also deterministic when repeated
|
|
725
|
+
AMScreenProperties *display = targetDisplay ?: AMDisplayForRect(current);
|
|
726
|
+
if (nil == display) {
|
|
727
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure format:@"Cannot determine the display to maximize the window on"]);
|
|
728
|
+
return NO;
|
|
729
|
+
}
|
|
730
|
+
CGRect visible = display.visibleBounds;
|
|
731
|
+
return [self applyRect:@{@"x": @(visible.origin.x), @"y": @(visible.origin.y),
|
|
732
|
+
@"width": @(visible.size.width), @"height": @(visible.size.height)}
|
|
733
|
+
current:current ref:ref display:nil error:error];
|
|
734
|
+
}
|
|
735
|
+
return [self applyRect:rect current:current ref:ref display:targetDisplay error:error];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Coordinates are global unless a display is given
|
|
739
|
+
+ (BOOL)applyRect:(NSDictionary<NSString *, NSNumber *> *)rect
|
|
740
|
+
current:(CGRect)current
|
|
741
|
+
ref:(AXUIElementRef)ref
|
|
742
|
+
display:(nullable AMScreenProperties *)targetDisplay
|
|
743
|
+
error:(NSError **)error
|
|
744
|
+
{
|
|
745
|
+
CGSize size = CGSizeMake(nil != rect[@"width"] ? rect[@"width"].doubleValue : current.size.width,
|
|
746
|
+
nil != rect[@"height"] ? rect[@"height"].doubleValue : current.size.height);
|
|
747
|
+
CGPoint origin = current.origin;
|
|
748
|
+
if (nil != targetDisplay) {
|
|
749
|
+
CGRect bounds = targetDisplay.bounds;
|
|
750
|
+
AMScreenProperties *sourceDisplay = AMDisplayForRect(current);
|
|
751
|
+
CGPoint sourceOrigin = nil == sourceDisplay ? CGPointZero : sourceDisplay.bounds.origin;
|
|
752
|
+
// x/y are relative to the target display; the relative position is kept if they are omitted
|
|
753
|
+
origin.x = bounds.origin.x + (nil != rect[@"x"] ? rect[@"x"].doubleValue : current.origin.x - sourceOrigin.x);
|
|
754
|
+
origin.y = bounds.origin.y + (nil != rect[@"y"] ? rect[@"y"].doubleValue : current.origin.y - sourceOrigin.y);
|
|
755
|
+
// Keep the window within the display
|
|
756
|
+
origin.x = MAX(bounds.origin.x, MIN(origin.x, CGRectGetMaxX(bounds) - size.width));
|
|
757
|
+
origin.y = MAX(bounds.origin.y, MIN(origin.y, CGRectGetMaxY(bounds) - size.height));
|
|
758
|
+
} else {
|
|
759
|
+
origin.x = nil != rect[@"x"] ? rect[@"x"].doubleValue : origin.x;
|
|
760
|
+
origin.y = nil != rect[@"y"] ? rect[@"y"].doubleValue : origin.y;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
BOOL moves = !CGPointEqualToPoint(origin, current.origin);
|
|
764
|
+
BOOL resizes = !CGSizeEqualToSize(size, current.size);
|
|
765
|
+
if (moves && !AMAXSetGeometry(ref, kAXPositionAttribute, kAXValueTypeCGPoint, &origin)) {
|
|
766
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure format:@"The window cannot be moved"]);
|
|
767
|
+
return NO;
|
|
768
|
+
}
|
|
769
|
+
if (resizes && !AMAXSetGeometry(ref, kAXSizeAttribute, kAXValueTypeCGSize, &size)) {
|
|
770
|
+
AMAssignError(error, [self windowErrorWithCode:AMWindowErrorFailure format:@"The window cannot be resized"]);
|
|
771
|
+
return NO;
|
|
772
|
+
}
|
|
773
|
+
// The size may have been adjusted for the new display, which can shift the window
|
|
774
|
+
if (moves && resizes) {
|
|
775
|
+
AMAXSetGeometry(ref, kAXPositionAttribute, kAXValueTypeCGPoint, &origin);
|
|
776
|
+
}
|
|
777
|
+
return YES;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
@end
|