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.
1196 lines
44 KiB
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
|