mirror of
https://github.com/virt-manager/virt-manager.git
synced 2024-12-24 21:34:47 +03:00
092a62552c
Signed-off-by: Cole Robinson <crobinso@redhat.com>
396 lines
13 KiB
Python
396 lines
13 KiB
Python
# This work is licensed under the GNU GPLv2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
import re
|
|
|
|
from gi.repository import Gdk
|
|
|
|
import dogtail.tree
|
|
import pyatspi
|
|
|
|
from virtinst import log
|
|
from . import utils
|
|
|
|
|
|
class _FuzzyPredicate(dogtail.predicate.Predicate):
|
|
"""
|
|
Object dogtail/pyatspi want for node searching.
|
|
"""
|
|
def __init__(self, name=None, roleName=None, labeller_text=None,
|
|
focusable=False, onscreen=False):
|
|
"""
|
|
:param name: Match node.name or node.labeller.text if
|
|
labeller_text not specified
|
|
:param roleName: Match node.roleName
|
|
:param labeller_text: Match node.labeller.text
|
|
:param focusable: Ensure node is focusable
|
|
"""
|
|
self._name = name
|
|
self._roleName = roleName
|
|
self._labeller_text = labeller_text
|
|
self._focusable = focusable
|
|
self._onscreen = onscreen
|
|
|
|
self._name_pattern = None
|
|
self._role_pattern = None
|
|
self._labeller_pattern = None
|
|
if self._name:
|
|
self._name_pattern = re.compile(self._name, re.DOTALL)
|
|
if self._roleName:
|
|
self._role_pattern = re.compile(self._roleName, re.DOTALL)
|
|
if self._labeller_text:
|
|
self._labeller_pattern = re.compile(self._labeller_text, re.DOTALL)
|
|
|
|
def makeScriptMethodCall(self, isRecursive):
|
|
ignore = isRecursive
|
|
return
|
|
def makeScriptVariableName(self):
|
|
return
|
|
def describeSearchResult(self, node=None):
|
|
if not node:
|
|
return ""
|
|
return node.node_string()
|
|
|
|
def satisfiedByNode(self, node):
|
|
"""
|
|
The actual search routine
|
|
"""
|
|
try:
|
|
if self._roleName and not self._role_pattern.match(node.roleName):
|
|
return
|
|
|
|
labeller = ""
|
|
if node.labeller:
|
|
labeller = node.labeller.text
|
|
|
|
if (self._name and
|
|
not self._name_pattern.match(node.name) and
|
|
not self._name_pattern.match(labeller)):
|
|
return
|
|
if (self._labeller_text and
|
|
not self._labeller_pattern.match(labeller)):
|
|
return
|
|
if (self._focusable and not
|
|
(node.focusable and
|
|
node.onscreen and
|
|
node.sensitive and
|
|
node.roleName not in ["page tab list", "radio button"])):
|
|
return False
|
|
return True
|
|
except Exception as e:
|
|
log.debug(
|
|
"got predicate exception name=%s role=%s labeller=%s: %s",
|
|
self._name, self._roleName, self._labeller_text, e)
|
|
|
|
|
|
def _debug_decorator(fn):
|
|
def _cb(self, *args, **kwargs):
|
|
try:
|
|
return fn(self, *args, **kwargs)
|
|
except Exception:
|
|
print("node=%s\nstates=%s" % (self, self.print_states()))
|
|
raise
|
|
return _cb
|
|
|
|
|
|
class _VMMDogtailNode(dogtail.tree.Node):
|
|
"""
|
|
Our extensions to the dogtail node wrapper class.
|
|
"""
|
|
# The class hackery means pylint can't figure this class out
|
|
# pylint: disable=no-member
|
|
|
|
@property
|
|
def active(self):
|
|
"""
|
|
If the window is the raised and active window or not
|
|
"""
|
|
return self.getState().contains(pyatspi.STATE_ACTIVE)
|
|
|
|
@property
|
|
def state_selected(self):
|
|
return self.getState().contains(pyatspi.STATE_SELECTED)
|
|
|
|
@property
|
|
def onscreen(self):
|
|
# We need to check that full widget is on screen because we use this
|
|
# function to check whether we can click a widget. We may click
|
|
# anywhere within the widget and clicks outside the screen bounds are
|
|
# silently ignored.
|
|
if self.roleName in ["frame"]:
|
|
return True
|
|
screen = Gdk.Screen.get_default()
|
|
return (self.position[0] >= 0 and
|
|
self.position[0] + self.size[0] < screen.get_width() and
|
|
self.position[1] >= 0 and
|
|
self.position[1] + self.size[1] < screen.get_height())
|
|
|
|
@_debug_decorator
|
|
def check_onscreen(self):
|
|
"""
|
|
Check in a loop that the widget is onscreen
|
|
"""
|
|
utils.check(lambda: self.onscreen)
|
|
|
|
@_debug_decorator
|
|
def check_not_onscreen(self):
|
|
"""
|
|
Check in a loop that the widget is not onscreen
|
|
"""
|
|
utils.check(lambda: not self.onscreen)
|
|
|
|
@_debug_decorator
|
|
def check_focused(self):
|
|
"""
|
|
Check in a loop that the widget is focused
|
|
"""
|
|
utils.check(lambda: self.focused)
|
|
|
|
@_debug_decorator
|
|
def check_sensitive(self):
|
|
"""
|
|
Check whether interactive widgets are sensitive or not
|
|
"""
|
|
valid_types = [
|
|
"push button",
|
|
"toggle button",
|
|
"check button",
|
|
"combo box",
|
|
"menu item",
|
|
"text",
|
|
"menu",
|
|
]
|
|
if self.roleName not in valid_types:
|
|
return True
|
|
utils.check(lambda: self.sensitive)
|
|
|
|
def click_secondary_icon(self):
|
|
"""
|
|
Helper for clicking the secondary icon of a text entry
|
|
"""
|
|
self.check_onscreen()
|
|
self.check_sensitive()
|
|
button = 1
|
|
clickX = self.position[0] + self.size[0] - 10
|
|
clickY = self.position[1] + (self.size[1] / 2)
|
|
dogtail.rawinput.click(clickX, clickY, button)
|
|
|
|
def click_combo_entry(self):
|
|
"""
|
|
Helper for clicking the arrow of a combo entry, to expose the menu.
|
|
Clicks middle of Y axis, but 1/10th of the height from the right side.
|
|
Using a small, hardcoded offset may not work on some themes (e.g. when
|
|
running virt-manager on KDE)
|
|
"""
|
|
self.check_onscreen()
|
|
self.check_sensitive()
|
|
button = 1
|
|
clickX = self.position[0] + self.size[0] - self.size[1] / 4
|
|
clickY = self.position[1] + self.size[1] / 2
|
|
dogtail.rawinput.click(clickX, clickY, button)
|
|
|
|
def click_expander(self):
|
|
"""
|
|
Helper for clicking expander, hitting the text part to actually
|
|
open it. Basically clicks top left corner with some indent
|
|
"""
|
|
self.check_onscreen()
|
|
self.check_sensitive()
|
|
button = 1
|
|
clickX = self.position[0] + 10
|
|
clickY = self.position[1] + 5
|
|
dogtail.rawinput.click(clickX, clickY, button)
|
|
|
|
def title_coordinates(self):
|
|
"""
|
|
Return clickable coordinates of a window's titlebar
|
|
"""
|
|
x = self.position[0] + (self.size[0] / 2)
|
|
y = self.position[1] + 10
|
|
return x, y
|
|
|
|
def click_title(self):
|
|
"""
|
|
Helper to click a window title bar, hitting the horizontal
|
|
center of the bar
|
|
"""
|
|
if self.roleName not in ["frame", "alert"]:
|
|
raise RuntimeError("Can't use click_title() on type=%s" %
|
|
self.roleName)
|
|
button = 1
|
|
clickX, clickY = self.title_coordinates()
|
|
dogtail.rawinput.click(clickX, clickY, button)
|
|
|
|
def click(self, *args, **kwargs):
|
|
"""
|
|
click wrapper, give up to a second for widget to appear on
|
|
screen, helps reduce some test flakiness
|
|
"""
|
|
# pylint: disable=arguments-differ,signature-differs
|
|
self.check_onscreen()
|
|
self.check_sensitive()
|
|
super().click(*args, **kwargs)
|
|
|
|
def point(self, *args, **kwargs):
|
|
# pylint: disable=signature-differs
|
|
super().point(*args, **kwargs)
|
|
|
|
if (self.roleName == "menu" and
|
|
self.accessible_parent.roleName == "menu"):
|
|
# Widget is a submenu, make sure the item is in selected
|
|
# state before we return
|
|
utils.check(lambda: self.state_selected)
|
|
|
|
def set_text(self, text):
|
|
self.check_onscreen()
|
|
self.check_sensitive()
|
|
assert hasattr(self, "text")
|
|
self.text = text
|
|
|
|
def bring_on_screen(self, key_name="Down", max_tries=100):
|
|
"""
|
|
Attempts to bring the item to screen by repeatedly clicking the given
|
|
key. Raises exception if max_tries attempts are exceeded.
|
|
"""
|
|
cur_try = 0
|
|
while not self.onscreen:
|
|
dogtail.rawinput.pressKey(key_name)
|
|
cur_try += 1
|
|
if cur_try > max_tries:
|
|
raise RuntimeError("Could not bring widget on screen")
|
|
return self
|
|
|
|
def window_maximize(self):
|
|
assert self.roleName in ["frame", "dialog"]
|
|
self.grab_focus()
|
|
s1 = self.size
|
|
self.keyCombo("<alt>F10")
|
|
utils.check(lambda: self.size != s1)
|
|
self.grab_focus()
|
|
|
|
def window_close(self):
|
|
assert self.roleName in ["frame", "alert", "dialog", "file chooser"]
|
|
self.grab_focus()
|
|
self.keyCombo("<alt>F4")
|
|
utils.check(lambda: not self.showing)
|
|
|
|
def window_find_focusable_child(self):
|
|
return self.find(None, focusable=True)
|
|
|
|
def grab_focus(self):
|
|
if self.roleName in ["frame", "alert", "dialog", "file chooser"]:
|
|
child = self.window_find_focusable_child()
|
|
child.grab_focus()
|
|
utils.check(lambda: self.active)
|
|
return
|
|
|
|
self.check_onscreen()
|
|
assert self.focusable
|
|
self.grabFocus()
|
|
self.check_focused()
|
|
|
|
|
|
#########################
|
|
# Widget search helpers #
|
|
#########################
|
|
|
|
def find(self, name, roleName=None, labeller_text=None,
|
|
check_active=True, recursive=True, focusable=False):
|
|
"""
|
|
Search root for any widget that contains the passed name/role regex
|
|
strings.
|
|
"""
|
|
pred = _FuzzyPredicate(name, roleName, labeller_text, focusable)
|
|
|
|
try:
|
|
ret = self.findChild(pred, recursive=recursive)
|
|
except dogtail.tree.SearchError:
|
|
raise dogtail.tree.SearchError("Didn't find widget with name='%s' "
|
|
"roleName='%s' labeller_text='%s'" %
|
|
(name, roleName, labeller_text)) from None
|
|
|
|
# Wait for independent windows to become active in the window manager
|
|
# before we return them. This ensures the window is actually onscreen
|
|
# so it sidesteps a lot of race conditions
|
|
if ret.roleName in ["frame", "dialog", "alert"] and check_active:
|
|
utils.check(lambda: ret.active)
|
|
return ret
|
|
|
|
def find_fuzzy(self, name, roleName=None, labeller_text=None):
|
|
"""
|
|
Search root for any widget that contains the passed name/role strings.
|
|
"""
|
|
name_pattern = None
|
|
role_pattern = None
|
|
labeller_pattern = None
|
|
if name:
|
|
name_pattern = ".*%s.*" % name
|
|
if roleName:
|
|
role_pattern = ".*%s.*" % roleName
|
|
if labeller_text:
|
|
labeller_pattern = ".*%s.*" % labeller_text
|
|
|
|
return self.find(name_pattern, role_pattern, labeller_pattern)
|
|
|
|
|
|
##########################
|
|
# Higher level behaviors #
|
|
##########################
|
|
|
|
def combo_select(self, combolabel, itemlabel):
|
|
"""
|
|
Lookup the combo, click it, select the menu item
|
|
"""
|
|
combo = self.find(combolabel, "combo box")
|
|
combo.click_combo_entry()
|
|
combo.find(itemlabel, "menu item").click()
|
|
|
|
def combo_check_default(self, combolabel, itemlabel):
|
|
"""
|
|
Lookup the combo and verify the menu item is selected
|
|
"""
|
|
combo = self.find(combolabel, "combo box")
|
|
combo.click_combo_entry()
|
|
item = combo.find(itemlabel, "menu item")
|
|
utils.check(lambda: item.selected)
|
|
dogtail.rawinput.pressKey("Escape")
|
|
|
|
|
|
#####################
|
|
# Debugging helpers #
|
|
#####################
|
|
|
|
def node_string(self):
|
|
msg = "name='%s' roleName='%s'" % (self.name, self.roleName)
|
|
if self.labeller:
|
|
msg += " labeller.text='%s'" % self.labeller.text
|
|
return msg
|
|
|
|
def fmt_nodes(self):
|
|
strs = []
|
|
def _walk(node):
|
|
try:
|
|
strs.append(node.node_string())
|
|
except Exception as e:
|
|
strs.append("got exception: %s" % e)
|
|
|
|
self.findChildren(_walk, isLambda=True)
|
|
return "\n".join(strs)
|
|
|
|
def print_nodes(self):
|
|
"""
|
|
Helper to print the entire node tree for the passed root. Useful
|
|
if to figure out the roleName for the object you are looking for
|
|
"""
|
|
print(self.fmt_nodes())
|
|
|
|
def print_states(self):
|
|
print([s.value_nick for s in self.getState().get_states()])
|
|
|
|
|
|
# This is the same hack dogtail uses to extend the Accessible class.
|
|
_bases = list(pyatspi.Accessibility.Accessible.__bases__)
|
|
_bases.insert(_bases.index(dogtail.tree.Node), _VMMDogtailNode)
|
|
_bases.remove(dogtail.tree.Node)
|
|
pyatspi.Accessibility.Accessible.__bases__ = tuple(_bases)
|