File indexing completed on 2024-05-12 16:02:29

0001 /*
0002  *  SPDX-FileCopyrightText: 2007 Cyrille Berger <cberger@cberger.net>
0003  *  SPDX-FileCopyrightText: 2008 Boudewijn Rempt <boud@valdyas.org>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "KisPopupButton.h"
0009 
0010 #include <QPointer>
0011 #include <QApplication>
0012 #include <QFrame>
0013 #include <QHBoxLayout>
0014 #include <QKeyEvent>
0015 #include <QScreen>
0016 #include <QStyleOption>
0017 #include <QStylePainter>
0018 #include <QWindow>
0019 
0020 #include "kis_global.h"
0021 #include <kis_debug.h>
0022 
0023 
0024 class KisPopupButtonFrame : public QFrame
0025 {
0026 public:
0027     KisPopupButtonFrame(QWidget *parent, bool detach)
0028         : QFrame(parent)
0029     {
0030         setDetached(detach);
0031     }
0032 
0033     void setDetached(bool detach)
0034     {
0035 #if defined Q_OS_ANDROID || defined Q_OS_MACOS
0036         // for some reason when calling destroy() the platform window isn't
0037         // hidden first, this corrupts state of the window stack
0038         hide();
0039 #endif
0040 
0041         // Need to destroy the platform window before changing window flags
0042         // so that Qt knows to actually apply the new flags...
0043         // At least on Windows, not doing this may result in weird window drop
0044         // shadows.
0045         destroy();
0046         if (detach) {
0047             setWindowFlags(Qt::Dialog);
0048             setFrameStyle(QFrame::NoFrame);
0049         } else {
0050             setWindowFlags(Qt::Popup);
0051             setFrameStyle(QFrame::Box | QFrame::Plain);
0052         }
0053     }
0054 
0055 protected:
0056     void keyPressEvent(QKeyEvent *event) override
0057     {
0058         if (event->matches(QKeySequence::Cancel)) {
0059             event->accept();
0060             hide();
0061         } else {
0062             QFrame::keyPressEvent(event);
0063         }
0064     }
0065 
0066     bool event(QEvent *e) override
0067     {
0068         if (e->type() == QEvent::Close) {
0069             e->ignore();
0070             hide();
0071             return true;
0072         }
0073         return QFrame::event(e);
0074     }
0075 };
0076 
0077 
0078 struct KisPopupButton::Private {
0079     Private()
0080         : frameLayout(nullptr)
0081     {}
0082     QPointer<KisPopupButtonFrame> frame;
0083     QPointer<QWidget> popupWidget;
0084     QPointer<QHBoxLayout> frameLayout;
0085     bool arrowVisible { true };
0086     bool isPopupDetached { false };
0087     bool isDetachedGeometrySet { false };
0088 };
0089 
0090 KisPopupButton::KisPopupButton(QWidget* parent)
0091         : QToolButton(parent)
0092         , m_d(new Private)
0093 {
0094     setObjectName("KisPopupButton");
0095     connect(this, SIGNAL(released()), SLOT(showPopupWidget()));
0096 }
0097 
0098 KisPopupButton::~KisPopupButton()
0099 {
0100     delete m_d->frame;
0101     delete m_d;
0102 }
0103 
0104 void KisPopupButton::setPopupWidgetDetached(bool detach)
0105 {
0106     m_d->isPopupDetached = detach;
0107     if (m_d->frame) {
0108         bool wasVisible = isPopupWidgetVisible();
0109         m_d->frame->setDetached(detach);
0110         if (wasVisible) {
0111             // Setting the window flags closes the widget, so make it visible again.
0112             setPopupWidgetVisible(true);
0113             if (detach) {
0114                 m_d->isDetachedGeometrySet = true;
0115             }
0116             adjustPosition();
0117         }
0118     }
0119 }
0120 
0121 void KisPopupButton::setPopupWidget(QWidget* widget)
0122 {
0123     if (widget) {
0124         delete m_d->frame;
0125         m_d->frame = new KisPopupButtonFrame(this->window(), m_d->isPopupDetached);
0126         m_d->frame->setProperty("_kis_excludeFromLayoutThumbnail", true);
0127         m_d->frame->setObjectName("popup frame");
0128         m_d->frame->setWindowTitle(widget->windowTitle());
0129         m_d->frameLayout = new QHBoxLayout(m_d->frame.data());
0130         m_d->frameLayout->setMargin(0);
0131         m_d->frameLayout->setSizeConstraint(QLayout::SetFixedSize);
0132         m_d->frame->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
0133         m_d->popupWidget = widget;
0134         m_d->popupWidget->setParent(m_d->frame.data());
0135         m_d->frameLayout->addWidget(m_d->popupWidget);
0136     }
0137 }
0138 
0139 void KisPopupButton::setPopupWidgetWidth(int w)
0140 {
0141     m_d->frame->resize(w, m_d->frame->height());
0142 }
0143 
0144 void KisPopupButton::showPopupWidget()
0145 {
0146     if (m_d->popupWidget && !m_d->frame->isVisible()) {
0147         setPopupWidgetVisible(true);
0148         adjustPosition();
0149     } else {
0150         hidePopupWidget();
0151     }
0152 }
0153 
0154 void KisPopupButton::hidePopupWidget()
0155 {
0156     setPopupWidgetVisible(false);
0157 }
0158 
0159 void KisPopupButton::setPopupWidgetVisible(bool visible)
0160 {
0161     if (m_d->popupWidget) {
0162         if (visible) {
0163             m_d->frame->raise();
0164             m_d->frame->show();
0165             m_d->frame->activateWindow();
0166         } else {
0167             m_d->frame->setVisible(false);
0168         }
0169     }
0170 }
0171 
0172 bool KisPopupButton::isPopupWidgetVisible()
0173 {
0174     return m_d->popupWidget && m_d->frame->isVisible();
0175 }
0176 
0177 void KisPopupButton::paintEvent ( QPaintEvent * event  )
0178 {
0179     QToolButton::paintEvent(event);
0180     if (m_d->arrowVisible) {
0181         paintPopupArrow();
0182     }
0183 }
0184 
0185 void KisPopupButton::paintPopupArrow()
0186 {
0187     QStylePainter p(this);
0188     QStyleOption option;
0189     option.rect = QRect(rect().right() - 15, rect().bottom() - 15, 14, 14);
0190     option.palette = palette();
0191     option.palette.setBrush(QPalette::ButtonText, Qt::black); // Force color to black
0192     option.state = QStyle::State_Enabled;
0193     p.setBrush(Qt::black); // work around some theme that don't use QPalette::ButtonText like they  should, but instead the QPainter brushes and pen
0194     p.setPen(Qt::black);
0195     p.drawPrimitive(QStyle::PE_IndicatorArrowDown, option);
0196 }
0197 
0198 void KisPopupButton::adjustPosition()
0199 {
0200     // If popup is not detached, or if its detached geometry hasn't been set,
0201     // we first move the popup to the "current" screen.
0202     if (!m_d->isPopupDetached || !m_d->isDetachedGeometrySet) {
0203         QScreen *currentScreen = [this]() {
0204             QWindow *mainWinHandle = this->window()->windowHandle();
0205             if (mainWinHandle) {
0206                 return mainWinHandle->screen();
0207             }
0208             return QApplication::primaryScreen();
0209         }();
0210         QWindow *winHandle = m_d->frame->windowHandle();
0211         if (winHandle) {
0212             winHandle->setScreen(currentScreen);
0213         }
0214     }
0215 
0216     QSize popSize = m_d->popupWidget->size();
0217     QRect popupRect(this->mapToGlobal(QPoint(0, this->size().height())), popSize);
0218 
0219     // Get the available geometry of the screen which contains the popup.
0220     QScreen *screen = [this]() {
0221         QWindow *winHandle = m_d->frame->windowHandle();
0222         if (winHandle && winHandle->screen()) {
0223             return winHandle->screen();
0224         }
0225         return QApplication::primaryScreen();
0226     }();
0227     QRect screenRect = screen->availableGeometry();
0228     if (m_d->isPopupDetached) {
0229         if (m_d->isDetachedGeometrySet) {
0230             popupRect.moveTo(m_d->frame->geometry().topLeft());
0231         } else {
0232             popupRect.moveTo(this->window()->geometry().center() - QRect(QPoint(0, 0), popSize).center());
0233             m_d->isDetachedGeometrySet = true;
0234         }
0235     }
0236     popupRect = kisEnsureInRect(popupRect, screenRect);
0237 
0238     m_d->frame->setGeometry(popupRect);
0239 }
0240 
0241 void KisPopupButton::setArrowVisible (bool v)
0242 {
0243     if (v) {
0244         m_d->arrowVisible = true;
0245     } else {
0246         m_d->arrowVisible = false;
0247     }
0248 }