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"