diff --git a/src/plugins/platforms/wasm/CMakeLists.txt b/src/plugins/platforms/wasm/CMakeLists.txt index d7c96afdaa..7cb073a552 100644 --- a/src/plugins/platforms/wasm/CMakeLists.txt +++ b/src/plugins/platforms/wasm/CMakeLists.txt @@ -32,6 +32,7 @@ qt_internal_add_plugin(QWasmIntegrationPlugin qwasmtheme.cpp qwasmtheme.h qwasmwindow.cpp qwasmwindow.h qwasmwindowclientarea.cpp qwasmwindowclientarea.h + qwasmwindowtreenode.cpp qwasmwindowtreenode.h qwasmwindownonclientarea.cpp qwasmwindownonclientarea.h qwasminputcontext.cpp qwasminputcontext.h qwasmwindowstack.cpp qwasmwindowstack.h diff --git a/src/plugins/platforms/wasm/qwasmcompositor.cpp b/src/plugins/platforms/wasm/qwasmcompositor.cpp index 92e3150738..66eaea62fa 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.cpp +++ b/src/plugins/platforms/wasm/qwasmcompositor.cpp @@ -8,21 +8,9 @@ #include -namespace { +using namespace emscripten; -QWasmWindowStack::PositionPreference positionPreferenceFromWindowFlags(Qt::WindowFlags flags) -{ - if (flags.testFlag(Qt::WindowStaysOnTopHint)) - return QWasmWindowStack::PositionPreference::StayOnTop; - if (flags.testFlag(Qt::WindowStaysOnBottomHint)) - return QWasmWindowStack::PositionPreference::StayOnBottom; - return QWasmWindowStack::PositionPreference::Regular; -} - -} // namespace - -QWasmCompositor::QWasmCompositor(QWasmScreen *screen) - : QObject(screen), m_windowStack(std::bind(&QWasmCompositor::onTopWindowChanged, this)) +QWasmCompositor::QWasmCompositor(QWasmScreen *screen) : QObject(screen) { QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } @@ -37,80 +25,20 @@ QWasmCompositor::~QWasmCompositor() m_isEnabled = false; // prevent frame() from creating a new m_context } -void QWasmCompositor::addWindow(QWasmWindow *window) +void QWasmCompositor::onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindow *window) { - if (m_windowStack.empty()) - window->window()->setFlag(Qt::WindowStaysOnBottomHint); - m_windowStack.pushWindow(window, positionPreferenceFromWindowFlags(window->window()->flags())); - window->requestActivateWindow(); - setActive(window); - - updateEnabledState(); + auto allWindows = screen()->allWindows(); + setEnabled(std::any_of(allWindows.begin(), allWindows.end(), [](QWasmWindow *element) { + return !element->context2d().isUndefined(); + })); + if (changeType == QWasmWindowTreeNodeChangeType::NodeRemoval) + m_requestUpdateWindows.remove(window); } -void QWasmCompositor::removeWindow(QWasmWindow *window) +void QWasmCompositor::setEnabled(bool enabled) { - m_requestUpdateWindows.remove(window); - m_windowStack.removeWindow(window); - if (m_windowStack.topWindow()) { - m_windowStack.topWindow()->requestActivateWindow(); - setActive(m_windowStack.topWindow()); - } - - updateEnabledState(); -} - -void QWasmCompositor::setActive(QWasmWindow *window) -{ - m_activeWindow = window; - - auto it = m_windowStack.begin(); - if (it == m_windowStack.end()) { - return; - } - for (; it != m_windowStack.end(); ++it) { - (*it)->onActivationChanged(*it == m_activeWindow); - } -} - -void QWasmCompositor::updateEnabledState() -{ - m_isEnabled = std::any_of(m_windowStack.begin(), m_windowStack.end(), [](QWasmWindow *window) { - return !window->context2d().isUndefined(); - }); -} - -void QWasmCompositor::raise(QWasmWindow *window) -{ - m_windowStack.raise(window); -} - -void QWasmCompositor::lower(QWasmWindow *window) -{ - m_windowStack.lower(window); -} - -void QWasmCompositor::windowPositionPreferenceChanged(QWasmWindow *window, Qt::WindowFlags flags) -{ - m_windowStack.windowPositionPreferenceChanged(window, positionPreferenceFromWindowFlags(flags)); -} - -QWindow *QWasmCompositor::windowAt(QPoint targetPointInScreenCoords, int padding) const -{ - const auto found = std::find_if( - m_windowStack.begin(), m_windowStack.end(), - [padding, &targetPointInScreenCoords](const QWasmWindow *window) { - const QRect geometry = window->windowFrameGeometry().adjusted(-padding, -padding, - padding, padding); - - return window->isVisible() && geometry.contains(targetPointInScreenCoords); - }); - return found != m_windowStack.end() ? (*found)->window() : nullptr; -} - -QWindow *QWasmCompositor::keyWindow() const -{ - return m_activeWindow ? m_activeWindow->window() : nullptr; + m_isEnabled = enabled; } void QWasmCompositor::requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType) @@ -159,7 +87,6 @@ void QWasmCompositor::deliverUpdateRequests() // update type: QWindow subclasses expect that requested and delivered updateRequests matches // exactly. m_inDeliverUpdateRequest = true; - for (auto it = requestUpdateWindows.constBegin(); it != requestUpdateWindows.constEnd(); ++it) { auto *window = it.key(); UpdateRequestDeliveryType updateType = it.value(); @@ -200,15 +127,8 @@ void QWasmCompositor::frame(const QList &windows) if (!m_isEnabled || !screen()) return; - std::for_each(windows.begin(), windows.end(), [](QWasmWindow *window) { window->paint(); }); -} - -void QWasmCompositor::onTopWindowChanged() -{ - constexpr int zOrderForElementInFrontOfScreen = 3; - int z = zOrderForElementInFrontOfScreen; - std::for_each(m_windowStack.rbegin(), m_windowStack.rend(), - [&z](QWasmWindow *window) { window->setZOrder(z++); }); + for (QWasmWindow *window : windows) + window->paint(); } QWasmScreen *QWasmCompositor::screen() diff --git a/src/plugins/platforms/wasm/qwasmcompositor.h b/src/plugins/platforms/wasm/qwasmcompositor.h index 7d3ea52b39..182ea2b167 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.h +++ b/src/plugins/platforms/wasm/qwasmcompositor.h @@ -15,6 +15,8 @@ QT_BEGIN_NAMESPACE class QWasmWindow; class QWasmScreen; +enum class QWasmWindowTreeNodeChangeType; + class QWasmCompositor final : public QObject { Q_OBJECT @@ -22,42 +24,29 @@ public: QWasmCompositor(QWasmScreen *screen); ~QWasmCompositor() final; - void addWindow(QWasmWindow *window); - void removeWindow(QWasmWindow *window); - void setVisible(QWasmWindow *window, bool visible); - void setActive(QWasmWindow *window); - void raise(QWasmWindow *window); - void lower(QWasmWindow *window); - void windowPositionPreferenceChanged(QWasmWindow *window, Qt::WindowFlags flags); - QWindow *windowAt(QPoint globalPoint, int padding = 0) const; - QWindow *keyWindow() const; + void onScreenDeleting(); QWasmScreen *screen(); + void setEnabled(bool enabled); enum UpdateRequestDeliveryType { ExposeEventDelivery, UpdateRequestDelivery }; void requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType = ExposeEventDelivery); void handleBackingStoreFlush(QWindow *window); + void onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindow *window); private: void frame(const QList &windows); - void onTopWindowChanged(); - void deregisterEventHandlers(); void requestUpdate(); void deliverUpdateRequests(); void deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType); - void updateEnabledState(); - - QWasmWindowStack m_windowStack; - QWasmWindow *m_activeWindow = nullptr; - bool m_isEnabled = true; QMap m_requestUpdateWindows; int m_requestAnimationFrameId = -1; diff --git a/src/plugins/platforms/wasm/qwasmcssstyle.cpp b/src/plugins/platforms/wasm/qwasmcssstyle.cpp index 6ac4c8d884..e0e1a99f48 100644 --- a/src/plugins/platforms/wasm/qwasmcssstyle.cpp +++ b/src/plugins/platforms/wasm/qwasmcssstyle.cpp @@ -35,6 +35,11 @@ const char *Style = R"css( background-color: lightgray; } +.qt-window-contents { + overflow: hidden; + position: relative; +} + .qt-window.transparent-for-input { pointer-events: none; } @@ -135,7 +140,7 @@ const char *Style = R"css( padding-bottom: 4px; } -.qt-window.has-border .title-bar { +.qt-window.has-border > .title-bar { display: flex; } diff --git a/src/plugins/platforms/wasm/qwasmscreen.cpp b/src/plugins/platforms/wasm/qwasmscreen.cpp index eac6ce0332..f338a7541e 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.cpp +++ b/src/plugins/platforms/wasm/qwasmscreen.cpp @@ -199,12 +199,18 @@ void QWasmScreen::resizeMaximizedWindows() QWindow *QWasmScreen::topWindow() const { - return m_compositor->keyWindow(); + return activeChild() ? activeChild()->window() : nullptr; } QWindow *QWasmScreen::topLevelAt(const QPoint &p) const { - return m_compositor->windowAt(p); + const auto found = + std::find_if(childStack().begin(), childStack().end(), [&p](const QWasmWindow *window) { + const QRect geometry = window->windowFrameGeometry(); + + return window->isVisible() && geometry.contains(p); + }); + return found != childStack().end() ? (*found)->window() : nullptr; } QPointF QWasmScreen::mapFromLocal(const QPointF &p) const @@ -232,6 +238,18 @@ void QWasmScreen::setGeometry(const QRect &rect) resizeMaximizedWindows(); } +void QWasmScreen::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + Q_UNUSED(parent); + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && childStack().size() == 1) { + child->window()->setFlag(Qt::WindowStaysOnBottomHint); + } + QWasmWindowTreeNode::onSubtreeChanged(changeType, parent, child); + m_compositor->onWindowTreeChanged(changeType, child); +} + void QWasmScreen::updateQScreenAndCanvasRenderSize() { // The HTML canvas has two sizes: the CSS size and the canvas render size. @@ -305,4 +323,27 @@ void QWasmScreen::installCanvasResizeObserver() resizeObserver.call("observe", m_shadowContainer); } +emscripten::val QWasmScreen::containerElement() +{ + return m_shadowContainer; +} + +QWasmWindowTreeNode *QWasmScreen::parentNode() +{ + return nullptr; +} + +QList QWasmScreen::allWindows() +{ + QList windows; + for (auto *child : childStack()) { + QWindowList list = child->window()->findChildren(Qt::FindChildrenRecursively); + std::transform( + list.begin(), list.end(), std::back_inserter(windows), + [](const QWindow *window) { return static_cast(window->handle()); }); + windows.push_back(child); + } + return windows; +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmscreen.h b/src/plugins/platforms/wasm/qwasmscreen.h index 633cf853f7..47ce11495f 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.h +++ b/src/plugins/platforms/wasm/qwasmscreen.h @@ -6,6 +6,8 @@ #include "qwasmcursor.h" +#include "qwasmwindowtreenode.h" + #include #include @@ -23,7 +25,7 @@ class QWasmCompositor; class QWasmDeadKeySupport; class QOpenGLContext; -class QWasmScreen : public QObject, public QPlatformScreen +class QWasmScreen : public QObject, public QPlatformScreen, public QWasmWindowTreeNode { Q_OBJECT public: @@ -41,6 +43,8 @@ public: QWasmCompositor *compositor(); QWasmDeadKeySupport *deadKeySupport() { return m_deadKeySupport.get(); } + QList allWindows(); + QRect geometry() const override; int depth() const override; QImage::Format format() const override; @@ -53,6 +57,10 @@ public: QWindow *topWindow() const; QWindow *topLevelAt(const QPoint &p) const override; + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; + QPointF mapFromLocal(const QPointF &p) const; QPointF clipPoint(const QPointF &p) const; @@ -65,6 +73,10 @@ public slots: void setGeometry(const QRect &rect); private: + // QWasmWindowTreeNode: + void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) final; + emscripten::val m_container; emscripten::val m_shadowContainer; std::unique_ptr m_compositor; diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 755a9ac930..466b713b1c 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -32,6 +32,17 @@ QT_BEGIN_NAMESPACE +namespace { +QWasmWindowStack::PositionPreference positionPreferenceFromWindowFlags(Qt::WindowFlags flags) +{ + if (flags.testFlag(Qt::WindowStaysOnTopHint)) + return QWasmWindowStack::PositionPreference::StayOnTop; + if (flags.testFlag(Qt::WindowStaysOnBottomHint)) + return QWasmWindowStack::PositionPreference::StayOnBottom; + return QWasmWindowStack::PositionPreference::Regular; +} +} // namespace + Q_GUI_EXPORT int qt_defaultDpiX(); QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, @@ -56,6 +67,7 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_clientArea = std::make_unique(this, compositor->screen(), m_windowContents); + m_windowContents.set("className", "qt-window-contents"); m_qtWindow.call("appendChild", m_windowContents); m_canvas["classList"].call("add", emscripten::val("qt-window-content")); @@ -82,8 +94,6 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_canvasContainer.call("appendChild", m_a11yContainer); m_a11yContainer["classList"].call("add", emscripten::val("qt-window-a11y-container")); - compositor->screen()->element().call("appendChild", m_qtWindow); - const bool rendersTo2dContext = w->surfaceType() != QSurface::OpenGLSurface; if (rendersTo2dContext) m_context2d = m_canvas.call("getContext", emscripten::val("2d")); @@ -92,7 +102,6 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_qtWindow.set("id", "qt-window-" + std::to_string(m_winId)); emscripten::val::module_property("specialHTMLTargets").set(canvasSelector(), m_canvas); - m_compositor->addWindow(this); m_flags = window()->flags(); const auto pointerCallback = std::function([this](emscripten::val event) { @@ -125,13 +134,16 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_keyDownCallback = std::make_unique(m_qtWindow, "keydown", keyCallback); m_keyUpCallback = std::make_unique(m_qtWindow, "keyup", keyCallback); + + setParent(parent()); } QWasmWindow::~QWasmWindow() { emscripten::val::module_property("specialHTMLTargets").delete_(canvasSelector()); - destroy(); - m_compositor->removeWindow(this); + m_canvasContainer.call("removeChild", m_canvas); + m_context2d = emscripten::val::undefined(); + commitParent(nullptr); if (m_requestAnimationFrameId > -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); #if QT_CONFIG(accessibility) @@ -162,8 +174,7 @@ void QWasmWindow::onCloseClicked() void QWasmWindow::onNonClientAreaInteraction() { - if (!isActive()) - requestActivateWindow(); + requestActivateWindow(); QGuiApplicationPrivate::instance()->closeAllPopups(); } @@ -178,14 +189,6 @@ bool QWasmWindow::onNonClientEvent(const PointerEvent &event) event.modifiers); } -void QWasmWindow::destroy() -{ - m_qtWindow["parentElement"].call("removeChild", m_qtWindow); - - m_canvasContainer.call("removeChild", m_canvas); - m_context2d = emscripten::val::undefined(); -} - void QWasmWindow::initialize() { QRect rect = windowGeometry(); @@ -258,21 +261,31 @@ void QWasmWindow::setGeometry(const QRect &rect) if (m_state.testFlag(Qt::WindowMaximized)) return platformScreen()->availableGeometry().marginsRemoved(frameMargins()); - const auto screenGeometry = screen()->geometry(); + auto offset = rect.topLeft() - (!parent() ? screen()->geometry().topLeft() : QPoint()); - QRect result(rect); - result.moveTop(std::max(std::min(rect.y(), screenGeometry.bottom()), - screenGeometry.y() + margins.top())); - result.setSize( - result.size().expandedTo(windowMinimumSize()).boundedTo(windowMaximumSize())); - return result; + // In viewport + auto containerGeometryInViewport = + QRectF::fromDOMRect(parentNode()->containerElement().call( + "getBoundingClientRect")) + .toRect(); + + auto rectInViewport = QRect(containerGeometryInViewport.topLeft() + offset, rect.size()); + + QRect cappedGeometry(rectInViewport); + cappedGeometry.moveTop( + std::max(std::min(rectInViewport.y(), containerGeometryInViewport.bottom()), + containerGeometryInViewport.y() + margins.top())); + cappedGeometry.setSize( + cappedGeometry.size().expandedTo(windowMinimumSize()).boundedTo(windowMaximumSize())); + return QRect(QPoint(rect.x(), rect.y() + cappedGeometry.y() - rectInViewport.y()), + rect.size()); })(); m_nonClientArea->onClientAreaWidthChange(clientAreaRect.width()); const auto frameRect = clientAreaRect .adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()) - .translated(-screen()->geometry().topLeft()); + .translated(!parent() ? -screen()->geometry().topLeft() : QPoint()); m_qtWindow["style"].set("left", std::to_string(frameRect.left()) + "px"); m_qtWindow["style"].set("top", std::to_string(frameRect.top()) + "px"); @@ -335,13 +348,13 @@ QMargins QWasmWindow::frameMargins() const void QWasmWindow::raise() { - m_compositor->raise(this); + bringToTop(); invalidate(); } void QWasmWindow::lower() { - m_compositor->lower(this); + sendToBottom(); invalidate(); } @@ -378,8 +391,11 @@ void QWasmWindow::onActivationChanged(bool active) void QWasmWindow::setWindowFlags(Qt::WindowFlags flags) { - if (flags.testFlag(Qt::WindowStaysOnTopHint) != m_flags.testFlag(Qt::WindowStaysOnTopHint)) - m_compositor->windowPositionPreferenceChanged(this, flags); + if (flags.testFlag(Qt::WindowStaysOnTopHint) != m_flags.testFlag(Qt::WindowStaysOnTopHint) + || flags.testFlag(Qt::WindowStaysOnBottomHint) + != m_flags.testFlag(Qt::WindowStaysOnBottomHint)) { + onPositionPreferenceChanged(positionPreferenceFromWindowFlags(flags)); + } m_flags = flags; dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); dom::syncCSSClassWith(m_qtWindow, "has-shadow", hasShadow()); @@ -461,6 +477,12 @@ void QWasmWindow::applyWindowState() setGeometry(newGeom); } +void QWasmWindow::commitParent(QWasmWindowTreeNode *parent) +{ + onParentChanged(m_commitedParent, parent, positionPreferenceFromWindowFlags(window()->flags())); + m_commitedParent = parent; +} + bool QWasmWindow::processKey(const KeyEvent &event) { constexpr bool ProceedToNativeEvent = false; @@ -600,10 +622,8 @@ void QWasmWindow::requestActivateWindow() return; } - if (window()->isTopLevel()) { - raise(); - m_compositor->setActive(this); - } + raise(); + setAsActiveNode(); if (!QWasmIntegration::get()->inputContext()) m_canvas.call("focus"); @@ -651,9 +671,41 @@ void QWasmWindow::setMask(const QRegion ®ion) m_qtWindow["style"].set("clipPath", emscripten::val(cssClipPath.str())); } +void QWasmWindow::setParent(const QPlatformWindow *) +{ + commitParent(parentNode()); +} + std::string QWasmWindow::canvasSelector() const { return "!qtwindow" + std::to_string(m_winId); } +emscripten::val QWasmWindow::containerElement() +{ + return m_windowContents; +} + +QWasmWindowTreeNode *QWasmWindow::parentNode() +{ + if (parent()) + return static_cast(parent()); + return platformScreen(); +} + +QWasmWindow *QWasmWindow::asWasmWindow() +{ + return this; +} + +void QWasmWindow::onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) +{ + if (previous) + previous->containerElement().call("removeChild", m_qtWindow); + if (current) + current->containerElement().call("appendChild", m_qtWindow); + QWasmWindowTreeNode::onParentChanged(previous, current, positionPreference); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h index f191c90954..0c523d3d9f 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -12,6 +12,8 @@ #include "qwasmscreen.h" #include "qwasmcompositor.h" #include "qwasmwindownonclientarea.h" +#include "qwasmwindowstack.h" +#include "qwasmwindowtreenode.h" #include #include "QtGui/qopenglcontext.h" @@ -38,14 +40,15 @@ struct PointerEvent; class QWasmDeadKeySupport; struct WheelEvent; -class QWasmWindow final : public QPlatformWindow, public QNativeInterface::Private::QWasmWindow +class QWasmWindow final : public QPlatformWindow, + public QWasmWindowTreeNode, + public QNativeInterface::Private::QWasmWindow { public: QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, QWasmCompositor *compositor, QWasmBackingStore *backingStore); ~QWasmWindow() final; - void destroy(); void paint(); void setZOrder(int order); void setWindowCursor(QByteArray cssCursorName); @@ -81,6 +84,7 @@ public: bool setMouseGrabEnabled(bool grab) final; bool windowEvent(QEvent *event) final; void setMask(const QRegion ®ion) final; + void setParent(const QPlatformWindow *window) final; QWasmScreen *platformScreen() const; void setBackingStore(QWasmBackingStore *store) { m_backingStore = store; } @@ -88,6 +92,7 @@ public: QWindow *window() const { return m_window; } std::string canvasSelector() const; + emscripten::val context2d() const { return m_context2d; } emscripten::val a11yContainer() const { return m_a11yContainer; } emscripten::val inputHandlerElement() const { return m_windowContents; } @@ -96,15 +101,25 @@ public: emscripten::val document() const override { return m_document; } emscripten::val clientArea() const override { return m_qtWindow; } + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; + private: friend class QWasmScreen; static constexpr auto minSizeForRegularWindows = 100; + // QWasmWindowTreeNode: + QWasmWindow *asWasmWindow() final; + void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) final; + void invalidate(); bool hasBorder() const; bool hasShadow() const; bool hasMaximizeButton() const; void applyWindowState(); + void commitParent(QWasmWindowTreeNode *parent); bool processKey(const KeyEvent &event); bool processPointer(const PointerEvent &event); @@ -128,6 +143,8 @@ private: std::unique_ptr m_nonClientArea; std::unique_ptr m_clientArea; + QWasmWindowTreeNode *m_commitedParent = nullptr; + std::unique_ptr m_keyDownCallback; std::unique_ptr m_keyUpCallback; diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp index bc597069e7..00ae1aaeb3 100644 --- a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp @@ -187,9 +187,11 @@ ResizeConstraints Resizer::getResizeConstraints() { const auto frameRect = QRectF::fromDOMRect(m_windowElement.call("getBoundingClientRect")); - const auto screenRect = QRectF::fromDOMRect( - m_window->platformScreen()->element().call("getBoundingClientRect")); - const int maxGrowTop = frameRect.top() - screenRect.top(); + auto containerGeometry = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call( + "getBoundingClientRect")); + + const int maxGrowTop = frameRect.top() - containerGeometry.top(); return ResizeConstraints{minShrink, maxGrow, maxGrowTop}; } @@ -211,6 +213,7 @@ void Resizer::startResize(Qt::Edges resizeEdges, const PointerEvent &event) const auto resizeConstraints = getResizeConstraints(); m_currentResizeData->minShrink = resizeConstraints.minShrink; + m_currentResizeData->maxGrow = QPoint(resizeConstraints.maxGrow.x(), std::min(resizeEdges & Qt::Edge::TopEdge ? resizeConstraints.maxGrowTop : INT_MAX, @@ -415,9 +418,15 @@ bool TitleBar::onDoubleClick() QPointF TitleBar::clipPointWithScreen(const QPointF &pointInTitleBarCoords) const { - auto *screen = m_window->platformScreen(); - return screen->clipPoint(screen->mapFromLocal( - dom::mapPoint(m_element, screen->element(), pointInTitleBarCoords))); + auto containerRect = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call( + "getBoundingClientRect")); + const auto p = dom::mapPoint(m_element, m_window->parentNode()->containerElement(), + pointInTitleBarCoords); + + auto result = QPointF(qBound(0., qreal(p.x()), containerRect.width()), + qBound(0., qreal(p.y()), containerRect.height())); + return m_window->parent() ? result : m_window->platformScreen()->mapFromLocal(result).toPoint(); } NonClientArea::NonClientArea(QWasmWindow *window, emscripten::val qtWindowElement) diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp new file mode 100644 index 0000000000..e16410dcde --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp @@ -0,0 +1,104 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowtreenode.h" + +#include "qwasmwindow.h" + +QWasmWindowTreeNode::QWasmWindowTreeNode() + : m_childStack(std::bind(&QWasmWindowTreeNode::onTopWindowChanged, this)) +{ +} + +QWasmWindowTreeNode::~QWasmWindowTreeNode() = default; + +void QWasmWindowTreeNode::onParentChanged(QWasmWindowTreeNode *previousParent, + QWasmWindowTreeNode *currentParent, + QWasmWindowStack::PositionPreference positionPreference) +{ + auto *window = asWasmWindow(); + if (previousParent) { + previousParent->m_childStack.removeWindow(window); + previousParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeRemoval, previousParent, + window); + } + + if (currentParent) { + currentParent->m_childStack.pushWindow(window, positionPreference); + currentParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeInsertion, currentParent, + window); + } +} + +QWasmWindow *QWasmWindowTreeNode::asWasmWindow() +{ + return nullptr; +} + +void QWasmWindowTreeNode::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && m_childStack.topWindow()) { + m_childStack.topWindow()->requestActivateWindow(); + } + + if (parentNode()) + parentNode()->onSubtreeChanged(changeType, parent, child); +} + +void QWasmWindowTreeNode::setWindowZOrder(QWasmWindow *window, int z) +{ + window->setZOrder(z); +} + +void QWasmWindowTreeNode::onPositionPreferenceChanged( + QWasmWindowStack::PositionPreference positionPreference) +{ + if (parentNode()) { + parentNode()->m_childStack.windowPositionPreferenceChanged(asWasmWindow(), + positionPreference); + } +} + +void QWasmWindowTreeNode::setAsActiveNode() +{ + if (parentNode()) + parentNode()->setActiveChildNode(asWasmWindow()); +} + +void QWasmWindowTreeNode::bringToTop() +{ + if (!parentNode()) + return; + parentNode()->m_childStack.raise(asWasmWindow()); + parentNode()->bringToTop(); +} + +void QWasmWindowTreeNode::sendToBottom() +{ + if (!parentNode()) + return; + m_childStack.lower(asWasmWindow()); +} + +void QWasmWindowTreeNode::onTopWindowChanged() +{ + constexpr int zOrderForElementInFrontOfScreen = 3; + int z = zOrderForElementInFrontOfScreen; + std::for_each(m_childStack.rbegin(), m_childStack.rend(), + [this, &z](QWasmWindow *window) { setWindowZOrder(window, z++); }); +} + +void QWasmWindowTreeNode::setActiveChildNode(QWasmWindow *activeChild) +{ + m_activeChild = activeChild; + + auto it = m_childStack.begin(); + if (it == m_childStack.end()) + return; + for (; it != m_childStack.end(); ++it) + (*it)->onActivationChanged(*it == m_activeChild); + + setAsActiveNode(); +} diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.h b/src/plugins/platforms/wasm/qwasmwindowtreenode.h new file mode 100644 index 0000000000..344fdb43cb --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.h @@ -0,0 +1,53 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWTREENODE_H +#define QWASMWINDOWTREENODE_H + +#include "qwasmwindowstack.h" + +namespace emscripten { +class val; +} + +class QWasmWindow; + +enum class QWasmWindowTreeNodeChangeType { + NodeInsertion, + NodeRemoval, +}; + +class QWasmWindowTreeNode +{ +public: + QWasmWindowTreeNode(); + virtual ~QWasmWindowTreeNode(); + + virtual emscripten::val containerElement() = 0; + virtual QWasmWindowTreeNode *parentNode() = 0; + +protected: + virtual void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference); + virtual QWasmWindow *asWasmWindow(); + virtual void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child); + virtual void setWindowZOrder(QWasmWindow *window, int z); + + void onPositionPreferenceChanged(QWasmWindowStack::PositionPreference positionPreference); + void setAsActiveNode(); + void bringToTop(); + void sendToBottom(); + + const QWasmWindowStack &childStack() const { return m_childStack; } + QWasmWindow *activeChild() const { return m_activeChild; } + +private: + void onTopWindowChanged(); + void setActiveChildNode(QWasmWindow *activeChild); + + QWasmWindowStack m_childStack; + QWasmWindow *m_activeChild = nullptr; +}; + +#endif // QWASMWINDOWTREENODE_H diff --git a/tests/auto/wasm/CMakeLists.txt b/tests/auto/wasm/CMakeLists.txt index 47031037e0..0d67ae8e79 100644 --- a/tests/auto/wasm/CMakeLists.txt +++ b/tests/auto/wasm/CMakeLists.txt @@ -30,6 +30,19 @@ qt_internal_add_test(tst_qwasmwindowstack Qt::Widgets ) +qt_internal_add_test(tst_qwasmwindowtreenode + SOURCES + tst_qwasmwindowtreenode.cpp + DEFINES + QT_NO_FOREACH + LIBRARIES + Qt::GuiPrivate + PUBLIC_LIBRARIES + Qt::Core + Qt::Gui + Qt::Widgets +) + qt_internal_add_test(tst_qwasmkeytranslator SOURCES tst_qwasmkeytranslator.cpp diff --git a/tests/auto/wasm/tst_qwasmwindowtreenode.cpp b/tests/auto/wasm/tst_qwasmwindowtreenode.cpp new file mode 100644 index 0000000000..2fd2ab2e66 --- /dev/null +++ b/tests/auto/wasm/tst_qwasmwindowtreenode.cpp @@ -0,0 +1,257 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "../../../src/plugins/platforms/wasm/qwasmwindowtreenode.h" +#include +#include +#include + +class QWasmWindow +{ +}; + +using OnSubtreeChangedCallback = std::function; +using SetWindowZOrderCallback = std::function; + +struct OnSubtreeChangedCallData +{ + QWasmWindowTreeNodeChangeType changeType; + QWasmWindowTreeNode *parent; + QWasmWindow *child; +}; + +struct SetWindowZOrderCallData +{ + QWasmWindow *window; + int z; +}; + +class TestWindowTreeNode final : public QWasmWindowTreeNode, public QWasmWindow +{ +public: + TestWindowTreeNode(OnSubtreeChangedCallback onSubtreeChangedCallback, + SetWindowZOrderCallback setWindowZOrderCallback) + : m_onSubtreeChangedCallback(std::move(onSubtreeChangedCallback)), + m_setWindowZOrderCallback(std::move(setWindowZOrderCallback)) + { + } + ~TestWindowTreeNode() final { } + + void setParent(TestWindowTreeNode *parent) + { + auto *previous = m_parent; + m_parent = parent; + onParentChanged(previous, parent, QWasmWindowStack::PositionPreference::Regular); + } + + void setContainerElement(emscripten::val container) { m_containerElement = container; } + + void bringToTop() { QWasmWindowTreeNode::bringToTop(); } + + void sendToBottom() { QWasmWindowTreeNode::sendToBottom(); } + + const QWasmWindowStack &childStack() { return QWasmWindowTreeNode::childStack(); } + + emscripten::val containerElement() final { return m_containerElement; } + + QWasmWindowTreeNode *parentNode() final { return m_parent; } + + QWasmWindow *asWasmWindow() final { return this; } + +protected: + void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) final + { + m_onSubtreeChangedCallback(changeType, parent, child); + } + + void setWindowZOrder(QWasmWindow *window, int z) final { m_setWindowZOrderCallback(window, z); } + + TestWindowTreeNode *m_parent = nullptr; + emscripten::val m_containerElement = emscripten::val::undefined(); + + OnSubtreeChangedCallback m_onSubtreeChangedCallback; + SetWindowZOrderCallback m_setWindowZOrderCallback; +}; + +class tst_QWasmWindowTreeNode : public QObject +{ + Q_OBJECT + +public: + tst_QWasmWindowTreeNode() { } + +private slots: + void init(); + + void nestedWindowStacks(); + void settingChildWindowZOrder(); +}; + +void tst_QWasmWindowTreeNode::init() { } + +bool operator==(const OnSubtreeChangedCallData &lhs, const OnSubtreeChangedCallData &rhs) +{ + return lhs.changeType == rhs.changeType && lhs.parent == rhs.parent && lhs.child == rhs.child; +} + +bool operator==(const SetWindowZOrderCallData &lhs, const SetWindowZOrderCallData &rhs) +{ + return lhs.window == rhs.window && lhs.z == rhs.z; +} + +void tst_QWasmWindowTreeNode::nestedWindowStacks() +{ + QList calls; + OnSubtreeChangedCallback mockOnSubtreeChanged = + [&calls](QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) { + calls.push_back(OnSubtreeChangedCallData{ changeType, parent, child }); + }; + SetWindowZOrderCallback ignoreSetWindowZOrder = [](QWasmWindow *, int) {}; + TestWindowTreeNode node(mockOnSubtreeChanged, ignoreSetWindowZOrder); + node.bringToTop(); + + OnSubtreeChangedCallback ignoreSubtreeChanged = [](QWasmWindowTreeNodeChangeType, + QWasmWindowTreeNode *, QWasmWindow *) {}; + TestWindowTreeNode node2(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node2.setParent(&node); + + QCOMPARE(node.childStack().size(), 1u); + QCOMPARE(node2.childStack().size(), 0u); + QCOMPARE(node.childStack().topWindow(), &node2); + QCOMPARE(calls.size(), 1u); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeInsertion, &node, + &node2 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + TestWindowTreeNode node3(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node3.setParent(&node); + + QCOMPARE(node.childStack().size(), 2u); + QCOMPARE(node2.childStack().size(), 0u); + QCOMPARE(node3.childStack().size(), 0u); + QCOMPARE(node.childStack().topWindow(), &node3); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeInsertion, &node, + &node3 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + TestWindowTreeNode node4(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node4.setParent(&node); + + QCOMPARE(node.childStack().size(), 3u); + QCOMPARE(node2.childStack().size(), 0u); + QCOMPARE(node3.childStack().size(), 0u); + QCOMPARE(node4.childStack().size(), 0u); + QCOMPARE(node.childStack().topWindow(), &node4); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeInsertion, &node, + &node4 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + node3.bringToTop(); + QCOMPARE(node.childStack().topWindow(), &node3); + + node4.setParent(nullptr); + QCOMPARE(node.childStack().size(), 2u); + QCOMPARE(node.childStack().topWindow(), &node3); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeRemoval, &node, + &node4 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + node2.setParent(nullptr); + QCOMPARE(node.childStack().size(), 1u); + QCOMPARE(node.childStack().topWindow(), &node3); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeRemoval, &node, + &node2 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + node3.setParent(nullptr); + QVERIFY(node.childStack().empty()); + QCOMPARE(node.childStack().topWindow(), nullptr); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeRemoval, &node, + &node3 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } +} + +void tst_QWasmWindowTreeNode::settingChildWindowZOrder() +{ + QList calls; + OnSubtreeChangedCallback ignoreSubtreeChanged = [](QWasmWindowTreeNodeChangeType, + QWasmWindowTreeNode *, QWasmWindow *) {}; + SetWindowZOrderCallback onSetWindowZOrder = [&calls](QWasmWindow *window, int z) { + calls.push_back(SetWindowZOrderCallData{ window, z }); + }; + SetWindowZOrderCallback ignoreSetWindowZOrder = [](QWasmWindow *, int) {}; + TestWindowTreeNode node(ignoreSubtreeChanged, onSetWindowZOrder); + + TestWindowTreeNode node2(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node2.setParent(&node); + + { + QCOMPARE(calls.size(), 1u); + SetWindowZOrderCallData expected{ &node2, 3 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + TestWindowTreeNode node3(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node3.setParent(&node); + + { + QCOMPARE(calls.size(), 2u); + SetWindowZOrderCallData expected{ &node2, 3 }; + QCOMPARE(calls[0], expected); + expected = SetWindowZOrderCallData{ &node3, 4 }; + QCOMPARE(calls[1], expected); + calls.clear(); + } + + TestWindowTreeNode node4(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node4.setParent(&node); + + { + QCOMPARE(calls.size(), 3u); + SetWindowZOrderCallData expected{ &node2, 3 }; + QCOMPARE(calls[0], expected); + expected = SetWindowZOrderCallData{ &node3, 4 }; + QCOMPARE(calls[1], expected); + expected = SetWindowZOrderCallData{ &node4, 5 }; + QCOMPARE(calls[2], expected); + calls.clear(); + } + + node2.bringToTop(); + + { + QCOMPARE(calls.size(), 3u); + SetWindowZOrderCallData expected{ &node3, 3 }; + QCOMPARE(calls[0], expected); + expected = SetWindowZOrderCallData{ &node4, 4 }; + QCOMPARE(calls[1], expected); + expected = SetWindowZOrderCallData{ &node2, 5 }; + QCOMPARE(calls[2], expected); + calls.clear(); + } +} + +QTEST_MAIN(tst_QWasmWindowTreeNode) +#include "tst_qwasmwindowtreenode.moc" diff --git a/tests/manual/wasm/qwasmwindow/qwasmwindow.py b/tests/manual/wasm/qwasmwindow/qwasmwindow.py index 04e1611220..5bcc39361b 100644 --- a/tests/manual/wasm/qwasmwindow/qwasmwindow.py +++ b/tests/manual/wasm/qwasmwindow/qwasmwindow.py @@ -14,6 +14,8 @@ from selenium.webdriver.common.action_chains import ActionChains import unittest from enum import Enum, auto +import time + class WidgetTestCase(unittest.TestCase): def setUp(self): self._driver = Chrome() @@ -28,8 +30,7 @@ class WidgetTestCase(unittest.TestCase): defaultWindowMinSize = 100 screen = Screen(self._driver, ScreenPosition.FIXED, x=0, y=0, width=600, height=600) - window = Window(screen, x=100, y=100, width=200, height=200) - window.set_visible(True) + window = Window(parent=screen, rect=Rect(x=100, y=100, width=200, height=200)) self.assertEqual(window.rect, Rect(x=100, y=100, width=200, height=200)) window.drag(Handle.TOP_LEFT, direction=UP(10) + LEFT(10)) @@ -62,8 +63,7 @@ class WidgetTestCase(unittest.TestCase): def test_cannot_resize_over_screen_top_edge(self): screen = Screen(self._driver, ScreenPosition.FIXED, x=200, y=200, width=300, height=300) - window = Window(screen, x=300, y=300, width=100, height=100) - window.set_visible(True) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100)) self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) frame_rect_before_resize = window.frame_rect @@ -77,8 +77,7 @@ class WidgetTestCase(unittest.TestCase): def test_window_move(self): screen = Screen(self._driver, ScreenPosition.FIXED, x=200, y=200, width=300, height=300) - window = Window(screen, x=300, y=300, width=100, height=100) - window.set_visible(True) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100)) self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) window.drag(Handle.TOP_WINDOW_BAR, direction=UP(30)) @@ -93,8 +92,7 @@ class WidgetTestCase(unittest.TestCase): def test_screen_limits_window_moves(self): screen = Screen(self._driver, ScreenPosition.RELATIVE, x=200, y=200, width=300, height=300) - window = Window(screen, x=300, y=300, width=100, height=100) - window.set_visible(True) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100)) self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) window.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300)) @@ -105,8 +103,7 @@ class WidgetTestCase(unittest.TestCase): x=200, y=2000, width=300, height=300, container_width=500, container_height=7000) screen.scroll_to() - window = Window(screen, x=300, y=2100, width=100, height=100) - window.set_visible(True) + window = Window(parent=screen, rect=Rect(x=300, y=2100, width=100, height=100)) self.assertEqual(window.rect, Rect(x=300, y=2100, width=100, height=100)) window.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300)) @@ -115,8 +112,7 @@ class WidgetTestCase(unittest.TestCase): def test_maximize(self): screen = Screen(self._driver, ScreenPosition.RELATIVE, x=200, y=200, width=300, height=300) - window = Window(screen, x=300, y=300, width=100, height=100, title='Maximize') - window.set_visible(True) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100), title='Maximize') self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) window.maximize() @@ -125,11 +121,9 @@ class WidgetTestCase(unittest.TestCase): def test_multitouch_window_move(self): screen = Screen(self._driver, ScreenPosition.FIXED, x=0, y=0, width=800, height=800) - windows = [Window(screen, x=50, y=50, width=100, height=100, title='First'), - Window(screen, x=400, y=400, width=100, height=100, title='Second'), - Window(screen, x=50, y=400, width=100, height=100, title='Third')] - for window in windows: - window.set_visible(True) + windows = [Window(screen, rect=Rect(x=50, y=50, width=100, height=100), title='First'), + Window(screen, rect=Rect(x=400, y=400, width=100, height=100), title='Second'), + Window(screen, rect=Rect(x=50, y=400, width=100, height=100), title='Third')] self.assertEqual(windows[0].rect, Rect(x=50, y=50, width=100, height=100)) self.assertEqual(windows[1].rect, Rect(x=400, y=400, width=100, height=100)) @@ -146,11 +140,9 @@ class WidgetTestCase(unittest.TestCase): def test_multitouch_window_resize(self): screen = Screen(self._driver, ScreenPosition.FIXED, x=0, y=0, width=800, height=800) - windows = [Window(screen, x=50, y=50, width=150, height=150, title='First'), - Window(screen, x=400, y=400, width=150, height=150, title='Second'), - Window(screen, x=50, y=400, width=150, height=150, title='Third')] - for window in windows: - window.set_visible(True) + windows = [Window(screen, rect=Rect(x=50, y=50, width=150, height=150), title='First'), + Window(screen, rect=Rect(x=400, y=400, width=150, height=150), title='Second'), + Window(screen, rect=Rect(x=50, y=400, width=150, height=150), title='Third')] self.assertEqual(windows[0].rect, Rect(x=50, y=50, width=150, height=150)) self.assertEqual(windows[1].rect, Rect(x=400, y=400, width=150, height=150)) @@ -167,8 +159,7 @@ class WidgetTestCase(unittest.TestCase): def test_newly_created_window_gets_keyboard_focus(self): screen = Screen(self._driver, ScreenPosition.FIXED, x=0, y=0, width=800, height=800) - window = Window(screen, x=0, y=0, width=800, height=800, title='root') - window.set_visible(True) + window = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root') ActionChains(self._driver).key_down('c').key_up('c').perform() @@ -179,29 +170,262 @@ class WidgetTestCase(unittest.TestCase): self.assertEqual(events[-1]['type'], 'keyRelease') self.assertEqual(events[-1]['key'], 'c') + def test_parent_window_limits_moves_of_children(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + w1 = Window(parent=screen, rect=Rect(x=200, y=200, width=400, height=400), title='w1') + w1_w1 = Window(parent=w1, rect=Rect(x=100, y=100, width=200, height=200), title='w1_w1') + w1_w1_w1 = Window(parent=w1_w1, rect=Rect(50, 50, 100, 100), title='w1_w1_w1') + + self.assertEqual(w1.rect, Rect(200, 200, 400, 400)) + self.assertEqual(w1_w1.rect, Rect(100, 100, 200, 200)) + self.assertEqual(w1_w1_w1.rect, Rect(50, 50, 100, 100)) + + # Left - Middle window + w1_w1.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300)) + + self.assertEqual( + w1_w1.frame_rect.x, -w1_w1.frame_rect.width / 2) + w1_w1.drag(Handle.TOP_WINDOW_BAR, direction=RIGHT(w1_w1.frame_rect.width / 2 + 100)) + + # Right - Middle window + w1_w1.drag(Handle.TOP_WINDOW_BAR, direction=RIGHT(300)) + + self.assertEqual( + w1_w1.frame_rect.x, w1.rect.width - w1_w1.frame_rect.width / 2) + w1_w1.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(w1.rect.width / 2)) + + # Left - Inner window + w1_w1_w1.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300)) + + self.assertEqual( + w1_w1_w1.frame_rect.x, -w1_w1_w1.frame_rect.width / 2) + + def test_child_window_activation(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root') + w1 = Window(parent=bottom, rect=Rect(x=100, y=100, width=600, height=600), title='w1') + w1_w1 = Window(parent=w1, rect=Rect(x=100, y=100, width=300, height=300), title='w1_w1') + w1_w1_w1 = Window(parent=w1_w1, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w1_w1') + w1_w1_w2 = Window(parent=w1_w1, rect=Rect(x=150, y=150, width=100, height=100), title='w1_w1_w2') + w1_w2 = Window(parent=w1, rect=Rect(x=300, y=300, width=300, height=300), title='w1_w2') + w1_w2_w1 = Window(parent=w1_w2, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w2_w1') + w2 = Window(parent=bottom, rect=Rect(x=300, y=300, width=450, height=450), title='w2') + + self.assertEqual(screen.window_stack_at_point(w1_w1.bounding_box.midpoint[0], w1_w1.bounding_box.midpoint[1]), + [w2, w1_w1_w2, w1_w1_w1, w1_w1, w1, bottom]) + + self.assertEqual(screen.window_stack_at_point(w2.bounding_box.midpoint[0], w2.bounding_box.midpoint[1]), + [w2, w1_w2_w1, w1_w2, w1, bottom]) + + for w in [w1, w1_w1, w1_w1_w1, w1_w1_w2, w1_w2, w1_w2_w1]: + self.assertFalse(w.active) + self.assertTrue(w2.active) + + w1.click(0, 0) + + for w in [w1, w1_w2, w1_w2_w1]: + self.assertTrue(w.active) + for w in [w1_w1, w1_w1_w1, w1_w1_w2, w2]: + self.assertFalse(w.active) + + self.assertEqual(screen.window_stack_at_point(w2.frame_rect.midpoint[0], w2.frame_rect.midpoint[1]), + [w1_w2_w1, w1_w2, w1, w2, bottom]) + + w1_w1_w1.click(0, 0) + + for w in [w1, w1_w1, w1_w1_w1]: + self.assertTrue(w.active) + for w in [w1_w1_w2, w1_w2, w1_w2_w1, w2]: + self.assertFalse(w.active) + + self.assertEqual(screen.window_stack_at_point(w1_w1_w1.bounding_box.midpoint[0], w1_w1_w1.bounding_box.midpoint[1]), + [w1_w1_w1, w1_w1_w2, w1_w1, w1, w2, bottom]) + + w1_w1_w2.click(w1_w1_w2.bounding_box.width, w1_w1_w2.bounding_box.height) + + for w in [w1, w1_w1, w1_w1_w2]: + self.assertTrue(w.active) + for w in [w1_w1_w1, w1_w2, w1_w2_w1, w2]: + self.assertFalse(w.active) + + self.assertEqual(screen.window_stack_at_point(w1_w1_w2.bounding_box.x, w1_w1_w2.bounding_box.y), + [w1_w1_w2, w1_w1_w1, w1_w1, w1, w2, bottom]) + + def test_window_reparenting(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + bottom = Window(parent=screen, rect=Rect(x=800, y=800, width=300, height=300), title='bottom') + w1 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w1') + w2 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w2') + w3 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w3') + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w2.set_parent(w1) + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w3.set_parent(w2) + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w2.set_parent(screen) + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w1.set_parent(w2) + + self.assertTrue( + w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w3.set_parent(screen) + + self.assertTrue( + w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w2.set_parent(w3) + + self.assertTrue( + w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + def test_window_closing(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + bottom = Window(parent=screen, rect=Rect(x=800, y=800, width=300, height=300), title='root') + bottom.close() + + w1 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w1') + w2 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w2') + w3 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w3') + + w3.close() + + self.assertFalse(w3 in screen.query_windows()) + self.assertTrue(w2 in screen.query_windows()) + self.assertTrue(w1 in screen.query_windows()) + + w4 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w4') + + self.assertTrue(w4 in screen.query_windows()) + self.assertTrue(w2 in screen.query_windows()) + self.assertTrue(w1 in screen.query_windows()) + + w2.close() + w1.close() + + self.assertTrue(w4 in screen.query_windows()) + self.assertFalse(w2 in screen.query_windows()) + self.assertFalse(w1 in screen.query_windows()) + + w4.close() + + self.assertFalse(w4 in screen.query_windows()) + def tearDown(self): self._driver.quit() - class ScreenPosition(Enum): FIXED = auto() RELATIVE = auto() IN_SCROLL_CONTAINER = auto() - class Screen: - def __init__(self, driver, positioning, x, y, width, height, container_width=0, container_height=0): + def __init__(self, driver, positioning=None, x=None, y=None, width=None, height=None, container_width=0, container_height=0, screen_name=None): self.driver = driver - self.x = x - self.y = y - self.width = width - self.height = height + if screen_name is not None: + screen_information = call_instance_function(self.driver, 'screenInformation') + if len(screen_information) != 1: + raise AssertionError('Expecting exactly one screen_information!') + self.screen_info = screen_information[0] + self.element = driver.find_element(By.CSS_SELECTOR, f'#test-screen-1') + return + if positioning == ScreenPosition.FIXED: - command = f'initializeScreenWithFixedPosition({self.x}, {self.y}, {self.width}, {self.height})' + command = f'initializeScreenWithFixedPosition({x}, {y}, {width}, {height})' elif positioning == ScreenPosition.RELATIVE: - command = f'initializeScreenWithRelativePosition({self.x}, {self.y}, {self.width}, {self.height})' + command = f'initializeScreenWithRelativePosition({x}, {y}, {width}, {height})' elif positioning == ScreenPosition.IN_SCROLL_CONTAINER: - command = f'initializeScreenInScrollContainer({container_width}, {container_height}, {self.x}, {self.y}, {self.width}, {self.height})' + command = f'initializeScreenInScrollContainer({container_width}, {container_height}, {x}, {y}, {width}, {height})' self.element = self.driver.execute_script( f''' return testSupport.{command}; @@ -223,26 +447,72 @@ class Screen: geo = self.screen_info['geometry'] return Rect(geo['x'], geo['y'], geo['width'], geo['height']) + @property + def name(self): + return self.screen_info['name'] + def scroll_to(self): ActionChains(self.driver).scroll_to_element(self.element).perform() - -class Window: - def __init__(self, screen, x, y, width, height, title='title'): - self.driver = screen.driver - self.title = title - self.driver.execute_script( + def hit_test_point(self, x, y): + return self.driver.execute_script( f''' - instance.createWindow({x}, {y}, {width}, {height}, '{screen.screen_info["name"]}', '{title}'); + return testSupport.hitTestPoint({x}, {y}, '{self.element.get_attribute("id")}'); ''' ) + + def window_stack_at_point(self, x, y): + return [ + Window(self, element=element) for element in [ + *filter(lambda elem: (elem.get_attribute('id') if elem.get_attribute('id') is not None else '') + .startswith('qt-window-'), self.hit_test_point(x, y))]] + + def query_windows(self): + return [ + Window(self, element=element) for element in self.element.shadow_root.find_elements( + By.CSS_SELECTOR, f'div#{self.name} > div.qt-window')] + + +class Window: + def __init__(self, parent=None, rect=None, title=None, element=None, visible=True): + self.driver = parent.driver + if element is not None: + self.element = element + self.title = element.find_element( + By.CSS_SELECTOR, f'.title-bar > .window-name').text + information = self.__window_information() + self.screen = Screen(self.driver, screen_name=information['screen']['name']) + pass + else: + self.title = title = title if title is not None else 'window' + if isinstance(parent, Window): + self.driver.execute_script( + f''' + instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'window', '{parent.title}', '{title}'); + ''' + ) + self.screen = parent.screen + else: + assert(isinstance(parent, Screen)) + self.driver.execute_script( + f''' + instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'screen', '{parent.name}', '{title}'); + ''' + ) + self.screen = parent self._window_id = self.__window_information()['id'] - self.element = screen.element.shadow_root.find_element( - By.CSS_SELECTOR, f'#qt-window-{self._window_id}') + self.element = self.screen.element.shadow_root.find_element( + By.CSS_SELECTOR, f'#qt-window-{self._window_id}') + if visible: + self.set_visible(True) + + def __eq__(self, other): + return self._window_id == other._window_id if isinstance(other, Window) else False def __window_information(self): information = call_instance_function(self.driver, 'windowInformation') - return next(filter(lambda e: e['title'] == self.title, information)) + #print(information) + return next(filter(lambda e: e['title'] == self.title, information)) @property def rect(self): @@ -308,6 +578,61 @@ class Window: offset = (0, -height/2 + top_frame_bar_width/2) return {'window': self, 'offset': offset} + @property + def bounding_box(self): + raw = self.driver.execute_script(""" + return arguments[0].getBoundingClientRect(); + """, self.element) + return Rect(raw['x'], raw['y'], raw['width'], raw['height']) + + @property + def active(self): + return not self.inactive + # self.assertFalse('inactive' in window_element.get_attribute( + # 'class').split(' '), window_element.get_attribute('id')) + + @property + def inactive(self): + window_chain = [ + *self.element.find_elements(By.XPATH, "ancestor::div"), self.element] + return next(filter(lambda elem: 'qt-window' in elem.get_attribute('class').split(' ') and + 'inactive' in elem.get_attribute( + 'class').split(' '), + window_chain + ), None) is not None + + def click(self, x, y): + rect = self.bounding_box + + SELENIUM_IMPRECISION_COMPENSATION = 2 + ActionChains(self.driver).move_to_element( + self.element).move_by_offset(-rect.width / 2 + x + SELENIUM_IMPRECISION_COMPENSATION, + -rect.height / 2 + y + SELENIUM_IMPRECISION_COMPENSATION).click().perform() + + def set_parent(self, parent): + if isinstance(parent, Screen): + # TODO won't work with screen that is not parent.screen + self.screen = parent + self.driver.execute_script( + f''' + instance.setWindowParent('{self.title}', 'none'); + ''' + ) + else: + assert(isinstance(parent, Window)) + self.screen = parent.screen + self.driver.execute_script( + f''' + instance.setWindowParent('{self.title}', '{parent.title}'); + ''' + ) + + def close(self): + self.driver.execute_script( + f''' + instance.closeWindow('{self.title}'); + ''' + ) class TouchDragAction: def __init__(self, origin, direction): @@ -477,10 +802,13 @@ class Rect: def __str__(self): return f'(x: {self.x}, y: {self.y}, width: {self.width}, height: {self.height})' + @property + def midpoint(self): + return self.x + self.width / 2, self.y + self.height / 2, + def assert_rects_equal(geo1, geo2, msg=None): if geo1.x != geo2.x or geo1.y != geo2.y or geo1.width != geo2.width or geo1.height != geo2.height: raise AssertionError(f'Rectangles not equal: \n{geo1} \nvs \n{geo2}') - unittest.main() diff --git a/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.cpp b/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.cpp index eda557e195..04a947d0ba 100644 --- a/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.cpp +++ b/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.cpp @@ -50,6 +50,17 @@ private: } }; +namespace { +DeleteOnCloseWindow *findWindowByTitle(const std::string &title) +{ + auto windows = qGuiApp->allWindows(); + auto window_it = std::find_if(windows.begin(), windows.end(), [&title](QWindow *window) { + return window->title() == QString::fromLatin1(title); + }); + return window_it == windows.end() ? nullptr : static_cast(*window_it); +} +} // namespace + using namespace emscripten; std::string toJSArray(const std::vector &elements) @@ -103,6 +114,7 @@ std::string windowToJSObject(const QWindow &window) << " id: " << std::to_string(window.winId()) << "," << " geometry: " << rectToJSObject(window.geometry()) << "," << " frameGeometry: " << rectToJSObject(window.frameGeometry()) << "," + << " screen: " << screenToJSObject(*window.screen()) << "," << " title: '" << window.title().toStdString() << "' }"; return out.str(); } @@ -132,14 +144,34 @@ void screenInformation() emscripten::val(toJSArray(screensAsJsObjects))); } -void createWindow(int x, int y, int w, int h, std::string screenId, std::string title) +void createWindow(int x, int y, int w, int h, std::string parentType, std::string parentId, + std::string title) { - auto screens = qGuiApp->screens(); - auto screen_it = std::find_if(screens.begin(), screens.end(), [&screenId](QScreen *screen) { - return screen->name() == QString::fromLatin1(screenId); - }); - if (screen_it == screens.end()) { - qWarning() << "No such screen: " << screenId; + QScreen *parentScreen = nullptr; + QWindow *parentWindow = nullptr; + if (parentType == "screen") { + auto screens = qGuiApp->screens(); + auto screen_it = std::find_if(screens.begin(), screens.end(), [&parentId](QScreen *screen) { + return screen->name() == QString::fromLatin1(parentId); + }); + if (screen_it == screens.end()) { + qWarning() << "No such screen: " << parentId; + return; + } + parentScreen = *screen_it; + } else if (parentType == "window") { + auto windows = qGuiApp->allWindows(); + auto window_it = std::find_if(windows.begin(), windows.end(), [&parentId](QWindow *window) { + return window->title() == QString::fromLatin1(parentId); + }); + if (window_it == windows.end()) { + qWarning() << "No such window: " << parentId; + return; + } + parentWindow = *window_it; + parentScreen = parentWindow->screen(); + } else { + qWarning() << "Wrong parent type " << parentType; return; } @@ -149,7 +181,8 @@ void createWindow(int x, int y, int w, int h, std::string screenId, std::string window->setFlag(Qt::WindowMaximizeButtonHint); window->setTitle(QString::fromLatin1(title)); window->setGeometry(x, y, w, h); - window->setScreen(*screen_it); + window->setScreen(parentScreen); + window->setParent(parentWindow); } void setWindowVisible(int windowId, bool visible) { @@ -165,12 +198,37 @@ void setWindowVisible(int windowId, bool visible) { (*window_it)->setVisible(visible); } +void setWindowParent(std::string windowTitle, std::string parentTitle) +{ + QWindow *window = findWindowByTitle(windowTitle); + if (!window) { + qWarning() << "Window could not be found " << parentTitle; + return; + } + QWindow *parent = nullptr; + if (parentTitle != "none") { + if ((parent = findWindowByTitle(parentTitle)) == nullptr) { + qWarning() << "Parent window could not be found " << parentTitle; + return; + } + } + window->setParent(parent); +} + +bool closeWindow(std::string title) +{ + QWindow *window = findWindowByTitle(title); + return window ? window->close() : false; +} + EMSCRIPTEN_BINDINGS(qwasmwindow) { emscripten::function("screenInformation", &screenInformation); emscripten::function("windowInformation", &windowInformation); emscripten::function("createWindow", &createWindow); emscripten::function("setWindowVisible", &setWindowVisible); + emscripten::function("setWindowParent", &setWindowParent); + emscripten::function("closeWindow", &closeWindow); } int main(int argc, char **argv) diff --git a/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.html b/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.html index 8d73160ed8..c8f9e977fe 100644 --- a/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.html +++ b/tests/manual/wasm/qwasmwindow/qwasmwindow_harness.html @@ -9,6 +9,7 @@ const testSandbox = document.createElement('div'); testSandbox.id = 'test-sandbox'; + let nextScreenId = 1; document.body.appendChild(testSandbox); const eventList = []; @@ -21,6 +22,7 @@ screenDiv.style.width = `${width}px`; screenDiv.style.height = `${height}px`; screenDiv.style.backgroundColor = 'lightblue'; + screenDiv.id = `test-screen-${nextScreenId++}`; return screenDiv; }; @@ -62,7 +64,11 @@ reportEvent: event => { eventList.push(event); }, - events: () => eventList + events: () => eventList, + hitTestPoint: (x, y, screenId) => { + return document + .querySelector(`#${screenId}`).shadowRoot.elementsFromPoint(x, y); + } }; })();