# -*- coding: utf-8 -*-
# Copyright: SCLE SFE
# Contributor: Julien Pagès <j.parkouss@gmail.com>
#
# This software is a computer program whose purpose is to test graphical
# applications written with the QT framework (http://qt.digia.com/).
#
# This software is governed by the CeCILL v2.1 license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL v2.1 license and that you accept its terms.
"""
Definition of widgets and models useable in funq.
"""
from funq.tools import wait_for
from funq.errors import FunqError
import json
import base64
class TreeItem(object): # pylint: disable=R0903
"""
Defines an abstract item that contains subitems
"""
client = None
items = None
@classmethod
def create(cls, client, data):
"""
Allow to create a TreeItem from a dico data decoded from json.
"""
self = cls()
self.client = client
for k, v in data.iteritems():
if k != 'items':
setattr(self, k, v)
self.items = [cls.create(client, d) for d in data.get('items', [])]
return self
class TreeItems(object):
"""
Abstract class to manipulate data that contains :class:`TreeItem`. Used
by modelitems and graphicsitems.
"""
client = None
items = None
ITEM_CLASS = TreeItem
@classmethod
def create(cls, client, data):
"""
Allow to create an instance of the class given some data coming from
decoded json.
"""
self = cls()
self.client = client
self.items = [
cls.ITEM_CLASS.create(client, v1) for v1 in data['items']]
return self
def iter(self):
"""
Allow to iterate on every items recursively.
Example::
for item in items.iter():
print item
"""
items = self.items
while items:
item = items.pop(0)
items = item.items + items
yield item
CPP_CLASSES = {}
class WidgetMetaClass(type):
"""
Saves a dict of accessible classes to handle inheritance of Widgets.
"""
def __new__(mcs, name, bases, attrs):
global CPP_CLASSES
cls = super(WidgetMetaClass, mcs).__new__(mcs, name, bases, attrs)
qt_name = getattr(cls, 'CPP_CLASS', None)
if qt_name:
CPP_CLASSES[qt_name] = cls
return cls
[docs]class Object(object):
"""
Allow to manipulate a QObject or derived.
:var client: client for the communication with libFunq
[type: :class:`funq.client.FunqClient`]
:var oid: ID of the managed C++ instance. [type: long]
:var path: complete path to the object [type: str]
:var classes: list of class names of the managed C++ instance,
in inheritance order (ie 'QObject' is last)
[type : list(str)]
"""
__metaclass__ = WidgetMetaClass
oid = None
client = None
path = None
@classmethod
def create(cls, client, data):
"""
Allow to create an Object or a subclass given data coming from
decoded json.
"""
# recherche la classe appropriee
global CPP_CLASSES
for cppcls in data['classes']:
if cppcls in CPP_CLASSES:
cls = CPP_CLASSES[cppcls]
break
self = cls()
for k, v in data.iteritems():
setattr(self, k, v)
setattr(self, 'client', client)
return self
[docs] def properties(self):
"""
Returns a dict of availables properties for this object with associated
values.
Example::
enabled = object.properties()["enabled"]
"""
return self.client.send_command('object_properties', oid=self.oid)
[docs] def set_properties(self, **properties):
"""
Define some properties on this object.
Example::
object.set_properties(text="My beautiful text")
"""
self.client.send_command('object_set_properties',
oid=self.oid,
properties=properties)
[docs] def set_property(self, name, value):
"""
Define one property on this object.
Example::
object.set_property('text', "My beautiful text")
"""
self.set_properties(**{name: value}) # pylint:disable=W0142
[docs] def wait_for_properties(self, props, timeout=10.0, timeout_interval=0.1):
"""
Wait for the properties to have the given values.
Example::
self.wait_for_properties({'enabled': True, 'visible': True})
"""
def check_props():
properties = self.properties()
for k, v in props.iteritems():
if properties.get(k) != v:
return False
return True
return wait_for(check_props, timeout, timeout_interval)
[docs] def call_slot(self, slot_name, params={}):
"""
**CAUTION**; This methods allows to call a slot (written on the tested
application). The slot must take a QVariant and returns a QVariant.
This is not really recommended to use this method, as it will trigger
code in the tested application in an unusual way.
The methods returns what the slot returned, decoded as python object.
:param slot_name: name of the slot
:param params: parameters (must be json serialisable) that will be send
to the tested application as a QVariant.
"""
return self.client.send_command(
'call_slot',
slot_name=slot_name,
params=params,
oid=self.oid
)['result_slot']
[docs]class ModelItem(TreeItem):
"""
Allow to manipulate a modelitem in a QAbstractModelItem or derived.
:var viewid: ID of the view attached to the model containing this item
[type: long]
:var row: item row number [type: int]
:var column: item column number [type: int]
:var value: item text value [type: unicode]
:var check_state: item text value of the check state, or None
:var itempath: Internal ID to localize this item [type: str ou None]
:var items: list of subitems [type: :class:`ModelItem`]
"""
viewid = None
row = None
column = None
itempath = None
check_state = None
def _action(self, itemaction, origin=None, offset_x=None, offset_y=None):
""" Send the 'model_item_action' action """
self.client.send_command('model_item_action',
oid=self.viewid,
itemaction=itemaction,
row=self.row, column=self.column,
origin=origin,
offset_x=offset_x,
offset_y=offset_y,
itempath=self.itempath)
[docs] def is_checkable(self):
"""Returns True if the item is checkable"""
return self.check_state is not None
[docs] def is_checked(self):
"""Returns True if the item is checked"""
return self.check_state == 'checked'
[docs] def select(self):
"""
Select this item.
"""
self._action("select")
[docs] def edit(self):
"""
Edit this item.
"""
self._action("edit")
[docs] def click(self, origin="center", offset_x=0, offset_y=0):
"""
Click on this item.
:param origin: Origin of the cursor coordinates of the ModelItem
object. Availables values: "center", "left" or "right".
:param offset_x: x position relative to the origin.
Negative value allowed.
:param offset_y: y position relative to the origin.
Negative value allowed.
"""
self._action(
"click", origin=origin, offset_x=offset_x, offset_y=offset_y
)
[docs] def dclick(self, origin="center", offset_x=0, offset_y=0):
"""
Double click on this item.
:param origin: Origin of the cursor coordinates of the ModelItem
object.
:param offset_x: x position relative to the origin.
Negative value allowed.
:param offset_y: y position relative to the origin.
Negative value allowed.
"""
self._action(
"doubleclick", origin=origin, offset_x=offset_x, offset_y=offset_y
)
[docs]class ModelItems(TreeItems):
"""
Allow to manipulate all modelitems in a QAbstractModelItem or derived.
:var items: list of :class:`ModelItem`
"""
ITEM_CLASS = ModelItem
[docs] def item_by_named_path(self, named_path, match_column=0, sep='/',
column=0):
"""
Returns the item (:class:`ModelItem`) that match the arborescence
defined by `named_path` and in the given column.
.. note::
The arguments are the same as for :meth:`row_by_named_path`, with
the addition of `column`.
:param column: the column of the desired item
"""
items = self.row_by_named_path(named_path,
match_column=match_column,
sep=sep)
if items:
return items[column]
[docs] def row_by_named_path(self, named_path, match_column=0, sep='/'):
"""
Returns the item list of :class:`ModelItem` that match the arborescence
defined by `named_path`, or None if the path does not exists.
.. important::
Use unicode characters in `named_path` to match elements with
non-ascii characters.
Example::
model_items.row_by_named_path([u'TG/CBO/AP (AUT 1)',
u'Paramètres tranche',
u'TG',
u'DANGER'])
:param named_path: path for the interesting ModelIndex. May be
defined with a list of str or with a single str
that will be splitted on `sep`.
:param match_column: column used to check`named_path` is a string.
"""
if isinstance(named_path, (list, tuple)):
parts = list(named_path)
else:
parts = named_path.split(sep)
item = self
while item and parts:
next_item = None
part = parts.pop(0)
for item_ in item.items:
if match_column == item_.column and item_.value == part:
# we found the item
# if it is the last part name, just return the columns
if not parts:
row = [it for it in item.items
if it.row == item_.row]
return sorted(row, key=lambda it: it.column)
else:
next_item = item_
item = next_item
return None
[docs]class AbstractItemView(Widget):
"""
Specific Widget to manipulate QAbstractItemView or derived.
"""
CPP_CLASS = 'QAbstractItemView'
editor_class_names = ('QLineEdit', 'QComboBox', 'QSpinBox',
'QDoubleSpinBox')
[docs] def model_items(self):
"""
Returns an instance of :class:`ModelItems` based on the model
associated to the view.
"""
data = self.client.send_command('model_items', oid=self.oid)
return ModelItems.create(self.client, data)
[docs] def current_editor(self, editor_class_name=None):
"""
Returns the editor actually opened on this view. One item must be
in editing mode, by using :meth:`ModelItem.dclick` or
:meth:`ModelItem.edit` for example.
Currently these editor types are handled:
'QLineEdit', 'QComboBox', 'QSpinBox' and 'QDoubleSpinBox'.
:param editor_class_name: name of the editor type. If None, every
type of editor will be tested (this may
actually be very slow)
"""
qt_path = '::qt_scrollarea_viewport::%s'
if editor_class_name:
return self.client.widget(path=self.path
+ qt_path % editor_class_name)
for editor_class_name in self.editor_class_names:
try:
return self.client.widget(path=self.path
+ qt_path % editor_class_name)
except FunqError:
pass
raise FunqError("MissingEditor", 'Unable to find an editor.'
' Possible editors: %s'
% repr(self.editor_class_names))
[docs]class TableView(AbstractItemView):
"""
Specific widget to manipulate a QTableView widget.
"""
CPP_CLASS = 'QTableView'
[docs]class TreeView(AbstractItemView):
"""
Specific widget to manipulate a QTreeView widget.
"""
CPP_CLASS = 'QTreeView'
[docs]class TabBar(Widget):
"""
Allow to manipulate a QTabBar Widget.
"""
CPP_CLASS = "QTabBar"
[docs] def tab_texts(self):
"""
Returns the list of texts in tabbar.
"""
data = self.client.send_command('tabbar_list', oid=self.oid)
return data["tabtexts"]
[docs] def set_current_tab(self, tab_index_or_name):
"""
Define the current tab given an index or a tab text.
"""
tabnames = self.tab_texts()
if isinstance(tab_index_or_name, int):
index = tab_index_or_name
if index < 0 or index >= len(tabnames):
raise ValueError("Invalid tab Index %d" % index)
else:
index = tabnames.index(tab_index_or_name)
self.set_property('currentIndex', index)
[docs]class GItem(TreeItem):
"""
Allow to manipulate a QGraphicsItem.
:var viewid: ID of the view attached to the model containing this item
[type: long]
:var gid: Internal gitem ID [type: unsigned long long]
:var objectname: value of the "objectName" property if it inherits
from QObject. [type: unicode or None]
:var classes: list of names of class inheritance if it inherits from
QObject. [type: list(str) or None]
:var items: list of subitems [type: :class:`GItem`]
"""
viewid = None
gid = None
objectname = None
classes = None
[docs] def is_qobject(self):
""" Returns True if this GItem inherits QObject """
return self.objectname is not None
[docs] def properties(self):
"""
Return the properties of the GItem. The GItem must inherits from
QObject.
"""
return self.client.send_command('gitem_properties',
oid=self.viewid,
gid=self.gid)
def _action(self, itemaction):
""" Send the command 'model_gitem_action' """
self.client.send_command('model_gitem_action',
oid=self.viewid,
itemaction=itemaction,
gid=self.gid)
[docs] def click(self):
"""
Click on this gitem.
"""
self._action("click")
[docs] def dclick(self):
"""
Double click on this gitem.
"""
self._action("doubleclick")
[docs]class GItems(TreeItems):
"""
Allow to manipulate a group of QGraphicsItems.
:var items: list of :class:`GItem` that are on top of the scene
(and not subitems)
"""
ITEM_CLASS = GItem
[docs]class GraphicsView(Widget):
"""
Allow to manipulate an instance of QGraphicsView.
"""
CPP_CLASS = 'QGraphicsView'
[docs] def gitems(self):
"""
Returns an instance of :class:`GItems`, that will contains every items
of this QGraphicsView.
"""
data = self.client.send_command('graphicsitems', oid=self.oid)
return GItems.create(self.client, data)
[docs] def dump_gitems(self, stream='gitems.json'):
"""
Write in a file the list of graphics items.
"""
data = self.client.send_command('graphicsitems', oid=self.oid)
if isinstance(stream, basestring):
stream = open(stream, 'w')
json.dump(data,
stream, sort_keys=True, indent=4, separators=(',', ': '))
[docs] def grab_scene(self, stream, format_="PNG"):
"""
Save the full QGraphicsScene content under the GraphicsView as an
image.
.. versionadded:: 1.2.0
"""
data = self.client.send_command('grab_graphics_view', format=format_,
oid=self.oid)
has_to_be_closed = False
if isinstance(stream, basestring):
stream = open(stream, 'wb')
has_to_be_closed = True
raw = base64.standard_b64decode(data['data'])
stream.write(raw)
if has_to_be_closed:
stream.close()
[docs]class ComboBox(Widget):
"""
Allow to manipulate a QCombobox.
"""
CPP_CLASS = 'QComboBox'
[docs] def model_items(self):
"""
Returns the items (:class:`ModelItems`) associated to this combobox.
"""
# create and show QComboBoxListView
self.click()
# get this QComboBoxListView widget
internal_qt_name = '::QComboBoxPrivateContainer::QComboBoxListView'
combo_edit_view = self.client.widget(path=self.path + internal_qt_name)
model_items = combo_edit_view.model_items()
# This properly close the QComboBoxListView widget
combo_edit_view.click()
return model_items
[docs] def set_current_text(self, text):
"""
Define the text of the combobox, ensuring that it is a possible value.
"""
if not isinstance(text, basestring):
raise TypeError('the text parameter must be a string'
' - got %s' % type(text))
model_items = self.model_items()
column = self.properties()['modelColumn']
index = -1
for item in model_items.items:
if column == int(item.column) and item.value == text:
index = int(item.row)
break
assert index > -1, ("Le texte `%s` n'est pas dans la combobox `%s`"
% (text, self.path))
self.set_property('currentIndex', index)
[docs]class QuickItem(Object):
"""
Represent a QQuickItem or derived.
You can get a :class:`QuickItem` instance by using
:meth:`QuickWindow.item`.
"""
CPP_CLASS = "QQuickItem"
[docs] def click(self):
"""
Click on the :class:`QuickItem`.
"""
self.client.send_command(
"quick_item_click",
oid=self.oid
)
[docs]class QuickWindow(Widget):
"""
Represent a QQuickWindow or QQuickView.
If your application is a qml app with one window, you can easily get the
:class:`QuickWindow` with :meth:`funq.FunqClient.active_widget`.
Example::
quick_window = self.funq.active_widget()
"""
CPP_CLASS = "QQuickWindow"
[docs] def item(self, alias=None, path=None, id=None):
"""
Search for a :class:`funq.models.QuickItem` and returns it.
An item can be identified by its id, using an alias or using a
raw path. The preferred way is using an id (defined in the qml
file) - this takes precedence over other methods.
For example, with the following qml code:
.. code-block:: qml
import QtQuick 2.0
Item {
id: root
width: 320
height: 480
Rectangle {
id: rect
color: "#272822"
width: 320
height: 480
}
}
You can use the following statements::
# using id
root = quick_window.item(id='root')
rect = quick_window.item(id='root.rect') # or just 'rect'
# using alias
# you must have something like the following line in your alias file:
# my_rect = QQuickView::QQuickItem::QQuickRectangle
rect = quick_window.item(alias='my_rect')
# using raw path - note the path is relative to the quick window
rect = quick_window.item(path='QQuickItem::QQuickRectangle')
:param alias: alias defined in the aliases file that points to the
item.
:param path: path of the item, relative to the view (do not pass
the full path)
:param id: id of the qml item.
"""
if not (alias or path or id):
raise TypeError("alias, path or id must be defined")
if alias and not id:
path = self.client.aliases[alias]
if not path.startswith(self.path):
raise TypeError("alias %r does not belong to this quick window"
% path)
# remove the window path here, c++ code only requires the
# object path from the root item.
path = path[len(self.path)+2:]
data = self.client.send_command(
'quick_item_find',
quick_window_oid=self.oid,
path=path,
qid=id,
)
return Object.create(self.client, data)