mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Add username autocompletion popup menu (#2866)
This commit is contained in:
parent
d21858b97f
commit
f605221042
|
@ -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)
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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"};
|
||||
|
||||
|
|
|
@ -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
|
76
src/widgets/splits/InputCompletionItem.cpp
Normal file
76
src/widgets/splits/InputCompletionItem.cpp
Normal 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
|
|
@ -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:
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue