File indexing completed on 2024-04-28 16:51:48

0001 /*
0002     SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "emojierplugin.h"
0008 
0009 #undef signals
0010 #include "emojidict.h"
0011 #include "emojiersettings.h"
0012 #include <QClipboard>
0013 #include <QGuiApplication>
0014 #include <QSortFilterProxyModel>
0015 #include <QStandardPaths>
0016 #include <qqml.h>
0017 
0018 class AbstractEmojiModel : public QAbstractListModel
0019 {
0020     Q_OBJECT
0021 public:
0022     enum EmojiRole { CategoryRole = Qt::UserRole + 1, AnnotationsRole };
0023 
0024     int rowCount(const QModelIndex &parent = {}) const override
0025     {
0026         return parent.isValid() ? 0 : m_emoji.size();
0027     }
0028     QVariant data(const QModelIndex &index, int role) const override
0029     {
0030         if (!checkIndex(index,
0031                         QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid
0032                             | QAbstractItemModel::CheckIndexOption::DoNotUseParent)
0033             || index.column() != 0)
0034             return {};
0035 
0036         const auto &emoji = m_emoji[index.row()];
0037         switch (role) {
0038         case Qt::DisplayRole:
0039             return emoji.content;
0040         case Qt::ToolTipRole:
0041             return emoji.description;
0042         case CategoryRole:
0043             return emoji.categoryName();
0044         case AnnotationsRole:
0045             return emoji.annotations;
0046         }
0047         return {};
0048     }
0049 
0050 protected:
0051     QList<Emoji> m_emoji;
0052 };
0053 
0054 class EmojiModel : public AbstractEmojiModel
0055 {
0056     Q_OBJECT
0057     Q_PROPERTY(QStringList categories MEMBER m_categories CONSTANT)
0058 public:
0059     enum EmojiRole { CategoryRole = Qt::UserRole + 1 };
0060 
0061     EmojiModel()
0062     {
0063         QLocale locale;
0064         QVector<QString> dicts;
0065         const auto bcp = locale.bcp47Name();
0066         const QString dictName = QLatin1String{"plasma/emoji/"} + QString(bcp).replace(QLatin1Char('-'), QLatin1Char('_')) + QLatin1String{".dict"};
0067         const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, dictName);
0068         if (!path.isEmpty()) {
0069             dicts << path;
0070         }
0071 
0072         const auto idxSpecific = bcp.indexOf(QLatin1Char('-'));
0073         if (idxSpecific > 0) {
0074 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0075             const QString genericDictName = QLatin1String{"plasma/emoji/"} + bcp.leftRef(idxSpecific) + QLatin1String{".dict"};
0076 #else
0077             const QString genericDictName = QLatin1String{"plasma/emoji/"} + QStringView(bcp).left(idxSpecific) + QLatin1String{".dict"};
0078 #endif
0079             const QString genericPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, genericDictName);
0080 
0081             if (!genericPath.isEmpty()) {
0082                 dicts << genericPath;
0083             }
0084         }
0085 
0086         // Always fallback to en, because some annotation data only have minimum data.
0087         const QString genericPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/emoji/en.dict"));
0088         dicts << genericPath;
0089 
0090         if (dicts.isEmpty()) {
0091             qWarning() << "could not find emoji dictionaries." << dictName;
0092             return;
0093         }
0094 
0095         QSet<QString> categories;
0096         EmojiDict dict;
0097         // We load in reverse order, because we want to preserve the order in en.dict.
0098         // en.dict almost always gives complete set of data.
0099         for (auto iter = dicts.crbegin(); iter != dicts.crend(); ++iter) {
0100             dict.load(*iter);
0101         }
0102         m_emoji = std::move(dict.m_emojis);
0103         for (const auto &emoji : m_emoji) {
0104             categories.insert(emoji.categoryName());
0105         }
0106         m_categories = categories.values();
0107         m_categories.sort();
0108     }
0109 
0110     Q_SCRIPTABLE QString findFirstEmojiForCategory(const QString &category)
0111     {
0112         for (const Emoji &emoji : m_emoji) {
0113             if (emoji.categoryName() == category)
0114                 return emoji.content;
0115         }
0116         return {};
0117     }
0118 
0119 private:
0120     QStringList m_categories;
0121 };
0122 
0123 class RecentEmojiModel : public AbstractEmojiModel
0124 {
0125     Q_OBJECT
0126     Q_PROPERTY(int count READ rowCount CONSTANT)
0127 public:
0128     RecentEmojiModel()
0129     {
0130         refresh();
0131     }
0132 
0133     Q_SCRIPTABLE void includeRecent(const QString &emoji, const QString &emojiDescription)
0134     {
0135         QStringList recent = m_settings.recent();
0136         QStringList recentDescriptions = m_settings.recentDescriptions();
0137 
0138         const int idx = recent.indexOf(emoji);
0139         if (idx >= 0) {
0140             recent.removeAt(idx);
0141             recentDescriptions.removeAt(idx);
0142         }
0143         recent.prepend(emoji);
0144         recent = recent.mid(0, 50);
0145         m_settings.setRecent(recent);
0146 
0147         recentDescriptions.prepend(emojiDescription);
0148         recentDescriptions = recentDescriptions.mid(0, 50);
0149         m_settings.setRecentDescriptions(recentDescriptions);
0150         m_settings.save();
0151 
0152         refresh();
0153     }
0154 
0155     Q_INVOKABLE void clearHistory()
0156     {
0157         m_settings.setRecent(QStringList());
0158         m_settings.setRecentDescriptions(QStringList());
0159         m_settings.save();
0160 
0161         refresh();
0162     }
0163 
0164 private:
0165     void refresh()
0166     {
0167         beginResetModel();
0168         auto recent = m_settings.recent();
0169         auto recentDescriptions = m_settings.recentDescriptions();
0170         int i = 0;
0171         m_emoji.clear();
0172         for (const QString &c : recent) {
0173             m_emoji += {c, recentDescriptions.at(i++), 0, {}};
0174         }
0175         endResetModel();
0176     }
0177 
0178     EmojierSettings m_settings;
0179 };
0180 
0181 class CategoryModelFilter : public QSortFilterProxyModel
0182 {
0183     Q_OBJECT
0184     Q_PROPERTY(QString category READ category WRITE setCategory)
0185 public:
0186     QString category() const
0187     {
0188         return m_category;
0189     }
0190     void setCategory(const QString &category)
0191     {
0192         if (m_category != category) {
0193             m_category = category;
0194             invalidateFilter();
0195         }
0196     }
0197 
0198     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
0199     {
0200         return m_category.isEmpty() || sourceModel()->index(source_row, 0, source_parent).data(EmojiModel::CategoryRole).toString() == m_category;
0201     }
0202 
0203 private:
0204     QString m_category;
0205 };
0206 
0207 class SearchModelFilter : public QSortFilterProxyModel
0208 {
0209     Q_OBJECT
0210     Q_PROPERTY(QString search READ search WRITE setSearch)
0211 public:
0212     QString search() const
0213     {
0214         return m_search;
0215     }
0216     void setSearch(const QString &search)
0217     {
0218         if (m_search != search) {
0219             m_search = search;
0220             invalidateFilter();
0221         }
0222     }
0223 
0224     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
0225     {
0226         const auto idx = sourceModel()->index(source_row, 0, source_parent);
0227         return idx.data(Qt::ToolTipRole).toString().contains(m_search, Qt::CaseInsensitive)
0228             || idx.data(AbstractEmojiModel::AnnotationsRole).toStringList().contains(m_search, Qt::CaseInsensitive);
0229     }
0230 
0231 private:
0232     QString m_search;
0233 };
0234 
0235 class CopyHelperPrivate : public QObject
0236 {
0237     Q_OBJECT
0238 public:
0239     Q_INVOKABLE static void copyTextToClipboard(const QString &text)
0240     {
0241         QClipboard *clipboard = qGuiApp->clipboard();
0242         clipboard->setText(text, QClipboard::Clipboard);
0243         clipboard->setText(text, QClipboard::Selection);
0244     }
0245 };
0246 
0247 void EmojierDeclarativePlugin::registerTypes(const char *uri)
0248 {
0249     Q_ASSERT(uri == QByteArray("org.kde.plasma.emoji"));
0250 
0251     qmlRegisterType<EmojiModel>(uri, 1, 0, "EmojiModel");
0252     qmlRegisterType<CategoryModelFilter>(uri, 1, 0, "CategoryModelFilter");
0253     qmlRegisterType<SearchModelFilter>(uri, 1, 0, "SearchModelFilter");
0254     qmlRegisterType<RecentEmojiModel>(uri, 1, 0, "RecentEmojiModel");
0255     qmlRegisterSingletonType<CopyHelperPrivate>(uri, 1, 0, "CopyHelper", [](QQmlEngine *, QJSEngine *) -> QObject * {
0256         return new CopyHelperPrivate;
0257     });
0258 }
0259 
0260 #include "emojierplugin.moc"