diff --git a/.gitignore b/.gitignore index 3d4a8526..193a78fd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ build dep-libs deps deps-install + +.cache +compile_commands.json diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index a8c37ad8..dc2fda66 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -94,6 +94,8 @@ set(SOURCES src/Scene/Qt/FindQtItem.h src/Scene/Qt/QtEvents.cpp src/Scene/Qt/QtEvents.h + src/Scene/Qt/QtEventFilter.cpp + src/Scene/Qt/QtEventFilter.h src/Scene/Qt/QtItem.cpp src/Scene/Qt/QtItem.h src/Scene/Qt/QtItemTools.cpp @@ -118,7 +120,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/Scene/Qt/QtEventFilter.cpp b/lib/src/Scene/Qt/QtEventFilter.cpp new file mode 100644 index 00000000..3ab10ff1 --- /dev/null +++ b/lib/src/Scene/Qt/QtEventFilter.cpp @@ -0,0 +1,39 @@ +#include "QtEventFilter.h" +#include +#include + + + +namespace spix{ + +QtEventFilter::QtEventFilter(QObject* parent) : QObject(parent) {} + +bool QtEventFilter::eventFilter(QObject* obj, QEvent* e){ + if(e->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(e); + + if(keyEvent->modifiers() == QFlags(Qt::ShiftModifier | Qt::ControlModifier)) { + emit pickerModeEntered(keyEvent); + return false; + } + } else if(e->type() == QEvent::KeyRelease) { + QKeyEvent* keyEvent = static_cast(e); + + if(keyEvent->key() == Qt::Key::Key_Shift || keyEvent->key() == Qt::Key::Key_Control){ + emit pickerModeExited(keyEvent); + return false; + } + } else if(e->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseClick = static_cast(e); + if(mouseClick->modifiers() == QFlags(Qt::ShiftModifier | Qt::ControlModifier)) { + emit pickClick(mouseClick); + return true; + } + return false; + } + + return QObject::eventFilter(obj, e); +} + +} // namespace spix + diff --git a/lib/src/Scene/Qt/QtEventFilter.h b/lib/src/Scene/Qt/QtEventFilter.h new file mode 100644 index 00000000..7ea7bc9b --- /dev/null +++ b/lib/src/Scene/Qt/QtEventFilter.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + + + +namespace spix { + +class QtEventFilter : public QObject { + + Q_OBJECT + +public: + QtEventFilter(QObject* parent); + +signals: + void pickerModeEntered(QKeyEvent* e); + void pickerModeExited(QKeyEvent* e); + void pickClick(QMouseEvent* e); + +protected: + bool eventFilter(QObject* obj, QEvent* e) override; + +}; + +} // namespace spix diff --git a/lib/src/Scene/Qt/QtScene.cpp b/lib/src/Scene/Qt/QtScene.cpp index 12431a4c..22a50307 100644 --- a/lib/src/Scene/Qt/QtScene.cpp +++ b/lib/src/Scene/Qt/QtScene.cpp @@ -6,6 +6,8 @@ #include "QtScene.h" #include "FindQtItem.h" +#include "QtEventFilter.h" +#include "Scene/Item.h" #include #include @@ -17,9 +19,156 @@ #include #include #include +#include +#include + + +namespace { + +QString GetObjectFinalName(QObject* obj, bool top){ + auto token = spix::qt::GetObjectName(obj); + + if(obj->inherits("QQuickText")){ + auto var = obj->property("text"); + if(!var.isValid()){ + return token; + } + // adds quotes around the text + auto text = "\"" + var.toString() + "\""; + if(top) return text; + } + + // for certain types we want to return the text + // not adding in now, but can add in later + // if(token.contains("Button")) return text; + // else if(token.contains("Label")) return text; + + return token; +} + +spix::ItemPath ItemPathForObject(QObject* obj){ + auto currentItem = obj; + QString path = ""; + + while(currentItem != nullptr) { + auto token = spix::qt::GetObjectName(currentItem); + int sameNameNumber = 0; + + const auto currentQuickItem = qobject_cast(currentItem); + + if(currentItem->parent() != nullptr){ + auto siblings = currentItem->parent()->children(); + for(const auto child : siblings) { + sameNameNumber += (token == spix::qt::GetObjectName(child)); + } + }else if(currentQuickItem != nullptr && currentQuickItem->parentItem() != nullptr) { + auto siblings = currentQuickItem->parentItem()->children(); + for(const auto child : siblings) { + sameNameNumber += (token == spix::qt::GetObjectName(child)); + } + } + if((currentItem->parent() == nullptr) + && (currentQuickItem != nullptr && currentQuickItem->parentItem() != nullptr)) { + currentItem = currentQuickItem->parentItem(); + } else { + currentItem = currentItem->parent(); + } + + if (sameNameNumber > 1) { + continue; + } + + path = token + "/" + path; + } + + return spix::ItemPath(path.toStdString()); +} + +} // namespace + namespace spix { + +QtScene::QtScene() { + m_filter = new QtEventFilter(qGuiApp); + + QObject::connect(qGuiApp, &QGuiApplication::focusWindowChanged, qGuiApp, [this](QWindow* window) { + if(!m_eventFilterInstalled) { + 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* e){ + int bestCandidate = -1; + bool parentIsGoodCandidate = true; + auto objects = recursiveItemsAt(quickWindow->contentItem(), e->pos(), bestCandidate, parentIsGoodCandidate); + + if(objects.size() == 1) { + auto quickItem = qobject_cast(objects[0]); + // quickItem->setOpacity(0.5); + auto itemPath = ItemPathForObject(quickItem); + + auto newPath = shortPath(itemPath, quickItem); + // prints the path and surrounds it with quotes + qDebug().setAutoInsertSpaces(false); + auto pathString = "\'" + newPath.string() + "\'"; + qDebug() << "Path: " << pathString.c_str(); + } + }); + } + }); +} + +QtScene::~QtScene(){ + delete m_filter; +} + +spix::ItemPath QtScene::shortPath(ItemPath oldPath, QQuickItem* oldItem){ + spix::ItemPath newPath; + + auto components = oldPath.components(); + + // add the first + if(components.size() > 1){ + newPath = spix::ItemPath(components.at(0).string()); + } else { + return oldPath; + } + + for(auto p = components.begin() + 1; p != components.end() - 1; p++){ + // iterate through the middle of the components + auto next = p + 1; + + // item + auto path = spix::ItemPath(newPath.string() + "/" + p->string() + "/" + next->string()); + auto shortPath = spix::ItemPath(newPath.string() + "/" + next->string()); + + auto item = qt::GetQQuickItemAtPath(path); + auto shortItem = qt::GetQQuickItemAtPath(shortPath); + + + if(shortItem == nullptr || item != shortItem){ + // need to add this item to path + newPath = spix::ItemPath(newPath.string() + "/" + p->string()); + } + // otherwise continue without this item + } + + // add the last item + auto name = GetObjectFinalName(oldItem, true); + newPath = spix::ItemPath(newPath.string() + "/" + name.toStdString()); + + return newPath; +} + std::unique_ptr QtScene::itemAtPath(const ItemPath& path) { // Find item within window @@ -87,4 +236,81 @@ 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* obj) const { + auto rect = obj->childrenRect(); + + for(const auto child : obj->childItems()) { + auto childRect = child->childrenRect(); + QPointF childGlobalPos = child->mapToScene(QPointF(0, 0)); + QPointF localChildPos = obj->mapFromScene(childGlobalPos); + childRect.moveTopLeft((localChildPos.toPoint())); + rect = rect.united(childRect); + } + + return rect; +} + +ObjectIds QtScene::recursiveItemsAt(QQuickItem* parent, const QPointF& pos, int& bestCandidate, bool parentIsGoodCandidate){ + Q_ASSERT(parent); + ObjectIds objects; + + bestCandidate = -1; + if(parentIsGoodCandidate) { + parentIsGoodCandidate = isGoodCandidateItem(parent, true); + } + + auto childItems = parent->childItems(); + std::stable_sort(childItems.begin(), childItems.end(), [](QQuickItem* a, QQuickItem* b){ + return a->z() < b->z(); + }); + + + for(int i = childItems.size() - 1; i >= 0; i--) { + const auto child = childItems.at(i); + 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 = 0; + 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 7d7a3761..04b1bf52 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 @@ -16,12 +17,19 @@ 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; @@ -29,9 +37,15 @@ class QtScene : public Scene { // Tasks void takeScreenshot(const ItemPath& targetItem, const std::string& filePath) override; std::string takeScreenshotAsBase64(const ItemPath& targetItem) override; + 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 diff --git a/lib/tests/CMakeLists.txt b/lib/tests/CMakeLists.txt index b68dae95..53b18582 100644 --- a/lib/tests/CMakeLists.txt +++ b/lib/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(TEST_SOURCES unittests/Scene/Qt/QtItem_test.cpp unittests/Scene/Qt/QtItemTools_test.cpp unittests/Scene/Qt/QtTestUtils.h + unittests/Scene/Qt/PickClick_test.cpp unittests/Utils/AnyRpcFunction_test.cpp unittests/Utils/AnyRpcUtils_test.cpp diff --git a/lib/tests/unittests/Scene/Qt/PickClick_test.cpp b/lib/tests/unittests/Scene/Qt/PickClick_test.cpp new file mode 100644 index 00000000..38d9d884 --- /dev/null +++ b/lib/tests/unittests/Scene/Qt/PickClick_test.cpp @@ -0,0 +1,64 @@ +#include "Spix/QtQmlBot.h" +#include + +#include +#include +#include +#include +#include + +class PickClickTest : public ::testing::Test { +protected: + PickClickTest() + : fakeArg {"hello"} + , fakeArgPtr(fakeArg) + , fakeArgList(&fakeArgPtr) + , fakeArgCount(1) + , app(fakeArgCount, fakeArgList) + , engine() + , component(&engine) + , w() + { + w.show(); + w.setObjectName("mainWindow"); + } + + + char fakeArg[50]; + char* fakeArgPtr; + char** fakeArgList; + int fakeArgCount; + QGuiApplication app; + QQmlApplicationEngine engine; + QQmlComponent component; + QQuickWindow w; +}; + + +TEST_F(PickClickTest, CursorShape){ + auto bot = spix::QtQmlBot(); + QTest::qWait(10); + + QTest::keyPress(&w, Qt::Key_Control, Qt::KeyboardModifier::ShiftModifier); + + EXPECT_TRUE(QGuiApplication::overrideCursor() != nullptr); + EXPECT_TRUE(QGuiApplication::overrideCursor()->shape() == Qt::CursorShape::CrossCursor); + + QTest::keyRelease(&w, Qt::Key_Shift); + QTest::keyRelease(&w, Qt::Key_Control); +} + + +TEST_F(PickClickTest, ClickItem){ + auto bot = spix::QtQmlBot(); + auto l = QTest::qWaitForWindowExposed(&w); + w.resize(10, 10); + + testing::internal::CaptureStderr(); + + QTest::mouseClick(&w, Qt::MouseButton::LeftButton, Qt::KeyboardModifier::ShiftModifier | Qt::KeyboardModifier::ControlModifier, QPoint(1, 1)); + + std::string out = testing::internal::GetCapturedStderr(); + + EXPECT_EQ(out, "Path: 'mainWindow'\n"); +}