You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

469 lines
15 KiB

/*
* SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa <hugo.pereira@free.fr>
* SPDX-FileCopyrightText: 2018, 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "breezeshadowhelper.h"
#include "breezemetrics.h"
#include "breezeboxshadowrenderer.h"
#include "breezehelper.h"
#include "breezepropertynames.h"
#include "breezestyleconfigdata.h"
#include <KWindowSystem>
#include <QDockWidget>
#include <QEvent>
#include <QApplication>
#include <QMenu>
#include <QPainter>
#include <QPixmap>
#include <QPlatformSurfaceEvent>
#include <QToolBar>
#include <QTextStream>
namespace
{
using Breeze::CompositeShadowParams;
using Breeze::ShadowParams;
const CompositeShadowParams s_shadowParams[] = {
// None
CompositeShadowParams(),
// Small
CompositeShadowParams(
QPoint(0, 3),
ShadowParams(QPoint(0, 0), 12, 0.26),
ShadowParams(QPoint(0, -2), 6, 0.16)),
// Medium
CompositeShadowParams(
QPoint(0, 4),
ShadowParams(QPoint(0, 0), 16, 0.24),
ShadowParams(QPoint(0, -2), 8, 0.14)),
// Large
CompositeShadowParams(
QPoint(0, 5),
ShadowParams(QPoint(0, 0), 20, 0.22),
ShadowParams(QPoint(0, -3), 10, 0.12)),
// Very Large
CompositeShadowParams(
QPoint(0, 6),
ShadowParams(QPoint(0, 0), 24, 0.2),
ShadowParams(QPoint(0, -3), 12, 0.1))
};
}
namespace Breeze
{
//_____________________________________________________
CompositeShadowParams ShadowHelper::lookupShadowParams(int shadowSizeEnum)
{
switch (shadowSizeEnum) {
case StyleConfigData::ShadowNone:
return s_shadowParams[0];
case StyleConfigData::ShadowSmall:
return s_shadowParams[1];
case StyleConfigData::ShadowMedium:
return s_shadowParams[2];
case StyleConfigData::ShadowLarge:
return s_shadowParams[3];
case StyleConfigData::ShadowVeryLarge:
return s_shadowParams[4];
default:
// Fallback to the Large size.
return s_shadowParams[3];
}
}
//_____________________________________________________
ShadowHelper::ShadowHelper( QObject* parent, Helper& helper ):
QObject( parent ),
_helper( helper )
{
}
//_______________________________________________________
ShadowHelper::~ShadowHelper()
{
qDeleteAll( _shadows );
}
//______________________________________________
void ShadowHelper::reset()
{
_tiles.clear();
_shadowTiles = TileSet();
}
//_______________________________________________________
bool ShadowHelper::registerWidget( QWidget* widget, bool force )
{
// make sure widget is not already registered
if( _widgets.contains( widget ) ) return false;
// check if widget qualifies
if( !( force || acceptWidget( widget ) ) )
{ return false; }
// try create shadow directly
installShadows( widget );
_widgets.insert( widget );
// install event filter
widget->removeEventFilter( this );
widget->installEventFilter( this );
// connect destroy signal
connect( widget, &QObject::destroyed, this, &ShadowHelper::widgetDeleted );
return true;
}
//_______________________________________________________
void ShadowHelper::unregisterWidget( QWidget* widget )
{
if( _widgets.remove( widget ) )
{
// uninstall the event filter
widget->removeEventFilter( this );
// disconnect all signals
disconnect( widget, nullptr, this, nullptr );
// uninstall the shadow
uninstallShadows( widget );
}
}
//_______________________________________________________
void ShadowHelper::loadConfig()
{
// reset
reset();
// update property for registered widgets
for( QWidget* widget : _widgets)
{ installShadows( widget ); }
}
//_______________________________________________________
bool ShadowHelper::eventFilter( QObject* object, QEvent* event )
{
if( Helper::isX11() )
{
// check event type
if( event->type() != QEvent::WinIdChange ) return false;
// cast widget
QWidget* widget( static_cast<QWidget*>( object ) );
// install shadows and update winId
installShadows( widget );
} else {
if( event->type() != QEvent::PlatformSurface ) return false;
QWidget* widget( static_cast<QWidget*>( object ) );
QPlatformSurfaceEvent* surfaceEvent( static_cast<QPlatformSurfaceEvent*>( event ) );
switch( surfaceEvent->surfaceEventType() )
{
case QPlatformSurfaceEvent::SurfaceCreated:
installShadows( widget );
break;
case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed:
// Don't care.
break;
}
}
return false;
}
//_______________________________________________________
TileSet ShadowHelper::shadowTiles(QWidget *widget)
{
CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize());
if (params.isNone()) {
return TileSet();
} else if (_shadowTiles.isValid()) {
return _shadowTiles;
}
const qreal dpr = devicePixelRatio(widget);
params *= dpr;
auto withOpacity = [](const QColor &color, qreal opacity) -> QColor {
QColor c(color);
c.setAlphaF(opacity);
return c;
};
const QColor color = StyleConfigData::shadowColor();
const qreal strength = static_cast<qreal>(StyleConfigData::shadowStrength()) / 255.0;
const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius)
.expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius));
const qreal frameRadius = _helper.frameRadius();
BoxShadowRenderer shadowRenderer;
shadowRenderer.setBorderRadius(frameRadius);
shadowRenderer.setBoxSize(boxSize);
shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius,
withOpacity(color, params.shadow1.opacity * strength));
shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius,
withOpacity(color, params.shadow2.opacity * strength));
QImage shadowTexture = shadowRenderer.render();
const QRect outerRect(QPoint(0, 0), shadowTexture.size());
QRect boxRect(QPoint(0, 0), boxSize);
boxRect.moveCenter(outerRect.center());
// Mask out inner rect.
QPainter painter(&shadowTexture);
painter.setRenderHint(QPainter::Antialiasing);
const QMargins margins = QMargins(
boxRect.left() - outerRect.left() - Metrics::Shadow_Overlap - params.offset.x(),
boxRect.top() - outerRect.top() - Metrics::Shadow_Overlap - params.offset.y(),
outerRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(),
outerRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y());
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.setCompositionMode(QPainter::CompositionMode_DestinationOut);
painter.drawRoundedRect(
outerRect - margins,
frameRadius,
frameRadius);
// We're done.
painter.end();
const QPoint innerRectTopLeft = outerRect.center();
_shadowTiles = TileSet(
QPixmap::fromImage(shadowTexture),
innerRectTopLeft.x(),
innerRectTopLeft.y(),
1, 1);
return _shadowTiles;
}
//_______________________________________________________
void ShadowHelper::widgetDeleted( QObject* object )
{
QWidget* widget( static_cast<QWidget*>( object ) );
_widgets.remove( widget );
}
//_______________________________________________________
void ShadowHelper::windowDeleted( QObject* object )
{
QWindow* window( static_cast<QWindow*>( object ) );
_shadows.remove( window );
}
//_______________________________________________________
bool ShadowHelper::isMenu( QWidget* widget ) const
{ return qobject_cast<QMenu*>( widget ); }
//_______________________________________________________
bool ShadowHelper::isToolTip( QWidget* widget ) const
{ return widget->inherits( "QTipLabel" ) || (widget->windowFlags() & Qt::WindowType_Mask) == Qt::ToolTip; }
//_______________________________________________________
bool ShadowHelper::isDockWidget( QWidget* widget ) const
{ return qobject_cast<QDockWidget*>( widget ); }
//_______________________________________________________
bool ShadowHelper::isToolBar( QWidget* widget ) const
{ return qobject_cast<QToolBar*>( widget ); }
//_______________________________________________________
bool ShadowHelper::acceptWidget( QWidget* widget ) const
{
// flags
if( widget->property( PropertyNames::netWMSkipShadow ).toBool() ) return false;
if( widget->property( PropertyNames::netWMForceShadow ).toBool() ) return true;
// menus
if( isMenu( widget ) ) return true;
// combobox dropdown lists
if( widget->inherits( "QComboBoxPrivateContainer" ) ) return true;
// tooltips
if( isToolTip( widget ) && !widget->inherits( "Plasma::ToolTip" ) )
{ return true; }
// detached widgets
if( isDockWidget( widget ) || isToolBar( widget ) )
{ return true; }
// reject
return false;
}
//______________________________________________
const QVector<KWindowShadowTile::Ptr>& ShadowHelper::createShadowTiles()
{
// make sure size is valid
if( _tiles.isEmpty() )
{
_tiles = {
createTile( _shadowTiles.pixmap( 1 ) ),
createTile( _shadowTiles.pixmap( 2 ) ),
createTile( _shadowTiles.pixmap( 5 ) ),
createTile( _shadowTiles.pixmap( 8 ) ),
createTile( _shadowTiles.pixmap( 7 ) ),
createTile( _shadowTiles.pixmap( 6 ) ),
createTile( _shadowTiles.pixmap( 3 ) ),
createTile( _shadowTiles.pixmap( 0 ) )
};
}
// return relevant list of shadow tiles
return _tiles;
}
//______________________________________________
KWindowShadowTile::Ptr ShadowHelper::createTile( const QPixmap& source )
{
KWindowShadowTile::Ptr tile = KWindowShadowTile::Ptr::create();
tile->setImage( source.toImage() );
return tile;
}
//_______________________________________________________
void ShadowHelper::installShadows( QWidget* widget )
{
if( !widget ) return;
// only toplevel widgets can cast drop-shadows
if( !widget->isWindow() ) return;
// widget must have valid native window
if( !widget->testAttribute( Qt::WA_WState_Created ) ) return;
// create shadow tiles if needed
shadowTiles(widget);
if( !_shadowTiles.isValid() ) return;
// create platform shadow tiles if needed
const QVector<KWindowShadowTile::Ptr>& tiles = createShadowTiles();
if( tiles.count() != numTiles ) return;
// get the underlying window for the widget
QWindow* window = widget->windowHandle();
// find a shadow associated with the widget
KWindowShadow*& shadow = _shadows[ window ];
if( !shadow )
{
// if there is no shadow yet, create one
shadow = new KWindowShadow( window );
// connect destroy signal
connect( window, &QWindow::destroyed, this, &ShadowHelper::windowDeleted );
}
if( shadow->isCreated() )
{ shadow->destroy(); }
shadow->setTopTile( tiles[ 0 ] );
shadow->setTopRightTile( tiles[ 1 ] );
shadow->setRightTile( tiles[ 2 ] );
shadow->setBottomRightTile( tiles[ 3 ] );
shadow->setBottomTile( tiles[ 4 ] );
shadow->setBottomLeftTile( tiles[ 5 ] );
shadow->setLeftTile( tiles[ 6 ] );
shadow->setTopLeftTile( tiles[ 7 ] );
shadow->setPadding( shadowMargins( widget ) );
shadow->setWindow( window );
shadow->create();
}
//_______________________________________________________
QMargins ShadowHelper::shadowMargins( QWidget* widget ) const
{
CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize());
if (params.isNone()) {
return QMargins();
}
qreal dpr = devicePixelRatio(widget);
params *= dpr;
const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius)
.expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius));
const QSize shadowSize = BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow1.radius, params.shadow1.offset)
.expandedTo(BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow2.radius, params.shadow2.offset));
const QRect shadowRect(QPoint(0, 0), shadowSize);
QRect boxRect(QPoint(0, 0), boxSize);
boxRect.moveCenter(shadowRect.center());
QMargins margins(
boxRect.left() - shadowRect.left() - Metrics::Shadow_Overlap - params.offset.x(),
boxRect.top() - shadowRect.top() - Metrics::Shadow_Overlap - params.offset.y(),
shadowRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(),
shadowRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y());
if (widget->inherits("QBalloonTip")) {
// Balloon tip needs special margins to deal with the arrow.
int top = widget->contentsMargins().top();
int bottom = widget->contentsMargins().bottom();
// Need to decrement default size further due to extra hard coded round corner.
margins -= 1;
// Arrow can be either to the top or the bottom. Adjust margins accordingly.
const int diff = qAbs(top - bottom);
if (top > bottom) {
margins.setTop(margins.top() - diff);
} else {
margins.setBottom(margins.bottom() - diff);
}
}
return margins;
}
//_______________________________________________________
void ShadowHelper::uninstallShadows( QWidget* widget )
{
delete _shadows.take( widget->windowHandle() );
}
//_______________________________________________________
qreal ShadowHelper::devicePixelRatio(QWidget *widget) {
// On Wayland, the compositor will upscale the shadow tiles if necessary.
return Helper::isWayland() ? 1 : widget->devicePixelRatioF();
}
}