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 }