#include #include #include #include #include #include #include #include #include #include "FlatButton.h" #include "Ripple.h" #include "RippleOverlay.h" #include "ThemeManager.h" // The ampersand is automatically set in QPushButton or QCheckbx // by KDEPlatformTheme plugin in Qt5. // [https://bugs.kde.org/show_bug.cgi?id=337491] // // A workaroud is to add // // [Development] // AutoCheckAccelerators=false // // to ~/.config/kdeglobals static QString removeKDEAccelerators(QString text) { return text.remove(QChar('&')); } void FlatButton::init() { ripple_overlay_ = new RippleOverlay(this); state_machine_ = new FlatButtonStateMachine(this); role_ = ui::Role::Default; ripple_style_ = ui::RippleStyle::PositionedRipple; icon_placement_ = ui::ButtonIconPlacement::LeftIcon; overlay_style_ = ui::OverlayStyle::GrayOverlay; bg_mode_ = Qt::TransparentMode; fixed_ripple_radius_ = 64; corner_radius_ = 3; base_opacity_ = 0.13; font_size_ = 10; // 10.5; use_fixed_ripple_radius_ = false; setStyle(&ThemeManager::instance()); setAttribute(Qt::WA_Hover); setMouseTracking(true); setCursor(QCursor(Qt::PointingHandCursor)); QPainterPath path; path.addRoundedRect(rect(), corner_radius_, corner_radius_); ripple_overlay_->setClipPath(path); ripple_overlay_->setClipping(true); state_machine_->setupProperties(); state_machine_->startAnimations(); } FlatButton::FlatButton(QWidget *parent, ui::ButtonPreset preset) : QPushButton(parent) { init(); applyPreset(preset); } FlatButton::FlatButton(const QString &text, QWidget *parent, ui::ButtonPreset preset) : QPushButton(text, parent) { init(); applyPreset(preset); } FlatButton::FlatButton(const QString &text, ui::Role role, QWidget *parent, ui::ButtonPreset preset) : QPushButton(text, parent) { init(); applyPreset(preset); setRole(role); } FlatButton::~FlatButton() {} void FlatButton::applyPreset(ui::ButtonPreset preset) { switch (preset) { case ui::ButtonPreset::FlatPreset: setOverlayStyle(ui::OverlayStyle::NoOverlay); break; case ui::ButtonPreset::CheckablePreset: setOverlayStyle(ui::OverlayStyle::NoOverlay); setCheckable(true); break; default: break; } } void FlatButton::setRole(ui::Role role) { role_ = role; state_machine_->setupProperties(); } ui::Role FlatButton::role() const { return role_; } void FlatButton::setForegroundColor(const QColor &color) { foreground_color_ = color; } QColor FlatButton::foregroundColor() const { if (!foreground_color_.isValid()) { if (bg_mode_ == Qt::OpaqueMode) { return ThemeManager::instance().themeColor("BrightWhite"); } switch (role_) { case ui::Role::Primary: return ThemeManager::instance().themeColor("Blue"); case ui::Role::Secondary: return ThemeManager::instance().themeColor("Gray"); case ui::Role::Default: default: return ThemeManager::instance().themeColor("Black"); } } return foreground_color_; } void FlatButton::setBackgroundColor(const QColor &color) { background_color_ = color; } QColor FlatButton::backgroundColor() const { if (!background_color_.isValid()) { switch (role_) { case ui::Role::Primary: return ThemeManager::instance().themeColor("Blue"); case ui::Role::Secondary: return ThemeManager::instance().themeColor("Gray"); case ui::Role::Default: default: return ThemeManager::instance().themeColor("Black"); } } return background_color_; } void FlatButton::setOverlayColor(const QColor &color) { overlay_color_ = color; setOverlayStyle(ui::OverlayStyle::TintedOverlay); } QColor FlatButton::overlayColor() const { if (!overlay_color_.isValid()) { return foregroundColor(); } return overlay_color_; } void FlatButton::setDisabledForegroundColor(const QColor &color) { disabled_color_ = color; } QColor FlatButton::disabledForegroundColor() const { if (!disabled_color_.isValid()) { return ThemeManager::instance().themeColor("FadedWhite"); } return disabled_color_; } void FlatButton::setDisabledBackgroundColor(const QColor &color) { disabled_background_color_ = color; } QColor FlatButton::disabledBackgroundColor() const { if (!disabled_background_color_.isValid()) { return ThemeManager::instance().themeColor("FadedWhite"); } return disabled_background_color_; } void FlatButton::setFontSize(qreal size) { font_size_ = size; QFont f(font()); f.setPointSizeF(size); setFont(f); update(); } qreal FlatButton::fontSize() const { return font_size_; } void FlatButton::setOverlayStyle(ui::OverlayStyle style) { overlay_style_ = style; update(); } ui::OverlayStyle FlatButton::overlayStyle() const { return overlay_style_; } void FlatButton::setRippleStyle(ui::RippleStyle style) { ripple_style_ = style; } ui::RippleStyle FlatButton::rippleStyle() const { return ripple_style_; } void FlatButton::setIconPlacement(ui::ButtonIconPlacement placement) { icon_placement_ = placement; update(); } ui::ButtonIconPlacement FlatButton::iconPlacement() const { return icon_placement_; } void FlatButton::setCornerRadius(qreal radius) { corner_radius_ = radius; updateClipPath(); update(); } qreal FlatButton::cornerRadius() const { return corner_radius_; } void FlatButton::setBackgroundMode(Qt::BGMode mode) { bg_mode_ = mode; state_machine_->setupProperties(); } Qt::BGMode FlatButton::backgroundMode() const { return bg_mode_; } void FlatButton::setBaseOpacity(qreal opacity) { base_opacity_ = opacity; state_machine_->setupProperties(); } qreal FlatButton::baseOpacity() const { return base_opacity_; } void FlatButton::setCheckable(bool value) { state_machine_->updateCheckedStatus(); state_machine_->setCheckedOverlayProgress(0); QPushButton::setCheckable(value); } void FlatButton::setHasFixedRippleRadius(bool value) { use_fixed_ripple_radius_ = value; } bool FlatButton::hasFixedRippleRadius() const { return use_fixed_ripple_radius_; } void FlatButton::setFixedRippleRadius(qreal radius) { fixed_ripple_radius_ = radius; setHasFixedRippleRadius(true); } QSize FlatButton::sizeHint() const { ensurePolished(); QSize label(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text()))); int w = 20 + label.width(); int h = label.height(); if (!icon().isNull()) { w += iconSize().width() + FlatButton::IconPadding; h = qMax(h, iconSize().height()); } return QSize(w, 20 + h); } void FlatButton::checkStateSet() { state_machine_->updateCheckedStatus(); QPushButton::checkStateSet(); } void FlatButton::mousePressEvent(QMouseEvent *event) { if (ui::RippleStyle::NoRipple != ripple_style_) { QPoint pos; qreal radiusEndValue; if (ui::RippleStyle::CenteredRipple == ripple_style_) { pos = rect().center(); } else { pos = event->pos(); } if (use_fixed_ripple_radius_) { radiusEndValue = fixed_ripple_radius_; } else { radiusEndValue = static_cast(width()) / 2; } Ripple *ripple = new Ripple(pos); ripple->setRadiusEndValue(radiusEndValue); ripple->setOpacityStartValue(0.35); ripple->setColor(foregroundColor()); ripple->radiusAnimation()->setDuration(250); ripple->opacityAnimation()->setDuration(250); ripple_overlay_->addRipple(ripple); } QPushButton::mousePressEvent(event); } void FlatButton::mouseReleaseEvent(QMouseEvent *event) { QPushButton::mouseReleaseEvent(event); state_machine_->updateCheckedStatus(); } void FlatButton::resizeEvent(QResizeEvent *event) { QPushButton::resizeEvent(event); updateClipPath(); } void FlatButton::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); const qreal cr = corner_radius_; if (cr > 0) { QPainterPath path; path.addRoundedRect(rect(), cr, cr); painter.setClipPath(path); painter.setClipping(true); } paintBackground(&painter); painter.setOpacity(1); painter.setClipping(false); paintForeground(&painter); } void FlatButton::paintBackground(QPainter *painter) { const qreal overlayOpacity = state_machine_->overlayOpacity(); const qreal checkedProgress = state_machine_->checkedOverlayProgress(); if (Qt::OpaqueMode == bg_mode_) { QBrush brush; brush.setStyle(Qt::SolidPattern); if (isEnabled()) { brush.setColor(backgroundColor()); } else { brush.setColor(disabledBackgroundColor()); } painter->setOpacity(1); painter->setBrush(brush); painter->setPen(Qt::NoPen); painter->drawRect(rect()); } QBrush brush; brush.setStyle(Qt::SolidPattern); painter->setPen(Qt::NoPen); if (!isEnabled()) { return; } if ((ui::OverlayStyle::NoOverlay != overlay_style_) && (overlayOpacity > 0)) { if (ui::OverlayStyle::TintedOverlay == overlay_style_) { brush.setColor(overlayColor()); } else { brush.setColor(Qt::gray); } painter->setOpacity(overlayOpacity); painter->setBrush(brush); painter->drawRect(rect()); } if (isCheckable() && checkedProgress > 0) { const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7; brush.setColor(foregroundColor()); painter->setOpacity(q * checkedProgress); painter->setBrush(brush); QRect r(rect()); r.setHeight(static_cast(r.height()) * checkedProgress); painter->drawRect(r); } } #define COLOR_INTERPOLATE(CH) (1 - progress) * source.CH() + progress *dest.CH() void FlatButton::paintForeground(QPainter *painter) { if (isEnabled()) { painter->setPen(foregroundColor()); const qreal progress = state_machine_->checkedOverlayProgress(); if (isCheckable() && progress > 0) { QColor source = foregroundColor(); QColor dest = Qt::TransparentMode == bg_mode_ ? Qt::white : backgroundColor(); if (qFuzzyCompare(1, progress)) { painter->setPen(dest); } else { painter->setPen(QColor(COLOR_INTERPOLATE(red), COLOR_INTERPOLATE(green), COLOR_INTERPOLATE(blue), COLOR_INTERPOLATE(alpha))); } } } else { painter->setPen(disabledForegroundColor()); } if (icon().isNull()) { painter->drawText(rect(), Qt::AlignCenter, removeKDEAccelerators(text())); return; } QSize textSize(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text()))); QSize base(size() - textSize); const int iw = iconSize().width() + IconPadding; QPoint pos((base.width() - iw) / 2, 0); QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize); QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize()); /* if (ui::LeftIcon == icon_placement_) { */ /* textGeometry.translate(iw, 0); */ /* } else { */ /* iconGeometry.translate(textSize.width() + IconPadding, 0); */ /* } */ painter->drawText(textGeometry, Qt::AlignCenter, removeKDEAccelerators(text())); QPixmap pixmap = icon().pixmap(iconSize()); QPainter icon(&pixmap); icon.setCompositionMode(QPainter::CompositionMode_SourceIn); icon.fillRect(pixmap.rect(), painter->pen().color()); painter->drawPixmap(iconGeometry, pixmap); } void FlatButton::updateClipPath() { const qreal radius = corner_radius_; QPainterPath path; path.addRoundedRect(rect(), radius, radius); ripple_overlay_->setClipPath(path); } FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent) : QStateMachine(parent) , button_(parent) , top_level_state_(new QState(QState::ParallelStates)) , config_state_(new QState(top_level_state_)) , checkable_state_(new QState(top_level_state_)) , checked_state_(new QState(checkable_state_)) , unchecked_state_(new QState(checkable_state_)) , neutral_state_(new QState(config_state_)) , neutral_focused_state_(new QState(config_state_)) , hovered_state_(new QState(config_state_)) , hovered_focused_state_(new QState(config_state_)) , pressed_state_(new QState(config_state_)) , overlay_opacity_(0) , checked_overlay_progress_(parent->isChecked() ? 1 : 0) , was_checked_(false) { Q_ASSERT(parent); parent->installEventFilter(this); config_state_->setInitialState(neutral_state_); addState(top_level_state_); setInitialState(top_level_state_); checkable_state_->setInitialState(parent->isChecked() ? checked_state_ : unchecked_state_); QSignalTransition *transition; QPropertyAnimation *animation; transition = new QSignalTransition(this, SIGNAL(buttonChecked())); transition->setTargetState(checked_state_); unchecked_state_->addTransition(transition); animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); animation->setDuration(200); transition->addAnimation(animation); transition = new QSignalTransition(this, SIGNAL(buttonUnchecked())); transition->setTargetState(unchecked_state_); checked_state_->addTransition(transition); animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); animation->setDuration(200); transition->addAnimation(animation); addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_); addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_); addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_); addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_); addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_); addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_); addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_); addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_); addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_); addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_); addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_); } FlatButtonStateMachine::~FlatButtonStateMachine() {} void FlatButtonStateMachine::setOverlayOpacity(qreal opacity) { overlay_opacity_ = opacity; button_->update(); } void FlatButtonStateMachine::setCheckedOverlayProgress(qreal opacity) { checked_overlay_progress_ = opacity; button_->update(); } void FlatButtonStateMachine::startAnimations() { start(); } void FlatButtonStateMachine::setupProperties() { QColor overlayColor; if (Qt::TransparentMode == button_->backgroundMode()) { overlayColor = button_->backgroundColor(); } else { overlayColor = button_->foregroundColor(); } const qreal baseOpacity = button_->baseOpacity(); neutral_state_->assignProperty(this, "overlayOpacity", 0); neutral_focused_state_->assignProperty(this, "overlayOpacity", 0); hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity); hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity); pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity); checked_state_->assignProperty(this, "checkedOverlayProgress", 1); unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0); button_->update(); } void FlatButtonStateMachine::updateCheckedStatus() { const bool checked = button_->isChecked(); if (was_checked_ != checked) { was_checked_ = checked; if (checked) { emit buttonChecked(); } else { emit buttonUnchecked(); } } } bool FlatButtonStateMachine::eventFilter(QObject *watched, QEvent *event) { if (QEvent::FocusIn == event->type()) { QFocusEvent *focusEvent = static_cast(event); if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) { emit buttonPressed(); return true; } } return QStateMachine::eventFilter(watched, event); } void FlatButtonStateMachine::addTransition(QObject *object, const char *signal, QState *fromState, QState *toState) { addTransition(new QSignalTransition(object, signal), fromState, toState); } void FlatButtonStateMachine::addTransition(QObject *object, QEvent::Type eventType, QState *fromState, QState *toState) { addTransition(new QEventTransition(object, eventType), fromState, toState); } void FlatButtonStateMachine::addTransition(QAbstractTransition *transition, QState *fromState, QState *toState) { transition->setTargetState(toState); QPropertyAnimation *animation; animation = new QPropertyAnimation(this, "overlayOpacity", this); animation->setDuration(150); transition->addAnimation(animation); fromState->addTransition(transition); }