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

0001 /*
0002     SPDX-FileCopyrightText: 2023 David Edmundson <davidedmundson@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "appletpopup.h"
0008 
0009 #include <QGuiApplication>
0010 #include <QQmlProperty>
0011 #include <qpa/qplatformwindow.h> // for QWINDOWSIZE_MAX
0012 
0013 #include <KConfigGroup>
0014 #include <KWindowSystem>
0015 
0016 #include "applet.h"
0017 #include "appletquickitem.h"
0018 #include "edgeeventforwarder.h"
0019 #include "waylandintegration_p.h"
0020 #include "windowresizehandler.h"
0021 
0022 // used in detecting if focus passes to config UI
0023 #include "configview.h"
0024 #include "private/utils.h"
0025 #include "sharedqmlengine.h"
0026 
0027 // This is a proxy object that connects to the Layout attached property of an item
0028 // it also handles turning properties to proper defaults
0029 // we need a wrapper as QQmlProperty can't disconnect
0030 
0031 namespace PlasmaQuick
0032 {
0033 
0034 class LayoutChangedProxy : public QObject
0035 {
0036     Q_OBJECT
0037 public:
0038     LayoutChangedProxy(QQuickItem *item);
0039     QSize minimumSize() const;
0040     QSize maximumSize() const;
0041     QSize implicitSize() const;
0042 Q_SIGNALS:
0043     void implicitSizeChanged();
0044     void minimumSizeChanged();
0045     void maximumSizeChanged();
0046 
0047 private:
0048     QQmlProperty m_minimumWidth;
0049     QQmlProperty m_maximumWidth;
0050     QQmlProperty m_minimumHeight;
0051     QQmlProperty m_maximumHeight;
0052     QQmlProperty m_preferredWidth;
0053     QQmlProperty m_preferredHeight;
0054     QPointer<QQuickItem> m_item;
0055 };
0056 }
0057 
0058 using namespace PlasmaQuick;
0059 
0060 AppletPopup::AppletPopup()
0061     : PopupPlasmaWindow()
0062 {
0063     setAnimated(true);
0064     setFlags(flags() | Qt::Dialog);
0065 
0066     PlasmaShellWaylandIntegration::get(this)->setRole(QtWayland::org_kde_plasma_surface::role::role_appletpopup);
0067 
0068     auto edgeForwarder = new EdgeEventForwarder(this);
0069     edgeForwarder->setMargins(padding());
0070     connect(this, &PlasmaWindow::paddingChanged, this, [edgeForwarder, this]() {
0071         edgeForwarder->setMargins(padding());
0072     });
0073     // edges that have a border are not on a screen edge
0074     // we want to forward on sides touching screen edges
0075     edgeForwarder->setActiveEdges(~borders());
0076     connect(this, &PlasmaWindow::bordersChanged, this, [edgeForwarder, this]() {
0077         edgeForwarder->setActiveEdges(~borders());
0078     });
0079 
0080     auto windowResizer = new WindowResizeHandler(this);
0081     windowResizer->setMargins(padding());
0082     connect(this, &PlasmaWindow::paddingChanged, this, [windowResizer, this]() {
0083         windowResizer->setMargins(padding());
0084     });
0085 
0086     auto updateWindowResizerEdges = [windowResizer, this]() {
0087         Qt::Edges activeEdges = borders();
0088         activeEdges.setFlag(PlasmaQuickPrivate::oppositeEdge(effectivePopupDirection()), false);
0089         windowResizer->setActiveEdges(activeEdges);
0090     };
0091     updateWindowResizerEdges();
0092     connect(this, &PlasmaWindow::bordersChanged, this, updateWindowResizerEdges);
0093     connect(this, &PopupPlasmaWindow::effectivePopupDirectionChanged, this, updateWindowResizerEdges);
0094 
0095     connect(this, &PlasmaWindow::mainItemChanged, this, &AppletPopup::onMainItemChanged);
0096     connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateMaxSize);
0097     connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateSize);
0098     connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateMinSize);
0099 
0100     connect(this, &PlasmaWindow::screenChanged, this, [this](QScreen *screen) {
0101         if (m_oldScreen) {
0102             disconnect(m_oldScreen, &QScreen::geometryChanged, this, &AppletPopup::updateMaxSize);
0103         }
0104         if (screen) {
0105             connect(screen, &QScreen::geometryChanged, this, &AppletPopup::updateMaxSize);
0106         }
0107         m_oldScreen = screen;
0108         updateMaxSize();
0109     });
0110 }
0111 
0112 AppletPopup::~AppletPopup()
0113 {
0114 }
0115 
0116 QQuickItem *AppletPopup::appletInterface() const
0117 {
0118     return m_appletInterface.data();
0119 }
0120 
0121 void AppletPopup::setAppletInterface(QQuickItem *appletInterface)
0122 {
0123     if (appletInterface == m_appletInterface) {
0124         return;
0125     }
0126 
0127     m_appletInterface = qobject_cast<AppletQuickItem *>(appletInterface);
0128     m_sizeExplicitlySetFromConfig = false;
0129 
0130     if (m_appletInterface) {
0131         KConfigGroup config = m_appletInterface->applet()->config();
0132         QSize size;
0133         size.rwidth() = config.readEntry("popupWidth", 0);
0134         size.rheight() = config.readEntry("popupHeight", 0);
0135         if (!size.isEmpty()) {
0136             m_sizeExplicitlySetFromConfig = true;
0137             resize(size.grownBy(padding()));
0138             return;
0139         }
0140     }
0141 
0142     Q_EMIT appletInterfaceChanged();
0143 }
0144 
0145 bool AppletPopup::hideOnWindowDeactivate() const
0146 {
0147     return m_hideOnWindowDeactivate;
0148 }
0149 
0150 void AppletPopup::setHideOnWindowDeactivate(bool hideOnWindowDeactivate)
0151 {
0152     if (hideOnWindowDeactivate == m_hideOnWindowDeactivate) {
0153         return;
0154     }
0155     m_hideOnWindowDeactivate = hideOnWindowDeactivate;
0156     Q_EMIT hideOnWindowDeactivateChanged();
0157 }
0158 
0159 void AppletPopup::hideEvent(QHideEvent *event)
0160 {
0161     // Persist the size if this contains an applet
0162     if (m_appletInterface) {
0163         KConfigGroup config = m_appletInterface->applet()->config();
0164         // save size without margins, so we're robust against theme changes
0165         const QSize popupSize = size().shrunkBy(padding());
0166         config.writeEntry("popupWidth", popupSize.width());
0167         config.writeEntry("popupHeight", popupSize.height());
0168         config.sync();
0169     }
0170 
0171     PopupPlasmaWindow::hideEvent(event);
0172 }
0173 
0174 void AppletPopup::focusOutEvent(QFocusEvent *ev)
0175 {
0176     if (m_hideOnWindowDeactivate) {
0177         bool parentHasFocus = false;
0178 
0179         QWindow *parentWindow = transientParent();
0180 
0181         while (parentWindow) {
0182             if (parentWindow->isActive() && !(parentWindow->flags() & Qt::WindowDoesNotAcceptFocus)) {
0183                 parentHasFocus = true;
0184                 break;
0185             }
0186 
0187             parentWindow = parentWindow->transientParent();
0188         }
0189 
0190         const QWindow *focusWindow = QGuiApplication::focusWindow();
0191         bool childHasFocus = focusWindow && ((focusWindow->isActive() && isAncestorOf(focusWindow)) || (focusWindow->type() & Qt::Popup) == Qt::Popup);
0192 
0193         const bool viewClicked = qobject_cast<const PlasmaQuick::SharedQmlEngine *>(focusWindow) || qobject_cast<const ConfigView *>(focusWindow);
0194 
0195         if (viewClicked || (!parentHasFocus && !childHasFocus)) {
0196             setVisible(false);
0197         }
0198     }
0199 
0200     PopupPlasmaWindow::focusOutEvent(ev);
0201 }
0202 
0203 void AppletPopup::onMainItemChanged()
0204 {
0205     QQuickItem *mainItem = PlasmaWindow::mainItem();
0206     if (!mainItem) {
0207         m_layoutChangedProxy.reset();
0208         return;
0209     }
0210 
0211     // update window to mainItem size hints
0212     m_layoutChangedProxy.reset(new LayoutChangedProxy(mainItem));
0213     connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::maximumSizeChanged, this, &AppletPopup::updateMaxSize);
0214     connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::minimumSizeChanged, this, &AppletPopup::updateMinSize);
0215     connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::implicitSizeChanged, this, &AppletPopup::updateSize);
0216 
0217     updateMinSize();
0218     updateMaxSize();
0219     updateSize();
0220 }
0221 
0222 void AppletPopup::updateMinSize()
0223 {
0224     if (!m_layoutChangedProxy) {
0225         return;
0226     }
0227     setMinimumSize(m_layoutChangedProxy->minimumSize().grownBy(padding()));
0228 }
0229 
0230 void AppletPopup::updateMaxSize()
0231 {
0232     if (!m_layoutChangedProxy) {
0233         return;
0234     }
0235     QSize size = m_layoutChangedProxy->maximumSize().grownBy(padding());
0236     if (screen()) {
0237         size.setWidth(std::min(size.width(), int(std::round(screen()->geometry().width() * 0.95))));
0238         size.setHeight(std::min(size.height(), int(std::round(screen()->geometry().height() * 0.95))));
0239     }
0240     setMaximumSize(size);
0241 }
0242 
0243 void AppletPopup::updateSize()
0244 {
0245     if (m_sizeExplicitlySetFromConfig) {
0246         return;
0247     }
0248     if (!m_layoutChangedProxy) {
0249         return;
0250     }
0251     resize(m_layoutChangedProxy->implicitSize().grownBy(padding()));
0252 }
0253 
0254 LayoutChangedProxy::LayoutChangedProxy(QQuickItem *item)
0255     : m_item(item)
0256 {
0257     m_minimumWidth = QQmlProperty(item, QStringLiteral("Layout.minimumWidth"), qmlContext(item));
0258     m_minimumHeight = QQmlProperty(item, QStringLiteral("Layout.minimumHeight"), qmlContext(item));
0259     m_maximumWidth = QQmlProperty(item, QStringLiteral("Layout.maximumWidth"), qmlContext(item));
0260     m_maximumHeight = QQmlProperty(item, QStringLiteral("Layout.maximumHeight"), qmlContext(item));
0261     m_preferredWidth = QQmlProperty(item, QStringLiteral("Layout.preferredWidth"), qmlContext(item));
0262     m_preferredHeight = QQmlProperty(item, QStringLiteral("Layout.preferredHeight"), qmlContext(item));
0263 
0264     m_minimumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
0265     m_minimumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
0266     m_maximumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
0267     m_maximumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
0268     m_preferredWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
0269     m_preferredHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
0270     connect(item, &QQuickItem::implicitWidthChanged, this, &LayoutChangedProxy::implicitSizeChanged);
0271     connect(item, &QQuickItem::implicitHeightChanged, this, &LayoutChangedProxy::implicitSizeChanged);
0272 }
0273 
0274 QSize LayoutChangedProxy::maximumSize() const
0275 {
0276     QSize size(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX);
0277     qreal width = m_maximumWidth.read().toReal();
0278     if (qIsFinite(width) && int(width) > 0) {
0279         size.setWidth(width);
0280     }
0281     qreal height = m_maximumHeight.read().toReal();
0282     if (qIsFinite(height) && int(height) > 0) {
0283         size.setHeight(height);
0284     }
0285 
0286     return size;
0287 }
0288 
0289 QSize LayoutChangedProxy::implicitSize() const
0290 {
0291     QSize size(200, 200);
0292 
0293     // Layout.preferredSize has precedent over implicit in layouts
0294     // so mimic that behaviour here
0295     if (m_item) {
0296         size = QSize(m_item->implicitWidth(), m_item->implicitHeight());
0297     }
0298     qreal width = m_preferredWidth.read().toReal();
0299     if (qIsFinite(width) && int(width) > 0) {
0300         size.setWidth(width);
0301     }
0302     qreal height = m_preferredHeight.read().toReal();
0303     if (qIsFinite(height) && int(height) > 0) {
0304         size.setHeight(height);
0305     }
0306     return size;
0307 }
0308 
0309 QSize LayoutChangedProxy::minimumSize() const
0310 {
0311     QSize size(0, 0);
0312     qreal width = m_minimumWidth.read().toReal();
0313     if (qIsFinite(width) && int(width) > 0) {
0314         size.setWidth(width);
0315     }
0316     qreal height = m_minimumHeight.read().toReal();
0317     if (qIsFinite(height) && int(height) > 0) {
0318         size.setHeight(height);
0319     }
0320 
0321     return size;
0322 }
0323 
0324 #include "appletpopup.moc"