summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorredsky17 <joedonofry@gmail.com>2019-02-07 23:15:25 +0000
committerredsky17 <joedonofry@gmail.com>2019-02-07 23:15:25 +0000
commitbb345a9a9f6ded16356f5e0607577bba089ccee4 (patch)
tree1fcdd6a4e6d9795853e43b3b235e845fe63a9c73 /src
parentMerge pull request #12 from rnhmjoj/fix-join (diff)
parentPrevent symlinks from overwriting files (diff)
downloadnheko-bb345a9a9f6ded16356f5e0607577bba089ccee4.tar.xz
Merge branch 'ui-enhancements'
Preparing for 0.6.3 release
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp29
-rw-r--r--src/Cache.h6
-rw-r--r--src/ChatPage.cpp10
-rw-r--r--src/ChatPage.h5
-rw-r--r--src/MainWindow.cpp35
-rw-r--r--src/MainWindow.h6
-rw-r--r--src/RoomInfoListItem.cpp12
-rw-r--r--src/RoomInfoListItem.h11
-rw-r--r--src/RoomList.cpp4
-rw-r--r--src/RoomList.h2
-rw-r--r--src/TextInputWidget.cpp35
-rw-r--r--src/TextInputWidget.h5
-rw-r--r--src/UserSettingsPage.cpp42
-rw-r--r--src/UserSettingsPage.h6
-rw-r--r--src/Utils.cpp131
-rw-r--r--src/Utils.h19
-rw-r--r--src/dialogs/ImageOverlay.cpp10
-rw-r--r--src/dialogs/ImageOverlay.h2
-rw-r--r--src/emoji/Category.cpp90
-rw-r--r--src/emoji/Category.h59
-rw-r--r--src/emoji/ItemDelegate.cpp48
-rw-r--r--src/emoji/ItemDelegate.h43
-rw-r--r--src/emoji/Panel.cpp236
-rw-r--r--src/emoji/Panel.h66
-rw-r--r--src/emoji/PickButton.cpp82
-rw-r--r--src/emoji/PickButton.h55
-rw-r--r--src/main.cpp10
-rw-r--r--src/timeline/TimelineItem.cpp58
-rw-r--r--src/timeline/TimelineItem.h11
-rw-r--r--src/timeline/widgets/ImageItem.cpp1
30 files changed, 1108 insertions, 21 deletions
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); } }