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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user