feat(StatusEmojiPopup): reimplement around C++ EmojiModel
- the new C++ EmojiModel provides a simple wrapper around the existing JSON to facilitate a faster access and to be able to search/filter in QML using SFPM - no more nested GridViews inside Repeaters - get rid of emoji manipulation and search/filter using JavaScript - included the C++ script to generate the emojiList.js
This commit is contained in:
parent
19e0be07d8
commit
9d9fb69e3b
|
@ -26,7 +26,7 @@ Language: Cpp
|
|||
# void formatted_code_again;
|
||||
|
||||
DisableFormat: false
|
||||
Standard: Cpp11
|
||||
Standard: Cpp17
|
||||
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: true
|
||||
|
|
|
@ -50,11 +50,11 @@ SplitView {
|
|||
modal: false
|
||||
anchors.centerIn: parent
|
||||
settings: settings
|
||||
emojiModel: StatusQUtils.Emoji.emojiModel
|
||||
onEmojiSelected: d.lastSelectedEmoji = emoji
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LogsAndControlsPanel {
|
||||
SplitView.minimumHeight: 200
|
||||
SplitView.preferredHeight: 200
|
||||
|
@ -73,8 +73,8 @@ SplitView {
|
|||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Last selected: %1".arg(d.lastSelectedEmoji)
|
||||
Label {
|
||||
text: "Last selected: %1 ('%2')".arg(d.lastSelectedEmoji).arg(settings.recentEmojis[0])
|
||||
}
|
||||
|
||||
Button {
|
||||
|
@ -88,3 +88,5 @@ SplitView {
|
|||
}
|
||||
|
||||
// category: Popups
|
||||
|
||||
// https://www.figma.com/design/Mr3rqxxgKJ2zMQ06UAKiWL/%F0%9F%92%AC-Chat%E2%8E%9CDesktop?node-id=1006-0&t=VC6BL8H0Il3VbDxX-0
|
||||
|
|
|
@ -116,6 +116,7 @@ add_library(StatusQ SHARED
|
|||
include/StatusQ/singleroleaggregator.h
|
||||
include/StatusQ/snapshotmodel.h
|
||||
include/StatusQ/snapshotobject.h
|
||||
include/StatusQ/statusemojimodel.h
|
||||
include/StatusQ/statussyntaxhighlighter.h
|
||||
include/StatusQ/statuswindow.h
|
||||
include/StatusQ/stringutilsinternal.h
|
||||
|
@ -148,6 +149,7 @@ add_library(StatusQ SHARED
|
|||
src/singleroleaggregator.cpp
|
||||
src/snapshotmodel.cpp
|
||||
src/snapshotobject.cpp
|
||||
src/statusemojimodel.cpp
|
||||
src/statussyntaxhighlighter.cpp
|
||||
src/statuswindow.cpp
|
||||
src/stringutilsinternal.cpp
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
|
||||
class StatusEmojiModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QJsonArray emojiJson READ emojiJson WRITE setEmojiJson NOTIFY emojiJsonChanged
|
||||
REQUIRED FINAL)
|
||||
Q_PROPERTY(QStringList categories READ categories CONSTANT FINAL)
|
||||
Q_PROPERTY(QStringList recentEmojis READ recentEmojis WRITE setRecentEmojis NOTIFY
|
||||
recentEmojisChanged FINAL)
|
||||
Q_PROPERTY(QString recentCategoryName READ recentCategoryName CONSTANT FINAL)
|
||||
Q_PROPERTY(QString baseSkinColorName READ baseSkinColorName CONSTANT FINAL)
|
||||
|
||||
public:
|
||||
enum EmojiRoles {
|
||||
AliasesRole = Qt::UserRole + 1,
|
||||
AliasesAsciiRole,
|
||||
CategoryRole,
|
||||
EmojiRole,
|
||||
EmojiOrderRole,
|
||||
KeywordsRole,
|
||||
NameRole,
|
||||
ShortnameRole,
|
||||
UnicodeRole,
|
||||
SkinColorRole,
|
||||
};
|
||||
Q_ENUM(EmojiRoles)
|
||||
|
||||
explicit StatusEmojiModel(QObject *parent = nullptr);
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE QString getEmojiUnicodeFromShortname(const QString &shortname) const;
|
||||
Q_INVOKABLE int getCategoryOffset(int categoryIndex) const;
|
||||
|
||||
Q_INVOKABLE void addRecentEmoji(const QString &hexcode);
|
||||
|
||||
signals:
|
||||
void emojiJsonChanged();
|
||||
void recentEmojisChanged();
|
||||
|
||||
private:
|
||||
QJsonArray emojiJson() const;
|
||||
void setEmojiJson(const QJsonArray &newEmojiJson);
|
||||
QJsonArray m_emojiJson;
|
||||
|
||||
QStringList categories() const;
|
||||
|
||||
QStringList recentEmojis() const;
|
||||
void setRecentEmojis(const QStringList &newRecentEmojis);
|
||||
QStringList m_recentEmojis;
|
||||
QJsonArray m_recentEmojiJson;
|
||||
|
||||
void cleanAndResizeRecentEmojis();
|
||||
void addRecentEmojisToModel(const QStringList &emojiHexcodes);
|
||||
|
||||
QString recentCategoryName() const;
|
||||
QString baseSkinColorName() const;
|
||||
};
|
|
@ -1,10 +1,6 @@
|
|||
import QtQuick 2.13
|
||||
import StatusQ.Core 0.1
|
||||
|
||||
import QtQuick 2.15
|
||||
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property string emojiId: ""
|
||||
|
||||
width: 14
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
/*!
|
||||
\qmltype StatusGridView
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick 2.13
|
||||
import QtQuick 2.15
|
||||
|
||||
import StatusQ 0.1
|
||||
|
||||
import "../../../assets/twemoji/twemoji.js" as Twemoji
|
||||
import "./emojiList.js" as EmojiJSON
|
||||
|
@ -20,6 +22,10 @@ QtObject {
|
|||
readonly property string base: Qt.resolvedUrl("../../../assets/twemoji/")
|
||||
property var emojiJSON: EmojiJSON
|
||||
|
||||
readonly property StatusEmojiModel emojiModel: StatusEmojiModel {
|
||||
emojiJson: EmojiJSON.emoji_json
|
||||
}
|
||||
|
||||
function parse(text, renderSize = size.small, renderFormat = format.svg) {
|
||||
const renderSizes = renderSize.split("x");
|
||||
if (!renderSize.includes("x") || renderSizes.length !== 2) {
|
||||
|
@ -87,13 +93,9 @@ QtObject {
|
|||
return value.match(emojiRegexp, "$1");
|
||||
}
|
||||
function getEmojiUnicode(shortname) {
|
||||
const _emoji = EmojiJSON.emoji_json.find(function(emoji) {
|
||||
return (emoji.shortname === shortname)
|
||||
})
|
||||
|
||||
if (_emoji !== undefined)
|
||||
return _emoji.unicode;
|
||||
return undefined;
|
||||
const _emoji = emojiModel.getEmojiUnicodeFromShortname(shortname);
|
||||
if (!!_emoji)
|
||||
return _emoji;
|
||||
}
|
||||
|
||||
function getEmojiCodepoint(iconCodePoint) {
|
||||
|
@ -119,10 +121,10 @@ QtObject {
|
|||
}
|
||||
|
||||
function getEmojiFromId(emojiId) {
|
||||
let shortcode = Emoji.getShortcodeFromId(emojiId)
|
||||
let emojiUnicode = Emoji.getEmojiUnicode(shortcode)
|
||||
let shortcode = getShortcodeFromId(emojiId)
|
||||
let emojiUnicode = getEmojiUnicode(shortcode)
|
||||
if (emojiUnicode) {
|
||||
return Emoji.fromCodePoint(emojiUnicode)
|
||||
return fromCodePoint(emojiUnicode)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
@ -153,6 +155,6 @@ QtObject {
|
|||
const encodedIcon = getEmojiCodepoint(iconCodePoint)
|
||||
|
||||
// Adding a space because otherwise, some emojis would fuse since emoji is just a string
|
||||
return Emoji.parse(encodedIcon, size || undefined) + ' '
|
||||
return parse(encodedIcon, size || undefined) + ' '
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(emojibase-parser LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core)
|
||||
|
||||
add_executable(emojibase-parser
|
||||
main.cpp
|
||||
emoji-json.qrc
|
||||
)
|
||||
target_link_libraries(emojibase-parser Qt${QT_VERSION_MAJOR}::Core)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS emojibase-parser
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>resources/json/data.raw.json</file>
|
||||
<file>resources/json/joypixels.raw.json</file>
|
||||
<file>resources/json/messages.raw.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
|
@ -0,0 +1,198 @@
|
|||
#include <QCoreApplication>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
auto getDoc(const QString &filename) -> QJsonDocument
|
||||
{
|
||||
QFile dataFile(filename);
|
||||
if (!dataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qWarning() << "Cannot open" << dataFile.fileName() << "file, quit...";
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument dataDoc = QJsonDocument::fromJson(dataFile.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error" << error.error << "while parsing" << dataFile.fileName() << "file:" << error.errorString()
|
||||
<< "; at offset" << error.offset;
|
||||
return {};
|
||||
}
|
||||
return dataDoc;
|
||||
}
|
||||
|
||||
// https://github.com/milesj/emojibase/blob/master/packages/data/en/messages.raw.json
|
||||
auto getGroups() -> QMap<int, QString>
|
||||
{
|
||||
return {
|
||||
{0, QStringLiteral("smileys, people & body")},
|
||||
{1, QStringLiteral("smileys, people & body")},
|
||||
// 2 -> skip components
|
||||
{3, QStringLiteral("animals & nature")},
|
||||
{4, QStringLiteral("food & drink")},
|
||||
{5, QStringLiteral("travel & places")},
|
||||
{6, QStringLiteral("activities")},
|
||||
{7, QStringLiteral("objects")},
|
||||
{8, QStringLiteral("symbols")},
|
||||
{9, QStringLiteral("flags")},
|
||||
};
|
||||
}
|
||||
|
||||
auto getShortnames() -> QMap<QString, QStringList>
|
||||
{
|
||||
// https://github.com/milesj/emojibase/blob/master/packages/data/en/shortcodes/joypixels.raw.json
|
||||
const auto filename = QStringLiteral(":/resources/json/joypixels.raw.json");
|
||||
QJsonDocument dataDoc = getDoc(filename);
|
||||
if (dataDoc.isEmpty() || dataDoc.isNull() || !dataDoc.isObject()) {
|
||||
qWarning() << "Empty, null or invalid JSON in" << filename;
|
||||
return {};
|
||||
}
|
||||
|
||||
constexpr auto wrapInColonsIfNotEmpty = [](const auto &string) {
|
||||
if (string.isEmpty())
|
||||
return string;
|
||||
return QStringLiteral(":%1:").arg(string);
|
||||
};
|
||||
|
||||
QJsonObject shortnamesObj = dataDoc.object();
|
||||
QMap<QString, QStringList> result;
|
||||
|
||||
for (auto it = shortnamesObj.constBegin(); it != shortnamesObj.constEnd(); it++) {
|
||||
const auto key = it.key();
|
||||
const auto value = it.value();
|
||||
QStringList shortnames;
|
||||
if (value.isString())
|
||||
shortnames.append(wrapInColonsIfNotEmpty(value.toString()));
|
||||
else if (value.isArray()) {
|
||||
const auto shortnamesArr = value.toArray();
|
||||
for (int i = 0; i < shortnamesArr.size(); i++)
|
||||
shortnames.append(wrapInColonsIfNotEmpty(shortnamesArr.at(i).toString()));
|
||||
}
|
||||
result.insert(key, shortnames);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto transformEmoji(const QString& label, const QString& hexcode, const QString& emoji, int order, const QString& category, const QJsonArray& keywords,
|
||||
const QJsonArray& aliases_ascii, const QString& shortname, const QJsonArray& aliases, bool hasSkins) -> QJsonObject {
|
||||
return {{"name", label},
|
||||
{"unicode", hexcode.toLower()},
|
||||
{"emoji_order", order},
|
||||
{"category", category},
|
||||
{"emoji", emoji},
|
||||
{"keywords", keywords},
|
||||
{"aliases_ascii", aliases_ascii},
|
||||
{"shortname", shortname},
|
||||
{"aliases", aliases},
|
||||
{"hasSkins", hasSkins}
|
||||
};
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
// https://github.com/milesj/emojibase/blob/master/packages/data/en/data.raw.json
|
||||
QJsonDocument dataDoc = getDoc(QStringLiteral(":/resources/json/data.raw.json"));
|
||||
|
||||
if (dataDoc.isEmpty() || dataDoc.isNull() || !dataDoc.isArray()) {
|
||||
qWarning() << "Unexpected dataFile structure, quit...";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
QJsonArray dataArray = dataDoc.array();
|
||||
const auto size = dataArray.size();
|
||||
|
||||
qWarning() << "Initially found" << size << "emojis";
|
||||
|
||||
// The mapping:
|
||||
// label (string) -> name
|
||||
// hexcode (string) -> unicode
|
||||
// order (int) -> emoji_order
|
||||
// group (int) -> category (string)
|
||||
// tags -> keywords (NEW)
|
||||
// emoji -> emoji (NEW)
|
||||
// shortname (array lookup) -> shortname + aliases (array)
|
||||
// emoticon (array or string!) -> aliases_ascii (array)
|
||||
|
||||
const auto groupsMap = getGroups();
|
||||
const auto shortNamesMap = getShortnames();
|
||||
|
||||
QJsonArray result;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
const auto value = dataArray.at(i).toObject();
|
||||
if (value.isEmpty()) {
|
||||
qWarning() << "Unexpected value type at index " << i << "; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto hexcode = value.value(QStringLiteral("hexcode")).toString();
|
||||
if (!value.contains(QStringLiteral("group"))) {
|
||||
qWarning() << "Skipping modifier emoji:" << hexcode;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto groupId = value.value(QStringLiteral("group")).toInt();
|
||||
if (groupId == 2) {
|
||||
qWarning() << "Skipping skin tone emoji:" << hexcode;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto emoji = value.value(QStringLiteral("emoji")).toString();
|
||||
const auto label = value.value(QStringLiteral("label")).toString();
|
||||
const auto order = value.value(QStringLiteral("order")).toInt();
|
||||
const auto category = groupsMap.value(groupId);
|
||||
const auto keywords = value.value(QStringLiteral("tags")).toArray();
|
||||
|
||||
auto shortnames = shortNamesMap.value(hexcode);
|
||||
const auto shortname = !shortnames.isEmpty() ? shortnames.takeFirst() : QString();
|
||||
const auto aliases = QJsonArray::fromStringList(shortnames);
|
||||
|
||||
qDebug() << "Found emoji:" << emoji << QStringLiteral("(hex: '%1' label: '%2' groupId: %3 order: %4)").arg(hexcode).arg(label).arg(groupId).arg(order);
|
||||
|
||||
QJsonArray aliases_ascii;
|
||||
const auto emoticon = value.value(QStringLiteral("emoticon"));
|
||||
if (emoticon.isString())
|
||||
aliases_ascii.append(emoticon.toString());
|
||||
else if (emoticon.isArray())
|
||||
aliases_ascii = emoticon.toArray();
|
||||
|
||||
const auto skins = value.value(QStringLiteral("skins"));
|
||||
result.append(transformEmoji(label, hexcode, emoji, order, category, keywords, aliases_ascii, shortname, aliases, skins.isArray() && !skins.isNull()));
|
||||
|
||||
if (skins.isArray()) {
|
||||
const auto skinsArr = skins.toArray();
|
||||
for (int j = 0; j < skinsArr.size(); j++) {
|
||||
const auto skinnedEmoji = skinsArr.at(j).toObject();
|
||||
const auto hexcode = skinnedEmoji.value(QStringLiteral("hexcode")).toString();
|
||||
const auto label = skinnedEmoji.value(QStringLiteral("label")).toString();
|
||||
const auto emoji = skinnedEmoji.value(QStringLiteral("emoji")).toString();
|
||||
const auto order = skinnedEmoji.value(QStringLiteral("order")).toInt();
|
||||
result.append(transformEmoji(label, hexcode, emoji, order, category, keywords, aliases_ascii, shortname, aliases, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QJsonDocument resultDoc(result);
|
||||
QFile resultFile(QByteArrayLiteral("/tmp/emojiList.json"));
|
||||
if (resultFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
||||
resultFile.write(resultDoc.toJson(QJsonDocument::Indented));
|
||||
qWarning() << "RESULT: exported" << result.size() << "emojis to" << resultFile.fileName();
|
||||
} else {
|
||||
qWarning() << "Cannot open" << resultFile.fileName() << "for writing the result, quit...";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
qWarning() << "RUNTIME:" << timer.elapsed() << "ms";
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
|
@ -23,6 +23,7 @@
|
|||
#include "StatusQ/rolesrenamingmodel.h"
|
||||
#include "StatusQ/rxvalidator.h"
|
||||
#include "StatusQ/snapshotobject.h"
|
||||
#include "StatusQ/statusemojimodel.h"
|
||||
#include "StatusQ/statussyntaxhighlighter.h"
|
||||
#include "StatusQ/statuswindow.h"
|
||||
#include "StatusQ/stringutilsinternal.h"
|
||||
|
@ -71,6 +72,7 @@ public:
|
|||
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
|
||||
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
||||
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
|
||||
qmlRegisterType<StatusEmojiModel>("StatusQ", 0, 1, "StatusEmojiModel");
|
||||
qmlRegisterType<SumAggregator>("StatusQ", 0, 1, "SumAggregator");
|
||||
qmlRegisterType<FunctionAggregator>("StatusQ", 0, 1, "FunctionAggregator");
|
||||
qmlRegisterType<WritableProxyModel>("StatusQ", 0, 1, "WritableProxyModel");
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
#include "StatusQ/statusemojimodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
constexpr auto kAliases = "aliases";
|
||||
constexpr auto kAliasesAscii = "aliases_ascii";
|
||||
constexpr auto kCategory = "category";
|
||||
constexpr auto kEmoji = "emoji";
|
||||
constexpr auto kEmojiOrder = "emoji_order";
|
||||
constexpr auto kKeywords = "keywords";
|
||||
constexpr auto kName = "name";
|
||||
constexpr auto kShortname = "shortname";
|
||||
constexpr auto kUnicode = "unicode";
|
||||
constexpr auto kHasSkins = "hasSkins";
|
||||
constexpr auto kSkinColor = "skinColor";
|
||||
constexpr auto kBaseColor = "base";
|
||||
|
||||
const auto skinColors = std::array<const char*, 5>{"1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff"};
|
||||
|
||||
constexpr auto MAX_EMOJI_NUMBER = 36;
|
||||
|
||||
constexpr auto kRecentCategoryName = "recent";
|
||||
}
|
||||
|
||||
StatusEmojiModel::StatusEmojiModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{}
|
||||
|
||||
int StatusEmojiModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_recentEmojis.size() + m_emojiJson.size();
|
||||
}
|
||||
|
||||
QVariant StatusEmojiModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
const auto row = index.row();
|
||||
if (row < 0 || row >= rowCount())
|
||||
return {};
|
||||
|
||||
const auto &emoji = row < m_recentEmojiJson.size()
|
||||
? m_recentEmojiJson.at(row).toObject()
|
||||
: m_emojiJson.at(row - m_recentEmojiJson.size()).toObject();
|
||||
const auto unicodeValue = emoji.value(kUnicode).toString();
|
||||
|
||||
switch (static_cast<EmojiRoles>(role)) {
|
||||
case AliasesRole:
|
||||
return emoji.value(kAliases).toArray();
|
||||
case AliasesAsciiRole:
|
||||
return emoji.value(kAliasesAscii).toArray();
|
||||
case CategoryRole:
|
||||
return emoji.value(kCategory).toString();
|
||||
case EmojiRole:
|
||||
return emoji.value(kEmoji).toString();
|
||||
case EmojiOrderRole:
|
||||
return emoji.value(kEmojiOrder).toInt();
|
||||
case KeywordsRole:
|
||||
return emoji.value(kKeywords).toArray();
|
||||
case NameRole:
|
||||
return emoji.value(kName).toString();
|
||||
case ShortnameRole:
|
||||
return emoji.value(kShortname).toString();
|
||||
case UnicodeRole:
|
||||
return unicodeValue;
|
||||
case SkinColorRole:
|
||||
const auto colorIt = std::find_if(skinColors.cbegin(),
|
||||
skinColors.cend(),
|
||||
[unicodeValue](const auto &skinColor) {
|
||||
return unicodeValue.contains(skinColor);
|
||||
});
|
||||
if (colorIt != skinColors.cend())
|
||||
return *colorIt;
|
||||
|
||||
if (emoji.value(kHasSkins).toBool(false))
|
||||
return kBaseColor;
|
||||
return QString();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> StatusEmojiModel::roleNames() const
|
||||
{
|
||||
static const QHash<int, QByteArray> roles{
|
||||
{AliasesRole, kAliases},
|
||||
{AliasesAsciiRole, kAliasesAscii},
|
||||
{CategoryRole, kCategory},
|
||||
{EmojiRole, kEmoji},
|
||||
{EmojiOrderRole, kEmojiOrder},
|
||||
{KeywordsRole, kKeywords},
|
||||
{NameRole, kName},
|
||||
{ShortnameRole, kShortname},
|
||||
{UnicodeRole, kUnicode},
|
||||
{SkinColorRole, kSkinColor},
|
||||
};
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
QJsonArray StatusEmojiModel::emojiJson() const
|
||||
{
|
||||
return m_emojiJson;
|
||||
}
|
||||
|
||||
void StatusEmojiModel::setEmojiJson(const QJsonArray &newEmojiJson)
|
||||
{
|
||||
if (newEmojiJson == m_emojiJson)
|
||||
return;
|
||||
|
||||
beginResetModel();
|
||||
m_emojiJson = newEmojiJson;
|
||||
emit emojiJsonChanged();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QString StatusEmojiModel::getEmojiUnicodeFromShortname(const QString &shortname) const
|
||||
{
|
||||
const auto it = std::find_if(m_emojiJson.cbegin(),
|
||||
m_emojiJson.cend(),
|
||||
[shortname](const auto &emoji) {
|
||||
return emoji.isObject()
|
||||
&& emoji.toObject().value(kShortname).toString()
|
||||
== shortname;
|
||||
});
|
||||
if (it != m_emojiJson.cend())
|
||||
return it->toObject().value(kUnicode).toString();
|
||||
return {};
|
||||
}
|
||||
|
||||
int StatusEmojiModel::getCategoryOffset(int categoryIndex) const {
|
||||
if (categoryIndex <= 0 || categoryIndex >= categories().size())
|
||||
return 0;
|
||||
|
||||
const auto categoryName = categories().at(categoryIndex);
|
||||
const auto catIt = std::find_if(m_emojiJson.cbegin(),
|
||||
m_emojiJson.cend(),
|
||||
[categoryName](const auto &emoji) {
|
||||
return emoji.isObject()
|
||||
&& emoji.toObject().value(kCategory).toString()
|
||||
== categoryName;
|
||||
});
|
||||
if (catIt != m_emojiJson.cend())
|
||||
return std::distance(m_emojiJson.cbegin(), catIt) + m_recentEmojiJson.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
QStringList StatusEmojiModel::categories() const
|
||||
{
|
||||
static const QStringList categories{kRecentCategoryName,
|
||||
QStringLiteral("smileys, people & body"),
|
||||
QStringLiteral("animals & nature"),
|
||||
QStringLiteral("food & drink"),
|
||||
QStringLiteral("travel & places"),
|
||||
QStringLiteral("activities"),
|
||||
QStringLiteral("objects"),
|
||||
QStringLiteral("symbols"),
|
||||
QStringLiteral("flags")};
|
||||
return categories;
|
||||
}
|
||||
|
||||
QStringList StatusEmojiModel::recentEmojis() const
|
||||
{
|
||||
return m_recentEmojis;
|
||||
}
|
||||
|
||||
void StatusEmojiModel::setRecentEmojis(const QStringList &newRecentEmojis)
|
||||
{
|
||||
if (m_recentEmojis == newRecentEmojis)
|
||||
return;
|
||||
m_recentEmojis = newRecentEmojis;
|
||||
cleanAndResizeRecentEmojis();
|
||||
addRecentEmojisToModel(m_recentEmojis);
|
||||
emit recentEmojisChanged();
|
||||
}
|
||||
|
||||
void StatusEmojiModel::addRecentEmoji(const QString &hexcode)
|
||||
{
|
||||
m_recentEmojis.prepend(hexcode);
|
||||
cleanAndResizeRecentEmojis();
|
||||
addRecentEmojisToModel(m_recentEmojis);
|
||||
emit recentEmojisChanged();
|
||||
}
|
||||
|
||||
void StatusEmojiModel::cleanAndResizeRecentEmojis()
|
||||
{
|
||||
m_recentEmojis.removeDuplicates();
|
||||
if (m_recentEmojis.size() > MAX_EMOJI_NUMBER) {
|
||||
while (m_recentEmojis.size() > MAX_EMOJI_NUMBER)
|
||||
m_recentEmojis.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
void StatusEmojiModel::addRecentEmojisToModel(const QStringList &emojiHexcodes)
|
||||
{
|
||||
const auto emojiForHexcode = [&](const QString &hexcode) -> QJsonValue {
|
||||
const auto it = std::find_if(m_emojiJson.cbegin(),
|
||||
m_emojiJson.cend(),
|
||||
[hexcode](const auto &emoji) {
|
||||
return emoji.isObject()
|
||||
&& emoji.toObject().value(kUnicode).toString()
|
||||
== hexcode;
|
||||
});
|
||||
if (it != m_emojiJson.cend())
|
||||
return *it;
|
||||
return {QJsonValue::Null};
|
||||
};
|
||||
|
||||
QJsonArray recentEmojiArr;
|
||||
for (const auto &hexcode : emojiHexcodes) {
|
||||
const auto emoji = emojiForHexcode(hexcode);
|
||||
if (!emoji.isNull() && emoji.isObject()) {
|
||||
auto emojiObj = emoji.toObject();
|
||||
emojiObj[kCategory] = kRecentCategoryName;
|
||||
emojiObj[kEmojiOrder] = 0; // sort by insertion order, before any regular ones
|
||||
recentEmojiArr.append(emojiObj);
|
||||
}
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_recentEmojiJson = recentEmojiArr;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QString StatusEmojiModel::recentCategoryName() const
|
||||
{
|
||||
return kRecentCategoryName;
|
||||
}
|
||||
|
||||
QString StatusEmojiModel::baseSkinColorName() const
|
||||
{
|
||||
return kBaseColor;
|
||||
}
|
|
@ -619,6 +619,7 @@ Item {
|
|||
sourceComponent: StatusEmojiPopup {
|
||||
height: 440
|
||||
settings: localAccountSensitiveSettings
|
||||
emojiModel: SQUtils.Emoji.emojiModel
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,10 @@ Rectangle {
|
|||
textInput.append(text)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
textInput.clear()
|
||||
}
|
||||
|
||||
implicitWidth: layout.implicitWidth + layout.anchors.leftMargin + layout.anchors.rightMargin
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin
|
||||
|
||||
|
@ -775,7 +779,8 @@ Rectangle {
|
|||
const emojis = StatusQUtils.Emoji.emojiJSON.emoji_json.filter(function (emoji) {
|
||||
return emoji.name.includes(emojiPart) ||
|
||||
emoji.shortname.includes(emojiPart) ||
|
||||
emoji.aliases.some(a => a.includes(emojiPart))
|
||||
emoji.aliases.some(a => a.includes(emojiPart)) ||
|
||||
emoji.keywords.some(k => k.includes(emojiPart))
|
||||
})
|
||||
|
||||
emojiSuggestions.openPopup(emojis, emojiPart)
|
||||
|
@ -976,7 +981,7 @@ Rectangle {
|
|||
index = emojiSuggestions.listView.currentIndex
|
||||
}
|
||||
|
||||
const unicode = emojiSuggestions.modelList[index].unicode_alternates || emojiSuggestions.modelList[index].unicode
|
||||
const unicode = emojiSuggestions.modelList[index].unicode
|
||||
replaceWithEmoji(extrapolateCursorPosition(), emojiSuggestions.shortname, unicode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,24 +2,26 @@ import QtQuick 2.15
|
|||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Core.Utils 0.1 as StatusQUtils
|
||||
|
||||
import utils 1.0
|
||||
|
||||
import shared.panels 1.0
|
||||
import shared.controls 1.0
|
||||
|
||||
StatusDropdown {
|
||||
id: popup
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
StatusDropdown {
|
||||
id: root
|
||||
|
||||
required property StatusEmojiModel emojiModel
|
||||
required property var settings
|
||||
|
||||
property var categories: []
|
||||
property alias searchString: searchBox.text
|
||||
property var skinColors: ["1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff"]
|
||||
property string emojiSize: ""
|
||||
|
||||
signal emojiSelected(string emoji, bool atCu)
|
||||
|
@ -28,186 +30,116 @@ StatusDropdown {
|
|||
width: 360
|
||||
padding: 0
|
||||
|
||||
function containsSkinColor(code) {
|
||||
return skinColors.some(function (color) {
|
||||
return code.includes(color)
|
||||
});
|
||||
function addEmoji(hexcode) {
|
||||
const extensionIndex = hexcode.lastIndexOf('.');
|
||||
let iconCodePoint = hexcode
|
||||
if (extensionIndex > -1) {
|
||||
iconCodePoint = iconCodePoint.substring(0, extensionIndex)
|
||||
}
|
||||
|
||||
function addEmoji(emoji) {
|
||||
const MAX_EMOJI_NUMBER = 36
|
||||
const extenstionIndex = emoji.filename.lastIndexOf('.');
|
||||
let iconCodePoint = emoji.filename
|
||||
if (extenstionIndex > -1) {
|
||||
iconCodePoint = iconCodePoint.substring(0, extenstionIndex)
|
||||
}
|
||||
const encodedIcon = StatusQUtils.Emoji.getEmojiCodepoint(iconCodePoint)
|
||||
|
||||
// Split the filename to get all the parts and then encode them from hex to utf8
|
||||
const splitCodePoint = iconCodePoint.split('-')
|
||||
let codePointParts = []
|
||||
splitCodePoint.forEach(function (codePoint) {
|
||||
codePointParts.push(`0x${codePoint}`)
|
||||
})
|
||||
const encodedIcon = String.fromCodePoint(...codePointParts);
|
||||
|
||||
// Add at the start of the list
|
||||
let recentEmojis = settings.recentEmojis
|
||||
if (recentEmojis === undefined) {
|
||||
recentEmojis = []
|
||||
}
|
||||
recentEmojis.unshift(emoji)
|
||||
// Remove duplicates
|
||||
recentEmojis = recentEmojis.filter(function (e, index) {
|
||||
return !recentEmojis.some(function (e2, index2) {
|
||||
return index2 < index && e2.filename === e.filename
|
||||
})
|
||||
})
|
||||
if (recentEmojis.length > MAX_EMOJI_NUMBER) {
|
||||
// remove last one
|
||||
recentEmojis.splice(MAX_EMOJI_NUMBER - 1)
|
||||
}
|
||||
emojiSectionsRepeater.itemAt(0).allEmojis = recentEmojis
|
||||
settings.recentEmojis = recentEmojis
|
||||
root.emojiModel.addRecentEmoji(hexcode)
|
||||
|
||||
// Adding a space because otherwise, some emojis would fuse since emoji is just a string
|
||||
popup.emojiSelected(StatusQUtils.Emoji.parse(encodedIcon, popup.emojiSize || undefined) + ' ', true)
|
||||
popup.close()
|
||||
root.emojiSelected(StatusQUtils.Emoji.parse(encodedIcon, root.emojiSize || undefined) + ' ', true)
|
||||
root.close()
|
||||
}
|
||||
|
||||
function populateCategories() {
|
||||
var categoryNames = {"recent": 0}
|
||||
var newCategories = [[]]
|
||||
|
||||
const emojisWithColors = [
|
||||
"1f64c",
|
||||
"1f44f",
|
||||
"1f44b",
|
||||
"1f44d",
|
||||
"1f44e",
|
||||
"1f44a",
|
||||
"270a",
|
||||
"270c",
|
||||
"1f44c",
|
||||
"270b",
|
||||
"1f450",
|
||||
"1f4aa",
|
||||
"1f64f",
|
||||
"261d",
|
||||
"1f446",
|
||||
"1f447",
|
||||
"1f448",
|
||||
"1f449",
|
||||
"1f595",
|
||||
"1f590",
|
||||
"1f918",
|
||||
"1f596",
|
||||
"270d",
|
||||
"1f485",
|
||||
"1f442",
|
||||
"1f443",
|
||||
"1f476",
|
||||
"1f466",
|
||||
"1f467",
|
||||
"1f468",
|
||||
"1f469",
|
||||
"1f471",
|
||||
"1f474",
|
||||
"1f475",
|
||||
"1f472",
|
||||
"1f473",
|
||||
"1f46e",
|
||||
"1f477",
|
||||
"1f482",
|
||||
"1f385",
|
||||
"1f47c",
|
||||
"1f478",
|
||||
"1f470",
|
||||
"1f6b6",
|
||||
"1f3c3",
|
||||
"1f483",
|
||||
"1f647",
|
||||
"1f481",
|
||||
"1f645",
|
||||
"1f646",
|
||||
"1f64b",
|
||||
"1f64e",
|
||||
"1f64d",
|
||||
"1f487",
|
||||
"1f486",
|
||||
"1f6a3",
|
||||
"1f3ca",
|
||||
"1f3c4",
|
||||
"1f6c0",
|
||||
"26f9",
|
||||
"1f3cb",
|
||||
"1f6b4",
|
||||
"1f6b5",
|
||||
"1f3c7",
|
||||
"1f575"
|
||||
]
|
||||
|
||||
StatusQUtils.Emoji.emojiJSON.emoji_json.forEach(function (emoji) {
|
||||
if (!categoryNames[emoji.category] && categoryNames[emoji.category] !== 0) {
|
||||
categoryNames[emoji.category] = newCategories.length
|
||||
newCategories.push([])
|
||||
}
|
||||
|
||||
if (settings.skinColor !== "") {
|
||||
if (emoji.unicode.includes(settings.skinColor)) {
|
||||
newCategories[categoryNames[emoji.category]].push(Object.assign({}, emoji, {filename: emoji.unicode}));
|
||||
} else {
|
||||
if (!emojisWithColors.includes(emoji.unicode) && !containsSkinColor(emoji.unicode)) {
|
||||
newCategories[categoryNames[emoji.category]].push(Object.assign({}, emoji, {filename: emoji.unicode}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!containsSkinColor(emoji.unicode)) {
|
||||
newCategories[categoryNames[emoji.category]].push(Object.assign({}, emoji, {filename: emoji.unicode}));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (newCategories[categoryNames.recent].length === 0) {
|
||||
newCategories[categoryNames.recent].push({category: "recent", empty: true})
|
||||
}
|
||||
|
||||
categories = newCategories;
|
||||
|
||||
const recent = settings.recentEmojis;
|
||||
if (!!recent) {
|
||||
emojiSectionsRepeater.itemAt(0).allEmojis = recent;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.emojiModel.recentEmojis = settings.recentEmojis
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
searchBox.text = ""
|
||||
searchBox.input.edit.forceActiveFocus()
|
||||
Qt.callLater(populateCategories);
|
||||
emojiGrid.positionViewAtBeginning()
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
popup.emojiSize = ""
|
||||
const recent = root.emojiModel.recentEmojis
|
||||
if (recent.length)
|
||||
settings.recentEmojis = recent
|
||||
searchBox.text = ""
|
||||
root.emojiSize = ""
|
||||
skinToneEmoji.expandSkinColorOptions = false
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property string searchStringLowercase: searchBox.text.toLowerCase()
|
||||
readonly property int headerMargin: 8
|
||||
readonly property int imageWidth: 26
|
||||
readonly property int imageMargin: 4
|
||||
|
||||
readonly property var filteredModel: SortFilterProxyModel {
|
||||
sourceModel: root.emojiModel
|
||||
|
||||
filters: [
|
||||
FastExpressionFilter {
|
||||
expression: {
|
||||
if (model.category === root.emojiModel.recentCategoryName) // don't show/duplicate recents when searching
|
||||
return false
|
||||
|
||||
d.searchStringLowercase
|
||||
return model.name.toLowerCase().includes(d.searchStringLowercase) ||
|
||||
model.shortname.toLowerCase().includes(d.searchStringLowercase) ||
|
||||
model.aliases.some(a => a.includes(d.searchStringLowercase)) ||
|
||||
model.keywords.some(k => k.includes(d.searchStringLowercase))
|
||||
}
|
||||
expectedRoles: ["name", "shortname", "aliases", "keywords", "category"]
|
||||
enabled: !!d.searchStringLowercase
|
||||
},
|
||||
AnyOf {
|
||||
ValueFilter {
|
||||
roleName: "skinColor"
|
||||
value: ""
|
||||
}
|
||||
ValueFilter {
|
||||
roleName: "skinColor"
|
||||
value: root.emojiModel.baseSkinColorName
|
||||
}
|
||||
enabled: settings.skinColor === ""
|
||||
},
|
||||
AnyOf {
|
||||
ValueFilter {
|
||||
roleName: "skinColor"
|
||||
value: ""
|
||||
}
|
||||
ValueFilter {
|
||||
roleName: "skinColor"
|
||||
value: settings.skinColor
|
||||
}
|
||||
enabled: settings.skinColor !== ""
|
||||
}
|
||||
]
|
||||
|
||||
sorters: RoleSorter {
|
||||
roleName: "emoji_order"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Style.current.smallPadding
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
readonly property int headerMargin: 8
|
||||
|
||||
id: emojiHeader
|
||||
Layout.fillWidth: true
|
||||
height: searchBox.height + emojiHeader.headerMargin
|
||||
Layout.preferredHeight: searchBox.height + d.headerMargin
|
||||
|
||||
SearchBox {
|
||||
input.edit.objectName: "StatusEmojiPopup_searchBox"
|
||||
id: searchBox
|
||||
anchors.right: skinToneEmoji.left
|
||||
anchors.rightMargin: emojiHeader.headerMargin
|
||||
anchors.rightMargin: d.headerMargin
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: emojiHeader.headerMargin
|
||||
anchors.topMargin: d.headerMargin
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: emojiHeader.headerMargin
|
||||
anchors.leftMargin: d.headerMargin
|
||||
minimumHeight: 36
|
||||
maximumHeight: 36
|
||||
input.topPadding: 0
|
||||
input.bottomPadding: 0
|
||||
}
|
||||
|
||||
Row {
|
||||
|
@ -222,7 +154,7 @@ StatusDropdown {
|
|||
visible: (opacity > 0.1)
|
||||
anchors.verticalCenter: searchBox.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: emojiHeader.headerMargin
|
||||
anchors.rightMargin: d.headerMargin
|
||||
Repeater {
|
||||
id: skinColorEmojiRepeater
|
||||
model: ["1f590-1f3fb", "1f590-1f3fc", "1f590-1f3fd", "1f590-1f3fe", "1f590-1f3ff", "1f590"]
|
||||
|
@ -235,7 +167,6 @@ StatusDropdown {
|
|||
anchors.fill: parent
|
||||
onClicked: {
|
||||
settings.skinColor = (index === 5) ? "" : modelData.split("-")[1];
|
||||
popup.populateCategories();
|
||||
skinToneEmoji.expandSkinColorOptions = false;
|
||||
}
|
||||
}
|
||||
|
@ -248,7 +179,7 @@ StatusDropdown {
|
|||
height: 22
|
||||
anchors.verticalCenter: searchBox.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: emojiHeader.headerMargin
|
||||
anchors.rightMargin: d.headerMargin
|
||||
visible: !skinToneEmoji.expandSkinColorOptions
|
||||
emojiId: "1f590" + ((settings.skinColor !== "" && visible) ? ("-" + settings.skinColor) : "")
|
||||
MouseArea {
|
||||
|
@ -261,57 +192,58 @@ StatusDropdown {
|
|||
}
|
||||
}
|
||||
|
||||
StatusScrollView {
|
||||
readonly property ScrollBar vScrollBar: ScrollBar.vertical
|
||||
property var categrorySectionHeightRatios: []
|
||||
property int activeCategory: 0
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: d.headerMargin
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 4
|
||||
font.weight: Font.Medium
|
||||
color: Style.current.secondaryText
|
||||
font.pixelSize: Style.current.additionalTextSize
|
||||
text: d.searchStringLowercase ? (d.filteredModel.count ? qsTr("Search Results") : qsTr("No results found"))
|
||||
: emojiGrid.currentCategory
|
||||
font.capitalization: Font.AllUppercase
|
||||
}
|
||||
|
||||
id: scrollView
|
||||
padding: Style.current.smallPadding
|
||||
StatusGridView {
|
||||
id: emojiGrid
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
contentWidth: availableWidth
|
||||
Layout.leftMargin: d.headerMargin
|
||||
|
||||
ScrollBar.vertical.onPositionChanged: function () {
|
||||
if (vScrollBar.position < categrorySectionHeightRatios[scrollView.activeCategory - 1]) {
|
||||
scrollView.activeCategory--
|
||||
} else if (vScrollBar.position > categrorySectionHeightRatios[scrollView.activeCategory]) {
|
||||
scrollView.activeCategory++
|
||||
readonly property string currentCategory: {
|
||||
const item = emojiGrid.itemAt(contentX, contentY + (contentY !== originY ? cellHeight : 0)) // taking the 2nd row; 1st might be split between 2 categories
|
||||
return !!item ? item.category : root.emojiModel.recentCategoryName
|
||||
}
|
||||
readonly property string currentCategoryIndex: root.emojiModel.categories.indexOf(currentCategory)
|
||||
|
||||
model: d.filteredModel
|
||||
|
||||
cellWidth: d.imageWidth + d.imageMargin * 2
|
||||
cellHeight: d.imageWidth + d.imageMargin * 2
|
||||
|
||||
ScrollBar.vertical: StatusScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
function scrollToCategory(category) {
|
||||
if (category === 0) {
|
||||
return vScrollBar.setPosition(0)
|
||||
}
|
||||
vScrollBar.setPosition(categrorySectionHeightRatios[category - 1])
|
||||
}
|
||||
delegate: Item {
|
||||
readonly property string category: model.category
|
||||
|
||||
contentHeight: {
|
||||
var totalHeight = 0
|
||||
var categoryHeights = []
|
||||
for (let i = 0; i < emojiSectionsRepeater.count; i++) {
|
||||
totalHeight += emojiSectionsRepeater.itemAt(i).height
|
||||
categoryHeights.push(totalHeight)
|
||||
width: emojiGrid.cellWidth
|
||||
height: emojiGrid.cellHeight
|
||||
|
||||
StatusEmoji {
|
||||
objectName: "statusEmoji_" + model.shortname.replace(/:/g, "")
|
||||
anchors.centerIn: parent
|
||||
width: d.imageWidth
|
||||
height: d.imageWidth
|
||||
emojiId: model.unicode
|
||||
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
onClicked: root.addEmoji(model.unicode)
|
||||
}
|
||||
var ratios = []
|
||||
categoryHeights.forEach(function (catHeight) {
|
||||
ratios.push(catHeight / totalHeight)
|
||||
})
|
||||
|
||||
categrorySectionHeightRatios = ratios
|
||||
return totalHeight - scrollView.topPadding - scrollView.bottomPadding
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: emojiSectionsRepeater
|
||||
model: popup.categories
|
||||
|
||||
StatusEmojiSection {
|
||||
width: scrollView.availableWidth
|
||||
searchString: popup.searchString
|
||||
addEmoji: popup.addEmoji
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -326,10 +258,10 @@ StatusDropdown {
|
|||
|
||||
StatusTabBarIconButton {
|
||||
icon.name: modelData
|
||||
highlighted: index === scrollView.activeCategory
|
||||
highlighted: !!d.searchStringLowercase ? index === 0 : index == emojiGrid.currentCategoryIndex
|
||||
onClicked: {
|
||||
scrollView.activeCategory = index
|
||||
scrollView.scrollToCategory(index)
|
||||
const offset = d.filteredModel.mapFromSource(root.emojiModel.getCategoryOffset(index))
|
||||
emojiGrid.positionViewAtIndex(offset, GridView.Beginning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
import utils 1.0
|
||||
import shared 1.0
|
||||
import shared.panels 1.0
|
||||
|
||||
Item {
|
||||
id: emojiSection
|
||||
property string searchString: ""
|
||||
readonly property string searchStringLowercase: searchString.toLowerCase()
|
||||
property int imageWidth: 26
|
||||
property int imageMargin: 4
|
||||
property var emojis: []
|
||||
property var allEmojis: modelData
|
||||
property var addEmoji: function () {}
|
||||
|
||||
visible: emojis.length > 0 || !!(modelData && modelData.length && modelData[0].empty && searchString === "")
|
||||
|
||||
anchors.top: index === 0 ? parent.top : parent.children[index - 1].bottom
|
||||
anchors.topMargin: 0
|
||||
|
||||
implicitHeight: visible ? childrenRect.height + Style.current.padding : 0
|
||||
|
||||
StyledText {
|
||||
id: categoryText
|
||||
text: modelData && modelData.length ? modelData[0].category.toUpperCase() : ""
|
||||
color: Style.current.secondaryText
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: noRecentText
|
||||
visible: !!(allEmojis && allEmojis.length && allEmojis[0].empty)
|
||||
text: qsTr("No recent emojis")
|
||||
color: Style.current.secondaryText
|
||||
font.pixelSize: 10
|
||||
anchors.top: categoryText.bottom
|
||||
anchors.topMargin: Style.current.smallPadding
|
||||
}
|
||||
|
||||
onSearchStringLowercaseChanged: {
|
||||
if (emojiSection.searchStringLowercase === "") {
|
||||
this.emojis = allEmojis
|
||||
return
|
||||
}
|
||||
this.emojis = modelData.filter(function (emoji) {
|
||||
if (emoji.empty) {
|
||||
return false
|
||||
}
|
||||
return emoji.name.includes(emojiSection.searchStringLowercase) ||
|
||||
emoji.shortname.includes(emojiSection.searchStringLowercase) ||
|
||||
emoji.aliases.some(a => a.includes(emojiSection.searchStringLowercase)) ||
|
||||
emoji.keywords.some(a => a.includes(emojiSection.searchStringLowercase)) ||
|
||||
emoji.aliases_ascii.some(a => a.includes(emojiSection.searchString))
|
||||
})
|
||||
}
|
||||
|
||||
onAllEmojisChanged: {
|
||||
if (!!emojiSection.allEmojis[0] && this.allEmojis[0].empty) {
|
||||
return
|
||||
}
|
||||
this.emojis = this.allEmojis
|
||||
}
|
||||
|
||||
StatusGridView {
|
||||
id: emojiGrid
|
||||
anchors.top: categoryText.bottom
|
||||
anchors.topMargin: Style.current.smallPadding
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
visible: count > 0
|
||||
cellWidth: emojiSection.imageWidth + emojiSection.imageMargin * 2
|
||||
cellHeight: emojiSection.imageWidth + emojiSection.imageMargin * 2
|
||||
model: emojiSection.emojis
|
||||
focus: true
|
||||
interactive: false
|
||||
|
||||
delegate: Item {
|
||||
id: emojiContainer
|
||||
width: emojiGrid.cellWidth
|
||||
height: emojiGrid.cellHeight
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: emojiSection.imageMargin
|
||||
anchors.leftMargin: emojiSection.imageMargin
|
||||
|
||||
StatusEmoji {
|
||||
objectName: "statusEmoji_" + modelData.shortname.replace(/:/g, "")
|
||||
|
||||
width: emojiSection.imageWidth
|
||||
height: emojiSection.imageWidth
|
||||
emojiId: modelData.filename
|
||||
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
emojiSection.addEmoji(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,8 +12,7 @@ StatusInputListPopup {
|
|||
if(listView.currentIndex < 0 || listView.currentIndex >= emojiSuggestions.modelList.count)
|
||||
return ""
|
||||
|
||||
return emojiSuggestions.modelList[listView.currentIndex].unicode_alternates ||
|
||||
emojiSuggestions.modelList[listView.currentIndex].unicode
|
||||
return emojiSuggestions.modelList[listView.currentIndex].unicode
|
||||
}
|
||||
|
||||
getImageSource: function (modelData) {
|
||||
|
|
|
@ -10,7 +10,6 @@ StatusChatInputImageArea 1.0 StatusChatInputImageArea.qml
|
|||
StatusChatInputReplyArea 1.0 StatusChatInputReplyArea.qml
|
||||
StatusChatInputTextFormationAction 1.0 StatusChatInputTextFormationAction.qml
|
||||
StatusEmojiPopup 1.0 StatusEmojiPopup.qml
|
||||
StatusEmojiSection 1.0 StatusEmojiSection.qml
|
||||
StatusEmojiSuggestionPopup 1.0 StatusEmojiSuggestionPopup.qml
|
||||
StatusGifPopup 1.0 StatusGifPopup.qml
|
||||
StatusImageModal 1.0 StatusImageModal.qml
|
||||
|
|
Loading…
Reference in New Issue