plasma-framework/src/plasmaquick/dialog.cpp
2021-07-26 12:25:03 +02:00

1441 lines
47 KiB
C++

/*
SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
SPDX-FileCopyrightText: 2014 Vishesh Handa <vhanda@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "dialog.h"
#include "../declarativeimports/core/framesvgitem.h"
#include "config-plasma.h"
#include "configview.h"
#include "dialogshadows_p.h"
#include "view.h"
#include <QLayout>
#include <QMenu>
#include <QPointer>
#include <QQuickItem>
#include <QScreen>
#include <QTimer>
#include <QPlatformSurfaceEvent>
#include <KWindowSystem/KWindowInfo>
#include <KWindowSystem>
#include <kquickaddons/quickviewsharedengine.h>
#include <KWindowEffects>
#include <Plasma/Corona>
#include <QDebug>
#if HAVE_KWAYLAND
#include "waylandintegration_p.h"
#include <KWayland/Client/plasmashell.h>
#include <KWayland/Client/surface.h>
#endif
#if HAVE_XCB_SHAPE
#include <QX11Info>
#include <xcb/shape.h>
#endif
#if HAVE_X11
#include <QtPlatformHeaders/QXcbWindowFunctions>
#endif
// Unfortunately QWINDOWSIZE_MAX is not exported
#define DIALOGSIZE_MAX ((1 << 24) - 1)
namespace PlasmaQuick
{
class DialogPrivate
{
public:
DialogPrivate(Dialog *dialog)
: q(dialog)
, location(Plasma::Types::BottomEdge)
, frameSvgItem(nullptr)
, hasMask(false)
, type(Dialog::Normal)
, hideOnWindowDeactivate(false)
, outputOnly(false)
, visible(false)
, componentComplete(dialog->parent() == nullptr)
, backgroundHints(Dialog::StandardBackground)
{
hintsCommitTimer.setSingleShot(true);
hintsCommitTimer.setInterval(0);
QObject::connect(&hintsCommitTimer, SIGNAL(timeout()), q, SLOT(updateLayoutParameters()));
}
void updateInputShape();
// SLOTS
/**
* Sync Borders updates the enabled borders of the frameSvgItem depending
* on the geometry of the window.
*
* \param windowGeometry The window geometry which should be taken into
* consideration when activating/deactivating certain borders
*/
void syncBorders(const QRect &windowGeometry);
/**
* This function sets the blurBehind, background contrast and shadows. It
* does so wrt the frameSvgItem. So make sure the frameSvgItem is the
* correct size before calling this function.
*/
void updateTheme();
void updateVisibility(bool visible);
void updateMinimumWidth();
void updateMinimumHeight();
void updateMaximumWidth();
void updateMaximumHeight();
/**
* Gets the maximum and minimum size hints for the window based on the contents. it doesn't actually resize anything
*/
void getSizeHints(QSize &min, QSize &max) const;
/**
* This function is an optimized version of updateMaximumHeight,
* updateMaximumWidth,updateMinimumWidth and updateMinimumHeight.
* It should be called when you need to call all 4 of these functions
* AND you have called syncToMainItemSize before.
*/
void updateLayoutParameters();
QRect availableScreenGeometryForPosition(const QPoint &pos) const;
/**
* This function checks the current position of the dialog and repositions
* it so that no part of it is not on the screen
*/
void repositionIfOffScreen();
void slotMainItemSizeChanged();
void slotWindowPositionChanged();
void syncToMainItemSize();
bool mainItemContainsPosition(const QPointF &point) const;
QPointF positionAdjustedForMainItem(const QPointF &point) const;
void setupWaylandIntegration();
void applyType();
Dialog *q;
Plasma::Types::Location location;
Plasma::FrameSvgItem *frameSvgItem;
QPointer<QQuickItem> mainItem;
QPointer<QQuickItem> visualParent;
QTimer hintsCommitTimer;
#if HAVE_KWAYLAND
QPointer<KWayland::Client::PlasmaShellSurface> shellSurface;
#endif
QRect cachedGeometry;
bool hasMask;
Dialog::WindowType type;
bool hideOnWindowDeactivate;
bool outputOnly;
bool visible;
Plasma::Theme theme;
bool componentComplete;
Dialog::BackgroundHints backgroundHints;
// Attached Layout property of mainItem, if any
QPointer<QObject> mainItemLayout;
};
QRect DialogPrivate::availableScreenGeometryForPosition(const QPoint &pos) const
{
// FIXME: QWindow::screen() never ever changes if the window is moved across
// virtual screens (normal two screens with X), this seems to be intentional
// as it's explicitly mentioned in the docs. Until that's changed or some
// more proper way of howto get the current QScreen for given QWindow is found,
// we simply iterate over the virtual screens and pick the one our QWindow
// says it's at.
QRect avail;
const auto screens = QGuiApplication::screens();
for (QScreen *screen : screens) {
// we check geometry() but then take availableGeometry()
// to reliably check in which screen a position is, we need the full
// geometry, including areas for panels
if (screen->geometry().contains(pos)) {
avail = screen->availableGeometry();
break;
}
}
/*
* if the heuristic fails (because the topleft of the dialog is offscreen)
* use at least our screen()
* the screen should be correctly updated now on Qt 5.3+ so should be
* more reliable anyways (could be tried to remove the whole Q_FOREACH
* at this point)
*
* important: screen can be a nullptr... see bug 345173
*/
if (avail.isEmpty() && q->screen()) {
avail = q->screen()->availableGeometry();
}
return avail;
}
void DialogPrivate::syncBorders(const QRect &geom)
{
QRect avail = availableScreenGeometryForPosition(geom.topLeft());
int borders = Plasma::FrameSvg::AllBorders;
// Tooltips always have all the borders
// floating windows have all borders
if (!q->flags().testFlag(Qt::ToolTip) && location != Plasma::Types::Floating) {
if (geom.x() <= avail.x() || location == Plasma::Types::LeftEdge) {
borders = borders & ~Plasma::FrameSvg::LeftBorder;
}
if (geom.y() <= avail.y() || location == Plasma::Types::TopEdge) {
borders = borders & ~Plasma::FrameSvg::TopBorder;
}
if (avail.right() <= geom.x() + geom.width() || location == Plasma::Types::RightEdge) {
borders = borders & ~Plasma::FrameSvg::RightBorder;
}
if (avail.bottom() <= geom.y() + geom.height() || location == Plasma::Types::BottomEdge) {
borders = borders & ~Plasma::FrameSvg::BottomBorder;
}
}
if (frameSvgItem->enabledBorders() != (Plasma::FrameSvg::EnabledBorder)borders) {
frameSvgItem->setEnabledBorders((Plasma::FrameSvg::EnabledBorder)borders);
}
}
void DialogPrivate::updateTheme()
{
if (backgroundHints == Dialog::NoBackground) {
frameSvgItem->setImagePath(QString());
KWindowEffects::enableBlurBehind(q, false);
KWindowEffects::enableBackgroundContrast(q, false);
q->setMask(QRegion());
DialogShadows::self()->removeWindow(q);
} else {
auto prefix = QStringLiteral("");
if ((backgroundHints & Dialog::SolidBackground) == Dialog::SolidBackground) {
prefix = QStringLiteral("solid/");
}
if (type == Dialog::Tooltip) {
frameSvgItem->setImagePath(prefix + QStringLiteral("widgets/tooltip"));
} else {
frameSvgItem->setImagePath(prefix + QStringLiteral("dialogs/background"));
}
KWindowEffects::enableBlurBehind(q, theme.blurBehindEnabled(), frameSvgItem->mask());
KWindowEffects::enableBackgroundContrast(q,
theme.backgroundContrastEnabled(),
theme.backgroundContrast(),
theme.backgroundIntensity(),
theme.backgroundSaturation(),
frameSvgItem->mask());
if (KWindowSystem::compositingActive()) {
if (hasMask) {
hasMask = false;
q->setMask(QRegion());
}
} else {
hasMask = true;
q->setMask(frameSvgItem->mask());
}
if (q->isVisible()) {
DialogShadows::self()->addWindow(q, frameSvgItem->enabledBorders());
}
}
updateInputShape();
}
void DialogPrivate::updateVisibility(bool visible)
{
if (visible) {
if (visualParent && visualParent->window()) {
q->setTransientParent(visualParent->window());
}
if (q->location() == Plasma::Types::FullScreen) {
frameSvgItem->setEnabledBorders(Plasma::FrameSvg::NoBorder);
// We cache the original size of the item, to retrieve it
// when the dialog is switched back from fullscreen.
if (q->geometry() != q->screen()->availableGeometry()) {
cachedGeometry = q->geometry();
}
q->setGeometry(q->screen()->availableGeometry());
} else {
if (!cachedGeometry.isNull()) {
q->resize(cachedGeometry.size());
slotWindowPositionChanged();
if (visualParent) {
q->setPosition(q->popupPosition(visualParent, q->size()));
}
cachedGeometry = QRect();
}
if (mainItem) {
syncToMainItemSize();
}
if (mainItemLayout) {
updateLayoutParameters();
}
#if HAVE_KWAYLAND
// if is a wayland window that was hidden, we need
// to set its position again as there won't be any move event to sync QWindow::position and shellsurface::position
if (shellSurface && type != Dialog::OnScreenDisplay) {
shellSurface->setPosition(q->position());
}
#endif
}
}
if (!q->flags().testFlag(Qt::ToolTip) && type != Dialog::Notification && type != Dialog::CriticalNotification) {
KWindowEffects::SlideFromLocation slideLocation = KWindowEffects::NoEdge;
switch (location) {
case Plasma::Types::TopEdge:
slideLocation = KWindowEffects::TopEdge;
break;
case Plasma::Types::LeftEdge:
slideLocation = KWindowEffects::LeftEdge;
break;
case Plasma::Types::RightEdge:
slideLocation = KWindowEffects::RightEdge;
break;
case Plasma::Types::BottomEdge:
slideLocation = KWindowEffects::BottomEdge;
break;
// no edge, no slide
default:
break;
}
KWindowEffects::slideWindow(q, slideLocation, -1);
}
if (visible) {
q->raise();
applyType();
}
}
void DialogPrivate::updateMinimumWidth()
{
Q_ASSERT(mainItem);
Q_ASSERT(mainItemLayout);
if (!componentComplete) {
return;
}
q->setMinimumWidth(0);
// this is to try to get the internal item resized a tad before, but
// the flicker almost always happen anyways, so is *probably* useless
// this other kind of flicker is the view not being always focused exactly
// on the scene
auto margin = frameSvgItem->fixedMargins();
int minimumWidth = mainItemLayout->property("minimumWidth").toInt() + margin->left() + margin->right();
if (q->screen()) {
minimumWidth = qMin(q->screen()->availableGeometry().width(), minimumWidth);
}
q->contentItem()->setWidth(qMax(q->width(), minimumWidth));
q->setWidth(qMax(q->width(), minimumWidth));
hintsCommitTimer.start();
}
void DialogPrivate::updateMinimumHeight()
{
Q_ASSERT(mainItem);
Q_ASSERT(mainItemLayout);
if (!componentComplete) {
return;
}
q->setMinimumHeight(0);
// this is to try to get the internal item resized a tad before, but
// the flicker almost always happen anyways, so is *probably* useless
// this other kind of flicker is the view not being always focused exactly
// on the scene
auto margin = frameSvgItem->fixedMargins();
int minimumHeight = mainItemLayout->property("minimumHeight").toInt() + margin->top() + margin->bottom();
if (q->screen()) {
minimumHeight = qMin(q->screen()->availableGeometry().height(), minimumHeight);
}
q->contentItem()->setHeight(qMax(q->height(), minimumHeight));
q->setHeight(qMax(q->height(), minimumHeight));
hintsCommitTimer.start();
}
void DialogPrivate::updateMaximumWidth()
{
Q_ASSERT(mainItem);
Q_ASSERT(mainItemLayout);
if (!componentComplete) {
return;
}
q->setMaximumWidth(DIALOGSIZE_MAX);
auto margin = frameSvgItem->fixedMargins();
int maximumWidth = mainItemLayout->property("maximumWidth").toInt() + margin->left() + margin->right();
if (q->screen()) {
maximumWidth = qMin(q->screen()->availableGeometry().width(), maximumWidth);
}
q->contentItem()->setWidth(qMax(q->width(), maximumWidth));
q->setWidth(qMax(q->width(), maximumWidth));
hintsCommitTimer.start();
}
void DialogPrivate::updateMaximumHeight()
{
Q_ASSERT(mainItem);
Q_ASSERT(mainItemLayout);
if (!componentComplete) {
return;
}
q->setMaximumHeight(DIALOGSIZE_MAX);
auto margin = frameSvgItem->fixedMargins();
int maximumHeight = mainItemLayout->property("maximumHeight").toInt() + margin->top() + margin->bottom();
if (q->screen()) {
maximumHeight = qMin(q->screen()->availableGeometry().height(), maximumHeight);
}
q->contentItem()->setHeight(qMax(q->height(), maximumHeight));
q->setHeight(qMin(q->height(), maximumHeight));
hintsCommitTimer.start();
}
void DialogPrivate::getSizeHints(QSize &min, QSize &max) const
{
if (!componentComplete || !mainItem || !mainItemLayout) {
return;
}
Q_ASSERT(mainItem);
Q_ASSERT(mainItemLayout);
auto margin = frameSvgItem->fixedMargins();
int minimumHeight = mainItemLayout->property("minimumHeight").toInt();
int maximumHeight = mainItemLayout->property("maximumHeight").toInt();
maximumHeight = maximumHeight ? maximumHeight : DIALOGSIZE_MAX;
int minimumWidth = mainItemLayout->property("minimumWidth").toInt();
int maximumWidth = mainItemLayout->property("maximumWidth").toInt();
maximumWidth = maximumWidth ? maximumWidth : DIALOGSIZE_MAX;
minimumHeight += margin->top() + margin->bottom();
maximumHeight += margin->top() + margin->bottom();
minimumWidth += margin->left() + margin->right();
maximumWidth += margin->left() + margin->right();
if (q->screen()) {
minimumWidth = qMin(q->screen()->availableGeometry().width(), minimumWidth);
minimumHeight = qMin(q->screen()->availableGeometry().height(), minimumHeight);
maximumWidth = qMin(q->screen()->availableGeometry().width(), maximumWidth);
maximumHeight = qMin(q->screen()->availableGeometry().height(), maximumHeight);
}
min = QSize(minimumWidth, minimumHeight);
max = QSize(maximumWidth, maximumHeight);
}
void DialogPrivate::updateLayoutParameters()
{
if (!componentComplete || !mainItem || !mainItemLayout) {
return;
}
mainItem->disconnect(q);
auto margin = frameSvgItem->fixedMargins();
QSize min;
QSize max(DIALOGSIZE_MAX, DIALOGSIZE_MAX);
getSizeHints(min, max);
const QSize finalSize(qBound(min.width(), q->width(), max.width()), qBound(min.height(), q->height(), max.height()));
if (visualParent) {
// it's important here that we're using re->size() as size, we don't want to do recursive resizeEvents
const QRect geom(q->popupPosition(visualParent, finalSize), finalSize);
q->adjustGeometry(geom);
} else {
q->resize(finalSize);
}
mainItem->setPosition(QPointF(margin->left(), margin->top()));
mainItem->setSize(QSizeF(q->width() - margin->left() - margin->right(), q->height() - margin->top() - margin->bottom()));
frameSvgItem->setSize(QSizeF(q->width(), q->height()));
repositionIfOffScreen();
updateTheme();
// FIXME: this seems to interfere with the geometry change that
// sometimes is still going on, causing flicker (this one is two repositions happening in quick succession).
// it may have to be delayed further
q->setMinimumSize(min);
q->setMaximumSize(max);
QObject::connect(mainItem, SIGNAL(widthChanged()), q, SLOT(slotMainItemSizeChanged()));
QObject::connect(mainItem, SIGNAL(heightChanged()), q, SLOT(slotMainItemSizeChanged()));
}
void DialogPrivate::repositionIfOffScreen()
{
if (!componentComplete) {
return;
}
const QRect avail = availableScreenGeometryForPosition(q->position());
int x = q->x();
int y = q->y();
if (x < avail.left()) {
x = avail.left();
} else if (x + q->width() > avail.right()) {
x = avail.right() - q->width() + 1;
}
if (y < avail.top()) {
y = avail.top();
} else if (y + q->height() > avail.bottom()) {
y = avail.bottom() - q->height() + 1;
}
q->setX(x);
q->setY(y);
}
void DialogPrivate::updateInputShape()
{
if (!q->isVisible()) {
return;
}
#if HAVE_XCB_SHAPE
if (KWindowSystem::isPlatformX11()) {
xcb_connection_t *c = QX11Info::connection();
static bool s_shapeExtensionChecked = false;
static bool s_shapeAvailable = false;
if (!s_shapeExtensionChecked) {
xcb_prefetch_extension_data(c, &xcb_shape_id);
const xcb_query_extension_reply_t *extension = xcb_get_extension_data(c, &xcb_shape_id);
if (extension->present) {
// query version
auto cookie = xcb_shape_query_version(c);
QScopedPointer<xcb_shape_query_version_reply_t, QScopedPointerPodDeleter> version(xcb_shape_query_version_reply(c, cookie, nullptr));
if (!version.isNull()) {
s_shapeAvailable = (version->major_version * 0x10 + version->minor_version) >= 0x11;
}
}
s_shapeExtensionChecked = true;
}
if (!s_shapeAvailable) {
return;
}
if (outputOnly) {
// set input shape, so that it doesn't accept any input events
xcb_shape_rectangles(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, q->winId(), 0, 0, 0, nullptr);
} else {
// delete the shape
xcb_shape_mask(c, XCB_SHAPE_SO_INTERSECT, XCB_SHAPE_SK_INPUT, q->winId(), 0, 0, XCB_PIXMAP_NONE);
}
}
#endif
}
void DialogPrivate::syncToMainItemSize()
{
Q_ASSERT(mainItem);
if (!componentComplete) {
return;
}
if (mainItem->width() <= 0 || mainItem->height() <= 0) {
qWarning() << "trying to show an empty dialog";
}
updateTheme();
if (visualParent) {
// fixedMargins will get all the borders, no matter if they are enabled
auto margins = frameSvgItem->fixedMargins();
const QSize fullSize = QSize(mainItem->width(), mainItem->height()) + QSize(margins->left() + margins->right(), margins->top() + margins->bottom());
// We get the popup position with the fullsize as we need the popup
// position in order to determine our actual size, as the position
// determines which borders will be shown.
const QRect geom(q->popupPosition(visualParent, fullSize), fullSize);
// We're then moving the window to where we think we would be with all
// the borders. This way when syncBorders is called, it has a geometry
// to work with.
syncBorders(geom);
} else {
syncBorders(q->geometry());
}
QSize s = QSize(mainItem->width(), mainItem->height())
+ QSize(frameSvgItem->fixedMargins()->left() + frameSvgItem->fixedMargins()->right(),
frameSvgItem->fixedMargins()->top() + frameSvgItem->fixedMargins()->bottom());
QSize min;
QSize max(DIALOGSIZE_MAX, DIALOGSIZE_MAX);
getSizeHints(min, max);
s = QSize(qBound(min.width(), s.width(), max.width()), qBound(min.height(), s.height(), max.height()));
q->contentItem()->setSize(s);
frameSvgItem->setSize(s);
if (visualParent) {
const QRect geom(q->popupPosition(visualParent, s), s);
if (geom == q->geometry()) {
return;
}
q->adjustGeometry(geom);
// The borders will instantly be updated but the geometry might take a
// while as sub-classes can reimplement adjustGeometry and animate it.
syncBorders(geom);
} else {
q->resize(s);
}
mainItem->setPosition(QPointF(frameSvgItem->fixedMargins()->left(), frameSvgItem->fixedMargins()->top()));
updateTheme();
}
void DialogPrivate::slotWindowPositionChanged()
{
// Tooltips always have all the borders
// floating windows have all borders
if (!q->isVisible() || q->flags().testFlag(Qt::ToolTip) || location == Plasma::Types::Floating) {
return;
}
syncBorders(q->geometry());
updateTheme();
if (mainItem) {
auto margin = frameSvgItem->fixedMargins();
mainItem->setPosition(QPoint(margin->left(), margin->top()));
mainItem->setSize(QSize(q->width() - margin->left() - margin->right(), q->height() - margin->top() - margin->bottom()));
}
}
bool DialogPrivate::mainItemContainsPosition(const QPointF &point) const
{
if (!mainItem) {
return false;
}
return QRectF(mainItem->mapToScene(QPoint(0, 0)), QSizeF(mainItem->width(), mainItem->height())).contains(point);
}
QPointF DialogPrivate::positionAdjustedForMainItem(const QPointF &point) const
{
if (!mainItem) {
return point;
}
QRectF itemRect(mainItem->mapToScene(QPoint(0, 0)), QSizeF(mainItem->width(), mainItem->height()));
return QPointF(qBound(itemRect.left(), point.x(), itemRect.right()), qBound(itemRect.top(), point.y(), itemRect.bottom()));
}
void DialogPrivate::setupWaylandIntegration()
{
#if HAVE_KWAYLAND
if (shellSurface) {
// already setup
return;
}
using namespace KWayland::Client;
PlasmaShell *interface = WaylandIntegration::self()->waylandPlasmaShell();
if (!interface) {
return;
}
Surface *s = Surface::fromWindow(q);
if (!s) {
return;
}
shellSurface = interface->createSurface(s, q);
#endif
}
void DialogPrivate::applyType()
{
if (type != Dialog::Normal) {
/*QXcbWindowFunctions::WmWindowType*/ int wmType = 0;
#if HAVE_X11
if (KWindowSystem::isPlatformX11()) {
switch (type) {
case Dialog::Normal:
Q_UNREACHABLE();
break;
case Dialog::Dock:
wmType = QXcbWindowFunctions::WmWindowType::Dock;
break;
case Dialog::DialogWindow:
wmType = QXcbWindowFunctions::WmWindowType::Dialog;
break;
case Dialog::PopupMenu:
wmType = QXcbWindowFunctions::WmWindowType::PopupMenu;
break;
case Dialog::Tooltip:
wmType = QXcbWindowFunctions::WmWindowType::Tooltip;
break;
case Dialog::Notification:
wmType = QXcbWindowFunctions::WmWindowType::Notification;
break;
case Dialog::OnScreenDisplay:
case Dialog::CriticalNotification:
// Not supported by Qt
break;
}
if (wmType) {
QXcbWindowFunctions::setWmWindowType(q, static_cast<QXcbWindowFunctions::WmWindowType>(wmType));
}
}
#endif
if (!wmType) {
KWindowSystem::setType(q->winId(), static_cast<NET::WindowType>(type));
}
} else {
q->setFlags(Qt::FramelessWindowHint | q->flags());
#if HAVE_KWAYLAND
// Only possible after setup
if (shellSurface) {
if (q->flags() & Qt::WindowStaysOnTopHint) {
shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel);
shellSurface->setPanelBehavior(KWayland::Client::PlasmaShellSurface::PanelBehavior::WindowsGoBelow);
} else {
shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal);
shellSurface->setPanelBehavior(KWayland::Client::PlasmaShellSurface::PanelBehavior::AlwaysVisible);
}
}
#endif
}
// an OSD can't be a Dialog, as qt xcb would attempt to set a transient parent for it
// see bug 370433
if (type == Dialog::OnScreenDisplay) {
q->setFlags((q->flags() & ~Qt::Dialog) | Qt::Window);
}
if (backgroundHints == Dialog::NoBackground) {
frameSvgItem->setImagePath(QString());
} else {
auto prefix = QStringLiteral("");
if ((backgroundHints & Dialog::SolidBackground) == Dialog::SolidBackground) {
prefix = QStringLiteral("solid/");
}
if (type == Dialog::Tooltip) {
frameSvgItem->setImagePath(prefix + QStringLiteral("widgets/tooltip"));
} else {
frameSvgItem->setImagePath(prefix + QStringLiteral("dialogs/background"));
}
}
if (type == Dialog::Dock || type == Dialog::Notification || type == Dialog::OnScreenDisplay || type == Dialog::CriticalNotification) {
KWindowSystem::setOnAllDesktops(q->winId(), true);
} else {
KWindowSystem::setOnAllDesktops(q->winId(), false);
}
#if HAVE_KWAYLAND
if (shellSurface) {
shellSurface->setPanelTakesFocus(!q->flags().testFlag(Qt::WindowDoesNotAcceptFocus));
}
#endif
}
Dialog::Dialog(QQuickItem *parent)
: QQuickWindow(parent ? parent->window() : nullptr)
, d(new DialogPrivate(this))
{
setColor(QColor(Qt::transparent));
setFlags(Qt::FramelessWindowHint | Qt::Dialog);
connect(this, &QWindow::xChanged, [=]() {
d->slotWindowPositionChanged();
});
connect(this, &QWindow::yChanged, [=]() {
d->slotWindowPositionChanged();
});
connect(this, &QWindow::visibleChanged, this, &Dialog::visibleChangedProxy);
connect(this, SIGNAL(visibleChanged(bool)), this, SLOT(updateInputShape()));
connect(this, SIGNAL(outputOnlyChanged()), this, SLOT(updateInputShape()));
// HACK: this property is invoked due to the initialization that gets done to contentItem() in the getter
property("data");
// Create the FrameSvg background.
d->frameSvgItem = new Plasma::FrameSvgItem(contentItem());
// This is needed as a transition thing for KWayland
setProperty("__plasma_frameSvg", QVariant::fromValue(d->frameSvgItem->frameSvg()));
connect(&d->theme, SIGNAL(themeChanged()), this, SLOT(updateTheme()));
}
Dialog::~Dialog()
{
if (!QCoreApplication::instance()->closingDown()) {
DialogShadows::self()->removeWindow(this);
}
// Prevent signals from super-class destructor invoking our now-destroyed slots
disconnect(this, nullptr, this, nullptr);
}
QQuickItem *Dialog::mainItem() const
{
return d->mainItem;
}
void Dialog::setMainItem(QQuickItem *mainItem)
{
if (d->mainItem != mainItem) {
d->hintsCommitTimer.stop();
if (d->mainItem) {
disconnect(d->mainItem, nullptr, this, nullptr);
d->mainItem->setParentItem(nullptr);
}
if (d->mainItemLayout) {
disconnect(d->mainItemLayout, nullptr, this, nullptr);
}
d->mainItem = mainItem;
if (mainItem) {
mainItem->setParentItem(contentItem());
connect(mainItem, SIGNAL(widthChanged()), this, SLOT(slotMainItemSizeChanged()));
connect(mainItem, SIGNAL(heightChanged()), this, SLOT(slotMainItemSizeChanged()));
d->slotMainItemSizeChanged();
// Extract the representation's Layout, if any
QObject *layout = nullptr;
setMinimumSize(QSize(0, 0));
setMaximumSize(QSize(DIALOGSIZE_MAX, DIALOGSIZE_MAX));
// Search a child that has the needed Layout properties
// HACK: here we are not type safe, but is the only way to access to a pointer of Layout
const auto lstChild = mainItem->children();
for (QObject *child : lstChild) {
// find for the needed property of Layout: minimum/maximum/preferred sizes and fillWidth/fillHeight
if (child->property("minimumWidth").isValid() && child->property("minimumHeight").isValid() && child->property("preferredWidth").isValid()
&& child->property("preferredHeight").isValid() && child->property("maximumWidth").isValid() && child->property("maximumHeight").isValid()
&& child->property("fillWidth").isValid() && child->property("fillHeight").isValid()) {
layout = child;
break;
}
}
d->mainItemLayout = layout;
if (layout) {
// Why queued connections?
// we need to be sure that the properties are
// already *all* updated when we call the management code
connect(layout, SIGNAL(minimumWidthChanged()), this, SLOT(updateMinimumWidth()));
connect(layout, SIGNAL(minimumHeightChanged()), this, SLOT(updateMinimumHeight()));
connect(layout, SIGNAL(maximumWidthChanged()), this, SLOT(updateMaximumWidth()));
connect(layout, SIGNAL(maximumHeightChanged()), this, SLOT(updateMaximumHeight()));
d->updateLayoutParameters();
}
}
// if this is called in Component.onCompleted we have to wait a loop the item is added to a scene
Q_EMIT mainItemChanged();
}
}
void DialogPrivate::slotMainItemSizeChanged()
{
syncToMainItemSize();
}
QQuickItem *Dialog::visualParent() const
{
return d->visualParent;
}
void Dialog::setVisualParent(QQuickItem *visualParent)
{
if (d->visualParent == visualParent) {
return;
}
d->visualParent = visualParent;
Q_EMIT visualParentChanged();
if (visualParent) {
if (visualParent->window()) {
setTransientParent(visualParent->window());
}
if (d->mainItem) {
d->syncToMainItemSize();
}
}
}
QPoint Dialog::popupPosition(QQuickItem *item, const QSize &size)
{
if (!item) {
// If no item was specified try to align at the center of the parent view
QQuickItem *parentItem = qobject_cast<QQuickItem *>(parent());
if (parentItem) {
QScreen *screen = parentItem->window()->screen();
switch (d->location) {
case Plasma::Types::TopEdge:
return QPoint(screen->availableGeometry().center().x() - size.width() / 2, screen->availableGeometry().y());
case Plasma::Types::LeftEdge:
return QPoint(screen->availableGeometry().x(), screen->availableGeometry().center().y() - size.height() / 2);
case Plasma::Types::RightEdge:
return QPoint(screen->availableGeometry().right() - size.width(), screen->availableGeometry().center().y() - size.height() / 2);
case Plasma::Types::BottomEdge:
return QPoint(screen->availableGeometry().center().x() - size.width() / 2, screen->availableGeometry().bottom() - size.height());
// Default center in the screen
default:
return screen->geometry().center() - QPoint(size.width() / 2, size.height() / 2);
}
} else {
return QPoint();
}
}
QPointF pos = item->mapToScene(QPointF(0, 0));
if (item->window()) {
pos = item->window()->mapToGlobal(pos.toPoint());
} else {
return QPoint();
}
// if the item is in a dock or in a window that ignores WM we want to position the popups outside of the dock
const KWindowInfo winInfo(item->window()->winId(), NET::WMWindowType);
const bool outsideParentWindow =
((winInfo.windowType(NET::AllTypesMask) == NET::Dock) || (item->window()->flags() & Qt::X11BypassWindowManagerHint)) && item->window()->mask().isNull();
QRect parentGeometryBounds;
if (outsideParentWindow) {
parentGeometryBounds = item->window()->geometry();
} else {
parentGeometryBounds = item->mapRectToScene(item->boundingRect()).toRect();
if (item->window()) {
parentGeometryBounds.moveTopLeft(item->window()->mapToGlobal(parentGeometryBounds.topLeft()));
pos = parentGeometryBounds.topLeft();
}
}
const QPoint topPoint(pos.x() + (item->mapRectToScene(item->boundingRect()).width() - size.width()) / 2, parentGeometryBounds.top() - size.height());
const QPoint bottomPoint(pos.x() + (item->mapRectToScene(item->boundingRect()).width() - size.width()) / 2, parentGeometryBounds.bottom());
const QPoint leftPoint(parentGeometryBounds.left() - size.width(), pos.y() + (item->mapRectToScene(item->boundingRect()).height() - size.height()) / 2);
const QPoint rightPoint(parentGeometryBounds.right(), pos.y() + (item->mapRectToScene(item->boundingRect()).height() - size.height()) / 2);
QPoint dialogPos;
if (d->location == Plasma::Types::TopEdge) {
dialogPos = bottomPoint;
} else if (d->location == Plasma::Types::LeftEdge) {
dialogPos = rightPoint;
} else if (d->location == Plasma::Types::RightEdge) {
dialogPos = leftPoint;
} else { // Types::BottomEdge
dialogPos = topPoint;
}
// find the correct screen for the item
// we do not rely on item->window()->screen() because
// QWindow::screen() is always only the screen where the window gets first created
// not actually the current window. See QWindow::screen() documentation
QRect avail = item->window()->screen()->availableGeometry();
if (outsideParentWindow && d->frameSvgItem->enabledBorders() != Plasma::FrameSvg::AllBorders) {
// make the panel look it's inside the panel, in order to not make it look cut
switch (d->location) {
case Plasma::Types::LeftEdge:
case Plasma::Types::RightEdge:
avail.setTop(qMax(avail.top(), parentGeometryBounds.top()));
avail.setBottom(qMin(avail.bottom(), parentGeometryBounds.bottom()));
break;
default:
avail.setLeft(qMax(avail.left(), parentGeometryBounds.left()));
avail.setRight(qMin(avail.right(), parentGeometryBounds.right()));
break;
}
}
if (dialogPos.x() < avail.left()) {
// popup hits lhs
if (d->location != Plasma::Types::LeftEdge || d->location == Plasma::Types::RightEdge) {
// move it
dialogPos.setX(avail.left());
} else {
// swap edge
dialogPos.setX(rightPoint.x());
}
}
if (dialogPos.x() + size.width() > avail.right()) {
// popup hits rhs
if (d->location == Plasma::Types::TopEdge || d->location == Plasma::Types::BottomEdge) {
dialogPos.setX(qMax(avail.left(), (avail.right() - size.width() + 1)));
} else {
dialogPos.setX(leftPoint.x());
}
}
if (dialogPos.y() < avail.top()) {
// hitting top
if (d->location == Plasma::Types::LeftEdge || d->location == Plasma::Types::RightEdge) {
dialogPos.setY(avail.top());
} else {
dialogPos.setY(bottomPoint.y());
}
}
if (dialogPos.y() + size.height() > avail.bottom()) {
// hitting bottom
if (d->location == Plasma::Types::TopEdge || d->location == Plasma::Types::BottomEdge) {
dialogPos.setY(topPoint.y());
} else {
dialogPos.setY(qMax(avail.top(), (avail.bottom() - size.height() + 1)));
}
}
return dialogPos;
}
Plasma::Types::Location Dialog::location() const
{
return d->location;
}
void Dialog::setLocation(Plasma::Types::Location location)
{
if (d->location == location) {
return;
}
d->location = location;
Q_EMIT locationChanged();
if (d->mainItem) {
d->syncToMainItemSize();
}
}
QObject *Dialog::margins() const
{
return d->frameSvgItem->fixedMargins();
}
QObject *Dialog::inset() const
{
return d->frameSvgItem->inset();
}
void Dialog::setFramelessFlags(Qt::WindowFlags flags)
{
if (d->type == Dialog::Normal)
flags |= Qt::Dialog;
setFlags(Qt::FramelessWindowHint | flags);
d->applyType();
Q_EMIT flagsChanged();
}
void Dialog::adjustGeometry(const QRect &geom)
{
setGeometry(geom);
}
void Dialog::resizeEvent(QResizeEvent *re)
{
QQuickWindow::resizeEvent(re);
// it's a spontaneous event generated in qguiapplication.cpp QGuiApplicationPrivate::processWindowScreenChangedEvent
// QWindowSystemInterfacePrivate::GeometryChangeEvent gce(window, QHighDpi::fromNativePixels(window->handle()->geometry(), window), QRect());
// This happens before the first show event when there is more than one screen,
// right after the window has been created, the window is still 0x0,
// but the resize event gets delivered with 0x0 again and executed with all the bad side effects
// this seems to happen for every window when there are multiple screens, so something we have probably to watch out for in the future
if (re->size().isEmpty() || re->size() == re->oldSize()) {
return;
}
// A dialog can be resized even if no mainItem has ever been set
if (!d->mainItem) {
return;
}
d->mainItem->disconnect(this);
d->frameSvgItem->setSize(QSizeF(re->size().width(), re->size().height()));
auto margin = d->frameSvgItem->fixedMargins();
d->mainItem->setPosition(QPointF(margin->left(), margin->top()));
d->mainItem->setSize(QSize(re->size().width() - margin->left() - margin->right(), re->size().height() - margin->top() - margin->bottom()));
QObject::connect(d->mainItem, SIGNAL(widthChanged()), this, SLOT(slotMainItemSizeChanged()));
QObject::connect(d->mainItem, SIGNAL(heightChanged()), this, SLOT(slotMainItemSizeChanged()));
}
void Dialog::setType(WindowType type)
{
if (type == d->type) {
return;
}
d->type = type;
d->applyType();
Q_EMIT typeChanged();
}
Dialog::WindowType Dialog::type() const
{
return d->type;
}
void Dialog::focusInEvent(QFocusEvent *ev)
{
QQuickWindow::focusInEvent(ev);
}
void Dialog::focusOutEvent(QFocusEvent *ev)
{
if (d->hideOnWindowDeactivate) {
bool parentHasFocus = false;
QWindow *parentWindow = transientParent();
while (parentWindow) {
if (parentWindow->isActive() && !(parentWindow->flags() & Qt::WindowDoesNotAcceptFocus)) {
parentHasFocus = true;
break;
}
parentWindow = parentWindow->transientParent();
}
const QWindow *focusWindow = QGuiApplication::focusWindow();
bool childHasFocus = focusWindow && ((focusWindow->isActive() && isAncestorOf(focusWindow)) || (focusWindow->type() & Qt::Popup) == Qt::Popup);
const bool viewClicked = qobject_cast<const KQuickAddons::QuickViewSharedEngine *>(focusWindow)
#if PLASMAQUICK_BUILD_DEPRECATED_SINCE(5, 83)
|| qobject_cast<const View *>(focusWindow)
#endif
|| qobject_cast<const ConfigView *>(focusWindow);
if (viewClicked || (!parentHasFocus && !childHasFocus)) {
setVisible(false);
Q_EMIT windowDeactivated();
}
}
QQuickWindow::focusOutEvent(ev);
}
void Dialog::showEvent(QShowEvent *event)
{
if (d->backgroundHints != Dialog::NoBackground) {
DialogShadows::self()->addWindow(this, d->frameSvgItem->enabledBorders());
}
KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager | NET::SkipSwitcher);
QQuickWindow::showEvent(event);
}
bool Dialog::event(QEvent *event)
{
if (event->type() == QEvent::Expose) {
if (!KWindowSystem::isPlatformWayland() || !isExposed()) {
return QQuickWindow::event(event);
}
/*
* expose event is the only place where to correctly
* register our wayland extensions, as showevent is a bit too
* soon and the platform window isn't shown yet
* create a shell surface every time the window gets exposed
* (only the first expose event, guarded by shelldurface existence)
* and tear it down when the window gets hidden
* see https://phabricator.kde.org/T6064
*/
#if HAVE_KWAYLAND
// sometimes non null regions arrive even for non visible windows
// for which surface creation would fail
if (!d->shellSurface && isVisible()) {
KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager | NET::SkipSwitcher);
d->setupWaylandIntegration();
d->updateVisibility(true);
d->updateTheme();
}
#endif
} else if (event->type() == QEvent::PlatformSurface) {
const QPlatformSurfaceEvent *pSEvent = static_cast<QPlatformSurfaceEvent *>(event);
if (pSEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) {
KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager | NET::SkipSwitcher);
}
} else if (event->type() == QEvent::Show) {
d->updateVisibility(true);
} else if (event->type() == QEvent::Hide) {
d->updateVisibility(false);
#if HAVE_KWAYLAND
delete d->shellSurface;
d->shellSurface = nullptr;
#endif
} else if (event->type() == QEvent::Move) {
#if HAVE_KWAYLAND
if (d->shellSurface) {
QMoveEvent *me = static_cast<QMoveEvent *>(event);
d->shellSurface->setPosition(me->pos());
}
#endif
}
/*Fitt's law: if the containment has margins, and the mouse cursor clicked
* on the mouse edge, forward the click in the containment boundaries
*/
if (d->mainItem && !d->mainItem->size().isEmpty()) {
switch (event->type()) {
case QEvent::MouseMove:
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease: {
QMouseEvent *me = static_cast<QMouseEvent *>(event);
// don't mess with position if the cursor is actually outside the view:
// somebody is doing a click and drag that must not break when the cursor i outside
if (geometry().contains(me->screenPos().toPoint()) && !d->mainItemContainsPosition(me->windowPos())) {
QMouseEvent me2(me->type(),
d->positionAdjustedForMainItem(me->windowPos()),
d->positionAdjustedForMainItem(me->windowPos()),
d->positionAdjustedForMainItem(me->windowPos()) + position(),
me->button(),
me->buttons(),
me->modifiers());
if (isVisible()) {
QCoreApplication::sendEvent(this, &me2);
}
return true;
}
break;
}
case QEvent::Wheel: {
QWheelEvent *we = static_cast<QWheelEvent *>(event);
const QPoint pos = we->position().toPoint();
if (!d->mainItemContainsPosition(pos)) {
QWheelEvent we2(d->positionAdjustedForMainItem(pos),
d->positionAdjustedForMainItem(pos) + position(),
we->pixelDelta(),
we->angleDelta(),
we->buttons(),
we->modifiers(),
we->phase(),
false /*inverted*/);
if (isVisible()) {
QCoreApplication::sendEvent(this, &we2);
}
return true;
}
break;
}
case QEvent::DragEnter: {
QDragEnterEvent *de = static_cast<QDragEnterEvent *>(event);
if (!d->mainItemContainsPosition(de->pos())) {
QDragEnterEvent de2(d->positionAdjustedForMainItem(de->pos()).toPoint(),
de->possibleActions(),
de->mimeData(),
de->mouseButtons(),
de->keyboardModifiers());
if (isVisible()) {
QCoreApplication::sendEvent(this, &de2);
}
return true;
}
break;
}
// DragLeave just works
case QEvent::DragLeave:
break;
case QEvent::DragMove: {
QDragMoveEvent *de = static_cast<QDragMoveEvent *>(event);
if (!d->mainItemContainsPosition(de->pos())) {
QDragMoveEvent de2(d->positionAdjustedForMainItem(de->pos()).toPoint(),
de->possibleActions(),
de->mimeData(),
de->mouseButtons(),
de->keyboardModifiers());
if (isVisible()) {
QCoreApplication::sendEvent(this, &de2);
}
return true;
}
break;
}
case QEvent::Drop: {
QDropEvent *de = static_cast<QDropEvent *>(event);
if (!d->mainItemContainsPosition(de->pos())) {
QDropEvent de2(d->positionAdjustedForMainItem(de->pos()).toPoint(),
de->possibleActions(),
de->mimeData(),
de->mouseButtons(),
de->keyboardModifiers());
if (isVisible()) {
QCoreApplication::sendEvent(this, &de2);
}
return true;
}
break;
}
default:
break;
}
}
return QQuickWindow::event(event);
}
void Dialog::hideEvent(QHideEvent *event)
{
QQuickWindow::hideEvent(event);
}
void Dialog::classBegin()
{
d->componentComplete = false;
}
void Dialog::componentComplete()
{
d->componentComplete = true;
QQuickWindow::setVisible(d->visible);
d->updateTheme();
}
bool Dialog::hideOnWindowDeactivate() const
{
return d->hideOnWindowDeactivate;
}
void Dialog::setHideOnWindowDeactivate(bool hide)
{
if (d->hideOnWindowDeactivate == hide) {
return;
}
d->hideOnWindowDeactivate = hide;
Q_EMIT hideOnWindowDeactivateChanged();
}
bool Dialog::isOutputOnly() const
{
return d->outputOnly;
}
void Dialog::setOutputOnly(bool outputOnly)
{
if (d->outputOnly == outputOnly) {
return;
}
d->outputOnly = outputOnly;
Q_EMIT outputOnlyChanged();
}
void Dialog::setVisible(bool visible)
{
// only update real visibility when we have finished component completion
// and all flags have been set
d->visible = visible;
if (d->componentComplete) {
if (visible && d->visualParent) {
setPosition(popupPosition(d->visualParent, size()));
}
QQuickWindow::setVisible(visible);
// signal will be emitted and proxied from the QQuickWindow code
} else {
Q_EMIT visibleChangedProxy();
}
}
bool Dialog::isVisible() const
{
if (d->componentComplete) {
return QQuickWindow::isVisible();
}
return d->visible;
}
Dialog::BackgroundHints Dialog::backgroundHints() const
{
return d->backgroundHints;
}
void Dialog::setBackgroundHints(Dialog::BackgroundHints hints)
{
if (d->backgroundHints == hints) {
return;
}
d->backgroundHints = hints;
d->updateTheme();
Q_EMIT backgroundHintsChanged();
}
}
#include "moc_dialog.cpp"