mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 03:00:46 +03:00
Use one CompletionProxy for everything including EmojiPicker
This commit is contained in:
parent
1b0af04cc8
commit
c2e625756c
9 changed files with 144 additions and 258 deletions
|
@ -36,15 +36,7 @@ Page {
|
|||
id: emojiPopup
|
||||
|
||||
colors: palette
|
||||
|
||||
model: EmojiProxyModel {
|
||||
category: Emoji.Category.People
|
||||
|
||||
sourceModel: EmojiModel {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
model: TimelineManager.completerFor("allemoji", "")
|
||||
}
|
||||
|
||||
Component {
|
||||
|
|
|
@ -49,6 +49,54 @@ Menu {
|
|||
anchors.right: parent.right
|
||||
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
|
||||
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 {
|
||||
id: emojiScroll
|
||||
}
|
||||
|
@ -160,6 +160,7 @@ Menu {
|
|||
|
||||
// Separator
|
||||
Rectangle {
|
||||
visible: emojiSearch.text === ''
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: emojiPopup.colors.alternateBase
|
||||
|
@ -167,6 +168,7 @@ Menu {
|
|||
|
||||
// Category picker row
|
||||
RowLayout {
|
||||
visible: emojiSearch.text === ''
|
||||
Layout.bottomMargin: 0
|
||||
Layout.preferredHeight: 42
|
||||
implicitHeight: 42
|
||||
|
@ -245,7 +247,8 @@ Menu {
|
|||
}
|
||||
ToolTip.visible: hovered
|
||||
onClicked: {
|
||||
emojiPopup.model.category = model.category;
|
||||
//emojiPopup.model.category = model.category;
|
||||
gridView.positionViewAtIndex(emojiPopup.model.sourceModel.categoryToIndex(model.category), GridView.Beginning);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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,
|
||||
const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
|
||||
|
||||
|
|
|
@ -12,16 +12,18 @@
|
|||
|
||||
CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
|
||||
int max_mistakes,
|
||||
size_t max_completions,
|
||||
QObject *parent)
|
||||
: QAbstractProxyModel(parent)
|
||||
, maxMistakes_(max_mistakes)
|
||||
, max_completions_(max_completions)
|
||||
{
|
||||
setSourceModel(model);
|
||||
QRegularExpression splitPoints("\\s+|-");
|
||||
|
||||
// insert all the full texts
|
||||
for (int i = 0; i < sourceModel()->rowCount(); i++) {
|
||||
if (i < 7)
|
||||
if (static_cast<size_t>(i) < max_completions_)
|
||||
mapping.push_back(i);
|
||||
|
||||
auto string1 = sourceModel()
|
||||
|
@ -82,14 +84,9 @@ CompletionProxyModel::invalidate()
|
|||
{
|
||||
auto key = searchString_.toUcs4();
|
||||
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();
|
||||
|
||||
std::string temp;
|
||||
for (auto v : mapping) {
|
||||
temp += std::to_string(v) + ", ";
|
||||
}
|
||||
nhlog::ui()->debug("mapping: {}", temp);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
|
@ -101,12 +98,22 @@ CompletionProxyModel::roleNames() const
|
|||
int
|
||||
CompletionProxyModel::rowCount(const QModelIndex &) const
|
||||
{
|
||||
return (int)mapping.size();
|
||||
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();
|
||||
}
|
||||
|
||||
QModelIndex
|
||||
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++) {
|
||||
if (mapping[i] == sourceIndex.row()) {
|
||||
return index(i, 0);
|
||||
|
@ -119,6 +126,12 @@ QModelIndex
|
|||
CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const
|
||||
{
|
||||
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())
|
||||
return QModelIndex();
|
||||
|
||||
|
|
|
@ -153,8 +153,9 @@ class CompletionProxyModel : public QAbstractProxyModel
|
|||
QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
|
||||
public:
|
||||
CompletionProxyModel(QAbstractItemModel *model,
|
||||
int max_mistakes = 2,
|
||||
QObject *parent = nullptr);
|
||||
int max_mistakes = 2,
|
||||
size_t max_completions = 7,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
void invalidate();
|
||||
|
||||
|
@ -184,4 +185,5 @@ private:
|
|||
trie<uint, int> trie_;
|
||||
std::vector<int> mapping;
|
||||
int maxMistakes_;
|
||||
size_t max_completions_;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,20 @@
|
|||
|
||||
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>
|
||||
EmojiModel::roleNames() const
|
||||
{
|
||||
|
@ -60,59 +74,3 @@ EmojiModel::data(const QModelIndex &index, int role) const
|
|||
|
||||
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;
|
||||
|
||||
Q_INVOKABLE int categoryToIndex(int category);
|
||||
|
||||
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::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>>();
|
||||
|
||||
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>(
|
||||
"im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");
|
||||
qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
|
||||
|
@ -595,6 +592,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
|
|||
auto proxy = new CompletionProxyModel(emojiModel);
|
||||
emojiModel->setParent(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") {
|
||||
auto roomModel = new RoomsModel(false);
|
||||
auto proxy = new CompletionProxyModel(roomModel, 4);
|
||||
|
|
Loading…
Reference in a new issue