Make default completer complete custom emoji

This commit is contained in:
Nicolas Werner 2023-05-25 21:50:54 +02:00
parent dd74bdc697
commit 51084748ee
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
12 changed files with 82 additions and 228 deletions

View file

@ -356,8 +356,6 @@ set(SRC_FILES
src/dialogs/ReCaptcha.h src/dialogs/ReCaptcha.h
# Emoji # Emoji
src/emoji/EmojiModel.cpp
src/emoji/EmojiModel.h
src/emoji/Provider.cpp src/emoji/Provider.cpp
src/emoji/Provider.h src/emoji/Provider.h

View file

@ -91,11 +91,9 @@ Open username completer.
Open room completer. Open room completer.
*:*:: *:*::
Open unicode emoji picker. Open the emoji picker. Unicode emoji are inserted directly. Custom emoji will
insert the HTML code for them into the input line. You can configure custom
*~*:: emoji in the room settings.
Open custom emoji picker. Requires an image pack with custom emojis. Selecting
an emoji will add HTML code for the inline image into the input line.
== KEYBOARD SHORTCUTS == KEYBOARD SHORTCUTS

View file

@ -21,7 +21,7 @@ Control {
property int avatarHeight: 24 property int avatarHeight: 24
property int avatarWidth: 24 property int avatarWidth: 24
property int rowMargin: 0 property int rowMargin: 0
property int rowSpacing: 5 property int rowSpacing: Nheko.paddingSmall
property alias count: listView.count property alias count: listView.count
signal completionClicked(string completion) signal completionClicked(string completion)
@ -199,16 +199,35 @@ Control {
spacing: rowSpacing spacing: rowSpacing
Label { Label {
visible: !!model.unicode
text: model.unicode text: model.unicode
color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text
font: Settings.emojiFont font: Settings.emojiFont
} }
Avatar {
visible: !model.unicode
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.shortcode
//userid: model.shortcode
url: (model.url ? model.url : "").replace("mxc://", "image://MxcImage/")
enabled: false
crop: false
}
Label { Label {
text: model.shortName Layout.leftMargin: Nheko.paddingSmall
Layout.rightMargin: Nheko.paddingSmall
text: model.shortcode
color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text
} }
Label {
text: "(" + model.packname + ")"
color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText
}
} }
} }
@ -237,39 +256,6 @@ Control {
} }
DelegateChoice {
roleValue: "customEmoji"
RowLayout {
id: del
anchors.centerIn: parent
spacing: rowSpacing
Avatar {
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.shortcode
//userid: model.shortcode
url: model.url.replace("mxc://", "image://MxcImage/")
enabled: false
crop: false
}
Label {
text: model.shortcode
color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text
}
Label {
text: "(" + model.packname + ")"
color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText
}
}
}
DelegateChoice { DelegateChoice {
roleValue: "room" roleValue: "room"

View file

@ -169,8 +169,6 @@ Rectangle {
messageInput.openCompleter(selectionStart-1, "emoji"); messageInput.openCompleter(selectionStart-1, "emoji");
} else if (lastChar == '#') { } else if (lastChar == '#') {
messageInput.openCompleter(selectionStart-1, "roomAliases"); messageInput.openCompleter(selectionStart-1, "roomAliases");
} else if (lastChar == "~") {
messageInput.openCompleter(selectionStart-1, "customEmoji");
} else if (lastChar == "/" && cursorPosition == 1) { } else if (lastChar == "/" && cursorPosition == 1) {
messageInput.openCompleter(selectionStart-1, "command"); messageInput.openCompleter(selectionStart-1, "command");
} }

View file

@ -6,14 +6,13 @@
#include "Cache_p.h" #include "Cache_p.h"
#include "CompletionModelRoles.h" #include "CompletionModelRoles.h"
#include "emoji/Provider.h"
CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId, CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId, QObject *parent)
bool stickers,
QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, room_id(roomId) , room_id(roomId)
{ {
auto packs = cache::client()->getImagePacks(room_id, stickers); auto packs = cache::client()->getImagePacks(room_id, false);
for (const auto &pack : packs) { for (const auto &pack : packs) {
QString packname = QString packname =
@ -32,7 +31,7 @@ CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId,
int int
CombinedImagePackModel::rowCount(const QModelIndex &) const CombinedImagePackModel::rowCount(const QModelIndex &) const
{ {
return (int)images.size(); return (int)(emoji::Provider::emoji.size() + images.size());
} }
QHash<int, QByteArray> QHash<int, QByteArray>
@ -46,37 +45,61 @@ CombinedImagePackModel::roleNames() const
{Roles::ShortCode, "shortcode"}, {Roles::ShortCode, "shortcode"},
{Roles::Body, "body"}, {Roles::Body, "body"},
{Roles::PackName, "packname"}, {Roles::PackName, "packname"},
{Roles::OriginalRow, "originalRow"}, {Roles::Unicode, "unicode"},
}; };
} }
QVariant QVariant
CombinedImagePackModel::data(const QModelIndex &index, int role) const CombinedImagePackModel::data(const QModelIndex &index, int role) const
{ {
using emoji::Provider;
if (hasIndex(index.row(), index.column(), index.parent())) { if (hasIndex(index.row(), index.column(), index.parent())) {
if (index.row() < (int)emoji::Provider::emoji.size()) {
switch (role) {
case CompletionModel::CompletionRole:
case Roles::Unicode:
return emoji::Provider::emoji[index.row()].unicode();
case Qt::ToolTipRole:
return Provider::emoji[index.row()].shortName() + ", " +
Provider::emoji[index.row()].unicodeName();
case CompletionModel::SearchRole2:
case Roles::Body:
return Provider::emoji[index.row()].unicodeName();
case CompletionModel::SearchRole:
case Roles::ShortCode:
return Provider::emoji[index.row()].shortName();
case Roles::PackName:
return emoji::categoryToName(Provider::emoji[index.row()].category);
default:
return {};
}
} else {
int row = index.row() - static_cast<int>(emoji::Provider::emoji.size());
switch (role) { switch (role) {
case CompletionModel::CompletionRole: case CompletionModel::CompletionRole:
return QStringLiteral( return QStringLiteral(
"<img data-mx-emoticon height=\"32\" src=\"%1\" alt=\"%2\" title=\"%2\">") "<img data-mx-emoticon height=\"32\" src=\"%1\" alt=\"%2\" title=\"%2\">")
.arg(QString::fromStdString(images[index.row()].image.url).toHtmlEscaped(), .arg(QString::fromStdString(images[row].image.url).toHtmlEscaped(),
!images[index.row()].image.body.empty() !images[row].image.body.empty()
? QString::fromStdString(images[index.row()].image.body) ? QString::fromStdString(images[row].image.body)
: images[index.row()].shortcode); : images[row].shortcode);
case Roles::Url: case Roles::Url:
return QString::fromStdString(images[index.row()].image.url); return QString::fromStdString(images[row].image.url);
case CompletionModel::SearchRole: case CompletionModel::SearchRole:
case Roles::ShortCode: case Roles::ShortCode:
return images[index.row()].shortcode; return images[row].shortcode;
case CompletionModel::SearchRole2: case CompletionModel::SearchRole2:
case Roles::Body: case Roles::Body:
return QString::fromStdString(images[index.row()].image.body); return QString::fromStdString(images[row].image.body);
case Roles::PackName: case Roles::PackName:
return images[index.row()].packname; return images[row].packname;
case Roles::OriginalRow: case Roles::Unicode:
return index.row(); return QString();
default: default:
return {}; return {};
} }
} }
}
return {}; return {};
} }

