File indexing completed on 2025-02-09 06:41:29

0001 /*
0002     SPDX-FileCopyrightText: 2023 David Edmundson <davidedmundson@kde.org>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 
0006 #include "transientplacementhint_p.h"
0007 #include <QSharedData>
0008 
0009 #include <QDebug>
0010 #include <QGuiApplication>
0011 #include <QScreen>
0012 #include <QWindow>
0013 
0014 // This class is proposed for Qt6.something, but it's not there yet.
0015 // keep as an implementation detail, and then drop eventually (famous last words)
0016 
0017 class TransientPlacementHintPrivate : public QSharedData
0018 {
0019 public:
0020     QRect parentAnchorRect;
0021     Qt::Edges parentAnchor = Qt::BottomEdge | Qt::RightEdge;
0022     Qt::Edges popupAnchor = Qt::TopEdge | Qt::LeftEdge;
0023     Qt::Orientations slideConstraintAdjustments = Qt::Horizontal | Qt::Vertical;
0024     Qt::Orientations flipConstraintAdjustments;
0025     int margin = 0;
0026 };
0027 /*!
0028  * Constructs a new QTransientPlacementHint
0029  */
0030 TransientPlacementHint::TransientPlacementHint()
0031     : d(new TransientPlacementHintPrivate)
0032 {
0033 }
0034 
0035 TransientPlacementHint::~TransientPlacementHint()
0036 {
0037 }
0038 
0039 TransientPlacementHint::TransientPlacementHint(const TransientPlacementHint &other)
0040 {
0041     d = other.d;
0042 }
0043 TransientPlacementHint &TransientPlacementHint::operator=(const TransientPlacementHint &other)
0044 {
0045     d = other.d;
0046     return *this;
0047 }
0048 
0049 bool TransientPlacementHint::isValid() const
0050 {
0051     return d->parentAnchorRect.isValid();
0052 }
0053 
0054 void TransientPlacementHint::setParentAnchorArea(const QRect &parentAnchorRect)
0055 {
0056     d->parentAnchorRect = parentAnchorRect;
0057 }
0058 
0059 QRect TransientPlacementHint::parentAnchorArea() const
0060 {
0061     return d->parentAnchorRect;
0062 }
0063 
0064 void TransientPlacementHint::setParentAnchor(Qt::Edges parentAnchor)
0065 {
0066     d->parentAnchor = parentAnchor;
0067 }
0068 
0069 Qt::Edges TransientPlacementHint::parentAnchor() const
0070 {
0071     return d->parentAnchor;
0072 }
0073 
0074 void TransientPlacementHint::setPopupAnchor(Qt::Edges popupAnchor)
0075 {
0076     d->popupAnchor = popupAnchor;
0077 }
0078 
0079 Qt::Edges TransientPlacementHint::popupAnchor() const
0080 {
0081     return d->popupAnchor;
0082 }
0083 
0084 void TransientPlacementHint::setSlideConstraintAdjustments(Qt::Orientations slideConstraintAdjustments)
0085 {
0086     d->slideConstraintAdjustments = slideConstraintAdjustments;
0087 }
0088 
0089 Qt::Orientations TransientPlacementHint::slideConstraintAdjustments() const
0090 {
0091     return d->slideConstraintAdjustments;
0092 }
0093 
0094 void TransientPlacementHint::setFlipConstraintAdjustments(Qt::Orientations flipConstraintAdjustments)
0095 {
0096     d->flipConstraintAdjustments = flipConstraintAdjustments;
0097 }
0098 
0099 Qt::Orientations TransientPlacementHint::flipConstraintAdjustments() const
0100 {
0101     return d->flipConstraintAdjustments;
0102 }
0103 
0104 int TransientPlacementHint::margin() const
0105 {
0106     return d->margin;
0107 }
0108 
0109 void TransientPlacementHint::setMargin(int margin)
0110 {
0111     d->margin = margin;
0112 }
0113 
0114 static QPoint popupPosition(const QRect &anchorRect, const Qt::Edges parentAnchor, const Qt::Edges popupAnchor, const QSize &popupSize)
0115 {
0116     QPoint anchorPoint;
0117     switch (parentAnchor & (Qt::LeftEdge | Qt::RightEdge)) {
0118     case Qt::LeftEdge:
0119         anchorPoint.setX(anchorRect.x());
0120         break;
0121     case Qt::RightEdge:
0122         anchorPoint.setX(anchorRect.x() + anchorRect.width());
0123         break;
0124     default:
0125         anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0));
0126     }
0127     switch (parentAnchor & (Qt::TopEdge | Qt::BottomEdge)) {
0128     case Qt::TopEdge:
0129         anchorPoint.setY(anchorRect.y());
0130         break;
0131     case Qt::BottomEdge:
0132         anchorPoint.setY(anchorRect.y() + anchorRect.height());
0133         break;
0134     default:
0135         anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0));
0136     }
0137     // calculate where the top left point of the popup will end up with the applied popup anchor
0138     QPoint popupPosAdjust;
0139     switch (popupAnchor & (Qt::LeftEdge | Qt::RightEdge)) {
0140     case Qt::LeftEdge:
0141         popupPosAdjust.setX(0);
0142         break;
0143     case Qt::RightEdge:
0144         popupPosAdjust.setX(-popupSize.width());
0145         break;
0146     default:
0147         popupPosAdjust.setX(qRound(-popupSize.width() / 2.0));
0148     }
0149     switch (popupAnchor & (Qt::TopEdge | Qt::BottomEdge)) {
0150     case Qt::TopEdge:
0151         popupPosAdjust.setY(0);
0152         break;
0153     case Qt::BottomEdge:
0154         popupPosAdjust.setY(-popupSize.height());
0155         break;
0156     default:
0157         popupPosAdjust.setY(qRound(-popupSize.height() / 2.0));
0158     }
0159     return anchorPoint + popupPosAdjust;
0160 }
0161 
0162 QRect TransientPlacementHelper::popupRect(QWindow *w, const TransientPlacementHint &placement)
0163 {
0164     // We are not checking the placement being valid, as visual parents with size 0 is an
0165     // allowed thing, also, every PlasmoidItem will initially be 0x0 when created
0166     QScreen *screen = nullptr;
0167     QRect globalParentAnchorRect = placement.parentAnchorArea();
0168     if (w->transientParent()) {
0169         globalParentAnchorRect = globalParentAnchorRect.translated(w->transientParent()->position());
0170         screen = w->transientParent()->screen();
0171     }
0172 
0173     const QMargins margin(placement.margin(), placement.margin(), placement.margin(), placement.margin());
0174     QSize paddedWindowSize = w->size().grownBy(margin);
0175     QRect popupRect = QRect(popupPosition(globalParentAnchorRect, placement.parentAnchor(), placement.popupAnchor(), paddedWindowSize), paddedWindowSize);
0176 
0177     if (!screen)
0178         screen = qApp->screenAt(globalParentAnchorRect.center());
0179     if (!screen)
0180         screen = qApp->primaryScreen();
0181 
0182     const QRect screenArea = screen->geometry();
0183 
0184     auto inScreenArea = [screenArea](const QRect &target, Qt::Edges edges = Qt::LeftEdge | Qt::RightEdge | Qt::TopEdge | Qt::BottomEdge) -> bool {
0185         if (edges & Qt::LeftEdge && target.left() < screenArea.left()) {
0186             return false;
0187         }
0188         if (edges & Qt::TopEdge && target.top() < screenArea.top()) {
0189             return false;
0190         }
0191         if (edges & Qt::RightEdge && target.right() > screenArea.right()) {
0192             return false;
0193         }
0194         if (edges & Qt::BottomEdge && target.bottom() > screenArea.bottom()) {
0195             return false;
0196         }
0197         return true;
0198     };
0199 
0200     // if that fits, we don't need to do anything
0201     if (inScreenArea(popupRect)) {
0202         return popupRect.marginsRemoved(margin);
0203     }
0204     // Otherwise,
0205     if (placement.flipConstraintAdjustments() & Qt::Horizontal) {
0206         if (!inScreenArea(popupRect, Qt::LeftEdge | Qt::RightEdge)) {
0207             // flip both edges (if either bit is set, XOR both)
0208             auto flippedParentAnchor = placement.parentAnchor();
0209             if (flippedParentAnchor & (Qt::LeftEdge | Qt::RightEdge)) {
0210                 flippedParentAnchor ^= (Qt::LeftEdge | Qt::RightEdge);
0211             }
0212             auto flippedPopupAnchor = placement.popupAnchor();
0213             if (flippedPopupAnchor & (Qt::LeftEdge | Qt::RightEdge)) {
0214                 flippedPopupAnchor ^= (Qt::LeftEdge | Qt::RightEdge);
0215             }
0216             QRect flippedPopupRect = QRect(popupPosition(globalParentAnchorRect, flippedParentAnchor, flippedPopupAnchor, w->size()), w->size());
0217             // if it still doesn't fit we should continue with the unflipped version
0218             if (inScreenArea(flippedPopupRect, Qt::LeftEdge | Qt::RightEdge)) {
0219                 popupRect.moveLeft(flippedPopupRect.left());
0220             }
0221         }
0222     }
0223     if (placement.slideConstraintAdjustments() & Qt::Horizontal) {
0224         if (!inScreenArea(popupRect, Qt::LeftEdge)) {
0225             popupRect.moveLeft(screenArea.left());
0226         }
0227         if (!inScreenArea(popupRect, Qt::RightEdge)) {
0228             popupRect.moveRight(screenArea.right());
0229         }
0230     }
0231     if (placement.flipConstraintAdjustments() & Qt::Vertical) {
0232         if (!inScreenArea(popupRect, Qt::TopEdge | Qt::BottomEdge)) {
0233             // flip both edges (if either bit is set, XOR both)
0234             auto flippedParentAnchor = placement.parentAnchor();
0235             if (flippedParentAnchor & (Qt::TopEdge | Qt::BottomEdge)) {
0236                 flippedParentAnchor ^= (Qt::TopEdge | Qt::BottomEdge);
0237             }
0238             auto flippedPopupAnchor = placement.popupAnchor();
0239             if (flippedPopupAnchor & (Qt::TopEdge | Qt::BottomEdge)) {
0240                 flippedPopupAnchor ^= (Qt::TopEdge | Qt::BottomEdge);
0241             }
0242             QRect flippedPopupRect = QRect(popupPosition(globalParentAnchorRect, flippedParentAnchor, flippedPopupAnchor, w->size()), w->size());
0243             // if it still doesn't fit we should continue with the unflipped version
0244             if (inScreenArea(flippedPopupRect, Qt::TopEdge | Qt::BottomEdge)) {
0245                 popupRect.moveTop(flippedPopupRect.top());
0246             }
0247         }
0248     }
0249     if (placement.slideConstraintAdjustments() & Qt::Vertical) {
0250         if (!inScreenArea(popupRect, Qt::TopEdge)) {
0251             popupRect.moveTop(screenArea.top());
0252         }
0253         if (!inScreenArea(popupRect, Qt::BottomEdge)) {
0254             popupRect.moveBottom(screenArea.bottom());
0255         }
0256     }
0257     return popupRect.marginsRemoved(margin);
0258 }