File indexing completed on 2024-05-05 05:35:25

0001 //////////////////////////////////////////////////////////////////////////////
0002 // oxygenshadowhelper.h
0003 // handle shadow pixmaps passed to window manager via X property
0004 // -------------------
0005 //
0006 // SPDX-FileCopyrightText: 2010 Hugo Pereira Da Costa <hugo.pereira@free.fr>
0007 // SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0008 //
0009 // SPDX-License-Identifier: MIT
0010 //////////////////////////////////////////////////////////////////////////////
0011 
0012 #include "oxygenshadowhelper.h"
0013 
0014 #include "oxygenpropertynames.h"
0015 #include "oxygenshadowcache.h"
0016 #include "oxygenstylehelper.h"
0017 
0018 #include <QApplication>
0019 #include <QDockWidget>
0020 #include <QEvent>
0021 #include <QMenu>
0022 #include <QPainter>
0023 #include <QTextStream>
0024 #include <QToolBar>
0025 
0026 namespace Oxygen
0027 {
0028 //_____________________________________________________
0029 ShadowHelper::ShadowHelper(QObject *parent, StyleHelper &helper)
0030     : QObject(parent)
0031     , _helper(helper)
0032     , _shadowCache(new ShadowCache(helper))
0033     , _size(0)
0034 {
0035 }
0036 
0037 //_______________________________________________________
0038 ShadowHelper::~ShadowHelper(void)
0039 {
0040     qDeleteAll(_shadows);
0041     delete _shadowCache;
0042 }
0043 
0044 //______________________________________________
0045 void ShadowHelper::reset(void)
0046 {
0047     _platformTiles.clear();
0048     _platformDockTiles.clear();
0049 
0050     _tiles = TileSet();
0051     _dockTiles = TileSet();
0052 
0053     // reset size
0054     _size = 0;
0055 }
0056 
0057 //_______________________________________________________
0058 bool ShadowHelper::registerWidget(QWidget *widget, bool force)
0059 {
0060     // make sure widget is not already registered
0061     if (_widgets.contains(widget))
0062         return false;
0063 
0064     // check if widget qualifies
0065     if (!(force || acceptWidget(widget))) {
0066         return false;
0067     }
0068 
0069     // try create shadow directly
0070     installShadows(widget);
0071     _widgets.insert(widget);
0072 
0073     // install event filter
0074     widget->removeEventFilter(this);
0075     widget->installEventFilter(this);
0076 
0077     // connect destroy signal
0078     connect(widget, SIGNAL(destroyed(QObject *)), SLOT(widgetDeleted(QObject *)));
0079 
0080     return true;
0081 }
0082 
0083 //_______________________________________________________
0084 void ShadowHelper::unregisterWidget(QWidget *widget)
0085 {
0086     if (_widgets.remove(widget)) {
0087         // uninstall the event filter
0088         widget->removeEventFilter(this);
0089 
0090         // disconnect all signals
0091         disconnect(widget, nullptr, this, nullptr);
0092 
0093         // uninstall the shadow
0094         uninstallShadows(widget);
0095     }
0096 }
0097 
0098 //_______________________________________________________
0099 void ShadowHelper::reparseCacheConfig(void)
0100 {
0101     // shadow cache
0102     _shadowCache->readConfig();
0103 }
0104 
0105 //_______________________________________________________
0106 void ShadowHelper::loadConfig(void)
0107 {
0108     // reset
0109     reset();
0110 
0111     // retrieve shadow pixmap
0112     _size = _shadowCache->shadowSize();
0113 
0114     QPixmap pixmap(_shadowCache->pixmap(ShadowCache::Key()));
0115     const QSize pixmapSize(pixmap.size() / _helper.devicePixelRatio(pixmap));
0116     if (!pixmap.isNull()) {
0117         QPainter painter(&pixmap);
0118 
0119         // add transparency
0120         painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
0121         painter.fillRect(pixmap.rect(), QColor(0, 0, 0, 150));
0122     }
0123 
0124     // recreate tileset
0125     _tiles = TileSet(pixmap, pixmapSize.width() / 2, pixmapSize.height() / 2, 1, 1);
0126 
0127     if (!pixmap.isNull()) {
0128         QPainter painter(&pixmap);
0129 
0130         // add round corners
0131         const QRect cornerRect((pixmap.width() - 10) / 2, (pixmap.height() - 10) / 2, 10, 10);
0132         _helper.roundCorner(QPalette().color(QPalette::Window)).render(cornerRect, &painter);
0133     }
0134 
0135     // recreate tileset
0136     _dockTiles = TileSet(pixmap, pixmapSize.width() / 2, pixmapSize.height() / 2, 1, 1);
0137 
0138     // update property for registered widgets
0139     for (QWidget *widget : _widgets) {
0140         installShadows(widget);
0141     }
0142 }
0143 
0144 //_______________________________________________________
0145 bool ShadowHelper::eventFilter(QObject *object, QEvent *event)
0146 {
0147     // check event type
0148     if (event->type() != QEvent::WinIdChange)
0149         return false;
0150 
0151     // cast widget
0152     QWidget *widget(static_cast<QWidget *>(object));
0153 
0154     // install shadows on the widget again
0155     installShadows(widget);
0156 
0157     return false;
0158 }
0159 
0160 //_______________________________________________________
0161 void ShadowHelper::widgetDeleted(QObject *object)
0162 {
0163     QWidget *widget(static_cast<QWidget *>(object));
0164     _widgets.remove(widget);
0165 }
0166 
0167 //_______________________________________________________
0168 void ShadowHelper::windowDeleted(QObject *object)
0169 {
0170     QWindow *window(static_cast<QWindow *>(object));
0171     _shadows.remove(window);
0172 }
0173 
0174 //_______________________________________________________
0175 bool ShadowHelper::isMenu(QWidget *widget) const
0176 {
0177     return qobject_cast<QMenu *>(widget);
0178 }
0179 
0180 //_______________________________________________________
0181 bool ShadowHelper::isToolTip(QWidget *widget) const
0182 {
0183     return widget->inherits("QTipLabel") || (widget->windowFlags() & Qt::WindowType_Mask) == Qt::ToolTip;
0184 }
0185 
0186 //_______________________________________________________
0187 bool ShadowHelper::isDockWidget(QWidget *widget) const
0188 {
0189     return qobject_cast<QDockWidget *>(widget);
0190 }
0191 
0192 //_______________________________________________________
0193 bool ShadowHelper::isToolBar(QWidget *widget) const
0194 {
0195     return qobject_cast<QToolBar *>(widget) || widget->inherits("Q3ToolBar");
0196 }
0197 
0198 //_______________________________________________________
0199 bool ShadowHelper::acceptWidget(QWidget *widget) const
0200 {
0201     // flags
0202     if (widget->property(PropertyNames::netWMSkipShadow).toBool())
0203         return false;
0204     if (widget->property(PropertyNames::netWMForceShadow).toBool())
0205         return true;
0206 
0207     // menus
0208     if (isMenu(widget))
0209         return true;
0210 
0211     // combobox dropdown lists
0212     if (widget->inherits("QComboBoxPrivateContainer"))
0213         return true;
0214 
0215     // tooltips
0216     if (isToolTip(widget) && !widget->inherits("Plasma::ToolTip")) {
0217         return true;
0218     }
0219 
0220     // detached widgets
0221     if (isDockWidget(widget) || isToolBar(widget)) {
0222         return true;
0223     }
0224 
0225     // reject
0226     return false;
0227 }
0228 
0229 //______________________________________________
0230 const QVector<KWindowShadowTile::Ptr> &ShadowHelper::createPlatformTiles(bool isDockWidget)
0231 {
0232     // make sure size is valid
0233     if (_size <= 0)
0234         return _platformTiles;
0235 
0236     // make sure shadow tiles are not already initialized
0237     if (isDockWidget) {
0238         // make sure shadow tiles are not already initialized
0239         if (_platformDockTiles.isEmpty() && _dockTiles.isValid()) {
0240             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(1)));
0241             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(2)));
0242             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(5)));
0243             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(8)));
0244             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(7)));
0245             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(6)));
0246             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(3)));
0247             _platformDockTiles.append(createPlatformTile(_dockTiles.pixmap(0)));
0248         }
0249 
0250     } else if (_platformTiles.isEmpty() && _tiles.isValid()) {
0251         _platformTiles.append(createPlatformTile(_tiles.pixmap(1)));
0252         _platformTiles.append(createPlatformTile(_tiles.pixmap(2)));
0253         _platformTiles.append(createPlatformTile(_tiles.pixmap(5)));
0254         _platformTiles.append(createPlatformTile(_tiles.pixmap(8)));
0255         _platformTiles.append(createPlatformTile(_tiles.pixmap(7)));
0256         _platformTiles.append(createPlatformTile(_tiles.pixmap(6)));
0257         _platformTiles.append(createPlatformTile(_tiles.pixmap(3)));
0258         _platformTiles.append(createPlatformTile(_tiles.pixmap(0)));
0259     }
0260 
0261     // return relevant list of shadow tiles
0262     return isDockWidget ? _platformDockTiles : _platformTiles;
0263 }
0264 
0265 //______________________________________________
0266 KWindowShadowTile::Ptr ShadowHelper::createPlatformTile(const QPixmap &pixmap)
0267 {
0268     KWindowShadowTile::Ptr tile = KWindowShadowTile::Ptr::create();
0269     tile->setImage(pixmap.toImage());
0270     return tile;
0271 }
0272 
0273 //_______________________________________________________
0274 void ShadowHelper::installShadows(QWidget *widget)
0275 {
0276     if (!widget)
0277         return;
0278 
0279     // only toplevel widgets can cast drop-shadows
0280     if (!widget->isWindow())
0281         return;
0282 
0283     // widget must have valid native window
0284     if (!widget->testAttribute(Qt::WA_WState_Created))
0285         return;
0286 
0287     // create shadow tiles if needed
0288     const bool isDockWidget(this->isDockWidget(widget) || this->isToolBar(widget));
0289     const QVector<KWindowShadowTile::Ptr> &tiles = createPlatformTiles(isDockWidget);
0290     if (tiles.count() != numTiles)
0291         return;
0292 
0293     // get the underlying window for the widget
0294     QWindow *window = widget->windowHandle();
0295 
0296     // find a shadow associated with the widget
0297     KWindowShadow *&shadow = _shadows[window];
0298 
0299     if (!shadow) {
0300         // if there is no shadow yet, create one
0301         shadow = new KWindowShadow(window);
0302 
0303         // connect destroy signal
0304         connect(window, &QWindow::destroyed, this, &ShadowHelper::windowDeleted);
0305     }
0306 
0307     if (shadow->isCreated()) {
0308         shadow->destroy();
0309     }
0310 
0311     shadow->setTopTile(tiles[0]);
0312     shadow->setTopRightTile(tiles[1]);
0313     shadow->setRightTile(tiles[2]);
0314     shadow->setBottomRightTile(tiles[3]);
0315     shadow->setBottomTile(tiles[4]);
0316     shadow->setBottomLeftTile(tiles[5]);
0317     shadow->setLeftTile(tiles[6]);
0318     shadow->setTopLeftTile(tiles[7]);
0319     shadow->setWindow(window);
0320     shadow->setPadding(shadowMargins(widget));
0321     shadow->create();
0322 }
0323 
0324 //_______________________________________________________
0325 QMargins ShadowHelper::shadowMargins(QWidget *widget) const
0326 {
0327     // const qreal devicePixelRatio( _helper.devicePixelRatio( isDockWidget ?
0328     // _dockTiles.pixmap( 0 ):_tiles.pixmap( 0 ) ) );
0329     const qreal devicePixelRatio(qApp->devicePixelRatio());
0330 
0331     // add padding
0332     /*
0333     in most cases all 4 paddings are identical, since offsets are handled when generating the pixmaps.
0334     There is one extra pixel needed with respect to actual shadow size, to deal with how
0335     menu backgrounds are rendered.
0336     Some special care is needed for QBalloonTip, since the later have an arrow
0337     */
0338 
0339     int topSize = 0;
0340     int rightSize = 0;
0341     int bottomSize = 0;
0342     int leftSize = 0;
0343 
0344     if (isToolTip(widget) && widget->inherits("QBalloonTip")) {
0345         // balloon tip needs special margins to deal with the arrow
0346         const QMargins margins = widget->contentsMargins();
0347         const int top = margins.top();
0348         const int bottom = margins.bottom();
0349         // also need to decrement default size further due to extra hard coded round corner
0350         const int size = (_size - 2) * devicePixelRatio;
0351 
0352         // it seems arrow can be either to the top or the bottom. Adjust margins accordingly
0353         if (top > bottom) {
0354             topSize = size - (top - bottom);
0355             rightSize = size;
0356             bottomSize = size;
0357             leftSize = size;
0358         } else {
0359             topSize = size;
0360             rightSize = size;
0361             bottomSize = size - (bottom - top);
0362             leftSize = size;
0363         }
0364 
0365     } else {
0366         const int size = _size * devicePixelRatio;
0367         topSize = size;
0368         rightSize = size;
0369         bottomSize = size;
0370         leftSize = size;
0371     }
0372 
0373     return QMargins(leftSize, topSize, rightSize, bottomSize);
0374 }
0375 
0376 //_______________________________________________________
0377 void ShadowHelper::uninstallShadows(QWidget *widget)
0378 {
0379     delete _shadows.take(widget->windowHandle());
0380 }
0381 }