Commit 1449633f authored by Aleix Pol Gonzalez's avatar Aleix Pol Gonzalez 🐧
Browse files

Include an emoji picker

Summary:
Shows all emoji in categories, it will save the new emoji into the
clipboard.

Test Plan:
Manual testing; run `ibus-ui-emojier-plasma`
{F7573133}

Reviewers: #plasma, #vdg, davidedmundson

Reviewed By: #plasma, davidedmundson

Subscribers: hein, #vdg, GB_2, mart, ngraham, davidedmundson, broulik, plasma-devel

Tags: #plasma, #vdg

Differential Revision: https://phabricator.kde.org/D24454
parent 917327d7
......@@ -12,6 +12,9 @@ if(IBUS_FOUND AND GLIB2_FOUND AND GIO_FOUND AND GOBJECT_FOUND)
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 @@ if(IBUS_FOUND AND GLIB2_FOUND AND GIO_FOUND AND GOBJECT_FOUND)
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)
......
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} )
#! /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
/*
* Copyright (C) 2019 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QApplication>
#include <QLocale>
#include <QByteArray>
#include <QStandardPaths>
#include <QAbstractListModel>
#include <QVector>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QSortFilterProxyModel>
#include <QQuickImageProvider>
#include <QCommandLineParser>
#include <QFontMetrics>
#include <QPainter>
#include <QClipboard>
#include <QQuickWindow>
#include <KLocalizedString>
#include <KAboutData>
#include <KQuickAddons/QtQuickSettings>
#include <KCrash>
#include <KDBusService>
#include <QDebug>
#include "emojiersettings.h"
#include "config-workspace.h"
#undef signals
#include <ibus.h>
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<Emoji> 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<QString> 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<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("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<EmojiModel>("org.kde.plasma.emoji", 1, 0, "EmojiModel");
qmlRegisterType<CategoryModelFilter>("org.kde.plasma.emoji", 1, 0, "CategoryModelFilter");
qmlRegisterType<SearchModelFilter>("org.kde.plasma.emoji", 1, 0, "SearchModelFilter");
qmlRegisterType<EmojierSettings>("org.kde.plasma.emoji", 1, 0, "EmojierSettings");
qmlRegisterType<RecentEmojiModel>("org.kde.plasma.emoji", 1, 0, "RecentEmojiModel");
qmlRegisterSingletonType<CopyHelperPrivate>("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<QQuickWindow*>(object);
if (!w)
continue;
w->setVisible(true);
w->raise();
}
});
return app.exec();
}
#include "emojier.moc"
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0 http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
<kcfgfile arg="true"/>
<group name="Emojier">
<entry name="recent" type="StringList"/>
<entry name="recentDescriptions" type="StringList"/>
</group>
</kcfg>
File=emojiersettings.kcfg
ClassName=EmojierSettings
Mutators=true
[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+.
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file>ui/emojier.qml</file>
<file>ui/CategoryPage.qml</file>
</qresource>
</RCC>
/*
* Copyright (C) 2019 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
}
}
}
}
/*
* Copyright (C) 2019 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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 })
}
}
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment