diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5c4de5ca..1b4159e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,8 +76,12 @@ jobs: # Functional tests - name: Build test app run: cd tests-functionnal/funq-test-app && cmake . && make + - name: Build test qml app + run: cd tests-functionnal/funq-test-qml-app && cmake . && make - name: Test injection run: xvfb-run -a funq tests-functionnal/funq-test-app/funq-test-app --exit-after-startup + - name: Test injection qml + run: xvfb-run -a funq tests-functionnal/funq-test-qml-app/funq-test-qml-app --exit-after-startup - name: Test functional run: cd tests-functionnal && xvfb-run -a nosetests if: ${{ matrix.nosetests != 0}} diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f218a7..bf16d116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Support QML children items ## [1.2.0] - 2019-08-12 ### Added diff --git a/client/doc/qml_tutorial.rst b/client/doc/qml_tutorial.rst index 47fca67b..a309d269 100644 --- a/client/doc/qml_tutorial.rst +++ b/client/doc/qml_tutorial.rst @@ -21,7 +21,7 @@ to the README.rst file about it. Create the test folder ---------------------- -.. code-block:: +.. code-block:: none mkdir qmltest cd qmltest diff --git a/client/doc/user_api/widgets_models.rst b/client/doc/user_api/widgets_models.rst index e92bdfd8..ffcc3e4b 100644 --- a/client/doc/user_api/widgets_models.rst +++ b/client/doc/user_api/widgets_models.rst @@ -257,3 +257,5 @@ Example: :: .. autoclass:: QuickItem .. automethod:: QuickItem.click + + .. automethod:: QuickItem.children diff --git a/client/funq/models.py b/client/funq/models.py index 66afc9d7..6abbd066 100644 --- a/client/funq/models.py +++ b/client/funq/models.py @@ -932,7 +932,14 @@ class QuickItem(Object): Represent a QQuickItem or derived. You can get a :class:`QuickItem` instance by using - :meth:`QuickWindow.item`. + :meth:`QuickWindow.item` or by iterate over :meth:`QuickItem.children` + result + + :var oid: Internal gitem ID [type: unsigned long long] + :var path: complete path to the object [type: str] + :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:`QuickItem`] """ CPP_CLASS = "QQuickItem" @@ -946,6 +953,45 @@ def click(self): oid=self.oid ) + def children(self, recursive=False): + """ + Returns children items on the :class:`QuickItem`. + + :param recursive: when `True`, will call recursively for children items + + Example:: + + quick_window = self.funq.active_widget() + root = quick_window.item('root') + children = root.children() + for child in children.iter(): + print(child.properties()) + """ + data = self.client.send_command("quick_item_children", oid=self.oid, + recursive=recursive) + return QuickItems.create(self.client, data) + + @classmethod + def create(cls, client, data): + """ + Allow to create a :class: `QuickItem` from a dict data decoded from + json. + """ + self = super(QuickItem, cls).create(client, data) + self.items = [cls.create(client, d) for d in data.get('items', [])] + + return self + + +class QuickItems(TreeItems): + """ + Allow to manipulate all children in a QQuickItem. + + :var items: list of :class:`QuickItem` + """ + + ITEM_CLASS = QuickItem + class QuickWindow(Widget): """ @@ -1026,4 +1072,4 @@ def item(self, alias=None, path=None, id=None): path=path, qid=id, ) - return Object.create(self.client, data) + return QuickItem.create(self.client, data) diff --git a/server/libFunq/player.cpp b/server/libFunq/player.cpp index d41f004f..6393dbb5 100644 --- a/server/libFunq/player.cpp +++ b/server/libFunq/player.cpp @@ -780,6 +780,55 @@ QtJson::JsonObject Player::model_item_action( return result; } +void dump_quick_items(Player * player, const QList & items, const qulonglong & viewid, bool recursive, QtJson::JsonObject & out) { +#ifdef QT_QUICK_LIB + QtJson::JsonArray outitems; + foreach(QQuickItem* item, items) { + QtJson::JsonObject outitem; + qulonglong oid = player->registerObject(item); + outitem["oid"] = oid; + outitem["viewid"] = viewid; + QObject * itemObject = dynamic_cast(item); + if (itemObject) { + const QMetaObject * mo = itemObject->metaObject(); + QStringList classes; + while (mo) { + classes << mo->className(); + mo = mo->superClass(); + } + outitem["classes"] = classes; + outitem["path"] = objectPath(itemObject); + } + if (recursive) { + dump_quick_items(player, item->childItems(), viewid, recursive, outitem); + } + outitems << outitem; + } + out["items"] = outitems; +#else + (void)player; + (void)items; + (void)viewid; + (void)recursive; + (void)out; +#endif +} + +QtJson::JsonObject Player::quick_item_children(const QtJson::JsonObject & command) { + QtJson::JsonObject result; +#ifdef QT_QUICK_LIB + QuickItemLocatorContext ctx(this, command, "oid"); + if (ctx.hasError()) { return ctx.lastError; } + + bool recursive = command["recursive"].toBool(); + + dump_quick_items(this, ctx.item->childItems(), ctx.id, recursive, result); +#else + result = createQtQuickOnlyError(); +#endif + return result; +} + void Player::_model_item_action(const QString & action, QAbstractItemView * widget, const QModelIndex & index) { diff --git a/server/libFunq/player.h b/server/libFunq/player.h index fc596919..c0c781c4 100644 --- a/server/libFunq/player.h +++ b/server/libFunq/player.h @@ -111,6 +111,7 @@ public slots: QtJson::JsonObject quick_item_find(const QtJson::JsonObject & command); QtJson::JsonObject quick_item_click(const QtJson::JsonObject & command); + QtJson::JsonObject quick_item_children(const QtJson::JsonObject & command); protected: QtJson::JsonObject createQtQuickOnlyError() { diff --git a/tests-functionnal/base.py b/tests-functionnal/base.py index 9a752152..a4747a15 100644 --- a/tests-functionnal/base.py +++ b/tests-functionnal/base.py @@ -44,3 +44,7 @@ def start_dialog(self, btn_name): def get_status_text(self): return self.funq.widget(path='mainWindow::statusBar::QLabel').properties()['text'] + + +class QmlAppTestCase(FunqTestCase): + __app_config_name__ = 'qml_app_test' diff --git a/tests-functionnal/funq-test-qml-app/CMakeLists.txt b/tests-functionnal/funq-test-qml-app/CMakeLists.txt new file mode 100644 index 00000000..d32e7cde --- /dev/null +++ b/tests-functionnal/funq-test-qml-app/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.14) + +# Set the project name and target +project(funq-test-qml-app) + +# Specify C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Set a default build type if none is specified (important for CI) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() + +# Find the appropriate version of Qt (either Qt5 or Qt6) +find_package(Qt6 QUIET COMPONENTS Widgets Quick Qml) +if (NOT Qt6_FOUND) + find_package(Qt5 REQUIRED COMPONENTS Widgets Quick Qml) + set(QT_VERSION_MAJOR 5) +else() + set(QT_VERSION_MAJOR 6) +endif() + +# Set the output directory for the executable +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/) + +# Add the main source file +add_executable(${PROJECT_NAME} main.cpp) + +# Add the resource file and link against the appropriate Qt libraries +if (QT_VERSION_MAJOR EQUAL 5) + qt5_add_resources(QT_RESOURCES resources.qrc) + target_link_libraries(${PROJECT_NAME} Qt5::Widgets Qt5::Quick Qt5::Qml) +elseif (QT_VERSION_MAJOR EQUAL 6) + qt_add_resources(QT_RESOURCES resources.qrc) + target_link_libraries(${PROJECT_NAME} Qt6::Widgets Qt6::Quick Qt6::Qml) +endif() + +# Include the generated resource files in the target +target_sources(${PROJECT_NAME} PRIVATE ${QT_RESOURCES}) + +# Platform-specific settings +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + # Windows-specific settings + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/Release) +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + # macOS-specific settings (e.g., bundle into an .app) + set(MACOSX_BUNDLE TRUE) + set_target_properties(${PROJECT_NAME} PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER com.yourdomain.funq-test-qml-app + MACOSX_BUNDLE_BUNDLE_NAME "FunqTestQMLApp" + MACOSX_BUNDLE_BUNDLE_VERSION "1.0" + MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0" + ) +elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") + # Linux-specific settings + set(CMAKE_INSTALL_RPATH "$ORIGIN") +endif() + +# Installation rules (optional, for installation packaging) +install(TARGETS ${PROJECT_NAME} + BUNDLE DESTINATION . + RUNTIME DESTINATION bin +) diff --git a/tests-functionnal/funq-test-qml-app/main.cpp b/tests-functionnal/funq-test-qml-app/main.cpp new file mode 100644 index 00000000..0a9de1ed --- /dev/null +++ b/tests-functionnal/funq-test-qml-app/main.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQuickView * view = new QQuickView(); + view->setSource(QUrl("qrc:///qml/children.qml")); + view->show(); + return app.exec(); +} diff --git a/tests-functionnal/funq-test-qml-app/qml/children.qml b/tests-functionnal/funq-test-qml-app/qml/children.qml new file mode 100644 index 00000000..06c5183c --- /dev/null +++ b/tests-functionnal/funq-test-qml-app/qml/children.qml @@ -0,0 +1,32 @@ +import QtQuick 2.0 + +Rectangle { + id: main + width: 600 + height: 600 + color: "red" + + Text { + text: "Parent" + } + + Rectangle { + width: 400 + height: 400 + color: "green" + anchors.centerIn: parent + Text { + text: "Child" + } + + Rectangle { + width: 200 + height: 200 + color: "blue" + anchors.centerIn: parent + Text { + text: "Grandchild" + } + } + } +} diff --git a/tests-functionnal/funq-test-qml-app/resources.qrc b/tests-functionnal/funq-test-qml-app/resources.qrc new file mode 100644 index 00000000..b4f85781 --- /dev/null +++ b/tests-functionnal/funq-test-qml-app/resources.qrc @@ -0,0 +1,5 @@ + + + qml/children.qml + + diff --git a/tests-functionnal/funq.conf b/tests-functionnal/funq.conf index f53f4ad9..011908fb 100644 --- a/tests-functionnal/funq.conf +++ b/tests-functionnal/funq.conf @@ -6,3 +6,11 @@ aliases = app_test.alias executable_stdout = NULL executable_stderr = NULL screenshot_on_error = 1 + +[qml_app_test] +executable = ./funq-test-qml-app/funq-test-qml-app +funq_port = 9999 +cwd = . +executable_stdout = NULL +executable_stderr = NULL +screenshot_on_error = 1 diff --git a/tests-functionnal/test_qml_item_children.py b/tests-functionnal/test_qml_item_children.py new file mode 100644 index 00000000..bb815385 --- /dev/null +++ b/tests-functionnal/test_qml_item_children.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +# Copyright: SCLE SFE +# Contributor: Rafael de Lucena Valle +# +# 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. + +from base import QmlAppTestCase +from funq.client import FunqClient + + +class TestQmlItemChilden(QmlAppTestCase): + + def get_children_by_property(self, prop, recursive=False): + self.funq = FunqClient() + widget = self.funq.active_widget() + item = widget.item(id='main') + children = item.children(recursive=recursive) + return [child.properties().get(prop) + for child in children.iter() if prop in child.properties()] + + def test_non_recursive_children(self): + children = self.get_children_by_property('text') + self.assertIn('Parent', children) + + def test_recursive_children(self): + children = self.get_children_by_property('text', recursive=True) + self.assertIn('Parent', children) + self.assertIn('Child', children) + self.assertIn('Grandchild', children)