File indexing completed on 2024-04-28 05:26:22

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa <hugo.pereira@free.fr>
0003  * SPDX-FileCopyrightText: 2018, 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "breezeshadowhelper.h"
0009 
0010 #include "breezeboxshadowrenderer.h"
0011 #include "breezehelper.h"
0012 #include "breezemetrics.h"
0013 #include "breezepropertynames.h"
0014 #include "breezestyleconfigdata.h"
0015 
0016 #include <KWindowSystem>
0017 
0018 #include <QApplication>
0019 #include <QDockWidget>
0020 #include <QEvent>
0021 #include <QMenu>
0022 #include <QPainter>
0023 #include <QPixmap>
0024 #include <QPlatformSurfaceEvent>
0025 #include <QTextStream>
0026 #include <QToolBar>
0027 
0028 namespace
0029 {
0030 using Breeze::CompositeShadowParams;
0031 using Breeze::ShadowParams;
0032 
0033 const CompositeShadowParams s_shadowParams[] = {
0034     // None
0035     CompositeShadowParams(),
0036     // Small
0037     CompositeShadowParams(QPoint(0, 3), ShadowParams(QPoint(0, 0), 12, 0.26), ShadowParams(QPoint(0, -2), 6, 0.16)),
0038     // Medium
0039     CompositeShadowParams(QPoint(0, 4), ShadowParams(QPoint(0, 0), 16, 0.24), ShadowParams(QPoint(0, -2), 8, 0.14)),
0040     // Large
0041     CompositeShadowParams(QPoint(0, 5), ShadowParams(QPoint(0, 0), 20, 0.22), ShadowParams(QPoint(0, -3), 10, 0.12)),
0042     // Very Large
0043     CompositeShadowParams(QPoint(0, 6), ShadowParams(QPoint(0, 0), 24, 0.2), ShadowParams(QPoint(0, -3), 12, 0.1))};
0044 }
0045 
0046 namespace Breeze
0047 {
0048 //_____________________________________________________
0049 CompositeShadowParams ShadowHelper::lookupShadowParams(int shadowSizeEnum)
0050 {
0051     switch (shadowSizeEnum) {
0052     case StyleConfigData::ShadowNone:
0053         return s_shadowParams[0];
0054     case StyleConfigData::ShadowSmall:
0055         return s_shadowParams[1];
0056     case StyleConfigData::ShadowMedium:
0057         return s_shadowParams[2];
0058     case StyleConfigData::ShadowLarge:
0059         return s_shadowParams[3];
0060     case StyleConfigData::ShadowVeryLarge:
0061         return s_shadowParams[4];
0062     default:
0063         // Fallback to the Large size.
0064         return s_shadowParams[3];
0065     }
0066 }
0067 
0068 //_____________________________________________________
0069 ShadowHelper::ShadowHelper(QObject *parent, Helper &helper)
0070     : QObject(parent)
0071     , _helper(helper)
0072 {
0073 }
0074 
0075 //_______________________________________________________
0076 ShadowHelper::~ShadowHelper()
0077 {
0078     qDeleteAll(_shadows);
0079 }
0080 
0081 //______________________________________________
0082 void ShadowHelper::reset()
0083 {
0084     _tiles.clear();
0085     _shadowTiles = TileSet();
0086 }
0087 
0088 //_______________________________________________________
0089 bool ShadowHelper::registerWidget(QWidget *widget, bool force)
0090 {
0091     // make sure widget is not already registered
0092     if (_widgets.contains(widget)) {
0093         return false;
0094     }
0095 
0096     // check if widget qualifies
0097     if (!(force || acceptWidget(widget))) {
0098         return false;
0099     }
0100 
0101     // try create shadow directly
0102     installShadows(widget);
0103     _widgets.insert(widget);
0104 
0105     // install event filter
0106     widget->removeEventFilter(this);
0107     widget->installEventFilter(this);
0108 
0109     // connect destroy signal
0110     connect(widget, &QObject::destroyed, this, &ShadowHelper::widgetDeleted);
0111 
0112     return true;
0113 }
0114 
0115 //_______________________________________________________
0116 void ShadowHelper::unregisterWidget(QWidget *widget)
0117 {
0118     if (_widgets.remove(widget)) {
0119         // uninstall the event filter
0120         widget->removeEventFilter(this);
0121 
0122         // disconnect all signals
0123         disconnect(widget, nullptr, this, nullptr);
0124 
0125         // uninstall the shadow
0126         uninstallShadows(widget);
0127     }
0128 }
0129 
0130 //_______________________________________________________
0131 void ShadowHelper::loadConfig()
0132 {
0133     // reset
0134     reset();
0135 
0136     // update property for registered widgets
0137     for (QWidget *widget : _widgets) {
0138         installShadows(widget);
0139     }
0140 }
0141 
0142 //_______________________________________________________
0143 bool ShadowHelper::eventFilter(QObject *object, QEvent *event)
0144 {
0145     if (Helper::isX11()) {
0146         // check event type
0147         if (event->type() != QEvent::WinIdChange) {
0148             return false;
0149         }
0150 
0151         // cast widget
0152         QWidget *widget(static_cast<QWidget *>(object));
0153 
0154         // install shadows and update winId
0155         installShadows(widget);
0156 
0157     } else {
0158         if (event->type() != QEvent::PlatformSurface) {
0159             return false;
0160         }
0161 
0162         QWidget *widget(static_cast<QWidget *>(object));
0163         QPlatformSurfaceEvent *surfaceEvent(static_cast<QPlatformSurfaceEvent *>(event));
0164 
0165         switch (surfaceEvent->surfaceEventType()) {
0166         case QPlatformSurfaceEvent::SurfaceCreated:
0167             installShadows(widget);
0168             break;
0169         case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed:
0170             // Don't care.
0171             break;
0172         }
0173     }
0174 
0175     return false;
0176 }
0177 
0178 //_______________________________________________________
0179 TileSet ShadowHelper::shadowTiles(QWidget *widget)
0180 {
0181     CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize());
0182 
0183     if (params.isNone()) {
0184         return TileSet();
0185     } else if (_shadowTiles.isValid()) {
0186         return _shadowTiles;
0187     }
0188 
0189     const qreal dpr = devicePixelRatio(widget);
0190     params *= dpr;
0191 
0192     auto withOpacity = [](const QColor &color, qreal opacity) -> QColor {
0193         QColor c(color);
0194         c.setAlphaF(opacity);
0195         return c;
0196     };
0197 
0198     const QColor color = StyleConfigData::shadowColor();
0199     const qreal strength = static_cast<qreal>(StyleConfigData::shadowStrength()) / 255.0;
0200 
0201     const QSize boxSize =
0202         BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius).expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius));
0203 
0204     const qreal frameRadius = _helper.frameRadius();
0205 
0206     BoxShadowRenderer shadowRenderer;
0207     shadowRenderer.setBorderRadius(frameRadius);
0208     shadowRenderer.setBoxSize(boxSize);
0209 
0210     shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius, withOpacity(color, params.shadow1.opacity * strength));
0211     shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius, withOpacity(color, params.shadow2.opacity * strength));
0212 
0213     QImage shadowTexture = shadowRenderer.render();
0214 
0215     const QRect outerRect(QPoint(0, 0), shadowTexture.size());
0216 
0217     QRect boxRect(QPoint(0, 0), boxSize);
0218     boxRect.moveCenter(outerRect.center());
0219 
0220     // Mask out inner rect.
0221     QPainter painter(&shadowTexture);
0222     painter.setRenderHint(QPainter::Antialiasing);
0223 
0224     const QMargins margins = QMargins(boxRect.left() - outerRect.left() - Metrics::Shadow_Overlap - params.offset.x(),
0225                                       boxRect.top() - outerRect.top() - Metrics::Shadow_Overlap - params.offset.y(),
0226                                       outerRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(),
0227                                       outerRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y());
0228 
0229     painter.setPen(Qt::NoPen);
0230     painter.setBrush(Qt::black);
0231     painter.setCompositionMode(QPainter::CompositionMode_DestinationOut);
0232     painter.drawRoundedRect(outerRect - margins, frameRadius, frameRadius);
0233 
0234     // We're done.
0235     painter.end();
0236 
0237     const QPoint innerRectTopLeft = outerRect.center();
0238     _shadowTiles = TileSet(QPixmap::fromImage(std::move(shadowTexture)), innerRectTopLeft.x(), innerRectTopLeft.y(), 1, 1);
0239 
0240     return _shadowTiles;
0241 }
0242 
0243 //_______________________________________________________
0244 void ShadowHelper::widgetDeleted(QObject *object)
0245 {
0246     QWidget *widget(static_cast<QWidget *>(object));
0247     _widgets.remove(widget);
0248 }
0249 
0250 //_______________________________________________________
0251 void ShadowHelper::windowDeleted(QObject *object)
0252 {
0253     QWindow *window(static_cast<QWindow *>(object));
0254     _shadows.remove(window);
0255 }
0256 
0257 //_______________________________________________________
0258 bool ShadowHelper::isMenu(QWidget *widget) const
0259 {
0260     return qobject_cast<QMenu *>(widget);
0261 }
0262 
0263 //_______________________________________________________
0264 bool ShadowHelper::isToolTip(QWidget *widget) const
0265 {
0266     return widget->inherits("QTipLabel") || (widget->windowFlags() & Qt::WindowType_Mask) == Qt::ToolTip;
0267 }
0268 
0269 //_______________________________________________________
0270 bool ShadowHelper::isDockWidget(QWidget *widget) const
0271 {
0272     return qobject_cast<QDockWidget *>(widget);
0273 }
0274 
0275 //_______________________________________________________
0276 bool ShadowHelper::isToolBar(QWidget *widget) const
0277 {
0278     return qobject_cast<QToolBar *>(widget);
0279 }
0280 
0281 //_______________________________________________________
0282 bool ShadowHelper::acceptWidget(QWidget *widget) const
0283 {
0284     // flags
0285     if (widget->property(PropertyNames::netWMSkipShadow).toBool()) {
0286         return false;
0287     }
0288     if (widget->property(PropertyNames::netWMForceShadow).toBool()) {
0289         return true;
0290     }
0291 
0292     // menus
0293     if (isMenu(widget)) {
0294         return true;
0295     }
0296 
0297     // combobox dropdown lists
0298     if (widget->inherits("QComboBoxPrivateContainer")) {
0299         return true;
0300     }
0301 
0302     // tooltips
0303     if (isToolTip(widget) && !widget->inherits("Plasma::ToolTip")) {
0304         return true;
0305     }
0306 
0307     // detached widgets
0308     if (isDockWidget(widget) || isToolBar(widget)) {
0309         return true;
0310     }
0311 
0312     // reject
0313     return false;
0314 }
0315 
0316 //______________________________________________
0317 const QVector<KWindowShadowTile::Ptr> &ShadowHelper::createShadowTiles()
0318 {
0319     // make sure size is valid
0320     if (_tiles.isEmpty()) {
0321         _tiles = {createTile(_shadowTiles.pixmap(1)),
0322                   createTile(_shadowTiles.pixmap(2)),
0323                   createTile(_shadowTiles.pixmap(5)),
0324                   createTile(_shadowTiles.pixmap(8)),
0325                   createTile(_shadowTiles.pixmap(7)),
0326                   createTile(_shadowTiles.pixmap(6)),
0327                   createTile(_shadowTiles.pixmap(3)),
0328                   createTile(_shadowTiles.pixmap(0))};
0329     }
0330 
0331     // return relevant list of shadow tiles
0332     return _tiles;
0333 }
0334 
0335 //______________________________________________
0336 KWindowShadowTile::Ptr ShadowHelper::createTile(const QPixmap &source)
0337 {
0338     KWindowShadowTile::Ptr tile = KWindowShadowTile::Ptr::create();
0339     tile->setImage(source.toImage());
0340     return tile;
0341 }
0342 
0343 //_______________________________________________________
0344 void ShadowHelper::installShadows(QWidget *widget)
0345 {
0346     if (!widget) {
0347         return;
0348     }
0349 
0350     // only toplevel widgets can cast drop-shadows
0351     if (!widget->isWindow()) {
0352         return;
0353     }
0354 
0355     // widget must have valid native window
0356     if (!widget->testAttribute(Qt::WA_WState_Created)) {
0357         return;
0358     }
0359 
0360     // create shadow tiles if needed
0361     shadowTiles(widget);
0362     if (!_shadowTiles.isValid()) {
0363         return;
0364     }
0365 
0366     // create platform shadow tiles if needed
0367     const QVector<KWindowShadowTile::Ptr> &tiles = createShadowTiles();
0368     if (tiles.count() != numTiles) {
0369         return;
0370     }
0371 
0372     // get the underlying window for the widget
0373     QWindow *window = widget->windowHandle();
0374 
0375     // find a shadow associated with the widget
0376     KWindowShadow *&shadow = _shadows[window];
0377 
0378     if (!shadow) {
0379         // if there is no shadow yet, create one
0380         shadow = new KWindowShadow(window);
0381 
0382         // connect destroy signal
0383         connect(window, &QWindow::destroyed, this, &ShadowHelper::windowDeleted);
0384     }
0385 
0386     if (shadow->isCreated()) {
0387         shadow->destroy();
0388     }
0389 
0390     shadow->setTopTile(tiles[0]);
0391     shadow->setTopRightTile(tiles[1]);
0392     shadow->setRightTile(tiles[2]);
0393     shadow->setBottomRightTile(tiles[3]);
0394     shadow->setBottomTile(tiles[4]);
0395     shadow->setBottomLeftTile(tiles[5]);
0396     shadow->setLeftTile(tiles[6]);
0397     shadow->setTopLeftTile(tiles[7]);
0398     shadow->setPadding(shadowMargins(widget));
0399     shadow->setWindow(window);
0400     shadow->create();
0401 }
0402 
0403 //_______________________________________________________
0404 QMargins ShadowHelper::shadowMargins(QWidget *widget) const
0405 {
0406     CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize());
0407     if (params.isNone()) {
0408         return QMargins();
0409     }
0410     qreal dpr = devicePixelRatio(widget);
0411     params *= dpr;
0412 
0413     const QSize boxSize =
0414         BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius).expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius));
0415 
0416     const QSize shadowSize = BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow1.radius, params.shadow1.offset)
0417                                  .expandedTo(BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow2.radius, params.shadow2.offset));
0418 
0419     const QRect shadowRect(QPoint(0, 0), shadowSize);
0420 
0421     QRect boxRect(QPoint(0, 0), boxSize);
0422     boxRect.moveCenter(shadowRect.center());
0423 
0424     QMargins margins(boxRect.left() - shadowRect.left() - Metrics::Shadow_Overlap - params.offset.x(),
0425                      boxRect.top() - shadowRect.top() - Metrics::Shadow_Overlap - params.offset.y(),
0426                      shadowRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(),
0427                      shadowRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y());
0428 
0429     if (widget->inherits("QBalloonTip")) {
0430         // Balloon tip needs special margins to deal with the arrow.
0431         int top = widget->contentsMargins().top();
0432         int bottom = widget->contentsMargins().bottom();
0433 
0434         // Need to decrement default size further due to extra hard coded round corner.
0435         margins -= 1;
0436 
0437         // Arrow can be either to the top or the bottom. Adjust margins accordingly.
0438         const int diff = qAbs(top - bottom);
0439         if (top > bottom) {
0440             margins.setTop(margins.top() - diff);
0441         } else {
0442             margins.setBottom(margins.bottom() - diff);
0443         }
0444     }
0445 
0446     return margins;
0447 }
0448 
0449 //_______________________________________________________
0450 void ShadowHelper::uninstallShadows(QWidget *widget)
0451 {
0452     delete _shadows.take(widget->windowHandle());
0453 }
0454 
0455 //_______________________________________________________
0456 qreal ShadowHelper::devicePixelRatio(QWidget *widget)
0457 {
0458     // On Wayland, the compositor will upscale the shadow tiles if necessary.
0459     return Helper::isWayland() ? 1 : widget->devicePixelRatioF();
0460 }
0461 
0462 }