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::Qml KF5::ConfigGui) + +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/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,217 @@ +/* + * 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 "emojiersettings.h" + +#undef signals +#include + +struct Emoji { + QString content; + QString description; + QString category; +}; + +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); + } + +private: + QStringList m_categories; +}; + +class RecentEmojiModel : public AbstractEmojiModel +{ + Q_OBJECT +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); + 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"))); + + return app.exec(); +} + +#include "emojier.moc" 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,8 @@ +[Desktop Entry] +Exec=ibus-ui-emojier-plasma +Name=Emoji Selector +OnlyShowIn=KDE; +Type=Application +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,58 @@ +/* + * 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 org.kde.kirigami 2.6 as Kirigami +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QQC2 +import org.kde.plasma.emoji 1.0 + +Kirigami.ScrollablePage +{ + id: view + property alias model: filter.sourceModel + property alias category: filter.category + GridView { + cellWidth: 64 + cellHeight: cellWidth + model: CategoryModelFilter { + id: filter + } + + 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,85 @@ +/* + * 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.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") + + SearchModelFilter { + id: emojiModel + sourceModel: EmojiModel { + id: emoji + } + } + + RecentEmojiModel { + id: recentEmojiModel + } + + function report(thing, description) { + console.log("Copied to clipboard:", thing) + CopyHelper.copyTextToClipboard(thing) + recentEmojiModel.includeRecent(thing, description); + Qt.quit() + } + + + pageStack.initialPage: Kirigami.ScrollablePage { + title: i18n("Categories") + actions.main: Kirigami.Action { + id: searchAction + icon.name: "search" + onTriggered: checked = !checked + shortcut: "Ctrl+F" + } + + width: Kirigami.Units.gridUnit * 14 + + ListView { + model: emoji.categories + + header: QQC2.TextField { + width: parent.width + visible: searchAction.checked + placeholderText: i18n("Search...") + onTextChanged: emojiModel.search = text + height: visible ? implicitHeight : 0 + } + + delegate: Kirigami.BasicListItem { + readonly property bool isRecent: modelData === ":recent:" + label: modelData.length === 0 ? i18n("All") + : isRecent ? i18n("Recent") + : modelData + + icon: isRecent ? "document-open-recent-symbolic" : "" + onClicked: { + window.pageStack.push("qrc:/ui/CategoryPage.qml", {title: label, category: isRecent ? "" : modelData, model: isRecent ? recentEmojiModel : emojiModel }) + } + } + } + } +}