robotframework-desktop 0.1.0__py3-none-any.whl
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.
- DesktopLibrary/__init__.py +278 -0
- DesktopLibrary/context.py +272 -0
- DesktopLibrary/cursor.py +243 -0
- DesktopLibrary/engines/__init__.py +4 -0
- DesktopLibrary/engines/image.py +174 -0
- DesktopLibrary/engines/ui.py +192 -0
- DesktopLibrary/geometry.py +187 -0
- DesktopLibrary/keywords/__init__.py +23 -0
- DesktopLibrary/keywords/application.py +149 -0
- DesktopLibrary/keywords/base.py +90 -0
- DesktopLibrary/keywords/config.py +159 -0
- DesktopLibrary/keywords/cursor.py +201 -0
- DesktopLibrary/keywords/element.py +260 -0
- DesktopLibrary/keywords/find.py +136 -0
- DesktopLibrary/keywords/keyboard.py +171 -0
- DesktopLibrary/keywords/mouse.py +357 -0
- DesktopLibrary/keywords/ocr.py +166 -0
- DesktopLibrary/keywords/region.py +288 -0
- DesktopLibrary/keywords/wait.py +178 -0
- DesktopLibrary/locator.py +223 -0
- DesktopLibrary/overlay.py +180 -0
- robotframework_desktop-0.1.0.dist-info/METADATA +215 -0
- robotframework_desktop-0.1.0.dist-info/RECORD +25 -0
- robotframework_desktop-0.1.0.dist-info/WHEEL +5 -0
- robotframework_desktop-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pyautogui
|
|
4
|
+
import pyperclip
|
|
5
|
+
from robot.api.deco import keyword
|
|
6
|
+
|
|
7
|
+
from .base import KeywordGroup, busy_action
|
|
8
|
+
|
|
9
|
+
_KEY_ALIASES = {
|
|
10
|
+
'ESCAPE': 'esc',
|
|
11
|
+
'ESC': 'esc',
|
|
12
|
+
'RETURN': 'enter',
|
|
13
|
+
'ENTER': 'enter',
|
|
14
|
+
'BACKSPACE': 'backspace',
|
|
15
|
+
'DELETE': 'delete',
|
|
16
|
+
'DEL': 'delete',
|
|
17
|
+
'PAGE_UP': 'pageup',
|
|
18
|
+
'PAGEUP': 'pageup',
|
|
19
|
+
'PAGE_DOWN': 'pagedown',
|
|
20
|
+
'PAGEDOWN': 'pagedown',
|
|
21
|
+
'CTRL': 'ctrl',
|
|
22
|
+
'CONTROL': 'ctrl',
|
|
23
|
+
'ALT': 'alt',
|
|
24
|
+
'SHIFT': 'shift',
|
|
25
|
+
'WIN': 'win',
|
|
26
|
+
'META': 'win',
|
|
27
|
+
'CMD': 'win',
|
|
28
|
+
'WINDOWS': 'win',
|
|
29
|
+
'CAPS_LOCK': 'capslock',
|
|
30
|
+
'NUM_LOCK': 'numlock',
|
|
31
|
+
'SCROLL_LOCK': 'scrolllock',
|
|
32
|
+
'PRINTSCREEN': 'printscreen',
|
|
33
|
+
'PRINT': 'printscreen',
|
|
34
|
+
'INSERT': 'insert',
|
|
35
|
+
'SPACE': 'space',
|
|
36
|
+
'TAB': 'tab',
|
|
37
|
+
'HOME': 'home',
|
|
38
|
+
'END': 'end',
|
|
39
|
+
'UP': 'up',
|
|
40
|
+
'DOWN': 'down',
|
|
41
|
+
'LEFT': 'left',
|
|
42
|
+
'RIGHT': 'right',
|
|
43
|
+
'PAUSE': 'pause',
|
|
44
|
+
'ALTGR': 'altright',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_key(name: str) -> str:
|
|
49
|
+
text = str(name).strip()
|
|
50
|
+
if len(text) == 1:
|
|
51
|
+
return text.lower()
|
|
52
|
+
upper = text.upper().replace(' ', '_')
|
|
53
|
+
if upper in _KEY_ALIASES:
|
|
54
|
+
return _KEY_ALIASES[upper]
|
|
55
|
+
key = upper.replace('_', '').lower()
|
|
56
|
+
if key in pyautogui.KEYBOARD_KEYS:
|
|
57
|
+
return key
|
|
58
|
+
raise ValueError(f"Unknown key '{name}'.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class KeyboardKeywords(KeywordGroup):
|
|
62
|
+
def _focus(self, element):
|
|
63
|
+
if element not in (None, ''):
|
|
64
|
+
self._click_at(self._locate(element))
|
|
65
|
+
time.sleep(0.05)
|
|
66
|
+
|
|
67
|
+
def _write(self, text: str):
|
|
68
|
+
if text.isascii():
|
|
69
|
+
pyautogui.write(text, interval=0.01)
|
|
70
|
+
else:
|
|
71
|
+
self._paste(text)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _paste(text: str):
|
|
75
|
+
try:
|
|
76
|
+
previous = pyperclip.paste()
|
|
77
|
+
except Exception:
|
|
78
|
+
previous = None
|
|
79
|
+
pyperclip.copy(text)
|
|
80
|
+
pyautogui.hotkey('ctrl', 'v')
|
|
81
|
+
time.sleep(0.1)
|
|
82
|
+
if previous is not None:
|
|
83
|
+
pyperclip.copy(previous)
|
|
84
|
+
|
|
85
|
+
@keyword
|
|
86
|
+
@busy_action
|
|
87
|
+
def input_text(self, element, text: str):
|
|
88
|
+
"""Clicks ``element`` (if given) and types ``text``.
|
|
89
|
+
|
|
90
|
+
``element`` can be empty to type into the currently focused control.
|
|
91
|
+
Non-ASCII text is typed through the clipboard.
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
| Input Text | username_field.png | john |
|
|
95
|
+
| Input Text | automation_id:txtName | John Doe |
|
|
96
|
+
| Input Text | ${EMPTY} | text typed into the focused field |
|
|
97
|
+
"""
|
|
98
|
+
self._focus(element)
|
|
99
|
+
self._ready()
|
|
100
|
+
self._write(str(text))
|
|
101
|
+
|
|
102
|
+
@keyword
|
|
103
|
+
@busy_action
|
|
104
|
+
def paste_text(self, element, text: str):
|
|
105
|
+
"""Clicks ``element`` (if given) and pastes ``text`` via the clipboard.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
| Paste Text | notes.png | Olá, ação! |
|
|
109
|
+
| Paste Text | ${EMPTY} | pasted into the focused control |
|
|
110
|
+
"""
|
|
111
|
+
self._focus(element)
|
|
112
|
+
self._ready()
|
|
113
|
+
self._paste(str(text))
|
|
114
|
+
|
|
115
|
+
@keyword
|
|
116
|
+
@busy_action
|
|
117
|
+
def press_special_key(self, key_constant: str):
|
|
118
|
+
"""Presses a special key such as ``ENTER``, ``TAB``, ``ESC``, ``DELETE``,
|
|
119
|
+
``F1``...``F12``, ``UP``, ``DOWN``, ``PAGE_UP``, ``HOME``.
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
| Double Click | field.png |
|
|
123
|
+
| Press Special Key | DELETE |
|
|
124
|
+
| Press Special Key | ENTER |
|
|
125
|
+
"""
|
|
126
|
+
self._ready()
|
|
127
|
+
pyautogui.press(to_key(key_constant))
|
|
128
|
+
|
|
129
|
+
@keyword
|
|
130
|
+
@busy_action
|
|
131
|
+
def key_down(self, key_constant: str):
|
|
132
|
+
"""Presses and holds a keyboard key.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
| Key Down | CTRL |
|
|
136
|
+
| Click | item1.png |
|
|
137
|
+
| Click | item2.png |
|
|
138
|
+
| Key Up | CTRL |
|
|
139
|
+
"""
|
|
140
|
+
self._ready()
|
|
141
|
+
pyautogui.keyDown(to_key(key_constant))
|
|
142
|
+
|
|
143
|
+
@keyword
|
|
144
|
+
@busy_action
|
|
145
|
+
def key_up(self, key_constant: str):
|
|
146
|
+
"""Releases a keyboard key held by `Key Down`.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
| Key Up | CTRL |
|
|
150
|
+
"""
|
|
151
|
+
pyautogui.keyUp(to_key(key_constant))
|
|
152
|
+
|
|
153
|
+
@keyword
|
|
154
|
+
@busy_action
|
|
155
|
+
def type_with_modifiers(self, text: str, *modifiers):
|
|
156
|
+
"""Types ``text`` while holding ``modifiers`` (``CTRL``, ``ALT``, ``SHIFT``, ``WIN``).
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
| Type With Modifiers | a | CTRL | | # select all |
|
|
160
|
+
| Type With Modifiers | s | CTRL | SHIFT |
|
|
161
|
+
"""
|
|
162
|
+
mods = [to_key(m) for m in modifiers]
|
|
163
|
+
self._ready()
|
|
164
|
+
for m in mods:
|
|
165
|
+
pyautogui.keyDown(m)
|
|
166
|
+
try:
|
|
167
|
+
for ch in str(text):
|
|
168
|
+
pyautogui.press(to_key(ch) if len(ch) == 1 else ch)
|
|
169
|
+
finally:
|
|
170
|
+
for m in reversed(mods):
|
|
171
|
+
pyautogui.keyUp(m)
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pyautogui
|
|
4
|
+
from robot.api.deco import keyword
|
|
5
|
+
|
|
6
|
+
from ..geometry import Region, sort_matches, to_region
|
|
7
|
+
from ..locator import parse_locator
|
|
8
|
+
from .base import KeywordGroup, busy_action
|
|
9
|
+
|
|
10
|
+
_BUTTONS = {'LEFT': 'left', 'MIDDLE': 'middle', 'RIGHT': 'right'}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MouseKeywords(KeywordGroup):
|
|
14
|
+
def __init__(self, ctx):
|
|
15
|
+
super().__init__(ctx)
|
|
16
|
+
self._held: set[str] = set()
|
|
17
|
+
|
|
18
|
+
# ---- clicks -------------------------------------------------------------
|
|
19
|
+
@keyword
|
|
20
|
+
@busy_action
|
|
21
|
+
def click(self, element, x_offset: int = 0, y_offset: int = 0):
|
|
22
|
+
"""Clicks the given ``element``.
|
|
23
|
+
|
|
24
|
+
``element`` can be any of the formats described in `Element locators`
|
|
25
|
+
(image path, dictionary, UI Automation locator, region or match).
|
|
26
|
+
``x_offset``/``y_offset`` are added to the offset defined in the locator.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
| Click | buttons/save.png |
|
|
30
|
+
| Click | save.png=0.9 | | | # custom similarity |
|
|
31
|
+
| Click | save.png | 10 | -5 | # click 10px right and 5px above the center |
|
|
32
|
+
| Click | automation_id:btnSave |
|
|
33
|
+
| Click | name:OK AND control_type:Button |
|
|
34
|
+
| ${btn}= | Create Dictionary | path=ok.png | similarity=0.8 |
|
|
35
|
+
| Click | ${btn} |
|
|
36
|
+
| Click | ${{ {"path": ["ok.png", "ok_dark.png"], "offset": {"x": 5, "y": 0}} }} |
|
|
37
|
+
"""
|
|
38
|
+
self._click_at(self._locate(element, x_offset, y_offset))
|
|
39
|
+
|
|
40
|
+
@keyword
|
|
41
|
+
@busy_action
|
|
42
|
+
def double_click(self, element, x_offset: int = 0, y_offset: int = 0):
|
|
43
|
+
"""Double clicks the given ``element``. See `Click` for arguments.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
| Double Click | file_icon.png |
|
|
47
|
+
| Double Click | name:report.txt |
|
|
48
|
+
"""
|
|
49
|
+
self._click_at(self._locate(element, x_offset, y_offset), clicks=2)
|
|
50
|
+
|
|
51
|
+
@keyword
|
|
52
|
+
@busy_action
|
|
53
|
+
def right_click(self, element, x_offset: int = 0, y_offset: int = 0):
|
|
54
|
+
"""Right clicks the given ``element``. See `Click` for arguments.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
| Right Click | desktop_icon.png |
|
|
58
|
+
| Right Click | automation_id:listItems | 0 | 20 |
|
|
59
|
+
"""
|
|
60
|
+
self._click_at(
|
|
61
|
+
self._locate(element, x_offset, y_offset), button='right'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _click_in(self, area, target, clicks=1, button='left'):
|
|
65
|
+
area_match = self._locate(area)
|
|
66
|
+
locator = parse_locator(target).with_region(
|
|
67
|
+
Region(*area_match.to_tuple())
|
|
68
|
+
)
|
|
69
|
+
self._click_at(self._locate(locator), clicks, button)
|
|
70
|
+
|
|
71
|
+
@keyword
|
|
72
|
+
@busy_action
|
|
73
|
+
def click_in(self, area_element, target_element):
|
|
74
|
+
"""Clicks ``target_element`` searched only inside ``area_element``.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
| Click In | dialog.png | ok.png |
|
|
78
|
+
| Click In | name:Settings | checkbox.png |
|
|
79
|
+
"""
|
|
80
|
+
self._click_in(area_element, target_element)
|
|
81
|
+
|
|
82
|
+
@keyword
|
|
83
|
+
@busy_action
|
|
84
|
+
def double_click_in(self, area_element, target_element):
|
|
85
|
+
"""Double clicks ``target_element`` inside ``area_element``.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
| Double Click In | file_list.png | report_icon.png |
|
|
89
|
+
"""
|
|
90
|
+
self._click_in(area_element, target_element, clicks=2)
|
|
91
|
+
|
|
92
|
+
@keyword
|
|
93
|
+
@busy_action
|
|
94
|
+
def right_click_in(self, area_element, target_element):
|
|
95
|
+
"""Right clicks ``target_element`` inside ``area_element``.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
| Right Click In | file_list.png | report_icon.png |
|
|
99
|
+
"""
|
|
100
|
+
self._click_in(area_element, target_element, button='right')
|
|
101
|
+
|
|
102
|
+
@keyword
|
|
103
|
+
@busy_action
|
|
104
|
+
def click_nth(self, element, index: int, sort_by_column: bool = True):
|
|
105
|
+
"""Clicks the ``index``-th (0 based) occurrence of ``element``.
|
|
106
|
+
|
|
107
|
+
Matches are sorted by column (left to right, then top to bottom) when
|
|
108
|
+
``sort_by_column`` is true, otherwise by row (top to bottom, then left
|
|
109
|
+
to right).
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
| Click Nth | checkbox.png | 0 |
|
|
113
|
+
| Click Nth | checkbox.png | 2 | ${FALSE} |
|
|
114
|
+
"""
|
|
115
|
+
matches = self.ctx.find_all(
|
|
116
|
+
element, self.ctx.timeout, wait_cursor=True
|
|
117
|
+
)
|
|
118
|
+
if not matches:
|
|
119
|
+
raise AssertionError(
|
|
120
|
+
f"Element '{parse_locator(element).describe()}' not found."
|
|
121
|
+
)
|
|
122
|
+
matches = sort_matches(matches, 'column' if sort_by_column else 'row')
|
|
123
|
+
index = int(index)
|
|
124
|
+
if not 0 <= index < len(matches):
|
|
125
|
+
raise AssertionError(
|
|
126
|
+
f'Index {index} out of range: {len(matches)} matches found.'
|
|
127
|
+
)
|
|
128
|
+
self.ctx.show_action(matches[index])
|
|
129
|
+
self._click_at(matches[index])
|
|
130
|
+
|
|
131
|
+
@keyword
|
|
132
|
+
@busy_action
|
|
133
|
+
def click_on_match(self, match):
|
|
134
|
+
"""Clicks a match returned by `Return Match From Region` (no offset).
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
| ${match}= | Return Match From Region | ${region} | item.png |
|
|
138
|
+
| Click On Match | ${match} |
|
|
139
|
+
"""
|
|
140
|
+
self._click_at(self._locate(match))
|
|
141
|
+
|
|
142
|
+
@keyword
|
|
143
|
+
@busy_action
|
|
144
|
+
def double_click_on_match(self, match):
|
|
145
|
+
"""Double clicks a match returned by `Return Match From Region`.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
| Double Click On Match | ${match} |
|
|
149
|
+
"""
|
|
150
|
+
self._click_at(self._locate(match), clicks=2)
|
|
151
|
+
|
|
152
|
+
@keyword
|
|
153
|
+
@busy_action
|
|
154
|
+
def click_on_region(self, region):
|
|
155
|
+
"""Clicks the center of ``region`` (e.g. from `Get Extended Region From`).
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
| ${region}= | Get Extended Region From | label.png | below | 1 |
|
|
159
|
+
| Click On Region | ${region} |
|
|
160
|
+
"""
|
|
161
|
+
self._click_at(self._locate(to_region(region)))
|
|
162
|
+
|
|
163
|
+
@keyword
|
|
164
|
+
@busy_action
|
|
165
|
+
def double_click_on_region(self, region):
|
|
166
|
+
"""Double clicks the center of ``region``.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
| Double Click On Region | ${region} |
|
|
170
|
+
"""
|
|
171
|
+
self._click_at(self._locate(to_region(region)), clicks=2)
|
|
172
|
+
|
|
173
|
+
@keyword
|
|
174
|
+
@busy_action
|
|
175
|
+
def click_region(
|
|
176
|
+
self, coordinates, wait_change: float = 0, timeout: float = 0
|
|
177
|
+
):
|
|
178
|
+
"""Clicks the center of the region ``coordinates`` ``[x, y, w, h]``.
|
|
179
|
+
|
|
180
|
+
If ``wait_change`` is greater than 0, waits up to that many seconds for
|
|
181
|
+
the region content to change after the click and fails otherwise.
|
|
182
|
+
If ``timeout`` is greater than 0 the region is highlighted for that
|
|
183
|
+
many seconds before clicking.
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
| ${coor}= | Create List | 0 | 0 | 100 | 100 |
|
|
187
|
+
| Click Region | ${coor} |
|
|
188
|
+
| Click Region | ${coor} | 2 | | # wait up to 2s for the region to change |
|
|
189
|
+
| Click Region | ${coor} | 0 | 1 | # highlight 1s before clicking |
|
|
190
|
+
"""
|
|
191
|
+
region = to_region(coordinates)
|
|
192
|
+
timeout = float(timeout)
|
|
193
|
+
if timeout > 0:
|
|
194
|
+
self.ctx.overlay.highlight(region, timeout)
|
|
195
|
+
time.sleep(timeout)
|
|
196
|
+
before = (
|
|
197
|
+
self.ctx.image.grab(region) if float(wait_change) > 0 else None
|
|
198
|
+
)
|
|
199
|
+
self._click_at(self._locate(region))
|
|
200
|
+
if before is not None:
|
|
201
|
+
changed = self.ctx._poll(
|
|
202
|
+
lambda: self.ctx.image.grab(region).tobytes()
|
|
203
|
+
!= before.tobytes(),
|
|
204
|
+
float(wait_change),
|
|
205
|
+
)
|
|
206
|
+
if not changed:
|
|
207
|
+
raise AssertionError(
|
|
208
|
+
f'{region} did not change in {wait_change} seconds.'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# ---- drag and drop --------------------------------------------------------
|
|
212
|
+
def _drag(self, sx, sy, tx, ty):
|
|
213
|
+
self._move(sx, sy)
|
|
214
|
+
pyautogui.mouseDown(sx, sy)
|
|
215
|
+
time.sleep(0.1)
|
|
216
|
+
pyautogui.moveTo(tx, ty, duration=max(0.3, self.ctx.move_mouse_delay))
|
|
217
|
+
time.sleep(0.1)
|
|
218
|
+
pyautogui.mouseUp(tx, ty)
|
|
219
|
+
|
|
220
|
+
@keyword
|
|
221
|
+
@busy_action
|
|
222
|
+
def drag_and_drop(self, source_element, target_element):
|
|
223
|
+
"""Drags ``source_element`` and drops it on ``target_element``.
|
|
224
|
+
|
|
225
|
+
If ``source_element`` is empty, the last match is dragged.
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
| Drag And Drop | file.png | trash.png |
|
|
229
|
+
| Drag And Drop | name:Item 1 | name:Drop Area |
|
|
230
|
+
| Drag And Drop | ${EMPTY} | trash.png |
|
|
231
|
+
"""
|
|
232
|
+
src = self.ctx.resolve_or_last(source_element)
|
|
233
|
+
dst = self._locate(target_element)
|
|
234
|
+
self._drag(*src.target, *dst.target)
|
|
235
|
+
|
|
236
|
+
@keyword
|
|
237
|
+
@busy_action
|
|
238
|
+
def drag_and_drop_by_offset(
|
|
239
|
+
self, source_element, x_offset: int, y_offset: int
|
|
240
|
+
):
|
|
241
|
+
"""Drags ``source_element`` and drops it ``x_offset``/``y_offset`` pixels away.
|
|
242
|
+
|
|
243
|
+
If ``source_element`` is empty, the last match is dragged.
|
|
244
|
+
|
|
245
|
+
Examples:
|
|
246
|
+
| Drag And Drop By Offset | slider.png | 100 | 0 |
|
|
247
|
+
| Drag And Drop By Offset | ${EMPTY} | 0 | -50 |
|
|
248
|
+
"""
|
|
249
|
+
src = self.ctx.resolve_or_last(source_element)
|
|
250
|
+
sx, sy = src.target
|
|
251
|
+
self._drag(sx, sy, sx + int(x_offset), sy + int(y_offset))
|
|
252
|
+
|
|
253
|
+
# ---- move / buttons / wheel -------------------------------------------------
|
|
254
|
+
@keyword
|
|
255
|
+
def mouse_move(self, element=''):
|
|
256
|
+
"""Moves the mouse pointer to ``element``.
|
|
257
|
+
|
|
258
|
+
If ``element`` is empty, moves to the last match.
|
|
259
|
+
|
|
260
|
+
Examples:
|
|
261
|
+
| Mouse Move | menu.png |
|
|
262
|
+
| Mouse Move | automation_id:btnHelp |
|
|
263
|
+
| Mouse Move | |
|
|
264
|
+
"""
|
|
265
|
+
match = self.ctx.resolve_or_last(element)
|
|
266
|
+
self._move(*match.target)
|
|
267
|
+
|
|
268
|
+
@keyword
|
|
269
|
+
def mouse_move_location(self, x: int = 0, y: int = 0):
|
|
270
|
+
"""Moves the mouse pointer to the absolute screen coordinate ``x``, ``y``.
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
| Mouse Move Location | 20 | 20 |
|
|
274
|
+
"""
|
|
275
|
+
self._move(int(x), int(y))
|
|
276
|
+
|
|
277
|
+
@keyword
|
|
278
|
+
def mouse_move_region(self, coordinates, highlight_timeout: float = 0):
|
|
279
|
+
"""Moves the mouse pointer to the center of region ``coordinates``.
|
|
280
|
+
|
|
281
|
+
Optionally highlights the region for ``highlight_timeout`` seconds.
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
| Mouse Move Region | [20, 20, 20, 20] |
|
|
285
|
+
| Mouse Move Region | ${coordinates} | 2 |
|
|
286
|
+
"""
|
|
287
|
+
region = to_region(coordinates)
|
|
288
|
+
if float(highlight_timeout) > 0:
|
|
289
|
+
self.ctx.overlay.highlight(region, float(highlight_timeout))
|
|
290
|
+
self._move(*region.center)
|
|
291
|
+
|
|
292
|
+
@keyword
|
|
293
|
+
@busy_action
|
|
294
|
+
def mouse_down(self, *mouse_buttons):
|
|
295
|
+
"""Presses and holds the given mouse buttons (``LEFT``, ``MIDDLE``, ``RIGHT``).
|
|
296
|
+
|
|
297
|
+
Defaults to ``LEFT`` when no button is given.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
| Mouse Move | item.png |
|
|
301
|
+
| Mouse Down | LEFT |
|
|
302
|
+
| Mouse Move | target.png |
|
|
303
|
+
| Mouse Up |
|
|
304
|
+
"""
|
|
305
|
+
self._ready()
|
|
306
|
+
for b in mouse_buttons or ('LEFT',):
|
|
307
|
+
btn = _BUTTONS[str(b).upper()]
|
|
308
|
+
pyautogui.mouseDown(button=btn)
|
|
309
|
+
self._held.add(btn)
|
|
310
|
+
|
|
311
|
+
@keyword
|
|
312
|
+
@busy_action
|
|
313
|
+
def mouse_up(self, *mouse_buttons):
|
|
314
|
+
"""Releases the given mouse buttons. If empty, releases all held buttons.
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
| Mouse Up | LEFT | RIGHT |
|
|
318
|
+
| Mouse Up |
|
|
319
|
+
"""
|
|
320
|
+
buttons = (
|
|
321
|
+
[_BUTTONS[str(b).upper()] for b in mouse_buttons]
|
|
322
|
+
or list(self._held)
|
|
323
|
+
or ['left']
|
|
324
|
+
)
|
|
325
|
+
for btn in buttons:
|
|
326
|
+
pyautogui.mouseUp(button=btn)
|
|
327
|
+
self._held.discard(btn)
|
|
328
|
+
|
|
329
|
+
def _wheel(self, steps, element, direction):
|
|
330
|
+
if element not in (None, ''):
|
|
331
|
+
self._move(*self._locate(element).target)
|
|
332
|
+
self._ready()
|
|
333
|
+
for _ in range(int(steps)):
|
|
334
|
+
pyautogui.scroll(direction * 120)
|
|
335
|
+
time.sleep(0.02)
|
|
336
|
+
|
|
337
|
+
@keyword
|
|
338
|
+
@busy_action
|
|
339
|
+
def wheel_down(self, steps: int, element=''):
|
|
340
|
+
"""Moves the mouse to ``element`` (optional) and scrolls down ``steps`` notches.
|
|
341
|
+
|
|
342
|
+
Examples:
|
|
343
|
+
| Wheel Down | 5 |
|
|
344
|
+
| Wheel Down | 5 | list.png |
|
|
345
|
+
"""
|
|
346
|
+
self._wheel(steps, element, -1)
|
|
347
|
+
|
|
348
|
+
@keyword
|
|
349
|
+
@busy_action
|
|
350
|
+
def wheel_up(self, steps: int, element=''):
|
|
351
|
+
"""Moves the mouse to ``element`` (optional) and scrolls up ``steps`` notches.
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
| Wheel Up | 5 |
|
|
355
|
+
| Wheel Up | 5 | automation_id:lstItems |
|
|
356
|
+
"""
|
|
357
|
+
self._wheel(steps, element, 1)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import unicodedata
|
|
2
|
+
|
|
3
|
+
from robot.api.deco import keyword
|
|
4
|
+
|
|
5
|
+
from ..geometry import Match, Region, to_region
|
|
6
|
+
from .base import KeywordGroup, busy_action
|
|
7
|
+
|
|
8
|
+
_ENGINE = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _engine():
|
|
12
|
+
global _ENGINE
|
|
13
|
+
if _ENGINE is None:
|
|
14
|
+
try:
|
|
15
|
+
from rapidocr import RapidOCR
|
|
16
|
+
except ImportError as err:
|
|
17
|
+
raise RuntimeError(
|
|
18
|
+
"OCR requires 'rapidocr' and 'onnxruntime': "
|
|
19
|
+
'pip install robotframework-desktop[ocr]'
|
|
20
|
+
) from err
|
|
21
|
+
_ENGINE = RapidOCR()
|
|
22
|
+
return _ENGINE
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize(text: str) -> str:
|
|
26
|
+
decomposed = unicodedata.normalize('NFKD', str(text))
|
|
27
|
+
plain = ''.join(c for c in decomposed if not unicodedata.combining(c))
|
|
28
|
+
return ' '.join(plain.lower().split())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ocr_lines(image):
|
|
32
|
+
"""Runs OCR and returns ``[(text, x1, y1, x2, y2), ...]`` in reading order."""
|
|
33
|
+
import cv2
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
array = cv2.cvtColor(np.array(image.convert('RGB')), cv2.COLOR_RGB2BGR)
|
|
37
|
+
result = _engine()(array)
|
|
38
|
+
boxes = getattr(result, 'boxes', None)
|
|
39
|
+
texts = getattr(result, 'txts', None)
|
|
40
|
+
if boxes is None or texts is None:
|
|
41
|
+
return []
|
|
42
|
+
lines = []
|
|
43
|
+
for box, text in zip(boxes, texts):
|
|
44
|
+
if not str(text).strip():
|
|
45
|
+
continue
|
|
46
|
+
xs = [float(p[0]) for p in box]
|
|
47
|
+
ys = [float(p[1]) for p in box]
|
|
48
|
+
lines.append((str(text).strip(), min(xs), min(ys), max(xs), max(ys)))
|
|
49
|
+
if not lines:
|
|
50
|
+
return []
|
|
51
|
+
height = sorted(l[4] - l[2] for l in lines)[len(lines) // 2] or 1
|
|
52
|
+
return sorted(lines, key=lambda l: (round(l[2] / (height * 0.6)), l[1]))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OcrKeywords(KeywordGroup):
|
|
56
|
+
def _check(self):
|
|
57
|
+
if not self.ctx.ocr_enabled:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
"OCR is disabled. Use 'Set Ocr Text Read true'."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _read(self, region: Region) -> str:
|
|
63
|
+
self._check()
|
|
64
|
+
lines = _ocr_lines(self.ctx.image.grab(region))
|
|
65
|
+
rows, last_y, height = [], None, None
|
|
66
|
+
for text, x1, y1, x2, y2 in lines:
|
|
67
|
+
if last_y is not None and abs(y1 - last_y) <= (height or 0) * 0.6:
|
|
68
|
+
rows[-1].append(text)
|
|
69
|
+
else:
|
|
70
|
+
rows.append([text])
|
|
71
|
+
last_y, height = y1, y2 - y1
|
|
72
|
+
return '\n'.join(' '.join(row) for row in rows).strip()
|
|
73
|
+
|
|
74
|
+
def _find_text(self, text: str, region: Region) -> Match:
|
|
75
|
+
self._check()
|
|
76
|
+
wanted = _normalize(text)
|
|
77
|
+
if not wanted:
|
|
78
|
+
raise ValueError('Text cannot be empty.')
|
|
79
|
+
|
|
80
|
+
def search():
|
|
81
|
+
for line, x1, y1, x2, y2 in _ocr_lines(
|
|
82
|
+
self.ctx.image.grab(region)
|
|
83
|
+
):
|
|
84
|
+
normalized = _normalize(line)
|
|
85
|
+
start = normalized.find(wanted)
|
|
86
|
+
if start < 0:
|
|
87
|
+
continue
|
|
88
|
+
char_w = (x2 - x1) / max(len(normalized), 1)
|
|
89
|
+
left = x1 + start * char_w
|
|
90
|
+
width = max(len(wanted) * char_w, 1)
|
|
91
|
+
return Match(
|
|
92
|
+
int(region.x + left),
|
|
93
|
+
int(region.y + y1),
|
|
94
|
+
int(round(width)),
|
|
95
|
+
int(round(y2 - y1)),
|
|
96
|
+
source=f'text:{text}',
|
|
97
|
+
)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
match = self.ctx._poll(search, self.ctx.timeout)
|
|
101
|
+
if not match:
|
|
102
|
+
raise AssertionError(f"Text '{text}' not found on screen.")
|
|
103
|
+
self.ctx.last_match = match
|
|
104
|
+
return match
|
|
105
|
+
|
|
106
|
+
@keyword
|
|
107
|
+
def set_ocr_text_read(self, ocr_text_read: bool):
|
|
108
|
+
"""Enables or disables OCR keywords (enabled by default).
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
| Set Ocr Text Read | true |
|
|
112
|
+
"""
|
|
113
|
+
self.ctx.ocr_enabled = str(ocr_text_read).lower() in (
|
|
114
|
+
'true',
|
|
115
|
+
'1',
|
|
116
|
+
'yes',
|
|
117
|
+
'on',
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@keyword
|
|
121
|
+
def get_text(self, element='') -> str:
|
|
122
|
+
"""Reads text with OCR (RapidOCR) from ``element`` area or from the
|
|
123
|
+
whole screen.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
| ${all}= | Get Text |
|
|
127
|
+
| ${txt}= | Get Text | status_area.png |
|
|
128
|
+
| ${txt}= | Get Text | automation_id:lblStatus |
|
|
129
|
+
"""
|
|
130
|
+
if element in (None, ''):
|
|
131
|
+
return self._read(self.ctx.search_region())
|
|
132
|
+
match = self.ctx.find(element)
|
|
133
|
+
return self._read(Region(*match.to_tuple()))
|
|
134
|
+
|
|
135
|
+
@keyword
|
|
136
|
+
def read_text_from_region(self, region) -> str:
|
|
137
|
+
"""Reads text with OCR from ``region`` ``[x, y, w, h]``.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
| ${region}= | Get Extended Region From Image | label.png | right | 3 |
|
|
141
|
+
| ${text}= | Read Text From Region | ${region} |
|
|
142
|
+
"""
|
|
143
|
+
return self._read(to_region(region))
|
|
144
|
+
|
|
145
|
+
@keyword
|
|
146
|
+
@busy_action
|
|
147
|
+
def click_text(self, text: str):
|
|
148
|
+
"""Clicks ``text`` found with OCR on the current screen. Matching is
|
|
149
|
+
case and accent insensitive.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
| Click Text | Hello |
|
|
153
|
+
"""
|
|
154
|
+
self._click_at(self._find_text(text, self.ctx.screen()))
|
|
155
|
+
|
|
156
|
+
@keyword
|
|
157
|
+
@busy_action
|
|
158
|
+
def region_click_text(self, text: str):
|
|
159
|
+
"""Clicks ``text`` found with OCR inside the region of interest
|
|
160
|
+
(see `Set ROI`).
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
| Set ROI | [0, 0, 600, 400] |
|
|
164
|
+
| Region Click Text | Hello |
|
|
165
|
+
"""
|
|
166
|
+
self._click_at(self._find_text(text, self.ctx.search_region()))
|