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"