File indexing completed on 2024-05-12 05:35:36

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 int AbstractEmojiModel::rowCount(const QModelIndex &parent) const
0019 {
0020     return parent.isValid() ? 0 : m_emoji.size();
0021 }
0022 
0023 QVariant AbstractEmojiModel::data(const QModelIndex &index, int role) const
0024 {
0025     if (!checkIndex(index,
0026                     QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid
0027                         | QAbstractItemModel::CheckIndexOption::DoNotUseParent)
0028         || index.column() != 0)
0029         return {};
0030 
0031     const auto &emoji = m_emoji[index.row()];
0032     switch (role) {
0033     case Qt::DisplayRole:
0034         return emoji.content;
0035     case Qt::ToolTipRole:
0036         return emoji.description;
0037     case CategoryRole:
0038         return emoji.categoryName();
0039     case AnnotationsRole:
0040         return emoji.annotations;
0041     }
0042     return {};
0043 }
0044 
0045 EmojiModel::EmojiModel()
0046 {
0047     const QHash<QString, QString> specialCases{{QLatin1String{"zh-TW"}, QLatin1String{"zh_Hant"}}, {QLatin1String{"zh-HK"}, QLatin1String{"zh_Hant_HK"}}};
0048     QLocale locale;
0049     QStringList dicts;
0050     auto bcp = locale.bcp47Name();
0051     bcp = specialCases.value(bcp, bcp);
0052     bcp.replace(QLatin1Char('-'), QLatin1Char('_'));
0053 
0054     const QString dictName = QLatin1String{"plasma/emoji/"} + bcp + QLatin1String{".dict"};
0055     const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, dictName);
0056     if (!path.isEmpty()) {
0057         dicts << path;
0058     }
0059 
0060     for (qsizetype underscoreIndex = -1; (underscoreIndex = bcp.lastIndexOf(QLatin1Char('_'), underscoreIndex)) != -1; --underscoreIndex) {
0061         const QString genericDictName = QLatin1String{"plasma/emoji/"} + QStringView(bcp).left(underscoreIndex) + QLatin1String{".dict"};
0062         const QString genericPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, genericDictName);
0063 
0064         if (!genericPath.isEmpty()) {
0065             dicts << genericPath;
0066         }
0067     }
0068 
0069     // Always fallback to en, because some annotation data only have minimum data.
0070     const QString genericPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/emoji/en.dict"));
0071     dicts << genericPath;
0072 
0073     if (dicts.isEmpty()) {
0074         qWarning() << "could not find emoji dictionaries." << dictName;
0075         return;
0076     }
0077 
0078     QSet<QString> categories;
0079     EmojiDict dict;
0080     // We load in reverse order, because we want to preserve the order in en.dict.
0081     // en.dict almost always gives complete set of data.
0082     for (auto iter = dicts.crbegin(); iter != dicts.crend(); ++iter) {
0083         dict.load(*iter);
0084     }
0085     m_emoji = std::move(dict.m_emojis);
0086     for (const auto &emoji : m_emoji) {
0087         categories.insert(emoji.categoryName());
0088     }
0089     m_categories = categories.values();
0090     m_categories.sort();
0091 }
0092 
0093 QString EmojiModel::findFirstEmojiForCategory(const QString &category)
0094 {
0095     for (const Emoji &emoji : m_emoji) {
0096         if (emoji.categoryName() == category)
0097             return emoji.content;
0098     }
0099     return {};
0100 }
0101 
0102 RecentEmojiModel::RecentEmojiModel()
0103 {
0104     refresh();
0105 }
0106 
0107 void RecentEmojiModel::includeRecent(const QString &emoji, const QString &emojiDescription)
0108 {
0109     QStringList recent = m_settings.recent();
0110     QStringList recentDescriptions = m_settings.recentDescriptions();
0111 
0112     const int idx = recent.indexOf(emoji);
0113     if (idx > 0) {
0114         Q_EMIT beginMoveRows(QModelIndex(), idx, idx, QModelIndex(), 0);
0115         recent.move(idx, 0);
0116         recentDescriptions.move(idx, 0);
0117         m_emoji.move(idx, 0);
0118         Q_EMIT endMoveRows();
0119     } else if (idx < 0) {
0120         Q_EMIT beginInsertRows(QModelIndex(), 0, 0);
0121         recent.prepend(emoji);
0122         recentDescriptions.prepend(emojiDescription);
0123         m_emoji.prepend(Emoji{emoji, emojiDescription, 0, {}});
0124         Q_EMIT endInsertRows();
0125 
0126         if (recent.size() > 50) {
0127             Q_EMIT beginRemoveRows(QModelIndex(), 50, recent.size() - 1);
0128             recent = recent.mid(0, 50);
0129             recentDescriptions = recentDescriptions.mid(0, 50);
0130             m_emoji = m_emoji.mid(0, 50);
0131             Q_EMIT endRemoveRows();
0132         }
0133     }
0134 
0135     m_settings.setRecent(recent);
0136     m_settings.setRecentDescriptions(recentDescriptions);
0137     m_settings.save();
0138 }
0139 
0140 void RecentEmojiModel::clearHistory()
0141 {
0142     m_settings.setRecent(QStringList());
0143     m_settings.setRecentDescriptions(QStringList());
0144     m_settings.save();
0145 
0146     refresh();
0147 }
0148 
0149 void RecentEmojiModel::refresh()
0150 {
0151     beginResetModel();
0152     auto recent = m_settings.recent();
0153     auto recentDescriptions = m_settings.recentDescriptions();
0154     int i = 0;
0155     m_emoji.clear();
0156     for (const QString &c : recent) {
0157         m_emoji += {c, recentDescriptions.at(i++), 0, {}};
0158     }
0159     endResetModel();
0160 }
0161 
0162 QString CategoryModelFilter::category() const
0163 {
0164     return m_category;
0165 }
0166 void CategoryModelFilter::setCategory(const QString &category)
0167 {
0168     if (m_category != category) {
0169         m_category = category;
0170         invalidateFilter();
0171     }
0172 }
0173 
0174 bool CategoryModelFilter::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
0175 {
0176     return m_category.isEmpty() || sourceModel()->index(source_row, 0, source_parent).data(EmojiModel::CategoryRole).toString() == m_category;
0177 }
0178 
0179 QString SearchModelFilter::search() const
0180 {
0181     return m_search;
0182 }
0183 void SearchModelFilter::setSearch(const QString &search)
0184 {
0185     if (m_search != search) {
0186         m_search = search;
0187         invalidateFilter();
0188     }
0189 }
0190 
0191 bool SearchModelFilter::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
0192 {
0193     const auto idx = sourceModel()->index(source_row, 0, source_parent);
0194     return idx.data(Qt::ToolTipRole).toString().contains(m_search, Qt::CaseInsensitive)
0195         || idx.data(AbstractEmojiModel::AnnotationsRole).toStringList().contains(m_search, Qt::CaseInsensitive);
0196 }
0197 
0198 void CopyHelperPrivate::copyTextToClipboard(const QString &text)
0199 {
0200     QClipboard *clipboard = qGuiApp->clipboard();
0201     clipboard->setText(text, QClipboard::Clipboard);
0202     clipboard->setText(text, QClipboard::Selection);
0203 }
0204 
0205 #include "emojierplugin.moc"