diff --git a/src/declarativeimports/core/framesvgitem.cpp b/src/declarativeimports/core/framesvgitem.cpp index 1a80adbcb..8f4404a1e 100644 --- a/src/declarativeimports/core/framesvgitem.cpp +++ b/src/declarativeimports/core/framesvgitem.cpp @@ -1,676 +1,683 @@ /* * Copyright 2010 Marco Martin * Copyright 2014 David Edmundson * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "framesvgitem.h" #include #include #include #include #include #include #include #include #include #include //floor() namespace Plasma { Q_GLOBAL_STATIC(ImageTexturesCache, s_cache) class FrameNode : public QSGNode { public: FrameNode(const QString& prefix, FrameSvg* svg) : QSGNode() , leftWidth(0) , rightWidth(0) , topHeight(0) , bottomHeight(0) { if (svg->enabledBorders() & FrameSvg::LeftBorder) leftWidth = svg->elementSize(prefix % QLatin1String("left")).width(); if (svg->enabledBorders() & FrameSvg::RightBorder) rightWidth = svg->elementSize(prefix % QLatin1String("right")).width(); if (svg->enabledBorders() & FrameSvg::TopBorder) topHeight = svg->elementSize(prefix % QLatin1String("top")).height(); if (svg->enabledBorders() & FrameSvg::BottomBorder) bottomHeight = svg->elementSize(prefix % QLatin1String("bottom")).height(); } QRect contentsRect(const QSize& size) const { const QSize contentSize(size.width() - leftWidth - rightWidth, size.height() - topHeight - bottomHeight); return QRect(QPoint(leftWidth, topHeight), contentSize); } private: int leftWidth; int rightWidth; int topHeight; int bottomHeight; }; class FrameItemNode : public ManagedTextureNode { public: enum FitMode { //render SVG at native resolution then stretch it in openGL FastStretch, //on resize re-render the part of the frame from the SVG Stretch, Tile }; FrameItemNode(FrameSvgItem* frameSvg, FrameSvg::EnabledBorders borders, FitMode fitMode, QSGNode* parent) : ManagedTextureNode() , m_frameSvg(frameSvg) , m_border(borders) , m_lastParent(parent) , m_fitMode(fitMode) { m_lastParent->appendChildNode(this); if (m_fitMode == Tile) { if (m_border == FrameSvg::TopBorder || m_border == FrameSvg::BottomBorder || m_border == FrameSvg::NoBorder) { static_cast(material())->setHorizontalWrapMode(QSGTexture::Repeat); static_cast(opaqueMaterial())->setHorizontalWrapMode(QSGTexture::Repeat); } if (m_border == FrameSvg::LeftBorder || m_border == FrameSvg::RightBorder || m_border == FrameSvg::NoBorder) { static_cast(material())->setVerticalWrapMode(QSGTexture::Repeat); static_cast(opaqueMaterial())->setVerticalWrapMode(QSGTexture::Repeat); } } if (m_fitMode == Tile || m_fitMode == FastStretch) { QString elementId = m_frameSvg->frameSvg()->actualPrefix() + FrameSvgHelpers::borderToElementId(m_border); m_elementNativeSize = m_frameSvg->frameSvg()->elementSize(elementId); if (m_elementNativeSize.isEmpty()) { //if the default element is empty, we can avoid the slower tiling path //this also avoids a divide by 0 error m_fitMode = FastStretch; } updateTexture(m_elementNativeSize, elementId); } } void updateTexture(const QSize &size, const QString &elementId) { QQuickWindow::CreateTextureOptions options; if (m_fitMode != Tile) { options = QQuickWindow::TextureCanUseAtlas; } setTexture(s_cache->loadTexture(m_frameSvg->window(), m_frameSvg->frameSvg()->image(size, elementId), options)); } void reposition(const QRect& frameGeometry, QSize& fullSize) { QRect nodeRect = FrameSvgHelpers::sectionRect(m_border, frameGeometry, fullSize); //ensure we're not passing a weird rectangle to updateTexturedRectGeometry if(!nodeRect.isValid() || nodeRect.isEmpty()) nodeRect = QRect(); //the position of the relevant texture within this texture ID. //for atlas' this will only be a small part of the texture QRectF textureRect; if (m_fitMode == Tile) { textureRect = QRectF(0,0,1,1); //we can never be in an atlas for tiled images. //if tiling horizontally if (m_border == FrameSvg::TopBorder || m_border == FrameSvg::BottomBorder || m_border == FrameSvg::NoBorder) { // cmp. CSS3's border-image-repeat: "repeat", though with first tile not centered, but aligned to left textureRect.setWidth((qreal) nodeRect.width() / m_elementNativeSize.width()); } //if tiling vertically if (m_border == FrameSvg::LeftBorder || m_border == FrameSvg::RightBorder || m_border == FrameSvg::NoBorder) { // cmp. CSS3's border-image-repeat: "repeat", though with first tile not centered, but aligned to top textureRect.setHeight((qreal) nodeRect.height() / m_elementNativeSize.height()); } } else if (m_fitMode == Stretch) { QString prefix = m_frameSvg->frameSvg()->actualPrefix(); QString elementId = prefix + FrameSvgHelpers::borderToElementId(m_border); //re-render the SVG at new size updateTexture(nodeRect.size(), elementId); textureRect = texture()->normalizedTextureSubRect(); } else if (texture()) { // for fast stretch. textureRect = texture()->normalizedTextureSubRect(); } QSGGeometry::updateTexturedRectGeometry(geometry(), nodeRect, textureRect); markDirty(QSGNode::DirtyGeometry); } private: FrameSvgItem* m_frameSvg; FrameSvg::EnabledBorders m_border; QSGNode *m_lastParent; QSize m_elementNativeSize; FitMode m_fitMode; }; FrameSvgItemMargins::FrameSvgItemMargins(Plasma::FrameSvg *frameSvg, QObject *parent) : QObject(parent), m_frameSvg(frameSvg), m_fixed(false) { //qDebug() << "margins at: " << left() << top() << right() << bottom(); } qreal FrameSvgItemMargins::left() const { if (m_fixed) { return m_frameSvg->fixedMarginSize(Types::LeftMargin); } else { return m_frameSvg->marginSize(Types::LeftMargin); } } qreal FrameSvgItemMargins::top() const { if (m_fixed) { return m_frameSvg->fixedMarginSize(Types::TopMargin); } else { return m_frameSvg->marginSize(Types::TopMargin); } } qreal FrameSvgItemMargins::right() const { if (m_fixed) { return m_frameSvg->fixedMarginSize(Types::RightMargin); } else { return m_frameSvg->marginSize(Types::RightMargin); } } qreal FrameSvgItemMargins::bottom() const { if (m_fixed) { return m_frameSvg->fixedMarginSize(Types::BottomMargin); } else { return m_frameSvg->marginSize(Types::BottomMargin); } } qreal FrameSvgItemMargins::horizontal() const { return left() + right(); } qreal FrameSvgItemMargins::vertical() const { return top() + bottom(); } QVector FrameSvgItemMargins::margins() const { qreal left, top, right, bottom; m_frameSvg->getMargins(left, top, right, bottom); return {left, top, right, bottom}; } void FrameSvgItemMargins::update() { emit marginsChanged(); } void FrameSvgItemMargins::setFixed(bool fixed) { if (fixed == m_fixed) { return; } m_fixed = fixed; emit marginsChanged(); } bool FrameSvgItemMargins::isFixed() const { return m_fixed; } FrameSvgItem::FrameSvgItem(QQuickItem *parent) : QQuickItem(parent), m_margins(nullptr), m_fixedMargins(nullptr), m_textureChanged(false), m_sizeChanged(false), m_fastPath(true) { m_frameSvg = new Plasma::FrameSvg(this); setFlag(ItemHasContents, true); connect(m_frameSvg, &FrameSvg::repaintNeeded, this, &FrameSvgItem::doUpdate); connect(&Units::instance(), &Units::devicePixelRatioChanged, this, &FrameSvgItem::updateDevicePixelRatio); connect(m_frameSvg, &Svg::fromCurrentThemeChanged, this, &FrameSvgItem::fromCurrentThemeChanged); connect(m_frameSvg, &Svg::statusChanged, this, &FrameSvgItem::statusChanged); + connect(m_frameSvg, &Svg::sizeChanged, this, &FrameSvgItem::maskChanged); } FrameSvgItem::~FrameSvgItem() { } class CheckMarginsChange { public: CheckMarginsChange(QVector& oldMargins, FrameSvgItemMargins *marginsObject) : m_oldMargins(oldMargins), m_marginsObject(marginsObject) {} ~CheckMarginsChange() { const QVector oldMarginsBefore = m_oldMargins; m_oldMargins = m_marginsObject ? m_marginsObject->margins() : QVector(); if (oldMarginsBefore != m_oldMargins) { m_marginsObject->update(); } } private: QVector& m_oldMargins; FrameSvgItemMargins *const m_marginsObject; }; void FrameSvgItem::setImagePath(const QString &path) { if (m_frameSvg->imagePath() == path) { return; } CheckMarginsChange checkMargins(m_oldMargins, m_margins); CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); updateDevicePixelRatio(); m_frameSvg->setImagePath(path); if (implicitWidth() <= 0) { setImplicitWidth(m_frameSvg->marginSize(Plasma::Types::LeftMargin) + m_frameSvg->marginSize(Plasma::Types::RightMargin)); } if (implicitHeight() <= 0) { setImplicitHeight(m_frameSvg->marginSize(Plasma::Types::TopMargin) + m_frameSvg->marginSize(Plasma::Types::BottomMargin)); } emit imagePathChanged(); if (isComponentComplete()) { applyPrefixes(); m_frameSvg->resizeFrame(QSizeF(width(), height())); m_textureChanged = true; update(); } } QString FrameSvgItem::imagePath() const { return m_frameSvg->imagePath(); } void FrameSvgItem::setPrefix(const QVariant &prefixes) { QStringList prefixList; //is this a simple string? if (prefixes.canConvert()) { prefixList << prefixes.toString(); } else if (prefixes.canConvert()) { prefixList = prefixes.toStringList(); } if (m_prefixes == prefixList) { return; } CheckMarginsChange checkMargins(m_oldMargins, m_margins); CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); m_prefixes = prefixList; applyPrefixes(); if (implicitWidth() <= 0) { setImplicitWidth(m_frameSvg->marginSize(Plasma::Types::LeftMargin) + m_frameSvg->marginSize(Plasma::Types::RightMargin)); } if (implicitHeight() <= 0) { setImplicitHeight(m_frameSvg->marginSize(Plasma::Types::TopMargin) + m_frameSvg->marginSize(Plasma::Types::BottomMargin)); } emit prefixChanged(); if (isComponentComplete()) { m_frameSvg->resizeFrame(QSizeF(width(), height())); m_textureChanged = true; update(); } } QVariant FrameSvgItem::prefix() const { return m_prefixes; } QString FrameSvgItem::usedPrefix() const { return m_frameSvg->prefix(); } FrameSvgItemMargins *FrameSvgItem::margins() { if (!m_margins) { m_margins = new FrameSvgItemMargins(m_frameSvg, this); } return m_margins; } FrameSvgItemMargins *FrameSvgItem::fixedMargins() { if (!m_fixedMargins) { m_fixedMargins = new FrameSvgItemMargins(m_frameSvg, this); m_fixedMargins->setFixed(true); } return m_fixedMargins; } void FrameSvgItem::setColorGroup(Plasma::Theme::ColorGroup group) { if (m_frameSvg->colorGroup() == group) { return; } m_frameSvg->setColorGroup(group); emit colorGroupChanged(); } Plasma::Theme::ColorGroup FrameSvgItem::colorGroup() const { return m_frameSvg->colorGroup(); } bool FrameSvgItem::fromCurrentTheme() const { return m_frameSvg->fromCurrentTheme(); } void FrameSvgItem::setStatus(Plasma::Svg::Status status) { m_frameSvg->setStatus(status); } Plasma::Svg::Status FrameSvgItem::status() const { return m_frameSvg->status(); } void FrameSvgItem::setEnabledBorders(const Plasma::FrameSvg::EnabledBorders borders) { if (m_frameSvg->enabledBorders() == borders) { return; } CheckMarginsChange checkMargins(m_oldMargins, m_margins); m_frameSvg->setEnabledBorders(borders); emit enabledBordersChanged(); m_textureChanged = true; update(); } Plasma::FrameSvg::EnabledBorders FrameSvgItem::enabledBorders() const { return m_frameSvg->enabledBorders(); } bool FrameSvgItem::hasElementPrefix(const QString &prefix) const { return m_frameSvg->hasElementPrefix(prefix); } + +QRegion FrameSvgItem::mask() const +{ + return m_frameSvg->mask(); +} + void FrameSvgItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { if (isComponentComplete()) { m_frameSvg->resizeFrame(newGeometry.size()); m_sizeChanged = true; } QQuickItem::geometryChanged(newGeometry, oldGeometry); } void FrameSvgItem::doUpdate() { if (m_frameSvg->isRepaintBlocked()) { return; } CheckMarginsChange checkMargins(m_oldMargins, m_margins); CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); //if the theme changed, the available prefix may have changed as well applyPrefixes(); if (implicitWidth() <= 0) { setImplicitWidth(m_frameSvg->marginSize(Plasma::Types::LeftMargin) + m_frameSvg->marginSize(Plasma::Types::RightMargin)); } if (implicitHeight() <= 0) { setImplicitHeight(m_frameSvg->marginSize(Plasma::Types::TopMargin) + m_frameSvg->marginSize(Plasma::Types::BottomMargin)); } QString prefix = m_frameSvg->actualPrefix(); bool hasOverlay = !prefix.startsWith(QLatin1String("mask-")) && m_frameSvg->hasElement(prefix % QLatin1String("overlay")); bool hasComposeOverBorder = m_frameSvg->hasElement(prefix % QLatin1String("hint-compose-over-border")) && m_frameSvg->hasElement(QLatin1String("mask-") % prefix % QLatin1String("center")); m_fastPath = !hasOverlay && !hasComposeOverBorder; //software rendering (at time of writing Qt5.10) doesn't seem to like our tiling/stretching in the 9-tiles. //also when using QPainter it's arguably faster to create and cache pixmaps of the whole frame, which is what the slow path does if (QQuickWindow::sceneGraphBackend() == QLatin1String("software")) { m_fastPath = false; } m_textureChanged = true; update(); emit repaintNeeded(); } Plasma::FrameSvg *FrameSvgItem::frameSvg() const { return m_frameSvg; } QSGNode *FrameSvgItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) { if (!window() || !m_frameSvg || (!m_frameSvg->hasElementPrefix(m_frameSvg->actualPrefix()) && !m_frameSvg->hasElementPrefix(m_frameSvg->prefix()))) { delete oldNode; return nullptr; } if (m_fastPath) { if (m_textureChanged) { delete oldNode; oldNode = nullptr; } if (!oldNode) { QString prefix = m_frameSvg->actualPrefix(); oldNode = new FrameNode(prefix, m_frameSvg); bool tileCenter = (m_frameSvg->hasElement(QStringLiteral("hint-tile-center")) || m_frameSvg->hasElement(prefix % QLatin1String("hint-tile-center"))); bool stretchBorders = (m_frameSvg->hasElement(QStringLiteral("hint-stretch-borders")) || m_frameSvg->hasElement(prefix % QLatin1String("hint-stretch-borders"))); FrameItemNode::FitMode borderFitMode = stretchBorders ? FrameItemNode::Stretch : FrameItemNode::Tile; FrameItemNode::FitMode centerFitMode = tileCenter ? FrameItemNode::Tile: FrameItemNode::Stretch; new FrameItemNode(this, FrameSvg::NoBorder, centerFitMode, oldNode); if (enabledBorders() & (FrameSvg::TopBorder | FrameSvg::LeftBorder)) { new FrameItemNode(this, FrameSvg::TopBorder | FrameSvg::LeftBorder, FrameItemNode::FastStretch, oldNode); } if (enabledBorders() & (FrameSvg::TopBorder | FrameSvg::RightBorder)) { new FrameItemNode(this, FrameSvg::TopBorder | FrameSvg::RightBorder, FrameItemNode::FastStretch, oldNode); } if (enabledBorders() & FrameSvg::TopBorder) { new FrameItemNode(this, FrameSvg::TopBorder, borderFitMode, oldNode); } if (enabledBorders() & FrameSvg::BottomBorder) { new FrameItemNode(this, FrameSvg::BottomBorder, borderFitMode, oldNode); } if (enabledBorders() & (FrameSvg::BottomBorder | FrameSvg::LeftBorder)) { new FrameItemNode(this, FrameSvg::BottomBorder | FrameSvg::LeftBorder, FrameItemNode::FastStretch, oldNode); } if (enabledBorders() & (FrameSvg::BottomBorder | FrameSvg::RightBorder)) { new FrameItemNode(this, FrameSvg::BottomBorder | FrameSvg::RightBorder, FrameItemNode::FastStretch, oldNode); } if (enabledBorders() & FrameSvg::LeftBorder) { new FrameItemNode(this, FrameSvg::LeftBorder, borderFitMode, oldNode); } if (enabledBorders() & FrameSvg::RightBorder) { new FrameItemNode(this, FrameSvg::RightBorder, borderFitMode, oldNode); } m_sizeChanged = true; m_textureChanged = false; } if (m_sizeChanged) { FrameNode* frameNode = static_cast(oldNode); QSize frameSize(width(), height()); QRect geometry = frameNode->contentsRect(frameSize); QSGNode *node = oldNode->firstChild(); while (node) { static_cast(node)->reposition(geometry, frameSize); node = node->nextSibling(); } m_sizeChanged = false; } } else { ManagedTextureNode *textureNode = dynamic_cast(oldNode); if (!textureNode) { delete oldNode; textureNode = new ManagedTextureNode; textureNode->setFiltering(QSGTexture::Nearest); m_textureChanged = true; //force updating the texture on our newly created node oldNode = textureNode; } if ((m_textureChanged || m_sizeChanged) || textureNode->texture()->textureSize() != m_frameSvg->size()) { QImage image = m_frameSvg->framePixmap().toImage(); textureNode->setTexture(s_cache->loadTexture(window(), image)); textureNode->setRect(0, 0, width(), height()); m_textureChanged = false; m_sizeChanged = false; } } return oldNode; } void FrameSvgItem::classBegin() { QQuickItem::classBegin(); m_frameSvg->setRepaintBlocked(true); } void FrameSvgItem::componentComplete() { CheckMarginsChange checkMargins(m_oldMargins, m_margins); CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); QQuickItem::componentComplete(); m_frameSvg->resizeFrame(QSize(width(), height())); m_frameSvg->setRepaintBlocked(false); m_textureChanged = true; } void FrameSvgItem::updateDevicePixelRatio() { m_frameSvg->setScaleFactor(qMax(1.0, floor(Units::instance().devicePixelRatio()))); //devicepixelratio is always set integer in the svg, so needs at least 192dpi to double up. //(it needs to be integer to have lines contained inside a svg piece to keep being pixel aligned) const auto newDevicePixelRation = qMax(1.0, floor(window() ? window()->devicePixelRatio() : qApp->devicePixelRatio())); if (newDevicePixelRation != m_frameSvg->devicePixelRatio()) { m_frameSvg->setDevicePixelRatio(qMax(1.0, newDevicePixelRation)); m_textureChanged = true; } } void FrameSvgItem::applyPrefixes() { if (m_frameSvg->imagePath().isEmpty()) { return; } const QString oldPrefix = m_frameSvg->prefix(); if (m_prefixes.isEmpty()) { m_frameSvg->setElementPrefix(QString()); if (oldPrefix != m_frameSvg->prefix()) { emit usedPrefixChanged(); } return; } bool found = false; for (const QString &prefix : qAsConst(m_prefixes)) { if (m_frameSvg->hasElementPrefix(prefix)) { m_frameSvg->setElementPrefix(prefix); found = true; break; } } if (!found) { //this setElementPrefix is done to keep the same behavior as before, when it was a simple string m_frameSvg->setElementPrefix(m_prefixes.constLast()); } if (oldPrefix != m_frameSvg->prefix()) { emit usedPrefixChanged(); } } void FrameSvgItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData & value) { if (change == ItemSceneChange && value.window) { updateDevicePixelRatio(); } QQuickItem::itemChange(change, value); } } // Plasma namespace diff --git a/src/declarativeimports/core/framesvgitem.h b/src/declarativeimports/core/framesvgitem.h index b92cba35c..017fdf013 100644 --- a/src/declarativeimports/core/framesvgitem.h +++ b/src/declarativeimports/core/framesvgitem.h @@ -1,269 +1,278 @@ /*************************************************************************** * Copyright 2010 Marco Martin * * Copyright 2014 David Edmundson * * * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #ifndef FRAMESVGITEM_P #define FRAMESVGITEM_P #include #include #include #include "units.h" namespace Plasma { class FrameSvg; /** * @class FrameSvgItemMargins * * @short The sizes of a frame's margins. */ class FrameSvgItemMargins : public QObject { Q_OBJECT /** * Width in pixels of the left margin. */ Q_PROPERTY(qreal left READ left NOTIFY marginsChanged) /** * Height in pixels of the top margin. */ Q_PROPERTY(qreal top READ top NOTIFY marginsChanged) /** * Width in pixels of the right margin. */ Q_PROPERTY(qreal right READ right NOTIFY marginsChanged) /** * Height in pixels of the bottom margin. */ Q_PROPERTY(qreal bottom READ bottom NOTIFY marginsChanged) /** * Width in pixels of the left and right margins combined. */ Q_PROPERTY(qreal horizontal READ horizontal NOTIFY marginsChanged) /** * Height in pixels of the top and bottom margins combined. */ Q_PROPERTY(qreal vertical READ vertical NOTIFY marginsChanged) public: FrameSvgItemMargins(Plasma::FrameSvg *frameSvg, QObject *parent = nullptr); qreal left() const; qreal top() const; qreal right() const; qreal bottom() const; qreal horizontal() const; qreal vertical() const; /// returns a vector with left, top, right, bottom QVector margins() const; void setFixed(bool fixed); bool isFixed() const; public Q_SLOTS: void update(); Q_SIGNALS: void marginsChanged(); private: FrameSvg *m_frameSvg; bool m_fixed; }; /** * @class FrameSvgItem * * @short Provides an SVG with borders. * * It is exposed as org.kde.plasma.core.FrameSvgItem */ class FrameSvgItem : public QQuickItem { Q_OBJECT Q_INTERFACES(QQmlParserStatus) /** * Theme relative path of the svg, like "widgets/background" */ Q_PROPERTY(QString imagePath READ imagePath WRITE setImagePath NOTIFY imagePathChanged) /** * prefix for the 9 piece svg, like "pushed" or "normal" for the button * see https://techbase.kde.org/Development/Tutorials/Plasma/ThemeDetails * for a list of paths and prefixes * It can also be an array of strings, specifying a fallback chain in case * the first element isn't found in the theme, eg ["toolbutton-normal", "normal"] * so it's easy to keep backwards compatibility with old themes * (Note: fallback chain is supported only @since 5.32) */ Q_PROPERTY(QVariant prefix READ prefix WRITE setPrefix NOTIFY prefixChanged) /** * the actual prefix that was used, if a fallback chain array was set as "prefix" * @since 5.34 */ Q_PROPERTY(QString usedPrefix READ usedPrefix NOTIFY usedPrefixChanged) /** * The margins of the frame, read only * @see FrameSvgItemMargins */ Q_PROPERTY(QObject *margins READ margins CONSTANT) /** * The margins of the frame, regardless if they are enabled or not * read only * @see FrameSvgItemMargins */ Q_PROPERTY(QObject *fixedMargins READ fixedMargins CONSTANT) /** * The borders that will be rendered, it's a flag combination of: * NoBorder * TopBorder * BottomBorder * LeftBorder * RightBorder */ Q_PROPERTY(Plasma::FrameSvg::EnabledBorders enabledBorders READ enabledBorders WRITE setEnabledBorders NOTIFY enabledBordersChanged) /** * Holds whether the current svg is present in the current theme and NO fallback is involved */ Q_PROPERTY(bool fromCurrentTheme READ fromCurrentTheme NOTIFY fromCurrentThemeChanged) /** * Set a color group for the FrameSvgItem. * if the Svg uses stylesheets and has elements * that are either TextColor or BackgroundColor class, * make them use ButtonTextColor/ButtonBackgroundColor * or ViewTextColor/ViewBackgroundColor, ComplementaryTextColor etc. */ Q_PROPERTY(Plasma::Theme::ColorGroup colorGroup READ colorGroup WRITE setColorGroup NOTIFY colorGroupChanged) /** * Sets the image in a selected status. * Svgs can be colored with system color themes, if the status is selected, * the TextColor will become HighlightedText color and BackgroundColor * will become HighlightColor, making the svg graphics (for instance an icon) * will look correct together selected text * @see Plasma::Svg::status * @since 5.23 */ Q_PROPERTY(Plasma::Svg::Status status READ status WRITE setStatus NOTIFY statusChanged) + /** + * The mask that tightly contains the painted areas + * @since 5.58 + */ + Q_PROPERTY(QRegion mask READ mask NOTIFY maskChanged) + public: /** * @return true if the svg has the necessary elements with the given prefix * to draw a frame * @param prefix the given prefix we want to check if drawable */ Q_INVOKABLE bool hasElementPrefix(const QString &prefix) const; /// @cond INTERNAL_DOCS FrameSvgItem(QQuickItem *parent = nullptr); ~FrameSvgItem(); void setImagePath(const QString &path); QString imagePath() const; void setPrefix(const QVariant &prefix); QVariant prefix() const; QString usedPrefix() const; void setEnabledBorders(const Plasma::FrameSvg::EnabledBorders borders); Plasma::FrameSvg::EnabledBorders enabledBorders() const; FrameSvgItemMargins *margins(); FrameSvgItemMargins *fixedMargins(); void setColorGroup(Plasma::Theme::ColorGroup group); Plasma::Theme::ColorGroup colorGroup() const; bool fromCurrentTheme() const; void setStatus(Plasma::Svg::Status status); Plasma::Svg::Status status() const; void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + QRegion mask() const; + /** * Only to be used from inside this library, is not intended to be invokable */ Plasma::FrameSvg *frameSvg() const; QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override; void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData & data) override; protected: void classBegin() override; void componentComplete() override; /// @endcond Q_SIGNALS: void imagePathChanged(); void prefixChanged(); void enabledBordersChanged(); void fromCurrentThemeChanged(); void colorGroupChanged(); void repaintNeeded(); void statusChanged(); void usedPrefixChanged(); + void maskChanged(); private Q_SLOTS: void doUpdate(); void updateDevicePixelRatio(); private: void applyPrefixes(); Plasma::FrameSvg *m_frameSvg; FrameSvgItemMargins *m_margins; FrameSvgItemMargins *m_fixedMargins; // logged margins to check for changes QVector m_oldMargins; QVector m_oldFixedMargins; QStringList m_prefixes; bool m_textureChanged; bool m_sizeChanged; bool m_fastPath; }; } #endif diff --git a/src/plasmaquick/dialog.cpp b/src/plasmaquick/dialog.cpp index 4c7e97189..ecdf968ca 100644 --- a/src/plasmaquick/dialog.cpp +++ b/src/plasmaquick/dialog.cpp @@ -1,1394 +1,1394 @@ /*************************************************************************** * Copyright 2011 Marco Martin * * Copyright 2013 Sebastian Kügler * * Copyright 2014 Martin Gräßlin * * Copyright 2014 Vishesh Handa * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "dialog.h" #include "config-plasma.h" #include "../declarativeimports/core/framesvgitem.h" #include "dialogshadows_p.h" #include "view.h" #include "configview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_KWAYLAND #include #include #endif #if HAVE_XCB_SHAPE #include #include #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(); Dialog *q; Plasma::Types::Location location; Plasma::FrameSvgItem *frameSvgItem; QPointer mainItem; QPointer visualParent; QTimer hintsCommitTimer; #if HAVE_KWAYLAND QPointer 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 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; Q_FOREACH (QScreen *screen, QGuiApplication::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->winId(), false); KWindowEffects::enableBackgroundContrast(q->winId(), false); q->setMask(QRegion()); DialogShadows::self()->removeWindow(q); } else { if (type == Dialog::Tooltip) { frameSvgItem->setImagePath(QStringLiteral("widgets/tooltip")); } else { frameSvgItem->setImagePath(QStringLiteral("dialogs/background")); } - KWindowEffects::enableBlurBehind(q->winId(), theme.blurBehindEnabled(), frameSvgItem->frameSvg()->mask()); + KWindowEffects::enableBlurBehind(q->winId(), theme.blurBehindEnabled(), frameSvgItem->mask()); KWindowEffects::enableBackgroundContrast(q->winId(), theme.backgroundContrastEnabled(), theme.backgroundContrast(), theme.backgroundIntensity(), theme.backgroundSaturation(), - frameSvgItem->frameSvg()->mask()); + frameSvgItem->mask()); if (KWindowSystem::compositingActive()) { if (hasMask) { hasMask = false; q->setMask(QRegion()); } } else { hasMask = true; - q->setMask(frameSvgItem->frameSvg()->mask()); + 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) { 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->winId(), slideLocation, -1); } if (visible) { q->raise(); if (type != Dialog::Normal) { KWindowSystem::setType(q->winId(), (NET::WindowType)type); } else { q->setFlags(Qt::FramelessWindowHint | q->flags()); } if (type == Dialog::Dock || type == Dialog::Notification || type == Dialog::OnScreenDisplay) { KWindowSystem::setOnAllDesktops(q->winId(), true); } else { KWindowSystem::setOnAllDesktops(q->winId(), false); } } } 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 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 = DialogShadows::self()->waylandPlasmaShellInterface(); if (!interface) { return; } Surface *s = Surface::fromWindow(q); if (!s) { return; } shellSurface = interface->createSurface(s, q); #endif } Dialog::Dialog(QQuickItem *parent) : QQuickWindow(parent ? parent->window() : nullptr), d(new DialogPrivate(this)) { setClearBeforeRendering(true); setColor(QColor(Qt::transparent)); setFlags(Qt::FramelessWindowHint | Qt::Dialog); connect(this, &QWindow::xChanged, [=]() { d->slotWindowPositionChanged(); }); connect(this, &QWindow::yChanged, [=]() { d->slotWindowPositionChanged(); }); // Given dialogs are skip task bar and don't have a decoration // minimizing them using e.g. "minimize all" should just close them connect(this, &QWindow::windowStateChanged, this, [this](Qt::WindowState newState) { if (newState == Qt::WindowMinimized) { setVisible(false); } }); connect(this, SIGNAL(visibleChanged(bool)), this, SIGNAL(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); } } 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 foreach (QObject *child, mainItem->children()) { //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 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; 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(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 = KWindowSystem::windowInfo(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; emit locationChanged(); if (d->mainItem) { d->syncToMainItemSize(); } } QObject *Dialog::margins() const { return d->frameSvgItem->fixedMargins(); } void Dialog::setFramelessFlags(Qt::WindowFlags flags) { setFlags(Qt::FramelessWindowHint | flags); 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; if (d->type != Normal) { KWindowSystem::setType(winId(), (NET::WindowType)type); } else { setFlags(Qt::FramelessWindowHint | flags()); } //an OSD can't be a Dialog, as qt xcb would attempt to set a transient parent for it //see bug 370433 if (type == OnScreenDisplay) { setFlags((flags() & ~Qt::Dialog) | Qt::Window); } if (d->backgroundHints == Dialog::NoBackground) { d->frameSvgItem->setImagePath(QString()); } else { if (d->type == Tooltip) { d->frameSvgItem->setImagePath(QStringLiteral("widgets/tooltip")); } else { d->frameSvgItem->setImagePath(QStringLiteral("dialogs/background")); } } if (type == Dock || type == Notification || type == OnScreenDisplay) { KWindowSystem::setOnAllDesktops(winId(), true); } else { KWindowSystem::setOnAllDesktops(winId(), false); } 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); const bool viewClicked = qobject_cast(focusWindow) || qobject_cast(focusWindow) || qobject_cast(focusWindow); if (viewClicked || (!parentHasFocus && !childHasFocus)) { setVisible(false); 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); QQuickWindow::showEvent(event); } bool Dialog::event(QEvent *event) { if (event->type() == QEvent::Expose) { auto ee = static_cast(event); if (!KWindowSystem::isPlatformWayland() || ee->region().isNull()) { 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); d->setupWaylandIntegration(); d->updateVisibility(true); d->updateTheme(); } #endif } else if (event->type() == QEvent::PlatformSurface) { const QPlatformSurfaceEvent *pSEvent = static_cast(event); if (pSEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) { KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager); } } 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(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(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(event); if (!d->mainItemContainsPosition(we->pos())) { QWheelEvent we2(d->positionAdjustedForMainItem(we->pos()), d->positionAdjustedForMainItem(we->pos()) + position(), we->pixelDelta(), we->angleDelta(), we->delta(), we->orientation(), we->buttons(), we->modifiers(), we->phase()); if (isVisible()) { QCoreApplication::sendEvent(this, &we2); } return true; } break; } case QEvent::DragEnter: { QDragEnterEvent *de = static_cast(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(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(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; emit hideOnWindowDeactivateChanged(); } bool Dialog::isOutputOnly() const { return d->outputOnly; } void Dialog::setOutputOnly(bool outputOnly) { if (d->outputOnly == outputOnly) { return; } d->outputOnly = outputOnly; 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())); } // Bug 381242: Qt remembers minimize state and re-applies it when showing setWindowStates(windowStates() & ~Qt::WindowMinimized); QQuickWindow::setVisible(visible); //signal will be emitted and proxied from the QQuickWindow code } else { 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(); emit backgroundHintsChanged(); } } #include "moc_dialog.cpp"