diff --git a/src/Cache.cpp b/src/Cache.cpp
index a9094e2d..d6a7b509 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -2059,6 +2059,7 @@ Cache::roomMembers(const std::string &room_id)
QHash<QString, QString> Cache::DisplayNames;
QHash<QString, QString> Cache::AvatarUrls;
+QHash<QString, QString> Cache::UserColors;
QString
Cache::displayName(const QString &room_id, const QString &user_id)
@@ -2090,6 +2091,16 @@ Cache::avatarUrl(const QString &room_id, const QString &user_id)
return QString();
}
+QString
+Cache::userColor(const QString &user_id)
+{
+ if (UserColors.contains(user_id)) {
+ return UserColors[user_id];
+ }
+
+ return QString();
+}
+
void
Cache::insertDisplayName(const QString &room_id,
const QString &user_id,
@@ -2119,3 +2130,21 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id)
auto fmt = QString("%1 %2").arg(room_id).arg(user_id);
AvatarUrls.remove(fmt);
}
+
+void
+Cache::insertUserColor(const QString &user_id, const QString &color_name)
+{
+ UserColors.insert(user_id, color_name);
+}
+
+void
+Cache::removeUserColor(const QString &user_id)
+{
+ UserColors.remove(user_id);
+}
+
+void
+Cache::clearUserColors()
+{
+ UserColors.clear();
+}
\ No newline at end of file
diff --git a/src/Cache.h b/src/Cache.h
index b730d6fc..1a133618 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -282,13 +282,16 @@ public:
static QHash<QString, QString> DisplayNames;
static QHash<QString, QString> AvatarUrls;
+ static QHash<QString, QString> UserColors;
static std::string displayName(const std::string &room_id, const std::string &user_id);
static QString displayName(const QString &room_id, const QString &user_id);
static QString avatarUrl(const QString &room_id, const QString &user_id);
+ static QString userColor(const QString &user_id);
static void removeDisplayName(const QString &room_id, const QString &user_id);
static void removeAvatarUrl(const QString &room_id, const QString &user_id);
+ static void removeUserColor(const QString &user_id);
static void insertDisplayName(const QString &room_id,
const QString &user_id,
@@ -296,6 +299,9 @@ public:
static void insertAvatarUrl(const QString &room_id,
const QString &user_id,
const QString &avatar_url);
+ static void insertUserColor(const QString &user_id, const QString &color_name);
+
+ static void clearUserColors();
//! Load saved data for the display names & avatars.
void populateMembers();
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index aaaddf6d..dd23fb80 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -546,7 +546,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
updateTypingUsers(room_id, room.second.ephemeral.typing);
updateRoomNotificationCount(
- room_id, room.second.unread_notifications.notification_count);
+ room_id,
+ room.second.unread_notifications.notification_count,
+ room.second.unread_notifications.highlight_count);
if (room.second.unread_notifications.notification_count > 0)
hasNotifications = true;
@@ -908,9 +910,11 @@ ChatPage::setGroupViewState(bool isEnabled)
}
void
-ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count)
+ChatPage::updateRoomNotificationCount(const QString &room_id,
+ uint16_t notification_count,
+ uint16_t highlight_count)
{
- room_list_->updateUnreadMessageCount(room_id, notification_count);
+ room_list_->updateUnreadMessageCount(room_id, notification_count, highlight_count);
}
void
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 2c728c17..7d3b3273 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -148,6 +148,7 @@ signals:
const QImage &icon);
void updateGroupsInfo(const mtx::responses::JoinedGroups &groups);
+ void themeChanged();
private slots:
void showUnreadMessageNotification(int count);
@@ -196,7 +197,9 @@ private:
Memberships getMemberships(const std::vector<Collection> &events) const;
//! Update the room with the new notification count.
- void updateRoomNotificationCount(const QString &room_id, uint16_t notification_count);
+ void updateRoomNotificationCount(const QString &room_id,
+ uint16_t notification_count,
+ uint16_t highlight_count);
//! Send desktop notification for the received messages.
void sendDesktopNotifications(const mtx::responses::Notifications &);
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 55dbba34..7d9a8902 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -17,6 +17,7 @@
#include <QApplication>
#include <QLayout>
+#include <QPluginLoader>
#include <QSettings>
#include <QShortcut>
@@ -112,7 +113,11 @@ MainWindow::MainWindow(QWidget *parent)
connect(
userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
-
+ connect(userSettingsPage_, &UserSettingsPage::themeChanged, this, []() {
+ Cache::clearUserColors();
+ });
+ connect(
+ userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
connect(trayIcon_,
SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
this,
@@ -162,6 +167,10 @@ MainWindow::MainWindow(QWidget *parent)
showChatPage();
}
+
+ if (loadJdenticonPlugin()) {
+ nhlog::ui()->info("loaded jdenticon.");
+ }
}
void
@@ -475,3 +484,27 @@ MainWindow::showDialog(QWidget *dialog)
dialog->raise();
dialog->show();
}
+
+bool
+MainWindow::loadJdenticonPlugin()
+{
+ QDir pluginsDir(qApp->applicationDirPath());
+
+ bool plugins = pluginsDir.cd("plugins");
+ if (plugins) {
+ foreach (QString fileName, pluginsDir.entryList(QDir::Files)) {
+ QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
+ QObject *plugin = pluginLoader.instance();
+ if (plugin) {
+ jdenticonInteface_ = qobject_cast<JdenticonInterface *>(plugin);
+ if (jdenticonInteface_) {
+ nhlog::ui()->info("Found jdenticon plugin.");
+ return true;
+ }
+ }
+ }
+ }
+
+ nhlog::ui()->info("jdenticon plugin not found.");
+ return false;
+}
\ No newline at end of file
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 2336a929..1aadbf4d 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -31,6 +31,8 @@
#include "dialogs/UserProfile.h"
#include "ui/OverlayModal.h"
+#include "jdenticoninterface.h"
+
class ChatPage;
class LoadingIndicator;
class OverlayModal;
@@ -129,6 +131,8 @@ private slots:
void removeOverlayProgressBar();
private:
+ bool loadJdenticonPlugin();
+
void showDialog(QWidget *dialog);
bool hasActiveUser();
void restoreWindowSize();
@@ -158,4 +162,6 @@ private:
//! Overlay modal used to project other widgets.
OverlayModal *modal_ = nullptr;
LoadingIndicator *spinner_ = nullptr;
+
+ JdenticonInterface *jdenticonInteface_ = nullptr;
};
diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index fcf5bd72..f17b383c 100644
--- a/src/RoomInfoListItem.cpp
+++ b/src/RoomInfoListItem.cpp
@@ -101,6 +101,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare
, roomName_{QString::fromStdString(std::move(info.name))}
, isPressed_(false)
, unreadMsgCount_(0)
+ , unreadHighlightedMsgCount_(0)
{
init(parent);
@@ -301,7 +302,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
if (unreadMsgCount_ > 0) {
QBrush brush;
brush.setStyle(Qt::SolidPattern);
- brush.setColor(bubbleBgColor());
+ if (unreadHighlightedMsgCount_ > 0) {
+ brush.setColor(mentionedColor());
+ } else {
+ brush.setColor(bubbleBgColor());
+ }
if (isPressed_)
brush.setColor(bubbleFgColor());
@@ -354,9 +359,10 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
}
void
-RoomInfoListItem::updateUnreadMessageCount(int count)
+RoomInfoListItem::updateUnreadMessageCount(int count, int highlightedCount)
{
- unreadMsgCount_ = count;
+ unreadMsgCount_ = count;
+ unreadHighlightedMsgCount_ = highlightedCount;
update();
}
diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
index 5fa89853..40c938c1 100644
--- a/src/RoomInfoListItem.h
+++ b/src/RoomInfoListItem.h
@@ -59,14 +59,15 @@ class RoomInfoListItem : public QWidget
Q_PROPERTY(QColor hoverTitleColor READ hoverTitleColor WRITE setHoverTitleColor)
Q_PROPERTY(QColor hoverSubtitleColor READ hoverSubtitleColor WRITE setHoverSubtitleColor)
+ Q_PROPERTY(QColor mentionedColor READ mentionedColor WRITE setMentionedColor)
Q_PROPERTY(QColor btnColor READ btnColor WRITE setBtnColor)
Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor)
public:
RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent = 0);
- void updateUnreadMessageCount(int count);
- void clearUnreadMessageCount() { updateUnreadMessageCount(0); };
+ void updateUnreadMessageCount(int count, int highlightedCount);
+ void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); };
QString roomId() { return roomId_; }
bool isPressed() const { return isPressed_; }
@@ -97,6 +98,7 @@ public:
QColor bubbleFgColor() const { return bubbleFgColor_; }
QColor bubbleBgColor() const { return bubbleBgColor_; }
+ QColor mentionedColor() const { return mentionedFontColor_; }
void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; }
void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; }
@@ -120,6 +122,7 @@ public:
void setBubbleFgColor(QColor &color) { bubbleFgColor_ = color; }
void setBubbleBgColor(QColor &color) { bubbleBgColor_ = color; }
+ void setMentionedColor(QColor &color) { mentionedFontColor_ = color; }
void setRoomName(const QString &name) { roomName_ = name; }
void setRoomType(bool isInvite)
@@ -184,7 +187,8 @@ private:
bool isPressed_ = false;
bool hasUnreadMessages_ = true;
- int unreadMsgCount_ = 0;
+ int unreadMsgCount_ = 0;
+ int unreadHighlightedMsgCount_ = 0;
QColor highlightedBackgroundColor_;
QColor hoverBackgroundColor_;
@@ -206,6 +210,7 @@ private:
QRectF declineBtnRegion_;
// Fonts
+ QColor mentionedFontColor_;
QFont unreadCountFont_;
int bubbleDiameter_;
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index c1b080c0..1abf3533 100644
--- a/src/RoomList.cpp
+++ b/src/RoomList.cpp
@@ -143,7 +143,7 @@ RoomList::removeRoom(const QString &room_id, bool reset)
}
void
-RoomList::updateUnreadMessageCount(const QString &roomid, int count)
+RoomList::updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount)
{
if (!roomExists(roomid)) {
nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}",
@@ -151,7 +151,7 @@ RoomList::updateUnreadMessageCount(const QString &roomid, int count)
return;
}
- rooms_[roomid]->updateUnreadMessageCount(count);
+ rooms_[roomid]->updateUnreadMessageCount(count, highlightedCount);
calculateUnreadMessageCount();
}
diff --git a/src/RoomList.h b/src/RoomList.h
index 88e6d1ad..155a969c 100644
--- a/src/RoomList.h
+++ b/src/RoomList.h
@@ -68,7 +68,7 @@ signals:
public slots:
void updateRoomAvatar(const QString &roomid, const QPixmap &img);
void highlightSelectedRoom(const QString &room_id);
- void updateUnreadMessageCount(const QString &roomid, int count);
+ void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount);
void updateRoomDescription(const QString &roomid, const DescInfo &info);
void closeJoinRoomDialog(bool isJoining, QString roomAlias);
void updateReadStatus(const std::map<QString, bool> &status);
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index 89513037..5fcba7a9 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -513,8 +513,22 @@ TextInputWidget::TextInputWidget(QWidget *parent)
sendMessageBtn_->setIcon(send_message_icon);
sendMessageBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight));
+ emojiBtn_ = new emoji::PickButton(this);
+ emojiBtn_->setToolTip(tr("Emoji"));
+
+#if defined(Q_OS_MAC)
+ // macOS has a native emoji picker.
+ emojiBtn_->hide();
+#endif
+
+ QIcon emoji_icon;
+ emoji_icon.addFile(":/icons/icons/ui/smile.png");
+ emojiBtn_->setIcon(emoji_icon);
+ emojiBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight));
+
topLayout_->addWidget(sendFileBtn_);
topLayout_->addWidget(input_);
+ topLayout_->addWidget(emojiBtn_);
topLayout_->addWidget(sendMessageBtn_);
setLayout(topLayout_);
@@ -527,6 +541,11 @@ TextInputWidget::TextInputWidget(QWidget *parent)
connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio);
connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo);
connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile);
+ connect(emojiBtn_,
+ SIGNAL(emojiSelected(const QString &)),
+ this,
+ SLOT(addSelectedEmoji(const QString &)));
+
connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping);
connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping);
@@ -536,6 +555,22 @@ TextInputWidget::TextInputWidget(QWidget *parent)
}
void
+TextInputWidget::addSelectedEmoji(const QString &emoji)
+{
+ QTextCursor cursor = input_->textCursor();
+
+ QTextCharFormat charfmt;
+ input_->setCurrentCharFormat(charfmt);
+
+ input_->insertPlainText(emoji);
+ cursor.movePosition(QTextCursor::End);
+
+ input_->setCurrentCharFormat(charfmt);
+
+ input_->show();
+}
+
+void
TextInputWidget::command(QString command, QString args)
{
if (command == "me") {
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 1fb6d7f2..8f634f6b 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -30,6 +30,7 @@
#include "SuggestionsPopup.h"
#include "dialogs/PreviewUploadOverlay.h"
+#include "emoji/PickButton.h"
namespace dialogs {
class PreviewUploadOverlay;
@@ -159,6 +160,9 @@ public slots:
void focusLineEdit() { input_->setFocus(); }
void addReply(const QString &username, const QString &msg);
+private slots:
+ void addSelectedEmoji(const QString &emoji);
+
signals:
void sendTextMessage(QString msg);
void sendEmoteMessage(QString msg);
@@ -189,6 +193,7 @@ private:
FlatButton *sendFileBtn_;
FlatButton *sendMessageBtn_;
+ emoji::PickButton *emojiBtn_;
QColor borderColor_;
};
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 15ad72e1..e3c0d190 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -49,7 +49,7 @@ UserSettings::load()
isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool();
isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool();
theme_ = settings.value("user/theme", "light").toString();
-
+ font_ = settings.value("user/font_family", "default").toString();
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
applyTheme();
@@ -63,6 +63,13 @@ UserSettings::setFontSize(double size)
}
void
+UserSettings::setFontFamily(QString family)
+{
+ font_ = family;
+ save();
+}
+
+void
UserSettings::setTheme(QString theme)
{
theme_ = theme;
@@ -106,6 +113,7 @@ UserSettings::save()
settings.setValue("group_view", isGroupViewEnabled_);
settings.setValue("desktop_notifications", hasDesktopNotifications_);
settings.setValue("theme", theme());
+ settings.setValue("font_family", font_);
settings.endGroup();
}
@@ -220,6 +228,23 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
fontSizeOptionLayout->addWidget(fontSizeLabel);
fontSizeOptionLayout->addWidget(fontSizeCombo_, 0, Qt::AlignRight);
+ auto fontFamilyOptionLayout = new QHBoxLayout;
+ fontFamilyOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin);
+ auto fontFamilyLabel = new QLabel(tr("Font Family"), this);
+ fontFamilyLabel->setFont(font);
+ fontSelectionCombo_ = new QComboBox(this);
+ QFontDatabase fontDb;
+ auto fontFamilies = fontDb.families();
+ for (const auto &family : fontFamilies) {
+ fontSelectionCombo_->addItem(family);
+ }
+
+ int fontIndex = fontSelectionCombo_->findText(settings_->font());
+ fontSelectionCombo_->setCurrentIndex(fontIndex);
+
+ fontFamilyOptionLayout->addWidget(fontFamilyLabel);
+ fontFamilyOptionLayout->addWidget(fontSelectionCombo_, 0, Qt::AlignRight);
+
auto themeOptionLayout_ = new QHBoxLayout;
themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin);
auto themeLabel_ = new QLabel(tr("Theme"), this);
@@ -229,6 +254,11 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
themeCombo_->addItem("Dark");
themeCombo_->addItem("System");
+ QString themeStr = settings_->theme();
+ themeStr.replace(0, 1, themeStr[0].toUpper());
+ int themeIndex = themeCombo_->findText(themeStr);
+ themeCombo_->setCurrentIndex(themeIndex);
+
themeOptionLayout_->addWidget(themeLabel_);
themeOptionLayout_->addWidget(themeCombo_, 0, Qt::AlignRight);
@@ -319,6 +349,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
mainLayout_->addLayout(scaleFactorOptionLayout);
mainLayout_->addLayout(fontSizeOptionLayout);
+ mainLayout_->addLayout(fontFamilyOptionLayout);
mainLayout_->addWidget(new HorizontalLine(this));
mainLayout_->addLayout(themeOptionLayout_);
mainLayout_->addWidget(new HorizontalLine(this));
@@ -348,14 +379,19 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
connect(themeCombo_,
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
- [this](const QString &text) { settings_->setTheme(text.toLower()); });
+ [this](const QString &text) {
+ settings_->setTheme(text.toLower());
+ emit themeChanged();
+ });
connect(scaleFactorCombo_,
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
[](const QString &factor) { utils::setScaleFactor(factor.toFloat()); });
connect(fontSizeCombo_,
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
[this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); });
-
+ connect(fontSelectionCombo_,
+ static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
+ [this](const QString &family) { settings_->setFontFamily(family.trimmed()); });
connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) {
settings_->setTray(!isDisabled);
if (isDisabled) {
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 373126ae..900f57e4 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -18,6 +18,7 @@
#pragma once
#include <QComboBox>
+#include <QFontDatabase>
#include <QFrame>
#include <QLabel>
#include <QLayout>
@@ -54,6 +55,7 @@ public:
}
void setFontSize(double size);
+ void setFontFamily(QString family);
void setGroupView(bool state)
{
@@ -90,6 +92,7 @@ public:
bool isReadReceiptsEnabled() const { return isReadReceiptsEnabled_; }
bool hasDesktopNotifications() const { return hasDesktopNotifications_; }
double fontSize() const { return baseFontSize_; }
+ QString font() const { return font_; }
signals:
void groupViewStateChanged(bool state);
@@ -103,6 +106,7 @@ private:
bool isReadReceiptsEnabled_;
bool hasDesktopNotifications_;
double baseFontSize_;
+ QString font_;
};
class HorizontalLine : public QFrame
@@ -128,6 +132,7 @@ protected:
signals:
void moveBack();
void trayOptionChanged(bool value);
+ void themeChanged();
private slots:
void importSessionKeys();
@@ -154,6 +159,7 @@ private:
QComboBox *themeCombo_;
QComboBox *scaleFactorCombo_;
QComboBox *fontSizeCombo_;
+ QComboBox *fontSelectionCombo_;
int sideMargin_ = 0;
};
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 8176cb43..1d1dbeb2 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -15,6 +15,8 @@
using TimelineEvent = mtx::events::collections::TimelineEvents;
+QHash<QString, QString> authorColors_;
+
QString
utils::localUser()
{
@@ -382,6 +384,135 @@ utils::linkColor()
return QPalette().color(QPalette::Link).name();
}
+int
+utils::hashQString(const QString &input)
+{
+ auto hash = 0;
+
+ for (int i = 0; i < input.length(); i++) {
+ hash = input.at(i).digitValue() + ((hash << 5) - hash);
+ }
+
+ return hash;
+}
+
+QString
+utils::generateContrastingHexColor(const QString &input, const QString &background)
+{
+ nhlog::ui()->debug("Background hex {}", background.toStdString());
+ const QColor backgroundCol(background);
+ const qreal backgroundLum = luminance(background);
+
+ // Create a color for the input
+ auto hash = hashQString(input);
+ // create a hue value based on the hash of the input.
+ auto userHue = qAbs(hash % 360);
+ nhlog::ui()->debug(
+ "User Hue {} : {}", input.toStdString(), QString::number(userHue).toStdString());
+ // start with moderate saturation and lightness values.
+ auto sat = 220;
+ auto lightness = 125;
+
+ // converting to a QColor makes the luminance calc easier.
+ QColor inputColor = QColor::fromHsl(userHue, sat, lightness);
+
+ // calculate the initial luminance and contrast of the
+ // generated color. It's possible that no additional
+ // work will be necessary.
+ auto lum = luminance(inputColor);
+ auto contrast = computeContrast(lum, backgroundLum);
+
+ // If the contrast doesn't meet our criteria,
+ // try again and again until they do by modifying first
+ // the lightness and then the saturation of the color.
+ while (contrast < 5) {
+ // if our lightness is at it's bounds, try changing
+ // saturation instead.
+ if (lightness == 242 || lightness == 13) {
+ qreal newSat = qBound(26.0, sat * 1.25, 242.0);
+ nhlog::ui()->info("newSat {}", QString::number(newSat).toStdString());
+
+ inputColor.setHsl(userHue, qFloor(newSat), lightness);
+ auto tmpLum = luminance(inputColor);
+ auto higherContrast = computeContrast(tmpLum, backgroundLum);
+ if (higherContrast > contrast) {
+ contrast = higherContrast;
+ sat = newSat;
+ } else {
+ newSat = qBound(26.0, sat / 1.25, 242.0);
+ inputColor.setHsl(userHue, qFloor(newSat), lightness);
+ tmpLum = luminance(inputColor);
+ auto lowerContrast = computeContrast(tmpLum, backgroundLum);
+ if (lowerContrast > contrast) {
+ contrast = lowerContrast;
+ sat = newSat;
+ }
+ }
+ } else {
+ qreal newLightness = qBound(13.0, lightness * 1.25, 242.0);
+
+ inputColor.setHsl(userHue, sat, qFloor(newLightness));
+
+ auto tmpLum = luminance(inputColor);
+ auto higherContrast = computeContrast(tmpLum, backgroundLum);
+
+ // Check to make sure we have actually improved contrast
+ if (higherContrast > contrast) {
+ contrast = higherContrast;
+ lightness = newLightness;
+ // otherwise, try going the other way instead.
+ } else {
+ newLightness = qBound(13.0, lightness / 1.25, 242.0);
+ inputColor.setHsl(userHue, sat, qFloor(newLightness));
+ tmpLum = luminance(inputColor);
+ auto lowerContrast = computeContrast(tmpLum, backgroundLum);
+ if (lowerContrast > contrast) {
+ contrast = lowerContrast;
+ lightness = newLightness;
+ }
+ }
+ }
+ }
+
+ // get the hex value of the generated color.
+ auto colorHex = inputColor.name();
+
+ nhlog::ui()->debug("Hex Generated for {}: [hex: {}, contrast: {}, luminance: {}]",
+ input.toStdString(),
+ colorHex.toStdString(),
+ QString::number(contrast).toStdString(),
+ QString::number(lum).toStdString());
+ return colorHex;
+}
+
+qreal
+utils::computeContrast(const qreal &one, const qreal &two)
+{
+ auto ratio = (one + 0.05) / (two + 0.05);
+
+ if (two > one) {
+ ratio = 1 / ratio;
+ }
+
+ return ratio;
+}
+
+qreal
+utils::luminance(const QColor &col)
+{
+ int colRgb[3] = {col.red(), col.green(), col.blue()};
+ qreal lumRgb[3];
+
+ for (int i = 0; i < 3; i++) {
+ qreal v = colRgb[i] / 255.0;
+ v <= 0.03928 ? lumRgb[i] = v / 12.92 : lumRgb[i] = qPow((v + 0.055) / 1.055, 2.4);
+ }
+
+ auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
+
+ return lum;
+}
+
void
utils::centerWidget(QWidget *widget, QWidget *parent)
{
diff --git a/src/Utils.h b/src/Utils.h
index 2a029943..8672e7d4 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -14,6 +14,8 @@
#include <mtx/events/collections.hpp>
#include <mtx/events/common.hpp>
+#include <qmath.h>
+
class QComboBox;
namespace utils {
@@ -227,6 +229,23 @@ markdownToHtml(const QString &text);
QString
linkColor();
+//! Returns the hash code of the input QString
+int
+hashQString(const QString &input);
+
+//! Generate a color (matching #RRGGBB) that has an acceptable contrast to background that is based
+//! on the input string.
+QString
+generateContrastingHexColor(const QString &input, const QString &background);
+
+//! Given two luminance values, compute the contrast ratio between them.
+qreal
+computeContrast(const qreal &one, const qreal &two);
+
+//! Compute the luminance of a single color. Based on https://stackoverflow.com/a/9733420
+qreal
+luminance(const QColor &col);
+
//! Center a widget in relation to another widget.
void
centerWidget(QWidget *widget, QWidget *parent);
diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index b40aa164..dbf5bbe4 100644
--- a/src/dialogs/ImageOverlay.cpp
+++ b/src/dialogs/ImageOverlay.cpp
@@ -76,6 +76,8 @@ ImageOverlay::paintEvent(QPaintEvent *event)
content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height());
close_button_ =
QRect(screen_.width() - margin - buttonSize, margin, buttonSize, buttonSize);
+ save_button_ =
+ QRect(screen_.width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize);
// Draw main content_.
painter.drawPixmap(content_, image_);
@@ -91,6 +93,12 @@ ImageOverlay::paintEvent(QPaintEvent *event)
painter.setPen(pen);
painter.drawLine(center - QPointF(15, 15), center + QPointF(15, 15));
painter.drawLine(center + QPointF(15, -15), center - QPointF(15, -15));
+
+ // Draw download button
+ center = save_button_.center();
+ painter.drawLine(center - QPointF(0, 15), center + QPointF(0, 15));
+ painter.drawLine(center - QPointF(15, 0), center + QPointF(0, 15));
+ painter.drawLine(center + QPointF(0, 15), center + QPointF(15, 0));
}
void
@@ -101,6 +109,8 @@ ImageOverlay::mousePressEvent(QMouseEvent *event)
if (close_button_.contains(event->pos()))
emit closing();
+ else if (save_button_.contains(event->pos()))
+ emit saving();
else if (!content_.contains(event->pos()))
emit closing();
}
diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h
index b4d42acb..26257fc1 100644
--- a/src/dialogs/ImageOverlay.h
+++ b/src/dialogs/ImageOverlay.h
@@ -35,6 +35,7 @@ protected:
signals:
void closing();
+ void saving();
private:
QPixmap originalImage_;
@@ -42,6 +43,7 @@ private:
QRect content_;
QRect close_button_;
+ QRect save_button_;
QRect screen_;
};
} // dialogs
diff --git a/src/emoji/Category.cpp b/src/emoji/Category.cpp
new file mode 100644
index 00000000..fbfbf4fc
--- /dev/null
+++ b/src/emoji/Category.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 <QPainter>
+#include <QScrollBar>
+#include <QStyleOption>
+
+#include "Config.h"
+
+#include "emoji/Category.h"
+
+using namespace emoji;
+
+Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent)
+ : QWidget(parent)
+{
+ mainLayout_ = new QVBoxLayout(this);
+ mainLayout_->setMargin(0);
+ mainLayout_->setSpacing(0);
+
+ emojiListView_ = new QListView();
+ itemModel_ = new QStandardItemModel(this);
+
+ delegate_ = new ItemDelegate(this);
+ data_ = new Emoji;
+
+ emojiListView_->setItemDelegate(delegate_);
+ emojiListView_->setModel(itemModel_);
+ emojiListView_->setViewMode(QListView::IconMode);
+ emojiListView_->setFlow(QListView::LeftToRight);
+ emojiListView_->setResizeMode(QListView::Adjust);
+ emojiListView_->verticalScrollBar()->setEnabled(false);
+ emojiListView_->horizontalScrollBar()->setEnabled(false);
+
+ const int cols = 7;
+ const int rows = emoji.size() / 7;
+
+ // TODO: Be precise here. Take the parent into consideration.
+ emojiListView_->setFixedSize(cols * 50 + 20, rows * 50 + 20);
+ emojiListView_->setGridSize(QSize(50, 50));
+ emojiListView_->setDragEnabled(false);
+ emojiListView_->setEditTriggers(QAbstractItemView::NoEditTriggers);
+
+ for (const auto &e : emoji) {
+ data_->unicode = e.unicode;
+
+ auto item = new QStandardItem;
+ item->setSizeHint(QSize(24, 24));
+
+ QVariant unicode(data_->unicode);
+ item->setData(unicode.toString(), Qt::UserRole);
+
+ itemModel_->appendRow(item);
+ }
+
+ QFont font;
+ font.setWeight(QFont::Medium);
+
+ category_ = new QLabel(category, this);
+ category_->setFont(font);
+ category_->setStyleSheet("margin: 20px 0 20px 8px;");
+
+ mainLayout_->addWidget(category_);
+ mainLayout_->addWidget(emojiListView_);
+
+ connect(emojiListView_, &QListView::clicked, this, &Category::clickIndex);
+}
+
+void
+Category::paintEvent(QPaintEvent *)
+{
+ QStyleOption opt;
+ opt.init(this);
+ QPainter p(this);
+ style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+}
diff --git a/src/emoji/Category.h b/src/emoji/Category.h
new file mode 100644
index 00000000..a14029c8
--- /dev/null
+++ b/src/emoji/Category.h
@@ -0,0 +1,59 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <QLabel>
+#include <QLayout>
+#include <QListView>
+#include <QStandardItemModel>
+
+#include "ItemDelegate.h"
+
+namespace emoji {
+
+class Category : public QWidget
+{
+ Q_OBJECT
+
+public:
+ Category(QString category, std::vector<Emoji> emoji, QWidget *parent = nullptr);
+
+signals:
+ void emojiSelected(const QString &emoji);
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+
+private slots:
+ void clickIndex(const QModelIndex &index)
+ {
+ emit emojiSelected(index.data(Qt::UserRole).toString());
+ };
+
+private:
+ QVBoxLayout *mainLayout_;
+
+ QStandardItemModel *itemModel_;
+ QListView *emojiListView_;
+
+ emoji::Emoji *data_;
+ emoji::ItemDelegate *delegate_;
+
+ QLabel *category_;
+};
+} // namespace emoji
diff --git a/src/emoji/ItemDelegate.cpp b/src/emoji/ItemDelegate.cpp
new file mode 100644
index 00000000..b79ae0fc
--- /dev/null
+++ b/src/emoji/ItemDelegate.cpp
@@ -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 <QDebug>
+#include <QPainter>
+
+#include "emoji/ItemDelegate.h"
+
+using namespace emoji;
+
+ItemDelegate::ItemDelegate(QObject *parent)
+ : QStyledItemDelegate(parent)
+{
+ data_ = new Emoji;
+}
+
+ItemDelegate::~ItemDelegate() { delete data_; }
+
+void
+ItemDelegate::paint(QPainter *painter,
+ const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ Q_UNUSED(index);
+
+ QStyleOptionViewItem viewOption(option);
+
+ auto emoji = index.data(Qt::UserRole).toString();
+
+ // QFont font("Emoji One");
+ QFont font;
+ painter->setFont(font);
+ painter->drawText(viewOption.rect, Qt::AlignCenter, emoji);
+}
diff --git a/src/emoji/ItemDelegate.h b/src/emoji/ItemDelegate.h
new file mode 100644
index 00000000..e0456308
--- /dev/null
+++ b/src/emoji/ItemDelegate.h
@@ -0,0 +1,43 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <QModelIndex>
+#include <QStandardItemModel>
+#include <QStyledItemDelegate>
+
+#include "Provider.h"
+
+namespace emoji {
+
+class ItemDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit ItemDelegate(QObject *parent = nullptr);
+ ~ItemDelegate();
+
+ void paint(QPainter *painter,
+ const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+
+private:
+ Emoji *data_;
+};
+} // namespace emoji
diff --git a/src/emoji/Panel.cpp b/src/emoji/Panel.cpp
new file mode 100644
index 00000000..710b501e
--- /dev/null
+++ b/src/emoji/Panel.cpp
@@ -0,0 +1,236 @@
+/*
+ * 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 <QPushButton>
+#include <QScrollBar>
+#include <QVBoxLayout>
+
+#include "ui/DropShadow.h"
+#include "ui/FlatButton.h"
+
+#include "emoji/Category.h"
+#include "emoji/Panel.h"
+
+using namespace emoji;
+
+Panel::Panel(QWidget *parent)
+ : QWidget(parent)
+ , shadowMargin_{2}
+ , width_{370}
+ , height_{350}
+ , categoryIconSize_{20}
+{
+ setStyleSheet("QWidget {border: none;}"
+ "QScrollBar:vertical { width: 0px; margin: 0px; }"
+ "QScrollBar::handle:vertical { min-height: 30px; }");
+
+ setAttribute(Qt::WA_ShowWithoutActivating, true);
+ setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
+
+ auto mainWidget = new QWidget(this);
+ mainWidget->setMaximumSize(width_, height_);
+
+ auto topLayout = new QVBoxLayout(this);
+ topLayout->addWidget(mainWidget);
+ topLayout->setMargin(shadowMargin_);
+ topLayout->setSpacing(0);
+
+ auto contentLayout = new QVBoxLayout(mainWidget);
+ contentLayout->setMargin(0);
+ contentLayout->setSpacing(0);
+
+ auto emojiCategories = new QFrame(mainWidget);
+
+ auto categoriesLayout = new QHBoxLayout(emojiCategories);
+ categoriesLayout->setSpacing(0);
+ categoriesLayout->setMargin(0);
+
+ QIcon icon;
+
+ auto peopleCategory = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/people.png");
+ peopleCategory->setIcon(icon);
+ peopleCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ auto natureCategory_ = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/nature.png");
+ natureCategory_->setIcon(icon);
+ natureCategory_->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ auto foodCategory_ = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/foods.png");
+ foodCategory_->setIcon(icon);
+ foodCategory_->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ auto activityCategory = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/activity.png");
+ activityCategory->setIcon(icon);
+ activityCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ auto travelCategory = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/travel.png");
+ travelCategory->setIcon(icon);
+ travelCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ auto objectsCategory = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/objects.png");
+ objectsCategory->setIcon(icon);
+ objectsCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ auto symbolsCategory = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/symbols.png");
+ symbolsCategory->setIcon(icon);
+ symbolsCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ auto flagsCategory = new FlatButton(emojiCategories);
+ icon.addFile(":/icons/icons/emoji-categories/flags.png");
+ flagsCategory->setIcon(icon);
+ flagsCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_));
+
+ categoriesLayout->addWidget(peopleCategory);
+ categoriesLayout->addWidget(natureCategory_);
+ categoriesLayout->addWidget(foodCategory_);
+ categoriesLayout->addWidget(activityCategory);
+ categoriesLayout->addWidget(travelCategory);
+ categoriesLayout->addWidget(objectsCategory);
+ categoriesLayout->addWidget(symbolsCategory);
+ categoriesLayout->addWidget(flagsCategory);
+
+ scrollArea_ = new QScrollArea(this);
+ scrollArea_->setWidgetResizable(true);
+ scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+ auto scrollWidget = new QWidget(this);
+ auto scrollLayout = new QVBoxLayout(scrollWidget);
+
+ scrollLayout->setMargin(0);
+ scrollLayout->setSpacing(0);
+ scrollArea_->setWidget(scrollWidget);
+
+ auto peopleEmoji =
+ new Category(tr("Smileys & People"), emoji_provider_.people, scrollWidget);
+ scrollLayout->addWidget(peopleEmoji);
+
+ auto natureEmoji =
+ new Category(tr("Animals & Nature"), emoji_provider_.nature, scrollWidget);
+ scrollLayout->addWidget(natureEmoji);
+
+ auto foodEmoji = new Category(tr("Food & Drink"), emoji_provider_.food, scrollWidget);
+ scrollLayout->addWidget(foodEmoji);
+
+ auto activityEmoji = new Category(tr("Activity"), emoji_provider_.activity, scrollWidget);
+ scrollLayout->addWidget(activityEmoji);
+
+ auto travelEmoji =
+ new Category(tr("Travel & Places"), emoji_provider_.travel, scrollWidget);
+ scrollLayout->addWidget(travelEmoji);
+
+ auto objectsEmoji = new Category(tr("Objects"), emoji_provider_.objects, scrollWidget);
+ scrollLayout->addWidget(objectsEmoji);
+
+ auto symbolsEmoji = new Category(tr("Symbols"), emoji_provider_.symbols, scrollWidget);
+ scrollLayout->addWidget(symbolsEmoji);
+
+ auto flagsEmoji = new Category(tr("Flags"), emoji_provider_.flags, scrollWidget);
+ scrollLayout->addWidget(flagsEmoji);
+
+ contentLayout->addWidget(scrollArea_);
+ contentLayout->addWidget(emojiCategories);
+
+ connect(peopleEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(peopleCategory, &QPushButton::clicked, [this, peopleEmoji]() {
+ this->showCategory(peopleEmoji);
+ });
+
+ connect(natureEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(natureCategory_, &QPushButton::clicked, [this, natureEmoji]() {
+ this->showCategory(natureEmoji);
+ });
+
+ connect(foodEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(foodCategory_, &QPushButton::clicked, [this, foodEmoji]() {
+ this->showCategory(foodEmoji);
+ });
+
+ connect(activityEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(activityCategory, &QPushButton::clicked, [this, activityEmoji]() {
+ this->showCategory(activityEmoji);
+ });
+
+ connect(travelEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(travelCategory, &QPushButton::clicked, [this, travelEmoji]() {
+ this->showCategory(travelEmoji);
+ });
+
+ connect(objectsEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(objectsCategory, &QPushButton::clicked, [this, objectsEmoji]() {
+ this->showCategory(objectsEmoji);
+ });
+
+ connect(symbolsEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(symbolsCategory, &QPushButton::clicked, [this, symbolsEmoji]() {
+ this->showCategory(symbolsEmoji);
+ });
+
+ connect(flagsEmoji, &Category::emojiSelected, this, &Panel::emojiSelected);
+ connect(flagsCategory, &QPushButton::clicked, [this, flagsEmoji]() {
+ this->showCategory(flagsEmoji);
+ });
+}
+
+void
+Panel::showCategory(const Category *category)
+{
+ auto posToGo = category->mapToParent(QPoint()).y();
+ auto current = scrollArea_->verticalScrollBar()->value();
+
+ if (current == posToGo)
+ return;
+
+ // HACK
+ // If we want to go to a previous category and position the label at the top
+ // the 6*50 offset won't work because not all the categories have the same
+ // height. To ensure the category is at the top, we move to the top and go as
+ // normal to the next category.
+ if (current > posToGo)
+ this->scrollArea_->ensureVisible(0, 0, 0, 0);
+
+ posToGo += 6 * 50;
+ this->scrollArea_->ensureVisible(0, posToGo, 0, 0);
+}
+
+void
+Panel::paintEvent(QPaintEvent *event)
+{
+ Q_UNUSED(event);
+
+ QStyleOption opt;
+ opt.init(this);
+ QPainter p(this);
+ style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+
+ DropShadow::draw(p,
+ shadowMargin_,
+ 4.0,
+ QColor(120, 120, 120, 92),
+ QColor(255, 255, 255, 0),
+ 0.0,
+ 1.0,
+ 0.6,
+ width(),
+ height());
+}
diff --git a/src/emoji/Panel.h b/src/emoji/Panel.h
new file mode 100644
index 00000000..ad233c27
--- /dev/null
+++ b/src/emoji/Panel.h
@@ -0,0 +1,66 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <QScrollArea>
+
+#include "Provider.h"
+
+namespace emoji {
+
+class Category;
+
+class Panel : public QWidget
+{
+ Q_OBJECT
+
+public:
+ Panel(QWidget *parent = nullptr);
+
+signals:
+ void mouseLeft();
+ void emojiSelected(const QString &emoji);
+
+protected:
+ void leaveEvent(QEvent *event) override
+ {
+ emit leaving();
+ QWidget::leaveEvent(event);
+ }
+
+ void paintEvent(QPaintEvent *event) override;
+
+signals:
+ void leaving();
+
+private:
+ void showCategory(const Category *category);
+
+ Provider emoji_provider_;
+
+ QScrollArea *scrollArea_;
+
+ int shadowMargin_;
+
+ // Panel dimensions.
+ int width_;
+ int height_;
+
+ int categoryIconSize_;
+};
+} // namespace emoji
diff --git a/src/emoji/PickButton.cpp b/src/emoji/PickButton.cpp
new file mode 100644
index 00000000..608b4fa2
--- /dev/null
+++ b/src/emoji/PickButton.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "emoji/Panel.h"
+#include "emoji/PickButton.h"
+
+using namespace emoji;
+
+// Number of milliseconds after which the panel will be hidden
+// if the mouse cursor is not on top of the widget.
+constexpr int HIDE_TIMEOUT = 300;
+
+PickButton::PickButton(QWidget *parent)
+ : FlatButton(parent)
+ , panel_{nullptr}
+{
+ connect(&hideTimer_, &QTimer::timeout, this, &PickButton::hidePanel);
+ connect(this, &QPushButton::clicked, this, [this]() {
+ if (panel_ && panel_->isVisible()) {
+ hidePanel();
+ return;
+ }
+
+ showPanel();
+ });
+}
+
+void
+PickButton::hidePanel()
+{
+ if (panel_ && !panel_->underMouse()) {
+ hideTimer_.stop();
+ panel_->hide();
+ }
+}
+
+void
+PickButton::showPanel()
+{
+ if (panel_.isNull()) {
+ panel_ = QSharedPointer<Panel>(new Panel(this));
+ connect(panel_.data(), &Panel::emojiSelected, this, &PickButton::emojiSelected);
+ connect(panel_.data(), &Panel::leaving, this, [this]() { panel_->hide(); });
+ }
+
+ if (panel_->isVisible())
+ return;
+
+ QPoint pos(rect().x(), rect().y());
+ pos = this->mapToGlobal(pos);
+
+ auto panel_size = panel_->sizeHint();
+
+ auto x = pos.x() - panel_size.width() + horizontal_distance_;
+ auto y = pos.y() - panel_size.height() - vertical_distance_;
+
+ panel_->move(x, y);
+ panel_->show();
+}
+
+void
+PickButton::leaveEvent(QEvent *e)
+{
+ hideTimer_.start(HIDE_TIMEOUT);
+ FlatButton::leaveEvent(e);
+}
diff --git a/src/emoji/PickButton.h b/src/emoji/PickButton.h
new file mode 100644
index 00000000..97ed8c37
--- /dev/null
+++ b/src/emoji/PickButton.h
@@ -0,0 +1,55 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <QEvent>
+#include <QTimer>
+#include <QWidget>
+
+#include "ui/FlatButton.h"
+
+namespace emoji {
+
+class Panel;
+
+class PickButton : public FlatButton
+{
+ Q_OBJECT
+public:
+ explicit PickButton(QWidget *parent = nullptr);
+
+signals:
+ void emojiSelected(const QString &emoji);
+
+protected:
+ void leaveEvent(QEvent *e) override;
+
+private:
+ void showPanel();
+ void hidePanel();
+
+ // Vertical distance from panel's bottom.
+ int vertical_distance_ = 10;
+
+ // Horizontal distance from panel's bottom right corner.
+ int horizontal_distance_ = 70;
+
+ QSharedPointer<Panel> panel_;
+ QTimer hideTimer_;
+};
+} // namespace emoji
diff --git a/src/main.cpp b/src/main.cpp
index 591d348a..0c196a33 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -127,6 +127,12 @@ main(int argc, char *argv[])
parser.addVersionOption();
parser.process(app);
+ QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Regular.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Italic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Bold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Semibold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf");
+
app.setWindowIcon(QIcon(":/logos/nheko.png"));
http::init();
@@ -147,6 +153,10 @@ main(int argc, char *argv[])
QSettings settings;
QFont font;
+ QString userFontFamily = settings.value("user/font_family", "").toString();
+ if (!userFontFamily.isEmpty()) {
+ font.setFamily(userFontFamily);
+ }
font.setPointSizeF(settings.value("user/font_size", font.pointSizeF()).toDouble());
app.setFont(font);
diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp
index e962d468..1c90eade 100644
--- a/src/timeline/TimelineItem.cpp
+++ b/src/timeline/TimelineItem.cpp
@@ -192,7 +192,8 @@ TimelineItem::init()
emit eventRedacted(event_id_);
});
});
-
+ connect(
+ ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor);
connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt);
connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer);
@@ -594,7 +595,7 @@ TimelineItem::markReceived(bool isEncrypted)
void
TimelineItem::generateBody(const QString &body)
{
- body_ = new TextLabel(body, this);
+ body_ = new TextLabel(replaceEmoji(body), this);
body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {
@@ -603,6 +604,24 @@ TimelineItem::generateBody(const QString &body)
});
}
+void
+TimelineItem::refreshAuthorColor()
+{
+ if (userName_) {
+ QString userColor = Cache::userColor(userName_->toolTip());
+ if (userColor.isEmpty()) {
+ // This attempts to refresh this item since it's not drawn
+ // which allows us to get the background color accurately.
+ qApp->style()->polish(this);
+ // generate user's unique color.
+ auto backCol = backgroundColor().name();
+ userColor =
+ utils::generateContrastingHexColor(userName_->toolTip(), backCol);
+ Cache::insertUserColor(userName_->toolTip(), userColor);
+ }
+ userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
+ }
+}
// The username/timestamp is displayed along with the message body.
void
TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body)
@@ -623,7 +642,7 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam
}
QFont usernameFont;
- usernameFont.setPointSizeF(usernameFont.pointSizeF());
+ usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1);
usernameFont.setWeight(QFont::Medium);
QFontMetrics fm(usernameFont);
@@ -637,6 +656,18 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam
userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop);
userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text()));
+ // TimelineItem isn't displayed. This forces the QSS to get
+ // loaded.
+ QString userColor = Cache::userColor(user_id);
+ if (userColor.isEmpty()) {
+ qApp->style()->polish(this);
+ // generate user's unique color.
+ auto backCol = backgroundColor().name();
+ userColor = utils::generateContrastingHexColor(user_id, backCol);
+ Cache::insertUserColor(user_id, userColor);
+ }
+ userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
+
auto filter = new UserProfileFilter(user_id, userName_);
userName_->installEventFilter(filter);
userName_->setCursor(Qt::PointingHandCursor);
@@ -667,6 +698,25 @@ TimelineItem::generateTimestamp(const QDateTime &time)
QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));
}
+QString
+TimelineItem::replaceEmoji(const QString &body)
+{
+ QString fmtBody = "";
+
+ QVector<uint> utf32_string = body.toUcs4();
+
+ for (auto &code : utf32_string) {
+ // TODO: Be more precise here.
+ if (code > 9000)
+ fmtBody += QString("<span style=\"font-family: emoji;\">") +
+ QString::fromUcs4(&code, 1) + "</span>";
+ else
+ fmtBody += QString::fromUcs4(&code, 1);
+ }
+
+ return fmtBody;
+}
+
void
TimelineItem::setupAvatarLayout(const QString &userName)
{
@@ -837,4 +887,4 @@ TimelineItem::openRawMessageViewer() const
"failed to serialize event ({}, {})", room_id, event_id);
}
});
-}
+}
\ No newline at end of file
diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h
index 8159e370..f81aa658 100644
--- a/src/timeline/TimelineItem.h
+++ b/src/timeline/TimelineItem.h
@@ -132,6 +132,8 @@ private:
class TimelineItem : public QWidget
{
Q_OBJECT
+ Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
+
public:
TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &e,
bool with_sender,
@@ -202,6 +204,9 @@ public:
const QString &room_id,
QWidget *parent);
+ void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
+ QColor backgroundColor() const { return backgroundColor_; }
+
void setUserAvatar(const QImage &pixmap);
DescInfo descriptionMessage() const { return descriptionMsg_; }
QString eventId() const { return event_id_; }
@@ -222,6 +227,9 @@ signals:
void eventRedacted(const QString &event_id);
void redactionFailed(const QString &msg);
+public slots:
+ void refreshAuthorColor();
+
protected:
void paintEvent(QPaintEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
@@ -256,6 +264,7 @@ private:
//! has been acknowledged by the server.
bool isReceived_ = false;
+ QString replaceEmoji(const QString &body);
QString event_id_;
QString room_id_;
@@ -282,6 +291,8 @@ private:
QLabel *timestamp_;
QLabel *userName_;
TextLabel *body_;
+
+ QColor backgroundColor_;
};
template<class Widget>
diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp
index f06b9a5b..4ee9e42a 100644
--- a/src/timeline/widgets/ImageItem.cpp
+++ b/src/timeline/widgets/ImageItem.cpp
@@ -158,6 +158,7 @@ ImageItem::mousePressEvent(QMouseEvent *event)
} else {
auto imgDialog = new dialogs::ImageOverlay(image_);
imgDialog->show();
+ connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs);
}
}
|