parent
2054aad975
commit
553a97c8bb
@ -200,6 +200,7 @@ set(SRC_FILES
|
|||||||
src/RunGuard.cc
|
src/RunGuard.cc
|
||||||
src/SideBarActions.cc
|
src/SideBarActions.cc
|
||||||
src/Splitter.cc
|
src/Splitter.cc
|
||||||
|
src/SuggestionsPopup.cpp
|
||||||
src/TextInputWidget.cc
|
src/TextInputWidget.cc
|
||||||
src/TopRoomBar.cc
|
src/TopRoomBar.cc
|
||||||
src/TrayIcon.cc
|
src/TrayIcon.cc
|
||||||
@ -296,6 +297,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
include/RoomList.h
|
include/RoomList.h
|
||||||
include/SideBarActions.h
|
include/SideBarActions.h
|
||||||
include/Splitter.h
|
include/Splitter.h
|
||||||
|
include/SuggestionsPopup.hpp
|
||||||
include/TextInputWidget.h
|
include/TextInputWidget.h
|
||||||
include/TopRoomBar.h
|
include/TopRoomBar.h
|
||||||
include/TrayIcon.h
|
include/TrayIcon.h
|
||||||
|
@ -15,6 +15,11 @@ static constexpr int emojiSize = 14;
|
|||||||
static constexpr int headerFontSize = 21;
|
static constexpr int headerFontSize = 21;
|
||||||
static constexpr int typingNotificationFontSize = 11;
|
static constexpr int typingNotificationFontSize = 11;
|
||||||
|
|
||||||
|
namespace popup {
|
||||||
|
static constexpr int font = fontSize;
|
||||||
|
static constexpr int avatar = 28;
|
||||||
|
}
|
||||||
|
|
||||||
namespace receipts {
|
namespace receipts {
|
||||||
static constexpr int font = 12;
|
static constexpr int font = 12;
|
||||||
}
|
}
|
||||||
|
63
include/SuggestionsPopup.hpp
Normal file
63
include/SuggestionsPopup.hpp
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class Avatar;
|
||||||
|
|
||||||
|
struct SearchResult
|
||||||
|
{
|
||||||
|
QString user_id;
|
||||||
|
QString display_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(SearchResult)
|
||||||
|
Q_DECLARE_METATYPE(QVector<SearchResult>)
|
||||||
|
|
||||||
|
class PopupItem : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor)
|
||||||
|
|
||||||
|
public:
|
||||||
|
PopupItem(QWidget *parent, const QString &user_id);
|
||||||
|
|
||||||
|
QColor hoverColor() const { return hoverColor_; }
|
||||||
|
void setHoverColor(QColor &color) { hoverColor_ = color; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void clicked(const QString &display_name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHBoxLayout *topLayout_;
|
||||||
|
|
||||||
|
Avatar *avatar_;
|
||||||
|
QLabel *userName_;
|
||||||
|
QString user_id_;
|
||||||
|
|
||||||
|
QColor hoverColor_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SuggestionsPopup : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SuggestionsPopup(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void addUsers(const QVector<SearchResult> &users);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void itemSelected(const QString &user);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVBoxLayout *layout_;
|
||||||
|
};
|
@ -18,7 +18,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <iterator>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QDebug>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QPaintEvent>
|
#include <QPaintEvent>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
@ -26,15 +30,20 @@
|
|||||||
|
|
||||||
#include "FlatButton.h"
|
#include "FlatButton.h"
|
||||||
#include "LoadingIndicator.h"
|
#include "LoadingIndicator.h"
|
||||||
|
#include "SuggestionsPopup.hpp"
|
||||||
|
|
||||||
#include "dialogs/PreviewUploadOverlay.h"
|
#include "dialogs/PreviewUploadOverlay.h"
|
||||||
|
|
||||||
#include "emoji/PickButton.h"
|
#include "emoji/PickButton.h"
|
||||||
|
|
||||||
|
class RoomState;
|
||||||
|
|
||||||
namespace dialogs {
|
namespace dialogs {
|
||||||
class PreviewUploadOverlay;
|
class PreviewUploadOverlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SearchResult;
|
||||||
|
|
||||||
class FilteredTextEdit : public QTextEdit
|
class FilteredTextEdit : public QTextEdit
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -61,18 +70,45 @@ signals:
|
|||||||
void video(QSharedPointer<QIODevice> data, const QString &filename);
|
void video(QSharedPointer<QIODevice> data, const QString &filename);
|
||||||
void file(QSharedPointer<QIODevice> data, const QString &filename);
|
void file(QSharedPointer<QIODevice> data, const QString &filename);
|
||||||
|
|
||||||
|
//! Trigger the suggestion popup.
|
||||||
|
void showSuggestions(const QString &query);
|
||||||
|
void resultsRetrieved(const QVector<SearchResult> &results);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void showResults(const QVector<SearchResult> &results);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent *event) override;
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
bool canInsertFromMimeData(const QMimeData *source) const override;
|
bool canInsertFromMimeData(const QMimeData *source) const override;
|
||||||
void insertFromMimeData(const QMimeData *source) override;
|
void insertFromMimeData(const QMimeData *source) override;
|
||||||
|
void focusOutEvent(QFocusEvent *event) override
|
||||||
|
{
|
||||||
|
popup_.hide();
|
||||||
|
QWidget::focusOutEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::deque<QString> true_history_, working_history_;
|
std::deque<QString> true_history_, working_history_;
|
||||||
size_t history_index_;
|
size_t history_index_;
|
||||||
QTimer *typingTimer_;
|
QTimer *typingTimer_;
|
||||||
|
|
||||||
|
SuggestionsPopup popup_;
|
||||||
|
|
||||||
|
void closeSuggestions() { popup_.hide(); }
|
||||||
|
void resetAnchor() { atTriggerPosition_ = -1; }
|
||||||
|
|
||||||
|
QString query()
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
|
||||||
|
return cursor.selectedText();
|
||||||
|
}
|
||||||
|
|
||||||
dialogs::PreviewUploadOverlay previewDialog_;
|
dialogs::PreviewUploadOverlay previewDialog_;
|
||||||
|
|
||||||
|
//! Latest position of the '@' character that triggers the username completer.
|
||||||
|
int atTriggerPosition_ = -1;
|
||||||
|
|
||||||
void textChanged();
|
void textChanged();
|
||||||
void uploadData(const QByteArray data, const QString &media, const QString &filename);
|
void uploadData(const QByteArray data, const QString &media, const QString &filename);
|
||||||
void afterCompletion(int);
|
void afterCompletion(int);
|
||||||
@ -97,6 +133,7 @@ public slots:
|
|||||||
void openFileSelection();
|
void openFileSelection();
|
||||||
void hideUploadSpinner();
|
void hideUploadSpinner();
|
||||||
void focusLineEdit() { input_->setFocus(); }
|
void focusLineEdit() { input_->setFocus(); }
|
||||||
|
void setRoomState(QSharedPointer<RoomState> state) { currState_ = state; }
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void addSelectedEmoji(const QString &emoji);
|
void addSelectedEmoji(const QString &emoji);
|
||||||
@ -132,5 +169,8 @@ private:
|
|||||||
FlatButton *sendMessageBtn_;
|
FlatButton *sendMessageBtn_;
|
||||||
emoji::PickButton *emojiBtn_;
|
emoji::PickButton *emojiBtn_;
|
||||||
|
|
||||||
|
//! State of the current room.
|
||||||
|
QSharedPointer<RoomState> currState_;
|
||||||
|
|
||||||
QColor borderColor_;
|
QColor borderColor_;
|
||||||
};
|
};
|
||||||
|
@ -54,4 +54,8 @@ scaleDown(uint64_t max_width, uint64_t max_height, const ImageType &source)
|
|||||||
return source.scaled(
|
return source.scaled(
|
||||||
final_width, final_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
final_width, final_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Calculate the Levenshtein distance between two strings with character skipping.
|
||||||
|
int
|
||||||
|
levenshtein_distance(const std::string &s1, const std::string &s2);
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,11 @@ QuickSwitcher {
|
|||||||
background-color: #202228;
|
background-color: #202228;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupItem {
|
||||||
|
background-color: #202228;
|
||||||
|
qproperty-hoverColor: rgba(45, 49, 57, 120);
|
||||||
|
}
|
||||||
|
|
||||||
RoomList,
|
RoomList,
|
||||||
RoomList > * {
|
RoomList > * {
|
||||||
background-color: #2d3139;
|
background-color: #2d3139;
|
||||||
|
@ -22,6 +22,11 @@ QuickSwitcher {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupItem {
|
||||||
|
background-color: white;
|
||||||
|
qproperty-hoverColor: rgba(192, 193, 195, 120);
|
||||||
|
}
|
||||||
|
|
||||||
RoomList,
|
RoomList,
|
||||||
RoomList > * {
|
RoomList > * {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
@ -25,6 +25,11 @@ QuickSwitcher {
|
|||||||
background-color: palette(window);
|
background-color: palette(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupItem {
|
||||||
|
background-color: palette(window);
|
||||||
|
qproperty-hoverColor: rgba(192, 193, 195, 120);
|
||||||
|
}
|
||||||
|
|
||||||
FlatButton {
|
FlatButton {
|
||||||
qproperty-foregroundColor: palette(text);
|
qproperty-foregroundColor: palette(text);
|
||||||
}
|
}
|
||||||
|
@ -158,6 +158,12 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
|
|||||||
typingDisplay_->setUsers(users);
|
typingDisplay_->setUsers(users);
|
||||||
});
|
});
|
||||||
connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping);
|
connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping);
|
||||||
|
connect(room_list_, &RoomList::roomChanged, text_input_, [this](const QString &room_id) {
|
||||||
|
if (roomStates_.find(room_id) != roomStates_.end())
|
||||||
|
text_input_->setRoomState(roomStates_[room_id]);
|
||||||
|
else
|
||||||
|
qWarning() << "no state found for room_id" << room_id;
|
||||||
|
});
|
||||||
|
|
||||||
connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo);
|
connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo);
|
||||||
connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit);
|
connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit);
|
||||||
@ -781,6 +787,11 @@ ChatPage::updateTypingUsers(const QString &roomid, const std::vector<std::string
|
|||||||
if (!userSettings_->isTypingNotificationsEnabled())
|
if (!userSettings_->isTypingNotificationsEnabled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (user_ids.empty()) {
|
||||||
|
typingUsers_[roomid] = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QStringList users;
|
QStringList users;
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
105
src/SuggestionsPopup.cpp
Normal file
105
src/SuggestionsPopup.cpp
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#include "Avatar.h"
|
||||||
|
#include "AvatarProvider.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include "DropShadow.h"
|
||||||
|
#include "SuggestionsPopup.hpp"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include "timeline/TimelineViewManager.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QStyleOption>
|
||||||
|
|
||||||
|
constexpr int PopupHMargin = 5;
|
||||||
|
constexpr int PopupItemMargin = 4;
|
||||||
|
|
||||||
|
PopupItem::PopupItem(QWidget *parent, const QString &user_id)
|
||||||
|
: QWidget(parent)
|
||||||
|
, avatar_{new Avatar(this)}
|
||||||
|
, user_id_{user_id}
|
||||||
|
{
|
||||||
|
setMouseTracking(true);
|
||||||
|
setAttribute(Qt::WA_Hover);
|
||||||
|
|
||||||
|
topLayout_ = new QHBoxLayout(this);
|
||||||
|
topLayout_->setContentsMargins(
|
||||||
|
PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin);
|
||||||
|
|
||||||
|
QFont font;
|
||||||
|
font.setPixelSize(conf::popup::font);
|
||||||
|
|
||||||
|
auto displayName = TimelineViewManager::displayName(user_id);
|
||||||
|
|
||||||
|
avatar_->setSize(conf::popup::avatar);
|
||||||
|
avatar_->setLetter(utils::firstChar(displayName));
|
||||||
|
|
||||||
|
// If it's a matrix id we use the second letter.
|
||||||
|
if (displayName.size() > 1 && displayName.at(0) == '@')
|
||||||
|
avatar_->setLetter(QChar(displayName.at(1)));
|
||||||
|
|
||||||
|
userName_ = new QLabel(displayName, this);
|
||||||
|
userName_->setFont(font);
|
||||||
|
|
||||||
|
topLayout_->addWidget(avatar_);
|
||||||
|
topLayout_->addWidget(userName_, 1);
|
||||||
|
|
||||||
|
/* AvatarProvider::resolve(user_id, [this](const QImage &img) { avatar_->setImage(img); });
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PopupItem::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QStyleOption opt;
|
||||||
|
opt.init(this);
|
||||||
|
QPainter p(this);
|
||||||
|
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
|
||||||
|
|
||||||
|
if (underMouse())
|
||||||
|
p.fillRect(rect(), hoverColor_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PopupItem::mousePressEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->buttons() != Qt::RightButton)
|
||||||
|
emit clicked(TimelineViewManager::displayName(user_id_));
|
||||||
|
|
||||||
|
QWidget::mousePressEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
SuggestionsPopup::SuggestionsPopup(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||||
|
setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
|
||||||
|
|
||||||
|
layout_ = new QVBoxLayout(this);
|
||||||
|
layout_->setMargin(0);
|
||||||
|
layout_->setSpacing(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SuggestionsPopup::addUsers(const QVector<SearchResult> &users)
|
||||||
|
{
|
||||||
|
// Remove all items from the layout.
|
||||||
|
QLayoutItem *item;
|
||||||
|
while ((item = layout_->takeAt(0)) != 0) {
|
||||||
|
delete item->widget();
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users.isEmpty()) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &u : users) {
|
||||||
|
auto user = new PopupItem(this, u.user_id);
|
||||||
|
layout_->addWidget(user);
|
||||||
|
connect(user, &PopupItem::clicked, this, &SuggestionsPopup::itemSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(geometry().width(), 40 * users.size());
|
||||||
|
}
|
@ -15,6 +15,8 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include <QAbstractTextDocumentLayout>
|
#include <QAbstractTextDocumentLayout>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
@ -28,17 +30,23 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QStyleOption>
|
#include <QStyleOption>
|
||||||
|
|
||||||
|
#include <variant.hpp>
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "RoomState.h"
|
||||||
#include "TextInputWidget.h"
|
#include "TextInputWidget.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
static constexpr size_t INPUT_HISTORY_SIZE = 127;
|
static constexpr size_t INPUT_HISTORY_SIZE = 127;
|
||||||
static constexpr int MAX_TEXTINPUT_HEIGHT = 120;
|
static constexpr int MAX_TEXTINPUT_HEIGHT = 120;
|
||||||
static constexpr int InputHeight = 26;
|
static constexpr int InputHeight = 26;
|
||||||
static constexpr int ButtonHeight = 24;
|
static constexpr int ButtonHeight = 24;
|
||||||
|
static constexpr int MaxPopupItems = 5;
|
||||||
|
|
||||||
FilteredTextEdit::FilteredTextEdit(QWidget *parent)
|
FilteredTextEdit::FilteredTextEdit(QWidget *parent)
|
||||||
: QTextEdit{parent}
|
: QTextEdit{parent}
|
||||||
, history_index_{0}
|
, history_index_{0}
|
||||||
|
, popup_{parent}
|
||||||
, previewDialog_{parent}
|
, previewDialog_{parent}
|
||||||
{
|
{
|
||||||
setFrameStyle(QFrame::NoFrame);
|
setFrameStyle(QFrame::NoFrame);
|
||||||
@ -64,9 +72,43 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
|
|||||||
this,
|
this,
|
||||||
&FilteredTextEdit::uploadData);
|
&FilteredTextEdit::uploadData);
|
||||||
|
|
||||||
|
qRegisterMetaType<SearchResult>();
|
||||||
|
qRegisterMetaType<QVector<SearchResult>>();
|
||||||
|
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
|
||||||
|
connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
|
||||||
|
popup_.hide();
|
||||||
|
|
||||||
|
auto cursor = textCursor();
|
||||||
|
const int end = cursor.position();
|
||||||
|
|
||||||
|
cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor);
|
||||||
|
cursor.setPosition(end, QTextCursor::KeepAnchor);
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
cursor.insertText(text);
|
||||||
|
});
|
||||||
|
|
||||||
previewDialog_.hide();
|
previewDialog_.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
FilteredTextEdit::showResults(const QVector<SearchResult> &results)
|
||||||
|
{
|
||||||
|
QPoint pos;
|
||||||
|
|
||||||
|
if (atTriggerPosition_ != -1) {
|
||||||
|
auto cursor = textCursor();
|
||||||
|
cursor.setPosition(atTriggerPosition_);
|
||||||
|
pos = viewport()->mapToGlobal(cursorRect(cursor).topLeft());
|
||||||
|
} else {
|
||||||
|
auto rect = cursorRect();
|
||||||
|
pos = viewport()->mapToGlobal(rect.topLeft());
|
||||||
|
}
|
||||||
|
|
||||||
|
popup_.addUsers(results);
|
||||||
|
popup_.move(pos.x(), pos.y() - popup_.height() - 10);
|
||||||
|
popup_.show();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
FilteredTextEdit::keyPressEvent(QKeyEvent *event)
|
FilteredTextEdit::keyPressEvent(QKeyEvent *event)
|
||||||
{
|
{
|
||||||
@ -79,7 +121,34 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
|
|||||||
typingTimer_->start();
|
typingTimer_->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate the new query
|
||||||
|
if (textCursor().position() < atTriggerPosition_ || atTriggerPosition_ == -1) {
|
||||||
|
resetAnchor();
|
||||||
|
closeSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popup_.isVisible()) {
|
||||||
|
switch (event->key()) {
|
||||||
|
case Qt::Key_Enter:
|
||||||
|
case Qt::Key_Return:
|
||||||
|
case Qt::Key_Escape:
|
||||||
|
case Qt::Key_Tab:
|
||||||
|
case Qt::Key_Space:
|
||||||
|
case Qt::Key_Backtab: {
|
||||||
|
closeSuggestions();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (event->key()) {
|
switch (event->key()) {
|
||||||
|
case Qt::Key_At:
|
||||||
|
atTriggerPosition_ = textCursor().position();
|
||||||
|
|
||||||
|
QTextEdit::keyPressEvent(event);
|
||||||
|
break;
|
||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
case Qt::Key_Enter:
|
case Qt::Key_Enter:
|
||||||
if (!(event->modifiers() & Qt::ShiftModifier)) {
|
if (!(event->modifiers() & Qt::ShiftModifier)) {
|
||||||
@ -124,6 +193,30 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
QTextEdit::keyPressEvent(event);
|
QTextEdit::keyPressEvent(event);
|
||||||
|
|
||||||
|
// Check if the current word should be autocompleted.
|
||||||
|
auto cursor = textCursor();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
|
||||||
|
auto word = cursor.selectedText();
|
||||||
|
|
||||||
|
if (cursor.position() == 0) {
|
||||||
|
closeSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor.position() == atTriggerPosition_ + 1) {
|
||||||
|
const auto q = query();
|
||||||
|
|
||||||
|
if (q.isEmpty()) {
|
||||||
|
closeSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit showSuggestions(query());
|
||||||
|
} else {
|
||||||
|
closeSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -340,6 +433,52 @@ TextInputWidget::TextInputWidget(QWidget *parent)
|
|||||||
setFixedHeight(widgetHeight);
|
setFixedHeight(widgetHeight);
|
||||||
input_->setFixedHeight(textInputHeight);
|
input_->setFixedHeight(textInputHeight);
|
||||||
});
|
});
|
||||||
|
connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) {
|
||||||
|
if (q.isEmpty() || currState_.isNull())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::thread worker([this, q = q.toLower().toStdString()]() {
|
||||||
|
std::multimap<int, std::pair<std::string, std::string>> items;
|
||||||
|
|
||||||
|
auto get_name = [](auto membership) {
|
||||||
|
auto name = membership.second.content.display_name;
|
||||||
|
auto key = membership.first;
|
||||||
|
|
||||||
|
// Remove the leading '@' character.
|
||||||
|
if (name.empty()) {
|
||||||
|
key.erase(0, 1);
|
||||||
|
name = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_pair(key, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &m : currState_->memberships) {
|
||||||
|
const auto user = get_name(m);
|
||||||
|
const int score = utils::levenshtein_distance(q, user.second);
|
||||||
|
|
||||||
|
items.emplace(score, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<SearchResult> results;
|
||||||
|
auto end = items.begin();
|
||||||
|
|
||||||
|
if (items.size() >= MaxPopupItems)
|
||||||
|
std::advance(end, MaxPopupItems);
|
||||||
|
|
||||||
|
for (auto it = items.begin(); it != end; it++) {
|
||||||
|
const auto user = it->second;
|
||||||
|
|
||||||
|
results.push_back(
|
||||||
|
SearchResult{QString::fromStdString(user.first),
|
||||||
|
QString::fromStdString(user.second)});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit input_->resultsRetrieved(results);
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.detach();
|
||||||
|
});
|
||||||
|
|
||||||
sendMessageBtn_ = new FlatButton(this);
|
sendMessageBtn_ = new FlatButton(this);
|
||||||
|
|
||||||
|
28
src/Utils.cc
28
src/Utils.cc
@ -149,3 +149,31 @@ utils::humanReadableFileSize(uint64_t bytes)
|
|||||||
|
|
||||||
return QString::number(size, 'g', 4) + ' ' + units[u];
|
return QString::number(size, 'g', 4) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
utils::levenshtein_distance(const std::string &s1, const std::string &s2)
|
||||||
|
{
|
||||||
|
const int nlen = s1.size();
|
||||||
|
const int hlen = s2.size();
|
||||||
|
|
||||||
|
if (hlen == 0)
|
||||||
|
return -1;
|
||||||
|
if (nlen == 1)
|
||||||
|
return s2.find(s1);
|
||||||
|
|
||||||
|
std::vector<int> row1(hlen + 1, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < nlen; ++i) {
|
||||||
|
std::vector<int> row2(1, i + 1);
|
||||||
|
|
||||||
|
for (int j = 0; j < hlen; ++j) {
|
||||||
|
const int cost = s1[i] != s2[j];
|
||||||
|
row2.push_back(
|
||||||
|
std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost)));
|
||||||
|
}
|
||||||
|
|
||||||
|
row1.swap(row2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return *std::min_element(row1.begin(), row1.end());
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user