diff --git a/src/ChatPage.cc b/src/ChatPage.cc
new file mode 100644
index 00000000..a17e8c65
--- /dev/null
+++ b/src/ChatPage.cc
@@ -0,0 +1,277 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ui_ChatPage.h"
+
+#include <QDebug>
+#include <QLabel>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QSettings>
+#include <QWidget>
+
+#include "ChatPage.h"
+#include "Sync.h"
+#include "UserInfoWidget.h"
+
+ChatPage::ChatPage(QWidget *parent)
+ : QWidget(parent)
+ , ui(new Ui::ChatPage)
+ , sync_interval_(2000)
+{
+ ui->setupUi(this);
+ matrix_client_ = new MatrixClient("matrix.org", parent);
+ content_downloader_ = new QNetworkAccessManager(parent);
+
+ room_list_ = new RoomList(this);
+
+ top_bar_ = new TopRoomBar(this);
+ ui->topBarLayout->addWidget(top_bar_);
+
+ view_manager_ = new HistoryViewManager(this);
+ ui->mainContentLayout->addWidget(view_manager_);
+
+ text_input_ = new TextInputWidget(this);
+ ui->contentLayout->addWidget(text_input_);
+
+ user_info_widget_ = new UserInfoWidget(ui->sideBarTopWidget);
+
+ connect(room_list_,
+ SIGNAL(roomChanged(const RoomInfo &)),
+ this,
+ SLOT(changeTopRoomInfo(const RoomInfo &)));
+
+ connect(room_list_,
+ SIGNAL(roomChanged(const RoomInfo &)),
+ view_manager_,
+ SLOT(setHistoryView(const RoomInfo &)));
+
+ connect(room_list_,
+ SIGNAL(fetchRoomAvatar(const QString &, const QUrl &)),
+ this,
+ SLOT(fetchRoomAvatar(const QString &, const QUrl &)));
+
+ connect(text_input_,
+ SIGNAL(sendTextMessage(const QString &)),
+ this,
+ SLOT(sendTextMessage(const QString &)));
+
+ ui->sideBarTopUserInfoLayout->addWidget(user_info_widget_);
+ ui->sideBarMainLayout->addWidget(room_list_);
+
+ connect(matrix_client_,
+ SIGNAL(initialSyncCompleted(SyncResponse)),
+ this,
+ SLOT(initialSyncCompleted(SyncResponse)));
+ connect(matrix_client_,
+ SIGNAL(syncCompleted(SyncResponse)),
+ this,
+ SLOT(syncCompleted(SyncResponse)));
+ connect(matrix_client_,
+ SIGNAL(getOwnProfileResponse(QUrl, QString)),
+ this,
+ SLOT(updateOwnProfileInfo(QUrl, QString)));
+ connect(matrix_client_,
+ SIGNAL(messageSent(QString, int)),
+ this,
+ SLOT(messageSent(QString, int)));
+}
+
+void ChatPage::messageSent(QString event_id, int txn_id)
+{
+ Q_UNUSED(event_id);
+
+ QSettings settings;
+ settings.setValue("client/transaction_id", txn_id + 1);
+}
+
+void ChatPage::sendTextMessage(const QString &msg)
+{
+ auto room = current_room_;
+ matrix_client_->sendTextMessage(current_room_.id(), msg);
+}
+
+void ChatPage::bootstrap(QString userid, QString homeserver, QString token)
+{
+ Q_UNUSED(userid);
+
+ matrix_client_->setServer(homeserver);
+ matrix_client_->setAccessToken(token);
+
+ matrix_client_->getOwnProfile();
+ matrix_client_->initialSync();
+}
+
+void ChatPage::startSync()
+{
+ matrix_client_->sync();
+}
+
+void ChatPage::setOwnAvatar(QByteArray img)
+{
+ if (img.size() == 0)
+ return;
+
+ QPixmap pixmap;
+ pixmap.loadFromData(img);
+ user_info_widget_->setAvatar(pixmap.toImage());
+}
+
+void ChatPage::syncCompleted(SyncResponse response)
+{
+ matrix_client_->setNextBatchToken(response.nextBatch());
+
+ /* room_list_->sync(response.rooms()); */
+ view_manager_->sync(response.rooms());
+}
+
+void ChatPage::initialSyncCompleted(SyncResponse response)
+{
+ if (!response.nextBatch().isEmpty())
+ matrix_client_->setNextBatchToken(response.nextBatch());
+
+ view_manager_->initialize(response.rooms());
+ room_list_->setInitialRooms(response.rooms());
+
+ sync_timer_ = new QTimer(this);
+ connect(sync_timer_, SIGNAL(timeout()), this, SLOT(startSync()));
+ sync_timer_->start(sync_interval_);
+}
+
+// TODO: This function should be part of the matrix client for generic media retrieval.
+void ChatPage::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url)
+{
+ // TODO: move this into a Utils function
+ QList<QString> url_parts = avatar_url.toString().split("mxc://");
+
+ if (url_parts.size() != 2) {
+ qDebug() << "Invalid format for room avatar " << avatar_url.toString();
+ return;
+ }
+
+ QString media_params = url_parts[1];
+ QString media_url = QString("%1/_matrix/media/r0/download/%2")
+ .arg(matrix_client_->getHomeServer(), media_params);
+
+ QNetworkRequest avatar_request(media_url);
+ QNetworkReply *reply = content_downloader_->get(avatar_request);
+ reply->setProperty("media_params", media_params);
+
+ connect(reply, &QNetworkReply::finished, [this, media_params, roomid, reply]() {
+ reply->deleteLater();
+
+ auto media = reply->property("media_params").toString();
+
+ if (media != media_params)
+ return;
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status == 0) {
+ qDebug() << reply->errorString();
+ return;
+ }
+
+ if (status >= 400) {
+ qWarning() << "Request " << reply->request().url() << " returned " << status;
+ return;
+ }
+
+ auto img = reply->readAll();
+
+ if (img.size() == 0)
+ return;
+
+ QPixmap pixmap;
+ pixmap.loadFromData(img);
+ room_avatars_.insert(roomid, pixmap);
+
+ this->room_list_->updateRoomAvatar(roomid, pixmap.toImage());
+
+ if (current_room_.id() == roomid) {
+ QIcon icon(pixmap);
+ this->top_bar_->updateRoomAvatar(icon);
+ }
+ });
+}
+
+void ChatPage::updateOwnProfileInfo(QUrl avatar_url, QString display_name)
+{
+ QSettings settings;
+ auto userid = settings.value("auth/user_id").toString();
+
+ user_info_widget_->setUserId(userid);
+ user_info_widget_->setDisplayName(display_name);
+
+ // TODO: move this into a Utils function
+ QList<QString> url_parts = avatar_url.toString().split("mxc://");
+
+ if (url_parts.size() != 2) {
+ qDebug() << "Invalid format for media " << avatar_url.toString();
+ return;
+ }
+
+ QString media_params = url_parts[1];
+ QString media_url = QString("%1/_matrix/media/r0/download/%2")
+ .arg(matrix_client_->getHomeServer(), media_params);
+
+ QNetworkRequest avatar_request(media_url);
+ QNetworkReply *reply = content_downloader_->get(avatar_request);
+ reply->setProperty("media_params", media_params);
+
+ connect(reply, &QNetworkReply::finished, [this, media_params, reply]() {
+ reply->deleteLater();
+
+ auto media = reply->property("media_params").toString();
+
+ if (media != media_params)
+ return;
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status == 0) {
+ qDebug() << reply->errorString();
+ return;
+ }
+
+ if (status >= 400) {
+ qWarning() << "Request " << reply->request().url() << " returned " << status;
+ return;
+ }
+
+ setOwnAvatar(reply->readAll());
+ });
+}
+
+void ChatPage::changeTopRoomInfo(const RoomInfo &info)
+{
+ top_bar_->updateRoomName(info.name());
+ top_bar_->updateRoomTopic(info.topic());
+
+ if (room_avatars_.contains(info.id())) {
+ QIcon icon(room_avatars_.value(info.id()));
+ top_bar_->updateRoomAvatar(icon);
+ }
+
+ current_room_ = info;
+}
+
+ChatPage::~ChatPage()
+{
+ sync_timer_->stop();
+ delete ui;
+}
diff --git a/src/Deserializable.cc b/src/Deserializable.cc
new file mode 100644
index 00000000..967f5ef4
--- /dev/null
+++ b/src/Deserializable.cc
@@ -0,0 +1,31 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonValue>
+
+#include "Deserializable.h"
+
+DeserializationException::DeserializationException(const std::string &msg) : msg_(msg)
+{
+}
+
+const char *DeserializationException::what() const throw()
+{
+ return msg_.c_str();
+}
diff --git a/src/HistoryView.cc b/src/HistoryView.cc
new file mode 100644
index 00000000..0949d17c
--- /dev/null
+++ b/src/HistoryView.cc
@@ -0,0 +1,145 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <random>
+
+#include <QDebug>
+#include <QScrollBar>
+#include <QtWidgets/QLabel>
+#include <QtWidgets/QSpacerItem>
+
+#include "HistoryView.h"
+#include "HistoryViewItem.h"
+
+const QList<QString> HistoryView::COLORS({"#FFF46E",
+ "#A58BFF",
+ "#50C9BA",
+ "#9EE6CF",
+ "#FFDD67",
+ "#2980B9",
+ "#FC993C",
+ "#2772DB",
+ "#CB8589",
+ "#DDE8B9",
+ "#55A44E",
+ "#A9EEE6",
+ "#53B759",
+ "#9E3997",
+ "#5D89D5",
+ "#BB86B7",
+ "#50a0cf",
+ "#3C989F",
+ "#5A4592",
+ "#235e5b",
+ "#d58247",
+ "#e0a729",
+ "#a2b636",
+ "#4BBE2E"});
+
+HistoryView::HistoryView(QList<Event> events, QWidget *parent)
+ : QWidget(parent)
+{
+ init();
+ addEvents(events);
+}
+
+HistoryView::HistoryView(QWidget *parent)
+ : QWidget(parent)
+{
+ init();
+}
+
+void HistoryView::sliderRangeChanged(int min, int max)
+{
+ Q_UNUSED(min);
+ scroll_area_->verticalScrollBar()->setValue(max);
+}
+
+QString HistoryView::chooseRandomColor()
+{
+ std::random_device random_device;
+ std::mt19937 engine{random_device()};
+ std::uniform_int_distribution<int> dist(0, HistoryView::COLORS.size() - 1);
+
+ return HistoryView::COLORS[dist(engine)];
+}
+
+void HistoryView::addEvents(const QList<Event> &events)
+{
+ for (int i = 0; i < events.size(); i++) {
+ auto event = events[i];
+
+ if (event.type() == "m.room.message") {
+ auto msg_type = event.content().value("msgtype").toString();
+
+ if (msg_type == "m.text" || msg_type == "m.notice") {
+ auto with_sender = last_sender_ != event.sender();
+ auto color = nick_colors_.value(event.sender());
+
+ if (color.isEmpty()) {
+ color = chooseRandomColor();
+ nick_colors_.insert(event.sender(), color);
+ }
+
+ addHistoryItem(event, color, with_sender);
+ last_sender_ = event.sender();
+ } else {
+ qDebug() << "Ignoring message" << msg_type;
+ }
+ }
+ }
+}
+
+void HistoryView::init()
+{
+ top_layout_ = new QVBoxLayout(this);
+ top_layout_->setSpacing(0);
+ top_layout_->setMargin(0);
+
+ scroll_area_ = new QScrollArea(this);
+ scroll_area_->setWidgetResizable(true);
+
+ scroll_widget_ = new QWidget();
+
+ scroll_layout_ = new QVBoxLayout();
+ scroll_layout_->addStretch(1);
+ scroll_layout_->setSpacing(0);
+
+ scroll_widget_->setLayout(scroll_layout_);
+
+ scroll_area_->setWidget(scroll_widget_);
+
+ top_layout_->addWidget(scroll_area_);
+
+ setLayout(top_layout_);
+
+ connect(scroll_area_->verticalScrollBar(),
+ SIGNAL(rangeChanged(int, int)),
+ this,
+ SLOT(sliderRangeChanged(int, int)));
+}
+
+void HistoryView::addHistoryItem(Event event, QString color, bool with_sender)
+{
+ // TODO: Probably create another function instead of passing the flag.
+ HistoryViewItem *item = new HistoryViewItem(event, with_sender, color, scroll_widget_);
+ scroll_layout_->addWidget(item);
+}
+
+HistoryView::~HistoryView()
+{
+}
diff --git a/src/HistoryViewItem.cc b/src/HistoryViewItem.cc
new file mode 100644
index 00000000..04c42f45
--- /dev/null
+++ b/src/HistoryViewItem.cc
@@ -0,0 +1,80 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDateTime>
+#include <QDebug>
+
+#include "HistoryViewItem.h"
+
+HistoryViewItem::HistoryViewItem(Event event, bool with_sender, QString color, QWidget *parent)
+ : QWidget(parent)
+{
+ QString sender = "";
+
+ if (with_sender)
+ sender = event.sender().split(":")[0].split("@")[1];
+
+ auto body = event.content().value("body").toString();
+
+ auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
+ auto local_time = timestamp.toString("HH:mm");
+
+ time_label_ = new QLabel(this);
+ time_label_->setWordWrap(true);
+ QString msg(
+ "<html>"
+ "<head/>"
+ "<body>"
+ " <span style=\"font-size: 7pt; color: #5d6565;\">"
+ " %1"
+ " </span>"
+ "</body>"
+ "</html>");
+ time_label_->setText(msg.arg(local_time));
+ time_label_->setStyleSheet("margin-left: 7px; margin-right: 7px; margin-top: 0;");
+ time_label_->setAlignment(Qt::AlignTop);
+
+ content_label_ = new QLabel(this);
+ content_label_->setWordWrap(true);
+ content_label_->setAlignment(Qt::AlignTop);
+ content_label_->setStyleSheet("margin: 0;");
+ QString content(
+ "<html>"
+ "<head/>"
+ "<body>"
+ " <span style=\"font-size: 10pt; font-weight: 600; color: %1\">"
+ " %2"
+ " </span>"
+ " <span style=\"font-size: 10pt;\">"
+ " %3"
+ " </span>"
+ "</body>"
+ "</html>");
+ content_label_->setText(content.arg(color).arg(sender).arg(body));
+
+ top_layout_ = new QHBoxLayout();
+ top_layout_->setMargin(0);
+
+ top_layout_->addWidget(time_label_);
+ top_layout_->addWidget(content_label_, 1);
+
+ setLayout(top_layout_);
+}
+
+HistoryViewItem::~HistoryViewItem()
+{
+}
diff --git a/src/HistoryViewManager.cc b/src/HistoryViewManager.cc
new file mode 100644
index 00000000..c7292747
--- /dev/null
+++ b/src/HistoryViewManager.cc
@@ -0,0 +1,83 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+#include <QStackedWidget>
+#include <QWidget>
+
+#include "HistoryView.h"
+#include "HistoryViewManager.h"
+
+HistoryViewManager::HistoryViewManager(QWidget *parent)
+ : QStackedWidget(parent)
+{
+ setStyleSheet(
+ "QWidget {background: #171919; color: #ebebeb;}"
+ "QScrollBar:vertical { background-color: #171919; width: 10px; border-radius: 20px; margin: 0px 2px 0 2px; }"
+ "QScrollBar::handle:vertical { border-radius : 50px; background-color : #1c3133; }"
+ "QScrollBar::add-line:vertical { border: none; background: none; }"
+ "QScrollBar::sub-line:vertical { border: none; background: none; }");
+}
+
+HistoryViewManager::~HistoryViewManager()
+{
+}
+
+void HistoryViewManager::initialize(const Rooms &rooms)
+{
+ for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
+ auto roomid = it.key();
+ auto events = it.value().timeline().events();
+
+ // Create a history view with the room events.
+ HistoryView *view = new HistoryView(events);
+ views_.insert(it.key(), view);
+
+ // Add the view in the widget stack.
+ addWidget(view);
+ }
+}
+
+void HistoryViewManager::sync(const Rooms &rooms)
+{
+ for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
+ auto roomid = it.key();
+
+ if (!views_.contains(roomid)) {
+ qDebug() << "Ignoring event from unknown room";
+ continue;
+ }
+
+ auto view = views_.value(roomid);
+ auto events = it.value().timeline().events();
+
+ view->addEvents(events);
+ }
+}
+
+void HistoryViewManager::setHistoryView(const RoomInfo &info)
+{
+ if (!views_.contains(info.id())) {
+ qDebug() << "Room List id is not present in view manager";
+ qDebug() << info.name();
+ return;
+ }
+
+ auto widget = views_.value(info.id());
+
+ setCurrentWidget(widget);
+}
diff --git a/src/InputValidator.cc b/src/InputValidator.cc
new file mode 100644
index 00000000..3713c501
--- /dev/null
+++ b/src/InputValidator.cc
@@ -0,0 +1,31 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "InputValidator.h"
+
+// FIXME: Maybe change the regex to match the real Matrix ID format and not email.
+InputValidator::InputValidator(QObject *parent)
+ : matrix_id_("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}")
+ , matrix_localpart_("[A-za-z0-9._%+-]{3,}")
+ , matrix_password_(".{8,}")
+ , server_domain_("(?!\\-)(?:[a-zA-Z\\d\\-]{0,62}[a-zA-Z\\d]\\.){1,126}(?!\\d+)[a-zA-Z\\d]{1,63}")
+{
+ id_ = new QRegExpValidator(matrix_id_, parent);
+ localpart_ = new QRegExpValidator(matrix_localpart_, parent);
+ password_ = new QRegExpValidator(matrix_password_, parent);
+ domain_ = new QRegExpValidator(server_domain_, parent);
+}
diff --git a/src/Login.cc b/src/Login.cc
new file mode 100644
index 00000000..f3b8e2f4
--- /dev/null
+++ b/src/Login.cc
@@ -0,0 +1,89 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonValue>
+
+#include "Deserializable.h"
+#include "Login.h"
+
+LoginRequest::LoginRequest()
+{
+}
+
+LoginRequest::LoginRequest(QString username, QString password)
+ : user_(username)
+ , password_(password)
+{
+}
+
+QByteArray LoginRequest::serialize()
+{
+ QJsonObject body{
+ {"type", "m.login.password"},
+ {"user", user_},
+ {"password", password_}};
+
+ return QJsonDocument(body).toJson(QJsonDocument::Compact);
+}
+
+void LoginRequest::setPassword(QString password)
+{
+ password_ = password;
+}
+
+void LoginRequest::setUser(QString username)
+{
+ user_ = username;
+}
+
+QString LoginResponse::getAccessToken()
+{
+ return access_token_;
+}
+
+QString LoginResponse::getHomeServer()
+{
+ return home_server_;
+}
+
+QString LoginResponse::getUserId()
+{
+ return user_id_;
+}
+
+void LoginResponse::deserialize(QJsonDocument data) throw(DeserializationException)
+{
+ if (!data.isObject())
+ throw DeserializationException("Login response is not a JSON object");
+
+ QJsonObject object = data.object();
+
+ if (object.value("access_token") == QJsonValue::Undefined)
+ throw DeserializationException("Login: missing access_token param");
+
+ if (object.value("home_server") == QJsonValue::Undefined)
+ throw DeserializationException("Login: missing home_server param");
+
+ if (object.value("user_id") == QJsonValue::Undefined)
+ throw DeserializationException("Login: missing user_id param");
+
+ access_token_ = object.value("access_token").toString();
+ home_server_ = object.value("home_server").toString();
+ user_id_ = object.value("user_id").toString();
+}
diff --git a/src/LoginPage.cc b/src/LoginPage.cc
new file mode 100644
index 00000000..68927c33
--- /dev/null
+++ b/src/LoginPage.cc
@@ -0,0 +1,147 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+
+#include "LoginPage.h"
+
+LoginPage::LoginPage(QWidget *parent)
+ : QWidget(parent)
+ , matrix_id_validator_(new InputValidator(parent))
+{
+ top_layout_ = new QVBoxLayout();
+
+ back_layout_ = new QHBoxLayout();
+ back_layout_->setSpacing(0);
+ back_layout_->setContentsMargins(5, 5, -1, -1);
+
+ back_button_ = new FlatButton(this);
+ back_button_->setMinimumSize(QSize(30, 30));
+ back_button_->setCursor(QCursor(Qt::PointingHandCursor));
+
+ QIcon icon;
+ icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off);
+
+ back_button_->setIcon(icon);
+ back_button_->setIconSize(QSize(24, 24));
+
+ back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
+ back_layout_->addStretch(1);
+
+ logo_layout_ = new QHBoxLayout();
+ logo_layout_->setContentsMargins(0, 20, 0, 20);
+ logo_ = new QLabel(this);
+ logo_->setText("nheko");
+ logo_->setStyleSheet("font-size: 22pt; font-weight: 400;");
+ logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
+
+ form_wrapper_ = new QHBoxLayout();
+ form_widget_ = new QWidget();
+ form_widget_->setMinimumSize(QSize(350, 200));
+
+ form_layout_ = new QVBoxLayout();
+ form_layout_->setSpacing(20);
+ form_layout_->setContentsMargins(0, 00, 0, 30);
+ form_widget_->setLayout(form_layout_);
+
+ form_wrapper_->addStretch(1);
+ form_wrapper_->addWidget(form_widget_);
+ form_wrapper_->addStretch(1);
+
+ username_input_ = new TextField();
+ username_input_->setLabel("Username");
+ username_input_->setInkColor("#577275");
+ username_input_->setBackgroundColor("#f9f9f9");
+
+ password_input_ = new TextField();
+ password_input_->setLabel("Password");
+ password_input_->setInkColor("#577275");
+ password_input_->setBackgroundColor("#f9f9f9");
+ password_input_->setEchoMode(QLineEdit::Password);
+
+ form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0);
+ form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0);
+
+ button_layout_ = new QHBoxLayout();
+ button_layout_->setSpacing(0);
+ button_layout_->setContentsMargins(0, 0, 0, 50);
+
+ login_button_ = new RaisedButton("LOGIN", this);
+ login_button_->setBackgroundColor(QColor("#171919"));
+ login_button_->setForegroundColor(QColor("#ebebeb"));
+ login_button_->setMinimumSize(350, 65);
+ login_button_->setCursor(QCursor(Qt::PointingHandCursor));
+ login_button_->setFontSize(17);
+ login_button_->setCornerRadius(3);
+
+ button_layout_->addStretch(1);
+ button_layout_->addWidget(login_button_);
+ button_layout_->addStretch(1);
+
+ error_label_ = new QLabel(this);
+ error_label_->setStyleSheet("color: #E22826; font-size: 11pt;");
+
+ top_layout_->addLayout(back_layout_);
+ top_layout_->addStretch(1);
+ top_layout_->addLayout(logo_layout_);
+ top_layout_->addLayout(form_wrapper_);
+ top_layout_->addStretch(1);
+ top_layout_->addLayout(button_layout_);
+ top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
+ top_layout_->addStretch(1);
+
+ connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
+ connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
+ connect(username_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
+ connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
+
+ username_input_->setValidator(matrix_id_validator_->id_);
+
+ setLayout(top_layout_);
+}
+
+void LoginPage::loginError(QString error)
+{
+ qWarning() << "Error Message: " << error;
+ error_label_->setText(error);
+}
+
+void LoginPage::onLoginButtonClicked()
+{
+ error_label_->setText("");
+
+ if (!username_input_->hasAcceptableInput()) {
+ loginError("Invalid Matrix ID");
+ } else if (password_input_->text().isEmpty()) {
+ loginError("Empty password");
+ } else {
+ QString user = username_input_->text().split("@").at(0);
+ QString home_server = username_input_->text().split("@").at(1);
+ QString password = password_input_->text();
+
+ emit userLogin(user, password, home_server);
+ }
+}
+
+void LoginPage::onBackButtonClicked()
+{
+ emit backButtonClicked();
+}
+
+LoginPage::~LoginPage()
+{
+}
diff --git a/src/MainWindow.cc b/src/MainWindow.cc
new file mode 100644
index 00000000..82976f23
--- /dev/null
+++ b/src/MainWindow.cc
@@ -0,0 +1,134 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "MainWindow.h"
+#include "ui_MainWindow.h"
+
+#include <QLayout>
+#include <QNetworkReply>
+#include <QSettings>
+
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent)
+ , ui_(new Ui::MainWindow)
+ , welcome_page_(new WelcomePage(parent))
+ , login_page_(new LoginPage(parent))
+ , register_page_(new RegisterPage(parent))
+ , chat_page_(new ChatPage(parent))
+ , matrix_client_(new MatrixClient("matrix.org", parent))
+{
+ ui_->setupUi(this);
+
+ // Initialize sliding widget manager.
+ sliding_stack_ = new SlidingStackWidget(this);
+ sliding_stack_->addWidget(welcome_page_);
+ sliding_stack_->addWidget(login_page_);
+ sliding_stack_->addWidget(register_page_);
+ sliding_stack_->addWidget(chat_page_);
+
+ setCentralWidget(sliding_stack_);
+
+ connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage()));
+ connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage()));
+
+ connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
+ connect(login_page_,
+ SIGNAL(userLogin(const QString &, const QString &, const QString &)),
+ this,
+ SLOT(matrixLogin(const QString &, const QString &, const QString &)));
+
+ connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
+ connect(register_page_,
+ SIGNAL(registerUser(const QString &, const QString &, const QString &)),
+ this,
+ SLOT(matrixRegister(const QString &, const QString &, const QString &)));
+
+ connect(matrix_client_, SIGNAL(loginError(QString)), login_page_, SLOT(loginError(QString)));
+ connect(matrix_client_,
+ SIGNAL(loginSuccess(QString, QString, QString)),
+ this,
+ SLOT(showChatPage(QString, QString, QString)));
+}
+
+void MainWindow::matrixLogin(const QString &username, const QString &password, const QString &home_server)
+{
+ qDebug() << "About to login into Matrix";
+ qDebug() << "Userame: " << username;
+
+ matrix_client_->setServer(home_server);
+ matrix_client_->login(username, password);
+}
+
+void MainWindow::showChatPage(QString userid, QString homeserver, QString token)
+{
+ QSettings settings;
+ settings.setValue("auth/access_token", token);
+ settings.setValue("auth/home_server", homeserver);
+ settings.setValue("auth/user_id", userid);
+
+ int index = sliding_stack_->getWidgetIndex(chat_page_);
+ sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT);
+
+ chat_page_->bootstrap(userid, homeserver, token);
+}
+
+void MainWindow::matrixRegister(const QString &username, const QString &password, const QString &server)
+{
+ qDebug() << "About to register to Matrix";
+ qDebug() << "Username: " << username << " Password: " << password << " Server: " << server;
+}
+
+void MainWindow::showWelcomePage()
+{
+ int index = sliding_stack_->getWidgetIndex(welcome_page_);
+
+ if (sliding_stack_->currentIndex() == sliding_stack_->getWidgetIndex(login_page_))
+ sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT);
+ else
+ sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT);
+}
+
+void MainWindow::showLoginPage()
+{
+ QSettings settings;
+
+ if (settings.contains("auth/access_token") &&
+ settings.contains("auth/home_server") &&
+ settings.contains("auth/user_id")) {
+ QString token = settings.value("auth/access_token").toString();
+ QString home_server = settings.value("auth/home_server").toString();
+ QString user_id = settings.value("auth/user_id").toString();
+
+ showChatPage(user_id, home_server, token);
+
+ return;
+ }
+
+ int index = sliding_stack_->getWidgetIndex(login_page_);
+ sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT);
+}
+
+void MainWindow::showRegisterPage()
+{
+ int index = sliding_stack_->getWidgetIndex(register_page_);
+ sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT);
+}
+
+MainWindow::~MainWindow()
+{
+ delete ui_;
+}
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
new file mode 100644
index 00000000..5510b6d9
--- /dev/null
+++ b/src/MatrixClient.cc
@@ -0,0 +1,365 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QSettings>
+#include <QUrl>
+#include <QUrlQuery>
+
+#include "Login.h"
+#include "MatrixClient.h"
+#include "Profile.h"
+
+MatrixClient::MatrixClient(QString server, QObject *parent)
+ : QNetworkAccessManager(parent)
+{
+ server_ = "https://" + server;
+ api_url_ = "/_matrix/client/r0";
+ token_ = "";
+
+ QSettings settings;
+ txn_id_ = settings.value("client/transaction_id", 1).toInt();
+
+ // FIXME: Other QNetworkAccessManagers use the finish handler.
+ connect(this, SIGNAL(finished(QNetworkReply *)), this, SLOT(onResponse(QNetworkReply *)));
+}
+
+MatrixClient::~MatrixClient()
+{
+}
+
+void MatrixClient::onVersionsResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ qDebug() << "Handling the versions response";
+
+ auto data = reply->readAll();
+ auto json = QJsonDocument::fromJson(data);
+
+ qDebug() << json;
+}
+
+void MatrixClient::onLoginResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ int status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status_code == 403) {
+ emit loginError("Wrong username or password");
+ return;
+ }
+
+ if (status_code == 404) {
+ emit loginError("Login endpoint was not found on the server");
+ return;
+ }
+
+ if (status_code != 200) {
+ qDebug() << "Login response: status code " << status_code;
+
+ if (status_code >= 400) {
+ qWarning() << "Login error: " << reply->errorString();
+ emit loginError("An unknown error occured. Please try again.");
+ return;
+ }
+ }
+
+ auto data = reply->readAll();
+ auto json = QJsonDocument::fromJson(data);
+
+ LoginResponse response;
+
+ try {
+ response.deserialize(json);
+ emit loginSuccess(response.getUserId(),
+ response.getHomeServer(),
+ response.getAccessToken());
+ } catch (DeserializationException &e) {
+ qWarning() << "Malformed JSON response" << e.what();
+ emit loginError("Malformed response. Possibly not a Matrix server");
+ }
+}
+
+void MatrixClient::onRegisterResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ qDebug() << "Handling the register response";
+}
+
+void MatrixClient::onGetOwnProfileResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status >= 400) {
+ qWarning() << reply->errorString();
+ return;
+ }
+
+ auto data = reply->readAll();
+ auto json = QJsonDocument::fromJson(data);
+
+ ProfileResponse response;
+
+ try {
+ response.deserialize(json);
+ emit getOwnProfileResponse(response.getAvatarUrl(), response.getDisplayName());
+ } catch (DeserializationException &e) {
+ qWarning() << "Profile malformed response" << e.what();
+ }
+}
+
+void MatrixClient::onInitialSyncResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status == 0 || status >= 400) {
+ qWarning() << reply->errorString();
+ return;
+ }
+
+ auto data = reply->readAll();
+
+ if (data.isEmpty())
+ return;
+
+ auto json = QJsonDocument::fromJson(data);
+
+ SyncResponse response;
+
+ try {
+ response.deserialize(json);
+ emit initialSyncCompleted(response);
+ } catch (DeserializationException &e) {
+ qWarning() << "Sync malformed response" << e.what();
+ }
+}
+
+void MatrixClient::onSyncResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status == 0 || status >= 400) {
+ qWarning() << reply->errorString();
+ return;
+ }
+
+ auto data = reply->readAll();
+
+ if (data.isEmpty())
+ return;
+
+ auto json = QJsonDocument::fromJson(data);
+
+ SyncResponse response;
+
+ try {
+ response.deserialize(json);
+ emit syncCompleted(response);
+ } catch (DeserializationException &e) {
+ qWarning() << "Sync malformed response" << e.what();
+ }
+}
+
+void MatrixClient::onSendTextMessageResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status == 0 || status >= 400) {
+ qWarning() << reply->errorString();
+ return;
+ }
+
+ auto data = reply->readAll();
+
+ if (data.isEmpty())
+ return;
+
+ auto json = QJsonDocument::fromJson(data);
+
+ if (!json.isObject()) {
+ qDebug() << "Send message response is not a JSON object";
+ return;
+ }
+
+ auto object = json.object();
+
+ if (!object.contains("event_id")) {
+ qDebug() << "SendTextMessage: missnig event_id from response";
+ return;
+ }
+
+ emit messageSent(object.value("event_id").toString(),
+ reply->property("txn_id").toInt());
+
+ incrementTransactionId();
+}
+
+void MatrixClient::onResponse(QNetworkReply *reply)
+{
+ switch (reply->property("endpoint").toInt()) {
+ case Endpoint::Versions:
+ onVersionsResponse(reply);
+ break;
+ case Endpoint::Login:
+ onLoginResponse(reply);
+ break;
+ case Endpoint::Register:
+ onRegisterResponse(reply);
+ case Endpoint::GetOwnProfile:
+ onGetOwnProfileResponse(reply);
+ case Endpoint::InitialSync:
+ onInitialSyncResponse(reply);
+ case Endpoint::Sync:
+ onSyncResponse(reply);
+ case Endpoint::SendTextMessage:
+ onSendTextMessageResponse(reply);
+ default:
+ break;
+ }
+}
+
+void MatrixClient::login(const QString &username, const QString &password)
+{
+ QUrl endpoint(server_);
+ endpoint.setPath(api_url_ + "/login");
+
+ QNetworkRequest request(endpoint);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ LoginRequest body(username, password);
+
+ QNetworkReply *reply = post(request, body.serialize());
+ reply->setProperty("endpoint", Endpoint::Login);
+}
+
+void MatrixClient::sync()
+{
+ QJsonObject filter{{"room",
+ QJsonObject{{"ephemeral", QJsonObject{{"limit", 0}}}}},
+ {"presence", QJsonObject{{"limit", 0}}}};
+
+ QUrlQuery query;
+ query.addQueryItem("set_presence", "online");
+ query.addQueryItem("filter", QJsonDocument(filter).toJson(QJsonDocument::Compact));
+ query.addQueryItem("access_token", token_);
+
+ if (next_batch_.isEmpty()) {
+ qDebug() << "Sync requires a valid next_batch token. Initial sync should be performed.";
+ return;
+ }
+
+ query.addQueryItem("since", next_batch_);
+
+ QUrl endpoint(server_);
+ endpoint.setPath(api_url_ + "/sync");
+ endpoint.setQuery(query);
+
+ QNetworkRequest request(QString(endpoint.toEncoded()));
+
+ QNetworkReply *reply = get(request);
+ reply->setProperty("endpoint", Endpoint::Sync);
+}
+
+void MatrixClient::sendTextMessage(QString roomid, QString msg)
+{
+ QUrlQuery query;
+ query.addQueryItem("access_token", token_);
+
+ QUrl endpoint(server_);
+ endpoint.setPath(api_url_ + QString("/rooms/%1/send/m.room.message/%2").arg(roomid).arg(txn_id_));
+ endpoint.setQuery(query);
+
+ QJsonObject body{
+ {"msgtype", "m.text"},
+ {"body", msg}};
+
+ QNetworkRequest request(QString(endpoint.toEncoded()));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ QNetworkReply *reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
+
+ reply->setProperty("endpoint", Endpoint::SendTextMessage);
+ reply->setProperty("txn_id", txn_id_);
+}
+
+void MatrixClient::initialSync()
+{
+ QJsonObject filter{{"room",
+ QJsonObject{{"timeline", QJsonObject{{"limit", 70}}},
+ {"ephemeral", QJsonObject{{"limit", 0}}}}},
+ {"presence", QJsonObject{{"limit", 0}}}};
+
+ QUrlQuery query;
+ query.addQueryItem("full_state", "true");
+ query.addQueryItem("set_presence", "online");
+ query.addQueryItem("filter", QJsonDocument(filter).toJson(QJsonDocument::Compact));
+ query.addQueryItem("access_token", token_);
+
+ QUrl endpoint(server_);
+ endpoint.setPath(api_url_ + "/sync");
+ endpoint.setQuery(query);
+
+ QNetworkRequest request(QString(endpoint.toEncoded()));
+
+ QNetworkReply *reply = get(request);
+ reply->setProperty("endpoint", Endpoint::InitialSync);
+}
+
+void MatrixClient::versions()
+{
+ QUrl endpoint(server_);
+ endpoint.setPath("/_matrix/client/versions");
+
+ QNetworkRequest request(endpoint);
+
+ QNetworkReply *reply = get(request);
+ reply->setProperty("endpoint", Endpoint::Versions);
+}
+
+void MatrixClient::getOwnProfile()
+{
+ // FIXME: Remove settings from the matrix client. The class should store the user's matrix ID.
+ QSettings settings;
+ auto userid = settings.value("auth/user_id", "").toString();
+
+ QUrlQuery query;
+ query.addQueryItem("access_token", token_);
+
+ QUrl endpoint(server_);
+ endpoint.setPath(api_url_ + "/profile/" + userid);
+ endpoint.setQuery(query);
+
+ QNetworkRequest request(QString(endpoint.toEncoded()));
+
+ QNetworkReply *reply = get(request);
+ reply->setProperty("endpoint", Endpoint::GetOwnProfile);
+}
diff --git a/src/Profile.cc b/src/Profile.cc
new file mode 100644
index 00000000..aa556370
--- /dev/null
+++ b/src/Profile.cc
@@ -0,0 +1,50 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QJsonObject>
+#include <QJsonValue>
+#include <QUrl>
+
+#include "Deserializable.h"
+#include "Profile.h"
+
+QUrl ProfileResponse::getAvatarUrl()
+{
+ return avatar_url_;
+}
+
+QString ProfileResponse::getDisplayName()
+{
+ return display_name_;
+}
+
+void ProfileResponse::deserialize(QJsonDocument data) throw(DeserializationException)
+{
+ if (!data.isObject())
+ throw DeserializationException("Profile response is not a JSON object");
+
+ QJsonObject object = data.object();
+
+ if (object.value("avatar_url") == QJsonValue::Undefined)
+ throw DeserializationException("Profile: missing avatar_url param");
+
+ if (object.value("displayname") == QJsonValue::Undefined)
+ throw DeserializationException("Profile: missing displayname param");
+
+ avatar_url_ = QUrl(object.value("avatar_url").toString());
+ display_name_ = object.value("displayname").toString();
+}
diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc
new file mode 100644
index 00000000..fcb43b86
--- /dev/null
+++ b/src/RegisterPage.cc
@@ -0,0 +1,166 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+#include <QToolTip>
+
+#include "RegisterPage.h"
+
+RegisterPage::RegisterPage(QWidget *parent)
+ : QWidget(parent)
+ , validator_(new InputValidator(parent))
+{
+ top_layout_ = new QVBoxLayout();
+
+ back_layout_ = new QHBoxLayout();
+ back_layout_->setSpacing(0);
+ back_layout_->setContentsMargins(5, 5, -1, -1);
+
+ back_button_ = new FlatButton(this);
+ back_button_->setMinimumSize(QSize(30, 30));
+ back_button_->setCursor(QCursor(Qt::PointingHandCursor));
+
+ QIcon icon;
+ icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off);
+
+ back_button_->setIcon(icon);
+ back_button_->setIconSize(QSize(24, 24));
+
+ back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
+ back_layout_->addStretch(1);
+
+ logo_layout_ = new QHBoxLayout();
+ logo_layout_->setContentsMargins(0, 20, 0, 20);
+ logo_ = new QLabel(this);
+ logo_->setText("nheko");
+ logo_->setStyleSheet("font-size: 22pt; font-weight: 400;");
+ logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
+
+ form_wrapper_ = new QHBoxLayout();
+ form_widget_ = new QWidget();
+ form_widget_->setMinimumSize(QSize(350, 300));
+
+ form_layout_ = new QVBoxLayout();
+ form_layout_->setSpacing(20);
+ form_layout_->setContentsMargins(0, 00, 0, 60);
+ form_widget_->setLayout(form_layout_);
+
+ form_wrapper_->addStretch(1);
+ form_wrapper_->addWidget(form_widget_);
+ form_wrapper_->addStretch(1);
+
+ username_input_ = new TextField();
+ username_input_->setLabel("Username");
+ username_input_->setInkColor("#577275");
+ username_input_->setBackgroundColor("#f9f9f9");
+
+ password_input_ = new TextField();
+ password_input_->setLabel("Password");
+ password_input_->setInkColor("#577275");
+ password_input_->setBackgroundColor("#f9f9f9");
+ password_input_->setEchoMode(QLineEdit::Password);
+
+ password_confirmation_ = new TextField();
+ password_confirmation_->setLabel("Password confirmation");
+ password_confirmation_->setInkColor("#577275");
+ password_confirmation_->setBackgroundColor("#f9f9f9");
+ password_confirmation_->setEchoMode(QLineEdit::Password);
+
+ server_input_ = new TextField();
+ server_input_->setLabel("Home Server");
+ server_input_->setInkColor("#577275");
+ server_input_->setBackgroundColor("#f9f9f9");
+
+ form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0);
+ form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0);
+ form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, 0);
+ form_layout_->addWidget(server_input_, Qt::AlignHCenter, 0);
+
+ button_layout_ = new QHBoxLayout();
+ button_layout_->setSpacing(0);
+ button_layout_->setContentsMargins(0, 0, 0, 50);
+
+ register_button_ = new RaisedButton("REGISTER", this);
+ register_button_->setBackgroundColor(QColor("#171919"));
+ register_button_->setForegroundColor(QColor("#ebebeb"));
+ register_button_->setMinimumSize(350, 65);
+ register_button_->setCursor(QCursor(Qt::PointingHandCursor));
+ register_button_->setFontSize(17);
+ register_button_->setCornerRadius(3);
+
+ button_layout_->addStretch(1);
+ button_layout_->addWidget(register_button_);
+ button_layout_->addStretch(1);
+
+ top_layout_->addLayout(back_layout_);
+ top_layout_->addStretch(1);
+ top_layout_->addLayout(logo_layout_);
+ top_layout_->addLayout(form_wrapper_);
+ top_layout_->addStretch(2);
+ top_layout_->addLayout(button_layout_);
+ top_layout_->addStretch(1);
+
+ connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
+ connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
+
+ connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+ connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+ connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+ connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
+
+ username_input_->setValidator(validator_->localpart_);
+ password_input_->setValidator(validator_->password_);
+ server_input_->setValidator(validator_->domain_);
+
+ setLayout(top_layout_);
+}
+
+void RegisterPage::onBackButtonClicked()
+{
+ emit backButtonClicked();
+}
+
+void RegisterPage::onRegisterButtonClicked()
+{
+ if (!username_input_->hasAcceptableInput()) {
+ QString text("Invalid username");
+ QPoint point = username_input_->mapToGlobal(username_input_->rect().topRight());
+ QToolTip::showText(point, text);
+ } else if (!password_input_->hasAcceptableInput()) {
+ QString text("Password is not long enough");
+ QPoint point = password_input_->mapToGlobal(password_input_->rect().topRight());
+ QToolTip::showText(point, text);
+ } else if (password_input_->text() != password_confirmation_->text()) {
+ QString text("Passwords don't match");
+ QPoint point = password_confirmation_->mapToGlobal(password_confirmation_->rect().topRight());
+ QToolTip::showText(point, text);
+ } else if (!server_input_->hasAcceptableInput()) {
+ QString text("Invalid server name");
+ QPoint point = server_input_->mapToGlobal(server_input_->rect().topRight());
+ QToolTip::showText(point, text);
+ } else {
+ QString username = username_input_->text();
+ QString password = password_input_->text();
+ QString server = server_input_->text();
+
+ emit registerUser(username, password, server);
+ }
+}
+
+RegisterPage::~RegisterPage()
+{
+}
diff --git a/src/RoomInfo.cc b/src/RoomInfo.cc
new file mode 100644
index 00000000..f8a7c56a
--- /dev/null
+++ b/src/RoomInfo.cc
@@ -0,0 +1,71 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "RoomInfo.h"
+
+RoomInfo::RoomInfo()
+ : name_("")
+ , topic_("")
+{
+}
+
+RoomInfo::RoomInfo(QString name, QString topic, QUrl avatar_url)
+ : name_(name)
+ , topic_(topic)
+ , avatar_url_(avatar_url)
+{
+}
+
+QString RoomInfo::id() const
+{
+ return id_;
+}
+
+QString RoomInfo::name() const
+{
+ return name_;
+}
+
+QString RoomInfo::topic() const
+{
+ return topic_;
+}
+
+QUrl RoomInfo::avatarUrl() const
+{
+ return avatar_url_;
+}
+
+void RoomInfo::setAvatarUrl(const QUrl &url)
+{
+ avatar_url_ = url;
+}
+
+void RoomInfo::setId(const QString &id)
+{
+ id_ = id;
+}
+
+void RoomInfo::setName(const QString &name)
+{
+ name_ = name;
+}
+
+void RoomInfo::setTopic(const QString &topic)
+{
+ topic_ = topic;
+}
diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc
new file mode 100644
index 00000000..dedae3fd
--- /dev/null
+++ b/src/RoomInfoListItem.cc
@@ -0,0 +1,138 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+
+#include "Ripple.h"
+#include "RoomInfo.h"
+#include "RoomInfoListItem.h"
+
+RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent)
+ : QWidget(parent)
+ , info_(info)
+ , is_pressed_(false)
+ , max_height_(65)
+{
+ normal_style_ =
+ "QWidget { background-color: #5d6565; color: #ebebeb;"
+ "border-bottom: 1px solid #171919;}"
+ "QLabel { border: none; }";
+
+ pressed_style_ =
+ "QWidget { background-color: #577275; color: #ebebeb;"
+ "border-bottom: 1px solid #171919;}"
+ "QLabel { border: none; }";
+
+ setStyleSheet(normal_style_);
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+ setAutoFillBackground(true);
+
+ setMinimumSize(parent->width(), max_height_);
+
+ topLayout_ = new QHBoxLayout(this);
+ topLayout_->setSpacing(0);
+ topLayout_->setMargin(0);
+
+ avatarWidget_ = new QWidget(this);
+ avatarWidget_->setMaximumSize(max_height_, max_height_);
+ textWidget_ = new QWidget(this);
+
+ avatarLayout_ = new QVBoxLayout(avatarWidget_);
+ avatarLayout_->setSpacing(0);
+ avatarLayout_->setContentsMargins(0, 5, 0, 5);
+
+ textLayout_ = new QVBoxLayout(textWidget_);
+ textLayout_->setSpacing(0);
+ textLayout_->setContentsMargins(0, 5, 0, 5);
+
+ roomAvatar_ = new Avatar(avatarWidget_);
+ roomAvatar_->setLetter(QChar(info_.name()[0]));
+ avatarLayout_->addWidget(roomAvatar_);
+
+ roomName_ = new QLabel(textWidget_);
+ roomName_->setText(info_.name());
+ roomName_->setMaximumSize(230, max_height_ / 2);
+ roomName_->setStyleSheet("font-weight: 500; font-size: 11.5pt");
+ roomName_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
+
+ roomTopic_ = new QLabel(textWidget_);
+ roomTopic_->setText(info_.topic());
+ roomTopic_->setMaximumSize(230, max_height_ / 2);
+ roomTopic_->setStyleSheet("color: #c9c9c9; font-size: 10pt");
+ roomTopic_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
+
+ textLayout_->addWidget(roomName_);
+ textLayout_->addWidget(roomTopic_);
+
+ topLayout_->addWidget(avatarWidget_);
+ topLayout_->addWidget(textWidget_);
+
+ setElidedText(roomName_, info_.name(), 220);
+ setElidedText(roomTopic_, info_.topic(), 220);
+
+ QPainterPath path;
+ path.addRoundedRect(rect(), 0, 0);
+
+ ripple_overlay_ = new RippleOverlay(this);
+ ripple_overlay_->setClipPath(path);
+ ripple_overlay_->setClipping(true);
+
+ setLayout(topLayout_);
+}
+
+void RoomInfoListItem::setPressedState(bool state)
+{
+ if (!is_pressed_ && state) {
+ is_pressed_ = state;
+ setStyleSheet(pressed_style_);
+ } else if (is_pressed_ && !state) {
+ is_pressed_ = state;
+ setStyleSheet(normal_style_);
+ }
+}
+
+void RoomInfoListItem::mousePressEvent(QMouseEvent *event)
+{
+ emit clicked(info_);
+
+ setPressedState(true);
+
+ // Ripple on mouse position by default.
+ QPoint pos = event->pos();
+ qreal radiusEndValue = static_cast<qreal>(width()) / 2;
+
+ Ripple *ripple = new Ripple(pos);
+
+ ripple->setRadiusEndValue(radiusEndValue);
+ ripple->setOpacityStartValue(0.35);
+ ripple->setColor(QColor("#171919"));
+ ripple->radiusAnimation()->setDuration(300);
+ ripple->opacityAnimation()->setDuration(500);
+
+ ripple_overlay_->addRipple(ripple);
+}
+
+void RoomInfoListItem::setElidedText(QLabel *label, QString text, int width)
+{
+ QFontMetrics metrics(label->font());
+ QString elidedText = metrics.elidedText(text, Qt::ElideRight, width);
+ label->setText(elidedText);
+}
+
+RoomInfoListItem::~RoomInfoListItem()
+{
+}
diff --git a/src/RoomList.cc b/src/RoomList.cc
new file mode 100644
index 00000000..1e147a48
--- /dev/null
+++ b/src/RoomList.cc
@@ -0,0 +1,119 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ui_RoomList.h"
+
+#include <QDebug>
+#include <QJsonArray>
+#include <QLabel>
+
+#include "RoomInfoListItem.h"
+#include "RoomList.h"
+#include "Sync.h"
+
+RoomList::RoomList(QWidget *parent)
+ : QWidget(parent)
+ , ui(new Ui::RoomList)
+{
+ ui->setupUi(this);
+}
+
+RoomList::~RoomList()
+{
+ delete ui;
+}
+
+RoomInfo RoomList::extractRoomInfo(const State &room_state)
+{
+ RoomInfo info;
+
+ auto events = room_state.events();
+
+ for (int i = 0; i < events.count(); i++) {
+ if (events[i].type() == "m.room.name") {
+ info.setName(events[i].content().value("name").toString());
+ } else if (events[i].type() == "m.room.topic") {
+ info.setTopic(events[i].content().value("topic").toString());
+ } else if (events[i].type() == "m.room.avatar") {
+ info.setAvatarUrl(QUrl(events[i].content().value("url").toString()));
+ } else if (events[i].type() == "m.room.canonical_alias") {
+ if (info.name().isEmpty())
+ info.setName(events[i].content().value("alias").toString());
+ }
+ }
+
+ return info;
+}
+
+void RoomList::setInitialRooms(const Rooms &rooms)
+{
+ available_rooms_.clear();
+
+ for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
+ RoomInfo info = RoomList::extractRoomInfo(it.value().state());
+ info.setId(it.key());
+
+ if (info.name().isEmpty())
+ continue;
+
+ if (!info.avatarUrl().isEmpty())
+ emit fetchRoomAvatar(info.id(), info.avatarUrl());
+
+ RoomInfoListItem *room_item = new RoomInfoListItem(info, ui->scrollArea);
+ connect(room_item,
+ SIGNAL(clicked(const RoomInfo &)),
+ this,
+ SLOT(highlightSelectedRoom(const RoomInfo &)));
+
+ available_rooms_.insert(it.key(), room_item);
+
+ ui->scrollVerticalLayout->addWidget(room_item);
+ }
+
+ // TODO: Move this into its own function.
+ auto first_room = available_rooms_.first();
+ first_room->setPressedState(true);
+ emit roomChanged(first_room->info());
+
+ ui->scrollVerticalLayout->addStretch(1);
+}
+
+void RoomList::highlightSelectedRoom(const RoomInfo &info)
+{
+ emit roomChanged(info);
+
+ for (auto it = available_rooms_.constBegin(); it != available_rooms_.constEnd(); it++) {
+ if (it.key() != info.id())
+ it.value()->setPressedState(false);
+ }
+}
+
+void RoomList::updateRoomAvatar(const QString &roomid, const QImage &avatar_image)
+{
+ if (!available_rooms_.contains(roomid)) {
+ qDebug() << "Avatar update on non existent room" << roomid;
+ return;
+ }
+
+ auto list_item = available_rooms_.value(roomid);
+ list_item->setAvatar(avatar_image);
+}
+
+void RoomList::appendRoom(QString name)
+{
+ Q_UNUSED(name);
+}
diff --git a/src/SlidingStackWidget.cc b/src/SlidingStackWidget.cc
new file mode 100644
index 00000000..c4d2f7cf
--- /dev/null
+++ b/src/SlidingStackWidget.cc
@@ -0,0 +1,151 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "SlidingStackWidget.h"
+
+SlidingStackWidget::SlidingStackWidget(QWidget *parent)
+ : QStackedWidget(parent)
+{
+ window_ = parent;
+
+ if (parent == Q_NULLPTR) {
+ qDebug() << "Using nullptr for parent";
+ window_ = this;
+ }
+
+ current_position_ = QPoint(0, 0);
+ speed_ = 400;
+ now_ = 0;
+ next_ = 0;
+ active_ = false;
+ animation_type_ = QEasingCurve::InOutCirc;
+}
+
+SlidingStackWidget::~SlidingStackWidget()
+{
+}
+
+void SlidingStackWidget::slideInNext()
+{
+ int now = currentIndex();
+
+ if (now < count() - 1)
+ slideInIndex(now + 1);
+}
+
+void SlidingStackWidget::slideInPrevious()
+{
+ int now = currentIndex();
+
+ if (now > 0)
+ slideInIndex(now - 1);
+}
+
+void SlidingStackWidget::slideInIndex(int index, AnimationDirection direction)
+{
+ // Take into consideration possible index overflow/undeflow.
+ if (index > count() - 1) {
+ direction = AnimationDirection::RIGHT_TO_LEFT;
+ index = index % count();
+ } else if (index < 0) {
+ direction = AnimationDirection::LEFT_TO_RIGHT;
+ index = (index + count()) % count();
+ }
+
+ slideInWidget(widget(index), direction);
+}
+
+void SlidingStackWidget::slideInWidget(QWidget *next_widget, AnimationDirection direction)
+{
+ // If an animation is currenlty executing we should wait for it to finish before
+ // another transition can start.
+ if (active_)
+ return;
+
+ active_ = true;
+
+ int now = currentIndex();
+ int next = indexOf(next_widget);
+
+ if (now == next) {
+ active_ = false;
+ return;
+ }
+
+ int offset_x = frameRect().width();
+
+ next_widget->setGeometry(0, 0, offset_x, 0);
+
+ if (direction == AnimationDirection::LEFT_TO_RIGHT) {
+ offset_x = -offset_x;
+ }
+
+ QPoint pnext = next_widget->pos();
+ QPoint pnow = widget(now)->pos();
+ current_position_ = pnow;
+
+ // Reposition the next widget outside of the display area.
+ next_widget->move(pnext.x() - offset_x, pnext.y());
+
+ // Make the widget visible.
+ next_widget->show();
+ next_widget->raise();
+
+ // Animate both the next and now widget.
+ QPropertyAnimation *animation_now = new QPropertyAnimation(widget(now), "pos");
+
+ animation_now->setDuration(speed_);
+ animation_now->setEasingCurve(animation_type_);
+ animation_now->setStartValue(QPoint(pnow.x(), pnow.y()));
+ animation_now->setEndValue(QPoint(pnow.x() + offset_x, pnow.y()));
+
+ QPropertyAnimation *animation_next = new QPropertyAnimation(next_widget, "pos");
+
+ animation_next->setDuration(speed_);
+ animation_next->setEasingCurve(animation_type_);
+ animation_next->setStartValue(QPoint(pnext.x() - offset_x, pnext.y()));
+ animation_next->setEndValue(QPoint(pnext.x(), pnext.y()));
+
+ QParallelAnimationGroup *animation_group = new QParallelAnimationGroup;
+
+ animation_group->addAnimation(animation_now);
+ animation_group->addAnimation(animation_next);
+
+ connect(animation_group, SIGNAL(finished()), this, SLOT(onAnimationFinished()));
+
+ next_ = next;
+ now_ = now;
+ animation_group->start();
+}
+
+void SlidingStackWidget::onAnimationFinished()
+{
+ setCurrentIndex(next_);
+
+ // The old widget is no longer necessary so we can hide it and
+ // move it back to its original position.
+ widget(now_)->hide();
+ widget(now_)->move(current_position_);
+
+ active_ = false;
+ emit animationFinished();
+}
+
+int SlidingStackWidget::getWidgetIndex(QWidget *widget)
+{
+ return indexOf(widget);
+}
diff --git a/src/Sync.cc b/src/Sync.cc
new file mode 100644
index 00000000..3ba6d220
--- /dev/null
+++ b/src/Sync.cc
@@ -0,0 +1,290 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonValue>
+
+#include "Deserializable.h"
+#include "Sync.h"
+
+QString SyncResponse::nextBatch() const
+{
+ return next_batch_;
+}
+
+void SyncResponse::deserialize(QJsonDocument data) throw(DeserializationException)
+{
+ if (!data.isObject())
+ throw DeserializationException("Sync response is not a JSON object");
+
+ QJsonObject object = data.object();
+
+ if (object.value("next_batch") == QJsonValue::Undefined)
+ throw DeserializationException("Sync: missing next_batch parameter");
+
+ if (object.value("rooms") == QJsonValue::Undefined)
+ throw DeserializationException("Sync: missing rooms parameter");
+
+ rooms_.deserialize(object.value("rooms"));
+ next_batch_ = object.value("next_batch").toString();
+}
+
+Rooms SyncResponse::rooms() const
+{
+ return rooms_;
+}
+
+QMap<QString, JoinedRoom> Rooms::join() const
+{
+ return join_;
+}
+
+void Rooms::deserialize(QJsonValue data) throw(DeserializationException)
+{
+ if (!data.isObject())
+ throw DeserializationException("Rooms value is not a JSON object");
+
+ QJsonObject object = data.toObject();
+
+ if (!object.contains("join"))
+ throw DeserializationException("rooms/join is missing");
+
+ if (!object.contains("invite"))
+ throw DeserializationException("rooms/invite is missing");
+
+ if (!object.contains("leave"))
+ throw DeserializationException("rooms/leave is missing");
+
+ if (!object.value("join").isObject())
+ throw DeserializationException("rooms/join must be a JSON object");
+
+ if (!object.value("invite").isObject())
+ throw DeserializationException("rooms/invite must be a JSON object");
+
+ if (!object.value("leave").isObject())
+ throw DeserializationException("rooms/leave must be a JSON object");
+
+ auto join = object.value("join").toObject();
+
+ for (auto it = join.constBegin(); it != join.constEnd(); it++) {
+ JoinedRoom tmp_room;
+
+ try {
+ tmp_room.deserialize(it.value());
+ join_.insert(it.key(), tmp_room);
+ } catch (DeserializationException &e) {
+ qWarning() << e.what();
+ qWarning() << "Skipping malformed object for room" << it.key();
+ }
+ }
+}
+
+State JoinedRoom::state() const
+{
+ return state_;
+}
+
+Timeline JoinedRoom::timeline() const
+{
+ return timeline_;
+}
+
+void JoinedRoom::deserialize(QJsonValue data) throw(DeserializationException)
+{
+ if (!data.isObject())
+ throw DeserializationException("JoinedRoom is not a JSON object");
+
+ QJsonObject object = data.toObject();
+
+ if (!object.contains("state"))
+ throw DeserializationException("join/state is missing");
+
+ if (!object.contains("timeline"))
+ throw DeserializationException("join/timeline is missing");
+
+ if (!object.contains("account_data"))
+ throw DeserializationException("join/account_data is missing");
+
+ if (!object.contains("unread_notifications"))
+ throw DeserializationException("join/unread_notifications is missing");
+
+ if (!object.value("state").isObject())
+ throw DeserializationException("join/state should be an object");
+
+ QJsonObject state = object.value("state").toObject();
+
+ if (!state.contains("events"))
+ throw DeserializationException("join/state/events is missing");
+
+ state_.deserialize(state.value("events"));
+ timeline_.deserialize(object.value("timeline"));
+}
+
+QJsonObject Event::content() const
+{
+ return content_;
+}
+
+QJsonObject Event::unsigned_content() const
+{
+ return unsigned_;
+}
+
+QString Event::sender() const
+{
+ return sender_;
+}
+
+QString Event::state_key() const
+{
+ return state_key_;
+}
+
+QString Event::type() const
+{
+ return type_;
+}
+
+QString Event::eventId() const
+{
+ return event_id_;
+}
+
+uint64_t Event::timestamp() const
+{
+ return origin_server_ts_;
+}
+
+void Event::deserialize(QJsonValue data) throw(DeserializationException)
+{
+ if (!data.isObject())
+ throw DeserializationException("Event is not a JSON object");
+
+ QJsonObject object = data.toObject();
+
+ if (!object.contains("content"))
+ throw DeserializationException("event/content is missing");
+
+ if (!object.contains("unsigned"))
+ throw DeserializationException("event/content is missing");
+
+ if (!object.contains("sender"))
+ throw DeserializationException("event/sender is missing");
+
+ if (!object.contains("event_id"))
+ throw DeserializationException("event/event_id is missing");
+
+ // TODO: Make this optional
+ /* if (!object.contains("state_key")) */
+ /* throw DeserializationException("event/state_key is missing"); */
+
+ if (!object.contains("type"))
+ throw DeserializationException("event/type is missing");
+
+ if (!object.contains("origin_server_ts"))
+ throw DeserializationException("event/origin_server_ts is missing");
+
+ content_ = object.value("content").toObject();
+ unsigned_ = object.value("unsigned").toObject();
+
+ sender_ = object.value("sender").toString();
+ state_key_ = object.value("state_key").toString();
+ type_ = object.value("type").toString();
+ event_id_ = object.value("event_id").toString();
+
+ origin_server_ts_ = object.value("origin_server_ts").toDouble();
+}
+
+QList<Event> State::events() const
+{
+ return events_;
+}
+
+void State::deserialize(QJsonValue data) throw(DeserializationException)
+{
+ if (!data.isArray())
+ throw DeserializationException("State is not a JSON array");
+
+ QJsonArray event_array = data.toArray();
+
+ for (int i = 0; i < event_array.count(); i++) {
+ Event event;
+
+ try {
+ event.deserialize(event_array.at(i));
+ events_.push_back(event);
+ } catch (DeserializationException &e) {
+ qWarning() << e.what();
+ qWarning() << "Skipping malformed state event";
+ }
+ }
+}
+
+QList<Event> Timeline::events() const
+{
+ return events_;
+}
+
+QString Timeline::previousBatch() const
+{
+ return prev_batch_;
+}
+
+bool Timeline::limited() const
+{
+ return limited_;
+}
+
+void Timeline::deserialize(QJsonValue data) throw(DeserializationException)
+{
+ if (!data.isObject())
+ throw DeserializationException("Timeline is not a JSON object");
+
+ auto object = data.toObject();
+
+ if (!object.contains("events"))
+ throw DeserializationException("timeline/events is missing");
+
+ if (!object.contains("prev_batch"))
+ throw DeserializationException("timeline/prev_batch is missing");
+
+ if (!object.contains("limited"))
+ throw DeserializationException("timeline/limited is missing");
+
+ prev_batch_ = object.value("prev_batch").toString();
+ limited_ = object.value("limited").toBool();
+
+ if (!object.value("events").isArray())
+ throw DeserializationException("timeline/events is not a JSON array");
+
+ auto timeline_events = object.value("events").toArray();
+
+ for (int i = 0; i < timeline_events.count(); i++) {
+ Event event;
+
+ try {
+ event.deserialize(timeline_events.at(i));
+ events_.push_back(event);
+ } catch (DeserializationException &e) {
+ qWarning() << e.what();
+ qWarning() << "Skipping malformed timeline event";
+ }
+ }
+}
diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc
new file mode 100644
index 00000000..ec92e77d
--- /dev/null
+++ b/src/TextInputWidget.cc
@@ -0,0 +1,91 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+#include <QPainter>
+#include <QStyleOption>
+
+#include "TextInputWidget.h"
+
+TextInputWidget::TextInputWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+ setCursor(Qt::ArrowCursor);
+ setStyleSheet("background-color: #171919; height: 45px;");
+
+ top_layout_ = new QHBoxLayout();
+ top_layout_->setSpacing(6);
+ top_layout_->setContentsMargins(6, 0, 0, 0);
+
+ send_file_button_ = new FlatButton(this);
+ send_file_button_->setCursor(Qt::PointingHandCursor);
+
+ QIcon send_file_icon;
+ send_file_icon.addFile(":/icons/icons/clip-dark.png", QSize(), QIcon::Normal, QIcon::Off);
+ send_file_button_->setForegroundColor(QColor("#577275"));
+ send_file_button_->setIcon(send_file_icon);
+ send_file_button_->setIconSize(QSize(24, 24));
+
+ input_ = new QLineEdit(this);
+ input_->setPlaceholderText("Write a message...");
+ input_->setStyleSheet("color: #ebebeb; font-size: 10pt; border-radius: 0; padding: 2px; margin-bottom: 4px;");
+
+ send_message_button_ = new FlatButton(this);
+ send_message_button_->setCursor(Qt::PointingHandCursor);
+ send_message_button_->setForegroundColor(QColor("#577275"));
+
+ QIcon send_message_icon;
+ send_message_icon.addFile(":/icons/icons/share-dark.png", QSize(), QIcon::Normal, QIcon::Off);
+ send_message_button_->setIcon(send_message_icon);
+ send_message_button_->setIconSize(QSize(24, 24));
+
+ top_layout_->addWidget(send_file_button_);
+ top_layout_->addWidget(input_);
+ top_layout_->addWidget(send_message_button_);
+
+ setLayout(top_layout_);
+
+ connect(send_message_button_, SIGNAL(clicked()), this, SLOT(onSendButtonClicked()));
+ connect(input_, SIGNAL(returnPressed()), send_message_button_, SIGNAL(clicked()));
+}
+
+void TextInputWidget::onSendButtonClicked()
+{
+ auto msg_text = input_->text();
+
+ if (msg_text.isEmpty())
+ return;
+
+ emit sendTextMessage(msg_text);
+ input_->clear();
+}
+
+void TextInputWidget::paintEvent(QPaintEvent *event)
+{
+ Q_UNUSED(event);
+
+ QStyleOption option;
+ option.initFrom(this);
+
+ QPainter painter(this);
+ style()->drawPrimitive(QStyle::PE_Widget, &option, &painter, this);
+}
+
+TextInputWidget::~TextInputWidget()
+{
+}
diff --git a/src/TopRoomBar.cc b/src/TopRoomBar.cc
new file mode 100644
index 00000000..7e390bdf
--- /dev/null
+++ b/src/TopRoomBar.cc
@@ -0,0 +1,93 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QStyleOption>
+
+#include "TopRoomBar.h"
+
+TopRoomBar::TopRoomBar(QWidget *parent)
+ : QWidget(parent)
+{
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+ setMinimumSize(QSize(0, 70));
+ setStyleSheet("background-color: #171919; color: #ebebeb;");
+
+ top_layout_ = new QHBoxLayout();
+ top_layout_->setSpacing(10);
+ top_layout_->setContentsMargins(10, 10, 0, 10);
+
+ avatar_ = new Avatar(this);
+ avatar_->setLetter(QChar('?'));
+ avatar_->setBackgroundColor(QColor("#ebebeb"));
+ avatar_->setSize(45);
+
+ text_layout_ = new QVBoxLayout();
+ text_layout_->setSpacing(0);
+ text_layout_->setContentsMargins(0, 0, 0, 0);
+
+ name_label_ = new QLabel(this);
+ name_label_->setStyleSheet("font-size: 11pt;");
+
+ topic_label_ = new QLabel(this);
+ topic_label_->setStyleSheet("font-size: 10pt; color: #6c7278;");
+
+ text_layout_->addWidget(name_label_);
+ text_layout_->addWidget(topic_label_);
+
+ settings_button_ = new FlatButton(this);
+ settings_button_->setForegroundColor(QColor("#ebebeb"));
+ settings_button_->setCursor(QCursor(Qt::PointingHandCursor));
+ settings_button_->setStyleSheet("width: 30px; height: 30px;");
+
+ QIcon settings_icon;
+ settings_icon.addFile(":/icons/icons/cog.png", QSize(), QIcon::Normal, QIcon::Off);
+ settings_button_->setIcon(settings_icon);
+ settings_button_->setIconSize(QSize(16, 16));
+
+ search_button_ = new FlatButton(this);
+ search_button_->setForegroundColor(QColor("#ebebeb"));
+ search_button_->setCursor(QCursor(Qt::PointingHandCursor));
+ search_button_->setStyleSheet("width: 30px; height: 30px;");
+
+ QIcon search_icon;
+ search_icon.addFile(":/icons/icons/search.png", QSize(), QIcon::Normal, QIcon::Off);
+ search_button_->setIcon(search_icon);
+ search_button_->setIconSize(QSize(16, 16));
+
+ top_layout_->addWidget(avatar_);
+ top_layout_->addLayout(text_layout_);
+ top_layout_->addStretch(1);
+ top_layout_->addWidget(search_button_);
+ top_layout_->addWidget(settings_button_);
+
+ setLayout(top_layout_);
+}
+
+void TopRoomBar::paintEvent(QPaintEvent *event)
+{
+ Q_UNUSED(event);
+
+ QStyleOption option;
+ option.initFrom(this);
+
+ QPainter painter(this);
+ style()->drawPrimitive(QStyle::PE_Widget, &option, &painter, this);
+}
+
+TopRoomBar::~TopRoomBar()
+{
+}
diff --git a/src/UserInfoWidget.cc b/src/UserInfoWidget.cc
new file mode 100644
index 00000000..a617d212
--- /dev/null
+++ b/src/UserInfoWidget.cc
@@ -0,0 +1,104 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "UserInfoWidget.h"
+#include "FlatButton.h"
+
+UserInfoWidget::UserInfoWidget(QWidget *parent)
+ : QWidget(parent)
+ , display_name_("User")
+ , userid_("@user:homeserver.org")
+{
+ QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
+
+ setSizePolicy(sizePolicy);
+ setMinimumSize(QSize(0, 65));
+
+ topLayout_ = new QHBoxLayout(this);
+ topLayout_->setSpacing(0);
+ topLayout_->setContentsMargins(5, 5, 5, 5);
+
+ avatarLayout_ = new QHBoxLayout();
+ textLayout_ = new QVBoxLayout();
+
+ userAvatar_ = new Avatar(this);
+ userAvatar_->setLetter(QChar('?'));
+ userAvatar_->setSize(50);
+ userAvatar_->setMaximumSize(QSize(55, 55));
+
+ displayNameLabel_ = new QLabel(this);
+ displayNameLabel_->setStyleSheet(
+ "padding: 0 9px;"
+ "color: #ebebeb;"
+ "font-size: 11pt;"
+ "margin-bottom: -10px;");
+ displayNameLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop);
+
+ userIdLabel_ = new QLabel(this);
+ userIdLabel_->setStyleSheet(
+ "padding: 0 8px 8px 8px;"
+ "color: #5D6565;"
+ "font-size: 10pt;");
+ userIdLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter);
+
+ avatarLayout_->addWidget(userAvatar_);
+ textLayout_->addWidget(displayNameLabel_);
+ textLayout_->addWidget(userIdLabel_);
+
+ topLayout_->addLayout(avatarLayout_);
+ topLayout_->addLayout(textLayout_);
+ topLayout_->addStretch(1);
+
+ buttonLayout_ = new QHBoxLayout();
+
+ settingsButton_ = new FlatButton(this);
+ settingsButton_->setForegroundColor(QColor("#ebebeb"));
+ settingsButton_->setCursor(QCursor(Qt::PointingHandCursor));
+ settingsButton_->setStyleSheet("width: 30px; height: 30px;");
+
+ QIcon icon;
+ icon.addFile(":/icons/icons/user-shape.png", QSize(), QIcon::Normal, QIcon::Off);
+
+ settingsButton_->setIcon(icon);
+ settingsButton_->setIconSize(QSize(16, 16));
+
+ buttonLayout_->addWidget(settingsButton_);
+
+ topLayout_->addLayout(buttonLayout_);
+}
+
+UserInfoWidget::~UserInfoWidget()
+{
+}
+
+void UserInfoWidget::setAvatar(const QImage &img)
+{
+ avatar_image_ = img;
+ userAvatar_->setImage(img);
+}
+
+void UserInfoWidget::setDisplayName(const QString &name)
+{
+ display_name_ = name;
+ displayNameLabel_->setText(name);
+}
+
+void UserInfoWidget::setUserId(const QString &userid)
+{
+ userid_ = userid;
+ userIdLabel_->setText(userid);
+}
diff --git a/src/WelcomePage.cc b/src/WelcomePage.cc
new file mode 100644
index 00000000..2220fad7
--- /dev/null
+++ b/src/WelcomePage.cc
@@ -0,0 +1,103 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QApplication>
+#include <QLayout>
+
+#include "WelcomePage.h"
+
+WelcomePage::WelcomePage(QWidget *parent)
+ : QWidget(parent)
+{
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ top_layout_ = new QVBoxLayout(this);
+ top_layout_->setSpacing(0);
+ top_layout_->setMargin(0);
+
+ intro_banner_ = new QLabel(this);
+ intro_banner_->setStyleSheet("background-color: #1c3133;");
+ intro_banner_->setAlignment(Qt::AlignCenter);
+
+ intro_text_ = new QLabel(this);
+ intro_text_->setText(QApplication::translate("WelcomePage",
+ "<html>"
+ "<head/>"
+ "<body>"
+ " <p align=\"center\"><span style=\" font-size:28pt;\"> nheko </span></p>"
+ " <p align=\"center\" style=\"margin: 0; line-height: 2pt\">"
+ " <span style=\" font-size:12pt; color:#6d7387;\"> "
+ " A desktop client for Matrix, the open protocol for decentralized communication."
+ " </span>"
+ " </p>\n"
+ " <p align=\"center\" style=\"margin: 1pt; line-height: 2pt;\">"
+ " <span style=\" font-size:12pt; color:#6d7387;\">Enjoy your stay!</span>"
+ " </p>"
+ "</body>"
+ "</html>",
+ Q_NULLPTR));
+
+ top_layout_->addWidget(intro_banner_);
+ top_layout_->addWidget(intro_text_, 0, Qt::AlignCenter);
+
+ button_layout_ = new QHBoxLayout();
+ button_layout_->setSpacing(0);
+ button_layout_->setContentsMargins(0, 20, 0, 80);
+
+ register_button_ = new RaisedButton("REGISTER", this);
+ register_button_->setBackgroundColor(QColor("#171919"));
+ register_button_->setForegroundColor(QColor("#ebebeb"));
+ register_button_->setMinimumSize(240, 60);
+ register_button_->setCursor(QCursor(Qt::PointingHandCursor));
+ register_button_->setFontSize(14);
+ register_button_->setCornerRadius(3);
+
+ login_button_ = new RaisedButton("LOGIN", this);
+ login_button_->setBackgroundColor(QColor("#171919"));
+ login_button_->setForegroundColor(QColor("#ebebeb"));
+ login_button_->setMinimumSize(240, 60);
+ login_button_->setCursor(QCursor(Qt::PointingHandCursor));
+ login_button_->setFontSize(14);
+ login_button_->setCornerRadius(3);
+
+ button_spacer_ = new QSpacerItem(20, 20, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
+
+ button_layout_->addStretch(1);
+ button_layout_->addWidget(register_button_);
+ button_layout_->addItem(button_spacer_);
+ button_layout_->addWidget(login_button_);
+ button_layout_->addStretch(1);
+
+ top_layout_->addLayout(button_layout_);
+
+ connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
+ connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
+}
+
+void WelcomePage::onLoginButtonClicked()
+{
+ emit userLogin();
+}
+
+void WelcomePage::onRegisterButtonClicked()
+{
+ emit userRegister();
+}
+
+WelcomePage::~WelcomePage()
+{
+}
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 00000000..9ef7ffd7
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,48 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QApplication>
+#include <QFontDatabase>
+
+#include "MainWindow.h"
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication::setApplicationName("nheko");
+ QCoreApplication::setApplicationVersion("ΩμÎγa");
+ QCoreApplication::setOrganizationName("Nheko");
+
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-Light.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-BoldItalic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-Semibold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-SemiboldItalic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-ExtraBold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/OpenSans-ExtraBoldItalic.ttf");
+
+ QApplication app(argc, argv);
+
+ QFont font("Open Sans");
+ app.setFont(font);
+
+ MainWindow w;
+ w.show();
+
+ return app.exec();
+}
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);
+}
|