├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── requirements.txt ├── setup.py └── uisoup ├── __init__.py ├── interfaces ├── __init__.py ├── i_element.py ├── i_keyboard.py ├── i_mouse.py └── i_soup.py ├── mac_soup ├── __init__.py ├── element.py ├── keyboard.py ├── mac_soup.py └── mouse.py ├── ui_inspector.py ├── utils ├── __init__.py ├── common.py ├── mac_utils.py └── win_utils.py └── win_soup ├── __init__.py ├── element.py ├── keyboard.py ├── mouse.py └── win_soup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | .idea 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | recursive-include uisoup *.py -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | UISoup 2 | ====== 3 | 4 | .. image:: https://img.shields.io/pypi/v/UISoup.svg 5 | :alt: Release Status 6 | :target: https://pypi.python.org/pypi/UISoup 7 | 8 | **This library supports UI-related testing using Python on Windows and Mac OS. (Only Python x86 is supported)** 9 | 10 | 11 | **How to use examples:** 12 | 13 | * Calculator: 14 | 15 | .. code:: python 16 | 17 | from uisoup import uisoup 18 | 19 | 20 | calculator = uisoup.get_window('Calculator') 21 | 22 | calculator.drag_to(50, 50, x_offset=30, y_offset=5) 23 | b1 = calculator.find(c_name='btn2') 24 | b1.click() 25 | ba = calculator.find(c_name='btnAdd') 26 | ba.click() 27 | b2 = calculator.find(c_name='btn3') 28 | b2.click() 29 | be = calculator.find(c_name='btnEquals') 30 | be.click() 31 | 32 | * Notepad: 33 | 34 | .. code:: python 35 | 36 | from uisoup import uisoup 37 | 38 | 39 | # You can use wildcard in names such as "?" and "*". 40 | notepad = uisoup.get_window('*Notepad') 41 | 42 | notepad.set_focus() 43 | kc = uisoup.keyboard.codes 44 | uisoup.keyboard.send(kc.SHIFT.modify(kc.KEY_H), kc.KEY_E, kc.KEY_L, 45 | kc.KEY_L, kc.KEY_O, kc.SPACE, kc.KEY_W, kc.KEY_O, 46 | kc.KEY_R, kc.KEY_L, kc.KEY_D, 47 | kc.SHIFT.modify(kc.KEY_1)) 48 | 49 | 50 | Also adds :code:`ui-inspector` script that allows you to inspect UI elements. Just type it in terminal. 51 | 52 | **Changelog:** 53 | 54 | UISoup 2.5.7 (released 27 Apr 2018) 55 | 56 | * Fixes: fix issue with pip.req in new version of pip. 57 | 58 | UISoup 2.5.5 (released 01 Jun 2017) 59 | 60 | * Fixes: fix issue #19 that blocks __str__ method of element. 61 | 62 | UISoup 2.5.4 (released 14 Apr 2017) 63 | 64 | * Additions: added support for Python3. 65 | * Additions: docstrings were updated. 66 | 67 | UISoup 2.4.3 (released 15 Apr 2015) 68 | 69 | * Additions: fixed mouse double click. 70 | 71 | UISoup 2.4.2 (released 8 Apr 2015) 72 | 73 | * Additions: updated with smooth mouse move. 74 | * Additions: updated with delay between key press in Keyboard.send() method. 75 | 76 | UISoup 2.4.1 (released 4 Mar 2015) 77 | 78 | * Mac OS Additions: added new element role "AXLink". 79 | * Mac OS Additions: fixed issue when we getting fail on execution "get attribute "AXURL" of UI element" string. 80 | 81 | UISoup 2.4 (released 5 Feb 2015) 82 | 83 | * Mac OS Additions: fixed issue when we can't work with windows that have double quotes in name. 84 | 85 | UISoup 2.2 (released 16 Dec 2014) 86 | 87 | * Mac OS Additions: added ability to see AXDialog windows. 88 | * Mac OS Additions: fixed issue when incorrect applescript specifier was constructed. 89 | 90 | UISoup 2.0 (released 20 Jun 2014) 91 | 92 | * Mac OS Additions: added version for Mac OS. 93 | 94 | UISoup 1.0 (released 28 Mar 2014) 95 | 96 | * Windows Additions: initial version for Windows. 97 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | comtypes>=1.1.3 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os import path 4 | 5 | from setuptools import setup 6 | 7 | 8 | def package_env(file_name, strict=False): 9 | file_path = path.join(path.dirname(__file__), file_name) 10 | if path.exists(file_path) or strict: 11 | return open(file_path).read() 12 | else: 13 | return '' 14 | 15 | 16 | def parse_requirements(filename): 17 | lineiter = (line.strip() for line in open(filename)) 18 | return [line for line in lineiter if line and not line.startswith("#")] 19 | 20 | 21 | if __name__ == '__main__': 22 | setup( 23 | name='UISoup', 24 | version='2.5.7', 25 | description='Cross Platform GUI Test Automation tool.', 26 | long_description=package_env('README.rst'), 27 | author='Max Beloborodko', 28 | author_email='f1ashhimself@gmail.com', 29 | packages=['uisoup'], 30 | include_package_data=True, 31 | install_requires=parse_requirements('requirements.txt'), 32 | zip_safe=False, 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'ui-inspector = uisoup.ui_inspector:main' 36 | ] 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /uisoup/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | 20 | from platform import system 21 | 22 | uisoup = None 23 | 24 | 25 | class TooSaltyUISoupException(Exception): 26 | pass 27 | 28 | if system() == 'Windows': 29 | from uisoup.win_soup import WinSoup 30 | uisoup = WinSoup() 31 | elif system() == 'Darwin': 32 | from uisoup.mac_soup import MacSoup 33 | uisoup = MacSoup() 34 | else: 35 | raise TooSaltyUISoupException('We are sorry but we don\'t have UISoup ' 36 | 'implementation for "%s" OS.' % system()) 37 | -------------------------------------------------------------------------------- /uisoup/interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | -------------------------------------------------------------------------------- /uisoup/interfaces/i_element.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | import re 20 | from inspect import ismethod 21 | from types import FunctionType 22 | from abc import ABCMeta, abstractmethod, abstractproperty 23 | import xml.dom.minidom 24 | 25 | from ..utils.common import CommonUtils 26 | 27 | if CommonUtils.is_python_3(): 28 | unicode = str 29 | 30 | 31 | class IElement(object): 32 | """ 33 | Class that describes UI object. 34 | """ 35 | 36 | __metaclass__ = ABCMeta 37 | 38 | @abstractmethod 39 | def click(self, x_offset=None, y_offset=None): 40 | """ 41 | Clicks by left mouse button on this object. 42 | 43 | :param int x_offset: if not defined half of element width will be used. 44 | :param int y_offset: y offset, if not defined half of element height 45 | will be used. 46 | """ 47 | 48 | @abstractmethod 49 | def right_click(self, x_offset=None, y_offset=None): 50 | """ 51 | Clicks by right mouse button on this object. 52 | 53 | :param int x_offset: if not defined half of element width will be used. 54 | :param int y_offset: y offset, if not defined half of element height 55 | will be used. 56 | """ 57 | 58 | @abstractmethod 59 | def double_click(self, x_offset=None, y_offset=None): 60 | """ 61 | Double clicks by left mouse button on this object. 62 | 63 | :param int x_offset: if not defined half of element width will be used. 64 | :param int y_offset: y offset, if not defined half of element height 65 | will be used 66 | """ 67 | 68 | @abstractmethod 69 | def drag_to(self, x, y, x_offset=None, y_offset=None, smooth=True): 70 | """ 71 | Drags this object to coordinates. 72 | 73 | :param int x: x coordinate. 74 | :param int y: y coordinate. 75 | :param int x_offset: if not defined half of element width will be used. 76 | :param int y_offset: if not defined half of element height 77 | will be used. 78 | :param bool smooth: indicates is it needed to simulate smooth movement. 79 | """ 80 | 81 | @abstractproperty 82 | def proc_id(self): 83 | """ 84 | Indicates process id. 85 | """ 86 | 87 | @abstractproperty 88 | def is_top_level_window(self): 89 | """ 90 | Indicates is top level window or not. 91 | """ 92 | 93 | @abstractproperty 94 | def is_selected(self): 95 | """ 96 | Indicates selected state. 97 | """ 98 | 99 | @abstractproperty 100 | def is_checked(self): 101 | """ 102 | Indicates checked state. 103 | """ 104 | 105 | @abstractproperty 106 | def is_visible(self): 107 | """ 108 | Indicates visible state. 109 | """ 110 | 111 | @abstractproperty 112 | def is_enabled(self): 113 | """ 114 | Indicates enabled state. 115 | """ 116 | 117 | @abstractproperty 118 | def acc_parent_count(self): 119 | """ 120 | Property for parent child count. 121 | """ 122 | 123 | @abstractproperty 124 | def acc_child_count(self): 125 | """ 126 | Property for element child count. 127 | """ 128 | 129 | @abstractproperty 130 | def acc_name(self): 131 | """ 132 | Property for element name. 133 | Also need to specify setter for this property 134 | """ 135 | 136 | @abstractmethod 137 | def set_focus(self): 138 | """ 139 | Sets focus to element. 140 | """ 141 | 142 | @abstractproperty 143 | def acc_c_name(self): 144 | """ 145 | Property for combined name (role name + name). 146 | """ 147 | 148 | @abstractproperty 149 | def acc_location(self): 150 | """ 151 | Property for element location. 152 | """ 153 | 154 | @abstractproperty 155 | def acc_value(self): 156 | """ 157 | Property for element value. 158 | Also need to specify setter for this property. 159 | """ 160 | 161 | @abstractmethod 162 | def set_value(self, value): 163 | """ 164 | Sets element value. 165 | 166 | :param str value: element value. 167 | """ 168 | 169 | @abstractproperty 170 | def acc_description(self): 171 | """ 172 | Property for element description. 173 | """ 174 | 175 | @abstractproperty 176 | def acc_parent(self): 177 | """ 178 | Property for element parent. 179 | """ 180 | 181 | @abstractproperty 182 | def acc_selection(self): 183 | """ 184 | Property for element selection. 185 | """ 186 | 187 | @abstractproperty 188 | def acc_focused_element(self): 189 | """ 190 | Property for element in focus. 191 | """ 192 | 193 | @abstractproperty 194 | def acc_role_name(self): 195 | """ 196 | Property for element role name. 197 | """ 198 | 199 | @abstractmethod 200 | def __iter__(self): 201 | """Iterate all child Element""" 202 | 203 | @abstractmethod 204 | def find(self, only_visible=True, **kwargs): 205 | """ 206 | Finds first child element. 207 | 208 | :param bool only_visible: flag that indicates will we search only 209 | through visible elements. 210 | :param str role: string or lambda e.g. lambda x: x == 13 211 | :param str name: string or lambda. 212 | :param str c_name: string or lambda. 213 | :param str location: string or lambda. 214 | :param str value: string or lambda. 215 | :param str description: string or lambda. 216 | :param str selection: string or lambda. 217 | :param str role_name: string or lambda. 218 | :param str parent_count: string or lambda. 219 | :param str child_count: string or lambda. 220 | :rtype: IElement 221 | :return: Element that was found otherwise exception will be raised. 222 | """ 223 | 224 | @abstractmethod 225 | def findall(self, only_visible=True, **kwargs): 226 | """ 227 | Find all child element. 228 | 229 | :param bool only_visible: flag that indicates will we search only 230 | through visible elements. 231 | :param str role: string or lambda e.g. lambda x: x == 13 232 | :param str name: string or lambda. 233 | :param str c_name: string or lambda. 234 | :param str location: string or lambda. 235 | :param str value: string or lambda. 236 | :param str description: string or lambda. 237 | :param str selection: string or lambda. 238 | :param str role_name: string or lambda. 239 | :param str parent_count: string or lambda. 240 | :param str child_count: string or lambda. 241 | :rtype: list[IElement] 242 | :return: List of all elements that was found otherwise None. 243 | """ 244 | 245 | @abstractmethod 246 | def is_object_exists(self, **kwargs): 247 | """ 248 | Verifies is object exists. 249 | 250 | :param bool only_visible: flag that indicates will we search only 251 | through visible elements. 252 | :param str role: string or lambda e.g. lambda x: x == 13 253 | :param str name: string or lambda. 254 | :param str c_name: string or lambda. 255 | :param str location: string or lambda. 256 | :param str value: string or lambda. 257 | :param str description: string or lambda. 258 | :param str selection: string or lambda. 259 | :param str role_name: string or lambda. 260 | :param str parent_count: string or lambda. 261 | :param str child_count: string or lambda. 262 | :rtype: bool 263 | :return: True if object exists otherwise False. 264 | """ 265 | 266 | def toxml(self): 267 | """ 268 | Convert Element Tree to XML. 269 | """ 270 | obj_document = xml.dom.minidom.Document() 271 | lst_queue = [(self, obj_document)] 272 | 273 | while lst_queue: 274 | obj_element, obj_tree = lst_queue.pop(0) 275 | role_name = obj_element.acc_role_name 276 | obj_name = obj_element.acc_name 277 | str_name = unicode(obj_name) if obj_name else '' 278 | str_location = ','.join(str(x) for x in obj_element.acc_location) 279 | obj_sub_tree = xml.dom.minidom.Element(role_name) 280 | obj_sub_tree.ownerDocument = obj_document 281 | 282 | try: 283 | obj_sub_tree.attributes['Name'] = str_name 284 | except: 285 | obj_sub_tree.attributes['Name'] = \ 286 | str_name.encode('unicode-escape') 287 | 288 | obj_sub_tree.attributes['Location'] = str_location 289 | obj_tree.appendChild(obj_sub_tree) 290 | 291 | if obj_element.acc_child_count: 292 | for obj_element_child in obj_element: 293 | lst_queue.append((obj_element_child, obj_sub_tree)) 294 | 295 | return obj_document.toprettyxml() 296 | 297 | def __str__(self): 298 | result = '[Role: %s | Name: %r | Child count: %d]' % \ 299 | (self.acc_role_name, 300 | self.acc_name, 301 | self.acc_child_count) 302 | 303 | return result 304 | 305 | def _match(self, only_visible, **kwargs): 306 | """ 307 | Match method. 308 | 309 | :param bool only_visible: flag that indicates will we search only 310 | through visible elements. 311 | :param str role: string or lambda e.g. lambda x: x == 13 312 | :param str name: string or lambda. 313 | :param str c_name: string or lambda. 314 | :param str location: string or lambda. 315 | :param str value: string or lambda. 316 | :param str description: string or lambda. 317 | :param str selection: string or lambda. 318 | :param str role_name: string or lambda. 319 | :param str parent_count: string or lambda. 320 | :param str child_count: string or lambda. 321 | :rtype: bool 322 | :return: True if element was matched otherwise False. 323 | """ 324 | try: 325 | if only_visible and not self.is_visible: 326 | return False 327 | 328 | for str_property, expected_result in kwargs.items(): 329 | attr = getattr(self, 'acc_' + str_property) 330 | if ismethod(attr): 331 | attr = attr() 332 | 333 | if type(expected_result) is FunctionType: 334 | if not expected_result(attr): 335 | return False 336 | else: 337 | regex = CommonUtils.convert_wildcard_to_regex( 338 | expected_result) 339 | if not re.match(regex, attr): 340 | return False 341 | except: 342 | return False 343 | else: 344 | return True 345 | -------------------------------------------------------------------------------- /uisoup/interfaces/i_keyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | from abc import ABCMeta, abstractmethod, abstractproperty 20 | 21 | from .. import TooSaltyUISoupException 22 | 23 | 24 | class Key(object): 25 | """ 26 | Decorator class to specify modifier key relations. 27 | """ 28 | 29 | def __init__(self, hex_key_code): 30 | self.code = hex_key_code 31 | self.children = None 32 | 33 | def modify(self, *args): 34 | """ 35 | Specifies Keys that will be modified by a current key. 36 | 37 | :param args: Keys to be modified by current key. 38 | :rtype: Key 39 | :return: New instance of Key with children Keys to be modified. 40 | """ 41 | for arg in args: 42 | if not isinstance(arg, Key): 43 | raise TooSaltyUISoupException('Key instance is expected.') 44 | 45 | modified_key_press = Key(self.code) 46 | modified_key_press.children = args 47 | return modified_key_press 48 | 49 | 50 | class IKeyboard(object): 51 | 52 | __metaclass__ = ABCMeta 53 | 54 | @abstractproperty 55 | def codes(self): 56 | """ 57 | Container class to store KeyCodes as Keys class. 58 | """ 59 | 60 | @abstractmethod 61 | def press_key(self, hex_key_code): 62 | """ 63 | Presses (and releases) key specified by a hex code. 64 | 65 | :param int hex_key_code: integer value holding hexadecimal code for 66 | a key to be pressed. 67 | """ 68 | 69 | @abstractmethod 70 | def press_key_and_hold(self, hex_key_code): 71 | """ 72 | Presses (and holds) key specified by a hex code. 73 | 74 | :param int hex_key_code: integer value holding hexadecimal code for 75 | a key to be pressed. 76 | """ 77 | 78 | @abstractmethod 79 | def release_key(self, hex_key_code): 80 | """ 81 | Releases key specified by a hex code. 82 | 83 | :param int hex_key_code: integer value holding hexadecimal code for 84 | a key to be released. 85 | """ 86 | 87 | @abstractmethod 88 | def send(self, *args, **kwargs): 89 | """ 90 | Send key events as specified by Keys. 91 | 92 | If Key contains children Keys they will be recursively 93 | processed with current Key code pressed as a modifier key. 94 | 95 | :param args: Keys to send. 96 | :param kwargs: "delay" between keys in seconds. 97 | """ 98 | -------------------------------------------------------------------------------- /uisoup/interfaces/i_mouse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | from abc import ABCMeta, abstractmethod, abstractproperty 20 | 21 | 22 | class IMouse(object): 23 | """ 24 | Class to simulate mouse activities. 25 | """ 26 | 27 | __metaclass__ = ABCMeta 28 | 29 | @abstractproperty 30 | def LEFT_BUTTON(self): 31 | """ 32 | Constant for left mouse button. 33 | """ 34 | 35 | @abstractproperty 36 | def RIGHT_BUTTON(self): 37 | """ 38 | Constant for right mouse button. 39 | """ 40 | 41 | @abstractmethod 42 | def move(self, x, y, smooth=True): 43 | """ 44 | Move the mouse to the specified coordinates. 45 | 46 | :param int x: x coordinate. 47 | :param int y: y coordinate. 48 | :param bool smooth: indicates is it needed to simulate smooth movement. 49 | """ 50 | 51 | @abstractmethod 52 | def drag(self, x1, y1, x2, y2, smooth=True): 53 | """ 54 | Drags the mouse to the specified coordinates. 55 | 56 | :param int x1: x start coordinate. 57 | :param int y1: y start coordinate. 58 | :param int x2: x target coordinate. 59 | :param int y2: y target coordinate. 60 | :param bool smooth: indicates is it needed to simulate smooth movement. 61 | """ 62 | 63 | @abstractmethod 64 | def press_button(self, x, y, button_name=LEFT_BUTTON): 65 | """ 66 | Presses mouse button as dictated by coordinates and button name. 67 | 68 | :param int x: x coordinate to press mouse at. 69 | :param int y: y coordinate to press mouse at. 70 | :param str button_name: mouse button name. Should be one 71 | of: 'b1c' - left button or 'b3c' - right button. 72 | """ 73 | 74 | @abstractmethod 75 | def release_button(self, button_name=LEFT_BUTTON): 76 | """ 77 | Releases mouse button by button name. 78 | 79 | :param str button_name: mouse button name. Should be one 80 | of: 'b1c' - left button or 'b3c' - right button. 81 | """ 82 | 83 | @abstractmethod 84 | def click(self, x, y, button_name=LEFT_BUTTON): 85 | """ 86 | Clicks as dictated by coordinates and button name. 87 | 88 | :param int x: x coordinate to click mouse at. 89 | :param int y: y coordinate to click mouse at. 90 | :param str button_name: mouse button name. Should be one 91 | of: 'b1c' - left button or 'b3c' - right button. 92 | """ 93 | 94 | @abstractmethod 95 | def double_click(self, x, y, button_name=LEFT_BUTTON): 96 | """ 97 | Double-clicks as dictated by coordinates and button name. 98 | 99 | :param int x: x coordinate to double-click at. 100 | :param int y: y coordinate to double-click at. 101 | :param str button_name: mouse button name. Should be one 102 | of: 'b1c' - left button or 'b3c' - right button. 103 | """ 104 | 105 | @abstractmethod 106 | def get_position(self): 107 | """ 108 | Returns current mouse cursor position. 109 | 110 | :rtype: tuple[int, int] 111 | :return: x and y coordinates of current mouse cursor position. 112 | """ 113 | -------------------------------------------------------------------------------- /uisoup/interfaces/i_soup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | from abc import ABCMeta, abstractmethod, abstractproperty 20 | 21 | 22 | class ISoup(object): 23 | """ 24 | Class to work with UI objects. 25 | """ 26 | 27 | __metaclass__ = ABCMeta 28 | 29 | @abstractproperty 30 | def mouse(self): 31 | """ 32 | Instance of IMouse implementation. 33 | """ 34 | 35 | @abstractproperty 36 | def keyboard(self): 37 | """ 38 | Instance of IKeyboard implementation. 39 | """ 40 | 41 | @abstractmethod 42 | def get_object_by_coordinates(self, x, y): 43 | """ 44 | Gets object by coordinates. 45 | 46 | :param int x: x coordinate. 47 | :param int y: y coordinate. 48 | :rtype: uisoup.interfaces.i_element.IElement 49 | :return: object that was found by given coordinates. 50 | """ 51 | 52 | @abstractmethod 53 | def is_window_exists(self, obj_handle): 54 | """ 55 | Verifies is window exists. 56 | 57 | :param str | int obj_handle: window name (string) or window 58 | handler (int) otherwise Desktop Window will be checked. 59 | :rtype: bool 60 | :return: True if window exists otherwise False. 61 | """ 62 | 63 | @abstractmethod 64 | def get_window(self, obj_handle=None): 65 | """ 66 | Gets window. 67 | 68 | :param str | int obj_handle: window name (string) or window 69 | handler (int) otherwise Desktop Window will be checked. 70 | :rtype: uisoup.interfaces.i_element.IElement 71 | :return: window object. 72 | """ 73 | 74 | @abstractmethod 75 | def get_visible_window_list(self): 76 | """ 77 | Gets list of visible windows. 78 | 79 | :rtype: list[uisoup.interfaces.i_element.IElement] 80 | :return: list of visible windows. 81 | """ 82 | 83 | @abstractmethod 84 | def get_visible_object_list(self, window_name): 85 | """ 86 | Gets list of visible objects for specified window. 87 | 88 | :param str window_name: window name. 89 | :rtype: list[uisoup.interfaces.i_element.IElement] 90 | :return: list of visible windows. 91 | """ 92 | -------------------------------------------------------------------------------- /uisoup/mac_soup/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014-2017 Max Beloborodko. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __author__ = 'f1ashhimself@gmail.com' 19 | 20 | from .mac_soup import MacSoup 21 | -------------------------------------------------------------------------------- /uisoup/mac_soup/element.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | from ..interfaces.i_element import IElement 20 | from ..utils.mac_utils import MacUtils 21 | from .. import TooSaltyUISoupException 22 | from .mouse import MacMouse 23 | 24 | 25 | class MacElement(IElement): 26 | 27 | _acc_role_name_map = { 28 | 'AXWindow': u'frm', 29 | 'AXTextArea': u'txt', 30 | 'AXTextField': u'txt', 31 | 'AXButton': u'btn', 32 | 'AXStaticText': u'lbl', 33 | 'AXRadioButton': u'rbtn', 34 | 'AXSlider': u'sldr', 35 | 'AXCell': u'tblc', 36 | 'AXImage': u'img', 37 | 'AXToolbar': u'tbar', 38 | 'AXScrollBar': u'scbr', 39 | 'AXMenuItem': u'mnu', 40 | 'AXMenu': u'mnu', 41 | 'AXMenuBar': u'mnu', 42 | 'AXMenuBarItem': u'mnu', 43 | 'AXCheckBox': u'chk', 44 | 'AXTabGroup': u'ptl', 45 | 'AXList': u'lst', 46 | 'AXMenuButton': u'cbo', 47 | 'AXRow': u'tblc', 48 | 'AXColumn': u'col', 49 | 'AXTable': u'tbl', 50 | 'AXScrollArea': u'sar', 51 | 'AXOutline': u'otl', 52 | 'AXValueIndicator': u'val', 53 | 'AXDisclosureTriangle': u'dct', 54 | 'AXGroup': u'grp', 55 | 'AXPopUpButton': u'pubtn', 56 | 'AXApplication': u'app', 57 | 'AXDocItem': u'doc', 58 | 'AXHeading': u'tch', 59 | 'AXGenericElement': u'gen', 60 | 'AXLink': u'lnk' 61 | } 62 | 63 | _mouse = MacMouse() 64 | 65 | def __init__(self, obj_selector, layer_num, process_name, process_id, 66 | class_id=None): 67 | """ 68 | Constructor. 69 | 70 | :param str obj_selector: object selector. 71 | :param int layer_num: layer number. I.e. main window will be layer 0. 72 | :param str process_name: process name. 73 | :param str process_id: process id. 74 | :param str class_id: element class identifier. 75 | """ 76 | self._object_selector = obj_selector 77 | self._layer_num = layer_num 78 | self._proc_id = process_id 79 | self._proc_name = process_name 80 | self._class_id = class_id 81 | self._cached_children = set() 82 | self._cached_properties = None 83 | 84 | @property 85 | def _properties(self): 86 | """ 87 | Property for element properties. 88 | """ 89 | if not self._cached_properties: 90 | self._cached_properties = \ 91 | MacUtils.ApplescriptExecutor.get_element_properties( 92 | self._object_selector, self._proc_name) 93 | 94 | return self._cached_properties 95 | 96 | @property 97 | def _role(self): 98 | """ 99 | Property for element role. 100 | """ 101 | return self._properties.get('AXRole', None) 102 | 103 | def _find_windows_by_same_proc(self): 104 | """ 105 | Find window by same process id. 106 | 107 | :rtype: list[uisoup.interfaces.i_element.IElement] 108 | :return: list of windows. 109 | """ 110 | axunknown_windows = \ 111 | MacUtils.ApplescriptExecutor.get_axunknown_windows(self._proc_name) 112 | axdialog_windows = \ 113 | MacUtils.ApplescriptExecutor.get_axdialog_windows(self._proc_name) 114 | 115 | mac_elements = \ 116 | [MacElement(element.applescript_specifier, 1, self._proc_name, 117 | self._proc_id, element.class_id) for element in 118 | axunknown_windows + axdialog_windows] 119 | 120 | return filter(lambda x: x.acc_child_count, mac_elements) 121 | 122 | def click(self, x_offset=0, y_offset=0): 123 | x, y, w, h = self.acc_location 124 | x += x_offset if x_offset is not None else w / 2 125 | y += y_offset if y_offset is not None else h / 2 126 | 127 | self._mouse.click(x, y) 128 | self._cached_properties = None 129 | 130 | def right_click(self, x_offset=0, y_offset=0): 131 | x, y, w, h = self.acc_location 132 | x += x_offset if x_offset is not None else w / 2 133 | y += y_offset if y_offset is not None else h / 2 134 | 135 | self._mouse.click(x, y, self._mouse.RIGHT_BUTTON) 136 | self._cached_properties = None 137 | 138 | def double_click(self, x_offset=0, y_offset=0): 139 | x, y, w, h = self.acc_location 140 | x += x_offset if x_offset is not None else w / 2 141 | y += y_offset if y_offset is not None else h / 2 142 | 143 | self._mouse.double_click(x, y) 144 | self._cached_properties = None 145 | 146 | def drag_to(self, x, y, x_offset=None, y_offset=None, smooth=True): 147 | el_x, el_y, el_w, el_h = self.acc_location 148 | el_x += x_offset if x_offset is not None else el_w / 2 149 | el_y += y_offset if y_offset is not None else el_h / 2 150 | 151 | self._mouse.drag(el_x, el_y, x, y, smooth) 152 | self._cached_properties = None 153 | 154 | @property 155 | def proc_id(self): 156 | return self._proc_id 157 | 158 | @property 159 | def is_top_level_window(self): 160 | return self.acc_parent_count == 0 161 | 162 | @property 163 | def is_selected(self): 164 | result = False 165 | if self.acc_role_name == self._acc_role_name_map['AXRadioButton'] and \ 166 | self._properties.get('AXValue', 'false') == 'true': 167 | result = True 168 | 169 | return result 170 | 171 | @property 172 | def is_checked(self): 173 | result = False 174 | if self.acc_role_name == self._acc_role_name_map['AXCheckBox'] and \ 175 | self._properties.get('AXValue', 'false') == 'true': 176 | result = True 177 | 178 | return result 179 | 180 | @property 181 | def is_visible(self): 182 | return True # We can't work with invisible elements under Mac OS. 183 | 184 | @property 185 | def is_enabled(self): 186 | return bool(self._properties.get('AXEnabled', False)) 187 | 188 | @property 189 | def acc_parent_count(self): 190 | return self._layer_num 191 | 192 | @property 193 | def acc_child_count(self): 194 | children_elements = MacUtils.ApplescriptExecutor.get_children_elements( 195 | self._object_selector, self.acc_parent_count, self._proc_name) 196 | 197 | return len(children_elements[0]) 198 | 199 | @property 200 | def acc_name(self): 201 | result = self._properties.get('AXDescription') or \ 202 | self._properties.get('AXTitle') 203 | 204 | if not result: 205 | if self.acc_role_name == self._acc_role_name_map['AXTextField']: 206 | result = self._class_id 207 | else: 208 | result = self._properties.get('AXValue') or self._class_id 209 | 210 | return MacUtils.replace_inappropriate_symbols(result or '') 211 | 212 | def set_focus(self): 213 | MacUtils.ApplescriptExecutor.set_element_attribute_value( 214 | self._object_selector, 'AXFocused', 'true', self._proc_name, False) 215 | 216 | @property 217 | def acc_c_name(self): 218 | return self.acc_role_name + self.acc_name if self.acc_name else '' 219 | 220 | @property 221 | def acc_location(self): 222 | x, y = self._properties.get('AXPosition', [0, 0]) 223 | w, h = self._properties.get('AXSize', [0, 0]) 224 | 225 | return map(int, [x, y, w, h]) 226 | 227 | @property 228 | def acc_value(self): 229 | return self._properties.get('AXValue', None) 230 | 231 | def set_value(self, value): 232 | MacUtils.ApplescriptExecutor.set_element_attribute_value( 233 | self._object_selector, 'AXValue', value, self._proc_name) 234 | self._cached_properties = None 235 | 236 | @property 237 | def acc_description(self): 238 | return self._properties.get('AXDescription', None) 239 | 240 | @property 241 | def acc_parent(self): 242 | result = None 243 | event_descriptor = \ 244 | MacUtils.ApplescriptExecutor.get_apple_event_descriptor( 245 | self._object_selector, self._proc_name) 246 | if self.acc_parent_count > 0 and event_descriptor.from_: 247 | result = \ 248 | MacElement(event_descriptor.from_.applescript_specifier, 249 | self.acc_parent_count - 1, 250 | self._proc_name, 251 | self.proc_id, 252 | event_descriptor.class_id) 253 | 254 | return result 255 | 256 | @property 257 | def acc_selection(self): 258 | return self._properties.get('AXSelectedText', None) 259 | 260 | @property 261 | def acc_focused_element(self): 262 | childs = self.findall() 263 | 264 | result = None 265 | for element in childs: 266 | if self._properties.get('AXFocused', 'false') == 'true': 267 | result = element 268 | break 269 | 270 | return result 271 | 272 | @property 273 | def acc_role_name(self): 274 | return self._acc_role_name_map.get(self._role, 'unknown') 275 | 276 | def __iter__(self): 277 | children_elements = MacUtils.ApplescriptExecutor.get_children_elements( 278 | self._object_selector, self._layer_num, self._proc_name) 279 | 280 | children = children_elements[0] 281 | layer_number = children_elements[1] 282 | 283 | if not len(children): 284 | raise StopIteration() 285 | 286 | for element in children: 287 | yield MacElement(element['selector'], layer_number, 288 | self._proc_name, self.proc_id, 289 | element['class_id']) 290 | 291 | def __findcacheiter(self, only_visible, **kwargs): 292 | """ 293 | Find child element in the cache. 294 | 295 | :param bool only_visible: flag that indicates will we search only 296 | visible. 297 | :rtype: uisoup.interfaces.i_element.IElement 298 | :return: yield found element. 299 | """ 300 | for obj_element in self._cached_children: 301 | if obj_element._match(only_visible, **kwargs): 302 | yield obj_element 303 | 304 | def _finditer(self, only_visible, **kwargs): 305 | """ 306 | Find child element. 307 | 308 | :param bool only_visible: flag that indicates will we search only 309 | visible. 310 | :rtype: uisoup.interfaces.i_element.IElement 311 | :return: yield found element. 312 | """ 313 | lst_queue = list(self) 314 | 315 | if self.is_top_level_window: 316 | lst_queue.extend(self._find_windows_by_same_proc()) 317 | 318 | while lst_queue: 319 | obj_element = lst_queue.pop(0) 320 | self._cached_children.add(obj_element) 321 | 322 | if obj_element._match(only_visible, **kwargs): 323 | yield obj_element 324 | 325 | if obj_element.acc_child_count: 326 | childs = [el for el in list(obj_element)] 327 | lst_queue[:0] = childs 328 | 329 | def find(self, only_visible=True, **kwargs): 330 | try: 331 | iter_ = next(self.__findcacheiter(only_visible, **kwargs)) if \ 332 | MacUtils.is_python_3() else self.__findcacheiter( 333 | only_visible, **kwargs).next() 334 | return iter_ 335 | except StopIteration: 336 | try: 337 | iter_ = next(self._finditer(only_visible, **kwargs)) if \ 338 | MacUtils.is_python_3() else self._finditer( 339 | only_visible, **kwargs).next() 340 | return iter_ 341 | except StopIteration: 342 | items_ = kwargs.items() if MacUtils.is_python_3() else \ 343 | kwargs.iteritems() 344 | attrs = ['%s=%s' % (k, v) for k, v in items_] 345 | raise TooSaltyUISoupException( 346 | 'Can\'t find object with attributes "%s".' % 347 | '; '.join(attrs)) 348 | 349 | def findall(self, only_visible=True, **kwargs): 350 | result = self._finditer(only_visible, **kwargs) 351 | if result: 352 | result = list(result) 353 | 354 | return result 355 | 356 | def is_object_exists(self, **kwargs): 357 | try: 358 | self.find(**kwargs) 359 | return True 360 | except TooSaltyUISoupException: 361 | return False 362 | -------------------------------------------------------------------------------- /uisoup/mac_soup/keyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2014 Oleksandr Iakovenko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'alex.jakovenko@gmail.com' 18 | 19 | from Quartz import CoreGraphics as CG 20 | 21 | from time import sleep 22 | 23 | from ..interfaces.i_keyboard import Key, IKeyboard 24 | 25 | 26 | class MacKeyboard(IKeyboard): 27 | 28 | class _KeyCodes(object): 29 | """ 30 | Holder for Macintosh keyboard codes stored as Keys. 31 | """ 32 | LEFT_ALT = Key(0x3A) # Left ALT key 33 | RIGHT_ALT = Key(0x3D) # Right ALT key 34 | LEFT_SHIFT = Key(0x38) # Left SHIFT key 35 | RIGHT_SHIFT = Key(0x3C) # Right SHIFT key 36 | LEFT_CONTROL = Key(0x3B) # Left CONTROL key 37 | RIGHT_CONTROL = Key(0x3E) # Right CONTROL key 38 | LEFT_COMMAND = Key(0x37) # Left COMMAND key 39 | RIGHT_COMMAND = Key(0x36) # Right COMMAND key 40 | BACKSPACE = Key(0x33) # BACKSPACE key 41 | TAB = Key(0x30) # TAB key 42 | CLEAR = Key(0x47) # CLEAR key 43 | RETURN = Key(0x24) # ENTER key 44 | SHIFT = LEFT_SHIFT # SHIFT key 45 | CONTROL = LEFT_CONTROL # CTRL key 46 | ALT = LEFT_ALT # ALT key 47 | CAPS_LOCK = Key(0x39) # CAPS LOCK key 48 | ESCAPE = Key(0x35) # ESC key 49 | SPACE = Key(0x31) # SPACEBAR 50 | PAGE_UP = Key(0x74) # PAGE UP key 51 | PAGE_DOWN = Key(0x79) # PAGE DOWN key 52 | END = Key(0x77) # END key 53 | HOME = Key(0x73) # HOME key 54 | LEFT = Key(0x7B) # LEFT ARROW key 55 | UP = Key(0x7E) # UP ARROW key 56 | RIGHT = Key(0x7C) # RIGHT ARROW key 57 | DOWN = Key(0x7D) # DOWN ARROW key 58 | INSERT = Key(0x72) # INS key 59 | DELETE = Key(0x75) # DEL key 60 | KEY_0 = Key(0x1D) # 0 key 61 | KEY_1 = Key(0x12) # 1 key 62 | KEY_2 = Key(0x13) # 2 key 63 | KEY_3 = Key(0x14) # 3 key 64 | KEY_4 = Key(0x15) # 4 key 65 | KEY_5 = Key(0x17) # 5 key 66 | KEY_6 = Key(0x16) # 6 key 67 | KEY_7 = Key(0x1A) # 7 key 68 | KEY_8 = Key(0x1C) # 8 key 69 | KEY_9 = Key(0x19) # 9 key 70 | KEY_A = Key(0x00) # A key 71 | KEY_B = Key(0x0B) # B key 72 | KEY_C = Key(0x08) # C key 73 | KEY_D = Key(0x02) # D key 74 | KEY_E = Key(0x0E) # E key 75 | KEY_F = Key(0x03) # F key 76 | KEY_G = Key(0x05) # G key 77 | KEY_H = Key(0x04) # H key 78 | KEY_I = Key(0x22) # I key 79 | KEY_J = Key(0x26) # J key 80 | KEY_K = Key(0x28) # K key 81 | KEY_L = Key(0x25) # L key 82 | KEY_M = Key(0x2E) # M key 83 | KEY_N = Key(0x2D) # N key 84 | KEY_O = Key(0x1F) # O key 85 | KEY_P = Key(0x23) # P key 86 | KEY_Q = Key(0x0C) # Q key 87 | KEY_R = Key(0x0F) # R key 88 | KEY_S = Key(0x01) # S key 89 | KEY_T = Key(0x11) # T key 90 | KEY_U = Key(0x20) # U key 91 | KEY_V = Key(0x09) # V key 92 | KEY_W = Key(0x0D) # W key 93 | KEY_X = Key(0x07) # X key 94 | KEY_Y = Key(0x10) # Y key 95 | KEY_Z = Key(0x06) # Z key 96 | NUMPAD0 = Key(0x52) # Numeric keypad 0 key 97 | NUMPAD1 = Key(0x53) # Numeric keypad 1 key 98 | NUMPAD2 = Key(0x54) # Numeric keypad 2 key 99 | NUMPAD3 = Key(0x55) # Numeric keypad 3 key 100 | NUMPAD4 = Key(0x56) # Numeric keypad 4 key 101 | NUMPAD5 = Key(0x57) # Numeric keypad 5 key 102 | NUMPAD6 = Key(0x58) # Numeric keypad 6 key 103 | NUMPAD7 = Key(0x59) # Numeric keypad 7 key 104 | NUMPAD8 = Key(0x5B) # Numeric keypad 8 key 105 | NUMPAD9 = Key(0x5C) # Numeric keypad 9 key 106 | MULTIPLY = Key(0x43) # Multiply key 107 | ADD = Key(0x45) # Add key 108 | SUBTRACT = Key(0x4E) # Subtract key 109 | DECIMAL = Key(0x41) # Decimal key 110 | DIVIDE = Key(0x4B) # Divide key 111 | F1 = Key(0x7A) # F1 key 112 | F2 = Key(0x78) # F2 key 113 | F3 = Key(0x63) # F3 key 114 | F4 = Key(0x76) # F4 key 115 | F5 = Key(0x60) # F5 key 116 | F6 = Key(0x61) # F6 key 117 | F7 = Key(0x62) # F7 key 118 | F8 = Key(0x64) # F8 key 119 | F9 = Key(0x65) # F9 key 120 | F10 = Key(0x6D) # F10 key 121 | F11 = Key(0x67) # F11 key 122 | F12 = Key(0x6F) # F12 key 123 | OEM_1 = Key(0x29) # For the US standard keyboard, the ';:' key 124 | OEM_PLUS = Key(0x18) # For any country/region, the '+' key 125 | OEM_COMMA = Key(0x2B) # For any country/region, the ',' key 126 | OEM_MINUS = Key(0x1B) # For any country/region, the '-' key 127 | OEM_PERIOD = Key(0x2F) # For any country/region, the '.' key 128 | OEM_2 = Key(0x2C) # For the US standard keyboard, the '/?' key 129 | OEM_3 = Key(0x32) # For the US standard keyboard, the '`~' key 130 | OEM_4 = Key(0x21) # For the US standard keyboard, the '[{' key 131 | OEM_5 = Key(0x2A) # For the US standard keyboard, the '\|' key 132 | OEM_6 = Key(0x1E) # For the US standard keyboard, the ']}' key 133 | OEM_7 = Key(0x27) # For the US standard keyboard, the ''"' key 134 | 135 | codes = _KeyCodes 136 | 137 | def press_key(self, hex_key_code): 138 | """ 139 | Presses (and releases) key specified by a hex code. 140 | 141 | :param int hex_key_code: hexadecimal code for a key to be pressed. 142 | """ 143 | self.press_key_and_hold(hex_key_code) 144 | self.release_key(hex_key_code) 145 | 146 | def press_key_and_hold(self, hex_key_code): 147 | """ 148 | Presses (and holds) key specified by a hex code. 149 | 150 | :param int hex_key_code: hexadecimal code for a key to be pressed. 151 | """ 152 | CG.CGEventPost( 153 | CG.kCGHIDEventTap, 154 | CG.CGEventCreateKeyboardEvent(None, hex_key_code, True)) 155 | 156 | def release_key(self, hex_key_code): 157 | """ 158 | Releases key specified by a hex code. 159 | 160 | :param int hex_key_code: hexadecimal code for a key to be pressed. 161 | """ 162 | CG.CGEventPost( 163 | CG.kCGHIDEventTap, 164 | CG.CGEventCreateKeyboardEvent(None, hex_key_code, False)) 165 | 166 | def send(self, *args, **kwargs): 167 | """ 168 | Send key events as specified by Keys. 169 | 170 | If Key contains children Keys they will be recursively 171 | processed with current Key code pressed as a modifier key. 172 | 173 | :param args: Keys to send. 174 | """ 175 | delay = kwargs.get('delay', 0) 176 | 177 | for key in args: 178 | if key.children: 179 | self.press_key_and_hold(key.code) 180 | self._wait_for_key_combo_to_be_processed() 181 | self.send(*key.children) 182 | self.release_key(key.code) 183 | else: 184 | self.press_key(key.code) 185 | self._wait_for_key_combo_to_be_processed() 186 | sleep(delay) 187 | 188 | def _wait_for_key_combo_to_be_processed(self): 189 | # For key combinations timeout is needed to be processed. 190 | # This method is expressive shortcut to be used where needed. 191 | sleep(.05) 192 | -------------------------------------------------------------------------------- /uisoup/mac_soup/mac_soup.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014-2017 Max Beloborodko. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __author__ = 'f1ashhimself@gmail.com' 19 | 20 | import sys 21 | import re 22 | 23 | from Quartz import CoreGraphics as CG 24 | 25 | from ..interfaces.i_soup import ISoup 26 | from ..utils.mac_utils import MacUtils 27 | from .element import MacElement 28 | from .mouse import MacMouse 29 | from .keyboard import MacKeyboard 30 | from .. import TooSaltyUISoupException 31 | 32 | if MacUtils.is_python_3(): 33 | unicode = str 34 | 35 | 36 | class MacSoup(ISoup): 37 | 38 | mouse = MacMouse() 39 | keyboard = MacKeyboard() 40 | _default_sys_encoding = sys.stdout.encoding or sys.getdefaultencoding() 41 | 42 | def get_object_by_coordinates(self, x, y): 43 | result = None 44 | 45 | try: 46 | window_handle = \ 47 | MacUtils.ApplescriptExecutor.get_frontmost_window_name() 48 | 49 | window = self.get_window(window_handle) 50 | 51 | # Sorting by layer from big to small. 52 | if MacUtils.is_python_3(): 53 | sorted_objects = \ 54 | sorted(window.findall(), key=lambda x: x._layer_num, 55 | reverse=True) 56 | else: 57 | sorted_objects = \ 58 | sorted(window.findall(), 59 | lambda x, y: y._layer_num - x._layer_num) 60 | 61 | cur_x, cur_y = self.mouse.get_position() 62 | for element in sorted_objects: 63 | x, y, w, h = element.acc_location 64 | if x <= cur_x < x + w and y <= cur_y < y + h: 65 | result = element 66 | break 67 | except: 68 | pass 69 | 70 | return result 71 | 72 | def is_window_exists(self, obj_handle): 73 | try: 74 | self.get_window(obj_handle) 75 | return True 76 | except TooSaltyUISoupException: 77 | return False 78 | 79 | def get_window(self, obj_handle=None): 80 | filters = CG.kCGWindowListOptionOnScreenOnly | \ 81 | CG.kCGWindowListExcludeDesktopElements * bool(obj_handle) 82 | 83 | obj_name = obj_handle if obj_handle else u'DesktopWindow Server' 84 | obj_name = \ 85 | obj_name if type(obj_name) == unicode else obj_name.decode('utf-8') 86 | 87 | regex = MacUtils.convert_wildcard_to_regex(obj_name) 88 | 89 | win_list = CG.CGWindowListCopyWindowInfo(filters, CG.kCGNullWindowID) 90 | 91 | window = filter(lambda x: 92 | re.match(MacUtils.replace_inappropriate_symbols(regex), 93 | MacUtils.replace_inappropriate_symbols( 94 | x.get('kCGWindowName', '')) + 95 | x.get('kCGWindowOwnerName', ''), 96 | re.IGNORECASE) 97 | if x.get('kCGWindowName', '') else False, 98 | win_list) 99 | 100 | window = window[0] if window else window 101 | if not window: 102 | obj_name = obj_name.encode(self._default_sys_encoding, 103 | errors='ignore') 104 | raise TooSaltyUISoupException('Can\'t find window "%s".' % 105 | obj_name) 106 | 107 | process_name = window['kCGWindowOwnerName'] 108 | window_name = window['kCGWindowName'] 109 | process_id = int(window['kCGWindowOwnerPID']) 110 | # Escape double quotes in window name. 111 | window_name = window_name.replace('"', '\\"') 112 | 113 | selector = \ 114 | MacUtils.ApplescriptExecutor.get_apple_event_descriptor( 115 | 'window "%s"' % window_name, 116 | process_name).applescript_specifier 117 | selector = \ 118 | selector if type(selector) == unicode else selector.decode('utf-8') 119 | 120 | return MacElement(selector, 0, process_name, process_id) 121 | 122 | def get_visible_window_list(self): 123 | win_list = CG.CGWindowListCopyWindowInfo( 124 | CG.kCGWindowListOptionOnScreenOnly | 125 | CG.kCGWindowListExcludeDesktopElements, 126 | CG.kCGNullWindowID) 127 | 128 | win_names = \ 129 | [w.get('kCGWindowName', '') + w.get('kCGWindowOwnerName', '') for 130 | w in win_list if w.get('kCGWindowName', '') and 0 not in 131 | [int(w.get('kCGWindowBounds', 0)['Height']), 132 | int(w.get('kCGWindowBounds', 0)['Width'])]] 133 | 134 | windows = list() 135 | for win_name in win_names: 136 | try: 137 | windows.append(self.get_window(win_name)) 138 | except TooSaltyUISoupException: 139 | continue 140 | 141 | return windows 142 | 143 | def get_visible_object_list(self, window_name): 144 | window = self.get_window(window_name) 145 | objects = window.findall( 146 | only_visible=True, 147 | location=lambda x: 0 not in x[2:]) 148 | 149 | return objects 150 | -------------------------------------------------------------------------------- /uisoup/mac_soup/mouse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2014 Oleksandr Iakovenko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'alex.jakovenko@gmail.com' 18 | 19 | 20 | from Quartz import CoreGraphics as CG 21 | 22 | from time import sleep 23 | 24 | from ..interfaces.i_mouse import IMouse 25 | from ..utils.mac_utils import MacUtils 26 | 27 | if MacUtils.is_python_3(): 28 | xrange = range 29 | 30 | 31 | class MacMouse(IMouse): 32 | 33 | LEFT_BUTTON = u'b1c' 34 | RIGHT_BUTTON = u'b3c' 35 | _SUPPORTED_BUTTON_NAMES = [LEFT_BUTTON, RIGHT_BUTTON] 36 | 37 | _LEFT_BUTTON_CODES = [ 38 | CG.kCGEventLeftMouseDown, 39 | CG.kCGEventLeftMouseDragged, 40 | CG.kCGEventLeftMouseUp] 41 | _RIGHT_BUTTON_CODES = [ 42 | CG.kCGEventRightMouseDown, 43 | CG.kCGEventRightMouseDragged, 44 | CG.kCGEventRightMouseUp] 45 | 46 | def _compose_mouse_event_chain(self, name, press=True, release=False): 47 | """ 48 | Composes chain of mouse events based on button name and action flags. 49 | 50 | :param str name: Mouse button name. Should be one of: 51 | 'b1c' - left button or 'b3c' - right button. 52 | :param bool press: flag indicating whether event should indicate 53 | button press. 54 | :param bool release: indicating whether event should indicate 55 | button release. 56 | """ 57 | mouse_event_chain = [] 58 | if name == self.LEFT_BUTTON: 59 | if press: 60 | mouse_event_chain.append(CG.kCGEventLeftMouseDown) 61 | if release: 62 | mouse_event_chain.append(CG.kCGEventLeftMouseUp) 63 | if name == self.RIGHT_BUTTON: 64 | if press: 65 | mouse_event_chain.append(CG.kCGEventRightMouseDown) 66 | if release: 67 | mouse_event_chain.append(CG.kCGEventRightMouseUp) 68 | 69 | return mouse_event_chain 70 | 71 | def _do_event(self, code, x, y): 72 | """ 73 | Generates mouse event for a special coordinate. 74 | 75 | :param int code: mouse event code. 76 | :param int x: x coordinate. 77 | :param int y: y coordinate. 78 | """ 79 | if code in self._LEFT_BUTTON_CODES: 80 | button = CG.kCGMouseButtonLeft 81 | elif code in self._RIGHT_BUTTON_CODES: 82 | button = CG.kCGMouseButtonRight 83 | else: 84 | button = CG.kCGMouseButtonCenter 85 | 86 | CG.CGEventPost( 87 | CG.kCGHIDEventTap, 88 | CG.CGEventCreateMouseEvent(None, code, (x, y), button) 89 | ) 90 | 91 | def _do_events(self, codes, x, y): 92 | """ 93 | Generates a sequence of mouse events for a special coordinate. 94 | 95 | :param list[int] codes: mouse event code. 96 | :param int x: x coordinate. 97 | :param int y: y coordinate. 98 | """ 99 | for code in codes: 100 | self._do_event(code, x, y) 101 | 102 | def move(self, x, y, smooth=True): 103 | MacUtils.verify_xy_coordinates(x, y) 104 | 105 | old_x, old_y = self.get_position() 106 | 107 | for i in xrange(100): 108 | intermediate_x = old_x + (x - old_x) * (i + 1) / 100.0 109 | intermediate_y = old_y + (y - old_y) * (i + 1) / 100.0 110 | smooth and sleep(.01) 111 | 112 | self._do_event(CG.kCGEventMouseMoved, int(intermediate_x), 113 | int(intermediate_y)) 114 | 115 | def drag(self, x1, y1, x2, y2, smooth=True): 116 | MacUtils.verify_xy_coordinates(x1, y1) 117 | MacUtils.verify_xy_coordinates(x2, y2) 118 | 119 | self.press_button(x1, y1, self.LEFT_BUTTON) 120 | 121 | for i in xrange(100): 122 | x = x1 + (x2 - x1) * (i + 1) / 100.0 123 | y = y1 + (y2 - y1) * (i + 1) / 100.0 124 | smooth and sleep(.01) 125 | self._do_event(CG.kCGEventLeftMouseDragged, x, y) 126 | 127 | self.release_button(self.LEFT_BUTTON) 128 | 129 | def press_button(self, x, y, button_name=LEFT_BUTTON): 130 | MacUtils.verify_xy_coordinates(x, y) 131 | MacUtils.verify_mouse_button_name(button_name, 132 | self._SUPPORTED_BUTTON_NAMES) 133 | 134 | event_codes = self._compose_mouse_event_chain( 135 | button_name, press=True, release=False) 136 | self._do_events(event_codes, x, y) 137 | 138 | def release_button(self, button_name=LEFT_BUTTON): 139 | MacUtils.verify_mouse_button_name(button_name, 140 | self._SUPPORTED_BUTTON_NAMES) 141 | 142 | event_codes = self._compose_mouse_event_chain( 143 | button_name, press=False, release=True) 144 | self._do_events(event_codes, 0, 0) 145 | 146 | def click(self, x, y, button_name=LEFT_BUTTON): 147 | MacUtils.verify_xy_coordinates(x, y) 148 | MacUtils.verify_mouse_button_name(button_name, 149 | self._SUPPORTED_BUTTON_NAMES) 150 | 151 | event_codes = self._compose_mouse_event_chain( 152 | button_name, press=True, release=True) 153 | self._do_events(event_codes, x, y) 154 | 155 | def double_click(self, x, y, button_name=LEFT_BUTTON): 156 | MacUtils.verify_xy_coordinates(x, y) 157 | MacUtils.verify_mouse_button_name(button_name, 158 | self._SUPPORTED_BUTTON_NAMES) 159 | 160 | if button_name == self.LEFT_BUTTON: 161 | button = CG.kCGMouseButtonLeft 162 | down = CG.kCGEventLeftMouseDown 163 | up = CG.kCGEventLeftMouseUp 164 | if button_name == self.RIGHT_BUTTON: 165 | button = CG.kCGMouseButtonRight 166 | down = CG.kCGEventRightMouseDown 167 | up = CG.kCGEventRightMouseUp 168 | 169 | # http://www.codeitive.com/0iJqgkejVj/performing-a-double-click-using-cgeventcreatemouseevent.html 170 | event = CG.CGEventCreateMouseEvent(None, down, (x, y), button) 171 | CG.CGEventPost(CG.kCGHIDEventTap, event) 172 | CG.CGEventSetType(event, up) 173 | CG.CGEventPost(CG.kCGHIDEventTap, event) 174 | 175 | CG.CGEventSetIntegerValueField(event, CG.kCGMouseEventClickState, 2) 176 | 177 | CG.CGEventSetType(event, down) 178 | CG.CGEventPost(CG.kCGHIDEventTap, event) 179 | CG.CGEventSetType(event, up) 180 | CG.CGEventPost(CG.kCGHIDEventTap, event) 181 | 182 | def get_position(self): 183 | position = CG.CGEventGetLocation(CG.CGEventCreate(None)) 184 | return int(position.x), int(position.y) 185 | -------------------------------------------------------------------------------- /uisoup/ui_inspector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014-2017 Max Beloborodko. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __author__ = 'f1ashhimself@gmail.com' 19 | 20 | 21 | from os import system 22 | from time import sleep 23 | from inspect import ismethod 24 | from platform import system as platform_system 25 | 26 | from . import uisoup 27 | 28 | 29 | class UIInspector(object): 30 | 31 | @classmethod 32 | def get_current_element_info(cls, obj_element): 33 | """ 34 | Gets current element info. 35 | 36 | :param obj_element: object element. 37 | :rtype: str 38 | :return: element attributes. 39 | """ 40 | dict_info = {} 41 | lst_attribute_name_list = ['acc_role_name', 42 | 'acc_name', 43 | 'acc_value', 44 | 'acc_location', 45 | 'acc_description', 46 | 'acc_child_count'] 47 | 48 | for attr in lst_attribute_name_list: 49 | try: 50 | value = getattr(obj_element, attr) 51 | if ismethod(value): 52 | dict_info[attr] = value() 53 | else: 54 | dict_info[attr] = value 55 | except: 56 | dict_info[attr] = None 57 | 58 | return '\n'.join('%s:\t%r' % (attr, dict_info[attr]) for 59 | attr in lst_attribute_name_list) 60 | 61 | 62 | def main(): 63 | """ 64 | Starts UI Inspector. 65 | """ 66 | 67 | try: 68 | x_old, y_old = None, None 69 | while True: 70 | x, y = uisoup.mouse.get_position() 71 | if (x, y) != (x_old, y_old): 72 | x_old, y_old = x, y 73 | obj_element = uisoup.get_object_by_coordinates(x, y) 74 | clear_command = \ 75 | 'cls' if platform_system() == 'Windows' else 'clear' 76 | printable_data = \ 77 | UIInspector.get_current_element_info(obj_element) 78 | system(clear_command) 79 | print(printable_data) 80 | sleep(0.5) 81 | except KeyboardInterrupt: 82 | system('cls') 83 | -------------------------------------------------------------------------------- /uisoup/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | -------------------------------------------------------------------------------- /uisoup/utils/common.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | import re 20 | import sys 21 | 22 | from .. import TooSaltyUISoupException 23 | 24 | 25 | class CommonUtils(object): 26 | 27 | @classmethod 28 | def convert_wildcard_to_regex(cls, wildcard): 29 | """ 30 | Converts wildcard to regex. 31 | 32 | :param str wildcard: wildcard. 33 | :rtype: str 34 | :return: regex pattern. 35 | """ 36 | regex = re.escape(wildcard) 37 | regex = regex.replace(r'\?', r'[\s\S]{1}') 38 | regex = regex.replace(r'\*', r'[\s\S]*') 39 | 40 | return '^%s$' % regex 41 | 42 | @classmethod 43 | def replace_inappropriate_symbols(cls, text): 44 | """ 45 | Replaces inappropriate symbols e.g. \xa0 (non-breaking space) to 46 | normal space. 47 | 48 | :param str text: text in which symbols should be replaced. 49 | :rtype: str 50 | :return: processed text. 51 | """ 52 | replace_pairs = [(u'\xa0', ' '), 53 | (u'\u2014', '-')] 54 | 55 | for from_, to_ in replace_pairs: 56 | text = text.replace(from_, to_) 57 | 58 | return text 59 | 60 | @classmethod 61 | def verify_xy_coordinates(cls, x, y): 62 | """ 63 | Verifies that x and y is instance of int otherwise raises exception. 64 | 65 | :param x: x variable. 66 | :param y: y variable. 67 | """ 68 | if not isinstance(x, (int, float)) or not isinstance(y, (int, float)): 69 | raise TooSaltyUISoupException( 70 | 'x and y arguments should hold int coordinates.') 71 | 72 | @classmethod 73 | def verify_mouse_button_name(cls, button_name, supported_names): 74 | """ 75 | Verifies that button name is supported otherwise raises exception. 76 | 77 | :param str button_name: button name. 78 | :param list[str] supported_names: supported button names. 79 | """ 80 | if button_name not in supported_names: 81 | raise TooSaltyUISoupException( 82 | 'Button name should be one of supported %s.' % 83 | repr(supported_names)) 84 | 85 | @classmethod 86 | def is_python_3(cls): 87 | """ 88 | Indicates is currently python 3 used or not. 89 | 90 | :rtype: bool 91 | :return: boolean indicator. 92 | """ 93 | return sys.version_info.major == 3 94 | -------------------------------------------------------------------------------- /uisoup/utils/mac_utils.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014-2017 Max Beloborodko. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __author__ = 'f1ashhimself@gmail.com' 19 | 20 | 21 | import struct 22 | from AppKit import NSAppleScript 23 | from Carbon import AppleEvents 24 | 25 | from ..utils.common import CommonUtils 26 | from .. import TooSaltyUISoupException 27 | 28 | if CommonUtils.is_python_3(): 29 | basestring = str 30 | 31 | 32 | class AppleEventDescriptor(object): 33 | 34 | @classmethod 35 | def _get_aeKeyword(cls, four_char_code): 36 | """ 37 | Gets aeKeyword from four character event code. 38 | 39 | Arguments: 40 | - four_char_code: str, four character event code. 41 | 42 | Return: 43 | - integer aeKeyword. 44 | """ 45 | 46 | return struct.unpack('>I', four_char_code)[0] 47 | 48 | @classmethod 49 | def _get_four_char_code(cls, ae_keyword): 50 | """ 51 | Gets four char event code from aeKeyword. 52 | 53 | Arguments: 54 | - ae_keyword: integer, aeKeyword. 55 | 56 | Return: 57 | - string four char code. 58 | """ 59 | 60 | return struct.pack('>I', ae_keyword) 61 | 62 | def __init__(self, event_descriptor): 63 | """ 64 | Constructor. 65 | 66 | Arguments: 67 | - event_descriptor: NSAppleEventDescriptor, instance. 68 | """ 69 | 70 | self._event_descriptor = event_descriptor 71 | 72 | def __iter__(self): 73 | """Iterate all nested elements""" 74 | 75 | if not self.class_name: 76 | i = 1 77 | while self._event_descriptor.descriptorAtIndex_(i): 78 | yield AppleEventDescriptor( 79 | self._event_descriptor.descriptorAtIndex_(i)) 80 | i += 1 81 | 82 | raise StopIteration() 83 | 84 | @property 85 | def form_(self): 86 | """ 87 | Property for element "from" field. 88 | """ 89 | 90 | from_ = self._event_descriptor.descriptorForKeyword_( 91 | self._get_aeKeyword(AppleEvents.keyAEKeyForm)) 92 | 93 | result = AppleEventDescriptor(from_).string_value if from_ else None 94 | 95 | return result 96 | 97 | @property 98 | def class_name(self): 99 | """ 100 | Property for element class name. 101 | """ 102 | 103 | result = None 104 | 105 | if self._event_descriptor.typeCodeValue(): 106 | ae_keyword = \ 107 | self._event_descriptor.descriptorForKeyword_( 108 | self._get_aeKeyword( 109 | AppleEvents.keyAEDesiredClass)) 110 | if ae_keyword: 111 | result = \ 112 | self._get_four_char_code(int(ae_keyword.typeCodeValue())) 113 | 114 | return result 115 | 116 | @property 117 | def class_id(self): 118 | """ 119 | Property for element class id. 120 | """ 121 | 122 | return (self.seld_.string_value or '').replace('\\', '\\\\') 123 | 124 | @property 125 | def seld_(self): 126 | """ 127 | Property for "seld" field. 128 | """ 129 | 130 | seld_ = self._event_descriptor.descriptorForKeyword_( 131 | self._get_aeKeyword(AppleEvents.keyAEKeyData)) 132 | 133 | result = AppleEventDescriptor(seld_) if seld_ else None 134 | 135 | return result 136 | 137 | @property 138 | def from_(self): 139 | """ 140 | Property for "from" field. 141 | """ 142 | 143 | from_ = self._event_descriptor.descriptorForKeyword_( 144 | self._get_aeKeyword(AppleEvents.keyAEContainer)) 145 | 146 | result = AppleEventDescriptor(from_) if from_ else None 147 | 148 | return result 149 | 150 | @property 151 | def string_value(self): 152 | """ 153 | Property for string value. 154 | """ 155 | 156 | return self._event_descriptor.stringValue() 157 | 158 | @property 159 | def applescript_specifier(self): 160 | """ 161 | Property for applescript specifier. 162 | """ 163 | 164 | if self.class_id: 165 | class_id = \ 166 | self.class_id if self.form_ == AppleEvents.kFAIndexParam else \ 167 | u'"%s"' % self.class_id.replace('"', '\\"') 168 | specifier = u'«class %s» %s' % (self.class_name, class_id) 169 | else: 170 | specifier = u'every «class %s»' % self.class_name 171 | 172 | if self.from_.class_name: 173 | # Sometimes applescript returns menu bar as a child of 174 | # window (cwin) but this is wrong and parent of menu bar should 175 | # be application (pcap) so we need to skip one parent (cwin) in 176 | # this case. 177 | parent_element = self.from_ 178 | if self.class_name == 'mbar' and\ 179 | self.from_.class_name == AppleEvents.cWindow: 180 | parent_element = parent_element.from_ 181 | specifier = '%s of %s' % (specifier, 182 | parent_element.applescript_specifier) 183 | else: 184 | specifier = '%s of application "System Events"' % specifier 185 | 186 | return specifier 187 | 188 | 189 | class MacUtils(CommonUtils): 190 | 191 | @classmethod 192 | def execute_applescript_command(cls, cmd): 193 | """ 194 | Executes applescript command. 195 | 196 | :param str | list[str] cmd: command or commands that should be 197 | executed. 198 | :rtype: AppleEventDescriptor 199 | :return: AppleEventDescriptor with result of executed command. 200 | """ 201 | cmd = '\n'.join([cmd] if isinstance(cmd, basestring) else cmd) 202 | 203 | script = NSAppleScript.alloc().initWithSource_(cmd) 204 | result = script.executeAndReturnError_(None) 205 | 206 | if not result[0]: 207 | error_message = 'Error when executing applescript command: %s' %\ 208 | result[1]['NSAppleScriptErrorMessage'] 209 | raise TooSaltyUISoupException(error_message.encode('utf-8', 210 | 'ignore')) 211 | 212 | return AppleEventDescriptor(result[0]) 213 | 214 | class ApplescriptExecutor(object): 215 | 216 | @classmethod 217 | def get_frontmost_window_name(cls): 218 | """ 219 | Gets front window name. 220 | 221 | :rtype: str 222 | :return: combined name (window name + process name). 223 | """ 224 | cmd = ['tell application "System Events" to tell process (name of first application process whose frontmost is true)', 225 | ' return (1st window whose value of attribute "AXMain" is true)', 226 | 'end tell'] 227 | 228 | event_descriptor = MacUtils.execute_applescript_command(cmd) 229 | 230 | return event_descriptor.seld_.string_value + \ 231 | event_descriptor.from_.seld_.string_value 232 | 233 | @classmethod 234 | def get_apple_event_descriptor(cls, obj_selector, process_name): 235 | """ 236 | Gets apple event descriptor. 237 | 238 | :param str obj_selector: object selector. 239 | :param str process_name: name of process. 240 | 241 | :rtype: AppleEventDescriptor 242 | :return: apple event descriptor. 243 | """ 244 | cmd = ['tell application "System Events" to tell process "%s"' % process_name, 245 | ' set visible to true', 246 | ' return %s' % obj_selector, 247 | 'end tell'] 248 | 249 | return MacUtils.execute_applescript_command(cmd) 250 | 251 | @classmethod 252 | def get_children_elements(cls, obj_selector, layer_num, process_name): 253 | """ 254 | Gets all direct children elements. 255 | 256 | :param str obj_selector: object selector. 257 | :param str layer_num: layer number, i.e. main window will be 258 | layer 0. 259 | :param str process_name: name of process. 260 | :rtype: tuple[dict] 261 | :return: Tuple that contains dict of element selectors and 262 | elements layer number. 263 | """ 264 | cmd = ['tell application "System Events" to tell process "%s"' % process_name, 265 | ' set visible to true', 266 | ' set uiElement to %s' % obj_selector, 267 | ' set layer to %s' % layer_num, 268 | ' if uiElement = null then', 269 | ' set layer to 0', 270 | ' set collectedElements to {UI elements of front window, layer}', 271 | ' else', 272 | ' set layer to layer + 1', 273 | ' set collectedElements to {null, layer}', 274 | ' if name of attributes of uiElement contains "AXChildren" then', 275 | ' set collectedElements to {value of attribute "AXChildren" of uiElement, layer}', 276 | ' end if', 277 | ' end if', 278 | 'end tell', 279 | 'return collectedElements'] 280 | 281 | event_descriptors_list = \ 282 | list(MacUtils.execute_applescript_command(cmd)) 283 | 284 | elements = [{'selector': el.applescript_specifier, 285 | 'class_id': el.class_id} for el in 286 | list(event_descriptors_list[0])] 287 | 288 | return elements, int(event_descriptors_list[1].string_value) 289 | 290 | @classmethod 291 | def get_element_properties(cls, obj_selector, process_name): 292 | """ 293 | Gets all element properties. 294 | 295 | :param str obj_selector: object selector. 296 | :param str process_name: name of process. 297 | :rtype: dict 298 | :return: Dict with element properties. 299 | """ 300 | cmd = ['tell application "System Events" to tell application process "%s"' % process_name, 301 | ' set visible to true', 302 | ' set res to {}', 303 | ' repeat with attr in attributes of %s' % obj_selector, 304 | ' try', 305 | ' set res to res & {{name of attr, value of attr}}', 306 | ' end try', 307 | ' end repeat', 308 | ' return res', 309 | 'end tell'] 310 | 311 | event_descriptors_list = list( 312 | MacUtils.execute_applescript_command(cmd)) 313 | 314 | # Unpacking properties to dict. 315 | el_properties = dict() 316 | for prop in event_descriptors_list: 317 | prop = list(prop) 318 | prop_name = prop[0].string_value 319 | prop_value = [e.string_value for e in list(prop[1])] if \ 320 | list(prop[1]) else prop[1].string_value 321 | 322 | el_properties[prop_name] = prop_value 323 | 324 | return el_properties 325 | 326 | @classmethod 327 | def set_element_attribute_value(cls, obj_selector, attribute_name, 328 | value, process_name, 329 | string_value=True): 330 | """ 331 | Sets element attribute. 332 | 333 | :param str obj_selector: object selector. 334 | :param str attribute_name: name of attribute. 335 | :param str value: name of value. 336 | :param str process_name: name of process. 337 | :param bool string_value: indicates will be value wrapped in 338 | brackets. 339 | :rtype: bool 340 | :return: indicator whether was operation successful or not. 341 | """ 342 | value = '"%s"' % value if string_value else value 343 | cmd = ['tell application "System Events" to tell application process "%s"' % process_name, 344 | ' set value of attribute "%s" of %s to %s' % (attribute_name, obj_selector, value), 345 | 'end tell'] 346 | 347 | try: 348 | MacUtils.execute_applescript_command(cmd) 349 | result = True 350 | except TooSaltyUISoupException: 351 | result = False 352 | 353 | return result 354 | 355 | @classmethod 356 | def get_axunknown_windows(cls, process_name): 357 | """ 358 | Gets AXUnknown windows by given process name. 359 | 360 | :param str process_name: name of process. 361 | :rtype: list 362 | :return: list of AppleEventDescriptor elements. 363 | """ 364 | 365 | cmd = ['tell application "System Events" to tell process "%s"' % process_name, 366 | ' set visible to true', 367 | ' set unknownWindows to {}', 368 | ' repeat with e in UI elements', 369 | ' if value of attribute "AXRole" of e is equal to "AXUnknown" then', 370 | ' set unknownWindows to unknownWindows & {e}', 371 | ' end if', 372 | ' end repeat', 373 | ' return unknownWindows', 374 | 'end tell'] 375 | 376 | return list(MacUtils.execute_applescript_command(cmd)) 377 | 378 | @classmethod 379 | def get_axdialog_windows(cls, process_name): 380 | """ 381 | Gets AXDialog windows by given process name. 382 | 383 | :param str process_name: name of process. 384 | :rtype: list 385 | :return: list of AppleEventDescriptor elements. 386 | """ 387 | 388 | cmd = ['tell application "System Events" to tell process "%s"' % process_name, 389 | ' set visible to true', 390 | ' set dialogWindows to {}', 391 | ' repeat with e in UI elements', 392 | ' if value of attribute "AXRole" of e is equal to "AXWindow" and value of attribute "AXSubrole" of e is equal to "AXDialog" then', 393 | ' set dialogWindows to dialogWindows & {e}', 394 | ' end if', 395 | ' end repeat', 396 | ' return dialogWindows', 397 | 'end tell'] 398 | 399 | return list(MacUtils.execute_applescript_command(cmd)) 400 | -------------------------------------------------------------------------------- /uisoup/utils/win_utils.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014-2017 Max Beloborodko. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __author__ = 'f1ashhimself@gmail.com' 19 | 20 | 21 | from ..utils.common import CommonUtils 22 | 23 | 24 | class WinUtils(CommonUtils): 25 | pass 26 | -------------------------------------------------------------------------------- /uisoup/win_soup/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014-2017 Max Beloborodko. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __author__ = 'f1ashhimself@gmail.com' 19 | 20 | from .win_soup import WinSoup 21 | -------------------------------------------------------------------------------- /uisoup/win_soup/element.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | import ctypes 20 | import ctypes.wintypes 21 | import comtypes 22 | import comtypes.automation 23 | import comtypes.client 24 | 25 | from .mouse import WinMouse 26 | from ..interfaces.i_element import IElement 27 | from ..utils.win_utils import WinUtils 28 | from .. import TooSaltyUISoupException 29 | 30 | if WinUtils.is_python_3(): 31 | xrange = range 32 | 33 | CO_E_OBJNOTCONNECTED = -2147220995 34 | 35 | 36 | class WinElement(IElement): 37 | """ 38 | http://msdn.microsoft.com/en-us/library/dd318466(v=VS.85).aspx 39 | """ 40 | 41 | _acc_role_name_map = { 42 | 1: u'ttl', # TitleBar 43 | 2: u'mnu', # MenuBar 44 | 3: u'scbr', # ScrollBar 45 | 4: u'grip', # Grip 46 | 5: u'snd', # Sound 47 | 6: u'cur', # Cursor 48 | 7: u'caret', # Caret 49 | 8: u'alrt', # Alert 50 | 9: u'frm', # Window 51 | 10: u'clnt', # Client 52 | 11: u'mnu', # PopupMenu 53 | 12: u'mnu', # MenuItem 54 | 13: u'ttip', # Tooltip 55 | 14: u'app', # Application 56 | 15: u'doc', # Document 57 | 16: u'pane', # Pane 58 | 17: u'chrt', # Chart 59 | 18: u'dlg', # Dialog 60 | 19: u'border', # Border 61 | 20: u'grp', # Grouping 62 | 21: u'sep', # Separator 63 | 22: u'tbar', # ToolBar 64 | 23: u'sbar', # StatusBar 65 | 24: u'tbl', # Table 66 | 25: u'chdr', # ColumnHeader 67 | 26: u'rhdr', # RowHeader 68 | 27: u'col', # Column 69 | 28: u'tblc', # Row 70 | 29: u'tblc', # Cell 71 | 30: u'lnk', # Link 72 | 31: u'hbal', # HelpBalloon 73 | 32: u'chr', # Character 74 | 33: u'lst', # List 75 | 34: u'lst', # ListItem 76 | 35: u'otl', # Outline 77 | 36: u'otl', # OutlineItem 78 | 37: u'ptab', # PageTab 79 | 38: u'ppage', # PropertyPage 80 | 39: u'val', # Indicator 81 | 40: u'grph', # Graphic 82 | 41: u'lbl', # Text 83 | 42: u'txt', # EditableText 84 | 43: u'btn', # PushButton 85 | 44: u'chk', # CheckBox 86 | 45: u'rbtn', # RadioButton 87 | 46: u'cbox', # ComboBox 88 | 47: u'ddwn', # DropDown 89 | 48: u'pbar', # ProgressBar 90 | 49: u'dial', # Dial 91 | 50: u'hkfield', # HotKeyField 92 | 51: u'sldr', # Slider 93 | 52: u'sbox', # SpinBox 94 | 53: u'dgrm', # Diagram 95 | 54: u'anim', # Animation 96 | 55: u'eqtn', # Equation 97 | 56: u'btn', # DropDownButton 98 | 57: u'mnu', # MenuButton 99 | 58: u'gbtn', # GridDropDownButton 100 | 59: u'wspace', # WhiteSpace 101 | 60: u'ptablst', # PageTabList 102 | 61: u'clock', # Clock 103 | 62: u'sbtn', # SplitButton 104 | 63: u'ip', # IPAddress 105 | 64: u'obtn' # OutlineButton 106 | } 107 | 108 | _mouse = WinMouse() 109 | 110 | class _StateFlag(object): 111 | SYSTEM_NORMAL = 0 112 | SYSTEM_UNAVAILABLE = 0x1 113 | SYSTEM_SELECTED = 0x2 114 | SYSTEM_FOCUSED = 0x4 115 | SYSTEM_PRESSED = 0x8 116 | SYSTEM_CHECKED = 0x10 117 | SYSTEM_MIXED = 0x20 118 | SYSTEM_READONLY = 0x40 119 | SYSTEM_HOTTRACKED = 0x80 120 | SYSTEM_DEFAULT = 0x100 121 | SYSTEM_EXPANDED = 0x200 122 | SYSTEM_COLLAPSED = 0x400 123 | SYSTEM_BUSY = 0x800 124 | SYSTEM_FLOATING = 0x1000 125 | SYSTEM_MARQUEED = 0x2000 126 | SYSTEM_ANIMATED = 0x4000 127 | SYSTEM_INVISIBLE = 0x8000 128 | SYSTEM_OFFSCREEN = 0x10000 129 | SYSTEM_SIZEABLE = 0x20000 130 | SYSTEM_MOVEABLE = 0x40000 131 | SYSTEM_SELFVOICING = 0x80000 132 | SYSTEM_FOCUSABLE = 0x100000 133 | SYSTEM_SELECTABLE = 0x200000 134 | SYSTEM_LINKED = 0x400000 135 | SYSTEM_TRAVERSED = 0x800000 136 | SYSTEM_MULTISELECTABLE = 0x1000000 137 | SYSTEM_EXTSELECTABLE = 0x2000000 138 | SYSTEM_ALERT_LOW = 0x4000000 139 | SYSTEM_ALERT_MEDIUM = 0x8000000 140 | SYSTEM_ALERT_HIGH = 0x10000000 141 | SYSTEM_PROTECTED = 0x20000000 142 | SYSTEM_HASPOPUP = 0x40000000 143 | SYSTEM_VALID = 0x7fffffff 144 | 145 | class _SelectionFlag(object): 146 | NONE = 0 147 | TAKEFOCUS = 0x1 148 | TAKESELECTION = 0x2 149 | EXTENDSELECTION = 0x4 150 | ADDSELECTION = 0x8 151 | REMOVESELECTION = 0x10 152 | VALID = 0x20 153 | 154 | class _EnumWindowsCallback(object): 155 | 156 | same_proc_handles = set() 157 | 158 | @classmethod 159 | def callback(cls, handle, proc_id): 160 | 161 | curr_proc_id = ctypes.c_long() 162 | 163 | ctypes.windll.user32.GetWindowThreadProcessId( 164 | handle, ctypes.byref(curr_proc_id)) 165 | 166 | if curr_proc_id.value == proc_id: 167 | cls.same_proc_handles.add(handle) 168 | 169 | return True 170 | 171 | def __init__(self, obj_handle, i_object_id): 172 | """ 173 | Constructor. 174 | 175 | :param obj_handle: instance of i_accessible or window handle. 176 | :param int i_object_id: object id. 177 | """ 178 | if isinstance(obj_handle, comtypes.gen.Accessibility.IAccessible): 179 | i_accessible = obj_handle 180 | else: 181 | i_accessible = ctypes.POINTER( 182 | comtypes.gen.Accessibility.IAccessible)() 183 | ctypes.oledll.oleacc.AccessibleObjectFromWindow( 184 | obj_handle, 185 | 0, 186 | ctypes.byref(comtypes.gen.Accessibility.IAccessible._iid_), 187 | ctypes.byref(i_accessible)) 188 | 189 | self._i_accessible = i_accessible 190 | self._i_object_id = i_object_id 191 | self._cached_children = set() 192 | 193 | def _check_state(self, state): 194 | """ 195 | Checks state. 196 | 197 | :param int state: state flag. 198 | 199 | :rtype: bool 200 | :return: bool flag indicator. 201 | """ 202 | return bool(self._acc_state & state) 203 | 204 | def _find_windows_by_same_proc(self): 205 | """ 206 | Find window by same process id. 207 | 208 | :rtype: list 209 | :return: list of windows. 210 | """ 211 | enum_windows_proc = \ 212 | ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_long, 213 | ctypes.c_long) 214 | self._EnumWindowsCallback.same_proc_handles = set() 215 | ctypes.windll.user32.EnumWindows( 216 | enum_windows_proc( 217 | self._EnumWindowsCallback.callback), self.proc_id) 218 | 219 | if self._hwnd in self._EnumWindowsCallback.same_proc_handles: 220 | self._EnumWindowsCallback.same_proc_handles.remove(self._hwnd) 221 | 222 | result = [WinElement(hwnd, 0) for hwnd in 223 | self._EnumWindowsCallback.same_proc_handles] 224 | 225 | return result 226 | 227 | @property 228 | def _hwnd(self): 229 | """ 230 | Property for window handler. 231 | """ 232 | hwnd = ctypes.c_int() 233 | ctypes.oledll.oleacc.WindowFromAccessibleObject(self._i_accessible, 234 | ctypes.byref(hwnd)) 235 | 236 | return hwnd.value 237 | 238 | @property 239 | def _role(self): 240 | """ 241 | Property for element role. 242 | """ 243 | obj_child_id = comtypes.automation.VARIANT() 244 | obj_child_id.vt = comtypes.automation.VT_I4 245 | obj_child_id.value = self._i_object_id 246 | obj_role = comtypes.automation.VARIANT() 247 | obj_role.vt = comtypes.automation.VT_BSTR 248 | 249 | self._i_accessible._IAccessible__com__get_accRole(obj_child_id, 250 | obj_role) 251 | 252 | return obj_role.value 253 | 254 | def _select(self, i_selection): 255 | if self._i_object_id: 256 | return self._i_accessible.accSelect(i_selection, self._i_object_id) 257 | else: 258 | return self._i_accessible.accSelect(i_selection) 259 | 260 | def click(self, x_offset=0, y_offset=0): 261 | x, y, w, h = self.acc_location 262 | x += x_offset if x_offset is not None else w / 2 263 | y += y_offset if y_offset is not None else h / 2 264 | 265 | self._mouse.click(x, y) 266 | 267 | def right_click(self, x_offset=0, y_offset=0): 268 | x, y, w, h = self.acc_location 269 | x += x_offset if x_offset is not None else w / 2 270 | y += y_offset if y_offset is not None else h / 2 271 | 272 | self._mouse.click(x, y, self._mouse.RIGHT_BUTTON) 273 | 274 | def double_click(self, x_offset=0, y_offset=0): 275 | x, y, w, h = self.acc_location 276 | x += x_offset if x_offset is not None else w / 2 277 | y += y_offset if y_offset is not None else h / 2 278 | 279 | self._mouse.double_click(x, y) 280 | 281 | def drag_to(self, x, y, x_offset=None, y_offset=None, smooth=True): 282 | el_x, el_y, el_w, el_h = self.acc_location 283 | el_x += x_offset if x_offset is not None else el_w / 2 284 | el_y += y_offset if y_offset is not None else el_h / 2 285 | 286 | self._mouse.drag(el_x, el_y, x, y, smooth) 287 | 288 | @property 289 | def proc_id(self): 290 | hwnd = ctypes.c_long(self._hwnd) 291 | proc_id = ctypes.c_ulong() 292 | ctypes.windll.user32.GetWindowThreadProcessId(hwnd, 293 | ctypes.byref(proc_id)) 294 | 295 | return proc_id.value 296 | 297 | @property 298 | def is_top_level_window(self): 299 | # Top level window have 2 parents, clnt and frm for Desktop. 300 | return self.acc_parent_count == 2 301 | 302 | @property 303 | def is_selected(self): 304 | return self._check_state(self._StateFlag.SYSTEM_SELECTED) 305 | 306 | @property 307 | def is_checked(self): 308 | return self._check_state(self._StateFlag.SYSTEM_CHECKED) 309 | 310 | @property 311 | def is_visible(self): 312 | return not self._check_state(self._StateFlag.SYSTEM_INVISIBLE) 313 | 314 | @property 315 | def is_enabled(self): 316 | return not self._check_state(self._StateFlag.SYSTEM_UNAVAILABLE) 317 | 318 | @property 319 | def acc_parent_count(self): 320 | parent_count = 0 321 | parent = self.acc_parent 322 | while parent: 323 | parent_count += 1 324 | parent = parent.acc_parent 325 | 326 | return parent_count 327 | 328 | @property 329 | def acc_child_count(self): 330 | if self._i_object_id == 0: 331 | return self._get_child_count_safely(self._i_accessible) 332 | else: 333 | return 0 334 | 335 | @property 336 | def acc_name(self): 337 | obj_child_id = comtypes.automation.VARIANT() 338 | obj_child_id.vt = comtypes.automation.VT_I4 339 | obj_child_id.value = self._i_object_id 340 | 341 | obj_name = comtypes.automation.BSTR() 342 | 343 | self._i_accessible._IAccessible__com__get_accName( 344 | obj_child_id, ctypes.byref(obj_name)) 345 | result = obj_name.value or '' 346 | 347 | return WinUtils.replace_inappropriate_symbols(result) 348 | 349 | def set_focus(self): 350 | self._select(self._SelectionFlag.TAKEFOCUS) 351 | 352 | @property 353 | def acc_c_name(self): 354 | return self.acc_role_name + self.acc_name if self.acc_name else '' 355 | 356 | @property 357 | def acc_location(self): 358 | obj_child_id = comtypes.automation.VARIANT() 359 | obj_child_id.vt = comtypes.automation.VT_I4 360 | obj_child_id.value = self._i_object_id 361 | 362 | obj_l, obj_t, obj_w, obj_h = ctypes.c_long(), ctypes.c_long(), \ 363 | ctypes.c_long(), ctypes.c_long() 364 | 365 | self._i_accessible._IAccessible__com_accLocation(ctypes.byref(obj_l), 366 | ctypes.byref(obj_t), 367 | ctypes.byref(obj_w), 368 | ctypes.byref(obj_h), 369 | obj_child_id) 370 | 371 | return obj_l.value, obj_t.value, obj_w.value, obj_h.value 372 | 373 | @property 374 | def acc_value(self): 375 | obj_child_id = comtypes.automation.VARIANT() 376 | obj_child_id.vt = comtypes.automation.VT_I4 377 | obj_child_id.value = self._i_object_id 378 | obj_bstr_value = comtypes.automation.BSTR() 379 | self._i_accessible._IAccessible__com__get_accValue( 380 | obj_child_id, ctypes.byref(obj_bstr_value)) 381 | 382 | return obj_bstr_value.value 383 | 384 | def set_value(self, value): 385 | obj_child_id = comtypes.automation.VARIANT() 386 | obj_child_id.vt = comtypes.automation.VT_I4 387 | obj_child_id.value = self._i_object_id 388 | 389 | self._i_accessible._IAccessible__com__set_accValue(obj_child_id, value) 390 | 391 | @property 392 | def acc_description(self): 393 | obj_child_id = comtypes.automation.VARIANT() 394 | obj_child_id.vt = comtypes.automation.VT_I4 395 | obj_child_id.value = self._i_object_id 396 | obj_description = comtypes.automation.BSTR() 397 | self._i_accessible._IAccessible__com__get_accDescription( 398 | obj_child_id, ctypes.byref(obj_description)) 399 | 400 | return obj_description.value 401 | 402 | @property 403 | def acc_parent(self): 404 | result = None 405 | if self._i_accessible.accParent: 406 | result = \ 407 | WinElement(self._i_accessible.accParent, self._i_object_id) 408 | 409 | return result 410 | 411 | @property 412 | def acc_selection(self): 413 | obj_children = comtypes.automation.VARIANT() 414 | self._i_accessible._IAccessible__com__get_accSelection( 415 | ctypes.byref(obj_children)) 416 | 417 | return obj_children.value 418 | 419 | @property 420 | def _acc_state(self): 421 | obj_child_id = comtypes.automation.VARIANT() 422 | obj_child_id.vt = comtypes.automation.VT_I4 423 | obj_child_id.value = self._i_object_id 424 | obj_state = comtypes.automation.VARIANT() 425 | self._i_accessible._IAccessible__com__get_accState( 426 | obj_child_id, ctypes.byref(obj_state)) 427 | 428 | return obj_state.value 429 | 430 | @property 431 | def acc_focused_element(self): 432 | result = None 433 | if self._i_accessible.accFocus: 434 | result = WinElement(self._i_accessible.accFocus, self._i_object_id) 435 | 436 | return result 437 | 438 | @property 439 | def acc_role_name(self): 440 | return self._acc_role_name_map.get(self._role, 'unknown') 441 | 442 | def __iter__(self): 443 | if self._i_object_id > 0: 444 | raise StopIteration() 445 | 446 | obj_acc_child_array = (comtypes.automation.VARIANT * 447 | self._i_accessible.accChildCount)() 448 | obj_acc_child_count = ctypes.c_long() 449 | 450 | ctypes.oledll.oleacc.AccessibleChildren( 451 | self._i_accessible, 452 | 0, 453 | self._i_accessible.accChildCount, 454 | obj_acc_child_array, 455 | ctypes.byref(obj_acc_child_count)) 456 | 457 | for i in xrange(obj_acc_child_count.value): 458 | obj_acc_child = obj_acc_child_array[i] 459 | if obj_acc_child.vt == comtypes.automation.VT_DISPATCH: 460 | yield WinElement(obj_acc_child.value.QueryInterface( 461 | comtypes.gen.Accessibility.IAccessible), 0) 462 | else: 463 | yield WinElement(self._i_accessible, obj_acc_child.value) 464 | 465 | def __findcacheiter(self, only_visible, **kwargs): 466 | """ 467 | Find child element in the cache. 468 | 469 | :param bool only_visible: flag that indicates will we search only 470 | :rtype: WinElement 471 | :return: yield found element. 472 | """ 473 | for obj_element in self._cached_children: 474 | if obj_element._match(only_visible, **kwargs): 475 | yield obj_element 476 | 477 | def _finditer(self, only_visible, **kwargs): 478 | """ 479 | Find child element. 480 | 481 | :param bool only_visible: flag that indicates will we search only 482 | :rtype: WinElement 483 | :return: yield found element. 484 | """ 485 | lst_queue = list(self) 486 | 487 | if self.is_top_level_window: 488 | lst_queue.extend(self._find_windows_by_same_proc()) 489 | 490 | while lst_queue: 491 | obj_element = lst_queue.pop(0) 492 | self._cached_children.add(obj_element) 493 | 494 | if obj_element._match(only_visible, **kwargs): 495 | yield obj_element 496 | 497 | if obj_element.acc_child_count: 498 | childs = [el for el in list(obj_element) if 499 | el._i_accessible != obj_element._i_accessible] 500 | lst_queue[:0] = childs 501 | 502 | def find(self, only_visible=True, **kwargs): 503 | try: 504 | iter_ = next(self.__findcacheiter(only_visible, **kwargs)) if \ 505 | WinUtils.is_python_3() else self.__findcacheiter( 506 | only_visible, **kwargs).next() 507 | return iter_ 508 | except StopIteration: 509 | try: 510 | iter_ = next(self._finditer(only_visible, **kwargs)) if \ 511 | WinUtils.is_python_3() else self.__findcacheiter( 512 | only_visible, **kwargs).next() 513 | return iter_ 514 | except StopIteration: 515 | items_ = kwargs.items() if WinUtils.is_python_3() else \ 516 | kwargs.iteritems() 517 | attrs = ['%s=%s' % (k, v) for k, v in items_] 518 | raise TooSaltyUISoupException( 519 | 'Can\'t find object with attributes "%s".' % 520 | '; '.join(attrs)) 521 | 522 | def findall(self, only_visible=True, **kwargs): 523 | result = self._finditer(only_visible, **kwargs) 524 | if result: 525 | result = list(result) 526 | 527 | return result 528 | 529 | def is_object_exists(self, **kwargs): 530 | try: 531 | self.find(**kwargs) 532 | return True 533 | except TooSaltyUISoupException: 534 | return False 535 | 536 | def _get_child_count_safely(self, i_accessible): 537 | """ 538 | Safely gets child count. 539 | 540 | :param i_accessible: instance of i_accessible. 541 | :rtype: int 542 | :return: object child count 543 | """ 544 | try: 545 | return i_accessible.accChildCount 546 | except Exception as ex: 547 | if isinstance(ex, comtypes.COMError) and getattr(ex, 'hresult') \ 548 | in (CO_E_OBJNOTCONNECTED,): 549 | return 0 550 | -------------------------------------------------------------------------------- /uisoup/win_soup/keyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | import ctypes 20 | from time import sleep 21 | 22 | from ..interfaces.i_keyboard import Key, IKeyboard 23 | 24 | send_input = ctypes.windll.user32.SendInput 25 | pointer_unsigned_long = ctypes.POINTER(ctypes.c_ulong) 26 | 27 | 28 | class KeyboardInput(ctypes.Structure): 29 | """ 30 | Keyboard input C struct definition. 31 | """ 32 | 33 | _fields_ = [("wVk", ctypes.c_ushort), 34 | ("wScan", ctypes.c_ushort), 35 | ("dwFlags", ctypes.c_ulong), 36 | ("time", ctypes.c_ulong), 37 | ("dwExtraInfo", pointer_unsigned_long)] 38 | 39 | 40 | class HardwareInput(ctypes.Structure): 41 | """ 42 | Hardware input C struct definition. 43 | """ 44 | 45 | _fields_ = [("uMsg", ctypes.c_ulong), 46 | ("wParamL", ctypes.c_short), 47 | ("wParamH", ctypes.c_ushort)] 48 | 49 | 50 | class MouseInput(ctypes.Structure): 51 | """ 52 | Hardware input C struct definition. 53 | """ 54 | 55 | _fields_ = [("dx", ctypes.c_long), 56 | ("dy", ctypes.c_long), 57 | ("mouseData", ctypes.c_ulong), 58 | ("dwFlags", ctypes.c_ulong), 59 | ("time", ctypes.c_ulong), 60 | ("dwExtraInfo", pointer_unsigned_long)] 61 | 62 | 63 | class EventStorage(ctypes.Union): 64 | """ 65 | Event storage C struct definition. 66 | """ 67 | 68 | _fields_ = [("ki", KeyboardInput), 69 | ("mi", MouseInput), 70 | ("hi", HardwareInput)] 71 | 72 | 73 | class Input(ctypes.Structure): 74 | """ 75 | Input C struct definition. 76 | """ 77 | 78 | _fields_ = [("type", ctypes.c_ulong), 79 | ("ii", EventStorage)] 80 | 81 | 82 | class WinKeyboard(IKeyboard): 83 | 84 | class _KeyCodes(object): 85 | """ 86 | Holder for Windows keyboard codes stored as Keys. 87 | """ 88 | 89 | BACKSPACE = Key(0x08) # BACKSPACE key 90 | TAB = Key(0x09) # TAB key 91 | CLEAR = Key(0x0C) # CLEAR key 92 | RETURN = Key(0x0D) # ENTER key 93 | SHIFT = Key(0x10) # SHIFT key 94 | CONTROL = Key(0x11) # CTRL key 95 | ALT = Key(0x12) # ALT key 96 | PAUSE = Key(0x13) # PAUSE key 97 | CAPS_LOCK = Key(0x14) # CAPS LOCK key 98 | ESCAPE = Key(0x1B) # ESC key 99 | SPACE = Key(0x20) # SPACEBAR 100 | PAGE_UP = Key(0x21) # PAGE UP key 101 | PAGE_DOWN = Key(0x22) # PAGE DOWN key 102 | END = Key(0x23) # END key 103 | HOME = Key(0x24) # HOME key 104 | LEFT = Key(0x25) # LEFT ARROW key 105 | UP = Key(0x26) # UP ARROW key 106 | RIGHT = Key(0x27) # RIGHT ARROW key 107 | DOWN = Key(0x28) # DOWN ARROW key 108 | PRINT_SCREEN = Key(0x2C) # PRINT SCREEN key 109 | INSERT = Key(0x2D) # INS key 110 | DELETE = Key(0x2E) # DEL key 111 | VK_HELP = Key(0x2F) # HELP key 112 | KEY_0 = Key(0x30) # 0 key 113 | KEY_1 = Key(0x31) # 1 key 114 | KEY_2 = Key(0x32) # 2 key 115 | KEY_3 = Key(0x33) # 3 key 116 | KEY_4 = Key(0x34) # 4 key 117 | KEY_5 = Key(0x35) # 5 key 118 | KEY_6 = Key(0x36) # 6 key 119 | KEY_7 = Key(0x37) # 7 key 120 | KEY_8 = Key(0x38) # 8 key 121 | KEY_9 = Key(0x39) # 9 key 122 | KEY_A = Key(0x41) # A key 123 | KEY_B = Key(0x42) # B key 124 | KEY_C = Key(0x43) # C key 125 | KEY_D = Key(0x44) # D key 126 | KEY_E = Key(0x45) # E key 127 | KEY_F = Key(0x46) # F key 128 | KEY_G = Key(0x47) # G key 129 | KEY_H = Key(0x48) # H key 130 | KEY_I = Key(0x49) # I key 131 | KEY_J = Key(0x4A) # J key 132 | KEY_K = Key(0x4B) # K key 133 | KEY_L = Key(0x4C) # L key 134 | KEY_M = Key(0x4D) # M key 135 | KEY_N = Key(0x4E) # N key 136 | KEY_O = Key(0x4F) # O key 137 | KEY_P = Key(0x50) # P key 138 | KEY_Q = Key(0x51) # Q key 139 | KEY_R = Key(0x52) # R key 140 | KEY_S = Key(0x53) # S key 141 | KEY_T = Key(0x54) # T key 142 | KEY_U = Key(0x55) # U key 143 | KEY_V = Key(0x56) # V key 144 | KEY_W = Key(0x57) # W key 145 | KEY_X = Key(0x58) # X key 146 | KEY_Y = Key(0x59) # Y key 147 | KEY_Z = Key(0x5A) # Z key 148 | LEFT_WIN = Key(0x5B) # Left Windows key (Natural keyboard) 149 | RIGHT_WIN = Key(0x5C) # Right Windows key (Natural keyboard) 150 | SLEEP = Key(0x5F) # Computer Sleep key 151 | NUMPAD0 = Key(0x60) # Numeric keypad 0 key 152 | NUMPAD1 = Key(0x61) # Numeric keypad 1 key 153 | NUMPAD2 = Key(0x62) # Numeric keypad 2 key 154 | NUMPAD3 = Key(0x63) # Numeric keypad 3 key 155 | NUMPAD4 = Key(0x64) # Numeric keypad 4 key 156 | NUMPAD5 = Key(0x65) # Numeric keypad 5 key 157 | NUMPAD6 = Key(0x66) # Numeric keypad 6 key 158 | NUMPAD7 = Key(0x67) # Numeric keypad 7 key 159 | NUMPAD8 = Key(0x68) # Numeric keypad 8 key 160 | NUMPAD9 = Key(0x69) # Numeric keypad 9 key 161 | MULTIPLY = Key(0x6A) # Multiply key 162 | ADD = Key(0x6B) # Add key 163 | SEPARATOR = Key(0x6C) # Separator key 164 | SUBTRACT = Key(0x6D) # Subtract key 165 | DECIMAL = Key(0x6E) # Decimal key 166 | DIVIDE = Key(0x6F) # Divide key 167 | F1 = Key(0x70) # F1 key 168 | F2 = Key(0x71) # F2 key 169 | F3 = Key(0x72) # F3 key 170 | F4 = Key(0x73) # F4 key 171 | F5 = Key(0x74) # F5 key 172 | F6 = Key(0x75) # F6 key 173 | F7 = Key(0x76) # F7 key 174 | F8 = Key(0x77) # F8 key 175 | F9 = Key(0x78) # F9 key 176 | F10 = Key(0x79) # F10 key 177 | F11 = Key(0x7A) # F11 key 178 | F12 = Key(0x7B) # F12 key 179 | NUM_LOCK = Key(0x90) # NUM LOCK key 180 | SCROLL_LOCK = Key(0x91) # SCROLL LOCK 181 | LEFT_SHIFT = Key(0xA0) # Left SHIFT key 182 | RIGHT_SHIFT = Key(0xA1) # Right SHIFT key 183 | LEFT_CONTROL = Key(0xA2) # Left CONTROL key 184 | RIGHT_CONTROL = Key(0xA3) # Right CONTROL key 185 | OEM_1 = Key(0xBA) # For the US standard keyboard, the ';:' key 186 | OEM_PLUS = Key(0xBB) # For any country/region, the '+' key 187 | OEM_COMMA = Key(0xBC) # For any country/region, the ',' key 188 | OEM_MINUS = Key(0xBD) # For any country/region, the '-' key 189 | OEM_PERIOD = Key(0xBE) # For any country/region, the '.' key 190 | OEM_2 = Key(0xBF) # For the US standard keyboard, the '/?' key 191 | OEM_3 = Key(0xC0) # For the US standard keyboard, the '`~' key 192 | OEM_4 = Key(0xDB) # For the US standard keyboard, the '[{' key 193 | OEM_5 = Key(0xDC) # For the US standard keyboard, the '\|' key 194 | OEM_6 = Key(0xDD) # For the US standard keyboard, the ']}' key 195 | OEM_7 = Key(0xDE) # For the US standard keyboard, the ''/"' key 196 | 197 | codes = _KeyCodes 198 | 199 | def press_key(self, hex_key_code): 200 | """ 201 | Presses (and releases) key specified by a hex code. 202 | 203 | :param int hex_key_code: hexadecimal code for a key to be pressed. 204 | """ 205 | self.press_key_and_hold(hex_key_code) 206 | self.release_key(hex_key_code) 207 | 208 | def press_key_and_hold(self, hex_key_code): 209 | """ 210 | Presses (and holds) key specified by a hex code. 211 | 212 | :param int hex_key_code: hexadecimal code for a key to be pressed. 213 | """ 214 | extra = ctypes.c_ulong(0) 215 | ii_ = EventStorage() 216 | ii_.ki = KeyboardInput(hex_key_code, 0x48, 0, 0, ctypes.pointer(extra)) 217 | x = Input(ctypes.c_ulong(1), ii_) 218 | send_input(1, ctypes.pointer(x), ctypes.sizeof(x)) 219 | 220 | def release_key(self, hex_key_code): 221 | """ 222 | Releases key specified by a hex code. 223 | 224 | :param int hex_key_code: hexadecimal code for a key to be pressed. 225 | """ 226 | extra = ctypes.c_ulong(0) 227 | ii_ = EventStorage() 228 | ii_.ki = KeyboardInput( 229 | hex_key_code, 0x48, 0x0002, 0, ctypes.pointer(extra)) 230 | x = Input(ctypes.c_ulong(1), ii_) 231 | send_input(1, ctypes.pointer(x), ctypes.sizeof(x)) 232 | 233 | def send(self, *args, **kwargs): 234 | """ 235 | Send key events as specified by Keys. 236 | 237 | If Key contains children Keys they will be recursively 238 | processed with current Key code pressed as a modifier key. 239 | 240 | :param args: keys to send. 241 | """ 242 | delay = kwargs.get('delay', 0) 243 | 244 | for key in args: 245 | if key.children: 246 | self.press_key_and_hold(key.code) 247 | self.send(*key.children) 248 | self.release_key(key.code) 249 | else: 250 | self.press_key(key.code) 251 | self._wait_for_key_combo_to_be_processed() 252 | sleep(delay) 253 | 254 | def _wait_for_key_combo_to_be_processed(self): 255 | # For key combinations timeout is needed to be processed. 256 | # This method is expressive shortcut to be used where needed. 257 | sleep(.05) 258 | -------------------------------------------------------------------------------- /uisoup/win_soup/mouse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2017 Max Beloborodko. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __author__ = 'f1ashhimself@gmail.com' 18 | 19 | import ctypes 20 | import ctypes.wintypes 21 | from time import sleep 22 | 23 | from ..interfaces.i_mouse import IMouse 24 | from ..utils.win_utils import WinUtils 25 | 26 | if WinUtils.is_python_3(): 27 | xrange = range 28 | 29 | 30 | class WinMouse(IMouse): 31 | _MOUSEEVENTF_MOVE = 0x0001 # mouse move 32 | _MOUSEEVENTF_LEFTDOWN = 0x0002 # left button down 33 | _MOUSEEVENTF_LEFTUP = 0x0004 # left button up 34 | _MOUSEEVENTF_RIGHTDOWN = 0x0008 # right button down 35 | _MOUSEEVENTF_RIGHTUP = 0x0010 # right button up 36 | _MOUSEEVENTF_MIDDLEDOWN = 0x0020 # middle button down 37 | _MOUSEEVENTF_MIDDLEUP = 0x0040 # middle button up 38 | _MOUSEEVENTF_ABSOLUTE = 0x8000 # absolute move 39 | _MOUSEEVENTF_XDOWN = 0x0080 # X button down 40 | _MOUSEEVENTF_XUP = 0x0100 # X button up 41 | _MOUSEEVENTF_WHEEL = 0x0800 # wheel button is rotated 42 | _MOUSEEVENTF_HWHEEL = 0x01000 # wheel button is tilted 43 | 44 | _SM_CXSCREEN = 0 45 | _SM_CYSCREEN = 1 46 | 47 | LEFT_BUTTON = u'b1c' 48 | RIGHT_BUTTON = u'b3c' 49 | _SUPPORTED_BUTTON_NAMES = [LEFT_BUTTON, RIGHT_BUTTON] 50 | 51 | def _compose_mouse_event(self, name, press=True, release=False): 52 | """ 53 | Composes mouse event based on button name and action flags. 54 | 55 | :param str name: mouse button name. Should be one 56 | of: 'b1c' - left button or 'b3c' - right button. 57 | :param bool press: flag indicating whether event should indicate 58 | button press. 59 | :param bool release: flag indicating whether event should indicate 60 | button release. 61 | """ 62 | mouse_event = 0 63 | if name == self.LEFT_BUTTON: 64 | if press: 65 | mouse_event += self._MOUSEEVENTF_LEFTDOWN 66 | if release: 67 | mouse_event += self._MOUSEEVENTF_LEFTUP 68 | if name == self.RIGHT_BUTTON: 69 | if press: 70 | mouse_event += self._MOUSEEVENTF_RIGHTDOWN 71 | if release: 72 | mouse_event += self._MOUSEEVENTF_RIGHTUP 73 | 74 | return mouse_event 75 | 76 | def _do_event(self, flags, x, y, data, extra_info): 77 | """ 78 | Generates mouse event fo a special coordinate. 79 | 80 | :param int flags: integer value holding flags that describes mouse 81 | events to trigger. Can be a combination of: 82 | _MOUSEEVENTF_MOVE = 0x0001 # mouse move 83 | _MOUSEEVENTF_LEFTDOWN = 0x0002 # left button down 84 | _MOUSEEVENTF_LEFTUP = 0x0004 # left button up 85 | _MOUSEEVENTF_RIGHTDOWN = 0x0008 # right button down 86 | _MOUSEEVENTF_RIGHTUP = 0x0010 # right button up 87 | _MOUSEEVENTF_MIDDLEDOWN = 0x0020 # middle button down 88 | _MOUSEEVENTF_MIDDLEUP = 0x0040 # middle button up 89 | _MOUSEEVENTF_ABSOLUTE = 0x8000 # absolute move 90 | _MOUSEEVENTF_XDOWN = 0x0080 # X button down 91 | _MOUSEEVENTF_XUP = 0x0100 # X button up 92 | _MOUSEEVENTF_WHEEL = 0x0800 # wheel button is rotated 93 | _MOUSEEVENTF_HWHEEL = 0x01000 # wheel button is tilted 94 | :param int x: x coordinate. 95 | :param int y: y coordinate. 96 | :param int data: value holding additional event data, for ex.: 97 | * If flags contains _MOUSEEVENTF_WHEEL, then data specifies the 98 | amount of wheel movement. A positive value indicates that the 99 | wheel was rotated forward, away from the user; a negative 100 | value indicates that the wheel was rotated backward, toward 101 | the user. One wheel click is defined as WHEEL_DELTA, which is 102 | 120. 103 | * If flags contains _MOUSEEVENTF_HWHEEL, then data specifies the 104 | amount of wheel movement. A positive value indicates that the 105 | wheel was tilted to the right; a negative value indicates that 106 | the wheel was tilted to the left. 107 | * If flags contains _MOUSEEVENTF_XDOWN or _MOUSEEVENTF_XUP, then 108 | data specifies which X buttons were pressed or released. This 109 | value may be any combination of the following flags. 110 | * If flags is not _MOUSEEVENTF_WHEEL, _MOUSEEVENTF_XDOWN, or 111 | _MOUSEEVENTF_XUP, then data should be zero. 112 | :param int extra_info: value with additional value associated with 113 | the mouse event. 114 | """ 115 | x_metric = ctypes.windll.user32.GetSystemMetrics(self._SM_CXSCREEN) 116 | y_metric = ctypes.windll.user32.GetSystemMetrics(self._SM_CYSCREEN) 117 | x_calc = 65536 * x / x_metric + 1 118 | y_calc = 65536 * y / y_metric + 1 119 | ctypes.windll.user32.mouse_event( 120 | flags, int(x_calc), int(y_calc), data, extra_info) 121 | 122 | def move(self, x, y, smooth=True): 123 | WinUtils.verify_xy_coordinates(x, y) 124 | 125 | old_x, old_y = self.get_position() 126 | 127 | for i in xrange(100): 128 | intermediate_x = old_x + (x - old_x) * (i + 1) / 100.0 129 | intermediate_y = old_y + (y - old_y) * (i + 1) / 100.0 130 | smooth and sleep(.01) 131 | 132 | self._do_event(self._MOUSEEVENTF_MOVE + self._MOUSEEVENTF_ABSOLUTE, 133 | int(intermediate_x), int(intermediate_y), 0, 0) 134 | 135 | def drag(self, x1, y1, x2, y2, smooth=True): 136 | WinUtils.verify_xy_coordinates(x1, y1) 137 | WinUtils.verify_xy_coordinates(x2, y2) 138 | 139 | self.press_button(x1, y1, self.LEFT_BUTTON) 140 | self.move(x2, y2, smooth=smooth) 141 | self.release_button(self.LEFT_BUTTON) 142 | 143 | def press_button(self, x, y, button_name=LEFT_BUTTON): 144 | WinUtils.verify_xy_coordinates(x, y) 145 | WinUtils.verify_mouse_button_name(button_name, 146 | self._SUPPORTED_BUTTON_NAMES) 147 | 148 | self.move(x, y) 149 | self._do_event( 150 | self._compose_mouse_event(button_name, press=True, release=False), 151 | 0, 0, 0, 0) 152 | 153 | def release_button(self, button_name=LEFT_BUTTON): 154 | WinUtils.verify_mouse_button_name(button_name, 155 | self._SUPPORTED_BUTTON_NAMES) 156 | 157 | self._do_event( 158 | self._compose_mouse_event(button_name, press=False, release=True), 159 | 0, 0, 0, 0) 160 | 161 | def click(self, x, y, button_name=LEFT_BUTTON): 162 | WinUtils.verify_xy_coordinates(x, y) 163 | WinUtils.verify_mouse_button_name(button_name, 164 | self._SUPPORTED_BUTTON_NAMES) 165 | 166 | self.move(x, y) 167 | self._do_event( 168 | self._compose_mouse_event(button_name, press=True, release=True), 169 | 0, 0, 0, 0) 170 | 171 | def double_click(self, x, y, button_name=LEFT_BUTTON): 172 | WinUtils.verify_xy_coordinates(x, y) 173 | WinUtils.verify_mouse_button_name(button_name, 174 | self._SUPPORTED_BUTTON_NAMES) 175 | 176 | self.move(x, y) 177 | self._do_event( 178 | self._compose_mouse_event(button_name, press=True, release=True), 179 | 0, 0, 0, 0) 180 | self._do_event( 181 | self._compose_mouse_event(button_name, press=True, release=True), 182 | 0, 0, 0, 0) 183 | 184 | def get_position(self): 185 | obj_point = ctypes.wintypes.POINT() 186 | ctypes.windll.user32.GetCursorPos(ctypes.byref(obj_point)) 187 | 188 | return obj_point.x, obj_point.y 189 | -------------------------------------------------------------------------------- /uisoup/win_soup/win_soup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014-2017 Max Beloborodko. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __author__ = 'f1ashhimself@gmail.com' 19 | 20 | import re 21 | import ctypes 22 | import ctypes.wintypes 23 | import comtypes 24 | import comtypes.automation 25 | import comtypes.client 26 | import sys 27 | 28 | from ..utils.win_utils import WinUtils 29 | from .. import TooSaltyUISoupException 30 | from ..interfaces.i_soup import ISoup 31 | from .element import WinElement 32 | from .mouse import WinMouse 33 | from .keyboard import WinKeyboard 34 | 35 | if WinUtils.is_python_3(): 36 | basestring = unicode = str 37 | 38 | comtypes.client.GetModule('oleacc.dll') 39 | 40 | 41 | class WinSoup(ISoup): 42 | 43 | mouse = WinMouse() 44 | keyboard = WinKeyboard() 45 | _default_sys_encoding = sys.stdout.encoding or sys.getdefaultencoding() 46 | 47 | class _EnumWindowsCallback(object): 48 | 49 | last_handle = None 50 | 51 | @classmethod 52 | def callback(cls, handle, wildcard): 53 | wildcard = ctypes.cast(wildcard, ctypes.c_wchar_p).value 54 | 55 | wildcard = WinUtils.replace_inappropriate_symbols(wildcard) 56 | length = ctypes.windll.user32.GetWindowTextLengthW(handle) + 1 57 | buff = ctypes.create_unicode_buffer(length) 58 | ctypes.windll.user32.GetWindowTextW(handle, buff, length) 59 | win_text = WinUtils.replace_inappropriate_symbols(buff.value) 60 | 61 | if re.match(wildcard, win_text): 62 | cls.last_handle = handle 63 | 64 | return True 65 | 66 | def get_object_by_coordinates(self, x, y): 67 | obj_point = ctypes.wintypes.POINT() 68 | obj_point.x = x 69 | obj_point.y = y 70 | i_accessible = ctypes.POINTER(comtypes.gen.Accessibility.IAccessible)() 71 | obj_child_id = comtypes.automation.VARIANT() 72 | ctypes.oledll.oleacc.AccessibleObjectFromPoint( 73 | obj_point, 74 | ctypes.byref(i_accessible), 75 | ctypes.byref(obj_child_id)) 76 | 77 | return WinElement(i_accessible, obj_child_id.value or 0) 78 | 79 | def is_window_exists(self, obj_handle): 80 | try: 81 | self.get_window(obj_handle) 82 | return True 83 | except TooSaltyUISoupException: 84 | return False 85 | 86 | def get_window(self, obj_handle=None): 87 | if obj_handle in (0, None): 88 | obj_handle = ctypes.windll.user32.GetDesktopWindow() 89 | elif isinstance(obj_handle, basestring): 90 | obj_name = unicode(obj_handle) 91 | 92 | regex = WinUtils.convert_wildcard_to_regex(obj_name) 93 | 94 | enum_windows_proc = \ 95 | ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), 96 | ctypes.POINTER(ctypes.c_int)) 97 | self._EnumWindowsCallback.last_handle = None 98 | ctypes.windll.user32.EnumWindows(enum_windows_proc( 99 | self._EnumWindowsCallback.callback), 100 | ctypes.c_wchar_p(regex)) 101 | 102 | obj_handle = self._EnumWindowsCallback.last_handle 103 | 104 | if not obj_handle: 105 | obj_name = obj_name.encode(self._default_sys_encoding, 106 | errors='ignore') 107 | raise TooSaltyUISoupException('Can\'t find window "%s".' % 108 | obj_name) 109 | 110 | try: 111 | return WinElement(obj_handle, 0) 112 | except: 113 | raise TooSaltyUISoupException( 114 | 'Error when retrieving window with handle=%r' % obj_handle) 115 | 116 | def get_visible_window_list(self): 117 | result = self.get_window().findall( 118 | only_visible=True, 119 | name=lambda x: x, 120 | role_name=lambda x: x in ['frm', 'pane'], 121 | location=lambda x: 0 not in x[2:]) 122 | 123 | return result 124 | 125 | def get_visible_object_list(self, window_name): 126 | window = self.get_window(window_name) 127 | objects = window.findall( 128 | only_visible=True, 129 | role_name=lambda x: x != 'frm', 130 | location=lambda x: 0 not in x[2:]) 131 | 132 | return objects 133 | --------------------------------------------------------------------------------