feat: add REPL for plugins (#6120)
This can be enabled by setting the `.plugins.repl.enabled` setting to true
Close Chatterino and make a backup of your settings.json before attempting any modifications.
```json5
{
"plugins": {
"repl": {
"enabled": true,
},
},
}
```
This commit is contained in:
@@ -0,0 +1,769 @@
|
||||
#include "widgets/PluginRepl.hpp"
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "singletons/Resources.hpp"
|
||||
# include "singletons/Settings.hpp"
|
||||
# include "singletons/Theme.hpp"
|
||||
# include "widgets/buttons/PixmapButton.hpp"
|
||||
# include "widgets/buttons/SvgButton.hpp"
|
||||
|
||||
# include <QBoxLayout>
|
||||
# include <QFontDatabase>
|
||||
# include <QScrollBar>
|
||||
# include <QSplitter>
|
||||
# include <QTextBlock>
|
||||
# include <QTextEdit>
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
using namespace chatterino;
|
||||
|
||||
class HistoricTextEdit : public QTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HistoricTextEdit(QWidget *parent = nullptr);
|
||||
|
||||
QSize sizeHint() const override
|
||||
{
|
||||
return this->minimumSizeHint();
|
||||
}
|
||||
|
||||
QSize minimumSizeHint() const override
|
||||
{
|
||||
auto margins = this->contentsMargins();
|
||||
|
||||
auto h = margins.top() + this->fontMetrics().height() +
|
||||
margins.bottom() + 15;
|
||||
return {0, h};
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void onSend(const QString & /* text */);
|
||||
|
||||
protected:
|
||||
bool event(QEvent *e) override;
|
||||
|
||||
private:
|
||||
/// If the item wouldn't change, std::nullopt is returned
|
||||
std::optional<QString> nextHistoryItem(qsizetype diff);
|
||||
|
||||
QStringList history;
|
||||
qsizetype historyIdx = 0;
|
||||
QString lastUnfinishedInput;
|
||||
};
|
||||
|
||||
HistoricTextEdit::HistoricTextEdit(QWidget *parent)
|
||||
: QTextEdit(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool HistoricTextEdit::event(QEvent *event)
|
||||
{
|
||||
if (event->type() != QEvent::KeyPress)
|
||||
{
|
||||
return QTextEdit::event(event);
|
||||
}
|
||||
|
||||
auto *e = dynamic_cast<QKeyEvent *>(event);
|
||||
if (!e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e->keyCombination() == QKeyCombination(Qt::Key_Up))
|
||||
{
|
||||
e->accept();
|
||||
auto cursor = this->textCursor();
|
||||
if (cursor.movePosition(QTextCursor::Up))
|
||||
{
|
||||
this->setTextCursor(cursor);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto next = this->nextHistoryItem(-1);
|
||||
if (next)
|
||||
{
|
||||
this->setPlainText(*std::move(next));
|
||||
this->moveCursor(QTextCursor::End);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e->keyCombination() == QKeyCombination(Qt::Key_Down))
|
||||
{
|
||||
e->accept();
|
||||
auto cursor = this->textCursor();
|
||||
if (cursor.movePosition(QTextCursor::Down))
|
||||
{
|
||||
this->setTextCursor(cursor);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto next = this->nextHistoryItem(1);
|
||||
if (next)
|
||||
{
|
||||
this->setPlainText(*std::move(next));
|
||||
this->moveCursor(QTextCursor::End);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e->keyCombination() == QKeyCombination(Qt::Key_Return))
|
||||
{
|
||||
auto text = this->toPlainText().trimmed();
|
||||
if (!text.isEmpty())
|
||||
{
|
||||
e->accept();
|
||||
if (this->history.empty() || this->history.back() != text)
|
||||
{
|
||||
this->history.append(text);
|
||||
}
|
||||
this->historyIdx = this->history.size();
|
||||
this->setPlainText({});
|
||||
this->lastUnfinishedInput = {};
|
||||
this->onSend(text);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return QTextEdit::event(event);
|
||||
}
|
||||
|
||||
std::optional<QString> HistoricTextEdit::nextHistoryItem(qsizetype diff)
|
||||
{
|
||||
if (this->history.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
bool wasUnfinishedInput = this->historyIdx >= this->history.size();
|
||||
|
||||
auto nextIdx =
|
||||
std::clamp(this->historyIdx + diff, 0LL, this->history.size());
|
||||
if (nextIdx == this->historyIdx)
|
||||
{
|
||||
return {}; // nothing changed
|
||||
}
|
||||
this->historyIdx = nextIdx;
|
||||
|
||||
if (this->historyIdx >= this->history.size())
|
||||
{
|
||||
return this->lastUnfinishedInput;
|
||||
}
|
||||
if (wasUnfinishedInput)
|
||||
{
|
||||
this->lastUnfinishedInput = this->toPlainText();
|
||||
}
|
||||
return this->history[this->historyIdx];
|
||||
}
|
||||
|
||||
sol::optional<QString> tryAsStringAndPop(lua_State *L)
|
||||
{
|
||||
auto s = sol::stack::get<sol::optional<std::string_view>>(L);
|
||||
auto qs = s.map([](auto sv) {
|
||||
return QString::fromUtf8(sv.data(), sv.size());
|
||||
});
|
||||
lua_pop(L, 1);
|
||||
return qs;
|
||||
}
|
||||
|
||||
/// This stringifies a value without recursing.
|
||||
///
|
||||
/// It's similar to luaL_tolstring with a two minor differences:
|
||||
/// - `__tostring` is not executed - this can potentially error because it might
|
||||
/// be called without a `self` argument (e.g. in `c2.Channel`).
|
||||
/// - The address for tables, threads, etc. isn't shown.
|
||||
QString stringifyValue(lua_State *L, int idx)
|
||||
{
|
||||
switch (lua_type(L, idx))
|
||||
{
|
||||
case LUA_TNONE:
|
||||
return u"(none)"_s;
|
||||
case LUA_TNIL:
|
||||
return u"nil"_s;
|
||||
case LUA_TSTRING: {
|
||||
// Safety: We know this is a string
|
||||
auto sv = sol::stack::unqualified_get<std::string_view>(L, idx);
|
||||
return QString::fromUtf8(sv.data(),
|
||||
static_cast<qsizetype>(sv.size()));
|
||||
}
|
||||
case LUA_TNUMBER: {
|
||||
if (lua_isinteger(L, idx) != 0)
|
||||
{
|
||||
auto v = static_cast<LUAI_UACINT>(lua_tointeger(L, idx));
|
||||
return QString::number(v);
|
||||
}
|
||||
auto v = static_cast<LUAI_UACNUMBER>(lua_tonumber(L, idx));
|
||||
return QString::number(v);
|
||||
}
|
||||
case LUA_TBOOLEAN: {
|
||||
if (lua_toboolean(L, idx) != 0)
|
||||
{
|
||||
return u"true"_s;
|
||||
}
|
||||
return u"false"_s;
|
||||
}
|
||||
case LUA_TTHREAD:
|
||||
case LUA_TFUNCTION:
|
||||
case LUA_TUSERDATA:
|
||||
case LUA_TLIGHTUSERDATA:
|
||||
case LUA_TTABLE:
|
||||
default: {
|
||||
int tt = luaL_getmetafield(L, idx, "__name");
|
||||
if (tt != LUA_TNIL)
|
||||
{
|
||||
auto name = tryAsStringAndPop(L);
|
||||
if (name)
|
||||
{
|
||||
return *std::move(name);
|
||||
}
|
||||
}
|
||||
|
||||
return QString::fromUtf8(luaL_typename(L, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString stringifyValue(sol::stack_proxy it)
|
||||
{
|
||||
return stringifyValue(it.lua_state(), it.stack_index());
|
||||
}
|
||||
|
||||
QString stringifyValue(const sol::object &obj)
|
||||
{
|
||||
static_assert(!sol::is_stack_based<std::remove_cvref_t<decltype(obj)>>());
|
||||
// XXX: in many cases the value is already on the stack
|
||||
obj.push();
|
||||
auto s = stringifyValue(obj.lua_state(), -1);
|
||||
obj.pop();
|
||||
return s;
|
||||
}
|
||||
|
||||
enum class ExpandFlag : uint8_t {
|
||||
None = 0,
|
||||
TryUsertypeStorage = 1 << 0,
|
||||
};
|
||||
using ExpandFlags = FlagsEnum<ExpandFlag>;
|
||||
|
||||
void stringify(sol::stack_proxy it, QString &s, size_t maxItems = 10,
|
||||
ExpandFlags flags = {})
|
||||
{
|
||||
lua::StackGuard g(it.lua_state());
|
||||
|
||||
auto typ = it.get_type();
|
||||
switch (typ)
|
||||
{
|
||||
case sol::type::userdata: {
|
||||
auto meta = lua_getmetatable(it.lua_state(), it.stack_index());
|
||||
if (meta == 0)
|
||||
{
|
||||
s.append(u"userdata");
|
||||
}
|
||||
|
||||
auto table =
|
||||
sol::stack::unqualified_get<sol::table>(it.lua_state());
|
||||
|
||||
s.append(u"userdata{");
|
||||
size_t n = 0;
|
||||
for (const auto &inner : table)
|
||||
{
|
||||
if (n != 0)
|
||||
{
|
||||
s.append(u", ");
|
||||
}
|
||||
s.append(stringifyValue(inner.first));
|
||||
|
||||
n++;
|
||||
if (n >= maxItems)
|
||||
{
|
||||
s.append(u", ...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
s.append('}');
|
||||
|
||||
lua_pop(it.lua_state(), 1); // metatable
|
||||
return;
|
||||
}
|
||||
case sol::type::table: {
|
||||
auto tbl = it.get<sol::optional<sol::table>>();
|
||||
if (!tbl)
|
||||
{
|
||||
s.append(stringifyValue(it));
|
||||
return;
|
||||
}
|
||||
s.append(u"{");
|
||||
size_t n = 0;
|
||||
|
||||
auto meta = lua_getmetatable(it.lua_state(), it.stack_index());
|
||||
if (meta != 0)
|
||||
{
|
||||
s.append(u"(metatable): "_s);
|
||||
stringify({it.lua_state(), -1}, s, maxItems / 2,
|
||||
flags | ExpandFlag::TryUsertypeStorage);
|
||||
n++;
|
||||
lua_pop(it.lua_state(), 1); // metatable
|
||||
}
|
||||
|
||||
if (flags.has(ExpandFlag::TryUsertypeStorage))
|
||||
{
|
||||
auto uts = sol::u_detail::maybe_get_usertype_storage_base(
|
||||
it.lua_state(), it.stack_index());
|
||||
if (uts)
|
||||
{
|
||||
for (const auto &[key, _] : uts->string_keys)
|
||||
{
|
||||
if (n == 0)
|
||||
{
|
||||
s.append('[');
|
||||
}
|
||||
else
|
||||
{
|
||||
s.append(u", ["_s);
|
||||
}
|
||||
# if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
|
||||
s.append(QString::fromUtf8(
|
||||
key.data(), static_cast<qsizetype>(key.size())));
|
||||
# else
|
||||
s.append(QUtf8StringView(key));
|
||||
# endif
|
||||
s.append("] = <dyn>");
|
||||
n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &inner : *tbl)
|
||||
{
|
||||
if (n == 0)
|
||||
{
|
||||
s.append('[');
|
||||
}
|
||||
else
|
||||
{
|
||||
s.append(u", ["_s);
|
||||
}
|
||||
|
||||
s.append(stringifyValue(inner.first));
|
||||
s.append("] = ");
|
||||
s.append(stringifyValue(inner.second));
|
||||
|
||||
n++;
|
||||
if (n >= maxItems)
|
||||
{
|
||||
s.append(u", ..."_s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
s.append('}');
|
||||
return;
|
||||
}
|
||||
case sol::type::none:
|
||||
case sol::type::lua_nil:
|
||||
case sol::type::string:
|
||||
case sol::type::number:
|
||||
case sol::type::thread:
|
||||
case sol::type::boolean:
|
||||
case sol::type::function:
|
||||
case sol::type::lightuserdata:
|
||||
case sol::type::poly:
|
||||
default:
|
||||
s.append(stringifyValue(it));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
PluginRepl::PluginRepl(QString id, QWidget *parent)
|
||||
: BaseWindow(
|
||||
{
|
||||
BaseWindow::EnableCustomFrame,
|
||||
BaseWindow::DisableCustomScaling,
|
||||
BaseWindow::DisableLayoutSave,
|
||||
},
|
||||
parent)
|
||||
, id(std::move(id))
|
||||
, isPinned(getSettings()->windowTopMost)
|
||||
{
|
||||
this->setAttribute(Qt::WA_DeleteOnClose);
|
||||
this->resize(600, 300);
|
||||
|
||||
this->setWindowTitle(this->id + u" - Plugin REPL"_s);
|
||||
|
||||
auto *root = new QVBoxLayout(this->getLayoutContainer());
|
||||
|
||||
// top row
|
||||
{
|
||||
auto *top = new QHBoxLayout;
|
||||
this->ui.clear = new SvgButton(
|
||||
{
|
||||
.dark = u":/buttons/cancel.svg"_s,
|
||||
.light = u":/buttons/cancelDark.svg"_s,
|
||||
},
|
||||
nullptr, {3, 3});
|
||||
this->ui.clear->setScaleIndependentSize({18, 18});
|
||||
this->ui.clear->setToolTip(u"Clear Output"_s);
|
||||
QObject::connect(this->ui.clear, &Button::leftClicked, this, [this] {
|
||||
this->ui.output->clear();
|
||||
});
|
||||
|
||||
this->ui.reload = new SvgButton(
|
||||
{
|
||||
.dark = u":/buttons/reloadLight.svg"_s,
|
||||
.light = u":/buttons/reloadDark.svg"_s,
|
||||
},
|
||||
nullptr, {3, 3});
|
||||
this->ui.reload->setScaleIndependentSize({18, 18});
|
||||
this->ui.reload->setToolTip(u"Reload"_s);
|
||||
QObject::connect(this->ui.reload, &Button::leftClicked, this, [this] {
|
||||
if (!this->plugin)
|
||||
{
|
||||
this->log(lua::api::LogLevel::Critical, "Plugin not loaded.");
|
||||
return;
|
||||
}
|
||||
this->log({}, u"Reloading..."_s);
|
||||
bool result = getApp()->getPlugins()->reload(this->id);
|
||||
if (result)
|
||||
{
|
||||
this->log({}, u"Reloaded."_s);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->log(lua::api::LogLevel::Critical, u"Failed to reload."_s);
|
||||
}
|
||||
});
|
||||
|
||||
this->ui.pin = new PixmapButton;
|
||||
this->ui.pin->setScaleIndependentSize({18, 18});
|
||||
this->ui.pin->setToolTip(u"Pin Window"_s);
|
||||
QObject::connect(this->ui.pin, &Button::leftClicked, this, [this] {
|
||||
this->isPinned = !this->isPinned;
|
||||
this->updatePinned();
|
||||
});
|
||||
|
||||
top->addStretch(1);
|
||||
top->addWidget(this->ui.clear);
|
||||
top->addWidget(this->ui.reload);
|
||||
top->addWidget(this->ui.pin);
|
||||
|
||||
root->addLayout(top);
|
||||
}
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Vertical, this);
|
||||
splitter->setChildrenCollapsible(false);
|
||||
root->addWidget(splitter, 1);
|
||||
|
||||
this->ui.output = new QTextEdit;
|
||||
splitter->addWidget(this->ui.output);
|
||||
this->ui.output->setUndoRedoEnabled(false);
|
||||
this->ui.output->setReadOnly(true);
|
||||
this->ui.output->setAcceptRichText(false);
|
||||
auto *input = new HistoricTextEdit(this);
|
||||
this->ui.input = input;
|
||||
QObject::connect(input, &HistoricTextEdit::onSend, this,
|
||||
&PluginRepl::tryRun);
|
||||
splitter->addWidget(this->ui.input);
|
||||
this->ui.input->setPlaceholderText(u"Type something..."_s);
|
||||
this->ui.input->setAcceptRichText(false);
|
||||
this->ui.input->setFocus();
|
||||
|
||||
this->pluginLoadedConn =
|
||||
getApp()->getPlugins()->onPluginLoaded.connect([this](Plugin *plug) {
|
||||
if (plug->id == this->id)
|
||||
{
|
||||
this->setPlugin(plug);
|
||||
}
|
||||
});
|
||||
|
||||
this->updatePinned();
|
||||
this->themeChangedEvent();
|
||||
this->tryUpdate();
|
||||
}
|
||||
|
||||
void PluginRepl::themeChangedEvent()
|
||||
{
|
||||
this->updateFont();
|
||||
if (getTheme()->isLightTheme())
|
||||
{
|
||||
this->charFormats.self.setForeground(QColor(0x505050));
|
||||
this->charFormats.warning.setForeground(QColor(0x715100));
|
||||
this->blockFormats.warning.setBackground(QColor(0xfffbd6));
|
||||
this->charFormats.error.setForeground(QColor(0xa4000f));
|
||||
this->blockFormats.error.setBackground(QColor(0xfdf2f5));
|
||||
}
|
||||
else
|
||||
{
|
||||
this->charFormats.self.setForeground(QColor(0xa0a0a0));
|
||||
this->charFormats.warning.setForeground(QColor(0xfce2a1));
|
||||
this->blockFormats.warning.setBackground(QColor(0x42381f));
|
||||
this->charFormats.error.setForeground(QColor(0xffb3d2));
|
||||
this->blockFormats.error.setBackground(QColor(0x4b2f36));
|
||||
}
|
||||
|
||||
if (this->isPinned)
|
||||
{
|
||||
this->ui.pin->setPixmap(getResources().buttons.pinEnabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->ui.pin->setPixmap(getTheme()->buttons.pin);
|
||||
}
|
||||
|
||||
auto pal = this->palette();
|
||||
pal.setColor(QPalette::Window,
|
||||
getTheme()->tabs.selected.backgrounds.regular);
|
||||
pal.setColor(QPalette::Base, getTheme()->splits.background);
|
||||
pal.setColor(QPalette::Text, getTheme()->window.text);
|
||||
|
||||
this->ui.output->setPalette(pal);
|
||||
|
||||
pal.setColor(QPalette::Base, getTheme()->splits.input.background);
|
||||
this->ui.input->setPalette(pal);
|
||||
}
|
||||
|
||||
void PluginRepl::tryRun(QString code)
|
||||
{
|
||||
if (!this->plugin)
|
||||
{
|
||||
this->log(lua::api::LogLevel::Critical, "Plugin not loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
this->log({}, u"> "_s + code);
|
||||
|
||||
bool addedReturn = false;
|
||||
size_t maxItems = 10;
|
||||
|
||||
if (code.startsWith('!'))
|
||||
{
|
||||
maxItems = std::numeric_limits<size_t>::max();
|
||||
auto v = QStringView(code).sliced(1).trimmed();
|
||||
auto nChars = v.constData() - code.constData();
|
||||
code.remove(0, nChars);
|
||||
}
|
||||
|
||||
if (!code.startsWith(u"return "))
|
||||
{
|
||||
code.prepend(u"return ");
|
||||
addedReturn = true;
|
||||
}
|
||||
|
||||
sol::protected_function_result res;
|
||||
try
|
||||
{
|
||||
std::string u8code = code.toStdString();
|
||||
std::string chunkName = "<inline code>";
|
||||
auto result =
|
||||
this->plugin->state().load(u8code, chunkName, sol::load_mode::text);
|
||||
|
||||
if (!result.valid() && addedReturn)
|
||||
{
|
||||
u8code = u8code.substr(7); // "return "
|
||||
result = this->plugin->state().load(u8code, chunkName,
|
||||
sol::load_mode::text);
|
||||
}
|
||||
|
||||
if (!result.valid())
|
||||
{
|
||||
auto err = result.get<sol::error>();
|
||||
this->log(lua::api::LogLevel::Critical, err.what());
|
||||
return;
|
||||
}
|
||||
|
||||
auto fn = result.get<sol::optional<sol::protected_function>>();
|
||||
if (!fn)
|
||||
{
|
||||
this->log(lua::api::LogLevel::Critical,
|
||||
u"Code didn't result in a callable function."_s);
|
||||
return;
|
||||
}
|
||||
|
||||
sol::protected_function_result res = (*fn)();
|
||||
this->logResult(res, {
|
||||
.maxItems = maxItems,
|
||||
});
|
||||
}
|
||||
catch (const sol::error &err)
|
||||
{
|
||||
this->log(lua::api::LogLevel::Critical, err.what());
|
||||
}
|
||||
}
|
||||
|
||||
void PluginRepl::logResult(const sol::protected_function_result &res,
|
||||
const LogOptions &opts)
|
||||
{
|
||||
if (!res.valid())
|
||||
{
|
||||
sol::error err = res;
|
||||
this->log(lua::api::LogLevel::Critical, err.what());
|
||||
return;
|
||||
}
|
||||
if (res.return_count() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QString msg;
|
||||
for (auto it : res)
|
||||
{
|
||||
if (!msg.isEmpty())
|
||||
{
|
||||
msg.append(' ');
|
||||
}
|
||||
stringify(it, msg, opts.maxItems);
|
||||
}
|
||||
this->log(lua::api::LogLevel::Info, msg);
|
||||
}
|
||||
|
||||
void PluginRepl::log(std::optional<lua::api::LogLevel> level,
|
||||
const QString &text)
|
||||
{
|
||||
auto [charFmt, blockFmt] = [&] {
|
||||
if (!level)
|
||||
{
|
||||
return std::tie(this->charFormats.self, this->blockFormats.self);
|
||||
}
|
||||
|
||||
switch (*level)
|
||||
{
|
||||
case lua::api::LogLevel::Debug:
|
||||
return std::tie(this->charFormats.debug,
|
||||
this->blockFormats.debug);
|
||||
case lua::api::LogLevel::Warning:
|
||||
return std::tie(this->charFormats.warning,
|
||||
this->blockFormats.warning);
|
||||
case lua::api::LogLevel::Critical:
|
||||
return std::tie(this->charFormats.error,
|
||||
this->blockFormats.error);
|
||||
case lua::api::LogLevel::Info:
|
||||
default:
|
||||
return std::tie(this->charFormats.info,
|
||||
this->blockFormats.info);
|
||||
}
|
||||
}();
|
||||
|
||||
auto *bar = this->ui.output->verticalScrollBar();
|
||||
bool atBottom = bar != nullptr && bar->value() == bar->maximum();
|
||||
auto cursor = this->ui.output->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
if (this->ui.output->document()->isEmpty())
|
||||
{
|
||||
cursor.setBlockFormat(blockFmt);
|
||||
cursor.setBlockCharFormat(charFmt);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor.insertBlock(blockFmt, charFmt);
|
||||
}
|
||||
cursor.insertText(text);
|
||||
|
||||
if (atBottom)
|
||||
{
|
||||
bar->setValue(bar->maximum());
|
||||
}
|
||||
}
|
||||
|
||||
void PluginRepl::tryUpdate()
|
||||
{
|
||||
auto it = getApp()->getPlugins()->plugins().find(this->id);
|
||||
if (it == getApp()->getPlugins()->plugins().end())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!PluginController::isPluginEnabled(this->id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->setPlugin(it->second.get());
|
||||
}
|
||||
|
||||
void PluginRepl::setPlugin(Plugin *plugin)
|
||||
{
|
||||
this->plugin = plugin;
|
||||
|
||||
if (!plugin)
|
||||
{
|
||||
this->pluginDestroyConn.release();
|
||||
this->pluginLogConn.release();
|
||||
return;
|
||||
}
|
||||
|
||||
this->pluginDestroyConn = this->plugin->onUnloaded.connect([this] {
|
||||
this->setPlugin(nullptr);
|
||||
this->log({}, u"Unloaded."_s);
|
||||
});
|
||||
this->pluginLogConn =
|
||||
this->plugin->onLog.connect([this](auto level, const auto &text) {
|
||||
this->log(level, text);
|
||||
});
|
||||
|
||||
this->log({}, u"Loaded."_s);
|
||||
}
|
||||
|
||||
QFont PluginRepl::currentFont()
|
||||
{
|
||||
auto family = getSettings()->pluginRepl.fontFamily.getValue();
|
||||
auto style = getSettings()->pluginRepl.fontStyle.getValue();
|
||||
auto fontSize = getSettings()->pluginRepl.fontSize.getValue();
|
||||
if (family.isEmpty())
|
||||
{
|
||||
auto font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
font.setStyleName(style);
|
||||
font.setPointSize(fontSize);
|
||||
return font;
|
||||
}
|
||||
|
||||
return QFontDatabase::font(family, style, fontSize);
|
||||
}
|
||||
|
||||
void PluginRepl::updateFont()
|
||||
{
|
||||
this->font = PluginRepl::currentFont();
|
||||
this->ui.input->setFont(this->font);
|
||||
this->ui.output->setFont(this->font);
|
||||
|
||||
// Set the tab width to be at least 4 spaces.
|
||||
//
|
||||
// setTabStopDistance:
|
||||
// > Do not set a value less than the horizontalAdvance() of the
|
||||
// > QChar::VisualTabCharacter character, otherwise the tab-character will
|
||||
// > be drawn incompletely.
|
||||
QFontMetricsF metrics(this->font);
|
||||
auto tabCharWidth = metrics.horizontalAdvance(QChar::VisualTabCharacter);
|
||||
auto spaceWidth = metrics.horizontalAdvance(QChar::Space);
|
||||
auto tabDistance = std::max(tabCharWidth, spaceWidth * 4.F);
|
||||
|
||||
this->ui.input->setTabStopDistance(tabDistance);
|
||||
this->ui.output->setTabStopDistance(tabDistance);
|
||||
}
|
||||
|
||||
void PluginRepl::updatePinned()
|
||||
{
|
||||
this->setTopMost(this->isPinned);
|
||||
if (this->isPinned)
|
||||
{
|
||||
this->ui.pin->setPixmap(getResources().buttons.pinEnabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->ui.pin->setPixmap(getTheme()->buttons.pin);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
# include "PluginRepl.moc"
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user