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.
945 lines
35 KiB
945 lines
35 KiB
/* |
|
* SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org> |
|
* SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa <hugo.pereira@free.fr> |
|
* SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org> |
|
* SPDX-FileCopyrightText: 2021 Paul McAuley <kde@paulmcauley.com> |
|
* |
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL |
|
*/ |
|
|
|
#include "breezedecoration.h" |
|
|
|
#include "breezesettingsprovider.h" |
|
#include "config-breeze.h" |
|
#include "config/breezeconfigwidget.h" |
|
|
|
#include "breezebutton.h" |
|
#include "breezesizegrip.h" |
|
|
|
#include "breezeboxshadowrenderer.h" |
|
|
|
#include <KDecoration2/DecorationButtonGroup> |
|
#include <KDecoration2/DecorationShadow> |
|
|
|
#include <KColorUtils> |
|
#include <KConfigGroup> |
|
#include <KPluginFactory> |
|
#include <KSharedConfig> |
|
|
|
#include <QDBusConnection> |
|
#include <QDBusMessage> |
|
#include <QDBusPendingCallWatcher> |
|
#include <QDBusPendingReply> |
|
#include <QPainter> |
|
#include <QPainterPath> |
|
#include <QTextStream> |
|
#include <QTimer> |
|
|
|
#if BREEZE_HAVE_X11 |
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) |
|
#include <private/qtx11extras_p.h> |
|
#else |
|
#include <QX11Info> |
|
#endif |
|
#endif |
|
|
|
#include <cmath> |
|
|
|
K_PLUGIN_FACTORY_WITH_JSON(BreezeDecoFactory, "breeze.json", registerPlugin<Breeze::Decoration>(); registerPlugin<Breeze::Button>(); |
|
registerPlugin<Breeze::ConfigWidget>();) |
|
|
|
namespace |
|
{ |
|
struct ShadowParams { |
|
ShadowParams() |
|
: offset(QPoint(0, 0)) |
|
, radius(0) |
|
, opacity(0) |
|
{ |
|
} |
|
|
|
ShadowParams(const QPoint &offset, int radius, qreal opacity) |
|
: offset(offset) |
|
, radius(radius) |
|
, opacity(opacity) |
|
{ |
|
} |
|
|
|
QPoint offset; |
|
int radius; |
|
qreal opacity; |
|
}; |
|
|
|
struct CompositeShadowParams { |
|
CompositeShadowParams() = default; |
|
|
|
CompositeShadowParams(const QPoint &offset, const ShadowParams &shadow1, const ShadowParams &shadow2) |
|
: offset(offset) |
|
, shadow1(shadow1) |
|
, shadow2(shadow2) |
|
{ |
|
} |
|
|
|
bool isNone() const |
|
{ |
|
return qMax(shadow1.radius, shadow2.radius) == 0; |
|
} |
|
|
|
QPoint offset; |
|
ShadowParams shadow1; |
|
ShadowParams shadow2; |
|
}; |
|
|
|
const CompositeShadowParams s_shadowParams[] = { |
|
// None |
|
CompositeShadowParams(), |
|
// Small |
|
CompositeShadowParams(QPoint(0, 4), ShadowParams(QPoint(0, 0), 16, 1), ShadowParams(QPoint(0, -2), 8, 0.4)), |
|
// Medium |
|
CompositeShadowParams(QPoint(0, 8), ShadowParams(QPoint(0, 0), 32, 0.9), ShadowParams(QPoint(0, -4), 16, 0.3)), |
|
// Large |
|
CompositeShadowParams(QPoint(0, 12), ShadowParams(QPoint(0, 0), 48, 0.8), ShadowParams(QPoint(0, -6), 24, 0.2)), |
|
// Very large |
|
CompositeShadowParams(QPoint(0, 16), ShadowParams(QPoint(0, 0), 64, 0.7), ShadowParams(QPoint(0, -8), 32, 0.1)), |
|
}; |
|
|
|
inline CompositeShadowParams lookupShadowParams(int size) |
|
{ |
|
switch (size) { |
|
case Breeze::InternalSettings::ShadowNone: |
|
return s_shadowParams[0]; |
|
case Breeze::InternalSettings::ShadowSmall: |
|
return s_shadowParams[1]; |
|
case Breeze::InternalSettings::ShadowMedium: |
|
return s_shadowParams[2]; |
|
case Breeze::InternalSettings::ShadowLarge: |
|
return s_shadowParams[3]; |
|
case Breeze::InternalSettings::ShadowVeryLarge: |
|
return s_shadowParams[4]; |
|
default: |
|
// Fallback to the Large size. |
|
return s_shadowParams[3]; |
|
} |
|
} |
|
} |
|
|
|
namespace Breeze |
|
{ |
|
using KDecoration2::ColorGroup; |
|
using KDecoration2::ColorRole; |
|
|
|
//________________________________________________________________ |
|
static int g_sDecoCount = 0; |
|
static int g_shadowSizeEnum = InternalSettings::ShadowLarge; |
|
static int g_shadowStrength = 255; |
|
static QColor g_shadowColor = Qt::black; |
|
static QSharedPointer<KDecoration2::DecorationShadow> g_sShadow; |
|
static QSharedPointer<KDecoration2::DecorationShadow> g_sShadowInactive; |
|
static int g_lastBorderSize; |
|
static QColor g_lastOutlineColor; |
|
|
|
//________________________________________________________________ |
|
Decoration::Decoration(QObject *parent, const QVariantList &args) |
|
: KDecoration2::Decoration(parent, args) |
|
, m_animation(new QVariantAnimation(this)) |
|
, m_shadowAnimation(new QVariantAnimation(this)) |
|
{ |
|
g_sDecoCount++; |
|
} |
|
|
|
//________________________________________________________________ |
|
Decoration::~Decoration() |
|
{ |
|
g_sDecoCount--; |
|
if (g_sDecoCount == 0) { |
|
// last deco destroyed, clean up shadow |
|
g_sShadow.clear(); |
|
} |
|
|
|
deleteSizeGrip(); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::setOpacity(qreal value) |
|
{ |
|
if (m_opacity == value) { |
|
return; |
|
} |
|
m_opacity = value; |
|
update(); |
|
|
|
if (m_sizeGrip) |
|
m_sizeGrip->update(); |
|
} |
|
|
|
//________________________________________________________________ |
|
QColor Decoration::titleBarColor() const |
|
{ |
|
const auto c = client().toStrongRef(); |
|
return c->color(c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::TitleBar); |
|
} |
|
|
|
//________________________________________________________________ |
|
QColor Decoration::fontColor() const |
|
{ |
|
const auto c = client().toStrongRef(); |
|
if (m_animation->state() == QAbstractAnimation::Running) { |
|
return KColorUtils::mix(c->color(ColorGroup::Inactive, ColorRole::Foreground), c->color(ColorGroup::Active, ColorRole::Foreground), m_opacity); |
|
} else { |
|
return c->color(c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::Foreground); |
|
} |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::init() |
|
{ |
|
const auto c = client().toStrongRef(); |
|
|
|
// active state change animation |
|
// It is important start and end value are of the same type, hence 0.0 and not just 0 |
|
m_animation->setStartValue(0.0); |
|
m_animation->setEndValue(1.0); |
|
// Linear to have the same easing as Breeze animations |
|
m_animation->setEasingCurve(QEasingCurve::Linear); |
|
connect(m_animation, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) { |
|
setOpacity(value.toReal()); |
|
}); |
|
|
|
m_shadowAnimation->setStartValue(0.0); |
|
m_shadowAnimation->setEndValue(1.0); |
|
m_shadowAnimation->setEasingCurve(QEasingCurve::OutCubic); |
|
connect(m_shadowAnimation, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) { |
|
m_shadowOpacity = value.toReal(); |
|
updateShadow(); |
|
}); |
|
|
|
// use DBus connection to update on breeze configuration change |
|
auto dbus = QDBusConnection::sessionBus(); |
|
dbus.connect(QString(), |
|
QStringLiteral("/KGlobalSettings"), |
|
QStringLiteral("org.kde.KGlobalSettings"), |
|
QStringLiteral("notifyChange"), |
|
this, |
|
SLOT(reconfigure())); |
|
|
|
dbus.connect(QStringLiteral("org.kde.KWin"), |
|
QStringLiteral("/org/kde/KWin"), |
|
QStringLiteral("org.kde.KWin.TabletModeManager"), |
|
QStringLiteral("tabletModeChanged"), |
|
QStringLiteral("b"), |
|
this, |
|
SLOT(onTabletModeChanged(bool))); |
|
|
|
auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), |
|
QStringLiteral("/org/kde/KWin"), |
|
QStringLiteral("org.freedesktop.DBus.Properties"), |
|
QStringLiteral("Get")); |
|
message.setArguments({QStringLiteral("org.kde.KWin.TabletModeManager"), QStringLiteral("tabletMode")}); |
|
auto call = new QDBusPendingCallWatcher(dbus.asyncCall(message), this); |
|
connect(call, &QDBusPendingCallWatcher::finished, this, [this, call]() { |
|
QDBusPendingReply<QVariant> reply = *call; |
|
if (!reply.isError()) { |
|
onTabletModeChanged(reply.value().toBool()); |
|
} |
|
|
|
call->deleteLater(); |
|
}); |
|
|
|
reconfigure(); |
|
updateTitleBar(); |
|
auto s = settings(); |
|
connect(s.data(), &KDecoration2::DecorationSettings::borderSizeChanged, this, &Decoration::recalculateBorders); |
|
|
|
// a change in font might cause the borders to change |
|
connect(s.data(), &KDecoration2::DecorationSettings::fontChanged, this, &Decoration::recalculateBorders); |
|
connect(s.data(), &KDecoration2::DecorationSettings::spacingChanged, this, &Decoration::recalculateBorders); |
|
|
|
// buttons |
|
connect(s.data(), &KDecoration2::DecorationSettings::spacingChanged, this, &Decoration::updateButtonsGeometryDelayed); |
|
connect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsLeftChanged, this, &Decoration::updateButtonsGeometryDelayed); |
|
connect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsRightChanged, this, &Decoration::updateButtonsGeometryDelayed); |
|
|
|
// full reconfiguration |
|
connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::reconfigure); |
|
connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, SettingsProvider::self(), &SettingsProvider::reconfigure, Qt::UniqueConnection); |
|
connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::updateButtonsGeometryDelayed); |
|
|
|
connect(c.data(), &KDecoration2::DecoratedClient::adjacentScreenEdgesChanged, this, &Decoration::recalculateBorders); |
|
connect(c.data(), &KDecoration2::DecoratedClient::maximizedHorizontallyChanged, this, &Decoration::recalculateBorders); |
|
connect(c.data(), &KDecoration2::DecoratedClient::maximizedVerticallyChanged, this, &Decoration::recalculateBorders); |
|
connect(c.data(), &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::recalculateBorders); |
|
connect(c.data(), &KDecoration2::DecoratedClient::captionChanged, this, [this]() { |
|
// update the caption area |
|
update(titleBar()); |
|
}); |
|
|
|
connect(c.data(), &KDecoration2::DecoratedClient::activeChanged, this, &Decoration::updateAnimationState); |
|
connect(c.data(), &KDecoration2::DecoratedClient::widthChanged, this, &Decoration::updateTitleBar); |
|
connect(c.data(), &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateTitleBar); |
|
connect(c.data(), &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::setOpaque); |
|
|
|
connect(c.data(), &KDecoration2::DecoratedClient::widthChanged, this, &Decoration::updateButtonsGeometry); |
|
connect(c.data(), &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateButtonsGeometry); |
|
connect(c.data(), &KDecoration2::DecoratedClient::adjacentScreenEdgesChanged, this, &Decoration::updateButtonsGeometry); |
|
connect(c.data(), &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateButtonsGeometry); |
|
|
|
createButtons(); |
|
updateShadow(); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::updateTitleBar() |
|
{ |
|
// The titlebar rect has margins around it so the window can be resized by dragging a decoration edge. |
|
auto s = settings(); |
|
const auto c = client().toStrongRef(); |
|
const bool maximized = isMaximized(); |
|
const int width = maximized ? c->width() : c->width() - 2 * s->smallSpacing() * Metrics::TitleBar_SideMargin; |
|
const int height = maximized ? borderTop() : borderTop() - s->smallSpacing() * Metrics::TitleBar_TopMargin; |
|
const int x = maximized ? 0 : s->smallSpacing() * Metrics::TitleBar_SideMargin; |
|
const int y = maximized ? 0 : s->smallSpacing() * Metrics::TitleBar_TopMargin; |
|
setTitleBar(QRect(x, y, width, height)); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::updateAnimationState() |
|
{ |
|
if (m_shadowAnimation->duration() > 0) { |
|
const auto c = client().toStrongRef(); |
|
m_shadowAnimation->setDirection(c->isActive() ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); |
|
m_shadowAnimation->setEasingCurve(c->isActive() ? QEasingCurve::OutCubic : QEasingCurve::InCubic); |
|
if (m_shadowAnimation->state() != QAbstractAnimation::Running) { |
|
m_shadowAnimation->start(); |
|
} |
|
|
|
} else { |
|
updateShadow(); |
|
} |
|
|
|
if (m_animation->duration() > 0) { |
|
const auto c = client().toStrongRef(); |
|
m_animation->setDirection(c->isActive() ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); |
|
if (m_animation->state() != QAbstractAnimation::Running) { |
|
m_animation->start(); |
|
} |
|
|
|
} else { |
|
update(); |
|
} |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::updateSizeGripVisibility() |
|
{ |
|
const auto c = client().toStrongRef(); |
|
// I am stealing the sizeGrip to indicate that the window is |
|
// active, so it should be visible provided that the client is |
|
// active. |
|
|
|
if (m_sizeGrip) { |
|
m_sizeGrip->setVisible(c->isActive() && !c->isShaded()); |
|
} |
|
} |
|
|
|
//________________________________________________________________ |
|
int Decoration::borderSize(bool bottom) const |
|
{ |
|
const int baseSize = settings()->smallSpacing()*0.50; |
|
if (m_internalSettings && (m_internalSettings->mask() & BorderSize)) { |
|
switch (m_internalSettings->borderSize()) { |
|
case InternalSettings::BorderNone: |
|
return 0; |
|
case InternalSettings::BorderNoSides: |
|
return bottom ? baseSize : 0; |
|
default: |
|
case InternalSettings::BorderTiny: |
|
return baseSize; |
|
case InternalSettings::BorderNormal: |
|
return baseSize * 2; |
|
case InternalSettings::BorderLarge: |
|
return baseSize * 3; |
|
case InternalSettings::BorderVeryLarge: |
|
return baseSize * 4; |
|
case InternalSettings::BorderHuge: |
|
return baseSize * 5; |
|
case InternalSettings::BorderVeryHuge: |
|
return baseSize * 6; |
|
case InternalSettings::BorderOversized: |
|
return baseSize * 10; |
|
} |
|
|
|
} else { |
|
switch (settings()->borderSize()) { |
|
case KDecoration2::BorderSize::None: |
|
return 0; |
|
case KDecoration2::BorderSize::NoSides: |
|
return bottom ? baseSize : 0; |
|
default: |
|
case KDecoration2::BorderSize::Tiny: |
|
return baseSize; |
|
case KDecoration2::BorderSize::Normal: |
|
return baseSize * 2; |
|
case KDecoration2::BorderSize::Large: |
|
return baseSize * 3; |
|
case KDecoration2::BorderSize::VeryLarge: |
|
return baseSize * 4; |
|
case KDecoration2::BorderSize::Huge: |
|
return baseSize * 5; |
|
case KDecoration2::BorderSize::VeryHuge: |
|
return baseSize * 6; |
|
case KDecoration2::BorderSize::Oversized: |
|
return baseSize * 10; |
|
} |
|
} |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::reconfigure() |
|
{ |
|
m_internalSettings = SettingsProvider::self()->internalSettings(this); |
|
|
|
setScaledCornerRadius(); |
|
|
|
// animation |
|
|
|
KSharedConfig::Ptr config = KSharedConfig::openConfig(); |
|
const KConfigGroup cg(config, QStringLiteral("KDE")); |
|
|
|
m_animation->setDuration(0); |
|
// Syncing anis between client and decoration is troublesome, so we're not using |
|
// any animations right now. |
|
// m_animation->setDuration( cg.readEntry("AnimationDurationFactor", 1.0f) * 100.0f ); |
|
|
|
// But the shadow is fine to animate like this! |
|
m_shadowAnimation->setDuration(cg.readEntry("AnimationDurationFactor", 1.0f) * 100.0f); |
|
|
|
// borders |
|
recalculateBorders(); |
|
|
|
// shadow |
|
updateShadow(); |
|
|
|
// size grip |
|
if (m_internalSettings->drawSizeGrip()) |
|
createSizeGrip(); |
|
else |
|
deleteSizeGrip(); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::recalculateBorders() |
|
{ |
|
const auto c = client().toStrongRef(); |
|
auto s = settings(); |
|
|
|
// left, right and bottom borders |
|
const int left = isLeftEdge() ? 0 : borderSize(); |
|
const int right = isRightEdge() ? 0 : borderSize(); |
|
const int bottom = (c->isShaded() || isBottomEdge()) ? 0 : borderSize(true); |
|
|
|
int top = 0; |
|
if (hideTitleBar()) { |
|
top = bottom; |
|
} else { |
|
QFontMetrics fm(s->font()); |
|
top += qMax(fm.height(), buttonHeight()); |
|
|
|
// padding below |
|
// extra pixel is used for the active window outline |
|
const int baseSize = s->smallSpacing(); |
|
top += baseSize * Metrics::TitleBar_BottomMargin + 1; |
|
|
|
// padding above |
|
top += baseSize * Metrics::TitleBar_TopMargin; |
|
} |
|
|
|
setBorders(QMargins(left, top, right, bottom)); |
|
|
|
// extended sizes |
|
const int extSize = s->largeSpacing(); |
|
int extSides = 0; |
|
int extBottom = 0; |
|
if (hasNoBorders()) { |
|
if (!isMaximizedHorizontally()) { |
|
extSides = extSize; |
|
} |
|
if (!isMaximizedVertically()) { |
|
extBottom = extSize; |
|
} |
|
|
|
} else if (hasNoSideBorders() && !isMaximizedHorizontally()) { |
|
extSides = extSize; |
|
} |
|
|
|
setResizeOnlyBorders(QMargins(extSides, 0, extSides, extBottom)); |
|
|
|
// Update shadows and clear outline to make sure outline changes when borders are changed |
|
updateShadow(); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::createButtons() |
|
{ |
|
m_leftButtons = new KDecoration2::DecorationButtonGroup(KDecoration2::DecorationButtonGroup::Position::Left, this, &Button::create); |
|
m_rightButtons = new KDecoration2::DecorationButtonGroup(KDecoration2::DecorationButtonGroup::Position::Right, this, &Button::create); |
|
updateButtonsGeometry(); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::updateButtonsGeometryDelayed() |
|
{ |
|
QTimer::singleShot(0, this, &Decoration::updateButtonsGeometry); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::updateButtonsGeometry() |
|
{ |
|
const auto s = settings(); |
|
|
|
// adjust button position |
|
const int bHeight = captionHeight() + (isTopEdge() ? s->smallSpacing() * Metrics::TitleBar_TopMargin : 0); |
|
const int bWidth = buttonHeight(); |
|
const int verticalOffset = (isTopEdge() ? s->smallSpacing() * Metrics::TitleBar_TopMargin : 0) + (captionHeight() - buttonHeight()) / 2; |
|
foreach (const QPointer<KDecoration2::DecorationButton> &button, m_leftButtons->buttons() + m_rightButtons->buttons()) { |
|
button.data()->setGeometry(QRectF(QPoint(0, 0), QSizeF(bWidth, bHeight))); |
|
static_cast<Button *>(button.data())->setOffset(QPointF(0, verticalOffset)); |
|
static_cast<Button *>(button.data())->setIconSize(QSize(bWidth, bWidth)); |
|
} |
|
|
|
// left buttons |
|
if (!m_leftButtons->buttons().isEmpty()) { |
|
// spacing |
|
m_leftButtons->setSpacing(s->smallSpacing() * Metrics::TitleBar_ButtonSpacing); |
|
|
|
// padding |
|
const int vPadding = isTopEdge() ? 0 : s->smallSpacing() * Metrics::TitleBar_TopMargin; |
|
const int hPadding = s->smallSpacing() * Metrics::TitleBar_SideMargin; |
|
if (isLeftEdge()) { |
|
// add offsets on the side buttons, to preserve padding, but satisfy Fitts law |
|
auto button = static_cast<Button *>(m_leftButtons->buttons().front().data()); |
|
button->setGeometry(QRectF(QPoint(0, 0), QSizeF(bWidth + hPadding, bHeight))); |
|
button->setFlag(Button::FlagFirstInList); |
|
button->setHorizontalOffset(hPadding); |
|
|
|
m_leftButtons->setPos(QPointF(0, vPadding)); |
|
|
|
} else { |
|
m_leftButtons->setPos(QPointF(hPadding + borderLeft(), vPadding)); |
|
} |
|
} |
|
|
|
// right buttons |
|
if (!m_rightButtons->buttons().isEmpty()) { |
|
// spacing |
|
m_rightButtons->setSpacing(s->smallSpacing() * Metrics::TitleBar_ButtonSpacing); |
|
|
|
// padding |
|
const int vPadding = isTopEdge() ? 0 : s->smallSpacing() * Metrics::TitleBar_TopMargin; |
|
const int hPadding = s->smallSpacing() * Metrics::TitleBar_SideMargin; |
|
if (isRightEdge()) { |
|
auto button = static_cast<Button *>(m_rightButtons->buttons().back().data()); |
|
button->setGeometry(QRectF(QPoint(0, 0), QSizeF(bWidth + hPadding, bHeight))); |
|
button->setFlag(Button::FlagLastInList); |
|
|
|
m_rightButtons->setPos(QPointF(size().width() - m_rightButtons->geometry().width(), vPadding)); |
|
|
|
} else { |
|
m_rightButtons->setPos(QPointF(size().width() - m_rightButtons->geometry().width() - hPadding - borderRight(), vPadding)); |
|
} |
|
} |
|
|
|
update(); |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::paint(QPainter *painter, const QRect &repaintRegion) |
|
{ |
|
// TODO: optimize based on repaintRegion |
|
auto c = client().toStrongRef(); |
|
auto s = settings(); |
|
|
|
// paint background |
|
if (!c->isShaded()) { |
|
painter->fillRect(rect(), Qt::transparent); |
|
painter->save(); |
|
painter->setRenderHint(QPainter::Antialiasing); |
|
painter->setPen(Qt::NoPen); |
|
painter->setBrush(c->color(c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::Frame)); |
|
|
|
// clip away the top part |
|
if (!hideTitleBar()) { |
|
painter->setClipRect(0, borderTop(), size().width(), size().height() - borderTop(), Qt::IntersectClip); |
|
} |
|
|
|
//if (s->isAlphaChannelSupported()) |
|
// painter->drawRoundedRect(rect(), m_scaledCornerRadius, m_scaledCornerRadius); |
|
//else |
|
painter->drawRect(rect()); |
|
|
|
if (c->isActive()) { |
|
painter->setPen( c->color( ColorGroup::Active, ColorRole::TitleBar ) ); |
|
QPointF p=rect().bottomRight()+QPointF(0.5, 0.5); |
|
// Unfortunately SizeGrip::GripSize is private |
|
const int sizeGripSize = 10; //SizeGrip::GripSize; |
|
QPointF q=p-QPoint(0, sizeGripSize); |
|
QPointF r=p-QPoint(sizeGripSize, 0); |
|
painter->drawLine( p, q ); |
|
painter->drawLine( p, r ); |
|
} |
|
|
|
|
|
painter->restore(); |
|
} |
|
|
|
if (!hideTitleBar()) { |
|
paintTitleBar(painter, repaintRegion); |
|
} |
|
|
|
if (hasBorders() && !s->isAlphaChannelSupported()) { |
|
painter->save(); |
|
painter->setRenderHint(QPainter::Antialiasing, false); |
|
painter->setBrush(Qt::NoBrush); |
|
painter->setPen(c->color(ColorGroup::Inactive, ColorRole::Foreground)); |
|
|
|
painter->drawRect(rect().adjusted(0, 0, -1, -1)); |
|
painter->restore(); |
|
} |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::paintTitleBar(QPainter *painter, const QRect &repaintRegion) |
|
{ |
|
const auto c = client().toStrongRef(); |
|
const QRect frontRect(QPoint(0, 1), QSize(size().width(), borderTop())); |
|
const QRect backRect(QPoint(0, 1), QSize(size().width(), borderTop() - 1)); |
|
|
|
QBrush frontBrush; |
|
QBrush backBrush(this->titleBarColor()); |
|
|
|
if (!backRect.intersects(repaintRegion)) { |
|
return; |
|
} |
|
|
|
painter->save(); |
|
painter->setPen(Qt::NoPen); |
|
|
|
// render a linear gradient on title area |
|
if (c->isActive() && m_internalSettings->drawBackgroundGradient()) { |
|
QLinearGradient gradient(0, 0, 0, frontRect.height()); |
|
gradient.setColorAt(0.0, titleBarColor().lighter(120)); |
|
gradient.setColorAt(0.8, titleBarColor()); |
|
|
|
frontBrush = gradient; |
|
|
|
} else { |
|
frontBrush = titleBarColor(); |
|
|
|
painter->setBrush(titleBarColor()); |
|
} |
|
|
|
auto s = settings(); |
|
if (isMaximized() || !s->isAlphaChannelSupported()) { |
|
painter->setBrush(backBrush); |
|
painter->drawRect(backRect); |
|
|
|
painter->setBrush(frontBrush); |
|
painter->drawRect(frontRect); |
|
|
|
} else if (c->isShaded()) { |
|
painter->setBrush(backBrush); |
|
painter->drawRoundedRect(backRect, m_scaledCornerRadius, m_scaledCornerRadius); |
|
|
|
painter->setBrush(frontBrush); |
|
painter->drawRoundedRect(frontRect, m_scaledCornerRadius, m_scaledCornerRadius); |
|
|
|
} else { |
|
painter->setClipRect(backRect, Qt::IntersectClip); |
|
|
|
auto drawThe = [=](const QRect &r) { |
|
// the rect is made a little bit larger to be able to clip away the rounded corners at the bottom and sides |
|
painter->drawRoundedRect(r.adjusted(isLeftEdge() ? -m_scaledCornerRadius : 0, |
|
isTopEdge() ? -m_scaledCornerRadius : 0, |
|
isRightEdge() ? m_scaledCornerRadius : 0, |
|
m_scaledCornerRadius), |
|
m_scaledCornerRadius, |
|
m_scaledCornerRadius); |
|
}; |
|
|
|
painter->setBrush(backBrush); |
|
drawThe(backRect); |
|
|
|
painter->setBrush(frontBrush); |
|
drawThe(frontRect); |
|
} |
|
|
|
painter->restore(); |
|
|
|
// draw caption |
|
painter->setFont(s->font()); |
|
painter->setPen(fontColor()); |
|
const auto cR = captionRect(); |
|
const QString caption = painter->fontMetrics().elidedText(c->caption(), Qt::ElideMiddle, cR.first.width()); |
|
painter->drawText(cR.first, cR.second | Qt::TextSingleLine, caption); |
|
|
|
// draw all buttons |
|
m_leftButtons->paint(painter, repaintRegion); |
|
m_rightButtons->paint(painter, repaintRegion); |
|
} |
|
|
|
//________________________________________________________________ |
|
int Decoration::buttonHeight() const |
|
{ |
|
const int baseSize = m_tabletMode ? settings()->gridUnit() * 2 : settings()->gridUnit(); |
|
switch (m_internalSettings->buttonSize()) { |
|
case InternalSettings::ButtonTiny: |
|
return baseSize; |
|
case InternalSettings::ButtonSmall: |
|
return baseSize * 1.5; |
|
default: |
|
case InternalSettings::ButtonDefault: |
|
return baseSize * 2; |
|
case InternalSettings::ButtonLarge: |
|
return baseSize * 2.5; |
|
case InternalSettings::ButtonVeryLarge: |
|
return baseSize * 3.5; |
|
} |
|
} |
|
|
|
void Decoration::onTabletModeChanged(bool mode) |
|
{ |
|
m_tabletMode = mode; |
|
recalculateBorders(); |
|
updateButtonsGeometry(); |
|
} |
|
|
|
//________________________________________________________________ |
|
int Decoration::captionHeight() const |
|
{ |
|
return hideTitleBar() ? borderTop() : borderTop() - settings()->smallSpacing() * (Metrics::TitleBar_BottomMargin + Metrics::TitleBar_TopMargin) - 1; |
|
} |
|
|
|
//________________________________________________________________ |
|
QPair<QRect, Qt::Alignment> Decoration::captionRect() const |
|
{ |
|
if (hideTitleBar()) { |
|
return qMakePair(QRect(), Qt::AlignCenter); |
|
} else { |
|
auto c = client().toStrongRef(); |
|
const int leftOffset = m_leftButtons->buttons().isEmpty() |
|
? Metrics::TitleBar_SideMargin * settings()->smallSpacing() |
|
: m_leftButtons->geometry().x() + m_leftButtons->geometry().width() + Metrics::TitleBar_SideMargin * settings()->smallSpacing(); |
|
|
|
const int rightOffset = m_rightButtons->buttons().isEmpty() |
|
? Metrics::TitleBar_SideMargin * settings()->smallSpacing() |
|
: size().width() - m_rightButtons->geometry().x() + Metrics::TitleBar_SideMargin * settings()->smallSpacing(); |
|
|
|
const int yOffset = settings()->smallSpacing() * Metrics::TitleBar_TopMargin; |
|
const QRect maxRect(leftOffset, yOffset, size().width() - leftOffset - rightOffset, captionHeight()); |
|
|
|
switch (m_internalSettings->titleAlignment()) { |
|
case InternalSettings::AlignLeft: |
|
return qMakePair(maxRect, Qt::AlignVCenter | Qt::AlignLeft); |
|
|
|
case InternalSettings::AlignRight: |
|
return qMakePair(maxRect, Qt::AlignVCenter | Qt::AlignRight); |
|
|
|
case InternalSettings::AlignCenter: |
|
return qMakePair(maxRect, Qt::AlignCenter); |
|
|
|
default: |
|
case InternalSettings::AlignCenterFullWidth: { |
|
// full caption rect |
|
const QRect fullRect = QRect(0, yOffset, size().width(), captionHeight()); |
|
QRect boundingRect(settings()->fontMetrics().boundingRect(c->caption()).toRect()); |
|
|
|
// text bounding rect |
|
boundingRect.setTop(yOffset); |
|
boundingRect.setHeight(captionHeight()); |
|
boundingRect.moveLeft((size().width() - boundingRect.width()) / 2); |
|
|
|
if (boundingRect.left() < leftOffset) { |
|
return qMakePair(maxRect, Qt::AlignVCenter | Qt::AlignLeft); |
|
} else if (boundingRect.right() > size().width() - rightOffset) { |
|
return qMakePair(maxRect, Qt::AlignVCenter | Qt::AlignRight); |
|
} else { |
|
return qMakePair(fullRect, Qt::AlignCenter); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
//________________________________________________________________ |
|
void Decoration::updateShadow() |
|
{ |
|
auto s = settings(); |
|
auto c = client().toStrongRef(); |
|
auto outlineColor = c->color(c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::TitleBar); |
|
auto backgroundColor = c->color(c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::Frame); |
|
// Bind lightness between 0.1 and 1.0 so it can never be completely black. |
|
// Outlines only have transparency if alpha channel is supported |
|
outlineColor.setHslF(outlineColor.hslHueF(), |
|
outlineColor.hslSaturationF(), |
|
qBound(0.1, outlineColor.lightnessF(), 1.0), |
|
s->isAlphaChannelSupported() ? 0.9 : 1.0); |
|
outlineColor.lightnessF() >= 0.5 ? outlineColor = outlineColor.darker(170) : outlineColor = outlineColor.lighter(170); |
|
|
|
// Animated case, no cached shadow object |
|
if ((m_shadowAnimation->state() == QAbstractAnimation::Running) && (m_shadowOpacity != 0.0) && (m_shadowOpacity != 1.0)) { |
|
setShadow(createShadowObject(0.5 + m_shadowOpacity * 0.5, outlineColor)); |
|
return; |
|
} |
|
|
|
if (g_shadowSizeEnum != m_internalSettings->shadowSize() || g_shadowStrength != m_internalSettings->shadowStrength() |
|
|| g_shadowColor != m_internalSettings->shadowColor()) { |
|
g_sShadow.clear(); |
|
g_sShadowInactive.clear(); |
|
g_shadowSizeEnum = m_internalSettings->shadowSize(); |
|
g_shadowStrength = m_internalSettings->shadowStrength(); |
|
g_shadowColor = m_internalSettings->shadowColor(); |
|
} |
|
|
|
auto &shadow = (c->isActive()) ? g_sShadow : g_sShadowInactive; |
|
if (!shadow || g_lastBorderSize != borderSize(true) || g_lastOutlineColor != outlineColor) { |
|
// Update both active and inactive shadows so outline stays consistent between the two |
|
g_sShadow = createShadowObject(1.0, outlineColor); |
|
g_sShadowInactive = createShadowObject(0.5, outlineColor); |
|
g_lastBorderSize = borderSize(true); |
|
g_lastOutlineColor = outlineColor; |
|
} |
|
setShadow(shadow); |
|
} |
|
|
|
//________________________________________________________________ |
|
QSharedPointer<KDecoration2::DecorationShadow> Decoration::createShadowObject(const float strengthScale, const QColor &outlineColor) |
|
{ |
|
CompositeShadowParams params = lookupShadowParams(m_internalSettings->shadowSize()); |
|
if (params.isNone()) { |
|
// If shadows are disabled, set shadow opacity to 0. |
|
// This allows the outline effect to show up without the shadow effect. |
|
params = CompositeShadowParams(QPoint(0, 4), ShadowParams(QPoint(0, 0), 16, 0), ShadowParams(QPoint(0, -2), 8, 0)); |
|
} |
|
|
|
auto withOpacity = [](const QColor &color, qreal opacity) -> QColor { |
|
QColor c(color); |
|
c.setAlphaF(opacity); |
|
return c; |
|
}; |
|
|
|
const QSize boxSize = |
|
BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius).expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius)); |
|
|
|
BoxShadowRenderer shadowRenderer; |
|
shadowRenderer.setBorderRadius(m_scaledCornerRadius + 0.5); |
|
shadowRenderer.setBoxSize(boxSize); |
|
|
|
const qreal strength = m_internalSettings->shadowStrength() / 255.0 * strengthScale; |
|
shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius, withOpacity(m_internalSettings->shadowColor(), params.shadow1.opacity * strength)); |
|
shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius, withOpacity(m_internalSettings->shadowColor(), params.shadow2.opacity * strength)); |
|
|
|
QImage shadowTexture = shadowRenderer.render(); |
|
|
|
QPainter painter(&shadowTexture); |
|
painter.setRenderHint(QPainter::Antialiasing); |
|
|
|
const QRect outerRect = shadowTexture.rect(); |
|
|
|
QRect boxRect(QPoint(0, 0), boxSize); |
|
boxRect.moveCenter(outerRect.center()); |
|
|
|
// Mask out inner rect. |
|
const QMargins padding = 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()); |
|
const QRect innerRect = outerRect - padding; |
|
|
|
painter.setPen(Qt::NoPen); |
|
painter.setBrush(Qt::black); |
|
painter.setCompositionMode(QPainter::CompositionMode_DestinationOut); |
|
painter.drawRoundedRect(innerRect, m_scaledCornerRadius + 0.5, m_scaledCornerRadius + 0.5); |
|
|
|
// Draw window outline |
|
const qreal outlineWidth = 1.001; |
|
const qreal penOffset = outlineWidth / 2; |
|
|
|
// Titlebar already has an outline, so move the top of the outline on the same level to avoid 2px width on top outline. |
|
QRectF outlineRect = innerRect + QMarginsF(penOffset, -penOffset, penOffset, penOffset); |
|
qreal cornerSize = m_scaledCornerRadius * 2; |
|
QRectF cornerRect(outlineRect.x(), outlineRect.y(), cornerSize, cornerSize); |
|
QPainterPath outlinePath; |
|
|
|
outlinePath.arcMoveTo(cornerRect, 180); |
|
outlinePath.arcTo(cornerRect, 180, -90); |
|
cornerRect.moveTopRight(outlineRect.topRight()); |
|
outlinePath.arcTo(cornerRect, 90, -90); |
|
|
|
// Check if border size is "no borders" or "no side-borders" |
|
if (borderSize(true) == 0) { |
|
outlinePath.lineTo(outlineRect.bottomRight()); |
|
outlinePath.lineTo(outlineRect.bottomLeft()); |
|
} else { |
|
cornerRect.moveBottomRight(outlineRect.bottomRight()); |
|
outlinePath.arcTo(cornerRect, 0, -90); |
|
cornerRect.moveBottomLeft(outlineRect.bottomLeft()); |
|
outlinePath.arcTo(cornerRect, 270, -90); |
|
} |
|
outlinePath.closeSubpath(); |
|
|
|
painter.setPen(QPen(outlineColor, outlineWidth)); |
|
painter.setBrush(Qt::NoBrush); |
|
painter.setCompositionMode(QPainter::CompositionMode_Source); |
|
painter.setRenderHint(QPainter::Antialiasing); |
|
painter.drawPath(outlinePath); |
|
|
|
painter.end(); |
|
|
|
auto ret = QSharedPointer<KDecoration2::DecorationShadow>::create(); |
|
ret->setPadding(padding); |
|
ret->setInnerShadowRect(QRect(outerRect.center(), QSize(1, 1))); |
|
ret->setShadow(shadowTexture); |
|
return ret; |
|
} |
|
|
|
//_________________________________________________________________ |
|
void Decoration::createSizeGrip() |
|
{ |
|
// do nothing if size grip already exist |
|
if (m_sizeGrip) |
|
return; |
|
|
|
#if BREEZE_HAVE_X11 |
|
if (!QX11Info::isPlatformX11()) |
|
return; |
|
|
|
// access client |
|
auto c = client().toStrongRef(); |
|
if (!c) |
|
return; |
|
|
|
if (c->windowId() != 0) { |
|
m_sizeGrip = new SizeGrip(this); |
|
connect(c.data(), &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateSizeGripVisibility); |
|
connect(c.data(), &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateSizeGripVisibility); |
|
connect(c.data(), &KDecoration2::DecoratedClient::resizeableChanged, this, &Decoration::updateSizeGripVisibility); |
|
connect( c.data(), &KDecoration2::DecoratedClient::activeChanged, this, &Decoration::updateSizeGripVisibility ); |
|
} |
|
#endif |
|
} |
|
|
|
//_________________________________________________________________ |
|
void Decoration::deleteSizeGrip() |
|
{ |
|
if (m_sizeGrip) { |
|
m_sizeGrip->deleteLater(); |
|
m_sizeGrip = nullptr; |
|
} |
|
} |
|
|
|
void Decoration::setScaledCornerRadius() |
|
{ |
|
m_scaledCornerRadius = Metrics::Frame_FrameRadius * settings()->smallSpacing(); |
|
} |
|
} // namespace |
|
|
|
#include "breezedecoration.moc"
|
|
|