summary refs log tree commit diff
path: root/src/ui
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2017-04-06 02:06:42 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2017-04-06 02:06:42 +0300
commit4f45575c791a73e6711583c680f60954de094666 (patch)
tree8b4b72e2e9a40ed57f9e02cba496a94123c1ea18 /src/ui
downloadnheko-4f45575c791a73e6711583c680f60954de094666.tar.xz
Initial commit
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/Avatar.cc143
-rw-r--r--src/ui/Badge.cc186
-rw-r--r--src/ui/FlatButton.cc761
-rw-r--r--src/ui/OverlayWidget.cc59
-rw-r--r--src/ui/RaisedButton.cc92
-rw-r--r--src/ui/Ripple.cc106
-rw-r--r--src/ui/RippleOverlay.cc61
-rw-r--r--src/ui/TextField.cc346
-rw-r--r--src/ui/Theme.cc73
-rw-r--r--src/ui/ThemeManager.cc20
10 files changed, 1847 insertions, 0 deletions
diff --git a/src/ui/Avatar.cc b/src/ui/Avatar.cc
new file mode 100644

index 00000000..4245c168 --- /dev/null +++ b/src/ui/Avatar.cc
@@ -0,0 +1,143 @@ +#include <QIcon> +#include <QPainter> +#include <QWidget> + +#include "Avatar.h" + +Avatar::Avatar(QWidget *parent) + : QWidget(parent) +{ + size_ = ui::AvatarSize; + type_ = ui::AvatarType::Letter; + letter_ = QChar('A'); + + QFont _font(font()); + _font.setPointSizeF(ui::FontSize); + setFont(_font); + + QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + setSizePolicy(policy); +} + +Avatar::~Avatar() +{ +} + +QColor Avatar::textColor() const +{ + if (!text_color_.isValid()) + return QColor("black"); + + return text_color_; +} + +QColor Avatar::backgroundColor() const +{ + if (!text_color_.isValid()) + return QColor("white"); + + return background_color_; +} + +int Avatar::size() const +{ + return size_; +} + +QSize Avatar::sizeHint() const +{ + return QSize(size_ + 2, size_ + 2); +} + +void Avatar::setTextColor(const QColor &color) +{ + text_color_ = color; +} + +void Avatar::setBackgroundColor(const QColor &color) +{ + background_color_ = color; +} + +void Avatar::setSize(int size) +{ + size_ = size; + + if (!image_.isNull()) { + pixmap_ = QPixmap::fromImage( + image_.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + + QFont _font(font()); + _font.setPointSizeF(size_ * (ui::FontSize) / 40); + + setFont(_font); + update(); +} + +void Avatar::setLetter(const QChar &letter) +{ + letter_ = letter; + type_ = ui::AvatarType::Letter; + update(); +} + +void Avatar::setImage(const QImage &image) +{ + image_ = image; + type_ = ui::AvatarType::Image; + pixmap_ = QPixmap::fromImage( + image_.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + update(); +} + +void Avatar::setIcon(const QIcon &icon) +{ + icon_ = icon; + type_ = ui::AvatarType::Icon; + update(); +} + +void Avatar::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QRect r = rect(); + const int hs = size_ / 2; + + if (type_ != ui::AvatarType::Image) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor()); + + painter.setPen(Qt::NoPen); + painter.setBrush(brush); + painter.drawEllipse(r.center(), hs, hs); + } + + switch (type_) { + case ui::AvatarType::Icon: { + icon_.paint(&painter, + QRect((width() - hs) / 2, (height() - hs) / 2, hs, hs), + Qt::AlignCenter, + QIcon::Normal); + break; + } + case ui::AvatarType::Image: { + QPainterPath ppath; + ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_); + painter.setClipPath(ppath); + painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_), pixmap_); + break; + } + case ui::AvatarType::Letter: { + painter.setPen(textColor()); + painter.setBrush(Qt::NoBrush); + painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_); + break; + } + default: + break; + } +} diff --git a/src/ui/Badge.cc b/src/ui/Badge.cc new file mode 100644
index 00000000..05531f6c --- /dev/null +++ b/src/ui/Badge.cc
@@ -0,0 +1,186 @@ +#include <QPainter> + +#include "Badge.h" + +Badge::Badge(QWidget *parent) + : OverlayWidget(parent) +{ + init(); +} + +Badge::Badge(const QIcon &icon, QWidget *parent) + : OverlayWidget(parent) +{ + init(); + setIcon(icon); +} + +Badge::Badge(const QString &text, QWidget *parent) + : OverlayWidget(parent) +{ + init(); + setText(text); +} + +Badge::~Badge() +{ +} + +void Badge::init() +{ + x_ = 0; + y_ = 0; + padding_ = 10; + + setAttribute(Qt::WA_TransparentForMouseEvents); + + QFont _font(font()); + _font.setPointSizeF(10); + _font.setStyleName("Bold"); + + setFont(_font); + setText(""); +} + +QString Badge::text() const +{ + return text_; +} + +QIcon Badge::icon() const +{ + return icon_; +} + +QSize Badge::sizeHint() const +{ + const int d = getDiameter(); + return QSize(d + 4, d + 4); +} + +qreal Badge::relativeYPosition() const +{ + return y_; +} + +qreal Badge::relativeXPosition() const +{ + return x_; +} + +QPointF Badge::relativePosition() const +{ + return QPointF(x_, y_); +} + +QColor Badge::backgroundColor() const +{ + if (!background_color_.isValid()) + return QColor("black"); + + return background_color_; +} + +QColor Badge::textColor() const +{ + if (!text_color_.isValid()) + return QColor("white"); + + return text_color_; +} + +void Badge::setTextColor(const QColor &color) +{ + text_color_ = color; +} + +void Badge::setBackgroundColor(const QColor &color) +{ + background_color_ = color; +} + +void Badge::setRelativePosition(const QPointF &pos) +{ + setRelativePosition(pos.x(), pos.y()); +} + +void Badge::setRelativePosition(qreal x, qreal y) +{ + x_ = x; + y_ = y; + update(); +} + +void Badge::setRelativeXPosition(qreal x) +{ + x_ = x; + update(); +} + +void Badge::setRelativeYPosition(qreal y) +{ + y_ = y; + update(); +} + +void Badge::setIcon(const QIcon &icon) +{ + icon_ = icon; + update(); +} + +void Badge::setText(const QString &text) +{ + text_ = text; + + if (!icon_.isNull()) + icon_ = QIcon(); + + size_ = fontMetrics().size(Qt::TextShowMnemonic, text); + + update(); +} + +void Badge::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.translate(x_, y_); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(isEnabled() ? backgroundColor() : QColor("#cccccc")); + + painter.setBrush(brush); + painter.setPen(Qt::NoPen); + + const int d = getDiameter(); + + QRectF r(0, 0, d, d); + r.translate(QPointF((width() - d), (height() - d)) / 2); + + if (icon_.isNull()) { + painter.drawEllipse(r); + painter.setPen(textColor()); + painter.setBrush(Qt::NoBrush); + painter.drawText(r.translated(0, -0.5), Qt::AlignCenter, text_); + } else { + painter.drawEllipse(r); + QRectF q(0, 0, 16, 16); + q.moveCenter(r.center()); + QPixmap pixmap = icon().pixmap(16, 16); + QPainter icon(&pixmap); + icon.setCompositionMode(QPainter::CompositionMode_SourceIn); + icon.fillRect(pixmap.rect(), textColor()); + painter.drawPixmap(q.toRect(), pixmap); + } +} + +int Badge::getDiameter() const +{ + if (icon_.isNull()) { + return qMax(size_.width(), size_.height()) + padding_; + } + // FIXME: Move this to Theme.h as the default + return 24; +} diff --git a/src/ui/FlatButton.cc b/src/ui/FlatButton.cc new file mode 100644
index 00000000..97711de5 --- /dev/null +++ b/src/ui/FlatButton.cc
@@ -0,0 +1,761 @@ + +#include <QBitmap> +#include <QEventTransition> +#include <QFontDatabase> +#include <QIcon> +#include <QMouseEvent> +#include <QPainter> +#include <QPainterPath> +#include <QResizeEvent> +#include <QSequentialAnimationGroup> +#include <QSignalTransition> + +#include "FlatButton.h" +#include "Ripple.h" +#include "RippleOverlay.h" +#include "ThemeManager.h" + +void FlatButton::init() +{ + ripple_overlay_ = new RippleOverlay(this); + state_machine_ = new FlatButtonStateMachine(this); + role_ = ui::Default; + ripple_style_ = ui::PositionedRipple; + icon_placement_ = ui::LeftIcon; + overlay_style_ = ui::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; + halo_visible_ = false; + + setStyle(&ThemeManager::instance()); + setAttribute(Qt::WA_Hover); + setMouseTracking(true); + + 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::FlatPreset: + setOverlayStyle(ui::NoOverlay); + break; + case ui::CheckablePreset: + setOverlayStyle(ui::NoOverlay); + setCheckable(true); + setHaloVisible(false); + 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::Primary: + return ThemeManager::instance().themeColor("Blue"); + case ui::Secondary: + return ThemeManager::instance().themeColor("Gray"); + case ui::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::Primary: + return ThemeManager::instance().themeColor("Blue"); + case ui::Secondary: + return ThemeManager::instance().themeColor("Gray"); + case ui::Default: + default: + return ThemeManager::instance().themeColor("Black"); + } + } + + return background_color_; +} + +void FlatButton::setOverlayColor(const QColor &color) +{ + overlay_color_ = color; + setOverlayStyle(ui::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::setHaloVisible(bool visible) +{ + halo_visible_ = visible; + update(); +} + +bool FlatButton::isHaloVisible() const +{ + return halo_visible_; +} + +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, 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::NoRipple != ripple_style_) { + QPoint pos; + qreal radiusEndValue; + + if (ui::CenteredRipple == ripple_style_) { + pos = rect().center(); + } else { + pos = event->pos(); + } + + if (use_fixed_ripple_radius_) { + radiusEndValue = fixed_ripple_radius_; + } else { + radiusEndValue = static_cast<qreal>(width()) / 2; + } + + Ripple *ripple = new Ripple(pos); + + ripple->setRadiusEndValue(radiusEndValue); + ripple->setOpacityStartValue(0.35); + ripple->setColor(foregroundColor()); + ripple->radiusAnimation()->setDuration(600); + ripple->opacityAnimation()->setDuration(1300); + + 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); + paintHalo(&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::NoOverlay != overlay_style_) && (overlayOpacity > 0)) { + if (ui::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<qreal>(r.height()) * checkedProgress); + painter->drawRect(r); + } +} + +void FlatButton::paintHalo(QPainter *painter) +{ + if (!halo_visible_) + return; + + const qreal opacity = state_machine_->haloOpacity(); + const qreal s = state_machine_->haloScaleFactor() * state_machine_->haloSize(); + const qreal radius = static_cast<qreal>(width()) * s; + + if (isEnabled() && opacity > 0) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(foregroundColor()); + painter->setOpacity(opacity); + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + const QPointF center = rect().center(); + painter->drawEllipse(center, radius, radius); + } +} + +#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, text()); + return; + } + + QSize textSize(fontMetrics().size(Qt::TextSingleLine, 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, 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_)) + , halo_animation_(new QSequentialAnimationGroup(this)) + , overlay_opacity_(0) + , checked_overlay_progress_(parent->isChecked() ? 1 : 0) + , halo_opacity_(0) + , halo_size_(0.8) + , halo_scale_factor_(1) + , 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_); + + neutral_state_->assignProperty(this, "haloSize", 0); + neutral_focused_state_->assignProperty(this, "haloSize", 0.7); + hovered_state_->assignProperty(this, "haloSize", 0); + pressed_state_->assignProperty(this, "haloSize", 4); + hovered_focused_state_->assignProperty(this, "haloSize", 0.7); + + QPropertyAnimation *grow = new QPropertyAnimation(this); + QPropertyAnimation *shrink = new QPropertyAnimation(this); + + grow->setTargetObject(this); + grow->setPropertyName("haloScaleFactor"); + grow->setStartValue(0.56); + grow->setEndValue(0.63); + grow->setEasingCurve(QEasingCurve::InOutSine); + grow->setDuration(840); + + shrink->setTargetObject(this); + shrink->setPropertyName("haloScaleFactor"); + shrink->setStartValue(0.63); + shrink->setEndValue(0.56); + shrink->setEasingCurve(QEasingCurve::InOutSine); + shrink->setDuration(840); + + halo_animation_->addAnimation(grow); + halo_animation_->addAnimation(shrink); + halo_animation_->setLoopCount(-1); +} + +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::setHaloOpacity(qreal opacity) +{ + halo_opacity_ = opacity; + button_->update(); +} + +void FlatButtonStateMachine::setHaloSize(qreal size) +{ + halo_size_ = size; + button_->update(); +} + +void FlatButtonStateMachine::setHaloScaleFactor(qreal factor) +{ + halo_scale_factor_ = factor; + button_->update(); +} + +void FlatButtonStateMachine::startAnimations() +{ + halo_animation_->start(); + 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_state_->assignProperty(this, "haloOpacity", 0); + neutral_focused_state_->assignProperty(this, "overlayOpacity", 0); + neutral_focused_state_->assignProperty(this, "haloOpacity", baseOpacity); + hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity); + hovered_state_->assignProperty(this, "haloOpacity", 0); + hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity); + hovered_focused_state_->assignProperty(this, "haloOpacity", baseOpacity); + pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity); + pressed_state_->assignProperty(this, "haloOpacity", 0); + 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<QFocusEvent *>(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); + + animation = new QPropertyAnimation(this, "haloOpacity", this); + animation->setDuration(170); + transition->addAnimation(animation); + + animation = new QPropertyAnimation(this, "haloSize", this); + animation->setDuration(350); + animation->setEasingCurve(QEasingCurve::OutCubic); + transition->addAnimation(animation); + + fromState->addTransition(transition); +} diff --git a/src/ui/OverlayWidget.cc b/src/ui/OverlayWidget.cc new file mode 100644
index 00000000..b4dfb918 --- /dev/null +++ b/src/ui/OverlayWidget.cc
@@ -0,0 +1,59 @@ +#include "OverlayWidget.h" +#include <QEvent> + +OverlayWidget::OverlayWidget(QWidget *parent) + : QWidget(parent) +{ + if (parent) + parent->installEventFilter(this); +} + +OverlayWidget::~OverlayWidget() +{ +} + +bool OverlayWidget::event(QEvent *event) +{ + if (!parent()) + return QWidget::event(event); + + switch (event->type()) { + case QEvent::ParentChange: { + parent()->installEventFilter(this); + setGeometry(overlayGeometry()); + break; + } + case QEvent::ParentAboutToChange: { + parent()->removeEventFilter(this); + break; + } + default: + break; + } + + return QWidget::event(event); +} + +bool OverlayWidget::eventFilter(QObject *obj, QEvent *event) +{ + switch (event->type()) { + case QEvent::Move: + case QEvent::Resize: + setGeometry(overlayGeometry()); + break; + default: + break; + } + + return QWidget::eventFilter(obj, event); +} + +QRect OverlayWidget::overlayGeometry() const +{ + QWidget *widget = parentWidget(); + + if (!widget) + return QRect(); + + return widget->rect(); +} diff --git a/src/ui/RaisedButton.cc b/src/ui/RaisedButton.cc new file mode 100644
index 00000000..74f549c4 --- /dev/null +++ b/src/ui/RaisedButton.cc
@@ -0,0 +1,92 @@ +#include <QEventTransition> +#include <QGraphicsDropShadowEffect> +#include <QPropertyAnimation> +#include <QState> +#include <QStateMachine> + +#include "RaisedButton.h" + +void RaisedButton::init() +{ + shadow_state_machine_ = new QStateMachine(this); + normal_state_ = new QState; + pressed_state_ = new QState; + effect_ = new QGraphicsDropShadowEffect; + + effect_->setBlurRadius(7); + effect_->setOffset(QPointF(0, 2)); + effect_->setColor(QColor(0, 0, 0, 75)); + + setBackgroundMode(Qt::OpaqueMode); + setMinimumHeight(42); + setGraphicsEffect(effect_); + setBaseOpacity(0.3); + + shadow_state_machine_->addState(normal_state_); + shadow_state_machine_->addState(pressed_state_); + + normal_state_->assignProperty(effect_, "offset", QPointF(0, 2)); + normal_state_->assignProperty(effect_, "blurRadius", 7); + + pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5)); + pressed_state_->assignProperty(effect_, "blurRadius", 29); + + QAbstractTransition *transition; + + transition = new QEventTransition(this, QEvent::MouseButtonPress); + transition->setTargetState(pressed_state_); + normal_state_->addTransition(transition); + + transition = new QEventTransition(this, QEvent::MouseButtonDblClick); + transition->setTargetState(pressed_state_); + normal_state_->addTransition(transition); + + transition = new QEventTransition(this, QEvent::MouseButtonRelease); + transition->setTargetState(normal_state_); + pressed_state_->addTransition(transition); + + QPropertyAnimation *animation; + + animation = new QPropertyAnimation(effect_, "offset", this); + animation->setDuration(100); + shadow_state_machine_->addDefaultAnimation(animation); + + animation = new QPropertyAnimation(effect_, "blurRadius", this); + animation->setDuration(100); + shadow_state_machine_->addDefaultAnimation(animation); + + shadow_state_machine_->setInitialState(normal_state_); + shadow_state_machine_->start(); +} + +RaisedButton::RaisedButton(QWidget *parent) + : FlatButton(parent) +{ + init(); +} + +RaisedButton::RaisedButton(const QString &text, QWidget *parent) + : FlatButton(parent) +{ + init(); + setText(text); +} + +RaisedButton::~RaisedButton() +{ +} + +bool RaisedButton::event(QEvent *event) +{ + if (QEvent::EnabledChange == event->type()) { + if (isEnabled()) { + shadow_state_machine_->start(); + effect_->setEnabled(true); + } else { + shadow_state_machine_->stop(); + effect_->setEnabled(false); + } + } + + return FlatButton::event(event); +} diff --git a/src/ui/Ripple.cc b/src/ui/Ripple.cc new file mode 100644
index 00000000..107bfd7f --- /dev/null +++ b/src/ui/Ripple.cc
@@ -0,0 +1,106 @@ +#include "Ripple.h" +#include "RippleOverlay.h" + +Ripple::Ripple(const QPoint &center, QObject *parent) + : QParallelAnimationGroup(parent) + , overlay_(0) + , radius_anim_(animate("radius")) + , opacity_anim_(animate("opacity")) + , radius_(0) + , opacity_(0) + , center_(center) +{ + init(); +} + +Ripple::Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent) + : QParallelAnimationGroup(parent) + , overlay_(overlay) + , radius_anim_(animate("radius")) + , opacity_anim_(animate("opacity")) + , radius_(0) + , opacity_(0) + , center_(center) +{ + init(); +} + +Ripple::~Ripple() +{ +} + +void Ripple::setRadius(qreal radius) +{ + Q_ASSERT(overlay_); + + if (radius_ == radius) + return; + + radius_ = radius; + overlay_->update(); +} + +void Ripple::setOpacity(qreal opacity) +{ + Q_ASSERT(overlay_); + + if (opacity_ == opacity) + return; + + opacity_ = opacity; + overlay_->update(); +} + +void Ripple::setColor(const QColor &color) +{ + if (brush_.color() == color) + return; + + brush_.setColor(color); + + if (overlay_) + overlay_->update(); +} + +void Ripple::setBrush(const QBrush &brush) +{ + brush_ = brush; + + if (overlay_) + overlay_->update(); +} + +void Ripple::destroy() +{ + Q_ASSERT(overlay_); + + overlay_->removeRipple(this); +} + +QPropertyAnimation *Ripple::animate(const QByteArray &property, + const QEasingCurve &easing, + int duration) +{ + QPropertyAnimation *animation = new QPropertyAnimation; + animation->setTargetObject(this); + animation->setPropertyName(property); + animation->setEasingCurve(easing); + animation->setDuration(duration); + + addAnimation(animation); + + return animation; +} + +void Ripple::init() +{ + setOpacityStartValue(0.5); + setOpacityEndValue(0); + setRadiusStartValue(0); + setRadiusEndValue(300); + + brush_.setColor(Qt::black); + brush_.setStyle(Qt::SolidPattern); + + connect(this, SIGNAL(finished()), this, SLOT(destroy())); +} diff --git a/src/ui/RippleOverlay.cc b/src/ui/RippleOverlay.cc new file mode 100644
index 00000000..add030d9 --- /dev/null +++ b/src/ui/RippleOverlay.cc
@@ -0,0 +1,61 @@ +#include <QPainter> + +#include "Ripple.h" +#include "RippleOverlay.h" + +RippleOverlay::RippleOverlay(QWidget *parent) + : OverlayWidget(parent) + , use_clip_(false) +{ + setAttribute(Qt::WA_TransparentForMouseEvents); + setAttribute(Qt::WA_NoSystemBackground); +} + +RippleOverlay::~RippleOverlay() +{ +} + +void RippleOverlay::addRipple(Ripple *ripple) +{ + ripple->setOverlay(this); + ripples_.push_back(ripple); + ripple->start(); +} + +void RippleOverlay::addRipple(const QPoint &position, qreal radius) +{ + Ripple *ripple = new Ripple(position); + ripple->setRadiusEndValue(radius); + addRipple(ripple); +} + +void RippleOverlay::removeRipple(Ripple *ripple) +{ + if (ripples_.removeOne(ripple)) + delete ripple; +} + +void RippleOverlay::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + + if (use_clip_) + painter.setClipPath(clip_path_); + + for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); it++) + paintRipple(&painter, *it); +} + +void RippleOverlay::paintRipple(QPainter *painter, Ripple *ripple) +{ + const qreal radius = ripple->radius(); + const QPointF center = ripple->center(); + + painter->setOpacity(ripple->opacity()); + painter->setBrush(ripple->brush()); + painter->drawEllipse(center, radius, radius); +} diff --git a/src/ui/TextField.cc b/src/ui/TextField.cc new file mode 100644
index 00000000..3b701549 --- /dev/null +++ b/src/ui/TextField.cc
@@ -0,0 +1,346 @@ +#include "TextField.h" + +#include <QApplication> +#include <QEventTransition> +#include <QFontDatabase> +#include <QPaintEvent> +#include <QPainter> +#include <QPropertyAnimation> + +TextField::TextField(QWidget *parent) + : QLineEdit(parent) +{ + state_machine_ = new TextFieldStateMachine(this); + label_ = 0; + label_font_size_ = 9.5; + show_label_ = false; + background_color_ = QColor("white"); + + setFrame(false); + setAttribute(Qt::WA_Hover); + setMouseTracking(true); + setTextMargins(0, 2, 0, 4); + + QFontDatabase db; + QFont font(db.font("Open Sans", "Regular", 11)); + setFont(font); + + state_machine_->start(); + QCoreApplication::processEvents(); +} + +TextField::~TextField() +{ +} + +void TextField::setBackgroundColor(const QColor &color) +{ + background_color_ = color; +} + +QColor TextField::backgroundColor() const +{ + return background_color_; +} + +void TextField::setShowLabel(bool value) +{ + if (show_label_ == value) { + return; + } + + show_label_ = value; + + if (!label_ && value) { + label_ = new TextFieldLabel(this); + state_machine_->setLabel(label_); + } + + if (value) { + setContentsMargins(0, 23, 0, 0); + } else { + setContentsMargins(0, 0, 0, 0); + } +} + +bool TextField::hasLabel() const +{ + return show_label_; +} + +void TextField::setLabelFontSize(qreal size) +{ + label_font_size_ = size; + + if (label_) { + QFont font(label_->font()); + font.setPointSizeF(size); + label_->setFont(font); + label_->update(); + } +} + +qreal TextField::labelFontSize() const +{ + return label_font_size_; +} + +void TextField::setLabel(const QString &label) +{ + label_text_ = label; + setShowLabel(true); + label_->update(); +} + +QString TextField::label() const +{ + return label_text_; +} + +void TextField::setTextColor(const QColor &color) +{ + text_color_ = color; + setStyleSheet(QString("QLineEdit { color: %1; }").arg(color.name())); +} + +QColor TextField::textColor() const +{ + if (!text_color_.isValid()) { + return QColor("black"); + } + + return text_color_; +} + +void TextField::setLabelColor(const QColor &color) +{ + label_color_ = color; +} + +QColor TextField::labelColor() const +{ + if (!label_color_.isValid()) { + return QColor("#abb"); // TODO: Move this into Theme.h + } + + return label_color_; +} + +void TextField::setInkColor(const QColor &color) +{ + ink_color_ = color; +} + +QColor TextField::inkColor() const +{ + if (!ink_color_.isValid()) { + return QColor("black"); + } + + return ink_color_; +} + +void TextField::setUnderlineColor(const QColor &color) +{ + underline_color_ = color; +} + +QColor TextField::underlineColor() const +{ + if (!underline_color_.isValid()) { + return QColor("black"); + } + + return underline_color_; +} + +bool TextField::event(QEvent *event) +{ + switch (event->type()) { + case QEvent::Resize: + case QEvent::Move: { + if (label_) + label_->setGeometry(rect()); + break; + } + default: + break; + } + + return QLineEdit::event(event); +} + +void TextField::paintEvent(QPaintEvent *event) +{ + QLineEdit::paintEvent(event); + + QPainter painter(this); + + if (text().isEmpty()) { + painter.setOpacity(1 - state_machine_->progress()); + //painter.fillRect(rect(), parentWidget()->palette().color(backgroundRole())); + painter.fillRect(rect(), backgroundColor()); + } + + const int y = height() - 1; + const int wd = width() - 5; + + QPen pen; + pen.setWidth(1); + pen.setColor(underlineColor()); + painter.setPen(pen); + painter.setOpacity(1); + painter.drawLine(2.5, y, wd, y); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(inkColor()); + + const qreal progress = state_machine_->progress(); + + if (progress > 0) { + painter.setPen(Qt::NoPen); + painter.setBrush(brush); + const int w = (1 - progress) * static_cast<qreal>(wd / 2); + painter.drawRect(w + 2.5, height() - 2, wd - 2 * w, 2); + } +} + +TextFieldStateMachine::TextFieldStateMachine(TextField *parent) + : QStateMachine(parent), text_field_(parent) +{ + normal_state_ = new QState; + focused_state_ = new QState; + + label_ = 0; + offset_anim_ = 0; + color_anim_ = 0; + progress_ = 0.0; + + addState(normal_state_); + addState(focused_state_); + + setInitialState(normal_state_); + + QEventTransition *transition; + QPropertyAnimation *animation; + + transition = new QEventTransition(parent, QEvent::FocusIn); + transition->setTargetState(focused_state_); + normal_state_->addTransition(transition); + + animation = new QPropertyAnimation(this, "progress", this); + animation->setEasingCurve(QEasingCurve::InCubic); + animation->setDuration(310); + transition->addAnimation(animation); + + transition = new QEventTransition(parent, QEvent::FocusOut); + transition->setTargetState(normal_state_); + focused_state_->addTransition(transition); + + animation = new QPropertyAnimation(this, "progress", this); + animation->setEasingCurve(QEasingCurve::OutCubic); + animation->setDuration(310); + transition->addAnimation(animation); + + normal_state_->assignProperty(this, "progress", 0); + focused_state_->assignProperty(this, "progress", 1); + + setupProperties(); + + connect(text_field_, SIGNAL(textChanged(QString)), this, SLOT(setupProperties())); +} + +TextFieldStateMachine::~TextFieldStateMachine() +{ +} + +void TextFieldStateMachine::setLabel(TextFieldLabel *label) +{ + if (label_) { + delete label_; + } + + if (offset_anim_) { + removeDefaultAnimation(offset_anim_); + delete offset_anim_; + } + + if (color_anim_) { + removeDefaultAnimation(color_anim_); + delete color_anim_; + } + + label_ = label; + + if (label_) { + offset_anim_ = new QPropertyAnimation(label_, "offset", this); + offset_anim_->setDuration(210); + offset_anim_->setEasingCurve(QEasingCurve::OutCubic); + addDefaultAnimation(offset_anim_); + + color_anim_ = new QPropertyAnimation(label_, "color", this); + color_anim_->setDuration(210); + addDefaultAnimation(color_anim_); + } + + setupProperties(); +} + +void TextFieldStateMachine::setupProperties() +{ + if (label_) { + const int m = text_field_->textMargins().top(); + + if (text_field_->text().isEmpty()) { + normal_state_->assignProperty(label_, "offset", QPointF(0, 26)); + } else { + normal_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); + } + + focused_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); + focused_state_->assignProperty(label_, "color", text_field_->inkColor()); + normal_state_->assignProperty(label_, "color", text_field_->labelColor()); + + if (0 != label_->offset().y() && !text_field_->text().isEmpty()) { + label_->setOffset(QPointF(0, 0 - m)); + } else if (!text_field_->hasFocus() && label_->offset().y() <= 0 && text_field_->text().isEmpty()) { + label_->setOffset(QPointF(0, 26)); + } + } + + text_field_->update(); +} + +TextFieldLabel::TextFieldLabel(TextField *parent) + : QWidget(parent), text_field_(parent) +{ + x_ = 0; + y_ = 26; + scale_ = 1; + color_ = parent->labelColor(); + + QFontDatabase db; + QFont font(db.font("Open Sans", "Medium", parent->labelFontSize())); + font.setLetterSpacing(QFont::PercentageSpacing, 102); + setFont(font); +} + +TextFieldLabel::~TextFieldLabel() +{ +} + +void TextFieldLabel::paintEvent(QPaintEvent *) +{ + if (!text_field_->hasLabel()) + return; + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.scale(scale_, scale_); + painter.setPen(color_); + painter.setOpacity(1); + + QPointF pos(2 + x_, height() - 36 + y_); + painter.drawText(pos.x(), pos.y(), text_field_->label()); +} diff --git a/src/ui/Theme.cc b/src/ui/Theme.cc new file mode 100644
index 00000000..4c5c19de --- /dev/null +++ b/src/ui/Theme.cc
@@ -0,0 +1,73 @@ +#include <QDebug> + +#include "Theme.h" + +Theme::Theme(QObject *parent) + : QObject(parent) +{ + setColor("Black", ui::Color::Black); + + setColor("BrightWhite", ui::Color::BrightWhite); + setColor("FadedWhite", ui::Color::FadedWhite); + setColor("MediumWhite", ui::Color::MediumWhite); + + setColor("BrightGreen", ui::Color::BrightGreen); + setColor("DarkGreen", ui::Color::DarkGreen); + setColor("LightGreen", ui::Color::LightGreen); + + setColor("Gray", ui::Color::Gray); + setColor("Red", ui::Color::Red); + setColor("Blue", ui::Color::Blue); + + setColor("Transparent", ui::Color::Transparent); +} + +Theme::~Theme() +{ +} + +QColor Theme::rgba(int r, int g, int b, qreal a) const +{ + QColor color(r, g, b); + color.setAlphaF(a); + + return color; +} + +QColor Theme::getColor(const QString &key) const +{ + if (!colors_.contains(key)) { + qWarning() << "Color with key" << key << "could not be found"; + return QColor(); + } + + return colors_.value(key); +} + +void Theme::setColor(const QString &key, const QColor &color) +{ + colors_.insert(key, color); +} + +void Theme::setColor(const QString &key, ui::Color &color) +{ + static const QColor palette[] = { + QColor("#171919"), + + QColor("#EBEBEB"), + QColor("#C9C9C9"), + QColor("#929292"), + + QColor("#1C3133"), + QColor("#577275"), + QColor("#46A451"), + + QColor("#5D6565"), + QColor("#E22826"), + QColor("#81B3A9"), + + rgba(0, 0, 0, 0), + }; + + colors_.insert(key, palette[color]); +} diff --git a/src/ui/ThemeManager.cc b/src/ui/ThemeManager.cc new file mode 100644
index 00000000..3c8a16ab --- /dev/null +++ b/src/ui/ThemeManager.cc
@@ -0,0 +1,20 @@ +#include <QFontDatabase> + +#include "ThemeManager.h" + +ThemeManager::ThemeManager() +{ + setTheme(new Theme); +} + +void ThemeManager::setTheme(Theme *theme) +{ + theme_ = theme; + theme_->setParent(this); +} + +QColor ThemeManager::themeColor(const QString &key) const +{ + Q_ASSERT(theme_); + return theme_->getColor(key); +}