Commit 8ef363cc authored by David Edmundson's avatar David Edmundson

[wayland] XdgPopup Positioning

Summary:
Support XDGShell Positioning. This gives a client a lot more control
over where the popup will be placed as well as control over how to
handle constraints. i.e what to do if the popup doesn't fit.

trasientOffset was replaced with a method on the client as semantically
it's the role of the client to handle constraints.

Both slide and flip constraint adjustments are implemented. Resize
constraint adjustment will be handled in a future patch.

WlShell is handled by treating it as 1x1 sized anchor with slide
constraint adjustment.

Test Plan:
Manual test of a client implementing xdgpopup exists in kwayland
Extensive unit test here

Existing WlShell test passes (after D16314 which fixes the original)
XdgPopup has a new unit test suite against manually calculated values

Reviewers: #kwin, graesslin

Reviewed By: #kwin, graesslin

Subscribers: zzag, kwin

Tags: #kwin

Differential Revision: https://phabricator.kde.org/D16325
parent d70c3568
......@@ -1169,9 +1169,11 @@ bool AbstractClient::hasTransientPlacementHint() const
return false;
}
QPoint AbstractClient::transientPlacementHint() const
QRect AbstractClient::transientPlacement(const QRect &bounds) const
{
return QPoint();
Q_UNUSED(bounds);
Q_UNREACHABLE();
return QRect();
}
bool AbstractClient::hasTransient(const AbstractClient *c, bool indirect) const
......
......@@ -392,9 +392,10 @@ public:
**/
virtual bool hasTransientPlacementHint() const;
/**
* @returns The recommended position of the transient in parent coordinates
* Only valid id hasTransientPlacementHint is true
* @returns The position the transient wishes to position itself
**/
virtual QPoint transientPlacementHint() const;
virtual QRect transientPlacement(const QRect &bounds) const;
const AbstractClient* transientFor() const;
AbstractClient* transientFor();
/**
......
......@@ -25,6 +25,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// Qt
#include <QtTest>
// KWayland
#include <KWayland/Client/xdgshell.h>
namespace KWayland
{
namespace Client
......@@ -43,7 +46,6 @@ class Shell;
class ShellSurface;
class ShmPool;
class Surface;
class XdgShellSurface;
}
}
......@@ -138,7 +140,7 @@ KWayland::Client::ShellSurface *createShellSurface(KWayland::Client::Surface *su
KWayland::Client::XdgShellSurface *createXdgShellV5Surface(KWayland::Client::Surface *surface, QObject *parent = nullptr);
KWayland::Client::XdgShellSurface *createXdgShellV6Surface(KWayland::Client::Surface *surface, QObject *parent = nullptr);
KWayland::Client::XdgShellSurface *createXdgShellStableSurface(KWayland::Client::Surface *surface, QObject *parent = nullptr);
KWayland::Client::XdgShellPopup *createXdgShellStablePopup(KWayland::Client::Surface *surface, KWayland::Client::XdgShellSurface *parentSurface, const KWayland::Client::XdgPositioner &positioner, QObject *parent = nullptr);
/**
* Creates a shared memory buffer of @p size in @p color and attaches it to the @p surface.
......
......@@ -480,6 +480,19 @@ XdgShellSurface *createXdgShellStableSurface(Surface *surface, QObject *parent)
return s;
}
XdgShellPopup *createXdgShellStablePopup(Surface *surface, XdgShellSurface *parentSurface, const XdgPositioner &positioner, QObject *parent)
{
if (!s_waylandConnection.xdgShellStable) {
return nullptr;
}
auto s = s_waylandConnection.xdgShellStable->createPopup(surface, parentSurface, positioner, parent);
if (!s->isValid()) {
delete s;
return nullptr;
}
return s;
}
QObject *createShellSurface(ShellSurfaceType type, KWayland::Client::Surface *surface, QObject *parent)
{
switch (type) {
......
......@@ -103,7 +103,6 @@ void TransientNoInputTest::testTransientNoFocus()
QVERIFY(transientClient != c);
QCOMPARE(transientClient->geometry(), QRect(c->x() + 10, c->y() + 20, 200, 20));
QVERIFY(transientClient->isTransient());
QCOMPARE(transientClient->transientPlacementHint(), QPoint(10, 20));
QVERIFY(!transientClient->wantsInput());
// workspace's active window should not have changed
......
......@@ -496,39 +496,19 @@ void Placement::placeOnScreenDisplay(AbstractClient* c, QRect& area)
void Placement::placeTransient(AbstractClient *c)
{
const QPoint target = c->transientFor()->pos() + c->transientFor()->clientPos() + c->transientPlacementHint();
c->move(target);
const QRect screen = screens()->geometry(c->transientFor()->screen());
// TODO: work around Qt's transient placement of sub-menus, see https://bugreports.qt.io/browse/QTBUG-51640
#define CHECK \
if (screen.contains(c->geometry())) { \
return; \
}
CHECK
if (screen.x() + screen.width() < c->x() + c->width()) {
// overlaps on right
c->move(screen.x() + screen.width() - c->width(), c->y());
CHECK
}
if (screen.y() + screen.height() < c->y() + c->height()) {
// overlaps on bottom
c->move(c->x(), screen.y() + screen.height() - c->height());
CHECK
}
if (screen.y() > c->y()) {
// top is not on screen
c->move(c->x(), screen.y());
CHECK
}
if (screen.x() > c->x()) {
// left is not on screen
c->move(screen.x(), c->y());
CHECK
const QPoint popupPos = c->transientPlacement(screen).topLeft();
c->move(popupPos);
// Potentially a client could set no constraint adjustments
// and we'll be offscreen.
// The spec implies we should place window the offscreen. However,
// practically Qt doesn't set any constraint adjustments yet so we can't.
// Also kwin generally doesn't let clients do what they want
if (!screen.contains(c->geometry())) {
c->keepInArea(screen);
}
#undef CHECK
// so far the sanitizing didn't help, let's move back to orig target position and use keepInArea
c->move(target);
c->keepInArea(screen);
}
void Placement::placeDialog(AbstractClient* c, QRect& area, Policy nextPlacement)
......
......@@ -327,7 +327,7 @@ void ShellClient::init()
m_hasPopupGrab = true;
});
connect(m_xdgShellSurface, &XdgShellSurfaceInterface::configureAcknowledged, this, [this](int serial) {
connect(m_xdgShellPopup, &XdgShellPopupInterface::configureAcknowledged, this, [this](int serial) {
m_lastAckedConfigureRequest = serial;
});
......@@ -1574,18 +1574,180 @@ void ShellClient::setTransient()
bool ShellClient::hasTransientPlacementHint() const
{
return isTransient() && transientFor() != nullptr;
return isTransient() && transientFor() != nullptr &&
(m_shellSurface || m_xdgShellPopup);
}
QPoint ShellClient::transientPlacementHint() const
QRect ShellClient::transientPlacement(const QRect &bounds) const
{
QRect anchorRect;
Qt::Edges anchorEdge;
Qt::Edges gravity;
QPoint offset;
PositionerConstraints constraintAdjustments;
const QPoint parentClientPos = transientFor()->pos() + transientFor()->clientPos();
QRect popupPosition;
// returns if a target is within the supplied bounds, optional edges argument states which side to check
auto inBounds = [bounds](const QRect &target, Qt::Edges edges = Qt::LeftEdge | Qt::RightEdge | Qt::TopEdge | Qt::BottomEdge) -> bool {
if (edges & Qt::LeftEdge && target.left() < bounds.left()) {
return false;
}
if (edges & Qt::TopEdge && target.top() < bounds.top()) {
return false;
}
if (edges & Qt::RightEdge && target.right() > bounds.right()) {
//normal QRect::right issue cancels out
return false;
}
if (edges & Qt::BottomEdge && target.bottom() > bounds.bottom()) {
return false;
}
return true;
};
if (m_shellSurface) {
return m_shellSurface->transientOffset();
anchorRect = QRect(m_shellSurface->transientOffset(), QSize(1,1));
anchorEdge = Qt::TopEdge | Qt::LeftEdge;
gravity = Qt::BottomEdge | Qt::RightEdge; //our single point represents the top left of the popup
constraintAdjustments = (PositionerConstraint::SlideX | PositionerConstraint::SlideY);
} else if (m_xdgShellPopup) {
anchorRect = m_xdgShellPopup->anchorRect();
anchorEdge = m_xdgShellPopup->anchorEdge();
gravity = m_xdgShellPopup->gravity();
offset = m_xdgShellPopup->anchorOffset();
constraintAdjustments = m_xdgShellPopup->constraintAdjustments();
} else {
Q_UNREACHABLE();
}
if (m_xdgShellPopup) {
return m_xdgShellPopup->transientOffset();
//initial position
popupPosition = QRect(popupOffset(anchorRect, anchorEdge, gravity) + offset + parentClientPos, geometry().size());
//if that fits, we don't need to do anything
if (inBounds(popupPosition)) {
return popupPosition;
}
//otherwise apply constraint adjustment per axis in order XDG Shell Popup states
if (constraintAdjustments & PositionerConstraint::FlipX) {
if (!inBounds(popupPosition, Qt::LeftEdge | Qt::RightEdge)) {
//flip both edges (if either bit is set, XOR both)
auto flippedAnchorEdge = anchorEdge;
if (flippedAnchorEdge & (Qt::LeftEdge | Qt::RightEdge)) {
flippedAnchorEdge ^= (Qt::LeftEdge | Qt::RightEdge);
}
auto flippedGravity = gravity;
if (flippedGravity & (Qt::LeftEdge | Qt::RightEdge)) {
flippedGravity ^= (Qt::LeftEdge | Qt::RightEdge);
}
auto flippedPopupPosition = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity) + offset + parentClientPos, geometry().size());
//if it still doesn't fit we should continue with the unflipped version
if (inBounds(flippedPopupPosition, Qt::LeftEdge | Qt::RightEdge)) {
popupPosition.setX(flippedPopupPosition.x());
}
}
}
if (constraintAdjustments & PositionerConstraint::SlideX) {
if (!inBounds(popupPosition, Qt::LeftEdge)) {
popupPosition.setX(bounds.x());
}
if (!inBounds(popupPosition, Qt::RightEdge)) {
popupPosition.setX(bounds.x() + bounds.width() - geometry().width());
}
}
if (constraintAdjustments & PositionerConstraint::ResizeX) {
//TODO
//but we need to sort out when this is run as resize should only happen before first configure
}
if (constraintAdjustments & PositionerConstraint::FlipY) {
if (!inBounds(popupPosition, Qt::TopEdge | Qt::BottomEdge)) {
//flip both edges (if either bit is set, XOR both)
auto flippedAnchorEdge = anchorEdge;
if (flippedAnchorEdge & (Qt::TopEdge | Qt::BottomEdge)) {
flippedAnchorEdge ^= (Qt::TopEdge | Qt::BottomEdge);
}
auto flippedGravity = gravity;
if (flippedGravity & (Qt::TopEdge | Qt::BottomEdge)) {
flippedGravity ^= (Qt::TopEdge | Qt::BottomEdge);
}
auto flippedPopupPosition = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity) + offset + parentClientPos, geometry().size());
//if it still doesn't fit we should continue with the unflipped version
if (inBounds(flippedPopupPosition, Qt::TopEdge | Qt::BottomEdge)) {
popupPosition.setY(flippedPopupPosition.y());
}
}
}
if (constraintAdjustments & PositionerConstraint::SlideY) {
if (!inBounds(popupPosition, Qt::TopEdge)) {
popupPosition.setY(bounds.y());
}
if (!inBounds(popupPosition, Qt::BottomEdge)) {
popupPosition.setY(bounds.y() + bounds.height() - geometry().height());
}
}
return QPoint();
if (constraintAdjustments & PositionerConstraint::ResizeY) {
//TODO
}
return popupPosition;
}
QPoint ShellClient::popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity) const
{
const QSize popupSize = geometry().size();
QPoint anchorPoint;
switch (anchorEdge & (Qt::LeftEdge | Qt::RightEdge)) {
case Qt::LeftEdge:
anchorPoint.setX(anchorRect.x());
break;
case Qt::RightEdge:
anchorPoint.setX(anchorRect.x() + anchorRect.width());
break;
default:
anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0));
}
switch (anchorEdge & (Qt::TopEdge | Qt::BottomEdge)) {
case Qt::TopEdge:
anchorPoint.setY(anchorRect.y());
break;
case Qt::BottomEdge:
anchorPoint.setY(anchorRect.y() + anchorRect.height());
break;
default:
anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0));
}
// calculate where the top left point of the popup will end up with the applied gravity
// gravity indicates direction. i.e if gravitating towards the top the popup's bottom edge
// will next to the anchor point
QPoint popupPosAdjust;
switch (gravity & (Qt::LeftEdge | Qt::RightEdge)) {
case Qt::LeftEdge:
popupPosAdjust.setX(-popupSize.width());
break;
case Qt::RightEdge:
popupPosAdjust.setX(0);
break;
default:
popupPosAdjust.setX(qRound(-popupSize.width() / 2.0));
}
switch (gravity & (Qt::TopEdge | Qt::BottomEdge)) {
case Qt::TopEdge:
popupPosAdjust.setY(-popupSize.height());
break;
case Qt::BottomEdge:
popupPosAdjust.setY(0);
break;
default:
popupPosAdjust.setY(qRound(-popupSize.height() / 2.0));
}
return anchorPoint + popupPosAdjust;
}
bool ShellClient::isWaitingForMoveResizeSync() const
......
......@@ -147,7 +147,7 @@ public:
bool isTransient() const override;
bool hasTransientPlacementHint() const override;
QPoint transientPlacementHint() const override;
QRect transientPlacement(const QRect &bounds) const override;
QMatrix4x4 inputTransformation() const override;
......@@ -210,6 +210,7 @@ private:
void updateMaximizeMode(MaximizeMode maximizeMode);
// called on surface commit and processes all m_pendingConfigureRequests up to m_lastAckedConfigureReqest
void updatePendingGeometry();
QPoint popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity) const;
static void deleteClient(ShellClient *c);
KWayland::Server::ShellSurfaceInterface *m_shellSurface;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment