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 ¢er, 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 ¢er, 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);
+}
|