mirror of https://github.com/Nheko-Reborn/nheko
This is necessary to support having a picker within QML. Eventually, this should replace the existing widget-based one.pull/217/head
parent
8984661187
commit
ee4dcef90f
@ -0,0 +1,27 @@ |
||||
import QtQuick 2.10 |
||||
import QtQuick.Controls 2.1 |
||||
import im.nheko 1.0 |
||||
import im.nheko.EmojiModel 1.0 |
||||
|
||||
import "../" |
||||
|
||||
ImageButton { |
||||
property var colors: currentActivePalette |
||||
|
||||
image: ":/icons/icons/ui/smile.png" |
||||
id: emojiButton |
||||
onClicked: emojiPopup.open() |
||||
|
||||
EmojiPicker { |
||||
id: emojiPopup |
||||
x: Math.round((emojiButton.width - width) / 2) |
||||
y: emojiButton.height |
||||
width: 7 * 52 |
||||
height: 6 * 52 |
||||
colors: emojiButton.colors |
||||
model: EmojiProxyModel { |
||||
category: Emoji.Category.People |
||||
sourceModel: EmojiModel {} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,176 @@ |
||||
import QtQuick 2.9 |
||||
import QtQuick.Controls 2.9 |
||||
import QtQuick.Layouts 1.3 |
||||
|
||||
import im.nheko 1.0 |
||||
import im.nheko.EmojiModel 1.0 |
||||
|
||||
import "../" |
||||
|
||||
Popup { |
||||
property var colors |
||||
property alias model: gridView.model |
||||
property var textArea |
||||
property string emojiCategory: "people" |
||||
|
||||
id: emojiPopup |
||||
|
||||
margins: 0 |
||||
|
||||
modal: true |
||||
focus: true |
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent |
||||
|
||||
ColumnLayout { |
||||
anchors.fill: parent |
||||
|
||||
// Search field |
||||
TextField { |
||||
id: emojiSearch |
||||
Layout.alignment: Qt.AlignVCenter |
||||
Layout.preferredWidth: parent.width - 4 |
||||
visible: emojiPopup.model.category === Emoji.Category.Search |
||||
placeholderText: qsTr("Search") |
||||
selectByMouse: true |
||||
rightPadding: clearSearch.width |
||||
|
||||
Timer { |
||||
id: searchTimer |
||||
interval: 350 // tweak as needed? |
||||
onTriggered: emojiPopup.model.filter = emojiSearch.text |
||||
} |
||||
|
||||
ToolButton { |
||||
id: clearSearch |
||||
anchors { |
||||
verticalCenter: parent.verticalCenter |
||||
right: parent.right |
||||
} |
||||
// clear the default hover effects. |
||||
background: Item {} |
||||
visible: emojiSearch.text !== '' |
||||
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) |
||||
focusPolicy: Qt.NoFocus |
||||
onClicked: emojiSearch.clear() |
||||
} |
||||
|
||||
onTextChanged: searchTimer.restart() |
||||
onVisibleChanged: if (visible) forceActiveFocus() |
||||
} |
||||
|
||||
// emoji grid |
||||
GridView { |
||||
id: gridView |
||||
|
||||
Layout.fillWidth: true |
||||
Layout.fillHeight: true |
||||
|
||||
cellWidth: 52 |
||||
cellHeight: 52 |
||||
|
||||
boundsBehavior: Flickable.DragOverBounds |
||||
|
||||
clip: true |
||||
|
||||
// Individual emoji |
||||
delegate: AbstractButton { |
||||
width: 48 |
||||
height: 48 |
||||
|
||||
contentItem: Text { |
||||
horizontalAlignment: Text.AlignHCenter |
||||
verticalAlignment: Text.AlignVCenter |
||||
|
||||
font.pointSize: 36 |
||||
text: model.unicode |
||||
} |
||||
|
||||
background: Rectangle { |
||||
anchors.fill: parent |
||||
color: hovered ? colors.highlight : 'transparent' |
||||
radius: 5 |
||||
} |
||||
|
||||
hoverEnabled: true |
||||
ToolTip.text: model.shortName |
||||
ToolTip.visible: hovered |
||||
// TODO: emit a signal and maybe add favorites at some point? |
||||
//onClicked: textArea.insert(textArea.cursorPosition, modelData.unicode) |
||||
} |
||||
|
||||
ScrollBar.vertical: ScrollBar {} |
||||
} |
||||
|
||||
// Separator |
||||
Rectangle { |
||||
Layout.fillWidth: true |
||||
Layout.preferredHeight: 2 |
||||
|
||||
color: emojiPopup.colors.highlight |
||||
} |
||||
|
||||
// Category picker row |
||||
Row { |
||||
Repeater { |
||||
model: ListModel { |
||||
// TODO: Would like to get 'simple' icons for the categories |
||||
ListElement { label: "😏"; category: Emoji.Category.People } |
||||
ListElement { label: "🌲"; category: Emoji.Category.Nature } |
||||
ListElement { label: "🍛"; category: Emoji.Category.Food } |
||||
ListElement { label: "🚁"; category: Emoji.Category.Activity } |
||||
ListElement { label: "🚅"; category: Emoji.Category.Travel } |
||||
ListElement { label: "💡"; category: Emoji.Category.Objects } |
||||
ListElement { label: "🔣"; category: Emoji.Category.Symbols } |
||||
ListElement { label: "🏁"; category: Emoji.Category.Flags } |
||||
ListElement { label: "🔍"; category: Emoji.Category.Search } |
||||
} |
||||
|
||||
delegate: AbstractButton { |
||||
width: 40 |
||||
height: 40 |
||||
|
||||
contentItem: Text { |
||||
horizontalAlignment: Text.AlignHCenter |
||||
verticalAlignment: Text.AlignVCenter |
||||
|
||||
font.pointSize: 30 |
||||
text: model.label |
||||
} |
||||
|
||||
background: Rectangle { |
||||
anchors.fill: parent |
||||
color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent' |
||||
radius: 5 |
||||
} |
||||
|
||||
hoverEnabled: true |
||||
ToolTip.text: { |
||||
switch (model.category) { |
||||
case Emoji.Category.People: |
||||
return qsTr('People'); |
||||
case Emoji.Category.Nature: |
||||
return qsTr('Nature'); |
||||
case Emoji.Category.Food: |
||||
return qsTr('Food'); |
||||
case Emoji.Category.Activity: |
||||
return qsTr('Activity'); |
||||
case Emoji.Category.Travel: |
||||
return qsTr('Travel'); |
||||
case Emoji.Category.Objects: |
||||
return qsTr('Objects'); |
||||
case Emoji.Category.Symbols: |
||||
return qsTr('Symbols'); |
||||
case Emoji.Category.Flags: |
||||
return qsTr('Flags'); |
||||
case Emoji.Category.Search: |
||||
return qsTr('Search'); |
||||
} |
||||
} |
||||
ToolTip.visible: hovered |
||||
|
||||
onClicked: emojiPopup.model.category = model.category |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,110 @@ |
||||
#include "EmojiModel.h" |
||||
|
||||
#include <Cache.h> |
||||
#include <MatrixClient.h> |
||||
|
||||
using namespace emoji; |
||||
|
||||
QHash<int, QByteArray> |
||||
EmojiModel::roleNames() const |
||||
{ |
||||
static QHash<int, QByteArray> roles; |
||||
|
||||
if (roles.isEmpty()) { |
||||
roles = QAbstractListModel::roleNames(); |
||||
roles[static_cast<int>(EmojiModel::Roles::Unicode)] = QByteArrayLiteral("unicode"); |
||||
roles[static_cast<int>(EmojiModel::Roles::ShortName)] = |
||||
QByteArrayLiteral("shortName"); |
||||
roles[static_cast<int>(EmojiModel::Roles::Category)] = |
||||
QByteArrayLiteral("category"); |
||||
roles[static_cast<int>(EmojiModel::Roles::Emoji)] = QByteArrayLiteral("emoji"); |
||||
} |
||||
|
||||
return roles; |
||||
} |
||||
|
||||
int |
||||
EmojiModel::rowCount(const QModelIndex &parent) const |
||||
{ |
||||
return parent == QModelIndex() ? Provider::emoji.count() : 0; |
||||
} |
||||
|
||||
QVariant |
||||
EmojiModel::data(const QModelIndex &index, int role) const |
||||
{ |
||||
if (hasIndex(index.row(), index.column(), index.parent())) { |
||||
switch (role) { |
||||
case Qt::DisplayRole: |
||||
case static_cast<int>(EmojiModel::Roles::Unicode): |
||||
return Provider::emoji[index.row()].unicode(); |
||||
|
||||
case Qt::ToolTipRole: |
||||
case static_cast<int>(EmojiModel::Roles::ShortName): |
||||
return Provider::emoji[index.row()].shortName(); |
||||
|
||||
case static_cast<int>(EmojiModel::Roles::Category): |
||||
return QVariant::fromValue(Provider::emoji[index.row()].category()); |
||||
|
||||
case static_cast<int>(EmojiModel::Roles::Emoji): |
||||
return QVariant::fromValue(Provider::emoji[index.row()]); |
||||
} |
||||
} |
||||
|
||||
return {}; |
||||
} |
||||
|
||||
EmojiProxyModel::EmojiProxyModel(QObject *parent) |
||||
: QSortFilterProxyModel(parent) |
||||
{} |
||||
|
||||
EmojiProxyModel::~EmojiProxyModel() {} |
||||
|
||||
Emoji::Category |
||||
EmojiProxyModel::category() const |
||||
{ |
||||
return category_; |
||||
} |
||||
|
||||
void |
||||
EmojiProxyModel::setCategory(Emoji::Category cat) |
||||
{ |
||||
if (category_ == cat) { |
||||
return; |
||||
} |
||||
|
||||
category_ = cat; |
||||
emit categoryChanged(); |
||||
|
||||
invalidateFilter(); |
||||
} |
||||
|
||||
QString |
||||
EmojiProxyModel::filter() const |
||||
{ |
||||
return filterRegExp().pattern(); |
||||
} |
||||
|
||||
void |
||||
EmojiProxyModel::setFilter(const QString &filter) |
||||
{ |
||||
if (filterRegExp().pattern() == filter) { |
||||
return; |
||||
} |
||||
|
||||
setFilterWildcard(filter); |
||||
emit filterChanged(); |
||||
} |
||||
|
||||
bool |
||||
EmojiProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const |
||||
{ |
||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); |
||||
const Emoji emoji = index.data(static_cast<int>(EmojiModel::Roles::Emoji)).value<Emoji>(); |
||||
|
||||
// TODO: Add favorites / recently used
|
||||
if (category_ != Emoji::Category::Search) { |
||||
return emoji.category() == category_; |
||||
} |
||||
|
||||
return filterRegExp().isEmpty() ? true : filterRegExp().indexIn(emoji.shortName()) != -1; |
||||
} |
@ -0,0 +1,68 @@ |
||||
#pragma once |
||||
|
||||
#include <QAbstractListModel> |
||||
#include <QSet> |
||||
#include <QSortFilterProxyModel> |
||||
#include <QVector> |
||||
|
||||
#include "Provider.h" |
||||
|
||||
namespace emoji { |
||||
|
||||
/*
|
||||
* Provides access to the emojis in Provider.h to QML |
||||
*/ |
||||
class EmojiModel : public QAbstractListModel |
||||
{ |
||||
Q_OBJECT |
||||
public: |
||||
enum Roles |
||||
{ |
||||
Unicode = Qt::UserRole, // unicode of emoji
|
||||
Category, // category of emoji
|
||||
ShortName, // shortext of the emoji
|
||||
Emoji, // Contains everything from the Emoji
|
||||
}; |
||||
|
||||
using QAbstractListModel::QAbstractListModel; |
||||
|
||||
QHash<int, QByteArray> roleNames() const override; |
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override; |
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; |
||||
|
||||
// TODO: Need a signal for when an emoji is selected
|
||||
// public signals:
|
||||
// void emojiSelected(const QString &emoji);
|
||||
}; |
||||
|
||||
class EmojiProxyModel : public QSortFilterProxyModel |
||||
{ |
||||
Q_OBJECT |
||||
|
||||
Q_PROPERTY( |
||||
emoji::Emoji::Category category READ category WRITE setCategory NOTIFY categoryChanged) |
||||
Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged) |
||||
|
||||
public: |
||||
explicit EmojiProxyModel(QObject *parent = nullptr); |
||||
~EmojiProxyModel() override; |
||||
|
||||
Emoji::Category category() const; |
||||
void setCategory(Emoji::Category cat); |
||||
|
||||
QString filter() const; |
||||
void setFilter(const QString &filter); |
||||
|
||||
signals: |
||||
void categoryChanged(); |
||||
void filterChanged(); |
||||
|
||||
protected: |
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; |
||||
|
||||
private: |
||||
Emoji::Category category_ = Emoji::Category::Search; |
||||
emoji::Provider emoji_provider_; |
||||
}; |
||||
|
||||
} |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue