feat: spellcheck input (#6446)

Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
Reviewed-by: fourtf <tf.four@gmail.com>
This commit is contained in:
Nerixyz
2026-01-02 15:56:29 +01:00
committed by GitHub
parent f4212028d6
commit 95bc67fea0
33 changed files with 854 additions and 14 deletions
+11
View File
@@ -11,6 +11,7 @@
#include "controllers/ignores/IgnoreController.hpp"
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/sound/ISoundController.hpp"
#include "controllers/spellcheck/SpellChecker.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/ffz/FfzEmotes.hpp"
@@ -198,6 +199,7 @@ Application::Application(Settings &_settings, const Paths &paths,
, streamerMode(new StreamerMode)
, twitchUsers(new TwitchUsers)
, pronouns(new pronouns::Pronouns)
, spellChecker(new SpellChecker)
#ifdef CHATTERINO_HAVE_PLUGINS
, plugins(new PluginController(paths))
#endif
@@ -608,6 +610,14 @@ eventsub::IController *Application::getEventSub()
return this->eventSub.get();
}
SpellChecker *Application::getSpellChecker()
{
assertInGuiThread();
assert(this->spellChecker);
return this->spellChecker.get();
}
void Application::aboutToQuit()
{
ABOUT_TO_QUIT.store(true);
@@ -660,6 +670,7 @@ void Application::stop()
this->logging.reset();
this->fonts.reset();
this->themes.reset();
this->spellChecker.reset();
STOPPED.store(true);
}
+4
View File
@@ -59,6 +59,7 @@ class Pronouns;
namespace eventsub {
class IController;
} // namespace eventsub
class SpellChecker;
class IApplication
{
@@ -113,6 +114,7 @@ public:
virtual ITwitchUsers *getTwitchUsers() = 0;
virtual pronouns::Pronouns *getPronouns() = 0;
virtual eventsub::IController *getEventSub() = 0;
virtual SpellChecker *getSpellChecker() = 0;
};
class Application : public IApplication
@@ -181,6 +183,7 @@ private:
std::unique_ptr<IStreamerMode> streamerMode;
std::unique_ptr<ITwitchUsers> twitchUsers;
std::unique_ptr<pronouns::Pronouns> pronouns;
std::unique_ptr<SpellChecker> spellChecker;
#ifdef CHATTERINO_HAVE_PLUGINS
std::unique_ptr<PluginController> plugins;
#endif
@@ -234,6 +237,7 @@ public:
ILinkResolver *getLinkResolver() override;
IStreamerMode *getStreamerMode() override;
ITwitchUsers *getTwitchUsers() override;
SpellChecker *getSpellChecker() override;
private:
void initNm(const Paths &paths);
+10
View File
@@ -292,6 +292,9 @@ set(SOURCE_FILES
controllers/sound/NullBackend.cpp
controllers/sound/NullBackend.hpp
controllers/spellcheck/SpellChecker.cpp
controllers/spellcheck/SpellChecker.hpp
controllers/twitch/LiveController.cpp
controllers/twitch/LiveController.hpp
@@ -825,6 +828,8 @@ set(SOURCE_FILES
widgets/splits/InputCompletionItem.hpp
widgets/splits/InputCompletionPopup.cpp
widgets/splits/InputCompletionPopup.hpp
widgets/splits/InputHighlighter.cpp
widgets/splits/InputHighlighter.hpp
widgets/splits/Split.cpp
widgets/splits/Split.hpp
widgets/splits/SplitCommon.hpp
@@ -931,6 +936,11 @@ if (CHATTERINO_PLUGINS)
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
endif()
if (CHATTERINO_SPELLCHECK)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_SPELLCHECK)
target_link_libraries(${LIBRARY_PROJECT} PRIVATE hunspell::hunspell)
endif()
if (CHATTERINO_ALLOW_PRIVATE_QT_API)
target_link_libraries(${LIBRARY_PROJECT} PUBLIC Qt${MAJOR_QT_VERSION}::GuiPrivate)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
+1
View File
@@ -47,6 +47,7 @@ Q_LOGGING_CATEGORY(chatterinoSeventv, "chatterino.seventv", logThreshold);
Q_LOGGING_CATEGORY(chatterinoSeventvEventAPI, "chatterino.seventv.eventapi",
logThreshold);
Q_LOGGING_CATEGORY(chatterinoSound, "chatterino.sound", logThreshold);
Q_LOGGING_CATEGORY(chatterinoSpellcheck, "chatterino.spellcheck", logThreshold);
Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode",
logThreshold);
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
+1
View File
@@ -36,6 +36,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings);
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventv);
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventvEventAPI);
Q_DECLARE_LOGGING_CATEGORY(chatterinoSound);
Q_DECLARE_LOGGING_CATEGORY(chatterinoSpellcheck);
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode);
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTheme);
+6
View File
@@ -111,6 +111,12 @@ void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor,
descriptor.channelName_ = data.value("name").toString();
}
descriptor.filters_ = loadFilters(root.value("filters"));
auto spellOverride = root["checkSpelling"];
if (spellOverride.isBool())
{
descriptor.spellCheckOverride = spellOverride.toBool();
}
}
TabDescriptor TabDescriptor::loadFromJSON(const QJsonObject &tabObj)
+2
View File
@@ -44,6 +44,8 @@ struct SplitDescriptor {
// Whether "Moderation Mode" (the sword icon) is enabled in this split or not
bool moderationMode_{false};
std::optional<bool> spellCheckOverride;
QList<QUuid> filters_;
static void loadFromJSON(SplitDescriptor &descriptor,
+120
View File
@@ -0,0 +1,120 @@
#include "controllers/spellcheck/SpellChecker.hpp"
#include "Application.hpp"
#include "common/QLogging.hpp"
#include "singletons/Paths.hpp"
#include "util/FilesystemHelpers.hpp"
#ifdef CHATTERINO_WITH_SPELLCHECK
# include <hunspell/hunspell.hxx>
#endif
namespace chatterino {
#ifdef CHATTERINO_WITH_SPELLCHECK
class SpellCheckerPrivate
{
public:
static std::unique_ptr<SpellCheckerPrivate> tryLoad();
Hunspell hunspell;
private:
SpellCheckerPrivate(const char *affpath, const char *dpath);
};
std::unique_ptr<SpellCheckerPrivate> SpellCheckerPrivate::tryLoad()
{
auto stdPath = qStringToStdPath(getApp()->getPaths().dictionariesDirectory);
auto aff = stdPath / "index.aff";
auto dic = stdPath / "index.dic";
if (!std::filesystem::exists(aff) || !std::filesystem::exists(dic))
{
qCInfo(chatterinoSpellcheck)
<< "Failed to find index.aff or index.dic in 'Dictionaries'";
return nullptr;
}
std::error_code ec;
auto affCanonical = std::filesystem::weakly_canonical(aff, ec);
if (ec)
{
qCInfo(chatterinoSpellcheck)
<< "Failed to canonicalize" << stdPathToQString(aff)
<< "error:" << QUtf8StringView(ec.message());
return nullptr;
}
auto dicCanonical = std::filesystem::weakly_canonical(dic, ec);
if (ec)
{
qCInfo(chatterinoSpellcheck)
<< "Failed to canonicalize" << stdPathToQString(dic)
<< "error:" << QUtf8StringView(ec.message());
return nullptr;
}
return std::unique_ptr<SpellCheckerPrivate>{new SpellCheckerPrivate(
affCanonical.string().c_str(), dicCanonical.string().c_str())};
}
SpellCheckerPrivate::SpellCheckerPrivate(const char *affpath, const char *dpath)
: hunspell(affpath, dpath)
{
}
SpellChecker::SpellChecker()
: private_(SpellCheckerPrivate::tryLoad())
{
}
#else
class SpellCheckerPrivate
{
};
SpellChecker::SpellChecker() = default;
#endif
SpellChecker::~SpellChecker() = default;
bool SpellChecker::isLoaded() const
{
return this->private_ != nullptr;
}
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
bool SpellChecker::check(const QString &word)
{
#ifdef CHATTERINO_WITH_SPELLCHECK
if (!this->private_)
{
return true;
}
return this->private_->hunspell.spell(word.toStdString());
#else
(void)word;
return true;
#endif
}
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
std::vector<std::string> SpellChecker::suggestions(const QString &word)
{
#ifdef CHATTERINO_WITH_SPELLCHECK
if (!this->private_)
{
return {};
}
auto stdWord = word.toStdString();
if (this->private_->hunspell.spell(stdWord))
{
return {};
}
return this->private_->hunspell.suggest(stdWord);
#else
(void)word;
return {};
#endif
}
} // namespace chatterino
@@ -0,0 +1,30 @@
#pragma once
#include <QString>
#include <memory>
#include <string>
#include <vector>
namespace chatterino {
class Channel;
class TwitchChannel;
class SpellCheckerPrivate;
class SpellChecker
{
public:
SpellChecker();
~SpellChecker();
bool isLoaded() const;
bool check(const QString &word);
std::vector<std::string> suggestions(const QString &word);
private:
std::unique_ptr<SpellCheckerPrivate> private_;
};
} // namespace chatterino
+1
View File
@@ -145,6 +145,7 @@ void Paths::initSubDirectories()
this->pluginsDirectory = makePath("Plugins");
this->themesDirectory = makePath("Themes");
this->crashdumpDirectory = makePath("Crashes");
this->dictionariesDirectory = makePath("Dictionaries");
#ifdef Q_OS_WIN
this->ipcDirectory = makePath("IPC");
#else
+3
View File
@@ -39,6 +39,9 @@ public:
// Custom themes live here. <appDataDirectory>/Themes
QString themesDirectory;
// Spell checking dictionaries <appDataDirectory>/Dictionaries
QString dictionariesDirectory;
// Directory for shared memory files.
// <appDataDirectory>/IPC on Windows
// /tmp elsewhere
+3
View File
@@ -353,6 +353,9 @@ public:
false,
};
BoolSetting enableSpellChecking = {"/behaviour/spellChecking/enabled",
false};
FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0};
EnumSetting<Qt::KeyboardModifier> pauseChatModifier = {
"/behaviour/pauseChatModifier", Qt::KeyboardModifier::NoModifier};
+6
View File
@@ -681,6 +681,12 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
QJsonArray filters;
WindowManager::encodeFilters(node->getSplit(), filters);
obj.insert("filters", filters);
auto spellOverride = node->getSplit()->checkSpellingOverride();
if (spellOverride)
{
obj["checkSpelling"] = *spellOverride;
}
}
break;
case SplitNode::Type::HorizontalContainer:
+8
View File
@@ -5,6 +5,7 @@
#include "controllers/completion/TabCompletionModel.hpp"
#include "singletons/Settings.hpp"
#include <QMenu>
#include <QMimeData>
#include <QMimeDatabase>
#include <QObject>
@@ -337,4 +338,11 @@ void ResizingTextEdit::insertFromMimeData(const QMimeData *source)
insertPlainText(source->text());
}
void ResizingTextEdit::contextMenuEvent(QContextMenuEvent *event)
{
QObjectPtr<QMenu> menu{this->createStandardContextMenu(event->pos())};
this->contextMenuRequested.invoke(menu.get(), event->pos());
menu->exec(event->globalPos());
}
} // namespace chatterino
+3
View File
@@ -21,6 +21,7 @@ public:
pajlada::Signals::NoArgSignal focused;
pajlada::Signals::NoArgSignal focusLost;
pajlada::Signals::Signal<const QMimeData *> imagePasted;
pajlada::Signals::Signal<QMenu *, QPoint> contextMenuRequested;
void setCompleter(QCompleter *c);
/**
@@ -39,6 +40,8 @@ protected:
bool canInsertFromMimeData(const QMimeData *source) const override;
void insertFromMimeData(const QMimeData *source) override;
void contextMenuEvent(QContextMenuEvent *event) override;
private:
// hadSpace is set to true in case the "textUnderCursor" word was after a
// space
+4
View File
@@ -146,6 +146,10 @@ AboutPage::AboutPage()
addLicense(form.getElement(), "Unicode",
"https://www.unicode.org/copyright.html",
":/licenses/unicode.txt");
#ifdef CHATTERINO_WITH_SPELLCHECK
addLicense(form.getElement(), "Hunspell",
"https://hunspell.github.io", ":/licenses/hunspell.txt");
#endif
}
// Attributions
+15
View File
@@ -481,6 +481,21 @@ void GeneralPage::initLayout(GeneralPageView &layout)
"yours is successfully sent in the matching channel.")
->addTo(layout);
#ifdef CHATTERINO_WITH_SPELLCHECK
SettingWidget::checkbox("Check spelling by default (experimental)",
s.enableSpellChecking)
->setTooltip(
u"Check the spelling of words in the input box of all splits by "
"default. This can be overwritten per split in the context menu."
" Chatterino does not include dictionaries - they have to "
"be downloaded or created manually. Chatterino expects Hunspell "
"dictionaries in '" %
getApp()->getPaths().dictionariesDirectory %
u"'. The file index.aff has to contain the affixes and index.dic "
u"must contain the dictionary (subject to change).")
->addTo(layout);
#endif
layout.addTitle("Messages");
SettingWidget::checkbox("Separate with lines", s.separateMessages)
+104
View File
@@ -0,0 +1,104 @@
#include "widgets/splits/InputHighlighter.hpp"
#include "Application.hpp"
#include "common/Aliases.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/spellcheck/SpellChecker.hpp"
#include "messages/Emote.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include <QTextCharFormat>
#include <QTextDocument>
namespace {
using namespace chatterino;
bool isIgnoredWord(TwitchChannel *twitch, const QString &word)
{
EmoteName name{word};
if (twitch)
{
if (twitch->bttvEmote(name) || twitch->ffzEmote(name) ||
twitch->seventvEmote(name))
{
return true;
}
auto locals = twitch->localTwitchEmotes();
if (locals->contains(name))
{
return true;
}
if (twitch->accessChatters()->contains(word))
{
return true;
}
}
if (getApp()->getBttvEmotes()->emote(name) ||
getApp()->getFfzEmotes()->emote(name) ||
getApp()->getSeventvEmotes()->globalEmote(name))
{
return true;
}
return getApp()
->getAccounts()
->twitch.getCurrent()
->twitchEmote(name)
.has_value();
}
} // namespace
namespace chatterino {
InputHighlighter::InputHighlighter(SpellChecker &spellChecker, QObject *parent)
: QSyntaxHighlighter(parent)
, spellChecker(spellChecker)
// FIXME: this also matches URLs - this probably needs to be some function like Firefox' mozEnglishWordUtils::FindNextWord
, wordRegex(R"(\p{L}(?:\P{Z}+\p{L}+)*)",
QRegularExpression::PatternOption::UseUnicodePropertiesOption)
{
this->spellFmt.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
this->spellFmt.setUnderlineColor(Qt::red);
}
InputHighlighter::~InputHighlighter() = default;
void InputHighlighter::setChannel(const std::shared_ptr<Channel> &channel)
{
auto twitch = std::dynamic_pointer_cast<TwitchChannel>(channel);
this->channel = twitch;
this->rehighlight();
}
void InputHighlighter::highlightBlock(const QString &text)
{
if (!this->spellChecker.isLoaded())
{
return;
}
auto *channel = this->channel.lock().get();
QStringView textView = text;
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
auto it = this->wordRegex.globalMatchView(textView);
#else
auto it = this->wordRegex.globalMatch(textView);
#endif
while (it.hasNext())
{
auto match = it.next();
auto text = match.captured();
if (!isIgnoredWord(channel, text) && !this->spellChecker.check(text))
{
this->setFormat(static_cast<int>(match.capturedStart()),
static_cast<int>(text.size()), this->spellFmt);
}
}
}
} // namespace chatterino
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <QRegularExpression>
#include <QString>
#include <QSyntaxHighlighter>
#include <memory>
class QTextDocument;
namespace chatterino {
class Channel;
class TwitchChannel;
class SpellChecker;
/// This highlights the text in the split input.
/// Currently, it only does spell checking.
class InputHighlighter : public QSyntaxHighlighter
{
public:
InputHighlighter(SpellChecker &spellChecker, QObject *parent);
~InputHighlighter() override;
InputHighlighter(const InputHighlighter &) = delete;
InputHighlighter(InputHighlighter &&) = delete;
InputHighlighter &operator=(const InputHighlighter &) = delete;
InputHighlighter &operator=(InputHighlighter &&) = delete;
void setChannel(const std::shared_ptr<Channel> &channel);
protected:
void highlightBlock(const QString &text) override;
private:
SpellChecker &spellChecker;
QTextCharFormat spellFmt;
std::weak_ptr<TwitchChannel> channel;
QRegularExpression wordRegex;
};
} // namespace chatterino
+10
View File
@@ -894,6 +894,16 @@ bool Split::getModerationMode() const
return this->moderationMode_;
}
std::optional<bool> Split::checkSpellingOverride() const
{
return this->input_->checkSpellingOverride();
}
void Split::setCheckSpellingOverride(std::optional<bool> override)
{
this->input_->setCheckSpellingOverride(override);
}
void Split::insertTextToInput(const QString &text)
{
this->input_->insertText(text);
+3
View File
@@ -62,6 +62,9 @@ public:
void setModerationMode(bool value);
bool getModerationMode() const;
std::optional<bool> checkSpellingOverride() const;
void setCheckSpellingOverride(std::optional<bool> override);
void insertTextToInput(const QString &text);
void showChangeChannelPopup(const char *dialogTitle, bool empty,
+2
View File
@@ -908,6 +908,7 @@ void SplitContainer::applyFromDescriptorRecursively(
split->setChannel(WindowManager::decodeChannel(splitNode));
split->setModerationMode(splitNode.moderationMode_);
split->setFilters(splitNode.filters_);
split->setCheckSpellingOverride(splitNode.spellCheckOverride);
this->insertSplit(split);
@@ -943,6 +944,7 @@ void SplitContainer::applyFromDescriptorRecursively(
split->setFilters(splitNode.filters_);
split->setChannel(WindowManager::decodeChannel(splitNode));
split->setModerationMode(splitNode.moderationMode_);
split->setCheckSpellingOverride(splitNode.spellCheckOverride);
auto node = std::make_shared<Node>();
node->parent_ = baseNode;
+85 -5
View File
@@ -5,6 +5,7 @@
#include "common/QLogging.hpp"
#include "controllers/commands/CommandController.hpp"
#include "controllers/hotkeys/HotkeyController.hpp"
#include "controllers/spellcheck/SpellChecker.hpp"
#include "messages/Link.hpp"
#include "messages/Message.hpp"
#include "providers/twitch/TwitchChannel.hpp"
@@ -25,6 +26,7 @@
#include "widgets/Notebook.hpp"
#include "widgets/Scrollbar.hpp"
#include "widgets/splits/InputCompletionPopup.hpp"
#include "widgets/splits/InputHighlighter.hpp"
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
@@ -73,14 +75,26 @@ SplitInput::SplitInput(QWidget *parent, Split *_chatWidget,
new QCompleter(this->split_->getChannel()->completionModel);
this->ui_.textEdit->setCompleter(completer);
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
auto *spellChecker = getApp()->getSpellChecker();
this->inputHighlighter = new InputHighlighter(*spellChecker, this);
this->inputHighlighter->setChannel(this->split_->getChannel());
this->signalHolder_.managedConnect(this->split_->channelChanged, [this] {
auto channel = this->split_->getChannel();
auto *completer = new QCompleter(channel->completionModel);
this->ui_.textEdit->setCompleter(completer);
this->inputHighlighter->setChannel(this->split_->getChannel());
});
getSettings()->enableSpellChecking.connect(
[this] {
this->checkSpellingChanged();
},
this->signalHolder_);
// misc
this->installKeyPressedEvent();
this->installTextEditEvents();
this->addShortcuts();
// The textEdit's signal will be destroyed before this SplitInput is
// destroyed, so we can safely ignore this signal's connection.
@@ -720,7 +734,7 @@ bool SplitInput::eventFilter(QObject *obj, QEvent *event)
return BaseWidget::eventFilter(obj, event);
}
void SplitInput::installKeyPressedEvent()
void SplitInput::installTextEditEvents()
{
// We can safely ignore this signal's connection because SplitInput owns
// the textEdit object, so it will always be deleted before SplitInput
@@ -752,10 +766,42 @@ void SplitInput::installKeyPressedEvent()
}
});
#ifdef DEBUG
assert(this->keyPressedEventInstalled == false);
this->keyPressedEventInstalled = true;
std::ignore = this->ui_.textEdit->contextMenuRequested.connect(
[this](QMenu *menu, QPoint pos) {
#ifdef CHATTERINO_WITH_SPELLCHECK
menu->addSeparator();
auto *spellcheckAction = new QAction("Check spelling", menu);
spellcheckAction->setCheckable(true);
spellcheckAction->setChecked(this->shouldCheckSpelling());
QObject::connect(spellcheckAction, &QAction::toggled, this,
[this](bool enabled) {
this->checkSpellingOverride_ = enabled;
this->checkSpellingChanged();
});
menu->addAction(spellcheckAction);
auto cursor = this->ui_.textEdit->cursorForPosition(pos);
cursor.select(QTextCursor::WordUnderCursor);
auto word = cursor.selectedText();
if (!word.isEmpty())
{
auto suggestions =
getApp()->getSpellChecker()->suggestions(word);
for (const auto &sugg : suggestions)
{
auto qSugg = QString::fromStdString(sugg);
menu->addAction(qSugg, [this, qSugg, cursor]() mutable {
cursor.insertText(qSugg);
this->ui_.textEdit->setTextCursor(cursor);
});
}
}
#else
(void)menu;
(void)pos;
(void)this;
#endif
});
}
void SplitInput::mousePressEvent(QMouseEvent *event)
@@ -1329,6 +1375,40 @@ void SplitInput::setBackgroundColor(QColor newColor)
this->updateTextEditPalette();
}
std::optional<bool> SplitInput::checkSpellingOverride() const
{
return this->checkSpellingOverride_;
}
void SplitInput::setCheckSpellingOverride(std::optional<bool> override)
{
this->checkSpellingOverride_ = override;
this->checkSpellingChanged();
}
bool SplitInput::shouldCheckSpelling() const
{
if (this->checkSpellingOverride_)
{
return *this->checkSpellingOverride_;
}
return getSettings()->enableSpellChecking;
}
void SplitInput::checkSpellingChanged()
{
QTextDocument *target = nullptr;
if (this->shouldCheckSpelling())
{
target = this->ui_.textEdit->document();
}
if (this->inputHighlighter->document() != target)
{
this->inputHighlighter->setDocument(target);
}
}
void SplitInput::updateFonts()
{
auto *app = getApp();
+12 -4
View File
@@ -20,11 +20,13 @@ namespace chatterino {
class Split;
class EmotePopup;
class InputCompletionPopup;
class InputHighlighter;
class MessageView;
class LabelButton;
class ResizingTextEdit;
class ChannelView;
class SvgButton;
class SpellCheckHighlighter;
enum class CompletionKind;
class SplitInput : public BaseWidget
@@ -78,6 +80,9 @@ public:
void triggerSelfMessageReceived();
std::optional<bool> checkSpellingOverride() const;
void setCheckSpellingOverride(std::optional<bool> override);
pajlada::Signals::Signal<const QString &> textChanged;
pajlada::Signals::NoArgSignal selectionChanged;
@@ -102,10 +107,7 @@ protected:
void addShortcuts() override;
void initLayout();
bool eventFilter(QObject *obj, QEvent *event) override;
#ifdef DEBUG
bool keyPressedEventInstalled{};
#endif
void installKeyPressedEvent();
void installTextEditEvents();
void onCursorPositionChanged();
void onTextChanged();
void updateEmoteButton();
@@ -188,6 +190,12 @@ protected:
QPropertyAnimation backgroundColorAnimation;
std::optional<bool> checkSpellingOverride_;
bool shouldCheckSpelling() const;
void checkSpellingChanged();
InputHighlighter *inputHighlighter = nullptr;
void updateFonts();
private Q_SLOTS: