Plasma-framework

appletpopup.cpp
1/*
2 SPDX-FileCopyrightText: 2023 David Edmundson <davidedmundson@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "appletpopup.h"
8
9#include <QGuiApplication>
10#include <QQmlProperty>
11#include <qpa/qplatformwindow.h> // for QWINDOWSIZE_MAX
12
13#include <KConfigGroup>
14#include <KWindowSystem>
15#include <KX11Extras>
16#include <QSize>
17
18#include "applet.h"
19#include "appletquickitem.h"
20#include "edgeeventforwarder.h"
21#include "plasmashellwaylandintegration.h"
22#include "windowresizehandler.h"
23
24// used in detecting if focus passes to config UI
25#include "configview.h"
26#include "private/utils.h"
27#include "sharedqmlengine.h"
28
29// This is a proxy object that connects to the Layout attached property of an item
30// it also handles turning properties to proper defaults
31// we need a wrapper as QQmlProperty can't disconnect
32
33namespace PlasmaQuick
34{
35
36class LayoutChangedProxy : public QObject
37{
39public:
40 LayoutChangedProxy(QQuickItem *item);
41 QSize minimumSize() const;
42 QSize maximumSize() const;
43 QSize implicitSize() const;
45 void implicitSizeChanged();
46 void minimumSizeChanged();
47 void maximumSizeChanged();
48
49private:
50 QQmlProperty m_minimumWidth;
51 QQmlProperty m_maximumWidth;
52 QQmlProperty m_minimumHeight;
53 QQmlProperty m_maximumHeight;
54 QQmlProperty m_preferredWidth;
55 QQmlProperty m_preferredHeight;
57};
58}
59
60using namespace PlasmaQuick;
61
62AppletPopup::AppletPopup()
64{
65 setAnimated(true);
66 setFlags(flags() | Qt::Dialog);
67
70 } else {
71 PlasmaShellWaylandIntegration::get(this)->setRole(QtWayland::org_kde_plasma_surface::role::role_appletpopup);
72 }
73
74 auto edgeForwarder = new EdgeEventForwarder(this);
75 edgeForwarder->setMargins(padding());
76 connect(this, &PlasmaWindow::paddingChanged, this, [edgeForwarder, this]() {
77 edgeForwarder->setMargins(padding());
78 });
79 // edges that have a border are not on a screen edge
80 // we want to forward on sides touching screen edges
81 edgeForwarder->setActiveEdges(~borders());
82 connect(this, &PlasmaWindow::bordersChanged, this, [edgeForwarder, this]() {
83 edgeForwarder->setActiveEdges(~borders());
84 });
85
86 auto windowResizer = new WindowResizeHandler(this);
87 windowResizer->setMargins(padding());
88 connect(this, &PlasmaWindow::paddingChanged, this, [windowResizer, this]() {
89 windowResizer->setMargins(padding());
90 });
91
92 auto updateWindowResizerEdges = [windowResizer, this]() {
93 Qt::Edges activeEdges = borders();
94 activeEdges.setFlag(PlasmaQuickPrivate::oppositeEdge(effectivePopupDirection()), false);
95 windowResizer->setActiveEdges(activeEdges);
96 };
97 updateWindowResizerEdges();
98 connect(this, &PlasmaWindow::bordersChanged, this, updateWindowResizerEdges);
99 connect(this, &PopupPlasmaWindow::effectivePopupDirectionChanged, this, updateWindowResizerEdges);
100
101 connect(this, &PlasmaWindow::mainItemChanged, this, &AppletPopup::onMainItemChanged);
102 connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateMaxSize);
103 connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateSize);
104 connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateMinSize);
105
106 connect(this, &PlasmaWindow::screenChanged, this, [this](QScreen *screen) {
107 if (m_oldScreen) {
108 disconnect(m_oldScreen, &QScreen::geometryChanged, this, &AppletPopup::updateMaxSize);
109 }
110 if (screen) {
111 connect(screen, &QScreen::geometryChanged, this, &AppletPopup::updateMaxSize);
112 }
113 m_oldScreen = screen;
114 updateMaxSize();
115 });
116}
117
118AppletPopup::~AppletPopup()
119{
120}
121
123{
124 return m_appletInterface.data();
125}
126
127void AppletPopup::setAppletInterface(QQuickItem *appletInterface)
128{
129 if (appletInterface == m_appletInterface) {
130 return;
131 }
132
133 m_appletInterface = qobject_cast<AppletQuickItem *>(appletInterface);
134 m_sizeExplicitlySetFromConfig = false;
135
136 if (m_appletInterface) {
137 KConfigGroup config = m_appletInterface->applet()->config();
138 QSize size;
139 size.rwidth() = config.readEntry("popupWidth", 0);
140 size.rheight() = config.readEntry("popupHeight", 0);
141 if (!size.isEmpty()) {
142 m_sizeExplicitlySetFromConfig = true;
143 resize(size.grownBy(padding()));
144 return;
145 }
146 }
147
148 Q_EMIT appletInterfaceChanged();
149}
150
152{
153 return m_hideOnWindowDeactivate;
154}
155
156void AppletPopup::setHideOnWindowDeactivate(bool hideOnWindowDeactivate)
157{
158 if (hideOnWindowDeactivate == m_hideOnWindowDeactivate) {
159 return;
160 }
161 m_hideOnWindowDeactivate = hideOnWindowDeactivate;
162 Q_EMIT hideOnWindowDeactivateChanged();
163}
164
165void AppletPopup::hideEvent(QHideEvent *event)
166{
167 // Persist the size if this contains an applet
168 if (m_appletInterface) {
169 KConfigGroup config = m_appletInterface->applet()->config();
170 // save size without margins, so we're robust against theme changes
171 const QSize popupSize = size().shrunkBy(padding());
172 config.writeEntry("popupWidth", popupSize.width());
173 config.writeEntry("popupHeight", popupSize.height());
174 config.sync();
175 }
176
178}
179
180void AppletPopup::focusOutEvent(QFocusEvent *ev)
181{
182 if (m_hideOnWindowDeactivate) {
183 bool parentHasFocus = false;
184
185 QWindow *parentWindow = transientParent();
186
187 while (parentWindow) {
188 if (parentWindow->isActive() && !(parentWindow->flags() & Qt::WindowDoesNotAcceptFocus)) {
189 parentHasFocus = true;
190 break;
191 }
192
193 parentWindow = parentWindow->transientParent();
194 }
195
196 const QWindow *focusWindow = QGuiApplication::focusWindow();
197 bool childHasFocus = focusWindow && ((focusWindow->isActive() && isAncestorOf(focusWindow)) || (focusWindow->type() & Qt::Popup) == Qt::Popup);
198
199 const bool viewClicked = qobject_cast<const PlasmaQuick::SharedQmlEngine *>(focusWindow) || qobject_cast<const ConfigView *>(focusWindow);
200
201 if (viewClicked || (!parentHasFocus && !childHasFocus)) {
202 setVisible(false);
203 }
204 }
205
207}
208
209void AppletPopup::onMainItemChanged()
210{
211 QQuickItem *mainItem = PlasmaWindow::mainItem();
212 if (!mainItem) {
213 m_layoutChangedProxy.reset();
214 return;
215 }
216
217 // update window to mainItem size hints
218 m_layoutChangedProxy.reset(new LayoutChangedProxy(mainItem));
219 connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::maximumSizeChanged, this, &AppletPopup::updateMaxSize);
220 connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::minimumSizeChanged, this, &AppletPopup::updateMinSize);
221 connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::implicitSizeChanged, this, &AppletPopup::updateSize);
222
223 updateMinSize();
224 updateMaxSize();
225 updateSize();
226}
227
228void AppletPopup::updateMinSize()
229{
230 if (!m_layoutChangedProxy) {
231 return;
232 }
233 setMinimumSize(m_layoutChangedProxy->minimumSize().grownBy(padding()));
234 // SetMinimumsize doesn't work since
235 // https://codereview.qt-project.org/c/qt/qtwayland/+/527831
236 // which fixes and conforms to the wayland protocol specification.
237 // This workaround is needed as the bug is in the protocol itself
238 resize(std::max(size().width(), minimumSize().width()), std::max(size().height(), minimumSize().height()));
239}
240
241void AppletPopup::updateMaxSize()
242{
243 if (!m_layoutChangedProxy) {
244 return;
245 }
246 QSize maxSize = m_layoutChangedProxy->maximumSize().grownBy(padding());
247 if (screen()) {
248 maxSize.setWidth(std::min(maxSize.width(), int(std::round(screen()->geometry().width() * 0.95))));
249 maxSize.setHeight(std::min(maxSize.height(), int(std::round(screen()->geometry().height() * 0.95))));
250 }
251 setMaximumSize(maxSize);
252 resize(std::min(size().width(), maxSize.width()), std::min(size().height(), maxSize.height()));
253}
254
255void AppletPopup::updateSize()
256{
257 if (m_sizeExplicitlySetFromConfig) {
258 return;
259 }
260 if (!m_layoutChangedProxy) {
261 return;
262 }
263 const QSize wantedSize = m_layoutChangedProxy->implicitSize().grownBy(padding());
264 QSize size = {std::clamp(wantedSize.width(), minimumSize().width(), maximumSize().width()),
265 std::clamp(wantedSize.height(), minimumSize().height(), maximumSize().height())};
266 resize(size);
267}
268
269LayoutChangedProxy::LayoutChangedProxy(QQuickItem *item)
270 : m_item(item)
271{
272 m_minimumWidth = QQmlProperty(item, QStringLiteral("Layout.minimumWidth"), qmlContext(item));
273 m_minimumHeight = QQmlProperty(item, QStringLiteral("Layout.minimumHeight"), qmlContext(item));
274 m_maximumWidth = QQmlProperty(item, QStringLiteral("Layout.maximumWidth"), qmlContext(item));
275 m_maximumHeight = QQmlProperty(item, QStringLiteral("Layout.maximumHeight"), qmlContext(item));
276 m_preferredWidth = QQmlProperty(item, QStringLiteral("Layout.preferredWidth"), qmlContext(item));
277 m_preferredHeight = QQmlProperty(item, QStringLiteral("Layout.preferredHeight"), qmlContext(item));
278
279 m_minimumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
280 m_minimumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
281 m_maximumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
282 m_maximumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
283 m_preferredWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
284 m_preferredHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
285 connect(item, &QQuickItem::implicitWidthChanged, this, &LayoutChangedProxy::implicitSizeChanged);
286 connect(item, &QQuickItem::implicitHeightChanged, this, &LayoutChangedProxy::implicitSizeChanged);
287}
288
289QSize LayoutChangedProxy::maximumSize() const
290{
291 QSize size(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX);
292 qreal width = m_maximumWidth.read().toReal();
293 if (qIsFinite(width) && int(width) > 0) {
294 size.setWidth(width);
295 }
296 qreal height = m_maximumHeight.read().toReal();
297 if (qIsFinite(height) && int(height) > 0) {
298 size.setHeight(height);
299 }
300
301 return size;
302}
303
304QSize LayoutChangedProxy::implicitSize() const
305{
306 QSize size(200, 200);
307
308 // Layout.preferredSize has precedent over implicit in layouts
309 // so mimic that behaviour here
310 if (m_item) {
311 size = QSize(m_item->implicitWidth(), m_item->implicitHeight());
312 }
313 qreal width = m_preferredWidth.read().toReal();
314 if (qIsFinite(width) && int(width) > 0) {
315 size.setWidth(width);
316 }
317 qreal height = m_preferredHeight.read().toReal();
318 if (qIsFinite(height) && int(height) > 0) {
319 size.setHeight(height);
320 }
321 return size;
322}
323
324QSize LayoutChangedProxy::minimumSize() const
325{
326 QSize size(0, 0);
327 qreal width = m_minimumWidth.read().toReal();
328 if (qIsFinite(width) && int(width) > 0) {
329 size.setWidth(width);
330 }
331 qreal height = m_minimumHeight.read().toReal();
332 if (qIsFinite(height) && int(height) > 0) {
333 size.setHeight(height);
334 }
335
336 return size;
337}
338
339#include "appletpopup.moc"
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
QString readEntry(const char *key, const char *aDefault=nullptr) const
bool sync() override
static bool isPlatformX11()
static void setType(WId win, NET::WindowType windowType)
AppletPopup
bool hideOnWindowDeactivate
Whether the dialog should be hidden when the dialog loses focus.
Definition appletpopup.h:42
QQuickItem * appletInterface
This property holds a pointer to the AppletInterface used by.
Definition appletpopup.h:35
The EdgeEventForwarder class This class forwards edge events to be replayed within the given margin T...
The PopupPlasmaWindow class is a styled Plasma window that can be positioned relative to an existing ...
static PlasmaShellWaylandIntegration * get(QWindow *window)
Returns the relevant PlasmaWaylandShellIntegration instance for this window creating one if needed.
KCRASH_EXPORT void setFlags(KCrash::CrashFlags flags)
The EdgeEventForwarder class This class forwards edge events to be replayed within the given margin T...
Definition action.h:20
QWindow * focusWindow()
QMetaMethod fromSignal(PointerToMemberFunction signal)
Q_EMITQ_EMIT
Q_OBJECTQ_OBJECT
Q_SIGNALSQ_SIGNALS
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T * data() const const
QVariant read(const QObject *object, const QString &name)
void implicitHeightChanged()
void implicitWidthChanged()
T * data() const const
void reset(T *other)
void geometryChanged(const QRect &geometry)
QSize grownBy(QMargins margins) const const
int height() const const
bool isEmpty() const const
int & rheight()
int & rwidth()
void setHeight(int height)
void setWidth(int width)
QSize shrunkBy(QMargins margins) const const
int width() const const
typedef Edges
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
qreal toReal(bool *ok) const const
virtual void focusOutEvent(QFocusEvent *ev)
QRect geometry() const const
virtual void hideEvent(QHideEvent *ev)
bool isActive() const const
bool isAncestorOf(const QWindow *child, AncestorMode mode) const const
QSize maximumSize() const const
QSize minimumSize() const const
void resize(const QSize &newSize)
QScreen * screen() const const
void setMaximumSize(const QSize &size)
void setMinimumSize(const QSize &size)
virtual QSize size() const const override
Qt::WindowType type() const const
void setVisible(bool visible)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:54:11 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.