You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ORPA-pyOpenRPA/3rdParty/pywinauto/controls/uia_controls.py

1196 lines
44 KiB

6 years ago
# GUI Application automation and testing library
# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors
# https://github.com/pywinauto/pywinauto/graphs/contributors
# http://pywinauto.readthedocs.io/en/latest/credits.html
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of pywinauto nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Wrap various UIA windows controls"""
import locale
import comtypes
import six
from .. import uia_element_info
from .. import findbestmatch
from .. import timings
from . import uiawrapper
from ..uia_defines import IUIA
from ..uia_defines import NoPatternInterfaceError
from ..uia_defines import toggle_state_on
from ..uia_defines import get_elem_interface
# ====================================================================
class ButtonWrapper(uiawrapper.UIAWrapper):
"""Wrap a UIA-compatible Button, CheckBox or RadioButton control"""
_control_types = ['Button',
'CheckBox',
'RadioButton',
]
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(ButtonWrapper, self).__init__(elem)
# -----------------------------------------------------------
def toggle(self):
"""
An interface to Toggle method of the Toggle control pattern.
Control supporting the Toggle pattern cycles through its
toggle states in the following order:
ToggleState_On, ToggleState_Off and,
if supported, ToggleState_Indeterminate
Usually applied for the check box control.
The radio button control does not implement IToggleProvider,
because it is not capable of cycling through its valid states.
Toggle a state of a check box control. (Use 'select' method instead)
Notice, a radio button control isn't supported by UIA.
https://msdn.microsoft.com/en-us/library/windows/desktop/ee671290(v=vs.85).aspx
"""
name = self.element_info.name
control_type = self.element_info.control_type
self.iface_toggle.Toggle()
if name and control_type:
self.actions.log('Toggled ' + control_type.lower() + ' "' + name + '"')
# Return itself so that action can be chained
return self
# -----------------------------------------------------------
def get_toggle_state(self):
"""
Get a toggle state of a check box control.
The toggle state is represented by an integer
0 - unchecked
1 - checked
2 - indeterminate
The following constants are defined in the uia_defines module
toggle_state_off = 0
toggle_state_on = 1
toggle_state_inderteminate = 2
"""
return self.iface_toggle.CurrentToggleState
# -----------------------------------------------------------
def is_dialog(self):
"""Buttons are never dialogs so return False"""
return False
# -----------------------------------------------------------
def click(self):
"""Click the Button control by using Invoke pattern"""
self.invoke()
# Return itself so that action can be chained
return self
# ====================================================================
class ComboBoxWrapper(uiawrapper.UIAWrapper):
"""Wrap a UIA CoboBox control"""
_control_types = ['ComboBox']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(ComboBoxWrapper, self).__init__(elem)
# -----------------------------------------------------------
def texts(self):
"""Return the text of the items in the combobox"""
texts = []
# ComboBox has to be expanded to populate a list of its children items
try:
self.expand()
for c in self.children():
texts.append(c.window_text())
except NoPatternInterfaceError:
return texts
else:
# Make sure we collapse back
self.collapse()
return texts
def select(self, item):
"""
Select the ComboBox item
The item can be either a 0 based index of the item to select
or it can be the string that you want to select
"""
# ComboBox has to be expanded to populate a list of its children items
self.expand()
try:
self._select(item)
# TODO: do we need to handle ValueError/IndexError for a wrong index ?
#except ValueError:
# raise # re-raise the last exception
finally:
# Make sure we collapse back in any case
self.collapse()
return self
# -----------------------------------------------------------
# TODO: add selected_texts for a combobox with a multi-select support
def selected_text(self):
"""
Return the selected text or None
Notice, that in case of multi-select it will be only the text from
a first selected item
"""
selection = self.get_selection()
if selection:
return selection[0].name
else:
return None
# -----------------------------------------------------------
# TODO: add selected_indices for a combobox with multi-select support
def selected_index(self):
"""Return the selected index"""
return self.selected_item_index()
# -----------------------------------------------------------
def item_count(self):
"""
Return the number of items in the combobox
The interface is kept mostly for a backward compatibility with
the native ComboBox interface
"""
return self.control_count()
# ====================================================================
class EditWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible Edit control"""
# TODO: this class supports only 1-line textboxes so there is no point
# TODO: in methods such as line_count(), line_length(), get_line(), etc
_control_types = ['Edit']
has_title = False
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(EditWrapper, self).__init__(elem)
# -----------------------------------------------------------
@property
def writable_props(self):
"""Extend default properties list."""
props = super(EditWrapper, self).writable_props
props.extend(['selection_indices'])
return props
# -----------------------------------------------------------
def line_count(self):
"""Return how many lines there are in the Edit"""
return self.window_text().count("\n") + 1
# -----------------------------------------------------------
def line_length(self, line_index):
"""Return how many characters there are in the line"""
# need to first get a character index of that line
lines = self.window_text().splitlines()
if line_index < len(lines):
return len(lines[line_index])
elif line_index == self.line_count() - 1:
return 0
else:
raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index))
# -----------------------------------------------------------
def get_line(self, line_index):
"""Return the line specified"""
lines = self.window_text().splitlines()
if line_index < len(lines):
return lines[line_index]
elif line_index == self.line_count() - 1:
return ""
else:
raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index))
# -----------------------------------------------------------
def get_value(self):
"""Return the current value of the element"""
return self.iface_value.CurrentValue
# -----------------------------------------------------------
def texts(self):
"""Get the text of the edit control"""
texts = [self.window_text(), ]
for i in range(self.line_count()):
texts.append(self.get_line(i))
return texts
# -----------------------------------------------------------
def text_block(self):
"""Get the text of the edit control"""
return self.window_text()
# -----------------------------------------------------------
def selection_indices(self):
"""The start and end indices of the current selection"""
selected_text = self.iface_text.GetSelection().GetElement(0).GetText(-1)
start = self.window_text().find(selected_text)
end = start + len(selected_text)
return (start, end)
# -----------------------------------------------------------
def set_window_text(self, text, append=False):
"""Override set_window_text for edit controls because it should not be
used for Edit controls.
Edit Controls should either use set_edit_text() or type_keys() to modify
the contents of the edit control.
"""
self.verify_actionable()
if append:
text = self.window_text() + text
self.set_focus()
# Set text using IUIAutomationValuePattern
self.iface_value.SetValue(text)
raise UserWarning("set_window_text() should probably not be called for Edit Controls")
# -----------------------------------------------------------
def set_edit_text(self, text, pos_start=None, pos_end=None):
"""Set the text of the edit control"""
self.verify_actionable()
# allow one or both of pos_start and pos_end to be None
if pos_start is not None or pos_end is not None:
# if only one has been specified - then set the other
# to the current selection start or end
start, end = self.selection_indices()
if pos_start is None:
pos_start = start
if pos_end is None and not isinstance(start, six.string_types):
pos_end = end
else:
pos_start = 0
pos_end = len(self.window_text())
if isinstance(text, six.text_type):
if six.PY3:
aligned_text = text
else:
aligned_text = text.encode(locale.getpreferredencoding())
elif isinstance(text, six.binary_type):
if six.PY3:
aligned_text = text.decode(locale.getpreferredencoding())
else:
aligned_text = text
else:
# convert a non-string input
if six.PY3:
aligned_text = six.text_type(text)
else:
aligned_text = six.binary_type(text)
# Calculate new text value
current_text = self.window_text()
new_text = current_text[:pos_start] + aligned_text + current_text[pos_end:]
# Set text using IUIAutomationValuePattern
self.iface_value.SetValue(new_text)
#win32functions.WaitGuiThreadIdle(self)
#time.sleep(Timings.after_editsetedittext_wait)
if isinstance(aligned_text, six.text_type):
self.actions.log('Set text to the edit box: ' + aligned_text)
else:
self.actions.log(b'Set text to the edit box: ' + aligned_text)
# return this control so that actions can be chained.
return self
# set set_text as an alias to set_edit_text
set_text = set_edit_text
# -----------------------------------------------------------
def select(self, start=0, end=None):
"""Set the edit selection of the edit control"""
self.verify_actionable()
self.set_focus()
# if we have been asked to select a string
if isinstance(start, six.text_type):
string_to_select = start
elif isinstance(start, six.binary_type):
string_to_select = start.decode(locale.getpreferredencoding())
elif isinstance(start, six.integer_types):
if isinstance(end, six.integer_types) and start > end:
start, end = end, start
string_to_select = self.window_text()[start:end]
if string_to_select:
document_range = self.iface_text.DocumentRange
search_range = document_range.FindText(string_to_select, False, False)
try:
search_range.Select()
except ValueError:
raise RuntimeError("Text '{0}' hasn't been found".format(string_to_select))
# return this control so that actions can be chained.
return self
# ====================================================================
class TabControlWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible Tab control"""
_control_types = ['Tab']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(TabControlWrapper, self).__init__(elem)
# ----------------------------------------------------------------
def get_selected_tab(self):
"""Return an index of a selected tab"""
return self.selected_item_index()
# ----------------------------------------------------------------
def tab_count(self):
"""Return a number of tabs"""
return self.control_count()
# ----------------------------------------------------------------
def select(self, item):
"""Select a tab by index or by name"""
self._select(item)
return self
# ----------------------------------------------------------------
def texts(self):
"""Tabs texts"""
return self.children_texts()
# ====================================================================
class SliderWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible Slider control"""
_control_types = ['Slider']
has_title = False
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(SliderWrapper, self).__init__(elem)
# -----------------------------------------------------------
def min_value(self):
"""Get the minimum value of the Slider"""
return self.iface_range_value.CurrentMinimum
# -----------------------------------------------------------
def max_value(self):
"""Get the maximum value of the Slider"""
return self.iface_range_value.CurrentMaximum
# -----------------------------------------------------------
def small_change(self):
"""
Get a small change of slider's thumb
This change is achieved by pressing left and right arrows
when slider's thumb has keyboard focus.
"""
return self.iface_range_value.CurrentSmallChange
# -----------------------------------------------------------
def large_change(self):
"""
Get a large change of slider's thumb
This change is achieved by pressing PgUp and PgDown keys
when slider's thumb has keyboard focus.
"""
return self.iface_range_value.CurrentLargeChange
# -----------------------------------------------------------
def value(self):
"""Get a current position of slider's thumb"""
return self.iface_range_value.CurrentValue
# -----------------------------------------------------------
def set_value(self, value):
"""Set position of slider's thumb"""
if isinstance(value, float):
value_to_set = value
elif isinstance(value, six.integer_types):
value_to_set = value
elif isinstance(value, six.text_type):
value_to_set = float(value)
else:
raise ValueError("value should be either string or number")
min_value = self.min_value()
max_value = self.max_value()
if not (min_value <= value_to_set <= max_value):
raise ValueError("value should be bigger than {0} and smaller than {1}".format(min_value, max_value))
self.iface_range_value.SetValue(value_to_set)
# ====================================================================
class HeaderWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible Header control"""
_control_types = ['Header']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(HeaderWrapper, self).__init__(elem)
# ====================================================================
class HeaderItemWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible Header Item control"""
_control_types = ['HeaderItem']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(HeaderItemWrapper, self).__init__(elem)
# ====================================================================
class ListItemWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible ListViewItem control"""
_control_types = ['DataItem', 'ListItem', ]
# -----------------------------------------------------------
def __init__(self, elem, container=None):
"""Initialize the control"""
super(ListItemWrapper, self).__init__(elem)
# Init a pointer to the item's container wrapper.
# It must be set by a container wrapper producing the item.
# Notice that the self.parent property isn't the same
# because it results in a different instance of a wrapper.
self.container = container
# -----------------------------------------------------------
def is_checked(self):
"""Return True if the ListItem is checked
Only items supporting Toggle pattern should answer.
Raise NoPatternInterfaceError if the pattern is not supported
"""
return self.iface_toggle.ToggleState_On == toggle_state_on
def texts(self):
"""Return a list of item texts"""
content = [ch.window_text() for ch in self.children(content_only=True)]
if content:
return content
else:
# For native list with small icons
return super(ListItemWrapper, self).texts()
# ====================================================================
class ListViewWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible ListView control"""
_control_types = ['DataGrid', 'List', ]
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(ListViewWrapper, self).__init__(elem)
# Check if control supports Grid pattern
# Control is actually a DataGrid or a List with Grid pattern support
try:
if self.iface_grid:
self.iface_grid_support = True
except NoPatternInterfaceError:
self.iface_grid_support = False
# -----------------------------------------------------------
def item_count(self):
"""A number of items in the ListView"""
if self.iface_grid_support:
return self.iface_grid.CurrentRowCount
else:
# TODO: This could be implemented by getting custom ItemCount Property using RegisterProperty
# TODO: See https://msdn.microsoft.com/ru-ru/library/windows/desktop/ff486373%28v=vs.85%29.aspx for details
# TODO: comtypes doesn't seem to support IUIAutomationRegistrar interface
return (len(self.children()))
# -----------------------------------------------------------
def column_count(self):
"""Return the number of columns"""
if self.iface_grid_support:
return self.iface_grid.CurrentColumnCount
else:
# ListBox doesn't have columns
return 0
# -----------------------------------------------------------
def get_header_control(self):
"""Return Header control associated with the ListView"""
try:
# A data grid control may have no header
hdr = self.children(control_type="Header")[0]
except(IndexError, NoPatternInterfaceError):
hdr = None
return hdr
# -----------------------------------------------------------
def get_column(self, col_index):
"""Get the information for a column of the ListView"""
col = None
try:
col = self.columns()[col_index]
except comtypes.COMError:
raise IndexError
return col
# -----------------------------------------------------------
def columns(self):
"""Get the information on the columns of the ListView"""
if self.iface_grid_support:
arr = self.iface_table.GetCurrentColumnHeaders()
cols = uia_element_info.elements_from_uia_array(arr)
return [uiawrapper.UIAWrapper(e) for e in cols]
else:
# ListBox doesn't have columns
return []
# -----------------------------------------------------------
def cell(self, row, column):
"""Return a cell in the ListView control
Only for controls with Grid pattern support
* **row** is an index of a row in the list.
* **column** is an index of a column in the specified row.
The returned cell can be of different control types.
Mostly: TextBlock, ImageControl, EditControl, DataItem
or even another layer of data items (Group, DataGrid)
"""
if not isinstance(row, six.integer_types) or not isinstance(column, six.integer_types):
raise TypeError("row and column must be numbers")
if not self.iface_grid_support:
return None
try:
e = self.iface_grid.GetItem(row, column)
elem_info = uia_element_info.UIAElementInfo(e)
cell_elem = uiawrapper.UIAWrapper(elem_info)
except (comtypes.COMError, ValueError):
raise IndexError
return cell_elem
# -----------------------------------------------------------
def get_item(self, row):
"""Return an item of the ListView control
* **row** can be either an index of the row or a string
with the text of a cell in the row you want returned.
"""
# Verify arguments
if isinstance(row, six.string_types):
# Try to find item using FindItemByProperty
# That way we can get access to virtualized (unloaded) items
com_elem = self.iface_item_container.FindItemByProperty(0, IUIA().UIA_dll.UIA_NamePropertyId, row)
# Try to load element using VirtualizedItem pattern
try:
get_elem_interface(com_elem, "VirtualizedItem").Realize()
itm = uiawrapper.UIAWrapper(uia_element_info.UIAElementInfo(com_elem))
except NoPatternInterfaceError:
# Item doesn't support VirtualizedItem pattern - item is already on screen or com_elem is NULL
try:
itm = uiawrapper.UIAWrapper(uia_element_info.UIAElementInfo(com_elem))
except ValueError:
# com_elem is NULL pointer
# Get DataGrid row
try:
itm = self.descendants(title=row)[0]
# Applications like explorer.exe usually return ListItem
# directly while other apps can return only a cell.
# In this case we need to take its parent - the whole row.
if not isinstance(itm, ListItemWrapper):
itm = itm.parent()
except IndexError:
raise ValueError("Element '{0}' not found".format(row))
elif isinstance(row, six.integer_types):
# Get the item by a row index
# TODO: Can't get virtualized items that way
# TODO: See TODO section of item_count() method for details
list_items = self.children(content_only=True)
itm = list_items[row]
else:
raise TypeError("String type or integer is expected")
# Give to the item a pointer on its container
itm.container = self
return itm
item = get_item # this is an alias to be consistent with other content elements
# -----------------------------------------------------------
def get_items(self):
"""Return all items of the ListView control"""
return self.children(content_only=True)
items = get_items # this is an alias to be consistent with other content elements
# -----------------------------------------------------------
def get_item_rect(self, item_index):
"""Return the bounding rectangle of the list view item
The method is kept mostly for a backward compatibility
with the native ListViewWrapper interface
"""
itm = self.get_item(item_index)
return itm.rectangle()
# -----------------------------------------------------------
def get_selected_count(self):
"""Return a number of selected items
The call can be quite expensieve as we retrieve all
the selected items in order to count them
"""
selection = self.get_selection()
if selection:
return len(selection)
else:
return 0
# -----------------------------------------------------------
def texts(self):
"""Return a list of item texts"""
return [elem.texts() for elem in self.children(content_only=True)]
# -----------------------------------------------------------
@property
def writable_props(self):
"""Extend default properties list."""
props = super(ListViewWrapper, self).writable_props
props.extend(['column_count',
'item_count',
'columns',
# 'items',
])
return props
# ====================================================================
class MenuItemWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible MenuItem control"""
_control_types = ['MenuItem']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(MenuItemWrapper, self).__init__(elem)
# -----------------------------------------------------------
def items(self):
"""Find all items of the menu item"""
return self.children(control_type="MenuItem")
# -----------------------------------------------------------
def select(self):
"""Apply Select pattern"""
try:
self.iface_selection_item.Select()
except(NoPatternInterfaceError):
try:
self.iface_invoke.Invoke()
except(NoPatternInterfaceError):
raise AttributeError
# ====================================================================
class MenuWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible MenuBar or Menu control"""
_control_types = ['MenuBar', 'Menu', ]
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(MenuWrapper, self).__init__(elem)
# -----------------------------------------------------------
def items(self):
"""Find all menu items"""
return self.children(control_type="MenuItem")
# -----------------------------------------------------------
def item_by_index(self, idx):
"""Find a menu item specified by the index"""
item = self.items()[idx]
return item
# -----------------------------------------------------------
@staticmethod
def _activate(item):
"""Activate the specified item"""
if not item.is_active():
item.set_focus()
try:
item.expand()
except(NoPatternInterfaceError):
pass
# -----------------------------------------------------------
def _sub_item_by_text(self, menu, name, exact):
"""Find a menu sub-item by the specified text"""
sub_item = None
items = menu.items()
if items:
if exact:
for i in items:
if name == i.window_text():
sub_item = i
break
else:
texts = []
for i in items:
texts.append(i.window_text())
sub_item = findbestmatch.find_best_match(name, texts, items)
self._activate(sub_item)
return sub_item
# -----------------------------------------------------------
def _sub_item_by_idx(self, menu, idx):
"""Find a menu sub-item by the specified index"""
sub_item = None
items = menu.items()
if items:
sub_item = items[idx]
self._activate(sub_item)
return sub_item
# -----------------------------------------------------------
def item_by_path(self, path, exact=False):
"""Find a menu item specified by the path
The full path syntax is specified in:
:py:meth:`.controls.menuwrapper.Menu.get_menu_path`
Note: $ - specifier is not supported
"""
# Get the path parts
part0, parts = path.split("->", 1)
part0 = part0.strip()
if len(part0) == 0:
raise IndexError()
# Find a top level menu item and select it. After selecting this item
# a new Menu control is created and placed on the dialog. It can be
# a direct child or a descendant.
# Sometimes we need to re-discover Menu again
try:
menu = None
if part0.startswith("#"):
menu = self._sub_item_by_idx(self, int(part0[1:]))
else:
menu = self._sub_item_by_text(self, part0, exact)
if not menu.items():
self._activate(menu)
timings.wait_until(
timings.Timings.window_find_timeout,
timings.Timings.window_find_retry,
lambda: len(self.top_level_parent().descendants(control_type="Menu")) > 0)
menu = self.top_level_parent().descendants(control_type="Menu")[0]
for cur_part in [p.strip() for p in parts.split("->")]:
if cur_part.startswith("#"):
menu = self._sub_item_by_idx(menu, int(cur_part[1:]))
else:
menu = self._sub_item_by_text(menu, cur_part, exact)
except(AttributeError):
raise IndexError()
return menu
# ====================================================================
class TooltipWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible Tooltip control"""
_control_types = ['ToolTip']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(TooltipWrapper, self).__init__(elem)
# ====================================================================
class ToolbarWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible ToolBar control
The control's children usually are: Buttons, SplitButton,
MenuItems, ThumbControls, TextControls, Separators, CheckBoxes.
Notice that ToolTip controls are children of the top window and
not of the toolbar.
"""
_control_types = ['ToolBar']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(ToolbarWrapper, self).__init__(elem)
@property
def writable_props(self):
"""Extend default properties list."""
props = super(ToolbarWrapper, self).writable_props
props.extend(['button_count'])
return props
# ----------------------------------------------------------------
def texts(self):
"""Return texts of the Toolbar"""
return self.children_texts()
#----------------------------------------------------------------
def button_count(self):
"""Return a number of buttons on the ToolBar"""
return len(self.children())
# ----------------------------------------------------------------
def button(self, button_identifier, exact=True):
"""Return a button by the specified identifier
* **button_identifier** can be either an index of a button or
a string with the text of the button.
* **exact** flag specifies if the exact match for the text look up
has to be applied.
"""
cc = self.children()
texts = [c.window_text() for c in cc]
if isinstance(button_identifier, six.string_types):
self.actions.log('Toolbar buttons: ' + str(texts))
if exact:
try:
button_index = texts.index(button_identifier)
except ValueError:
raise findbestmatch.MatchError(items=texts, tofind=button_identifier)
else:
# one of these will be returned for the matching text
indices = [i for i in range(0, len(texts))]
# find which index best matches that text
button_index = findbestmatch.find_best_match(button_identifier, texts, indices)
else:
button_index = button_identifier
return cc[button_index]
# ----------------------------------------------------------------
def check_button(self, button_identifier, make_checked, exact=True):
"""Find where the button is and toggle it
* **button_identifier** can be either an index of the button or
a string with the text on the button.
* **make_checked** specifies the required toggled state of the button.
If the button is already in the specified state the state isn't changed.
* **exact** flag specifies if the exact match for the text look up
has to be applied
"""
self.actions.logSectionStart('Checking "' + self.window_text() +
'" toolbar button "' + str(button_identifier) + '"')
button = self.button(button_identifier, exact=exact)
if make_checked:
self.actions.log('Pressing down toolbar button "' + str(button_identifier) + '"')
else:
self.actions.log('Pressing up toolbar button "' + str(button_identifier) + '"')
if not button.is_enabled():
self.actions.log('Toolbar button is not enabled!')
raise RuntimeError("Toolbar button is not enabled!")
res = (button.get_toggle_state() == toggle_state_on)
if res != make_checked:
button.toggle()
self.actions.logSectionEnd()
return button
# ====================================================================
class TreeItemWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible TreeItem control
In addition to the provided methods of the wrapper
additional inherited methods can be especially helpful:
select(), extend(), collapse(), is_extended(), is_collapsed(),
click_input(), rectangle() and many others
"""
_control_types = ['TreeItem']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(TreeItemWrapper, self).__init__(elem)
# -----------------------------------------------------------
def is_checked(self):
"""Return True if the TreeItem is checked
Only items supporting Toggle pattern should answer.
Raise NoPatternInterfaceError if the pattern is not supported
"""
return (self.iface_toggle.ToggleState_On == toggle_state_on)
# -----------------------------------------------------------
def ensure_visible(self):
"""Make sure that the TreeView item is visible"""
self.iface_scroll_item.ScrollIntoView()
# -----------------------------------------------------------
def get_child(self, child_spec, exact=False):
"""Return the child item of this item
Accepts either a string or an index.
If a string is passed then it returns the child item
with the best match for the string.
"""
cc = self.children(control_type='TreeItem')
if isinstance(child_spec, six.string_types):
texts = [c.window_text() for c in cc]
if exact:
if child_spec in texts:
index = texts.index(child_spec)
else:
raise IndexError('There is no child equal to "' + str(child_spec) + '" in ' + str(texts))
else:
indices = range(0, len(texts))
index = findbestmatch.find_best_match(
child_spec, texts, indices, limit_ratio=.6)
else:
index = child_spec
return cc[index]
# -----------------------------------------------------------
def _calc_click_coords(self):
"""Override the BaseWrapper helper method
Try to get coordinates of a text box inside the item.
If no text box found just set coordinates
close to a left part of the item rectangle
The returned coordinates are always absolute
"""
tt = self.children(control_type="Text")
if tt:
point = tt[0].rectangle().mid_point()
# convert from POINT to a simple tuple
coords = (point.x, point.y)
else:
rect = self.rectangle()
coords = (rect.left + int(float(rect.width()) / 4.),
rect.top + int(float(rect.height()) / 2.))
return coords
# -----------------------------------------------------------
def sub_elements(self):
"""Return a list of all visible sub-items of this control"""
return self.descendants(control_type="TreeItem")
# ====================================================================
class TreeViewWrapper(uiawrapper.UIAWrapper):
"""Wrap an UIA-compatible Tree control"""
_control_types = ['Tree']
# -----------------------------------------------------------
def __init__(self, elem):
"""Initialize the control"""
super(TreeViewWrapper, self).__init__(elem)
@property
def writable_props(self):
"""Extend default properties list."""
props = super(TreeViewWrapper, self).writable_props
props.extend(['item_count'])
return props
# -----------------------------------------------------------
def item_count(self):
"""Return a number of items in TreeView"""
return len(self.descendants(control_type="TreeItem"))
# -----------------------------------------------------------
def roots(self):
"""Return root elements of TreeView"""
return self.children(control_type="TreeItem")
# -----------------------------------------------------------
def get_item(self, path, exact=False):
r"""Read a TreeView item
* **path** a path to the item to return. This can be one of
the following:
* A string separated by \\ characters. The first character must
be \\. This string is split on the \\ characters and each of
these is used to find the specific child at each level. The
\\ represents the root item - so you don't need to specify the
root itself.
* A list/tuple of strings - The first item should be the root
element.
* A list/tuple of integers - The first item the index which root
to select. Indexing always starts from zero: get_item((0, 2, 3))
* **exact** a flag to request exact match of strings in the path
or apply a fuzzy logic of best_match thus allowing non-exact
path specifiers
"""
if not self.item_count():
return None
# Ensure the path is absolute
if isinstance(path, six.string_types):
if not path.startswith("\\"):
raise RuntimeError(
"Only absolute paths allowed - "
"please start the path with \\")
path = path.split("\\")[1:]
current_elem = None
# find the correct root elem
if isinstance(path[0], int):
current_elem = self.roots()[path[0]]
else:
roots = self.roots()
texts = [r.window_text() for r in roots]
if exact:
if path[0] in texts:
current_elem = roots[texts.index(path[0])]
else:
raise IndexError("There is no root element equal to '{0}'".format(path[0]))
else:
try:
current_elem = findbestmatch.find_best_match(
path[0], texts, roots, limit_ratio=.6)
except IndexError:
raise IndexError("There is no root element similar to '{0}'".format(path[0]))
# now for each of the lower levels
# just index into it's children
for child_spec in path[1:]:
try:
# ensure that the item is expanded as this is sometimes
# required for loading tree view branches
current_elem.expand()
current_elem = current_elem.get_child(child_spec, exact)
except IndexError:
if isinstance(child_spec, six.string_types):
raise IndexError("Item '{0}' does not have a child '{1}'".format(
current_elem.window_text(), child_spec))
else:
raise IndexError("Item '{0}' does not have {1} children".format(
current_elem.window_text(), child_spec + 1))
except comtypes.COMError:
raise IndexError("Item '{0}' does not have a child '{1}'".format(
current_elem.window_text(), child_spec))
return current_elem
# -----------------------------------------------------------
def print_items(self):
"""Print all items with line indents"""
self.text = ""
def _print_one_level(item, ident):
"""Get texts for the item and its children"""
self.text += " " * ident + item.window_text() + "\n"
for child in item.children(control_type="TreeItem"):
_print_one_level(child, ident + 1)
for root in self.roots():
_print_one_level(root, 0)
return self.text