mirror of https://github.com/Nheko-Reborn/nheko
parent
094c0b09ab
commit
b47d2a809c
@ -1,496 +0,0 @@ |
|||||||
/*
|
|
||||||
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> |
|
||||||
* |
|
||||||
* This program is free software: you can redistribute it and/or modify |
|
||||||
* it under the terms of the GNU General Public License as published by |
|
||||||
* the Free Software Foundation, either version 3 of the License, or |
|
||||||
* (at your option) any later version. |
|
||||||
* |
|
||||||
* This program is distributed in the hope that it will be useful, |
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
* GNU General Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License |
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/ |
|
||||||
|
|
||||||
#include <QAbstractItemView> |
|
||||||
#include <QAbstractTextDocumentLayout> |
|
||||||
#include <QBuffer> |
|
||||||
#include <QClipboard> |
|
||||||
#include <QCompleter> |
|
||||||
#include <QFileDialog> |
|
||||||
#include <QMimeData> |
|
||||||
#include <QMimeDatabase> |
|
||||||
#include <QMimeType> |
|
||||||
#include <QPainter> |
|
||||||
#include <QStyleOption> |
|
||||||
#include <QtConcurrent> |
|
||||||
|
|
||||||
#include "Cache.h" |
|
||||||
#include "ChatPage.h" |
|
||||||
#include "CompletionModelRoles.h" |
|
||||||
#include "CompletionProxyModel.h" |
|
||||||
#include "Logging.h" |
|
||||||
#include "TextInputWidget.h" |
|
||||||
#include "Utils.h" |
|
||||||
#include "emoji/EmojiSearchModel.h" |
|
||||||
#include "emoji/Provider.h" |
|
||||||
#include "ui/FlatButton.h" |
|
||||||
#include "ui/LoadingIndicator.h" |
|
||||||
|
|
||||||
#if defined(Q_OS_MAC) |
|
||||||
#include "emoji/MacHelper.h" |
|
||||||
#endif |
|
||||||
|
|
||||||
static constexpr size_t INPUT_HISTORY_SIZE = 127; |
|
||||||
static constexpr int MAX_TEXTINPUT_HEIGHT = 120; |
|
||||||
static constexpr int ButtonHeight = 22; |
|
||||||
|
|
||||||
FilteredTextEdit::FilteredTextEdit(QWidget *parent) |
|
||||||
: QTextEdit{parent} |
|
||||||
, history_index_{0} |
|
||||||
, suggestionsPopup_{parent} |
|
||||||
, previewDialog_{parent} |
|
||||||
{ |
|
||||||
setFrameStyle(QFrame::NoFrame); |
|
||||||
connect(document()->documentLayout(), |
|
||||||
&QAbstractTextDocumentLayout::documentSizeChanged, |
|
||||||
this, |
|
||||||
&FilteredTextEdit::updateGeometry); |
|
||||||
connect(document()->documentLayout(), |
|
||||||
&QAbstractTextDocumentLayout::documentSizeChanged, |
|
||||||
this, |
|
||||||
[this]() { emit heightChanged(document()->size().toSize().height()); }); |
|
||||||
working_history_.push_back(""); |
|
||||||
connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged); |
|
||||||
setAcceptRichText(false); |
|
||||||
|
|
||||||
completer_ = new QCompleter(this); |
|
||||||
completer_->setWidget(this); |
|
||||||
auto model = new emoji::EmojiSearchModel(this); |
|
||||||
model->sort(0, Qt::AscendingOrder); |
|
||||||
completer_->setModel((emoji_completion_model_ = new CompletionProxyModel(model, this))); |
|
||||||
emoji_completion_model_->setFilterRole(CompletionModel::SearchRole); |
|
||||||
completer_->setModelSorting(QCompleter::UnsortedModel); |
|
||||||
completer_->popup()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
|
||||||
completer_->popup()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
|
||||||
|
|
||||||
connect(completer_, |
|
||||||
QOverload<const QModelIndex &>::of(&QCompleter::activated), |
|
||||||
[this](auto &index) { |
|
||||||
emoji_popup_open_ = false; |
|
||||||
auto text = index.data(CompletionModel::CompletionRole).toString(); |
|
||||||
insertCompletion(text); |
|
||||||
}); |
|
||||||
|
|
||||||
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); |
|
||||||
connect( |
|
||||||
&suggestionsPopup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { |
|
||||||
suggestionsPopup_.hide(); |
|
||||||
|
|
||||||
auto cursor = textCursor(); |
|
||||||
const int end = cursor.position(); |
|
||||||
|
|
||||||
cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); |
|
||||||
cursor.setPosition(end, QTextCursor::KeepAnchor); |
|
||||||
cursor.removeSelectedText(); |
|
||||||
cursor.insertText(text); |
|
||||||
}); |
|
||||||
|
|
||||||
// For cycling through the suggestions by hitting tab.
|
|
||||||
connect(this, |
|
||||||
&FilteredTextEdit::selectNextSuggestion, |
|
||||||
&suggestionsPopup_, |
|
||||||
&SuggestionsPopup::selectNextSuggestion); |
|
||||||
connect(this, |
|
||||||
&FilteredTextEdit::selectPreviousSuggestion, |
|
||||||
&suggestionsPopup_, |
|
||||||
&SuggestionsPopup::selectPreviousSuggestion); |
|
||||||
connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { |
|
||||||
suggestionsPopup_.selectHoveredSuggestion<UserItem>(); |
|
||||||
}); |
|
||||||
|
|
||||||
previewDialog_.hide(); |
|
||||||
} |
|
||||||
|
|
||||||
void |
|
||||||
FilteredTextEdit::insertCompletion(QString completion) |
|
||||||
{ |
|
||||||
// Paint the current word and replace it with 'completion'
|
|
||||||
auto cur_text = textAfterPosition(trigger_pos_); |
|
||||||
auto tc = textCursor(); |
|
||||||
tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, cur_text.length()); |
|
||||||
tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, cur_text.length()); |
|
||||||
tc.insertText(completion); |
|
||||||
setTextCursor(tc); |
|
||||||
} |
|
||||||
|
|
||||||
void |
|
||||||
FilteredTextEdit::showResults(const std::vector<SearchResult> &results) |
|
||||||
{ |
|
||||||
QPoint pos; |
|
||||||
|
|
||||||
if (isAnchorValid()) { |
|
||||||
auto cursor = textCursor(); |
|
||||||
cursor.setPosition(atTriggerPosition_); |
|
||||||
pos = viewport()->mapToGlobal(cursorRect(cursor).topLeft()); |
|
||||||
} else { |
|
||||||
auto rect = cursorRect(); |
|
||||||
pos = viewport()->mapToGlobal(rect.topLeft()); |
|
||||||
} |
|
||||||
|
|
||||||
suggestionsPopup_.addUsers(results); |
|
||||||
suggestionsPopup_.move(pos.x(), pos.y() - suggestionsPopup_.height() - 10); |
|
||||||
suggestionsPopup_.show(); |
|
||||||
} |
|
||||||
|
|
||||||
void |
|
||||||
FilteredTextEdit::keyPressEvent(QKeyEvent *event) |
|
||||||
{ |
|
||||||
const bool isModifier = (event->modifiers() != Qt::NoModifier); |
|
||||||
|
|
||||||
#if defined(Q_OS_MAC) |
|
||||||
if (event->modifiers() == (Qt::ControlModifier | Qt::MetaModifier) && |
|
||||||
event->key() == Qt::Key_Space) |
|
||||||
MacHelper::showEmojiWindow(); |
|
||||||
#endif |
|
||||||
|
|
||||||
if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_U) |
|
||||||
QTextEdit::setText(""); |
|
||||||
|
|
||||||
// calculate the new query
|
|
||||||
if (textCursor().position() < atTriggerPosition_ || !isAnchorValid()) { |
|
||||||
resetAnchor(); |
|
||||||
closeSuggestions(); |
|
||||||
} |
|
||||||
|
|
||||||
if (suggestionsPopup_.isVisible()) { |
|
||||||
switch (event->key()) { |
|
||||||
case Qt::Key_Down: |
|
||||||
case Qt::Key_Tab: |
|
||||||
emit selectNextSuggestion(); |
|
||||||
return; |
|
||||||
case Qt::Key_Enter: |
|
||||||
case Qt::Key_Return: |
|
||||||
emit selectHoveredSuggestion(); |
|
||||||
return; |
|
||||||
case Qt::Key_Escape: |
|
||||||
closeSuggestions(); |
|
||||||
return; |
|
||||||
case Qt::Key_Up: |
|
||||||
case Qt::Key_Backtab: { |
|
||||||
emit selectPreviousSuggestion(); |
|
||||||
return; |
|
||||||
} |
|
||||||
default: |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (emoji_popup_open_) { |
|
||||||
auto fake_key = (event->key() == Qt::Key_Backtab) ? Qt::Key_Up : Qt::Key_Down; |
|
||||||
switch (event->key()) { |
|
||||||
case Qt::Key_Backtab: |
|
||||||
case Qt::Key_Tab: { |
|
||||||
// Simulate up/down arrow press
|
|
||||||
auto ev = new QKeyEvent(QEvent::KeyPress, fake_key, Qt::NoModifier); |
|
||||||
QCoreApplication::postEvent(completer_->popup(), ev); |
|
||||||
return; |
|
||||||
} |
|
||||||
default: |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
switch (event->key()) { |
|
||||||
case Qt::Key_At: |
|
||||||
atTriggerPosition_ = textCursor().position(); |
|
||||||
anchorType_ = AnchorType::Sigil; |
|
||||||
|
|
||||||
QTextEdit::keyPressEvent(event); |
|
||||||
break; |
|
||||||
case Qt::Key_Tab: { |
|
||||||
auto cursor = textCursor(); |
|
||||||
const int initialPos = cursor.position(); |
|
||||||
|
|
||||||
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); |
|
||||||
auto word = cursor.selectedText(); |
|
||||||
|
|
||||||
const int startOfWord = cursor.position(); |
|
||||||
|
|
||||||
// There is a word to complete.
|
|
||||||
if (initialPos != startOfWord) { |
|
||||||
atTriggerPosition_ = startOfWord; |
|
||||||
anchorType_ = AnchorType::Tab; |
|
||||||
|
|
||||||
emit showSuggestions(word); |
|
||||||
} else { |
|
||||||
QTextEdit::keyPressEvent(event); |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
case Qt::Key_Colon: { |
|
||||||
QTextEdit::keyPressEvent(event); |
|
||||||
trigger_pos_ = textCursor().position() - 1; |
|
||||||
emoji_completion_model_->setFilterRegExp(""); |
|
||||||
emoji_popup_open_ = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
case Qt::Key_Return: |
|
||||||
case Qt::Key_Enter: |
|
||||||
if (emoji_popup_open_) { |
|
||||||
if (!completer_->popup()->currentIndex().isValid()) { |
|
||||||
// No completion to select, do normal behavior
|
|
||||||
completer_->popup()->hide(); |
|
||||||
emoji_popup_open_ = false; |
|
||||||
} else { |
|
||||||
event->ignore(); |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (!(event->modifiers() & Qt::ShiftModifier)) { |
|
||||||
submit(); |
|
||||||
} else { |
|
||||||
QTextEdit::keyPressEvent(event); |
|
||||||
} |
|
||||||
break; |
|
||||||
case Qt::Key_Up: { |
|
||||||
auto initial_cursor = textCursor(); |
|
||||||
QTextEdit::keyPressEvent(event); |
|
||||||
|
|
||||||
if (textCursor() == initial_cursor && textCursor().atStart() && |
|
||||||
history_index_ + 1 < working_history_.size()) { |
|
||||||
++history_index_; |
|
||||||
setPlainText(working_history_[history_index_]); |
|
||||||
moveCursor(QTextCursor::End); |
|
||||||
} else if (textCursor() == initial_cursor) { |
|
||||||
// Move to the start of the text if there aren't any lines to move up to.
|
|
||||||
initial_cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1); |
|
||||||
setTextCursor(initial_cursor); |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
case Qt::Key_Down: { |
|
||||||
auto initial_cursor = textCursor(); |
|
||||||
QTextEdit::keyPressEvent(event); |
|
||||||
|
|
||||||
if (textCursor() == initial_cursor && textCursor().atEnd() && history_index_ > 0) { |
|
||||||
--history_index_; |
|
||||||
setPlainText(working_history_[history_index_]); |
|
||||||
moveCursor(QTextCursor::End); |
|
||||||
} else if (textCursor() == initial_cursor) { |
|
||||||
// Move to the end of the text if there aren't any lines to move down to.
|
|
||||||
initial_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor, 1); |
|
||||||
setTextCursor(initial_cursor); |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
default: |
|
||||||
QTextEdit::keyPressEvent(event); |
|
||||||
|
|
||||||
if (isModifier) |
|
||||||
return; |
|
||||||
|
|
||||||
if (emoji_popup_open_ && textAfterPosition(trigger_pos_).length() > 2) { |
|
||||||
// Update completion
|
|
||||||
// Don't include the trigger token in the search
|
|
||||||
emoji_completion_model_->setFilterWildcard( |
|
||||||
textAfterPosition(trigger_pos_).remove(0, 1)); |
|
||||||
completer_->complete(completerRect()); |
|
||||||
} |
|
||||||
|
|
||||||
if (emoji_popup_open_ && (completer_->completionCount() < 1 || |
|
||||||
!textAfterPosition(trigger_pos_) |
|
||||||
.contains(QRegularExpression(":[^\r\n\t\f\v :]+$")))) { |
|
||||||
// No completions for this word or another word than the completer was
|
|
||||||
// started with
|
|
||||||
emoji_popup_open_ = false; |
|
||||||
completer_->popup()->hide(); |
|
||||||
} |
|
||||||
|
|
||||||
if (textCursor().position() == 0) { |
|
||||||
resetAnchor(); |
|
||||||
closeSuggestions(); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// Check if the current word should be autocompleted.
|
|
||||||
auto cursor = textCursor(); |
|
||||||
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); |
|
||||||
auto word = cursor.selectedText(); |
|
||||||
|
|
||||||
if (hasAnchor(cursor.position(), anchorType_) && isAnchorValid()) { |
|
||||||
if (word.isEmpty()) { |
|
||||||
closeSuggestions(); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
emit showSuggestions(word); |
|
||||||
} else { |
|
||||||
resetAnchor(); |
|
||||||
closeSuggestions(); |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
QRect |
|
||||||
FilteredTextEdit::completerRect() |
|
||||||
{ |
|
||||||
// Move left edge to the beginning of the word
|
|
||||||
auto cursor = textCursor(); |
|
||||||
auto rect = cursorRect(); |
|
||||||
cursor.movePosition( |
|
||||||
QTextCursor::Left, QTextCursor::MoveAnchor, textAfterPosition(trigger_pos_).length()); |
|
||||||
auto cursor_global_x = viewport()->mapToGlobal(cursorRect(cursor).topLeft()).x(); |
|
||||||
auto rect_global_left = viewport()->mapToGlobal(rect.bottomLeft()).x(); |
|
||||||
auto dx = qAbs(rect_global_left - cursor_global_x); |
|
||||||
rect.moveLeft(rect.left() - dx); |
|
||||||
|
|
||||||
auto item_height = completer_->popup()->sizeHintForRow(0); |
|
||||||
auto max_height = item_height * completer_->maxVisibleItems(); |
|
||||||
auto height = (completer_->completionCount() > completer_->maxVisibleItems()) |
|
||||||
? max_height |
|
||||||
: completer_->completionCount() * item_height; |
|
||||||
rect.setWidth(completer_->popup()->sizeHintForColumn(0)); |
|
||||||
rect.moveBottom(-height); |
|
||||||
return rect; |
|
||||||
} |
|
||||||
|
|
||||||
QSize |
|
||||||
FilteredTextEdit::sizeHint() const |
|
||||||
{ |
|
||||||
ensurePolished(); |
|
||||||
auto margins = viewportMargins(); |
|
||||||
margins += document()->documentMargin(); |
|
||||||
QSize size = document()->size().toSize(); |
|
||||||
size.rwidth() += margins.left() + margins.right(); |
|
||||||
size.rheight() += margins.top() + margins.bottom(); |
|
||||||
return size; |
|
||||||
} |
|
||||||
|
|
||||||
QSize |
|
||||||
FilteredTextEdit::minimumSizeHint() const |
|
||||||
{ |
|
||||||
ensurePolished(); |
|
||||||
auto margins = viewportMargins(); |
|
||||||
margins += document()->documentMargin(); |
|
||||||
margins += contentsMargins(); |
|
||||||
QSize size(fontMetrics().averageCharWidth() * 10, |
|
||||||
fontMetrics().lineSpacing() + margins.top() + margins.bottom()); |
|
||||||
return size; |
|
||||||
} |
|
||||||
|
|
||||||
void |
|
||||||
FilteredTextEdit::submit() |
|
||||||
{} |
|
||||||
|
|
||||||
void |
|
||||||
FilteredTextEdit::textChanged() |
|
||||||
{ |
|
||||||
working_history_[history_index_] = toPlainText(); |
|
||||||
} |
|
||||||
|
|
||||||
TextInputWidget::TextInputWidget(QWidget *parent) |
|
||||||
: QWidget(parent) |
|
||||||
{ |
|
||||||
QFont f; |
|
||||||
f.setPointSizeF(f.pointSizeF()); |
|
||||||
const int fontHeight = QFontMetrics(f).height(); |
|
||||||
const int contentHeight = static_cast<int>(fontHeight * 2.5); |
|
||||||
const int InputHeight = static_cast<int>(fontHeight * 1.5); |
|
||||||
|
|
||||||
setFixedHeight(contentHeight); |
|
||||||
setCursor(Qt::ArrowCursor); |
|
||||||
|
|
||||||
topLayout_ = new QHBoxLayout(); |
|
||||||
topLayout_->setSpacing(0); |
|
||||||
topLayout_->setContentsMargins(13, 1, 13, 0); |
|
||||||
|
|
||||||
QIcon send_file_icon; |
|
||||||
send_file_icon.addFile(":/icons/icons/ui/paper-clip-outline.png"); |
|
||||||
|
|
||||||
sendFileBtn_ = new FlatButton(this); |
|
||||||
sendFileBtn_->setToolTip(tr("Send a file")); |
|
||||||
sendFileBtn_->setIcon(send_file_icon); |
|
||||||
sendFileBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); |
|
||||||
|
|
||||||
spinner_ = new LoadingIndicator(this); |
|
||||||
spinner_->setFixedHeight(InputHeight); |
|
||||||
spinner_->setFixedWidth(InputHeight); |
|
||||||
spinner_->setObjectName("FileUploadSpinner"); |
|
||||||
spinner_->hide(); |
|
||||||
|
|
||||||
input_ = new FilteredTextEdit(this); |
|
||||||
input_->setFixedHeight(InputHeight); |
|
||||||
input_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
|
||||||
input_->setPlaceholderText(tr("Write a message...")); |
|
||||||
|
|
||||||
connect(input_, |
|
||||||
&FilteredTextEdit::heightChanged, |
|
||||||
this, |
|
||||||
[this, InputHeight, contentHeight](int height) { |
|
||||||
int widgetHeight = |
|
||||||
std::min(MAX_TEXTINPUT_HEIGHT, std::max(height, contentHeight)); |
|
||||||
int textInputHeight = |
|
||||||
std::min(widgetHeight - 1, std::max(height, InputHeight)); |
|
||||||
|
|
||||||
setFixedHeight(widgetHeight); |
|
||||||
input_->setFixedHeight(textInputHeight); |
|
||||||
|
|
||||||
emit heightChanged(widgetHeight); |
|
||||||
}); |
|
||||||
connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { |
|
||||||
if (q.isEmpty()) |
|
||||||
return; |
|
||||||
|
|
||||||
QtConcurrent::run([this, q = q.toLower().toStdString()]() { |
|
||||||
try { |
|
||||||
emit input_->resultsRetrieved(cache::searchUsers( |
|
||||||
ChatPage::instance()->currentRoom().toStdString(), q)); |
|
||||||
} catch (const lmdb::error &e) { |
|
||||||
nhlog::db()->error("Suggestion retrieval failed: {}", e.what()); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
sendMessageBtn_ = new FlatButton(this); |
|
||||||
sendMessageBtn_->setToolTip(tr("Send a message")); |
|
||||||
|
|
||||||
QIcon send_message_icon; |
|
||||||
send_message_icon.addFile(":/icons/icons/ui/cursor.png"); |
|
||||||
sendMessageBtn_->setIcon(send_message_icon); |
|
||||||
sendMessageBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); |
|
||||||
|
|
||||||
topLayout_->addWidget(sendFileBtn_); |
|
||||||
topLayout_->addWidget(input_); |
|
||||||
topLayout_->addWidget(sendMessageBtn_); |
|
||||||
|
|
||||||
setLayout(topLayout_); |
|
||||||
|
|
||||||
connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); |
|
||||||
connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); |
|
||||||
} |
|
||||||
|
|
||||||
void |
|
||||||
TextInputWidget::focusInEvent(QFocusEvent *event) |
|
||||||
{ |
|
||||||
input_->setFocus(event->reason()); |
|
||||||
} |
|
||||||
|
|
||||||
void |
|
||||||
TextInputWidget::paintEvent(QPaintEvent *) |
|
||||||
{ |
|
||||||
QStyleOption opt; |
|
||||||
opt.init(this); |
|
||||||
QPainter p(this); |
|
||||||
|
|
||||||
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); |
|
||||||
} |
|
@ -1,173 +0,0 @@ |
|||||||
/*
|
|
||||||
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> |
|
||||||
* |
|
||||||
* This program is free software: you can redistribute it and/or modify |
|
||||||
* it under the terms of the GNU General Public License as published by |
|
||||||
* the Free Software Foundation, either version 3 of the License, or |
|
||||||
* (at your option) any later version. |
|
||||||
* |
|
||||||
* This program is distributed in the hope that it will be useful, |
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
* GNU General Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License |
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/ |
|
||||||
|
|
||||||
#pragma once |
|
||||||
|
|
||||||
#include <deque> |
|
||||||
#include <optional> |
|
||||||
|
|
||||||
#include <QCoreApplication> |
|
||||||
#include <QHBoxLayout> |
|
||||||
#include <QPaintEvent> |
|
||||||
#include <QTextEdit> |
|
||||||
#include <QWidget> |
|
||||||
|
|
||||||
#include "dialogs/PreviewUploadOverlay.h" |
|
||||||
#include "popups/SuggestionsPopup.h" |
|
||||||
|
|
||||||
struct SearchResult; |
|
||||||
|
|
||||||
class CompletionProxyModel; |
|
||||||
class FlatButton; |
|
||||||
class LoadingIndicator; |
|
||||||
class QCompleter; |
|
||||||
|
|
||||||
class FilteredTextEdit : public QTextEdit |
|
||||||
{ |
|
||||||
Q_OBJECT |
|
||||||
|
|
||||||
public: |
|
||||||
explicit FilteredTextEdit(QWidget *parent = nullptr); |
|
||||||
|
|
||||||
QSize sizeHint() const override; |
|
||||||
QSize minimumSizeHint() const override; |
|
||||||
|
|
||||||
void submit(); |
|
||||||
|
|
||||||
signals: |
|
||||||
void heightChanged(int height); |
|
||||||
void startedUpload(); |
|
||||||
|
|
||||||
//! Trigger the suggestion popup.
|
|
||||||
void showSuggestions(const QString &query); |
|
||||||
void resultsRetrieved(const std::vector<SearchResult> &results); |
|
||||||
void selectNextSuggestion(); |
|
||||||
void selectPreviousSuggestion(); |
|
||||||
void selectHoveredSuggestion(); |
|
||||||
|
|
||||||
public slots: |
|
||||||
void showResults(const std::vector<SearchResult> &results); |
|
||||||
|
|
||||||
protected: |
|
||||||
void keyPressEvent(QKeyEvent *event) override; |
|
||||||
void focusOutEvent(QFocusEvent *event) override |
|
||||||
{ |
|
||||||
suggestionsPopup_.hide(); |
|
||||||
QTextEdit::focusOutEvent(event); |
|
||||||
} |
|
||||||
|
|
||||||
private: |
|
||||||
bool emoji_popup_open_ = false; |
|
||||||
CompletionProxyModel *emoji_completion_model_; |
|
||||||
std::deque<QString> true_history_, working_history_; |
|
||||||
int trigger_pos_; // Where emoji completer was triggered
|
|
||||||
size_t history_index_; |
|
||||||
QCompleter *completer_; |
|
||||||
|
|
||||||
SuggestionsPopup suggestionsPopup_; |
|
||||||
|
|
||||||
enum class AnchorType |
|
||||||
{ |
|
||||||
Tab = 0, |
|
||||||
Sigil = 1, |
|
||||||
}; |
|
||||||
|
|
||||||
AnchorType anchorType_ = AnchorType::Sigil; |
|
||||||
|
|
||||||
int anchorWidth(AnchorType anchor) { return static_cast<int>(anchor); } |
|
||||||
|
|
||||||
void closeSuggestions() { suggestionsPopup_.hide(); } |
|
||||||
void resetAnchor() { atTriggerPosition_ = -1; } |
|
||||||
bool isAnchorValid() { return atTriggerPosition_ != -1; } |
|
||||||
bool hasAnchor(int pos, AnchorType anchor) |
|
||||||
{ |
|
||||||
return pos == atTriggerPosition_ + anchorWidth(anchor); |
|
||||||
} |
|
||||||
QRect completerRect(); |
|
||||||
QString query() |
|
||||||
{ |
|
||||||
auto cursor = textCursor(); |
|
||||||
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); |
|
||||||
return cursor.selectedText(); |
|
||||||
} |
|
||||||
QString textAfterPosition(int pos) |
|
||||||
{ |
|
||||||
auto tc = textCursor(); |
|
||||||
tc.setPosition(pos); |
|
||||||
tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); |
|
||||||
return tc.selectedText(); |
|
||||||
} |
|
||||||
|
|
||||||
dialogs::PreviewUploadOverlay previewDialog_; |
|
||||||
|
|
||||||
//! Latest position of the '@' character that triggers the username completer.
|
|
||||||
int atTriggerPosition_ = -1; |
|
||||||
|
|
||||||
void insertCompletion(QString completion); |
|
||||||
void textChanged(); |
|
||||||
void afterCompletion(int); |
|
||||||
}; |
|
||||||
|
|
||||||
class TextInputWidget : public QWidget |
|
||||||
{ |
|
||||||
Q_OBJECT |
|
||||||
|
|
||||||
Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) |
|
||||||
|
|
||||||
public: |
|
||||||
TextInputWidget(QWidget *parent = nullptr); |
|
||||||
|
|
||||||
QColor borderColor() const { return borderColor_; } |
|
||||||
void setBorderColor(QColor &color) { borderColor_ = color; } |
|
||||||
void disableInput() |
|
||||||
{ |
|
||||||
input_->setEnabled(false); |
|
||||||
input_->setPlaceholderText(tr("Connection lost. Nheko is trying to re-connect...")); |
|
||||||
} |
|
||||||
void enableInput() |
|
||||||
{ |
|
||||||
input_->setEnabled(true); |
|
||||||
input_->setPlaceholderText(tr("Write a message...")); |
|
||||||
} |
|
||||||
|
|
||||||
public slots: |
|
||||||
void focusLineEdit() { input_->setFocus(); } |
|
||||||
|
|
||||||
signals: |
|
||||||
void heightChanged(int height); |
|
||||||
|
|
||||||
void sendJoinRoomRequest(const QString &room); |
|
||||||
void sendInviteRoomRequest(const QString &userid, const QString &reason); |
|
||||||
void sendKickRoomRequest(const QString &userid, const QString &reason); |
|
||||||
void sendBanRoomRequest(const QString &userid, const QString &reason); |
|
||||||
void sendUnbanRoomRequest(const QString &userid, const QString &reason); |
|
||||||
void changeRoomNick(const QString &displayname); |
|
||||||
|
|
||||||
protected: |
|
||||||
void focusInEvent(QFocusEvent *event) override; |
|
||||||
void paintEvent(QPaintEvent *) override; |
|
||||||
|
|
||||||
private: |
|
||||||
QHBoxLayout *topLayout_; |
|
||||||
FilteredTextEdit *input_; |
|
||||||
|
|
||||||
LoadingIndicator *spinner_; |
|
||||||
|
|
||||||
FlatButton *sendFileBtn_; |
|
||||||
FlatButton *sendMessageBtn_; |
|
||||||
QColor borderColor_; |
|
||||||
}; |
|
Loading…
Reference in new issue