/* * SPDX-FileCopyrightText: 2014 Martin Gräßlin * SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa * SPDX-FileCopyrightText: 2018 Vlad Zahorodnii * SPDX-FileCopyrightText: 2021 Paul McAuley * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "breezedecoration.h" #include "breezesettingsprovider.h" #include "breezebutton.h" #include "breezeboxshadowrenderer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(BreezeDecoFactory, "breeze.json", registerPlugin(); registerPlugin();) 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]; } } inline qreal lookupOutlineIntensity(int intensity) { switch (intensity) { case Breeze::InternalSettings::OutlineOff: return 0; case Breeze::InternalSettings::OutlineLow: return Breeze::Metrics::Bias_Default / 2; case Breeze::InternalSettings::OutlineMedium: return Breeze::Metrics::Bias_Default; case Breeze::InternalSettings::OutlineHigh: return Breeze::Metrics::Bias_Default * 2; case Breeze::InternalSettings::OutlineMaximum: return Breeze::Metrics::Bias_Default * 3; default: // Fallback to the Medium intensity. return Breeze::Metrics::Bias_Default; } } } 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 std::shared_ptr g_sShadow; static std::shared_ptr g_sShadowInactive; static int g_lastBorderSize; //________________________________________________________________ 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.reset(); } } //________________________________________________________________ void Decoration::setOpacity(qreal value) { if (m_opacity == value) { return; } m_opacity = value; update(); } //________________________________________________________________ QColor Decoration::titleBarColor() const { const auto c = client(); if (hideTitleBar()) { return c->color(ColorGroup::Inactive, ColorRole::TitleBar); } else if (m_animation->state() == QAbstractAnimation::Running) { return KColorUtils::mix(c->color(ColorGroup::Inactive, ColorRole::TitleBar), c->color(ColorGroup::Active, ColorRole::TitleBar), m_opacity); } else { return c->color(c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::TitleBar); } } //________________________________________________________________ QColor Decoration::fontColor() const { const auto c = client(); 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); } } //________________________________________________________________ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) bool Decoration::init() #else void Decoration::init() #endif { const auto c = client(); // 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 reply = *call; if (!reply.isError()) { onTabletModeChanged(reply.value().toBool()); } call->deleteLater(); }); reconfigure(); updateTitleBar(); auto s = settings(); connect(s.get(), &KDecoration2::DecorationSettings::borderSizeChanged, this, &Decoration::recalculateBorders); // a change in font might cause the borders to change connect(s.get(), &KDecoration2::DecorationSettings::fontChanged, this, &Decoration::recalculateBorders); connect(s.get(), &KDecoration2::DecorationSettings::spacingChanged, this, &Decoration::recalculateBorders); // buttons connect(s.get(), &KDecoration2::DecorationSettings::spacingChanged, this, &Decoration::updateButtonsGeometryDelayed); connect(s.get(), &KDecoration2::DecorationSettings::decorationButtonsLeftChanged, this, &Decoration::updateButtonsGeometryDelayed); connect(s.get(), &KDecoration2::DecorationSettings::decorationButtonsRightChanged, this, &Decoration::updateButtonsGeometryDelayed); // full reconfiguration connect(s.get(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::reconfigure); connect(s.get(), &KDecoration2::DecorationSettings::reconfigured, SettingsProvider::self(), &SettingsProvider::reconfigure, Qt::UniqueConnection); connect(s.get(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::updateButtonsGeometryDelayed); connect(c, &KDecoration2::DecoratedClient::adjacentScreenEdgesChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::maximizedHorizontallyChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::maximizedVerticallyChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::captionChanged, this, [this]() { // update the caption area update(titleBar()); }); connect(c, &KDecoration2::DecoratedClient::activeChanged, this, &Decoration::updateAnimationState); connect(c, &KDecoration2::DecoratedClient::adjacentScreenEdgesChanged, this, &Decoration::updateTitleBar); connect(c, &KDecoration2::DecoratedClient::widthChanged, this, &Decoration::updateTitleBar); connect(c, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateTitleBar); connect(c, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::setOpaque); connect(c, &KDecoration2::DecoratedClient::widthChanged, this, &Decoration::updateButtonsGeometry); connect(c, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateButtonsGeometry); connect(c, &KDecoration2::DecoratedClient::adjacentScreenEdgesChanged, this, &Decoration::updateButtonsGeometry); connect(c, &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateButtonsGeometry); createButtons(); updateShadow(); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) return true; #endif } //________________________________________________________________ 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(); const bool maximized = isMaximized(); const int width = maximized ? c->width() : c->width() - 2 * s->smallSpacing() * Metrics::TitleBar_SideMargin; const int height = (maximized || isTopEdge()) ? borderTop() : borderTop() - s->smallSpacing() * Metrics::TitleBar_TopMargin; const int x = maximized ? 0 : s->smallSpacing() * Metrics::TitleBar_SideMargin; const int y = (maximized || isTopEdge()) ? 0 : s->smallSpacing() * Metrics::TitleBar_TopMargin; setTitleBar(QRect(x, y, width, height)); } //________________________________________________________________ void Decoration::updateAnimationState() { if (m_shadowAnimation->duration() > 0) { const auto c = client(); 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(); m_animation->setDirection(c->isActive() ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); if (m_animation->state() != QAbstractAnimation::Running) { m_animation->start(); } } else { update(); } } //________________________________________________________________ int Decoration::borderSize(bool bottom) const { const int baseSize = settings()->smallSpacing(); if (m_internalSettings && (m_internalSettings->mask() & BorderSize)) { switch (m_internalSettings->borderSize()) { case InternalSettings::BorderNone: return outlinesEnabled() ? 1 : 0; case InternalSettings::BorderNoSides: return bottom ? qMax(4, baseSize) : outlinesEnabled() ? 1 : 0; default: case InternalSettings::BorderTiny: return bottom ? qMax(4, baseSize) : 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 outlinesEnabled() ? 1 : 0; case KDecoration2::BorderSize::NoSides: return bottom ? qMax(4, baseSize) : outlinesEnabled() ? 1 : 0; default: case KDecoration2::BorderSize::Tiny: return bottom ? qMax(4, baseSize) : 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(); } //________________________________________________________________ void Decoration::recalculateBorders() { const auto c = client(); 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(), buttonSize()); // padding below const int baseSize = s->smallSpacing(); top += baseSize * Metrics::TitleBar_BottomMargin; // 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)); } //________________________________________________________________ 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 auto buttonList = m_leftButtons->buttons() + m_rightButtons->buttons(); for (const QPointer &button : buttonList) { auto btn = static_cast