mirror of https://github.com/Nheko-Reborn/nheko
commit
7afb164244
After Width: | Height: | Size: 573 B |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,18 @@ |
|||||||
|
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 |
||||||
|
property var emojiPicker |
||||||
|
property string room_id |
||||||
|
property string event_id |
||||||
|
|
||||||
|
image: ":/icons/icons/ui/smile.png" |
||||||
|
id: emojiButton |
||||||
|
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, room_id, event_id) |
||||||
|
|
||||||
|
} |
@ -0,0 +1,290 @@ |
|||||||
|
import QtQuick 2.9 |
||||||
|
import QtQuick.Controls 2.9 |
||||||
|
import QtQuick.Layouts 1.3 |
||||||
|
import QtGraphicalEffects 1.9 |
||||||
|
|
||||||
|
import im.nheko 1.0 |
||||||
|
import im.nheko.EmojiModel 1.0 |
||||||
|
|
||||||
|
import "../" |
||||||
|
|
||||||
|
Popup { |
||||||
|
|
||||||
|
function show(showAt, room_id, event_id) { |
||||||
|
console.debug("Showing emojiPicker for " + event_id + "in room " + room_id) |
||||||
|
parent = showAt |
||||||
|
x = Math.round((showAt.width - width) / 2) |
||||||
|
y = showAt.height |
||||||
|
emojiPopup.room_id = room_id |
||||||
|
emojiPopup.event_id = event_id |
||||||
|
open() |
||||||
|
} |
||||||
|
|
||||||
|
property string room_id |
||||||
|
property string event_id |
||||||
|
property var colors |
||||||
|
property alias model: gridView.model |
||||||
|
property var textArea |
||||||
|
property string emojiCategory: "people" |
||||||
|
property real highlightHue: colors.highlight.hslHue |
||||||
|
property real highlightSat: colors.highlight.hslSaturation |
||||||
|
property real highlightLight: colors.highlight.hslLightness |
||||||
|
|
||||||
|
id: emojiPopup |
||||||
|
|
||||||
|
margins: 0 |
||||||
|
bottomPadding: 1 |
||||||
|
leftPadding: 1 |
||||||
|
rightPadding: 1 |
||||||
|
|
||||||
|
modal: true |
||||||
|
focus: true |
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
id: columnView |
||||||
|
anchors.fill: parent |
||||||
|
spacing: 0 |
||||||
|
Layout.bottomMargin: 0 |
||||||
|
Layout.leftMargin: 3 |
||||||
|
Layout.rightMargin: 3 |
||||||
|
Layout.topMargin: 2 |
||||||
|
|
||||||
|
// emoji grid |
||||||
|
GridView { |
||||||
|
id: gridView |
||||||
|
|
||||||
|
Layout.preferredHeight: emojiPopup.height |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.leftMargin: 4 |
||||||
|
|
||||||
|
cellWidth: 52 |
||||||
|
cellHeight: 52 |
||||||
|
|
||||||
|
boundsBehavior: Flickable.StopAtBounds |
||||||
|
|
||||||
|
clip: true |
||||||
|
|
||||||
|
// Individual emoji |
||||||
|
delegate: AbstractButton { |
||||||
|
width: 48 |
||||||
|
height: 48 |
||||||
|
contentItem: Text { |
||||||
|
horizontalAlignment: Text.AlignHCenter |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
font.family: settings.emojiFont |
||||||
|
|
||||||
|
font.pixelSize: 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 |
||||||
|
|
||||||
|
// give the emoji a little oomf |
||||||
|
DropShadow { |
||||||
|
width: parent.width; |
||||||
|
height: parent.height; |
||||||
|
horizontalOffset: 3 |
||||||
|
verticalOffset: 3 |
||||||
|
radius: 8.0 |
||||||
|
samples: 17 |
||||||
|
color: "#80000000" |
||||||
|
source: parent.contentItem |
||||||
|
} |
||||||
|
// TODO: maybe add favorites at some point? |
||||||
|
onClicked: { |
||||||
|
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id + " in room " + emojiPopup.room_id) |
||||||
|
emojiPopup.close() |
||||||
|
timelineManager.queueReactionMessage(emojiPopup.room_id, emojiPopup.event_id, model.unicode) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Search field |
||||||
|
header: TextField { |
||||||
|
id: emojiSearch |
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
anchors.rightMargin: emojiScroll.width + 4 |
||||||
|
placeholderText: qsTr("Search") |
||||||
|
selectByMouse: true |
||||||
|
rightPadding: clearSearch.width |
||||||
|
|
||||||
|
Timer { |
||||||
|
id: searchTimer |
||||||
|
interval: 350 // tweak as needed? |
||||||
|
onTriggered: { |
||||||
|
emojiPopup.model.filter = emojiSearch.text |
||||||
|
emojiPopup.model.category = EmojiCategory.Search |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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() |
||||||
|
} |
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar { |
||||||
|
id: emojiScroll |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Separator |
||||||
|
Rectangle { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.preferredHeight: 1 |
||||||
|
|
||||||
|
color: emojiPopup.colors.dark |
||||||
|
} |
||||||
|
|
||||||
|
// Category picker row |
||||||
|
RowLayout { |
||||||
|
Layout.bottomMargin: 0 |
||||||
|
Layout.preferredHeight: 42 |
||||||
|
implicitHeight: 42 |
||||||
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom |
||||||
|
// Display the normal categories |
||||||
|
Repeater { |
||||||
|
model: ListModel { |
||||||
|
// TODO: Would like to get 'simple' icons for the categories |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/people.png"; category: EmojiCategory.People } |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/nature.png"; category: EmojiCategory.Nature } |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/foods.png"; category: EmojiCategory.Food } |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/activity.png"; category: EmojiCategory.Activity } |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/travel.png"; category: EmojiCategory.Travel } |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/objects.png"; category: EmojiCategory.Objects } |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/symbols.png"; category: EmojiCategory.Symbols } |
||||||
|
ListElement { image: ":/icons/icons/emoji-categories/flags.png"; category: EmojiCategory.Flags } |
||||||
|
} |
||||||
|
|
||||||
|
delegate: AbstractButton { |
||||||
|
Layout.preferredWidth: 36 |
||||||
|
Layout.preferredHeight: 36 |
||||||
|
|
||||||
|
contentItem: Image { |
||||||
|
horizontalAlignment: Image.AlignHCenter |
||||||
|
verticalAlignment: Image.AlignVCenter |
||||||
|
fillMode: Image.Pad |
||||||
|
sourceSize.width: 32 |
||||||
|
sourceSize.height: 32 |
||||||
|
source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText) |
||||||
|
} |
||||||
|
|
||||||
|
MouseArea |
||||||
|
{ |
||||||
|
id: mouseArea |
||||||
|
anchors.fill: parent |
||||||
|
onPressed: mouse.accepted = false |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
} |
||||||
|
|
||||||
|
background: Rectangle { |
||||||
|
anchors.fill: parent |
||||||
|
|
||||||
|
color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.20) : 'transparent' |
||||||
|
radius: 5 |
||||||
|
border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent' |
||||||
|
} |
||||||
|
|
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.text: { |
||||||
|
switch (model.category) { |
||||||
|
case EmojiCategory.People: |
||||||
|
return qsTr('People'); |
||||||
|
case EmojiCategory.Nature: |
||||||
|
return qsTr('Nature'); |
||||||
|
case EmojiCategory.Food: |
||||||
|
return qsTr('Food'); |
||||||
|
case EmojiCategory.Activity: |
||||||
|
return qsTr('Activity'); |
||||||
|
case EmojiCategory.Travel: |
||||||
|
return qsTr('Travel'); |
||||||
|
case EmojiCategory.Objects: |
||||||
|
return qsTr('Objects'); |
||||||
|
case EmojiCategory.Symbols: |
||||||
|
return qsTr('Symbols'); |
||||||
|
case EmojiCategory.Flags: |
||||||
|
return qsTr('Flags'); |
||||||
|
} |
||||||
|
} |
||||||
|
ToolTip.visible: hovered |
||||||
|
|
||||||
|
onClicked: { |
||||||
|
emojiPopup.model.category = model.category |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Separator |
||||||
|
Rectangle { |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.preferredWidth: 1 |
||||||
|
implicitWidth: 1 |
||||||
|
height: parent.height |
||||||
|
|
||||||
|
color: emojiPopup.colors.dark |
||||||
|
} |
||||||
|
|
||||||
|
// Search Button is special |
||||||
|
AbstractButton { |
||||||
|
id: searchBtn |
||||||
|
hoverEnabled: true |
||||||
|
Layout.alignment: Qt.AlignRight |
||||||
|
Layout.bottomMargin: 0 |
||||||
|
|
||||||
|
ToolTip.text: qsTr("Search") |
||||||
|
ToolTip.visible: hovered |
||||||
|
onClicked: { |
||||||
|
// clear any filters |
||||||
|
emojiPopup.model.category = EmojiCategory.Search |
||||||
|
gridView.positionViewAtBeginning() |
||||||
|
emojiSearch.forceActiveFocus() |
||||||
|
} |
||||||
|
Layout.preferredWidth: 36 |
||||||
|
Layout.preferredHeight: 36 |
||||||
|
implicitWidth: 36 |
||||||
|
implicitHeight: 36 |
||||||
|
|
||||||
|
contentItem: Image { |
||||||
|
anchors.right: parent.right |
||||||
|
horizontalAlignment: Image.AlignHCenter |
||||||
|
verticalAlignment: Image.AlignVCenter |
||||||
|
sourceSize.width: 32 |
||||||
|
sourceSize.height: 32 |
||||||
|
fillMode: Image.Pad |
||||||
|
smooth: true |
||||||
|
source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText) |
||||||
|
} |
||||||
|
|
||||||
|
MouseArea |
||||||
|
{ |
||||||
|
id: mouseArea |
||||||
|
anchors.fill: parent |
||||||
|
onPressed: mouse.accepted = false |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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() {} |
||||||
|
|
||||||
|
EmojiCategory |
||||||
|
EmojiProxyModel::category() const |
||||||
|
{ |
||||||
|
return category_; |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
EmojiProxyModel::setCategory(EmojiCategory 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_ != EmojiCategory::Search) { |
||||||
|
return emoji.category == category_; |
||||||
|
} |
||||||
|
|
||||||
|
return filterRegExp().isEmpty() ? true : filterRegExp().indexIn(emoji.shortName) != -1; |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
#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; |
||||||
|
}; |
||||||
|
|
||||||
|
class EmojiProxyModel : public QSortFilterProxyModel |
||||||
|
{ |
||||||
|
Q_OBJECT |
||||||
|
|
||||||
|
Q_PROPERTY( |
||||||
|
emoji::EmojiCategory 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; |
||||||
|
|
||||||
|
EmojiCategory category() const; |
||||||
|
void setCategory(EmojiCategory 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: |
||||||
|
EmojiCategory category_ = EmojiCategory::Search; |
||||||
|
emoji::Provider emoji_provider_; |
||||||
|
}; |
||||||
|
|
||||||
|
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue