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 }