Use one CompletionProxy for everything including EmojiPicker

pull/556/head
Nicolas Werner 4 years ago
parent 1b0af04cc8
commit c2e625756c
No known key found for this signature in database
GPG Key ID: C8D75E610773F2D9
  1. 10
      resources/qml/TimelineView.qml
  2. 151
      resources/qml/emoji/EmojiPicker.qml
  3. 44
      src/Cache_p.h
  4. 29
      src/CompletionProxyModel.cpp
  5. 2
      src/CompletionProxyModel.h
  6. 70
      src/emoji/EmojiModel.cpp
  7. 32
      src/emoji/EmojiModel.h
  8. 50
      src/emoji/EmojiSearchModel.h
  9. 8
      src/timeline/TimelineViewManager.cpp

@ -36,15 +36,7 @@ Page {
id: emojiPopup id: emojiPopup
colors: palette colors: palette
model: TimelineManager.completerFor("allemoji", "")
model: EmojiProxyModel {
category: Emoji.Category.People
sourceModel: EmojiModel {
}
}
} }
Component { Component {

@ -49,6 +49,54 @@ Menu {
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 2 anchors.topMargin: 2
// Search field
TextField {
id: emojiSearch
//width: gridView.width - 6
Layout.topMargin: 3
Layout.preferredWidth: 7 * 52 + 20 - 6
placeholderText: qsTr("Search")
selectByMouse: true
rightPadding: clearSearch.width
onTextChanged: searchTimer.restart()
onVisibleChanged: {
if (visible)
forceActiveFocus();
}
Timer {
id: searchTimer
interval: 350 // tweak as needed?
onTriggered: {
emojiPopup.model.searchString = emojiSearch.text;
emojiPopup.model.category = Emoji.Category.Search;
}
}
ToolButton {
id: clearSearch
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()
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
// clear the default hover effects.
background: Item {
}
}
}
// emoji grid // emoji grid
GridView { GridView {
id: gridView id: gridView
@ -104,54 +152,6 @@ Menu {
} }
// 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
onTextChanged: searchTimer.restart()
onVisibleChanged: {
if (visible)
forceActiveFocus();
}
Timer {
id: searchTimer
interval: 350 // tweak as needed?
onTriggered: {
emojiPopup.model.filter = emojiSearch.text;
emojiPopup.model.category = Emoji.Category.Search;
}
}
ToolButton {
id: clearSearch
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()
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
// clear the default hover effects.
background: Item {
}
}
}
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
id: emojiScroll id: emojiScroll
} }
@ -160,6 +160,7 @@ Menu {
// Separator // Separator
Rectangle { Rectangle {
visible: emojiSearch.text === ''
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
color: emojiPopup.colors.alternateBase color: emojiPopup.colors.alternateBase
@ -167,6 +168,7 @@ Menu {
// Category picker row // Category picker row
RowLayout { RowLayout {
visible: emojiSearch.text === ''
Layout.bottomMargin: 0 Layout.bottomMargin: 0
Layout.preferredHeight: 42 Layout.preferredHeight: 42
implicitHeight: 42 implicitHeight: 42
@ -245,7 +247,8 @@ Menu {
} }
ToolTip.visible: hovered ToolTip.visible: hovered
onClicked: { onClicked: {
emojiPopup.model.category = model.category; //emojiPopup.model.category = model.category;
gridView.positionViewAtIndex(emojiPopup.model.sourceModel.categoryToIndex(model.category), GridView.Beginning);
} }
MouseArea { MouseArea {
@ -276,56 +279,6 @@ Menu {
} }
// Separator
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: 1
implicitWidth: 1
height: parent.height
color: emojiPopup.colors.alternateBase
}
// 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 = Emoji.Category.Search;
gridView.positionViewAtBeginning();
emojiSearch.forceActiveFocus();
}
Layout.preferredWidth: 36
Layout.preferredHeight: 36
implicitWidth: 36
implicitHeight: 36
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor
}
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)
}
}
} }
} }

