Add new QML-based emoji picker (work in progress)

This is necessary to support having a picker within QML.
Eventually, this should replace the existing widget-based one.
This commit is contained in:
Joseph Donofry 2020-05-13 00:35:26 -04:00
parent 8984661187
commit ee4dcef90f
No known key found for this signature in database
GPG Key ID: E8A1D78EF044B0CB
16 changed files with 15558 additions and 4710 deletions

View File

@ -243,6 +243,7 @@ set(SRC_FILES
# Emoji
src/emoji/Category.cpp
src/emoji/EmojiModel.cpp
src/emoji/ItemDelegate.cpp
src/emoji/Panel.cpp
src/emoji/PickButton.cpp
@ -447,9 +448,11 @@ qt5_wrap_cpp(MOC_HEADERS
# Emoji
src/emoji/Category.h
src/emoji/EmojiModel.h
src/emoji/ItemDelegate.h
src/emoji/Panel.h
src/emoji/PickButton.h
src/emoji/Provider.h
# Timeline
src/timeline/ReactionsModel.h

View File

@ -6,6 +6,7 @@ import QtQuick.Window 2.2
import im.nheko 1.0
import "./delegates"
import "./emoji"
MouseArea {
anchors.left: parent.left
@ -71,17 +72,16 @@ MouseArea {
Layout.preferredHeight: 16
width: 16
}
ImageButton {
EmojiButton {
visible: timelineSettings.buttons
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
id: reactButton
hoverEnabled: true
image: ":/icons/icons/ui/smile.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("React")
onClicked: chat.model.reactAction(model.id)
// onClicked: chat.model.reactAction(model.id)
}
ImageButton {
visible: timelineSettings.buttons

View File

@ -0,0 +1,27 @@
import QtQuick 2.10
import QtQuick.Controls 2.1
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
import "../"
ImageButton {
property var colors: currentActivePalette
image: ":/icons/icons/ui/smile.png"
id: emojiButton
onClicked: emojiPopup.open()
EmojiPicker {
id: emojiPopup
x: Math.round((emojiButton.width - width) / 2)
y: emojiButton.height
width: 7 * 52
height: 6 * 52
colors: emojiButton.colors
model: EmojiProxyModel {
category: Emoji.Category.People
sourceModel: EmojiModel {}
}
}
}

View File

@ -0,0 +1,176 @@
import QtQuick 2.9
import QtQuick.Controls 2.9
import QtQuick.Layouts 1.3
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
import "../"
Popup {
property var colors
property alias model: gridView.model
property var textArea
property string emojiCategory: "people"
id: emojiPopup
margins: 0
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
ColumnLayout {
anchors.fill: parent
// Search field
TextField {
id: emojiSearch
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: parent.width - 4
visible: emojiPopup.model.category === Emoji.Category.Search
placeholderText: qsTr("Search")
selectByMouse: true
rightPadding: clearSearch.width
Timer {
id: searchTimer
interval: 350 // tweak as needed?
onTriggered: emojiPopup.model.filter = emojiSearch.text
}
ToolButton {
id: clearSearch
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
// clear the default hover effects.
background: Item {}
visible: emojiSearch.text !== ''
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
focusPolicy: Qt.NoFocus
onClicked: emojiSearch.clear()
}
onTextChanged: searchTimer.restart()
onVisibleChanged: if (visible) forceActiveFocus()
}
// emoji grid
GridView {
id: gridView
Layout.fillWidth: true
Layout.fillHeight: true
cellWidth: 52
cellHeight: 52
boundsBehavior: Flickable.DragOverBounds
clip: true
// Individual emoji
delegate: AbstractButton {
width: 48
height: 48
contentItem: Text {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pointSize: 36
text: model.unicode
}
background: Rectangle {
anchors.fill: parent
color: hovered ? colors.highlight : 'transparent'
radius: 5
}
hoverEnabled: true
ToolTip.text: model.shortName
ToolTip.visible: hovered
// TODO: emit a signal and maybe add favorites at some point?
//onClicked: textArea.insert(textArea.cursorPosition, modelData.unicode)
}
ScrollBar.vertical: ScrollBar {}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
color: emojiPopup.colors.highlight
}
// Category picker row
Row {
Repeater {
model: ListModel {
// TODO: Would like to get 'simple' icons for the categories
ListElement { label: "😏"; category: Emoji.Category.People }
ListElement { label: "🌲"; category: Emoji.Category.Nature }
ListElement { label: "🍛"; category: Emoji.Category.Food }
ListElement { label: "🚁"; category: Emoji.Category.Activity }
ListElement { label: "🚅"; category: Emoji.Category.Travel }
ListElement { label: "💡"; category: Emoji.Category.Objects }
ListElement { label: "🔣"; category: Emoji.Category.Symbols }
ListElement { label: "🏁"; category: Emoji.Category.Flags }
ListElement { label: "🔍"; category: Emoji.Category.Search }
}
delegate: AbstractButton {
width: 40
height: 40
contentItem: Text {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pointSize: 30
text: model.label
}
background: Rectangle {
anchors.fill: parent
color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent'
radius: 5
}
hoverEnabled: true
ToolTip.text: {
switch (model.category) {
case Emoji.Category.People:
return qsTr('People');
case Emoji.Category.Nature:
return qsTr('Nature');
case Emoji.Category.Food:
return qsTr('Food');
case Emoji.Category.Activity:
return qsTr('Activity');
case Emoji.Category.Travel:
return qsTr('Travel');
case Emoji.Category.Objects:
return qsTr('Objects');
case Emoji.Category.Symbols:
return qsTr('Symbols');
case Emoji.Category.Flags:
return qsTr('Flags');
case Emoji.Category.Search:
return qsTr('Search');
}
}
ToolTip.visible: hovered
onClicked: emojiPopup.model.category = model.category
}
}
}
}
}

View File

@ -120,6 +120,8 @@
<file>qml/Reactions.qml</file>
<file>qml/ScrollHelper.qml</file>
<file>qml/TimelineRow.qml</file>
<file>qml/emoji/EmojiButton.qml</file>
<file>qml/emoji/EmojiPicker.qml</file>
<file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file>
<file>qml/delegates/NoticeMessage.qml</file>

View File

@ -14,8 +14,9 @@ class Emoji(object):
def generate_code(emojis, category):
tmpl = Template('''
const std::vector<Emoji> emoji::Provider::{{ category }} = {
// {{ category.capitalize() }}
{%- for e in emoji %}
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}"},
{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::Emoji::Category::{{ category.capitalize() }}},
{%- endfor %}
};
''')
@ -23,6 +24,19 @@ const std::vector<Emoji> emoji::Provider::{{ category }} = {
d = dict(category=category, emoji=emojis)
print(tmpl.render(d))
def generate_qml_list(**kwargs):
tmpl = Template('''
const QVector<Emoji> emoji::Provider::emoji = {
{%- for c in kwargs.items() %}
// {{ c[0].capitalize() }}
{%- for e in c[1] %}
{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::Emoji::Category::{{ c[0].capitalize() }}},
{%- endfor %}
{%- endfor %}
};
''')
d = dict(kwargs=kwargs)
print(tmpl.render(d))
if __name__ == '__main__':
if len(sys.argv) < 2:
@ -87,3 +101,4 @@ if __name__ == '__main__':
generate_code(objects, 'objects')
generate_code(symbols, 'symbols')
generate_code(flags, 'flags')
generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags)

View File

@ -585,9 +585,7 @@ ChatPage::resetUI()
void
ChatPage::reactMessage(const QString &id)
{
view_manager_->queueReactionMessage(current_room_,
id,
"👀");
view_manager_->queueReactionMessage(current_room_, id, "👀");
}
void

View File

@ -59,12 +59,12 @@ Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent)
emojiListView_->setEditTriggers(QAbstractItemView::NoEditTriggers);
for (const auto &e : emoji) {
data_->unicode = e.unicode;
data_->setUnicode(e.unicode());
auto item = new QStandardItem;
item->setSizeHint(QSize(emojiSize, emojiSize));
QVariant unicode(data_->unicode);
QVariant unicode(data_->unicode());
item->setData(unicode.toString(), Qt::UserRole);
itemModel_->appendRow(item);

110
src/emoji/EmojiModel.cpp Normal file
View File

@ -0,0 +1,110 @@
#include "EmojiModel.h"
#include <Cache.h>
#include <MatrixClient.h>
using namespace emoji;
QHash<int, QByteArray>
EmojiModel::roleNames() const
{
static QHash<int, QByteArray> roles;
if (roles.isEmpty()) {
roles = QAbstractListModel::roleNames();
roles[static_cast<int>(EmojiModel::Roles::Unicode)] = QByteArrayLiteral("unicode");
roles[static_cast<int>(EmojiModel::Roles::ShortName)] =
QByteArrayLiteral("shortName");
roles[static_cast<int>(EmojiModel::Roles::Category)] =
QByteArrayLiteral("category");
roles[static_cast<int>(EmojiModel::Roles::Emoji)] = QByteArrayLiteral("emoji");
}
return roles;
}
int
EmojiModel::rowCount(const QModelIndex &parent) const
{
return parent == QModelIndex() ? Provider::emoji.count() : 0;
}
QVariant
EmojiModel::data(const QModelIndex &index, int role) const
{
if (hasIndex(index.row(), index.column(), index.parent())) {
switch (role) {
case Qt::DisplayRole:
case static_cast<int>(EmojiModel::Roles::Unicode):
return Provider::emoji[index.row()].unicode();
case Qt::ToolTipRole:
case static_cast<int>(EmojiModel::Roles::ShortName):
return Provider::emoji[index.row()].shortName();
case static_cast<int>(EmojiModel::Roles::Category):
return QVariant::fromValue(Provider::emoji[index.row()].category());
case static_cast<int>(EmojiModel::Roles::Emoji):
return QVariant::fromValue(Provider::emoji[index.row()]);
}
}
return {};
}
EmojiProxyModel::EmojiProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
{}
EmojiProxyModel::~EmojiProxyModel() {}
Emoji::Category
EmojiProxyModel::category() const
{
return category_;
}
void
EmojiProxyModel::setCategory(Emoji::Category cat)
{
if (category_ == cat) {
return;
}
category_ = cat;
emit categoryChanged();
invalidateFilter();
}
QString
EmojiProxyModel::filter() const
{
return filterRegExp().pattern();
}
void
EmojiProxyModel::setFilter(const QString &filter)
{
if (filterRegExp().pattern() == filter) {
return;
}
setFilterWildcard(filter);
emit filterChanged();
}
bool
EmojiProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
const Emoji emoji = index.data(static_cast<int>(EmojiModel::Roles::Emoji)).value<Emoji>();
// TODO: Add favorites / recently used
if (category_ != Emoji::Category::Search) {
return emoji.category() == category_;
}
return filterRegExp().isEmpty() ? true : filterRegExp().indexIn(emoji.shortName()) != -1;
}

68
src/emoji/EmojiModel.h Normal file
View File

@ -0,0 +1,68 @@
#pragma once
#include <QAbstractListModel>
#include <QSet>
#include <QSortFilterProxyModel>
#include <QVector>
#include "Provider.h"
namespace emoji {
/*
* Provides access to the emojis in Provider.h to QML
*/
class EmojiModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles
{
Unicode = Qt::UserRole, // unicode of emoji
Category, // category of emoji
ShortName, // shortext of the emoji
Emoji, // Contains everything from the Emoji
};
using QAbstractListModel::QAbstractListModel;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
// TODO: Need a signal for when an emoji is selected
// public signals:
// void emojiSelected(const QString &emoji);
};
class EmojiProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(
emoji::Emoji::Category category READ category WRITE setCategory NOTIFY categoryChanged)
Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged)
public:
explicit EmojiProxyModel(QObject *parent = nullptr);
~EmojiProxyModel() override;
Emoji::Category category() const;
void setCategory(Emoji::Category cat);
QString filter() const;
void setFilter(const QString &filter);
signals:
void categoryChanged();
void filterChanged();
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
Emoji::Category category_ = Emoji::Category::Search;
emoji::Provider emoji_provider_;
};
}

File diff suppressed because it is too large Load Diff

View File

@ -17,22 +17,61 @@
#pragma once
#include <QObject>
#include <QSet>
#include <QString>
#include <QVector>
#include <vector>
namespace emoji {
struct Emoji
class Emoji
{
// Unicode code.
QString unicode;
// Keyboard shortcut e.g :emoji:
QString shortname;
Q_GADGET
Q_PROPERTY(const QString &unicode READ unicode CONSTANT)
Q_PROPERTY(const QString &shortName READ shortName CONSTANT)
Q_PROPERTY(emoji::Emoji::Category category READ category CONSTANT)
public:
enum class Category
{
People,
Nature,
Food,
Activity,
Travel,
Objects,
Symbols,
Flags,
Search
};
Q_ENUM(Category)
Emoji(const QString &unicode = {},
const QString &shortName = {},
Emoji::Category cat = Emoji::Category::Search)
: unicode_(unicode)
, shortName_(shortName)
, category_(cat)
{}
inline const QString &unicode() const { return unicode_; }
inline const QString &shortName() const { return shortName_; }
inline Emoji::Category category() const { return category_; }
inline void setUnicode(const QString &unicode) { unicode_ = unicode; }
private:
QString unicode_;
QString shortName_;
Emoji::Category category_;
};
class Provider
{
public:
// all emoji for QML purposes
static const QVector<Emoji> emoji;
static const std::vector<Emoji> people;
static const std::vector<Emoji> nature;
static const std::vector<Emoji> food;
@ -42,4 +81,6 @@ public:
static const std::vector<Emoji> symbols;
static const std::vector<Emoji> flags;
};
} // namespace emoji
Q_DECLARE_METATYPE(emoji::Emoji)

View File

@ -1384,28 +1384,26 @@ struct SendMessageVisitor
void operator()(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &msg)
{
QString txn_id_qstr = txn_id_qstr_;
TimelineModel *model = model_;
http::client()->send_room_message<mtx::events::msg::Reaction, mtx::events::EventType::Reaction>(
model->room_id_.toStdString(),
txn_id_qstr.toStdString(),
msg.content,
[txn_id_qstr, model](const mtx::responses::EventId &res,
mtx::http::RequestErr err) {
if (err) {
const int status_code =
static_cast<int>(err->status_code);
nhlog::net()->warn("[{}] failed to send message: {} {}",
txn_id_qstr.toStdString(),
err->matrix_error.error,
status_code);
emit model->messageFailed(txn_id_qstr);
}
emit model->messageSent(
txn_id_qstr, QString::fromStdString(res.event_id.to_string()));
});
http::client()
->send_room_message<mtx::events::msg::Reaction, mtx::events::EventType::Reaction>(
model->room_id_.toStdString(),
txn_id_qstr.toStdString(),
msg.content,
[txn_id_qstr, model](const mtx::responses::EventId &res,
mtx::http::RequestErr err) {
if (err) {
const int status_code = static_cast<int>(err->status_code);
nhlog::net()->warn("[{}] failed to send message: {} {}",
txn_id_qstr.toStdString(),
err->matrix_error.error,
status_code);
emit model->messageFailed(txn_id_qstr);
}
emit model->messageSent(
txn_id_qstr, QString::fromStdString(res.event_id.to_string()));
});
}
QString txn_id_qstr_;

View File

@ -126,7 +126,8 @@ class TimelineModel : public QAbstractListModel
int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY
typingUsersChanged)
Q_PROPERTY(QString reaction READ reaction WRITE setReaction NOTIFY reactionChanged RESET resetReaction)
Q_PROPERTY(QString reaction READ reaction WRITE setReaction NOTIFY reactionChanged RESET
resetReaction)
Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply)
Q_PROPERTY(
bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
@ -230,7 +231,7 @@ public slots:
if (!reaction_.isEmpty()) {
reaction_ = "";
emit reactionChanged(reaction_);
}
}
}
QString reply() const { return reply_; }
void setReply(QString newReply)

View File

@ -13,6 +13,8 @@
#include "MxcImageProvider.h"
#include "UserSettingsPage.h"
#include "dialogs/ImageOverlay.h"
#include "emoji/EmojiModel.h"
#include "emoji/Provider.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
@ -72,6 +74,12 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
qmlRegisterUncreatableType<QAbstractItemModel>(
"im.nheko.EmojiModel", 1, 0, "QAbstractItemModel", "Used by proxy models");
qmlRegisterUncreatableType<emoji::Emoji>(
"im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");
#ifdef USE_QUICK_VIEW
view = new QQuickView();
@ -290,7 +298,7 @@ TimelineViewManager::queueReactionMessage(const QString &roomId,
mtx::events::msg::Reaction reaction;
reaction.relates_to.rel_type = mtx::common::RelationType::Annotation;
reaction.relates_to.event_id = reactedEvent.toStdString();
reaction.relates_to.key = reactionKey.toStdString();
reaction.relates_to.key = reactionKey.toStdString();
auto model = models.value(roomId);
model->sendMessage(reaction);

View File

@ -1,5 +1,6 @@
#pragma once
#include <QHash>
#include <QQuickView>
#include <QQuickWidget>
#include <QSharedPointer>
@ -12,6 +13,8 @@
#include "Logging.h"
#include "TimelineModel.h"
#include "Utils.h"
#include "emoji/EmojiModel.h"
#include "emoji/Provider.h"
class MxcImageProvider;
class BlurhashProvider;
@ -102,7 +105,8 @@ private:
QHash<QString, QSharedPointer<TimelineModel>> models;
TimelineModel *timeline_ = nullptr;
bool isInitialSync_ = true;
bool isInitialSync_ = true;
QSharedPointer<UserSettings> settings;
QHash<QString, QColor> userColors;