Add username autocompletion popup menu (#2866)

This commit is contained in:
Tal Neoran 2021-06-19 19:29:17 +03:00 committed by GitHub
parent d21858b97f
commit f605221042
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 193 additions and 123 deletions

View file

@ -2,6 +2,7 @@
## Unversioned
- Major: Added username autocompletion popup menu when typing usernames with an @ prefix. (#1979, #2866)
- Major: Added ability to toggle visibility of Channel Tabs - This can be done by right-clicking the tab area or pressing the keyboard shortcut (default: Ctrl+U). (#2600)
- Minor: Restore automod functionality for moderators (#2817, #2887)
- Minor: Add setting for username style (#2889, #2891)

View file

@ -318,8 +318,8 @@ SOURCES += \
src/widgets/settingspages/NotificationPage.cpp \
src/widgets/settingspages/SettingsPage.cpp \
src/widgets/splits/ClosedSplits.cpp \
src/widgets/splits/EmoteInputItem.cpp \
src/widgets/splits/EmoteInputPopup.cpp \
src/widgets/splits/InputCompletionItem.cpp \
src/widgets/splits/InputCompletionPopup.cpp \
src/widgets/splits/Split.cpp \
src/widgets/splits/SplitContainer.cpp \
src/widgets/splits/SplitHeader.cpp \
@ -579,8 +579,8 @@ HEADERS += \
src/widgets/settingspages/NotificationPage.hpp \
src/widgets/settingspages/SettingsPage.hpp \
src/widgets/splits/ClosedSplits.hpp \
src/widgets/splits/EmoteInputItem.hpp \
src/widgets/splits/EmoteInputPopup.hpp \
src/widgets/splits/InputCompletionItem.hpp \
src/widgets/splits/InputCompletionPopup.hpp \
src/widgets/splits/Split.hpp \
src/widgets/splits/SplitContainer.hpp \
src/widgets/splits/SplitHeader.hpp \

View file

@ -437,10 +437,10 @@ set(SOURCE_FILES
widgets/splits/ClosedSplits.cpp
widgets/splits/ClosedSplits.hpp
widgets/splits/EmoteInputItem.cpp
widgets/splits/EmoteInputItem.hpp
widgets/splits/EmoteInputPopup.cpp
widgets/splits/EmoteInputPopup.hpp
widgets/splits/InputCompletionItem.cpp
widgets/splits/InputCompletionItem.hpp
widgets/splits/InputCompletionPopup.cpp
widgets/splits/InputCompletionPopup.hpp
widgets/splits/Split.cpp
widgets/splits/Split.hpp
widgets/splits/SplitContainer.cpp

View file

@ -171,6 +171,8 @@ public:
"/behaviour/autocompletion/userCompletionOnlyWithAt", false};
BoolSetting emoteCompletionWithColon = {
"/behaviour/autocompletion/emoteCompletionWithColon", true};
BoolSetting showUsernameCompletionMenu = {
"/behaviour/autocompletion/showUsernameCompletionMenu", true};
FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0};
EnumSetting<Qt::KeyboardModifier> pauseChatModifier = {

View file

@ -612,6 +612,8 @@ void GeneralPage::initLayout(GeneralPageView &layout)
layout.addCheckbox("Color @usernames", s.colorUsernames);
layout.addCheckbox("Try to find usernames without @ prefix",
s.findAllUsernames);
layout.addCheckbox("Show username autocompletion popup menu",
s.showUsernameCompletionMenu);
const QStringList usernameDisplayModes = {"Username", "Localized name",
"Username and localized name"};

View file

@ -1,63 +0,0 @@
#include "EmoteInputItem.hpp"
namespace chatterino {
EmoteInputItem::EmoteInputItem(const EmotePtr &emote, const QString &text,
ActionCallback action)
: emote_(emote)
, text_(text)
, action_(action)
{
}
void EmoteInputItem::action()
{
if (this->action_ && this->emote_)
this->action_(this->emote_->name.string);
}
void EmoteInputItem::paint(QPainter *painter, const QRect &rect) const
{
painter->setRenderHint(QPainter::SmoothPixmapTransform);
painter->setRenderHint(QPainter::Antialiasing);
auto margin = 4;
auto imageHeight = ICON_SIZE.height() - margin * 2;
QRect iconRect{
rect.topLeft() + QPoint{margin, margin},
QSize{imageHeight, imageHeight},
};
if (this->emote_)
{
if (auto image = this->emote_->images.getImage(2))
{
if (auto pixmap = image->pixmapOrLoad())
{
if (image->height() != 0)
{
auto aspectRatio =
double(image->width()) / double(image->height());
iconRect = {
rect.topLeft() + QPoint{margin, margin},
QSize(int(imageHeight * aspectRatio), imageHeight)};
painter->drawPixmap(iconRect, *pixmap);
}
}
}
}
QRect textRect =
QRect(iconRect.topRight() + QPoint{margin, 0},
QSize(rect.width() - iconRect.width(), iconRect.height()));
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, this->text_);
}
QSize EmoteInputItem::sizeHint(const QRect &rect) const
{
return QSize(rect.width(), ICON_SIZE.height());
}
} // namespace chatterino

View file

@ -0,0 +1,76 @@
#include "InputCompletionItem.hpp"
namespace chatterino {
InputCompletionItem::InputCompletionItem(const EmotePtr &emote,
const QString &text,
ActionCallback action)
: emote_(emote)
, text_(text)
, action_(action)
{
}
void InputCompletionItem::action()
{
if (this->action_)
{
if (this->emote_)
this->action_(this->emote_->name.string);
else
this->action_(this->text_);
}
}
void InputCompletionItem::paint(QPainter *painter, const QRect &rect) const
{
auto margin = 4;
QRect textRect;
if (this->emote_)
{
painter->setRenderHint(QPainter::SmoothPixmapTransform);
painter->setRenderHint(QPainter::Antialiasing);
auto imageHeight = ICON_SIZE.height() - margin * 2;
QRect iconRect{
rect.topLeft() + QPoint{margin, margin},
QSize{imageHeight, imageHeight},
};
if (auto image = this->emote_->images.getImage(2))
{
if (auto pixmap = image->pixmapOrLoad())
{
if (image->height() != 0)
{
auto aspectRatio =
double(image->width()) / double(image->height());
iconRect = {
rect.topLeft() + QPoint{margin, margin},
QSize(int(imageHeight * aspectRatio), imageHeight)};
painter->drawPixmap(iconRect, *pixmap);
}
}
}
textRect =
QRect(iconRect.topRight() + QPoint{margin, 0},
QSize(rect.width() - iconRect.width(), iconRect.height()));
}
else
{
textRect = QRect(rect.topLeft() + QPoint{margin, 0},
QSize(rect.width(), rect.height()));
}
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, this->text_);
}
QSize InputCompletionItem::sizeHint(const QRect &rect) const
{
return QSize(rect.width(), ICON_SIZE.height());
}
} // namespace chatterino

View file

@ -6,13 +6,13 @@
namespace chatterino {
class EmoteInputItem : public GenericListItem
class InputCompletionItem : public GenericListItem
{
using ActionCallback = std::function<void(const QString &)>;
public:
EmoteInputItem(const EmotePtr &emote, const QString &text,
ActionCallback action);
InputCompletionItem(const EmotePtr &emote, const QString &text,
ActionCallback action);
// GenericListItem interface
public:

View file

@ -1,4 +1,4 @@
#include "EmoteInputPopup.hpp"
#include "InputCompletionPopup.hpp"
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
@ -10,7 +10,7 @@
#include "singletons/Emotes.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/listview/GenericListView.hpp"
#include "widgets/splits/EmoteInputItem.hpp"
#include "widgets/splits/InputCompletionItem.hpp"
namespace chatterino {
namespace {
@ -41,7 +41,7 @@ namespace {
}
} // namespace
EmoteInputPopup::EmoteInputPopup(QWidget *parent)
InputCompletionPopup::InputCompletionPopup(QWidget *parent)
: BasePopup({BasePopup::EnableCustomFrame, BasePopup::Frameless,
BasePopup::DontFocus},
parent)
@ -56,7 +56,7 @@ EmoteInputPopup::EmoteInputPopup(QWidget *parent)
this->redrawTimer_.setInterval(33);
}
void EmoteInputPopup::initLayout()
void InputCompletionPopup::initLayout()
{
LayoutCreator creator = {this};
@ -71,7 +71,7 @@ void EmoteInputPopup::initLayout()
});
}
void EmoteInputPopup::updateEmotes(const QString &text, ChannelPtr channel)
void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel)
{
std::vector<_Emote> emotes;
auto tc = dynamic_cast<TwitchChannel *>(channel.get());
@ -122,11 +122,11 @@ void EmoteInputPopup::updateEmotes(const QString &text, ChannelPtr channel)
int count = 0;
for (auto &&emote : emotes)
{
this->model_.addItem(std::make_unique<EmoteInputItem>(
this->model_.addItem(std::make_unique<InputCompletionItem>(
emote.emote, emote.displayName + " - " + emote.providerName,
this->callback_));
if (count++ == maxEmoteCount)
if (count++ == maxEntryCount)
break;
}
@ -136,22 +136,45 @@ void EmoteInputPopup::updateEmotes(const QString &text, ChannelPtr channel)
}
}
bool EmoteInputPopup::eventFilter(QObject *watched, QEvent *event)
void InputCompletionPopup::updateUsers(const QString &text, ChannelPtr channel)
{
auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel)
{
auto chatters = twitchChannel->accessChatters()->filterByPrefix(text);
this->model_.clear();
int count = 0;
for (const auto &name : chatters)
{
this->model_.addItem(std::make_unique<InputCompletionItem>(
nullptr, name, this->callback_));
if (count++ == maxEntryCount)
break;
}
if (!chatters.empty())
{
this->ui_.listView->setCurrentIndex(this->model_.index(0));
}
}
}
bool InputCompletionPopup::eventFilter(QObject *watched, QEvent *event)
{
return this->ui_.listView->eventFilter(watched, event);
}
void EmoteInputPopup::setInputAction(ActionCallback callback)
void InputCompletionPopup::setInputAction(ActionCallback callback)
{
this->callback_ = std::move(callback);
}
void EmoteInputPopup::showEvent(QShowEvent *)
void InputCompletionPopup::showEvent(QShowEvent *)
{
this->redrawTimer_.start();
}
void EmoteInputPopup::hideEvent(QHideEvent *)
void InputCompletionPopup::hideEvent(QHideEvent *)
{
this->redrawTimer_.stop();
}

View file

@ -9,16 +9,17 @@ namespace chatterino {
class GenericListView;
class EmoteInputPopup : public BasePopup
class InputCompletionPopup : public BasePopup
{
using ActionCallback = std::function<void(const QString &)>;
constexpr static int maxEmoteCount = 200;
constexpr static int maxEntryCount = 200;
public:
EmoteInputPopup(QWidget *parent = nullptr);
InputCompletionPopup(QWidget *parent = nullptr);
void updateEmotes(const QString &text, ChannelPtr channel);
void updateUsers(const QString &text, ChannelPtr channel);
virtual bool eventFilter(QObject *, QEvent *event) override;
void setInputAction(ActionCallback callback);

View file

@ -15,7 +15,7 @@
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/EffectLabel.hpp"
#include "widgets/helper/ResizingTextEdit.hpp"
#include "widgets/splits/EmoteInputPopup.hpp"
#include "widgets/splits/InputCompletionPopup.hpp"
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
#include "widgets/splits/SplitInput.hpp"
@ -45,7 +45,7 @@ SplitInput::SplitInput(Split *_chatWidget)
// misc
this->installKeyPressedEvent();
this->ui_.textEdit->focusLost.connect([this] {
this->hideColonMenu();
this->hideCompletionPopup();
});
this->scaleChangedEvent(this->scale());
}
@ -202,7 +202,7 @@ void SplitInput::installKeyPressedEvent()
auto app = getApp();
this->ui_.textEdit->keyPressed.connect([this, app](QKeyEvent *event) {
if (auto popup = this->emoteInputPopup_.get())
if (auto popup = this->inputCompletionPopup_.get())
{
if (popup->isVisible())
{
@ -451,26 +451,30 @@ void SplitInput::installKeyPressedEvent()
void SplitInput::onTextChanged()
{
this->updateColonMenu();
this->updateCompletionPopup();
}
void SplitInput::onCursorPositionChanged()
{
this->updateColonMenu();
this->updateCompletionPopup();
}
void SplitInput::updateColonMenu()
void SplitInput::updateCompletionPopup()
{
auto channel = this->split_->getChannel().get();
if (!getSettings()->emoteCompletionWithColon ||
(!dynamic_cast<TwitchChannel *>(channel) &&
!(channel->getType() == Channel::Type::TwitchWhispers)))
auto tc = dynamic_cast<TwitchChannel *>(channel);
bool showEmoteCompletion =
getSettings()->emoteCompletionWithColon &&
(tc || (channel->getType() == Channel::Type::TwitchWhispers));
bool showUsernameCompletion =
tc && getSettings()->showUsernameCompletionMenu;
if (!showEmoteCompletion && !showUsernameCompletion)
{
this->hideColonMenu();
this->hideCompletionPopup();
return;
}
// check if in :
// check if in completion prefix
auto &edit = *this->ui_.textEdit;
auto text = edit.toPlainText();
@ -478,7 +482,7 @@ void SplitInput::updateColonMenu()
if (text.length() == 0)
{
this->hideColonMenu();
this->hideCompletionPopup();
return;
}
@ -486,41 +490,54 @@ void SplitInput::updateColonMenu()
{
if (text[i] == ' ')
{
this->hideColonMenu();
this->hideCompletionPopup();
return;
}
else if (text[i] == ':')
else if (text[i] == ':' && showEmoteCompletion)
{
if (i == 0 || text[i - 1].isSpace())
this->showColonMenu(text.mid(i, position - i + 1).mid(1));
this->showCompletionPopup(text.mid(i, position - i + 1).mid(1),
true);
else
this->hideColonMenu();
this->hideCompletionPopup();
return;
}
else if (text[i] == '@' && showUsernameCompletion)
{
if (i == 0 || text[i - 1].isSpace())
this->showCompletionPopup(text.mid(i, position - i + 1).mid(1),
false);
else
this->hideCompletionPopup();
return;
}
}
this->hideColonMenu();
this->hideCompletionPopup();
}
void SplitInput::showColonMenu(const QString &text)
void SplitInput::showCompletionPopup(const QString &text, bool emoteCompletion)
{
if (!this->emoteInputPopup_.get())
if (!this->inputCompletionPopup_.get())
{
this->emoteInputPopup_ = new EmoteInputPopup(this);
this->emoteInputPopup_->setInputAction(
this->inputCompletionPopup_ = new InputCompletionPopup(this);
this->inputCompletionPopup_->setInputAction(
[that = QObjectRef(this)](const QString &text) mutable {
if (auto this2 = that.get())
{
this2->insertColonText(text);
this2->hideColonMenu();
this2->insertCompletionText(text);
this2->hideCompletionPopup();
}
});
}
auto popup = this->emoteInputPopup_.get();
auto popup = this->inputCompletionPopup_.get();
assert(popup);
popup->updateEmotes(text, this->split_->getChannel());
if (emoteCompletion) // autocomplete emotes
popup->updateEmotes(text, this->split_->getChannel());
else // autocomplete usernames
popup->updateUsers(text, this->split_->getChannel());
auto pos = this->mapToGlobal({0, 0}) - QPoint(0, popup->height()) +
QPoint((this->width() - popup->width()) / 2, 0);
@ -529,13 +546,13 @@ void SplitInput::showColonMenu(const QString &text)
popup->show();
}
void SplitInput::hideColonMenu()
void SplitInput::hideCompletionPopup()
{
if (auto popup = this->emoteInputPopup_.get())
if (auto popup = this->inputCompletionPopup_.get())
popup->hide();
}
void SplitInput::insertColonText(const QString &input_)
void SplitInput::insertCompletionText(const QString &input_)
{
auto &edit = *this->ui_.textEdit;
auto input = input_ + ' ';
@ -545,10 +562,21 @@ void SplitInput::insertColonText(const QString &input_)
for (int i = clamp(position, 0, text.length() - 1); i >= 0; i--)
{
bool done = false;
if (text[i] == ':')
{
auto cursor = edit.textCursor();
done = true;
}
else if (text[i] == '@')
{
input = "@" + input_ +
(getSettings()->mentionUsersWithComma ? ", " : " ");
done = true;
}
if (done)
{
auto cursor = edit.textCursor();
edit.setText(text.remove(i, position - i).insert(i, input));
cursor.setPosition(i + input.size());

View file

@ -16,7 +16,7 @@ namespace chatterino {
class Split;
class EmotePopup;
class EmoteInputPopup;
class InputCompletionPopup;
class EffectLabel;
class ResizingTextEdit;
@ -48,15 +48,15 @@ private:
void onCursorPositionChanged();
void onTextChanged();
void updateEmoteButton();
void updateColonMenu();
void showColonMenu(const QString &text);
void hideColonMenu();
void insertColonText(const QString &text);
void updateCompletionPopup();
void showCompletionPopup(const QString &text, bool emoteCompletion);
void hideCompletionPopup();
void insertCompletionText(const QString &text);
void openEmotePopup();
Split *const split_;
QObjectRef<EmotePopup> emotePopup_;
QObjectRef<EmoteInputPopup> emoteInputPopup_;
QObjectRef<InputCompletionPopup> inputCompletionPopup_;
struct {
ResizingTextEdit *textEdit;