@ -405,6 +405,50 @@ private:
event); event);
} }
template<typename T>
std::optional<mtx::events::StateEvent<T>> getStateEvent(lmdb::txn txn,
const std::string &room_id,
std::string_view state_key = "")
{
constexpr auto type = mtx::events::state_content_to_type<T>;
static_assert(type != mtx::events::EventType::Unsupported,
"Not a supported type in state events.");
if (room_id.empty())
return std::nullopt;
std::string_view value;
if (state_key.empty()) {
auto db = getStatesDb(txn, room_id);
if (!db.get(txn, to_string(type), value)) {
return std::nullopt;
}
} else {
auto db = getStatesKeyDb(txn, room_id);
std::string d = json::object({{"key", state_key}}).dump();
std::string_view data = d;
auto cursor = lmdb::cursor::open(txn, db);
if (!cursor.get(state_key, data, MDB_GET_BOTH))
return std::nullopt;
try {
auto eventsDb = getEventsDb(txn, room_id);
if (!eventsDb.get(
txn, json::parse(data)["id"].get<std::string>(), value))
return std::nullopt;
} catch (std::exception &e) {
return std::nullopt;
}
}
try {
return json::parse(value).get<mtx::events::StateEvent<T>>();
} catch (std::exception &e) {
return std::nullopt;
}
}
void saveInvites(lmdb::txn &txn, void saveInvites(lmdb::txn &txn,
const std::map<std::string, mtx::responses::InvitedRoom> &rooms); const std::map<std::string, mtx::responses::InvitedRoom> &rooms);

