diff --git a/applets/kimpanel/backend/ibus/CMakeLists.txt b/applets/kimpanel/backend/ibus/CMakeLists.txt --- a/applets/kimpanel/backend/ibus/CMakeLists.txt +++ b/applets/kimpanel/backend/ibus/CMakeLists.txt @@ -12,6 +12,9 @@ find_package(Qt5X11Extras) find_package(XCB COMPONENTS XCB KEYSYMS) + + add_subdirectory(emojier) + if (Qt5X11Extras_FOUND AND XCB_XCB_FOUND AND XCB_KEYSYMS_FOUND) include_directories(${Qt5X11Extras_INCLUDE_DIRS}) include_directories(${XCB_XCB_INCLUDE_DIRS}) @@ -27,7 +30,7 @@ ibus15/propertymanager.cpp) add_definitions(-DQT_NO_KEYWORDS) add_executable(kimpanel-ibus-panel ${kimpanel_ibus_panel_SRCS}) - target_link_libraries(kimpanel-ibus-panel ${IBUS_LIBRARIES} GLIB2::GLIB2 ${GIO_LIBRARIES} ${GOBJECT_LIBRARIES} Qt5::Core Qt5::DBus Qt5::Gui Qt5::X11Extras XCB::KEYSYMS) + target_link_libraries(kimpanel-ibus-panel ${IBUS_LIBRARIES} GLIB2::GLIB2 ${GIO_LIBRARIES} ${GOBJECT_LIBRARIES} Qt5::Core Qt5::DBus Qt5::Gui Qt5::X11Extras XCB::KEYSYMS) # configure_file(${CMAKE_CURRENT_SOURCE_DIR}/kimpanel.xml.in ${CMAKE_CURRENT_BINARY_DIR}/kimpanel.xml @ONLY) # install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kimpanel.xml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/ibus/component) diff --git a/applets/kimpanel/backend/ibus/emojier/CMakeLists.txt b/applets/kimpanel/backend/ibus/emojier/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/CMakeLists.txt @@ -0,0 +1,8 @@ +kconfig_add_kcfg_files(emojier_KCFG emojiersettings.kcfgc GENERATE_MOC) + +add_executable(ibus-ui-emojier-plasma emojier.cpp resources.qrc ${emojier_KCFG}) +target_link_libraries(ibus-ui-emojier-plasma Qt5::Widgets ${IBUS_LIBRARIES} ${GOBJECT_LIBRARIES} Qt5::Quick KF5::ConfigGui KF5::I18n KF5::CoreAddons KF5::Crash KF5::QuickAddons KF5::DBusAddons) + +install(TARGETS ibus-ui-emojier-plasma ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES org.kde.plasma.emojier.desktop DESTINATION ${DATA_INSTALL_DIR}/kglobalaccel) +install(PROGRAMS org.kde.plasma.emojier.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) diff --git a/applets/kimpanel/backend/ibus/emojier/Messages.sh b/applets/kimpanel/backend/ibus/emojier/Messages.sh new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name "*.ui"` >> rc.cpp || exit 11 +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/org.kde.plasma.emojier.pot +rm -f rc.cpp diff --git a/applets/kimpanel/backend/ibus/emojier/emojier.cpp b/applets/kimpanel/backend/ibus/emojier/emojier.cpp new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/emojier.cpp @@ -0,0 +1,310 @@ +/* + * 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 "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() + ".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.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{} }; + } + } + + Q_SCRIPTABLE void includeRecent(const QString &emoji, const QString &emojiDescription) { + QStringList recent = m_settings->recent(); + recent.prepend(emoji); + recent = recent.mid(0, 50); + m_settings->setRecent(recent); + + QStringList recentDescriptions = m_settings->recentDescriptions(); + recentDescriptions.prepend(emojiDescription); + recentDescriptions = recentDescriptions.mid(0, 50); + m_settings->setRecentDescriptions(recentDescriptions); + + m_settings->save(); + } + +private: + QScopedPointer 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("org.kde.plasma.emojier"); + + KAboutData about(QStringLiteral("org.kde.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); + + { + QCommandLineParser parser; + about.setupCommandLine(&parser); + parser.process(app); + about.processCommandLine(&parser); + } + + KDBusService* service = new KDBusService(KDBusService::Unique, &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/emojiersettings.kcfg b/applets/kimpanel/backend/ibus/emojier/emojiersettings.kcfg new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/emojiersettings.kcfg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/applets/kimpanel/backend/ibus/emojier/emojiersettings.kcfgc b/applets/kimpanel/backend/ibus/emojier/emojiersettings.kcfgc new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/emojiersettings.kcfgc @@ -0,0 +1,3 @@ +File=emojiersettings.kcfg +ClassName=EmojierSettings +Mutators=true diff --git a/applets/kimpanel/backend/ibus/emojier/org.kde.plasma.emojier.desktop b/applets/kimpanel/backend/ibus/emojier/org.kde.plasma.emojier.desktop new file mode 100755 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/org.kde.plasma.emojier.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Exec=ibus-ui-emojier-plasma +Name=Emoji Selector +OnlyShowIn=KDE; +Type=Application +Icon=preferences-desktop-emoticons +X-DBUS-StartupType=Unique +X-KDE-StartupNotify=false +X-KDE-Shortcuts=Meta+. diff --git a/applets/kimpanel/backend/ibus/emojier/resources.qrc b/applets/kimpanel/backend/ibus/emojier/resources.qrc new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/resources.qrc @@ -0,0 +1,7 @@ + + + + ui/emojier.qml + ui/CategoryPage.qml + + diff --git a/applets/kimpanel/backend/ibus/emojier/ui/CategoryPage.qml b/applets/kimpanel/backend/ibus/emojier/ui/CategoryPage.qml new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/ui/CategoryPage.qml @@ -0,0 +1,82 @@ +/* + * 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 org.kde.kirigami 2.6 as Kirigami +import QtQuick.Controls 2.11 as QQC2 +import org.kde.plasma.emoji 1.0 + +Kirigami.ScrollablePage +{ + id: view + property alias model: emojiModel.sourceModel + property alias category: filter.category + + header: QQC2.TextField { + id: searchField + Layout.fillWidth: true + placeholderText: i18n("Search...") + onTextChanged: emojiModel.search = text + height: visible ? implicitHeight : 0 + visible: false + } + + actions.main: Kirigami.Action { + icon.name: "search" + tooltip: i18n("Search...") + shortcut: StandardKey.Find + onTriggered: { + searchField.visible = true + searchField.focus = true + } + } + + GridView { + cellWidth: 64 + cellHeight: cellWidth + model: CategoryModelFilter { + id: filter + sourceModel: SearchModelFilter { + id: emojiModel + } + } + + delegate: QQC2.Label { + font.pointSize: 30 + fontSizeMode: Text.Fit + minimumPointSize: 10 + text: model.display + + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.text: model.toolTip + QQC2.ToolTip.visible: mouse.containsMouse + + opacity: mouse.containsMouse ? 0.7 : 1 + + MouseArea { + id: mouse + anchors.fill: parent + hoverEnabled: true + onClicked: window.report(model.display, model.toolTip) + } + } + } +} diff --git a/applets/kimpanel/backend/ibus/emojier/ui/emojier.qml b/applets/kimpanel/backend/ibus/emojier/ui/emojier.qml new file mode 100644 --- /dev/null +++ b/applets/kimpanel/backend/ibus/emojier/ui/emojier.qml @@ -0,0 +1,90 @@ +/* + * 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) { + 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() + } + + 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 }) + } + } + } + } +}