Libplasma

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 "sharedqmlengine.h"
27#include "utils.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
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 if (!size().isEmpty()) {
239 resize(std::max(size().width(), minimumSize().width()), std::max(size().height(), minimumSize().height()));
240 }
241}
242
243void AppletPopup::updateMaxSize()
244{
245 if (!m_layoutChangedProxy) {
246 return;
247 }
248 QSize maxSize = m_layoutChangedProxy->maximumSize().grownBy(padding());
249 if (screen()) {
250 maxSize.setWidth(std::min(maxSize.width(), int(std::round(screen()->geometry().width() * 0.95))));
251 maxSize.setHeight(std::min(maxSize.height(), int(std::round(screen()->geometry().height() * 0.95))));
252 }
253 setMaximumSize(maxSize);
254 if (!size().isEmpty() && !maxSize.isEmpty()) {
255 resize(std::min(size().width(), maxSize.width()), std::min(size().height(), maxSize.height()));
256 }
257}
258
259void AppletPopup::updateSize()
260{
261 if (m_sizeExplicitlySetFromConfig) {
262 return;
263 }
264 if (!m_layoutChangedProxy) {
265 return;
266 }
267 const QSize wantedSize = m_layoutChangedProxy->implicitSize().grownBy(padding());
268
269 // NOTE: not using std::clamp as it might assert due to (possible) malformed values, sich as min > max
270 QSize size = {
271 std::min(std::max(minimumSize().width(), wantedSize.width()), maximumSize().width()),
272 std::min(std::max(minimumSize().height(), wantedSize.height()), maximumSize().height())
273 };
274
275 resize(size);
276}
277
278LayoutChangedProxy::LayoutChangedProxy(QQuickItem *item)
279 : m_item(item)
280{
281 m_minimumWidth = QQmlProperty(item, QStringLiteral("Layout.minimumWidth"), qmlContext(item));
282 m_minimumHeight = QQmlProperty(item, QStringLiteral("Layout.minimumHeight"), qmlContext(item));
283 m_maximumWidth = QQmlProperty(item, QStringLiteral("Layout.maximumWidth"), qmlContext(item));
284 m_maximumHeight = QQmlProperty(item, QStringLiteral("Layout.maximumHeight"), qmlContext(item));
285 m_preferredWidth = QQmlProperty(item, QStringLiteral("Layout.preferredWidth"), qmlContext(item));
286 m_preferredHeight = QQmlProperty(item, QStringLiteral("Layout.preferredHeight"), qmlContext(item));
287
288 m_minimumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
289 m_minimumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
290 m_maximumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
291 m_maximumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
292 m_preferredWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
293 m_preferredHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
294 connect(item, &QQuickItem::implicitWidthChanged, this, &LayoutChangedProxy::implicitSizeChanged);
295 connect(item, &QQuickItem::implicitHeightChanged, this, &LayoutChangedProxy::implicitSizeChanged);
296}
297
298QSize LayoutChangedProxy::maximumSize() const
299{
300 QSize size(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX);
301 qreal width = m_maximumWidth.read().toReal();
302 if (qIsFinite(width) && int(width) > 0) {
303 size.setWidth(width);
304 }
305 qreal height = m_maximumHeight.read().toReal();
306 if (qIsFinite(height) && int(height) > 0) {
307 size.setHeight(height);
308 }
309
310 return size;
311}
312
313QSize LayoutChangedProxy::implicitSize() const
314{
315 QSize size(200, 200);
316
317 // Layout.preferredSize has precedent over implicit in layouts
318 // so mimic that behaviour here
319 if (m_item) {
320 size = QSize(m_item->implicitWidth(), m_item->implicitHeight());
321 }
322 qreal width = m_preferredWidth.read().toReal();
323 if (qIsFinite(width) && int(width) > 0) {
324 size.setWidth(width);
325 }
326 qreal height = m_preferredHeight.read().toReal();
327 if (qIsFinite(height) && int(height) > 0) {
328 size.setHeight(height);
329 }
330 return size;
331}
332
333QSize LayoutChangedProxy::minimumSize() const
334{
335 QSize size(0, 0);
336 qreal width = m_minimumWidth.read().toReal();
337 if (qIsFinite(width) && int(width) > 0) {
338 size.setWidth(width);
339 }
340 qreal height = m_minimumHeight.read().toReal();
341 if (qIsFinite(height) && int(height) > 0) {
342 size.setHeight(height);
343 }
344
345 return size;
346}
347
348#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 qobject_cast(QObject *object)
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 Oct 11 2024 12:09:37 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.