autogui 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE +29 -0
- data/README.md +431 -0
- data/bin/autogui +19 -0
- data/docs/api.md +186 -0
- data/docs/guide.md +243 -0
- data/docs/publishing.md +90 -0
- data/examples/hello.rb +20 -0
- data/examples/locate_demo.rb +16 -0
- data/lib/autogui/exceptions.rb +9 -0
- data/lib/autogui/geometry.rb +98 -0
- data/lib/autogui/image.rb +344 -0
- data/lib/autogui/keys.rb +50 -0
- data/lib/autogui/message_box.rb +125 -0
- data/lib/autogui/platform/darwin.rb +185 -0
- data/lib/autogui/platform/linux.rb +172 -0
- data/lib/autogui/platform/windows.rb +400 -0
- data/lib/autogui/platform.rb +41 -0
- data/lib/autogui/run.rb +168 -0
- data/lib/autogui/screenshot.rb +95 -0
- data/lib/autogui/tween.rb +254 -0
- data/lib/autogui/version.rb +5 -0
- data/lib/autogui/window.rb +170 -0
- data/lib/autogui.rb +738 -0
- metadata +90 -0
data/lib/autogui.rb
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
require_relative "autogui/version"
|
|
7
|
+
require_relative "autogui/exceptions"
|
|
8
|
+
require_relative "autogui/geometry"
|
|
9
|
+
require_relative "autogui/keys"
|
|
10
|
+
require_relative "autogui/tween"
|
|
11
|
+
require_relative "autogui/image"
|
|
12
|
+
require_relative "autogui/screenshot"
|
|
13
|
+
require_relative "autogui/platform"
|
|
14
|
+
require_relative "autogui/message_box"
|
|
15
|
+
require_relative "autogui/window"
|
|
16
|
+
require_relative "autogui/run"
|
|
17
|
+
|
|
18
|
+
# AutoGUI — desktop GUI automation for Ruby (mouse, keyboard, screenshots).
|
|
19
|
+
#
|
|
20
|
+
# require "autogui"
|
|
21
|
+
# AutoGUI.move_to(100, 150)
|
|
22
|
+
# AutoGUI.click
|
|
23
|
+
# AutoGUI.write("Hello world!")
|
|
24
|
+
#
|
|
25
|
+
# Install: <tt>gem install autogui</tt>
|
|
26
|
+
# Docs: README.md, docs/api.md, docs/guide.md
|
|
27
|
+
# Fail-safe: moving the cursor to a screen corner raises {FailSafeException}.
|
|
28
|
+
module AutoGUI
|
|
29
|
+
class << self
|
|
30
|
+
attr_accessor :pause, :failsafe, :minimum_duration, :minimum_sleep,
|
|
31
|
+
:darwin_catch_up_time, :log_screenshots, :log_screenshots_limit,
|
|
32
|
+
:use_image_not_found_exception
|
|
33
|
+
attr_reader :failsafe_points
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@pause = 0.1
|
|
37
|
+
@failsafe = true
|
|
38
|
+
@minimum_duration = 0.1
|
|
39
|
+
@minimum_sleep = 0.05
|
|
40
|
+
@darwin_catch_up_time = 0.01
|
|
41
|
+
@log_screenshots = false
|
|
42
|
+
@log_screenshots_limit = 10
|
|
43
|
+
@log_screenshot_filenames = []
|
|
44
|
+
@use_image_not_found_exception = false
|
|
45
|
+
@failsafe_points = [Point.new(0, 0)]
|
|
46
|
+
|
|
47
|
+
# Python-style constant aliases
|
|
48
|
+
def self.PAUSE
|
|
49
|
+
@pause
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.PAUSE=(value)
|
|
53
|
+
@pause = value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.FAILSAFE
|
|
57
|
+
@failsafe
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.FAILSAFE=(value)
|
|
61
|
+
@failsafe = value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.FAILSAFE_POINTS
|
|
65
|
+
@failsafe_points
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.MINIMUM_DURATION
|
|
69
|
+
@minimum_duration
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.MINIMUM_DURATION=(value)
|
|
73
|
+
@minimum_duration = value
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.MINIMUM_SLEEP
|
|
77
|
+
@minimum_sleep
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.MINIMUM_SLEEP=(value)
|
|
81
|
+
@minimum_sleep = value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.DARWIN_CATCH_UP_TIME
|
|
85
|
+
@darwin_catch_up_time
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.DARWIN_CATCH_UP_TIME=(value)
|
|
89
|
+
@darwin_catch_up_time = value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.LOG_SCREENSHOTS
|
|
93
|
+
@log_screenshots
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.LOG_SCREENSHOTS=(value)
|
|
97
|
+
@log_screenshots = value
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
module_function
|
|
101
|
+
|
|
102
|
+
def platform_module
|
|
103
|
+
Platform.current
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def getPointOnLine(x1, y1, x2, y2, n)
|
|
107
|
+
[((x2 - x1) * n) + x1, ((y2 - y1) * n) + y1]
|
|
108
|
+
end
|
|
109
|
+
alias get_point_on_line getPointOnLine
|
|
110
|
+
|
|
111
|
+
def linear(n)
|
|
112
|
+
Tween.linear(n)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Tween.singleton_methods(false).each do |name|
|
|
116
|
+
next if method_defined?(name) || name == :check
|
|
117
|
+
|
|
118
|
+
define_singleton_method(name) { |*args| Tween.public_send(name, *args) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def failSafeCheck
|
|
122
|
+
return unless @failsafe
|
|
123
|
+
|
|
124
|
+
pos = position
|
|
125
|
+
return unless @failsafe_points.any? { |p| p.x == pos.x && p.y == pos.y }
|
|
126
|
+
|
|
127
|
+
raise FailSafeException,
|
|
128
|
+
"AutoGUI fail-safe triggered from mouse moving to a corner of the screen. " \
|
|
129
|
+
"To disable this fail-safe, set AutoGUI.FAILSAFE to false. DISABLING FAIL-SAFE IS NOT RECOMMENDED."
|
|
130
|
+
end
|
|
131
|
+
alias fail_safe_check failSafeCheck
|
|
132
|
+
|
|
133
|
+
def sleep(seconds)
|
|
134
|
+
Kernel.sleep(seconds.to_f)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def countdown(seconds)
|
|
138
|
+
seconds.to_i.downto(1) do |i|
|
|
139
|
+
print "#{i} "
|
|
140
|
+
$stdout.flush
|
|
141
|
+
Kernel.sleep(1)
|
|
142
|
+
end
|
|
143
|
+
puts
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def useImageNotFoundException(value = true)
|
|
147
|
+
@use_image_not_found_exception = value
|
|
148
|
+
end
|
|
149
|
+
alias use_image_not_found_exception! useImageNotFoundException
|
|
150
|
+
|
|
151
|
+
def position(x = nil, y = nil)
|
|
152
|
+
posx, posy = platform_module.position
|
|
153
|
+
posx = x.to_i unless x.nil?
|
|
154
|
+
posy = y.to_i unless y.nil?
|
|
155
|
+
Point.new(posx, posy)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def size
|
|
159
|
+
Size.new(*platform_module.size)
|
|
160
|
+
end
|
|
161
|
+
alias resolution size
|
|
162
|
+
|
|
163
|
+
def onScreen(x, y = nil)
|
|
164
|
+
pt = normalize_xy(x, y)
|
|
165
|
+
return false if pt.nil?
|
|
166
|
+
|
|
167
|
+
w, h = platform_module.size
|
|
168
|
+
pt.x >= 0 && pt.x < w && pt.y >= 0 && pt.y < h
|
|
169
|
+
end
|
|
170
|
+
alias on_screen onScreen
|
|
171
|
+
|
|
172
|
+
def isValidKey(key)
|
|
173
|
+
!platform_module.keyboard_mapping[key].nil? || key.length == 1
|
|
174
|
+
end
|
|
175
|
+
alias valid_key? isValidKey
|
|
176
|
+
alias is_valid_key isValidKey
|
|
177
|
+
|
|
178
|
+
def mouseDown(x = nil, y = nil, button: PRIMARY, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
179
|
+
with_checks(_pause) do
|
|
180
|
+
button = normalize_button(button)
|
|
181
|
+
pt = normalize_xy(x, y)
|
|
182
|
+
mouse_move_drag("move", pt.x, pt.y, 0, 0, duration, tween)
|
|
183
|
+
log_screenshot(logScreenshot, "mouseDown", "#{pt.x},#{pt.y}")
|
|
184
|
+
platform_module.mouse_down(pt.x, pt.y, button)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
alias mouse_down mouseDown
|
|
188
|
+
|
|
189
|
+
def mouseUp(x = nil, y = nil, button: PRIMARY, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
190
|
+
with_checks(_pause) do
|
|
191
|
+
button = normalize_button(button)
|
|
192
|
+
pt = normalize_xy(x, y)
|
|
193
|
+
mouse_move_drag("move", pt.x, pt.y, 0, 0, duration, tween)
|
|
194
|
+
log_screenshot(logScreenshot, "mouseUp", "#{pt.x},#{pt.y}")
|
|
195
|
+
platform_module.mouse_up(pt.x, pt.y, button)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
alias mouse_up mouseUp
|
|
199
|
+
|
|
200
|
+
def click(x = nil, y = nil, clicks: 1, interval: 0.0, button: PRIMARY, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
201
|
+
with_checks(_pause) do
|
|
202
|
+
button = normalize_button(button)
|
|
203
|
+
pt = normalize_xy(x, y)
|
|
204
|
+
mouse_move_drag("move", pt.x, pt.y, 0, 0, duration, tween)
|
|
205
|
+
log_screenshot(logScreenshot, "click", "#{button},#{clicks},#{pt.x},#{pt.y}")
|
|
206
|
+
clicks.to_i.times do
|
|
207
|
+
failSafeCheck
|
|
208
|
+
platform_module.click(pt.x, pt.y, button)
|
|
209
|
+
Kernel.sleep(interval.to_f) if interval.to_f.positive?
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def leftClick(x = nil, y = nil, interval: 0.0, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
215
|
+
click(x, y, clicks: 1, interval: interval, button: LEFT, duration: duration, tween: tween, logScreenshot: logScreenshot, _pause: _pause)
|
|
216
|
+
end
|
|
217
|
+
alias left_click leftClick
|
|
218
|
+
|
|
219
|
+
def rightClick(x = nil, y = nil, interval: 0.0, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
220
|
+
click(x, y, clicks: 1, interval: interval, button: RIGHT, duration: duration, tween: tween, logScreenshot: logScreenshot, _pause: _pause)
|
|
221
|
+
end
|
|
222
|
+
alias right_click rightClick
|
|
223
|
+
|
|
224
|
+
def middleClick(x = nil, y = nil, interval: 0.0, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
225
|
+
click(x, y, clicks: 1, interval: interval, button: MIDDLE, duration: duration, tween: tween, logScreenshot: logScreenshot, _pause: _pause)
|
|
226
|
+
end
|
|
227
|
+
alias middle_click middleClick
|
|
228
|
+
|
|
229
|
+
def doubleClick(x = nil, y = nil, interval: 0.0, button: LEFT, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
230
|
+
click(x, y, clicks: 2, interval: interval, button: button, duration: duration, tween: tween, logScreenshot: logScreenshot, _pause: _pause)
|
|
231
|
+
end
|
|
232
|
+
alias double_click doubleClick
|
|
233
|
+
|
|
234
|
+
def tripleClick(x = nil, y = nil, interval: 0.0, button: LEFT, duration: 0.0, tween: nil, logScreenshot: nil, _pause: true)
|
|
235
|
+
click(x, y, clicks: 3, interval: interval, button: button, duration: duration, tween: tween, logScreenshot: logScreenshot, _pause: _pause)
|
|
236
|
+
end
|
|
237
|
+
alias triple_click tripleClick
|
|
238
|
+
|
|
239
|
+
def scroll(clicks, x = nil, y = nil, logScreenshot: nil, _pause: true)
|
|
240
|
+
with_checks(_pause) do
|
|
241
|
+
x, y = x.to_a if x.is_a?(Array)
|
|
242
|
+
pt = position(x, y)
|
|
243
|
+
log_screenshot(logScreenshot, "scroll", "#{clicks},#{pt.x},#{pt.y}")
|
|
244
|
+
platform_module.scroll(clicks, pt.x, pt.y)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def hscroll(clicks, x = nil, y = nil, logScreenshot: nil, _pause: true)
|
|
249
|
+
with_checks(_pause) do
|
|
250
|
+
x, y = x.to_a if x.is_a?(Array)
|
|
251
|
+
pt = position(x, y)
|
|
252
|
+
log_screenshot(logScreenshot, "hscroll", "#{clicks},#{pt.x},#{pt.y}")
|
|
253
|
+
platform_module.hscroll(clicks, pt.x, pt.y)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def vscroll(clicks, x = nil, y = nil, logScreenshot: nil, _pause: true)
|
|
258
|
+
with_checks(_pause) do
|
|
259
|
+
x, y = x.to_a if x.is_a?(Array)
|
|
260
|
+
pt = position(x, y)
|
|
261
|
+
log_screenshot(logScreenshot, "vscroll", "#{clicks},#{pt.x},#{pt.y}")
|
|
262
|
+
platform_module.vscroll(clicks, pt.x, pt.y)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def moveTo(x = nil, y = nil, duration: 0.0, tween: nil, logScreenshot: false, _pause: true)
|
|
267
|
+
with_checks(_pause) do
|
|
268
|
+
pt = normalize_xy(x, y)
|
|
269
|
+
log_screenshot(logScreenshot, "moveTo", "#{pt.x},#{pt.y}")
|
|
270
|
+
mouse_move_drag("move", pt.x, pt.y, 0, 0, duration, tween)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
alias move_to moveTo
|
|
274
|
+
|
|
275
|
+
def moveRel(xOffset = nil, yOffset = nil, duration: 0.0, tween: nil, logScreenshot: false, _pause: true)
|
|
276
|
+
with_checks(_pause) do
|
|
277
|
+
if xOffset.is_a?(Array)
|
|
278
|
+
yOffset = xOffset[1]
|
|
279
|
+
xOffset = xOffset[0]
|
|
280
|
+
end
|
|
281
|
+
xOffset = 0 if xOffset.nil?
|
|
282
|
+
yOffset = 0 if yOffset.nil?
|
|
283
|
+
log_screenshot(logScreenshot, "moveRel", "#{xOffset},#{yOffset}")
|
|
284
|
+
mouse_move_drag("move", nil, nil, xOffset, yOffset, duration, tween)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
alias move_rel moveRel
|
|
288
|
+
alias move moveRel
|
|
289
|
+
|
|
290
|
+
def dragTo(x = nil, y = nil, duration: 0.0, tween: nil, button: PRIMARY, logScreenshot: nil, _pause: true, mouseDownUp: true)
|
|
291
|
+
with_checks(_pause) do
|
|
292
|
+
pt = normalize_xy(x, y)
|
|
293
|
+
log_screenshot(logScreenshot, "dragTo", "#{pt.x},#{pt.y}")
|
|
294
|
+
mouseDown(button: button, logScreenshot: false, _pause: false) if mouseDownUp
|
|
295
|
+
mouse_move_drag("drag", pt.x, pt.y, 0, 0, duration, tween, button)
|
|
296
|
+
mouseUp(button: button, logScreenshot: false, _pause: false) if mouseDownUp
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
alias drag_to dragTo
|
|
300
|
+
|
|
301
|
+
def dragRel(xOffset = 0, yOffset = 0, duration: 0.0, tween: nil, button: PRIMARY, logScreenshot: nil, _pause: true, mouseDownUp: true)
|
|
302
|
+
with_checks(_pause) do
|
|
303
|
+
if xOffset.is_a?(Array)
|
|
304
|
+
yOffset = xOffset[1]
|
|
305
|
+
xOffset = xOffset[0]
|
|
306
|
+
end
|
|
307
|
+
xOffset = 0 if xOffset.nil?
|
|
308
|
+
yOffset = 0 if yOffset.nil?
|
|
309
|
+
return if xOffset.to_i.zero? && yOffset.to_i.zero?
|
|
310
|
+
|
|
311
|
+
mousex, mousey = platform_module.position
|
|
312
|
+
log_screenshot(logScreenshot, "dragRel", "#{xOffset},#{yOffset}")
|
|
313
|
+
mouseDown(button: button, logScreenshot: false, _pause: false) if mouseDownUp
|
|
314
|
+
mouse_move_drag("drag", mousex, mousey, xOffset, yOffset, duration, tween, button)
|
|
315
|
+
mouseUp(button: button, logScreenshot: false, _pause: false) if mouseDownUp
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
alias drag_rel dragRel
|
|
319
|
+
alias drag dragRel
|
|
320
|
+
|
|
321
|
+
def keyDown(key, logScreenshot: nil, _pause: true)
|
|
322
|
+
with_checks(_pause) do
|
|
323
|
+
key = key.downcase if key.length > 1
|
|
324
|
+
log_screenshot(logScreenshot, "keyDown", key)
|
|
325
|
+
platform_module.key_down(key)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
alias key_down keyDown
|
|
329
|
+
|
|
330
|
+
def keyUp(key, logScreenshot: nil, _pause: true)
|
|
331
|
+
with_checks(_pause) do
|
|
332
|
+
key = key.downcase if key.length > 1
|
|
333
|
+
log_screenshot(logScreenshot, "keyUp", key)
|
|
334
|
+
platform_module.key_up(key)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
alias key_up keyUp
|
|
338
|
+
|
|
339
|
+
def press(keys, presses: 1, interval: 0.0, logScreenshot: nil, _pause: true)
|
|
340
|
+
with_checks(_pause) do
|
|
341
|
+
keys = normalize_keys(keys)
|
|
342
|
+
log_screenshot(logScreenshot, "press", keys.join(","))
|
|
343
|
+
presses.to_i.times do
|
|
344
|
+
keys.each do |k|
|
|
345
|
+
failSafeCheck
|
|
346
|
+
platform_module.key_down(k)
|
|
347
|
+
platform_module.key_up(k)
|
|
348
|
+
end
|
|
349
|
+
Kernel.sleep(interval.to_f) if interval.to_f.positive?
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def hold(keys, logScreenshot: nil, _pause: true)
|
|
355
|
+
raise ArgumentError, "hold requires a block (AutoGUI.hold('shift') { ... })" unless block_given?
|
|
356
|
+
|
|
357
|
+
keys = normalize_keys(keys)
|
|
358
|
+
with_checks(_pause) do
|
|
359
|
+
log_screenshot(logScreenshot, "hold", keys.join(","))
|
|
360
|
+
keys.each do |k|
|
|
361
|
+
failSafeCheck
|
|
362
|
+
platform_module.key_down(k)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
begin
|
|
366
|
+
yield
|
|
367
|
+
ensure
|
|
368
|
+
keys.each do |k|
|
|
369
|
+
failSafeCheck
|
|
370
|
+
platform_module.key_up(k)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def typewrite(message, interval: 0.0, logScreenshot: nil, _pause: true)
|
|
376
|
+
with_checks(_pause) do
|
|
377
|
+
log_screenshot(logScreenshot, "write", message.to_s[0, 12])
|
|
378
|
+
chars = message.is_a?(String) ? message.chars : Array(message)
|
|
379
|
+
chars.each do |c|
|
|
380
|
+
c = c.downcase if c.length > 1
|
|
381
|
+
press(c, _pause: false)
|
|
382
|
+
Kernel.sleep(interval.to_f) if interval.to_f.positive?
|
|
383
|
+
failSafeCheck
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
alias write typewrite
|
|
388
|
+
|
|
389
|
+
def hotkey(*args, interval: 0.0, logScreenshot: nil, _pause: true)
|
|
390
|
+
with_checks(_pause) do
|
|
391
|
+
args = args[0].to_a if args.length == 1 && args[0].is_a?(Array)
|
|
392
|
+
log_screenshot(logScreenshot, "hotkey", args.join(","))
|
|
393
|
+
args.each do |c|
|
|
394
|
+
c = c.downcase if c.length > 1
|
|
395
|
+
platform_module.key_down(c)
|
|
396
|
+
Kernel.sleep(interval.to_f) if interval.to_f.positive?
|
|
397
|
+
end
|
|
398
|
+
args.reverse_each do |c|
|
|
399
|
+
c = c.downcase if c.length > 1
|
|
400
|
+
platform_module.key_up(c)
|
|
401
|
+
Kernel.sleep(interval.to_f) if interval.to_f.positive?
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
alias shortcut hotkey
|
|
406
|
+
|
|
407
|
+
def screenshot(imageFilename = nil, region: nil)
|
|
408
|
+
img = platform_module.screenshot(region)
|
|
409
|
+
img.save(imageFilename) if imageFilename
|
|
410
|
+
img
|
|
411
|
+
end
|
|
412
|
+
alias grab screenshot
|
|
413
|
+
|
|
414
|
+
def locateOnScreen(image, grayscale: false, confidence: nil, region: nil, minSearchTime: 0)
|
|
415
|
+
deadline = Time.now + minSearchTime.to_f
|
|
416
|
+
loop do
|
|
417
|
+
hay = screenshot(region: region)
|
|
418
|
+
box = Screenshot.locate(image, hay, grayscale: grayscale, confidence: confidence)
|
|
419
|
+
if box
|
|
420
|
+
if region
|
|
421
|
+
return Box.new(box.left + region[0], box.top + region[1], box.width, box.height)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
return box
|
|
425
|
+
end
|
|
426
|
+
break if Time.now >= deadline
|
|
427
|
+
|
|
428
|
+
Kernel.sleep(0.05)
|
|
429
|
+
end
|
|
430
|
+
raise ImageNotFoundException, "image not found on screen" if @use_image_not_found_exception
|
|
431
|
+
|
|
432
|
+
nil
|
|
433
|
+
end
|
|
434
|
+
alias locate_on_screen locateOnScreen
|
|
435
|
+
|
|
436
|
+
def locateAllOnScreen(image, grayscale: false, confidence: nil, region: nil)
|
|
437
|
+
hay = screenshot(region: region)
|
|
438
|
+
enum = Screenshot.locate_all(image, hay, grayscale: grayscale, confidence: confidence)
|
|
439
|
+
if region
|
|
440
|
+
enum = enum.lazy.map { |b| Box.new(b.left + region[0], b.top + region[1], b.width, b.height) }
|
|
441
|
+
end
|
|
442
|
+
enum
|
|
443
|
+
end
|
|
444
|
+
alias locate_all_on_screen locateAllOnScreen
|
|
445
|
+
|
|
446
|
+
def locateCenterOnScreen(image, grayscale: false, confidence: nil, region: nil, minSearchTime: 0)
|
|
447
|
+
box = locateOnScreen(image, grayscale: grayscale, confidence: confidence, region: region, minSearchTime: minSearchTime)
|
|
448
|
+
box && center(box)
|
|
449
|
+
end
|
|
450
|
+
alias locate_center_on_screen locateCenterOnScreen
|
|
451
|
+
|
|
452
|
+
def locate(needleImage, haystackImage, grayscale: false, confidence: nil)
|
|
453
|
+
box = Screenshot.locate(needleImage, haystackImage, grayscale: grayscale, confidence: confidence)
|
|
454
|
+
raise ImageNotFoundException, "image not found" if box.nil? && @use_image_not_found_exception
|
|
455
|
+
|
|
456
|
+
box
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def locateAll(needleImage, haystackImage, grayscale: false, confidence: nil)
|
|
460
|
+
Screenshot.locate_all(needleImage, haystackImage, grayscale: grayscale, confidence: confidence)
|
|
461
|
+
end
|
|
462
|
+
alias locate_all locateAll
|
|
463
|
+
|
|
464
|
+
def locateOnWindow(image, title, grayscale: false, confidence: nil)
|
|
465
|
+
wins = getWindowsWithTitle(title)
|
|
466
|
+
raise AutoGUIException, "no window with title #{title.inspect}" if wins.empty?
|
|
467
|
+
|
|
468
|
+
w = wins.first
|
|
469
|
+
locateOnScreen(image, grayscale: grayscale, confidence: confidence, region: [w.left, w.top, w.width, w.height])
|
|
470
|
+
end
|
|
471
|
+
alias locate_on_window locateOnWindow
|
|
472
|
+
|
|
473
|
+
def center(coords)
|
|
474
|
+
Screenshot.center(coords)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def pixel(x, y)
|
|
478
|
+
platform_module.pixel(x.to_i, y.to_i)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def pixelMatchesColor(x, y, expectedRGBColor, tolerance: 0)
|
|
482
|
+
Screenshot.pixel_matches_color(x, y, expectedRGBColor, tolerance)
|
|
483
|
+
end
|
|
484
|
+
alias pixel_matches_color pixelMatchesColor
|
|
485
|
+
|
|
486
|
+
def alert(text = "", title = "", button = "OK")
|
|
487
|
+
MessageBox.alert(text, title.empty? ? "AutoGUI Alert" : title, button)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def confirm(text = "", title = "", buttons = %w[OK Cancel])
|
|
491
|
+
MessageBox.confirm(text, title.empty? ? "AutoGUI Confirm" : title, buttons)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def prompt(text = "", title = "", default = "")
|
|
495
|
+
MessageBox.prompt(text, title.empty? ? "AutoGUI Prompt" : title, default)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def password(text = "", title = "", default = "", mask = "*")
|
|
499
|
+
MessageBox.password(text, title.empty? ? "AutoGUI Password" : title, default, mask)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def mouseInfo
|
|
503
|
+
displayMousePosition
|
|
504
|
+
end
|
|
505
|
+
alias mouse_info mouseInfo
|
|
506
|
+
|
|
507
|
+
def displayMousePosition(xOffset = 0, yOffset = 0)
|
|
508
|
+
puts "Press Ctrl-C to quit."
|
|
509
|
+
puts "xOffset: #{xOffset} yOffset: #{yOffset}" unless xOffset.zero? && yOffset.zero?
|
|
510
|
+
loop do
|
|
511
|
+
x, y = position
|
|
512
|
+
rx = x - xOffset
|
|
513
|
+
ry = y - yOffset
|
|
514
|
+
rgb =
|
|
515
|
+
begin
|
|
516
|
+
onScreen(rx, ry) ? pixel(x, y) : %w[NaN NaN NaN]
|
|
517
|
+
rescue StandardError
|
|
518
|
+
%w[NaN NaN NaN]
|
|
519
|
+
end
|
|
520
|
+
line = format("X: %4d Y: %4d RGB: (%3s, %3s, %3s)", rx, ry, rgb[0], rgb[1], rgb[2])
|
|
521
|
+
print line
|
|
522
|
+
print "\b" * line.length
|
|
523
|
+
$stdout.flush
|
|
524
|
+
Kernel.sleep(0.05)
|
|
525
|
+
end
|
|
526
|
+
rescue Interrupt
|
|
527
|
+
puts
|
|
528
|
+
end
|
|
529
|
+
alias display_mouse_position displayMousePosition
|
|
530
|
+
|
|
531
|
+
def run(commandStr, _ssCount = nil)
|
|
532
|
+
Run.run(commandStr, _ssCount)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def printInfo(dontPrint = false)
|
|
536
|
+
plat, ruby_ver, ver, exe, res, ts = getInfo
|
|
537
|
+
msg = <<~MSG
|
|
538
|
+
Platform: #{plat}
|
|
539
|
+
Ruby Version: #{ruby_ver}
|
|
540
|
+
AutoGUI Version: #{ver}
|
|
541
|
+
Executable: #{exe}
|
|
542
|
+
Resolution: #{res}
|
|
543
|
+
Timestamp: #{ts}
|
|
544
|
+
MSG
|
|
545
|
+
puts msg unless dontPrint
|
|
546
|
+
msg
|
|
547
|
+
end
|
|
548
|
+
alias print_info printInfo
|
|
549
|
+
|
|
550
|
+
def getInfo
|
|
551
|
+
[RbConfig::CONFIG["host_os"], RUBY_DESCRIPTION, VERSION, RbConfig.ruby, size, Time.now]
|
|
552
|
+
end
|
|
553
|
+
alias get_info getInfo
|
|
554
|
+
|
|
555
|
+
%i[getAllWindows getAllTitles getWindowsWithTitle getWindowsAt getActiveWindow getActiveWindowTitle
|
|
556
|
+
get_all_windows get_all_titles get_windows_with_title get_windows_at get_active_window get_active_window_title].each do |name|
|
|
557
|
+
define_singleton_method(name) { |*args, **kwargs| WindowFunctions.public_send(name, *args, **kwargs) }
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def with_checks(_pause)
|
|
561
|
+
failSafeCheck
|
|
562
|
+
result = yield
|
|
563
|
+
Kernel.sleep(@pause.to_f) if _pause && @pause.to_f.positive?
|
|
564
|
+
result
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def normalize_button(button)
|
|
568
|
+
button = button.to_s.downcase
|
|
569
|
+
linux = Platform.linux?
|
|
570
|
+
valid = linux ? %w[left middle right primary secondary 1 2 3 4 5 6 7] : %w[left middle right primary secondary 1 2 3]
|
|
571
|
+
unless valid.include?(button)
|
|
572
|
+
raise AutoGUIException, "button argument must be one of #{valid.inspect}"
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
if %w[primary secondary].include?(button)
|
|
576
|
+
swapped = platform_module.mouse_is_swapped?
|
|
577
|
+
if button == "primary"
|
|
578
|
+
return swapped ? RIGHT : LEFT
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
return swapped ? LEFT : RIGHT
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
{ "left" => LEFT, "middle" => MIDDLE, "right" => RIGHT, "1" => LEFT, "2" => MIDDLE, "3" => RIGHT,
|
|
585
|
+
"4" => "4", "5" => "5", "6" => "6", "7" => "7" }[button]
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def normalize_xy(first, second)
|
|
589
|
+
if first.nil? && second.nil?
|
|
590
|
+
return position
|
|
591
|
+
elsif first.nil? && !second.nil?
|
|
592
|
+
return Point.new(position.x, second)
|
|
593
|
+
elsif second.nil? && !first.nil? && !first.is_a?(Array) && !first.is_a?(String) && !first.is_a?(Point) && !first.is_a?(Box)
|
|
594
|
+
return Point.new(first, position.y)
|
|
595
|
+
elsif first.is_a?(String)
|
|
596
|
+
loc = locateOnScreen(first)
|
|
597
|
+
return loc && center(loc)
|
|
598
|
+
elsif first.is_a?(Point)
|
|
599
|
+
return first
|
|
600
|
+
elsif first.is_a?(Box)
|
|
601
|
+
return center(first)
|
|
602
|
+
elsif first.is_a?(Array) || first.is_a?(Size)
|
|
603
|
+
arr = first.to_a
|
|
604
|
+
if arr.length == 2 && second.nil?
|
|
605
|
+
return Point.new(arr[0], arr[1])
|
|
606
|
+
elsif arr.length == 4 && second.nil?
|
|
607
|
+
return center(arr)
|
|
608
|
+
elsif !second.nil?
|
|
609
|
+
raise AutoGUIException, "When passing a sequence for firstArg, secondArg must not be passed (received #{second.inspect})."
|
|
610
|
+
else
|
|
611
|
+
raise AutoGUIException, "The supplied sequence must have exactly 2 or exactly 4 elements (#{arr.length} were received)."
|
|
612
|
+
end
|
|
613
|
+
else
|
|
614
|
+
Point.new(first, second)
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def normalize_keys(keys)
|
|
619
|
+
if keys.is_a?(String)
|
|
620
|
+
keys = keys.downcase if keys.length > 1
|
|
621
|
+
[keys]
|
|
622
|
+
else
|
|
623
|
+
Array(keys).map { |s| s.length > 1 ? s.downcase : s }
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def mouse_move_drag(move_or_drag, x, y, x_offset, y_offset, duration, tween = nil, _button = nil)
|
|
628
|
+
tween ||= method(:linear)
|
|
629
|
+
x_offset = x_offset.nil? ? 0 : x_offset.to_i
|
|
630
|
+
y_offset = y_offset.nil? ? 0 : y_offset.to_i
|
|
631
|
+
return if x.nil? && y.nil? && x_offset.zero? && y_offset.zero?
|
|
632
|
+
|
|
633
|
+
startx, starty = position
|
|
634
|
+
x = x.nil? ? startx : x.to_i
|
|
635
|
+
y = y.nil? ? starty : y.to_i
|
|
636
|
+
x += x_offset
|
|
637
|
+
y += y_offset
|
|
638
|
+
|
|
639
|
+
steps = [[x, y]]
|
|
640
|
+
sleep_amount = 0
|
|
641
|
+
if duration.to_f > @minimum_duration
|
|
642
|
+
width, height = size
|
|
643
|
+
num_steps = [width, height].max
|
|
644
|
+
sleep_amount = duration.to_f / num_steps
|
|
645
|
+
if sleep_amount < @minimum_sleep
|
|
646
|
+
num_steps = (duration.to_f / @minimum_sleep).to_i
|
|
647
|
+
num_steps = 1 if num_steps < 1
|
|
648
|
+
sleep_amount = duration.to_f / num_steps
|
|
649
|
+
end
|
|
650
|
+
steps = (0...num_steps).map do |n|
|
|
651
|
+
getPointOnLine(startx, starty, x, y, tween.call(n.to_f / num_steps))
|
|
652
|
+
end
|
|
653
|
+
steps << [x, y]
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
tween_x = x
|
|
657
|
+
tween_y = y
|
|
658
|
+
steps.each do |sx, sy|
|
|
659
|
+
Kernel.sleep(sleep_amount) if steps.length > 1
|
|
660
|
+
tween_x = sx.round
|
|
661
|
+
tween_y = sy.round
|
|
662
|
+
failSafeCheck unless @failsafe_points.any? { |p| p.x == tween_x && p.y == tween_y }
|
|
663
|
+
platform_module.move_to(tween_x, tween_y)
|
|
664
|
+
end
|
|
665
|
+
failSafeCheck unless @failsafe_points.any? { |p| p.x == tween_x && p.y == tween_y }
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def log_screenshot(log, func_name, func_args, folder = ".")
|
|
669
|
+
return if log == false
|
|
670
|
+
return if log.nil? && !@log_screenshots
|
|
671
|
+
|
|
672
|
+
func_args = "#{func_args[0, 12]}..." if func_args.length > 12
|
|
673
|
+
now = Time.now
|
|
674
|
+
filename = format(
|
|
675
|
+
"%04d-%02d-%02d_%02d-%02d-%02d-%03d_%s_%s.png",
|
|
676
|
+
now.year, now.month, now.day, now.hour, now.min, now.sec,
|
|
677
|
+
(now.usec / 1000), func_name, func_args.gsub(/[^\w,.-]/, "_")
|
|
678
|
+
)
|
|
679
|
+
if @log_screenshots_limit && @log_screenshot_filenames.length >= @log_screenshots_limit
|
|
680
|
+
old = File.join(folder, @log_screenshot_filenames.shift)
|
|
681
|
+
File.unlink(old) if File.exist?(old)
|
|
682
|
+
end
|
|
683
|
+
screenshot(File.join(folder, filename))
|
|
684
|
+
@log_screenshot_filenames << filename
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# module_function copies methods to the singleton class but does not copy
|
|
688
|
+
# subsequent `alias` names. Re-alias them on the singleton class.
|
|
689
|
+
class << self
|
|
690
|
+
{
|
|
691
|
+
get_point_on_line: :getPointOnLine,
|
|
692
|
+
fail_safe_check: :failSafeCheck,
|
|
693
|
+
use_image_not_found_exception!: :useImageNotFoundException,
|
|
694
|
+
on_screen: :onScreen,
|
|
695
|
+
valid_key?: :isValidKey,
|
|
696
|
+
is_valid_key: :isValidKey,
|
|
697
|
+
mouse_down: :mouseDown,
|
|
698
|
+
mouse_up: :mouseUp,
|
|
699
|
+
left_click: :leftClick,
|
|
700
|
+
right_click: :rightClick,
|
|
701
|
+
middle_click: :middleClick,
|
|
702
|
+
double_click: :doubleClick,
|
|
703
|
+
triple_click: :tripleClick,
|
|
704
|
+
move_to: :moveTo,
|
|
705
|
+
move_rel: :moveRel,
|
|
706
|
+
move: :moveRel,
|
|
707
|
+
drag_to: :dragTo,
|
|
708
|
+
drag_rel: :dragRel,
|
|
709
|
+
drag: :dragRel,
|
|
710
|
+
key_down: :keyDown,
|
|
711
|
+
key_up: :keyUp,
|
|
712
|
+
write: :typewrite,
|
|
713
|
+
shortcut: :hotkey,
|
|
714
|
+
grab: :screenshot,
|
|
715
|
+
locate_on_screen: :locateOnScreen,
|
|
716
|
+
locate_all_on_screen: :locateAllOnScreen,
|
|
717
|
+
locate_center_on_screen: :locateCenterOnScreen,
|
|
718
|
+
locate_all: :locateAll,
|
|
719
|
+
locate_on_window: :locateOnWindow,
|
|
720
|
+
pixel_matches_color: :pixelMatchesColor,
|
|
721
|
+
mouse_info: :mouseInfo,
|
|
722
|
+
display_mouse_position: :displayMousePosition,
|
|
723
|
+
print_info: :printInfo,
|
|
724
|
+
get_info: :getInfo
|
|
725
|
+
}.each do |snake, camel|
|
|
726
|
+
alias_method snake, camel if method_defined?(camel) || private_method_defined?(camel)
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
begin
|
|
732
|
+
_right, _bottom = AutoGUI.size
|
|
733
|
+
AutoGUI.failsafe_points.concat(
|
|
734
|
+
[AutoGUI::Point.new(0, _bottom - 1), AutoGUI::Point.new(_right - 1, 0), AutoGUI::Point.new(_right - 1, _bottom - 1)]
|
|
735
|
+
)
|
|
736
|
+
rescue StandardError
|
|
737
|
+
nil
|
|
738
|
+
end
|