View file

@ -18,27 +18,14 @@ public:
ShortCode, ShortCode,
Body, Body,
PackName, PackName,
OriginalRow, Unicode,
}; };
CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); CombinedImagePackModel(const std::string &roomId, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
mtx::events::msc2545::PackImage imageAt(int row)
{
if (row < 0 || static_cast<size_t>(row) >= images.size())
return {};
return images.at(static_cast<size_t>(row)).image;
}
QString shortcodeAt(int row)
{
if (row < 0 || static_cast<size_t>(row) >= images.size())
return {};
return images.at(static_cast<size_t>(row)).shortcode;
}
private: private:
std::string room_id; std::string room_id;

View file

@ -18,8 +18,8 @@ Q_DECLARE_METATYPE(TextEmoji)
Q_DECLARE_METATYPE(SectionDescription) Q_DECLARE_METATYPE(SectionDescription)
Q_DECLARE_METATYPE(QList<SectionDescription>) Q_DECLARE_METATYPE(QList<SectionDescription>)
static QString QString
categoryToName(emoji::Emoji::Category cat) emoji::categoryToName(emoji::Emoji::Category cat)
{ {
switch (cat) { switch (cat) {
case emoji::Emoji::Category::People: case emoji::Emoji::Category::People:

View file

@ -41,7 +41,6 @@
#include "UsersModel.h" #include "UsersModel.h"
#include "Utils.h" #include "Utils.h"
#include "dock/Dock.h" #include "dock/Dock.h"
#include "emoji/EmojiModel.h"
#include "emoji/Provider.h" #include "emoji/Provider.h"
#include "encryption/DeviceVerificationFlow.h" #include "encryption/DeviceVerificationFlow.h"
#include "encryption/SelfVerificationStatus.h" #include "encryption/SelfVerificationStatus.h"
@ -289,9 +288,6 @@ MainWindow::registerQmlTypes()
"FilteredCommunitiesModel", "FilteredCommunitiesModel",
QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel")); QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel"));
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterUncreatableType<emoji::Emoji>(
"im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models"));
qmlRegisterUncreatableType<MediaUpload>( qmlRegisterUncreatableType<MediaUpload>(
"im.nheko", 1, 0, "MediaUpload", QStringLiteral("MediaUploads can not be created in Qml")); "im.nheko", 1, 0, "MediaUpload", QStringLiteral("MediaUploads can not be created in Qml"));
qmlRegisterUncreatableMetaObject(emoji::staticMetaObject, qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,

View file

@ -1,78 +0,0 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "EmojiModel.h"
#include <Cache.h>
#include <MatrixClient.h>
#include "CompletionModelRoles.h"
using namespace emoji;
int
EmojiModel::categoryToIndex(int category)
{
auto dist = std::distance(
Provider::emoji.begin(),
std::lower_bound(Provider::emoji.begin(),
Provider::emoji.end(),
static_cast<Emoji::Category>(category),
[](const struct Emoji &e, Emoji::Category c) { return e.category < c; }));
return static_cast<int>(dist);
}
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::UnicodeName)] = QByteArrayLiteral("unicodeName");
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() ? (int)Provider::emoji.size() : 0;
}
QVariant
EmojiModel::data(const QModelIndex &index, int role) const
{
if (hasIndex(index.row(), index.column(), index.parent())) {
switch (role) {
case Qt::DisplayRole:
case CompletionModel::CompletionRole:
case static_cast<int>(EmojiModel::Roles::Unicode):
return Provider::emoji[index.row()].unicode();
case Qt::ToolTipRole:
return Provider::emoji[index.row()].shortName() + ", " +
Provider::emoji[index.row()].unicodeName();
case CompletionModel::SearchRole2:
case static_cast<int>(EmojiModel::Roles::UnicodeName):
return Provider::emoji[index.row()].unicodeName();
case CompletionModel::SearchRole:
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 {};
}

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#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
UnicodeName, // true unicode name of the emoji
Emoji, // Contains everything from the Emoji
};
using QAbstractListModel::QAbstractListModel;
Q_INVOKABLE int categoryToIndex(int category);
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;
};
}

