diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 6150fece..09aec96c 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -89,6 +89,8 @@ set(SOURCES src/Scene/Mock/MockScene.h src/Scene/Mock/MockItem.cpp src/Scene/Mock/MockItem.h + src/Scene/Qt/QtEventFilter.cpp + src/Scene/Qt/QtEventFilter.h src/Scene/Qt/QtEvents.cpp src/Scene/Qt/QtEvents.h src/Scene/Qt/QtItem.cpp @@ -114,7 +116,7 @@ source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX source FILES ${SOURCES}) # # Qt MOC Files # -cmake_language(CALL "qt${SPIX_QT_MAJOR}_wrap_cpp" MOC_FILES "include/Spix/QtQmlBot.h") +cmake_language(CALL "qt${SPIX_QT_MAJOR}_wrap_cpp" MOC_FILES "include/Spix/QtQmlBot.h" "src/Scene/Qt/QtEventFilter.h") # diff --git a/lib/src/Data/ItemPath.cpp b/lib/src/Data/ItemPath.cpp index ce57301c..e0541753 100644 --- a/lib/src/Data/ItemPath.cpp +++ b/lib/src/Data/ItemPath.cpp @@ -24,6 +24,20 @@ ItemPath::ItemPath(const std::string& path) while (pathss) { std::string component; + if (pathss.peek() == '\"') { + getline(pathss, component, '\"'); + getline(pathss, component, '\"'); + component = '\"' + component + '\"'; + m_components.push_back(std::move(component)); + } + + if (pathss.peek() == '(') { + getline(pathss, component, '('); + getline(pathss, component, ')'); + component = '(' + component + ')'; + m_components.push_back(std::move(component)); + } + getline(pathss, component, '/'); if (component.length() > 0) { m_components.push_back(std::move(component)); diff --git a/lib/src/Scene/Qt/QtEventFilter.cpp b/lib/src/Scene/Qt/QtEventFilter.cpp new file mode 100644 index 00000000..a0ef8a69 --- /dev/null +++ b/lib/src/Scene/Qt/QtEventFilter.cpp @@ -0,0 +1,43 @@ +// +// Created by sebastian on 28.02.24. +// + +#include +#include + +#include "QtEventFilter.h" +#include +namespace spix { + +QtEventFilter::QtEventFilter(QObject* parent) +: QObject(parent) +{ +} + +bool QtEventFilter::eventFilter(QObject* obj, QEvent* event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->modifiers() == QFlags(Qt::ShiftModifier | Qt::ControlModifier)) { + emit pickerModeEntered(keyEvent); + return false; + } + } else if (event->type() == QEvent::KeyRelease) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == 16777249 || keyEvent->key() == 16777248) { + emit pickerModeExited(keyEvent); + return false; + } + } else if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseClick = static_cast(event); + if (mouseClick->modifiers() == QFlags(Qt::ShiftModifier | Qt::ControlModifier)) { + emit pickClick(mouseClick); + return true; + } + return false; + } + + return QObject::eventFilter(obj, event); +} + +} // namespace spix \ No newline at end of file diff --git a/lib/src/Scene/Qt/QtEventFilter.h b/lib/src/Scene/Qt/QtEventFilter.h new file mode 100644 index 00000000..75ddd1c3 --- /dev/null +++ b/lib/src/Scene/Qt/QtEventFilter.h @@ -0,0 +1,26 @@ +// +// Created by sebastian on 28.02.24. +// + +#pragma once +#include +#include +#include + +namespace spix { + +class QtEventFilter : public QObject { + Q_OBJECT +public: + QtEventFilter(QObject* parent); + +signals: + void pickerModeEntered(QKeyEvent* event); + void pickerModeExited(QKeyEvent* event); + void pickClick(QMouseEvent* event); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; +}; + +} // namespace spix \ No newline at end of file diff --git a/lib/src/Scene/Qt/QtItemTools.cpp b/lib/src/Scene/Qt/QtItemTools.cpp index a1bf8b0f..990c0a0b 100644 --- a/lib/src/Scene/Qt/QtItemTools.cpp +++ b/lib/src/Scene/Qt/QtItemTools.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include namespace spix { @@ -62,7 +64,39 @@ QString GetObjectName(QObject* object) return object->objectName(); } -QObject* FindChildItem(QObject* object, const QString& name) +QString PropertValueByObject(QObject* object, QString propertyName) +{ + if (object == nullptr) { + return ""; + } + + auto property = object->property(propertyName.toStdString().c_str()); + auto objectVisible = object->property("visible"); + + if (property.isNull() || objectVisible.isNull()) { + return ""; + } + + if (property.isValid() && objectVisible.toBool()) { + return property.toString(); + } + + return ""; +} + +QString TypeByObject(QObject* object) +{ + if (object == nullptr) { + return ""; + } + + auto typeName = QString(object->metaObject()->className()); + typeName.replace(QRegularExpression("QQuick|_QML.*"), ""); + return typeName; +} + +QObject* FindChildItem(QObject* object, const QString& name, const std::optional& propertyName = {}, + const std::optional& propertyValue = {}, const std::optional& type = {}) { if (object == nullptr) { return nullptr; @@ -75,8 +109,27 @@ QObject* FindChildItem(QObject* object, const QString& name) if (GetObjectName(child) == name) { return child; } - if (auto item = FindChildItem(child, name)) { - return item; + if (propertyName.has_value() && propertyValue.has_value()) { + if (PropertValueByObject(child, propertyName.value()) == propertyValue.value()) { + return child; + } + + if (auto item = FindChildItem(child, name, propertyName.value(), propertyValue.value(), {})) { + return item; + } + } else if (type.has_value()) { + if (TypeByObject(child) == type.value()) { + return child; + } + + if (auto item = FindChildItem(child, name, {}, {}, type)) { + return item; + } + + } else { + if (auto item = FindChildItem(child, name)) { + return item; + } } } } else { @@ -85,8 +138,19 @@ QObject* FindChildItem(QObject* object, const QString& name) if (GetObjectName(child) == name) { return child; } - if (auto item = FindChildItem(child, name)) { - return item; + + if (propertyValue.has_value() && propertyName.has_value()) { + if (auto item = FindChildItem(child, name, propertyName.value(), propertyValue.value(), {})) { + return item; + } + } else if (type.has_value()) { + if (auto item = FindChildItem(child, name, {}, {}, type)) { + return item; + } + } else { + if (auto item = FindChildItem(child, name)) { + return item; + } } } } @@ -94,6 +158,34 @@ QObject* FindChildItem(QObject* object, const QString& name) return nullptr; } +QVector FindChildItems(QObject* object, const std::optional& type = {}) +{ + QVector result = {}; + if (object == nullptr) { + return {}; + } + + using Index = QObjectList::size_type; + if (auto qquickitem = qobject_cast(object)) { + for (Index i = 0; i < qquickitem->childItems().size(); ++i) { + auto child = qquickitem->childItems().at(i); + result = result + FindChildItems(child, type); + if (type.has_value() && TypeByObject(child) == type.value()) { + result.push_back(child); + } + } + } else { + for (Index i = 0; i < object->children().size(); ++i) { + auto child = object->children().at(i); + result = result + FindChildItems(child, type); + if (type.has_value() && TypeByObject(child) == type.value()) { + result.push_back(child); + } + } + } + return result; +} + QGenericReturnArgument GetReturnArgForQMetaType(int type, QMLReturnVariant& retVar) { switch (type) { diff --git a/lib/src/Scene/Qt/QtItemTools.h b/lib/src/Scene/Qt/QtItemTools.h index dce53efc..0cad3866 100644 --- a/lib/src/Scene/Qt/QtItemTools.h +++ b/lib/src/Scene/Qt/QtItemTools.h @@ -12,6 +12,7 @@ #include #include #include +#include class QString; @@ -25,6 +26,8 @@ QQuickItem* RepeaterChildAtIndex(QQuickItem* repeater, int index); QQuickItem* RepeaterChildWithName(QQuickItem* repeater, const QString& name); QString GetObjectName(QObject* object); +QString PropertValueByObject(QObject* object, QString propertyName); +QString TypeByObject(QObject* object); /** * @brief Find a child object with the given name. @@ -33,12 +36,21 @@ QString GetObjectName(QObject* object); * encounters a `QQuickItem`, it no longer iterates over the object's * `children()`, but rather its `childItems()`. */ -QObject* FindChildItem(QObject* object, const QString& name); +QObject* FindChildItem(QObject* object, const QString& name, const std::optional& propertyName, + const std::optional& propertyValue, const std::optional& type); +QVector FindChildItems(QObject* object, const std::optional& type); + +template +T FindChildItem(QObject* object, const QString& name, const std::optional& propertyName, + const std::optional& propertyValue, const std::optional& type) +{ + return qobject_cast(FindChildItem(object, name, propertyName, propertyValue, type)); +} template T FindChildItem(QObject* object, const QString& name) { - return qobject_cast(FindChildItem(object, name)); + return qobject_cast(FindChildItem(object, name, {}, {}, {})); } using QMLReturnVariant = std::variant; diff --git a/lib/src/Scene/Qt/QtScene.cpp b/lib/src/Scene/Qt/QtScene.cpp index d320e7d7..9daad378 100644 --- a/lib/src/Scene/Qt/QtScene.cpp +++ b/lib/src/Scene/Qt/QtScene.cpp @@ -5,6 +5,7 @@ ****/ #include "QtScene.h" +#include "QtEventFilter.cpp" #include #include @@ -19,6 +20,67 @@ namespace { +QString getNameForObject(QObject* object) +{ + QString name; + if (spix::qt::PropertValueByObject(object, QString::fromStdString("text")) != "") { + name = "\"" + spix::qt::PropertValueByObject(object, QString::fromStdString("text")) + "\""; + } else if (spix::qt::PropertValueByObject(object, QString::fromStdString("source")) != "") { + name = "(source=" + spix::qt::PropertValueByObject(object, QString::fromStdString("source")) + ")"; + } else if (spix::qt::GetObjectName(object) != "") { + name = spix::qt::GetObjectName(object); + } else { + name = "#" + spix::qt::TypeByObject(object); + } + + return name; +} + +spix::ItemPath ItemPathForObject(QObject* object) +{ + auto currentItem = object; + QString path = ""; + + while (currentItem != nullptr) { + // take object name if given + auto token = getNameForObject(currentItem); + int sameNameNumber = 0; + + const auto currentQuickItem = qobject_cast(currentItem); + + if (currentItem->parent() != nullptr) { + auto siblings = currentItem->parent()->children(); + for (const auto child : siblings) { + if (token == getNameForObject(child)) { + sameNameNumber++; + } + } + } else if (currentQuickItem != nullptr && currentQuickItem->parentItem()) { + auto siblings = currentQuickItem->parentItem()->children(); + for (const auto child : siblings) { + if (token == getNameForObject(child)) { + sameNameNumber++; + } + } + } + + if ((currentItem->parent() == nullptr) + && (currentQuickItem != nullptr && currentQuickItem->parentItem() != nullptr)) { + currentItem = currentQuickItem->parentItem(); + } else { + currentItem = currentItem->parent(); + } + + if (sameNameNumber > 1) { + continue; + } + // add id to front + path = token + "/" + path; + } + + return spix::ItemPath(path.toStdString()); +} + QQuickWindow* getQQuickWindowWithName(const std::string& name) { QString qtName = QString::fromStdString(name); @@ -48,6 +110,7 @@ QQuickItem* getQQuickItemWithRoot(const spix::ItemPath& path, QObject* root) auto rootClassName = root->metaObject()->className(); auto itemName = path.rootComponent(); QQuickItem* subItem = nullptr; + QVector subItems = {}; if (itemName.compare(0, 1, ".") == 0) { auto propertyName = itemName.substr(1); @@ -55,13 +118,45 @@ QQuickItem* getQQuickItemWithRoot(const spix::ItemPath& path, QObject* root) if (propertyValue.isValid()) { subItem = propertyValue.value(); } - } else { - if (rootClassName == spix::qt::repeater_class_name) { - QQuickItem* repeater = static_cast(root); - subItem = spix::qt::RepeaterChildWithName(repeater, QString::fromStdString(itemName)); - } else { - subItem = spix::qt::FindChildItem(root, itemName.c_str()); + + } else if (itemName.compare(0, 1, "\"") == 0) { + // remove "" + size_t found = itemName.find("\""); + auto searchText = itemName.substr(found + 1, itemName.length() - 2); + + subItem = spix::qt::FindChildItem( + root, itemName.c_str(), QString::fromStdString("text"), QString::fromStdString(searchText), {}); + + } else if (itemName.compare(0, 1, "#") == 0) { + // remove # + size_t found = itemName.find("#"); + auto type = QString::fromStdString(itemName.substr(found + 1)); + + subItems = spix::qt::FindChildItems(root, type); + + for (const auto item : subItems) { + auto foundItem = getQQuickItemWithRoot(path.subPath(1), item); + if (foundItem != nullptr) { + return foundItem; + } } + } else if (itemName.compare(0, 1, "(") == 0) { + // remove () + size_t foundBracketSign = itemName.find('('); + auto searchText = itemName.substr(foundBracketSign + 1, itemName.length() - 2); + + // Split in to property and value + size_t foundEqualSign = searchText.find('='); + auto property = QString::fromStdString(searchText.substr(0, foundEqualSign)); + auto value = QString::fromStdString(searchText.substr(foundEqualSign + 1)); + + subItem = spix::qt::FindChildItem(root, itemName.c_str(), property, value, {}); + + } else if (rootClassName == spix::qt::repeater_class_name) { + QQuickItem* repeater = static_cast(root); + subItem = spix::qt::RepeaterChildWithName(repeater, QString::fromStdString(itemName)); + } else { + subItem = spix::qt::FindChildItem(root, itemName.c_str()); } if (path.length() == 1) { @@ -94,6 +189,49 @@ QQuickItem* getQQuickItemAtPath(const spix::ItemPath& path) namespace spix { +/** +Create a QtScene with an EventFilter +**/ +QtScene::QtScene() +{ + m_filter = new QtEventFilter(qGuiApp); + + QObject::connect(qGuiApp, &QGuiApplication::focusWindowChanged, qGuiApp, [this](QWindow* window) { + if (m_eventFilterInstalled == false) { + m_eventFilterInstalled = true; + window->installEventFilter(m_filter); + + QObject::connect(m_filter, &QtEventFilter::pickerModeEntered, m_filter, + []() { QGuiApplication::setOverrideCursor(QCursor(Qt::CrossCursor)); }); + + QObject::connect(m_filter, &QtEventFilter::pickerModeExited, m_filter, + []() { QGuiApplication::restoreOverrideCursor(); }); + + auto quickWindow = qobject_cast(window); + QObject::connect(m_filter, &QtEventFilter::pickClick, m_filter, [this, quickWindow](QMouseEvent* event) { + int bestCanidate = -1; + bool parentIsGoodCandidate = true; + auto objects + = recursiveItemsAt(quickWindow->contentItem(), event->pos(), bestCanidate, parentIsGoodCandidate); + + if (objects.size() == 1) { + auto quickItem = qobject_cast(objects[0]); + quickItem->setOpacity(0.5); + auto itemPath = ItemPathForObject(quickItem); + + auto newPath = shortPath(itemPath, quickItem); + qDebug() << "Path: " << QString::fromUtf8(newPath.string().c_str()); + } + }); + } + }); +} + +QtScene::~QtScene() +{ + delete m_filter; +} + std::unique_ptr QtScene::itemAtPath(const ItemPath& path) { auto windowName = path.rootComponent(); @@ -114,6 +252,34 @@ std::unique_ptr QtScene::itemAtPath(const ItemPath& path) return std::make_unique(item); } +spix::ItemPath QtScene::shortPath(ItemPath oldPath, QQuickItem* oldItem) +{ + spix::ItemPath newItemPath; + QQuickItem* newItem; + std::string tempNewItemPath = oldPath.rootComponent(); + + auto partsPath = oldPath.components(); + + for (auto part = partsPath.rbegin(); part != std::prev(partsPath.rend()); ++part) { + + size_t found = tempNewItemPath.find("/"); + if (found != std::string::npos) { + auto subfix = tempNewItemPath.substr(found + 1, tempNewItemPath.length()); + tempNewItemPath = oldPath.rootComponent().append("/").append(part->c_str()).append("/").append(subfix); + } else { + tempNewItemPath.append("/").append(part->c_str()); + } + + auto newItem = getQQuickItemAtPath(ItemPath(tempNewItemPath)); + + if (newItem == oldItem) { + newItemPath = ItemPath(tempNewItemPath); + return newItemPath; + } + } + return oldPath; +} + Events& QtScene::events() { return m_events; @@ -169,4 +335,106 @@ std::string QtScene::takeScreenshotAsBase64(const ItemPath& targetItem) return byteArray.toBase64().toStdString(); } +bool QtScene::itemHasContents(QQuickItem* item) +{ + return item->flags().testFlag(QQuickItem::ItemHasContents); +} + +bool QtScene::isGoodCandidateItem(QQuickItem* item, bool ignoreItemHasContents = false) +{ + return !(!item->isVisible() || qFuzzyCompare(item->opacity() + qreal(1.0), qreal(1.0)) + || (!ignoreItemHasContents && !itemHasContents(item))); +} + +QRectF QtScene::combinedChildrenRect(QQuickItem* object) const +{ + auto rect = object->childrenRect(); + + for (const auto child : object->childItems()) { + // Get Rect from Childitems + auto childRect = child->childrenRect(); + + // Get Global positon of childRect + QPointF childGlobalPos = child->mapToScene(QPointF(0, 0)); + + // Convert global position to local coordinates of the parent object + QPointF localChildPos = object->mapFromScene(childGlobalPos); + + // Adjust childRect to be in local coordinates of the parent object + childRect.moveTopLeft(localChildPos.toPoint()); + + // Adding the childRect to the rect + rect = rect.united(childRect); + } + + return rect; +} + +/** + Search for best matching Object on the Position. +**/ +ObjectIds QtScene::recursiveItemsAt( + QQuickItem* parent, const QPointF& pos, int& bestCandidate, bool parentIsGoodCandidate) +{ + Q_ASSERT(parent); // nulll check + ObjectIds objects; + + bestCandidate = -1; + if (parentIsGoodCandidate) { + // inherit the parent item opacity when looking for a good candidate item + // i.e. QQuickItem::isVisible is taking the parent into account already, but + // the opacity doesn't - we have to do this manually + // Yet we have to ignore ItemHasContents apparently, as the QQuickRootItem + // at least seems to not have this flag set. + parentIsGoodCandidate = isGoodCandidateItem(parent, true); + } + + // sorting based on z positon + auto childItems = parent->childItems(); + std::stable_sort( + childItems.begin(), childItems.end(), [](QQuickItem* lhs, QQuickItem* rhs) { return lhs->z() < rhs->z(); }); + + for (int i = childItems.size() - 1; i >= 0; --i) { // backwards to match z order + const auto child = childItems.at(i); + // position of child + const auto requestedPoint = parent->mapToItem(child, pos); + + if (!child->childItems().isEmpty() + && (child->contains(requestedPoint) || combinedChildrenRect(child).contains(requestedPoint))) { + const int count = objects.count(); + int bc; // possibly better candidate among subChildren + + objects << recursiveItemsAt(child, requestedPoint, bc, parentIsGoodCandidate); + + if (bestCandidate == -1 && parentIsGoodCandidate && bc != -1) { + bestCandidate = count + bc; + } + } + + if (child->contains(requestedPoint)) { + if (bestCandidate == -1 && parentIsGoodCandidate && isGoodCandidateItem(child)) { + bestCandidate = objects.count(); + } + objects << child; + } + + if (bestCandidate != -1) { + break; + } + } + + if (bestCandidate == -1 && parentIsGoodCandidate && itemHasContents(parent)) { + bestCandidate = objects.count(); + } + + objects << parent; + + if (bestCandidate != -1) { + objects = ObjectIds() << objects[bestCandidate]; + bestCandidate = 0; + } + + return objects; +} + } // namespace spix diff --git a/lib/src/Scene/Qt/QtScene.h b/lib/src/Scene/Qt/QtScene.h index 18978451..81f5159c 100644 --- a/lib/src/Scene/Qt/QtScene.h +++ b/lib/src/Scene/Qt/QtScene.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -17,12 +18,18 @@ class QQuickWindow; namespace spix { +class QtEventFilter; class ItemPath; +using ObjectIds = QVector; + class QtScene : public Scene { public: + QtScene(); + ~QtScene(); // Request objects std::unique_ptr itemAtPath(const ItemPath& path) override; + ItemPath shortPath(ItemPath oldPath, QQuickItem* itemPath); // Events Events& events() override; @@ -30,9 +37,15 @@ class QtScene : public Scene { // Tasks void takeScreenshot(const ItemPath& targetItem, const std::string& filePath) override; std::string takeScreenshotAsBase64(const ItemPath& targetItem); + ObjectIds recursiveItemsAt(QQuickItem* parent, const QPointF& pos, int& bestCandidate, bool parentIsGoodCandidate); + QRectF combinedChildrenRect(QQuickItem* object) const; private: QtEvents m_events; + bool m_eventFilterInstalled = false; + QtEventFilter* m_filter; + bool itemHasContents(QQuickItem* item); + bool isGoodCandidateItem(QQuickItem* item, bool ignoreItemHasContents); }; } // namespace spix