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 windowResizer->setActiveEdges(~nearbyBorders());
94 };
95 updateWindowResizerEdges();
96 connect(this, &PopupPlasmaWindow::nearbyBordersChanged, this, updateWindowResizerEdges);
97 connect(this, &PopupPlasmaWindow::effectivePopupDirectionChanged, this, updateWindowResizerEdges);
98
99 connect(this, &PlasmaWindow::mainItemChanged, this, &AppletPopup::onMainItemChanged);
100 connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateMaxSize);
101 connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateSize);
102 connect(this, &PlasmaWindow::paddingChanged, this, &AppletPopup::updateMinSize);
103
104 connect(this, &PlasmaWindow::screenChanged, this, [this](QScreen *screen) {
105 if (m_oldScreen) {
106 disconnect(m_oldScreen, &QScreen::geometryChanged, this, &AppletPopup::updateMaxSize);
107 }
108 if (screen) {
109 connect(screen, &QScreen::geometryChanged, this, &AppletPopup::updateMaxSize);
110 }
111 m_oldScreen = screen;
112 updateMaxSize();
113 });
114}
115
116AppletPopup::~AppletPopup()
117{
118}
119
121{
122 return m_appletInterface.data();
123}
124
125void AppletPopup::setAppletInterface(QQuickItem *appletInterface)
126{
127 if (appletInterface == m_appletInterface) {
128 return;
129 }
130
132 m_sizeExplicitlySetFromConfig = false;
133
134 if (m_appletInterface) {
135 KConfigGroup config = m_appletInterface->applet()->config();
136 QSize size;
137 size.rwidth() = config.readEntry("popupWidth", 0);
138 size.rheight() = config.readEntry("popupHeight", 0);
139 if (!size.isEmpty()) {
140 m_sizeExplicitlySetFromConfig = true;
141 resize(size.grownBy(padding()));
142 return;
143 }
144 }
145
146 Q_EMIT appletInterfaceChanged();
147}
148
150{
151 return m_hideOnWindowDeactivate;
152}
153
154void AppletPopup::setHideOnWindowDeactivate(bool hideOnWindowDeactivate)
155{
156 if (hideOnWindowDeactivate == m_hideOnWindowDeactivate) {
157 return;
158 }
159 m_hideOnWindowDeactivate = hideOnWindowDeactivate;
160 Q_EMIT hideOnWindowDeactivateChanged();
161}
162
163void AppletPopup::hideEvent(QHideEvent *event)
164{
165 // Persist the size if this contains an applet
166 if (m_appletInterface) {
167 KConfigGroup config = m_appletInterface->applet()->config();
168 // save size without margins, so we're robust against theme changes
169 const QSize popupSize = size().shrunkBy(padding());
170 config.writeEntry("popupWidth", popupSize.width());
171 config.writeEntry("popupHeight", popupSize.height());
172 config.sync();
173 }
174
176}
177
178void AppletPopup::focusOutEvent(QFocusEvent *ev)
179{
180 if (m_hideOnWindowDeactivate) {
181 bool parentHasFocus = false;
182
183 QWindow *parentWindow = transientParent();
184
185 while (parentWindow) {
186 if (parentWindow->isActive() && !(parentWindow->flags() & Qt::WindowDoesNotAcceptFocus)) {
187 parentHasFocus = true;
188 break;
189 }
190
191 parentWindow = parentWindow->transientParent();
192 }
193
194 const QWindow *focusWindow = QGuiApplication::focusWindow();
195 bool childHasFocus = focusWindow && ((focusWindow->isActive() && isAncestorOf(focusWindow)) || (focusWindow->type() & Qt::Popup) == Qt::Popup);
196
197 const bool viewClicked = qobject_cast<const PlasmaQuick::SharedQmlEngine *>(focusWindow) || qobject_cast<const ConfigView *>(focusWindow);
198
199 if (viewClicked || (!parentHasFocus && !childHasFocus)) {
200 setVisible(false);
201 }
202 }
203
205}
206
207void AppletPopup::onMainItemChanged()
208{
209 QQuickItem *mainItem = PlasmaWindow::mainItem();
210 if (!mainItem) {
211 m_layoutChangedProxy.reset();
212 return;
213 }
214
215 // update window to mainItem size hints
216 m_layoutChangedProxy.reset(new LayoutChangedProxy(mainItem));
217 connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::maximumSizeChanged, this, &AppletPopup::updateMaxSize);
218 connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::minimumSizeChanged, this, &AppletPopup::updateMinSize);
219 connect(m_layoutChangedProxy.data(), &LayoutChangedProxy::implicitSizeChanged, this, &AppletPopup::updateSize);
220
221 updateMinSize();
222 updateMaxSize();
223 updateSize();
224}
225
226void AppletPopup::updateMinSize()
227{
228 if (!m_layoutChangedProxy) {
229 return;
230 }
231 setMinimumSize(m_layoutChangedProxy->minimumSize().grownBy(padding()));
232 // SetMinimumsize doesn't work since
233 // https://codereview.qt-project.org/c/qt/qtwayland/+/527831
234 // which fixes and conforms to the wayland protocol specification.
235 // This workaround is needed as the bug is in the protocol itself
236 if (!size().isEmpty()) {
237 resize(std::max(size().width(), minimumSize().width()), std::max(size().height(), minimumSize().height()));
238 }
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 if (!size().isEmpty() && !maxSize.isEmpty()) {
253 resize(std::min(size().width(), maxSize.width()), std::min(size().height(), maxSize.height()));
254 }
255}
256
257void AppletPopup::updateSize()
258{
259 if (m_sizeExplicitlySetFromConfig) {
260 return;
261 }
262 if (!m_layoutChangedProxy) {
263 return;
264 }
265 const QSize wantedSize = m_layoutChangedProxy->implicitSize().grownBy(padding());
266
267 // NOTE: not using std::clamp as it might assert due to (possible) malformed values, sich as min > max
268 QSize size = {
269 std::min(std::max(minimumSize().width(), wantedSize.width()), maximumSize().width()),
270 std::min(std::max(minimumSize().height(), wantedSize.height()), maximumSize().height())
271 };
272
273 Q_ASSERT(!size.isEmpty());
274 if (!size.isEmpty()) {
275 resize(size);
276 }
277}
278
279LayoutChangedProxy::LayoutChangedProxy(QQuickItem *item)
280 : m_item(item)
281{
282 m_minimumWidth = QQmlProperty(item, QStringLiteral("Layout.minimumWidth"), qmlContext(item));
283 m_minimumHeight = QQmlProperty(item, QStringLiteral("Layout.minimumHeight"), qmlContext(item));
284 m_maximumWidth = QQmlProperty(item, QStringLiteral("Layout.maximumWidth"), qmlContext(item));
285 m_maximumHeight = QQmlProperty(item, QStringLiteral("Layout.maximumHeight"), qmlContext(item));
286 m_preferredWidth = QQmlProperty(item, QStringLiteral("Layout.preferredWidth"), qmlContext(item));
287 m_preferredHeight = QQmlProperty(item, QStringLiteral("Layout.preferredHeight"), qmlContext(item));
288
289 m_minimumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
290 m_minimumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::minimumSizeChanged).methodIndex());
291 m_maximumWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
292 m_maximumHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::maximumSizeChanged).methodIndex());
293 m_preferredWidth.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
294 m_preferredHeight.connectNotifySignal(this, QMetaMethod::fromSignal(&LayoutChangedProxy::implicitSizeChanged).methodIndex());
295 connect(item, &QQuickItem::implicitWidthChanged, this, &LayoutChangedProxy::implicitSizeChanged);
296 connect(item, &QQuickItem::implicitHeightChanged, this, &LayoutChangedProxy::implicitSizeChanged);
297}
298
299QSize LayoutChangedProxy::maximumSize() const
300{
301 QSize size(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX);
302 qreal width = m_maximumWidth.read().toReal();
303 if (qIsFinite(width) && int(width) > 0) {
304 size.setWidth(width);
305 }
306 qreal height = m_maximumHeight.read().toReal();
307 if (qIsFinite(height) && int(height) > 0) {
308 size.setHeight(height);
309 }
310
311 return size;
312}
313
314QSize LayoutChangedProxy::implicitSize() const
315{
316 QSize size(200, 200);
317
318 // Layout.preferredSize has precedent over implicit in layouts
319 // so mimic that behaviour here
320 if (m_item) {
321 size = QSize(m_item->implicitWidth(), m_item->implicitHeight());
322 }
323 qreal width = m_preferredWidth.read().toReal();
324 if (qIsFinite(width) && int(width) > 0) {
325 size.setWidth(width);
326 }
327 qreal height = m_preferredHeight.read().toReal();
328 if (qIsFinite(height) && int(height) > 0) {
329 size.setHeight(height);
330 }
331 return size;
332}
333
334QSize LayoutChangedProxy::minimumSize() const
335{
336 QSize size(0, 0);
337 qreal width = m_minimumWidth.read().toReal();
338 if (qIsFinite(width) && int(width) > 0) {
339 size.setWidth(width);
340 }
341 qreal height = m_minimumHeight.read().toReal();
342 if (qIsFinite(height) && int(height) > 0) {
343 size.setHeight(height);
344 }
345
346 return size;
347}
348
349#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
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 Dec 6 2024 12:04:40 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.