View file

@ -91,5 +91,7 @@ public:
static const std::array<Emoji, 3681> emoji; static const std::array<Emoji, 3681> emoji;
}; };
QString
categoryToName(emoji::Emoji::Category cat);
} // namespace emoji } // namespace emoji
Q_DECLARE_METATYPE(emoji::Emoji) Q_DECLARE_METATYPE(emoji::Emoji)

View file

@ -27,7 +27,6 @@
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "UsersModel.h" #include "UsersModel.h"
#include "Utils.h" #include "Utils.h"
#include "emoji/EmojiModel.h"
#include "encryption/VerificationManager.h" #include "encryption/VerificationManager.h"
#include "voip/CallManager.h" #include "voip/CallManager.h"
#include "voip/WebRTCSession.h" #include "voip/WebRTCSession.h"
@ -454,15 +453,10 @@ TimelineViewManager::completerFor(const QString &completerName, const QString &r
userModel->setParent(proxy); userModel->setParent(proxy);
return proxy; return proxy;
} else if (completerName == QLatin1String("emoji")) { } else if (completerName == QLatin1String("emoji")) {
auto emojiModel = new emoji::EmojiModel(); auto emojiModel = new CombinedImagePackModel(roomId.toStdString());
auto proxy = new CompletionProxyModel(emojiModel); auto proxy = new CompletionProxyModel(emojiModel);
emojiModel->setParent(proxy); emojiModel->setParent(proxy);
return proxy; return proxy;
} else if (completerName == QLatin1String("allemoji")) {
auto emojiModel = new emoji::EmojiModel();
auto proxy = new CompletionProxyModel(emojiModel, 1, static_cast<size_t>(-1) / 4);
emojiModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("room")) { } else if (completerName == QLatin1String("room")) {
auto roomModel = new RoomsModel(false); auto roomModel = new RoomsModel(false);
auto proxy = new CompletionProxyModel(roomModel, 4); auto proxy = new CompletionProxyModel(roomModel, 4);
@ -473,22 +467,12 @@ TimelineViewManager::completerFor(const QString &completerName, const QString &r
auto proxy = new CompletionProxyModel(roomModel); auto proxy = new CompletionProxyModel(roomModel);
roomModel->setParent(proxy); roomModel->setParent(proxy);
return proxy; return proxy;
} else if (completerName == QLatin1String("stickers")) {
auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true);
auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4);
stickerModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("emojigrid")) { } else if (completerName == QLatin1String("emojigrid")) {
auto stickerModel = new GridImagePackModel(roomId.toStdString(), false); auto stickerModel = new GridImagePackModel(roomId.toStdString(), false);
return stickerModel; return stickerModel;
} else if (completerName == QLatin1String("stickergrid")) { } else if (completerName == QLatin1String("stickergrid")) {
auto stickerModel = new GridImagePackModel(roomId.toStdString(), true); auto stickerModel = new GridImagePackModel(roomId.toStdString(), true);
return stickerModel; return stickerModel;
} else if (completerName == QLatin1String("customEmoji")) {
auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), false);
auto proxy = new CompletionProxyModel(stickerModel);
stickerModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("command")) { } else if (completerName == QLatin1String("command")) {
auto commandCompleter = new CommandCompleter(); auto commandCompleter = new CommandCompleter();
auto proxy = new CompletionProxyModel(commandCompleter); auto proxy = new CompletionProxyModel(commandCompleter);