Support child windows on WASM

Setting parents for WASM platform windows is now supported. This means
that windows now reside in a hierarchical window tree, with the screen
and individual windows being nodes (QWasmWindowTreeNode), each
maintaining their own child window stack.

The divs backing windows are properly reparented in response to Qt
window parent changes, so that the html structure reflects what is
happening in Qt.

Change-Id: I55c91d90caf58714342dcd747043967ebfdf96bb
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
This commit is contained in:
Mikolaj Boc 2023-06-14 14:54:34 +02:00
parent eb92d52dc7
commit fc4fca6d9d
16 changed files with 1072 additions and 207 deletions

View File

@ -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

View File

@ -8,21 +8,9 @@
#include <emscripten/html5.h>
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)
{
if (m_windowStack.empty())
window->window()->setFlag(Qt::WindowStaysOnBottomHint);
m_windowStack.pushWindow(window, positionPreferenceFromWindowFlags(window->window()->flags()));
window->requestActivateWindow();
setActive(window);
updateEnabledState();
}
void QWasmCompositor::removeWindow(QWasmWindow *window)
void QWasmCompositor::onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType,
QWasmWindow *window)
{
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);
m_windowStack.removeWindow(window);
if (m_windowStack.topWindow()) {
m_windowStack.topWindow()->requestActivateWindow();
setActive(m_windowStack.topWindow());
}
updateEnabledState();
}
void QWasmCompositor::setActive(QWasmWindow *window)
void QWasmCompositor::setEnabled(bool enabled)
{
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<QWasmWindow *> &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()

View File

@ -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<QWasmWindow *> &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<QWasmWindow *, UpdateRequestDeliveryType> m_requestUpdateWindows;
int m_requestAnimationFrameId = -1;

View File

@ -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;
}

View File

@ -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<void>("observe", m_shadowContainer);
}
emscripten::val QWasmScreen::containerElement()
{
return m_shadowContainer;
}
QWasmWindowTreeNode *QWasmScreen::parentNode()
{
return nullptr;
}
QList<QWasmWindow *> QWasmScreen::allWindows()
{
QList<QWasmWindow *> windows;
for (auto *child : childStack()) {
QWindowList list = child->window()->findChildren<QWindow *>(Qt::FindChildrenRecursively);
std::transform(
list.begin(), list.end(), std::back_inserter(windows),
[](const QWindow *window) { return static_cast<QWasmWindow *>(window->handle()); });
windows.push_back(child);
}
return windows;
}
QT_END_NAMESPACE

View File

@ -6,6 +6,8 @@
#include "qwasmcursor.h"
#include "qwasmwindowtreenode.h"
#include <qpa/qplatformscreen.h>
#include <QtCore/qscopedpointer.h>
@ -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<QWasmWindow *> 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<QWasmCompositor> m_compositor;

View File

@ -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<ClientArea>(this, compositor->screen(), m_windowContents);
m_windowContents.set("className", "qt-window-contents");
m_qtWindow.call<void>("appendChild", m_windowContents);
m_canvas["classList"].call<void>("add", emscripten::val("qt-window-content"));
@ -82,8 +94,6 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport,
m_canvasContainer.call<void>("appendChild", m_a11yContainer);
m_a11yContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-container"));
compositor->screen()->element().call<void>("appendChild", m_qtWindow);
const bool rendersTo2dContext = w->surfaceType() != QSurface::OpenGLSurface;
if (rendersTo2dContext)
m_context2d = m_canvas.call<emscripten::val>("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<qstdweb::EventCallback>(m_qtWindow, "keydown", keyCallback);
m_keyUpCallback = std::make_unique<qstdweb::EventCallback>(m_qtWindow, "keyup", keyCallback);
setParent(parent());
}
QWasmWindow::~QWasmWindow()
{
emscripten::val::module_property("specialHTMLTargets").delete_(canvasSelector());
destroy();
m_compositor->removeWindow(this);
m_canvasContainer.call<void>("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,7 +174,6 @@ void QWasmWindow::onCloseClicked()
void QWasmWindow::onNonClientAreaInteraction()
{
if (!isActive())
requestActivateWindow();
QGuiApplicationPrivate::instance()->closeAllPopups();
}
@ -178,14 +189,6 @@ bool QWasmWindow::onNonClientEvent(const PointerEvent &event)
event.modifiers);
}
void QWasmWindow::destroy()
{
m_qtWindow["parentElement"].call<emscripten::val>("removeChild", m_qtWindow);
m_canvasContainer.call<void>("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<emscripten::val>(
"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);
}
setAsActiveNode();
if (!QWasmIntegration::get()->inputContext())
m_canvas.call<void>("focus");
@ -651,9 +671,41 @@ void QWasmWindow::setMask(const QRegion &region)
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<QWasmWindow *>(parent());
return platformScreen();
}
QWasmWindow *QWasmWindow::asWasmWindow()
{
return this;
}
void QWasmWindow::onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current,
QWasmWindowStack::PositionPreference positionPreference)
{
if (previous)
previous->containerElement().call<void>("removeChild", m_qtWindow);
if (current)
current->containerElement().call<void>("appendChild", m_qtWindow);
QWasmWindowTreeNode::onParentChanged(previous, current, positionPreference);
}
QT_END_NAMESPACE

View File

@ -12,6 +12,8 @@
#include "qwasmscreen.h"
#include "qwasmcompositor.h"
#include "qwasmwindownonclientarea.h"
#include "qwasmwindowstack.h"
#include "qwasmwindowtreenode.h"
#include <QtCore/private/qstdweb_p.h>
#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 &region) 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<NonClientArea> m_nonClientArea;
std::unique_ptr<ClientArea> m_clientArea;
QWasmWindowTreeNode *m_commitedParent = nullptr;
std::unique_ptr<qstdweb::EventCallback> m_keyDownCallback;
std::unique_ptr<qstdweb::EventCallback> m_keyUpCallback;

View File

@ -187,9 +187,11 @@ ResizeConstraints Resizer::getResizeConstraints() {
const auto frameRect =
QRectF::fromDOMRect(m_windowElement.call<emscripten::val>("getBoundingClientRect"));
const auto screenRect = QRectF::fromDOMRect(
m_window->platformScreen()->element().call<emscripten::val>("getBoundingClientRect"));
const int maxGrowTop = frameRect.top() - screenRect.top();
auto containerGeometry =
QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>(
"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<emscripten::val>(
"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)

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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 <QtGui/QWindow>
#include <QTest>
#include <emscripten/val.h>
class QWasmWindow
{
};
using OnSubtreeChangedCallback = std::function<void(
QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, QWasmWindow *child)>;
using SetWindowZOrderCallback = std::function<void(QWasmWindow *window, int z)>;
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<OnSubtreeChangedCallData> 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<SetWindowZOrderCallData> 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"

View File

@ -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,25 +447,71 @@ 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(
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')
#print(information)
return next(filter(lambda e: e['title'] == self.title, information))
@property
@ -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()

View File

@ -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<DeleteOnCloseWindow *>(*window_it);
}
} // namespace
using namespace emscripten;
std::string toJSArray(const std::vector<std::string> &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)
{
QScreen *parentScreen = nullptr;
QWindow *parentWindow = nullptr;
if (parentType == "screen") {
auto screens = qGuiApp->screens();
auto screen_it = std::find_if(screens.begin(), screens.end(), [&screenId](QScreen *screen) {
return screen->name() == QString::fromLatin1(screenId);
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: " << screenId;
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)

View File

@ -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);
}
};
})();
</script>