@ -12,16 +12,18 @@
CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
int max_mistakes, int max_mistakes,
size_t max_completions,
QObject *parent) QObject *parent)
: QAbstractProxyModel(parent) : QAbstractProxyModel(parent)
, maxMistakes_(max_mistakes) , maxMistakes_(max_mistakes)
, max_completions_(max_completions)
{ {
setSourceModel(model); setSourceModel(model);
QRegularExpression splitPoints("\\s+|-"); QRegularExpression splitPoints("\\s+|-");
// insert all the full texts // insert all the full texts
for (int i = 0; i < sourceModel()->rowCount(); i++) { for (int i = 0; i < sourceModel()->rowCount(); i++) {
if (i < 7) if (static_cast<size_t>(i) < max_completions_)
mapping.push_back(i); mapping.push_back(i);
auto string1 = sourceModel() auto string1 = sourceModel()
@ -82,14 +84,9 @@ CompletionProxyModel::invalidate()
{ {
auto key = searchString_.toUcs4(); auto key = searchString_.toUcs4();
beginResetModel(); beginResetModel();
mapping = trie_.search(key, 7, maxMistakes_); if (!key.empty()) // return default model data, if no search string
mapping = trie_.search(key, max_completions_, maxMistakes_);
endResetModel(); endResetModel();
std::string temp;
for (auto v : mapping) {
temp += std::to_string(v) + ", ";
}
nhlog::ui()->debug("mapping: {}", temp);
} }
QHash<int, QByteArray> QHash<int, QByteArray>
@ -101,12 +98,22 @@ CompletionProxyModel::roleNames() const
int int
CompletionProxyModel::rowCount(const QModelIndex &) const CompletionProxyModel::rowCount(const QModelIndex &) const
{ {
if (searchString_.isEmpty())
return std::min(static_cast<int>(std::min<size_t>(max_completions_,
std::numeric_limits<int>::max())),
sourceModel()->rowCount());
else
return (int)mapping.size(); return (int)mapping.size();
} }
QModelIndex QModelIndex
CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
{ {
// return default model data, if no search string
if (searchString_.isEmpty()) {
return index(sourceIndex.row(), 0);
}
for (int i = 0; i < (int)mapping.size(); i++) { for (int i = 0; i < (int)mapping.size(); i++) {
if (mapping[i] == sourceIndex.row()) { if (mapping[i] == sourceIndex.row()) {
return index(i, 0); return index(i, 0);
@ -119,6 +126,12 @@ QModelIndex
CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const
{ {
auto row = proxyIndex.row(); auto row = proxyIndex.row();
// return default model data, if no search string
if (searchString_.isEmpty()) {
return index(row, 0);
}
if (row < 0 || row >= (int)mapping.size()) if (row < 0 || row >= (int)mapping.size())
return QModelIndex(); return QModelIndex();

@ -154,6 +154,7 @@ class CompletionProxyModel : public QAbstractProxyModel
public: public:
CompletionProxyModel(QAbstractItemModel *model, CompletionProxyModel(QAbstractItemModel *model,
int max_mistakes = 2, int max_mistakes = 2,
size_t max_completions = 7,
QObject *parent = nullptr); QObject *parent = nullptr);
void invalidate(); void invalidate();
@ -184,4 +185,5 @@ private:
trie<uint, int> trie_; trie<uint, int> trie_;
std::vector<int> mapping; std::vector<int> mapping;
int maxMistakes_; int maxMistakes_;
size_t max_completions_;
}; };

@ -11,6 +11,20 @@
using namespace emoji; using namespace emoji;
int
EmojiModel::categoryToIndex(int category)
{
auto dist = std::distance(Provider::emoji.begin(),
std::lower_bound(Provider::emoji.begin(),
Provider::emoji.end(),
static_cast<Emoji::Category>(category),
[](const struct Emoji &e, Emoji::Category c) {
return e.category < c;
}));
return static_cast<int>(dist);
}
QHash<int, QByteArray> QHash<int, QByteArray>
EmojiModel::roleNames() const EmojiModel::roleNames() const
{ {
@ -60,59 +74,3 @@ EmojiModel::data(const QModelIndex &index, int role) const
return {}; 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;
}

@ -30,38 +30,10 @@ public:
using QAbstractListModel::QAbstractListModel; using QAbstractListModel::QAbstractListModel;
Q_INVOKABLE int categoryToIndex(int category);
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
}; };
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_;
};
} }

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "EmojiModel.h"
#include <CompletionModelRoles.h>
#include <QDebug>
#include <QEvent>
#include <QSortFilterProxyModel>
namespace emoji {
// Map emoji data to searchable data
class EmojiSearchModel : public QSortFilterProxyModel
{
public:
EmojiSearchModel(QObject *parent = nullptr)
: QSortFilterProxyModel(parent)
{
setSourceModel(new EmojiModel(this));
}
QVariant data(const QModelIndex &index, int role = Qt::UserRole + 1) const override
{
switch (role) {
case Qt::DisplayRole: {
auto emoji = QSortFilterProxyModel::data(index, role).toString();
return emoji + " :" +
toShortcode(data(index, EmojiModel::ShortName).toString()) + ":";
}
case CompletionModel::CompletionRole:
return QSortFilterProxyModel::data(index, EmojiModel::Unicode);
case CompletionModel::SearchRole: {
return toShortcode(
QSortFilterProxyModel::data(index, EmojiModel::ShortName).toString());
}
default:
return QSortFilterProxyModel::data(index, role);
}
}
private:
QString toShortcode(QString shortname) const
{
return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower();
}
};
}

@ -172,9 +172,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qRegisterMetaType<std::vector<DeviceInfo>>(); qRegisterMetaType<std::vector<DeviceInfo>>();
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel"); qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
qmlRegisterUncreatableType<QAbstractItemModel>(
"im.nheko.EmojiModel", 1, 0, "QAbstractItemModel", "Used by proxy models");
qmlRegisterUncreatableType<emoji::Emoji>( qmlRegisterUncreatableType<emoji::Emoji>(
"im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models"); "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");
qmlRegisterUncreatableMetaObject(emoji::staticMetaObject, qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
@ -595,6 +592,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
auto proxy = new CompletionProxyModel(emojiModel); auto proxy = new CompletionProxyModel(emojiModel);
emojiModel->setParent(proxy); emojiModel->setParent(proxy);
return proxy; return proxy;
} else if (completerName == "allemoji") {
auto emojiModel = new emoji::EmojiModel();
auto proxy = new CompletionProxyModel(emojiModel, 1, static_cast<size_t>(-1) / 4);
emojiModel->setParent(proxy);
return proxy;
} else if (completerName == "room") { } else if (completerName == "room") {
auto roomModel = new RoomsModel(false); auto roomModel = new RoomsModel(false);
auto proxy = new CompletionProxyModel(roomModel, 4); auto proxy = new CompletionProxyModel(roomModel, 4);

Loading…
Cancel
Save