diff --git a/applets/kimpanel/backend/ibus/emojier/emojier.cpp b/applets/kimpanel/backend/ibus/emojier/emojier.cpp index db974b624..c5805b047 100644 --- a/applets/kimpanel/backend/ibus/emojier/emojier.cpp +++ b/applets/kimpanel/backend/ibus/emojier/emojier.cpp @@ -1,322 +1,334 @@ /* * Copyright (C) 2019 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "emojiersettings.h" #include "config-workspace.h" #undef signals #include struct Emoji { QString content; QString description; QString category; }; class TextImageProvider : public QQuickImageProvider { public: TextImageProvider() : QQuickImageProvider(QQuickImageProvider::Pixmap) { } QPixmap requestPixmap(const QString &id, QSize *_size, const QSize &requestedSize) override { QPixmap dummy; const QString renderString = id.mid(1); //drop initial / QSize size = requestedSize; QFont font; if (!size.isValid()) { QFontMetrics fm(font, &dummy); size = { fm.horizontalAdvance(renderString), fm.height() }; } else { font.setPointSize((requestedSize.height() * 3) / 4); } if (_size) { *_size = size; } QPixmap pixmap(size.width(), size.height()); pixmap.fill(Qt::transparent); QPainter p; p.begin(&pixmap); p.setFont(font); p.drawText(QRect(0, 0, size.width(), size.height()), Qt::AlignCenter, renderString); p.end(); return pixmap; } }; class AbstractEmojiModel : public QAbstractListModel { Q_OBJECT public: enum EmojiRole { CategoryRole = Qt::UserRole + 1 }; int rowCount(const QModelIndex & parent = {}) const override { return parent.isValid() ? 0 : m_emoji.count(); } QVariant data(const QModelIndex & index, int role) const override { if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid | QAbstractItemModel::CheckIndexOption::DoNotUseParent) || index.column() != 0) return {}; const auto &emoji = m_emoji[index.row()]; switch(role) { case Qt::DisplayRole: return emoji.content; case Qt::ToolTipRole: return emoji.description; case CategoryRole: return emoji.category; } return {}; } protected: QVector m_emoji; }; class EmojiModel : public AbstractEmojiModel { Q_OBJECT Q_PROPERTY(QStringList categories MEMBER m_categories CONSTANT) public: enum EmojiRole { CategoryRole = Qt::UserRole + 1 }; EmojiModel() { QLocale locale; const QString dictName = "ibus/dicts/emoji-" + locale.bcp47Name().replace(QLatin1Char('-'), QLatin1Char('_')) + ".dict"; const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, dictName); if (path.isEmpty()) { qWarning() << "could not find" << dictName; return; } GSList *list = ibus_emoji_data_load (path.toUtf8().constData()); m_emoji.reserve(g_slist_length(list)); QSet categories; for (GSList *l = list; l; l = l->next) { IBusEmojiData *data = (IBusEmojiData *) l->data; if (!IBUS_IS_EMOJI_DATA (data)) { qWarning() << "Your dict format is no longer supported.\n" "Need to create the dictionaries again."; g_slist_free (list); return; } const QString category = QString::fromUtf8(ibus_emoji_data_get_category(data)); categories.insert(category); m_emoji += { QString::fromUtf8(ibus_emoji_data_get_emoji(data)), ibus_emoji_data_get_description(data), category }; } categories.remove({}); - m_categories = categories.toList(); + m_categories = categories.values(); m_categories.sort(); m_categories.prepend({}); m_categories.prepend(QStringLiteral(":recent:")); g_slist_free (list); } Q_SCRIPTABLE QString findFirstEmojiForCategory(const QString &category) { for (const Emoji &emoji : m_emoji) { if (emoji.category == category) return emoji.content; } return {}; } private: QStringList m_categories; }; class RecentEmojiModel : public AbstractEmojiModel { Q_OBJECT Q_PROPERTY(int count READ rowCount CONSTANT) public: RecentEmojiModel() - : m_settings(new EmojierSettings) { - auto recent = m_settings->recent(); - auto recentDescriptions = m_settings->recentDescriptions(); - - int i = 0; - for (QString c : recent) { - m_emoji += { QString(c), recentDescriptions.at(i++), QString{} }; - } + refresh(); } Q_SCRIPTABLE void includeRecent(const QString &emoji, const QString &emojiDescription) { - QStringList recent = m_settings->recent(); + QStringList recent = m_settings.recent(); + QStringList recentDescriptions = m_settings.recentDescriptions(); + + const int idx = recent.indexOf(emoji); + if (idx >= 0) { + recent.removeAt(idx); + recentDescriptions.removeAt(idx); + } recent.prepend(emoji); recent = recent.mid(0, 50); - m_settings->setRecent(recent); + m_settings.setRecent(recent); - QStringList recentDescriptions = m_settings->recentDescriptions(); recentDescriptions.prepend(emojiDescription); recentDescriptions = recentDescriptions.mid(0, 50); - m_settings->setRecentDescriptions(recentDescriptions); + m_settings.setRecentDescriptions(recentDescriptions); + m_settings.save(); - m_settings->save(); + refresh(); } private: - QScopedPointer m_settings; + void refresh() + { + beginResetModel(); + auto recent = m_settings.recent(); + auto recentDescriptions = m_settings.recentDescriptions(); + int i = 0; + m_emoji.clear(); + for (const QString &c : recent) { + m_emoji += { c, recentDescriptions.at(i++), QString{} }; + } + endResetModel(); + } + + EmojierSettings m_settings; }; class CategoryModelFilter : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QString category READ category WRITE setCategory) public: QString category() const { return m_category; } void setCategory(const QString &category) { if (m_category != category) { m_category = category; invalidateFilter(); } } bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override { return m_category.isEmpty() || sourceModel()->index(source_row, 0, source_parent).data(EmojiModel::CategoryRole).toString() == m_category; } private: QString m_category; }; class SearchModelFilter : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QString search READ search WRITE setSearch) public: QString search() const { return m_search; } void setSearch(const QString &search) { if (m_search != search) { m_search = search; invalidateFilter(); } } bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override { return sourceModel()->index(source_row, 0, source_parent).data(Qt::ToolTipRole).toString().contains(m_search, Qt::CaseInsensitive); } private: QString m_search; }; class CopyHelperPrivate : public QObject { Q_OBJECT public: Q_INVOKABLE static void copyTextToClipboard(const QString& text) { qGuiApp->clipboard()->setText(text); } }; int main(int argc, char** argv) { QApplication app(argc, argv); app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); app.setWindowIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-emoticons"))); KCrash::initialize(); KQuickAddons::QtQuickSettings::init(); KLocalizedString::setApplicationDomain("plasma.emojier"); KAboutData about(QStringLiteral("plasma.emojier"), QStringLiteral("Emojier"), QStringLiteral(WORKSPACE_VERSION_STRING), i18n("Emoji Picker"), KAboutLicense::GPL, i18n("(C) 2019 Aleix Pol i Gonzalez")); about.addAuthor( QStringLiteral("Aleix Pol i Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org") ); about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); // about.setProductName(""); about.setProgramLogo(app.windowIcon()); KAboutData::setApplicationData(about); KDBusService::StartupOptions startup = nullptr; { QCommandLineParser parser; QCommandLineOption replaceOption({QStringLiteral("replace")}, i18n("Replace an existing instance")); parser.addOption(replaceOption); about.setupCommandLine(&parser); parser.process(app); about.processCommandLine(&parser); if (parser.isSet(replaceOption)) { startup |= KDBusService::Replace; } } KDBusService* service = new KDBusService(KDBusService::Unique | startup, &app); EmojiModel m; qmlRegisterType("org.kde.plasma.emoji", 1, 0, "EmojiModel"); qmlRegisterType("org.kde.plasma.emoji", 1, 0, "CategoryModelFilter"); qmlRegisterType("org.kde.plasma.emoji", 1, 0, "SearchModelFilter"); - qmlRegisterType("org.kde.plasma.emoji", 1, 0, "EmojierSettings"); qmlRegisterType("org.kde.plasma.emoji", 1, 0, "RecentEmojiModel"); qmlRegisterSingletonType("org.kde.plasma.emoji", 1, 0, "CopyHelper", [] (QQmlEngine*, QJSEngine*) -> QObject* { return new CopyHelperPrivate; }); QQmlApplicationEngine engine(QUrl(QStringLiteral("qrc:/ui/emojier.qml"))); engine.addImageProvider(QLatin1String("text"), new TextImageProvider); QObject::connect(service, &KDBusService::activateRequested, &engine, [&engine](const QStringList &/*arguments*/, const QString &/*workingDirectory*/) { for (QObject* object : engine.rootObjects()) { auto w = qobject_cast(object); if (!w) continue; w->setVisible(true); w->raise(); } }); return app.exec(); } #include "emojier.moc" diff --git a/applets/kimpanel/backend/ibus/emojier/ui/emojier.qml b/applets/kimpanel/backend/ibus/emojier/ui/emojier.qml index 4fe3e7fa0..21fbc61c0 100644 --- a/applets/kimpanel/backend/ibus/emojier/ui/emojier.qml +++ b/applets/kimpanel/backend/ibus/emojier/ui/emojier.qml @@ -1,92 +1,93 @@ /* * Copyright (C) 2019 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.11 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.11 as QQC2 import org.kde.kirigami 2.6 as Kirigami import org.kde.plasma.emoji 1.0 Kirigami.ApplicationWindow { id: window title: i18n("Emoji Picker") EmojiModel { id: emoji } RecentEmojiModel { id: recentEmojiModel } function report(thing, description) { if (!visible) return; console.log("Copied to clipboard:", thing) CopyHelper.copyTextToClipboard(thing) recentEmojiModel.includeRecent(thing, description); visible = false } - Component.onCompleted: { - globalDrawer.actions[recentEmojiModel.count === 0 ? 1 : 0].trigger() + onVisibilityChanged: { + if (visible) + globalDrawer.actions[recentEmojiModel.count === 0 ? 1 : 0].trigger() } globalDrawer: Kirigami.GlobalDrawer { id: drawer title: i18n("Categories") collapsible: !topContent.activeFocus collapsed: true modal: false function createCategoryActions(categories) { var actions = [] for(var i in categories) { var cat = categories[i]; var catAction = categoryActionComponent.createObject(drawer, { category: cat }); actions.push(catAction) } return actions; } actions: createCategoryActions(emoji.categories) Component { id: categoryActionComponent Kirigami.Action { readonly property bool isRecent: category === ":recent:" property string category checked: window.pageStack.get(0).title === text text: category.length === 0 ? i18n("All") : isRecent ? i18n("Recent") : category.replace(/&/g, "&&"); enabled: !isRecent || recentEmojiModel.count > 0 icon.name: isRecent ? "document-open-recent-symbolic" : category.length === 0 ? "view-list-icons" : "image://text/" + emoji.findFirstEmojiForCategory(category) onTriggered: { window.pageStack.replace("qrc:/ui/CategoryPage.qml", {title: text, category: isRecent ? "" : category, model: isRecent ? recentEmojiModel : emoji }) } } } } }