File indexing completed on 2025-02-02 04:11:27

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "script_console.hpp"
0008 #include "ui_script_console.h"
0009 
0010 #include <QEvent>
0011 #include <QRegularExpression>
0012 
0013 #include <KCompletion>
0014 
0015 #include "app/settings/settings.hpp"
0016 #include "app/scripting/script_engine.hpp"
0017 #include "plugin/plugin.hpp"
0018 #include "widgets/dialogs/plugin_ui_dialog.hpp"
0019 
0020 using namespace glaxnimate::gui;
0021 using namespace glaxnimate;
0022 
0023 class ScriptConsole::Private
0024 {
0025 public:
0026     Ui::ScriptConsole ui;
0027 
0028     std::vector<app::scripting::ScriptContext> script_contexts;
0029     const plugin::Plugin* current_plugin = nullptr;
0030     ScriptConsole* parent;
0031     std::map<QString, QVariant> globals;
0032     QRegularExpression re_completion{R"(^[a-zA-Z_0-9.\s\[\]]+$)"};
0033     KCompletion completion;
0034 
0035     bool ensure_script_contexts()
0036     {
0037         if ( script_contexts.empty() )
0038         {
0039             create_script_context();
0040             if ( script_contexts.empty() )
0041                 return false;
0042         }
0043 
0044         return true;
0045     }
0046 
0047     bool execute_script ( const plugin::Plugin& plugin, const plugin::PluginScript& script, const QVariantList& args )
0048     {
0049         if ( !ensure_script_contexts() )
0050             return false;
0051 
0052         for ( const auto& ctx : script_contexts )
0053         {
0054             if ( ctx->engine() == plugin.data().engine )
0055             {
0056                 current_plugin = &plugin;
0057                 bool ok = false;
0058                 try {
0059                     ok = ctx->run_from_module(plugin.data().dir, script.module, script.function, args);
0060                     if ( !ok )
0061                         parent->error(plugin.data().name, i18n("Could not run the plugin"));
0062                 } catch ( const app::scripting::ScriptError& err ) {
0063                     console_error(err);
0064                     parent->error(plugin.data().name, i18n("Plugin raised an exception"));
0065                     ok = false;
0066                 }
0067                 current_plugin = nullptr;
0068                 return ok;
0069             }
0070         }
0071 
0072         parent->error(plugin.data().name, i18n("Could not find an interpreter"));
0073         return false;
0074     }
0075 
0076     void set_completions(const QString& prefix)
0077     {
0078         auto match = re_completion.match(prefix);
0079         if ( !match.hasMatch() )
0080         {
0081             ui.console_input->completionObject()->setItems({});
0082             return;
0083         }
0084 
0085         if ( !ensure_script_contexts() )
0086             return;
0087 
0088         int last_dot = prefix.lastIndexOf('.');
0089         QString evaluated;
0090         if ( last_dot != -1 )
0091             evaluated = prefix.left(last_dot);
0092 
0093         auto ctx = script_contexts[ui.console_language->currentIndex()].get();
0094         auto completions = ctx->eval_completions(evaluated);
0095         if ( !evaluated.isEmpty() )
0096         {
0097             for ( auto& item : completions )
0098                 item = evaluated + "." + item;
0099         }
0100         ui.console_input->completionObject()->setItems(completions);
0101     }
0102 
0103     void run_snippet(const QString& text, bool echo)
0104     {
0105         if ( !ensure_script_contexts() )
0106             return;
0107 
0108         auto c = ui.console_output->textCursor();
0109 
0110         if ( echo )
0111             console_stdout("> " + text);
0112 
0113         auto ctx = script_contexts[ui.console_language->currentIndex()].get();
0114         try {
0115             QString out = ctx->eval_to_string(text);
0116             if ( !out.isEmpty() )
0117                 console_stdout(out);
0118         } catch ( const app::scripting::ScriptError& err ) {
0119             console_error(err);
0120         }
0121 
0122         c.clearSelection();
0123         c.movePosition(QTextCursor::End);
0124         ui.console_output->setTextCursor(c);
0125     }
0126 
0127     void console_commit(QString text)
0128     {
0129         if ( text.isEmpty() )
0130             return;
0131 
0132         run_snippet(text.replace("\n", " "), true);
0133 
0134         ui.console_input->addToHistory(text);
0135         ui.console_input->clearEditText();
0136     }
0137 
0138 
0139     void console_stderr(const QString& line)
0140     {
0141         ui.console_output->setTextColor(Qt::red);
0142         ui.console_output->append(line);
0143     }
0144 
0145     void console_stdout(const QString& line)
0146     {
0147         ui.console_output->setTextColor(parent->palette().text().color());
0148         ui.console_output->append(line);
0149     }
0150 
0151     void console_error(const app::scripting::ScriptError& err)
0152     {
0153         console_stderr(err.message());
0154     }
0155 
0156     void create_script_context()
0157     {
0158         for ( const auto& engine : app::scripting::ScriptEngineFactory::instance().engines() )
0159         {
0160             auto ctx = engine->create_context();
0161 
0162             if ( !ctx )
0163                 continue;
0164 
0165             connect(ctx.get(), &app::scripting::ScriptExecutionContext::stdout_line, [this](const QString& s){ console_stdout(s);});
0166             connect(ctx.get(), &app::scripting::ScriptExecutionContext::stderr_line, [this](const QString& s){ console_stderr(s);});
0167 
0168             try {
0169                 ctx->app_module("glaxnimate");
0170                 ctx->app_module("glaxnimate_gui");
0171                 for ( const auto& p : globals )
0172                     ctx->expose(p.first, p.second);
0173             } catch ( const app::scripting::ScriptError& err ) {
0174                 console_error(err);
0175             }
0176 
0177             script_contexts.push_back(std::move(ctx));
0178         }
0179     }
0180 
0181     PluginUiDialog * create_dialog(const QString& ui_file)
0182     {
0183         if ( !current_plugin )
0184             return nullptr;
0185 
0186         if ( !current_plugin->data().dir.exists(ui_file) )
0187         {
0188             current_plugin->logger().stream(app::log::Error) << "UI file not found:" << ui_file;
0189             return nullptr;
0190         }
0191 
0192         QFile file(current_plugin->data().dir.absoluteFilePath(ui_file));
0193         if ( !file.open(QIODevice::ReadOnly) )
0194         {
0195             current_plugin->logger().stream(app::log::Error) << "Could not open UI file:" << ui_file;
0196             return nullptr;
0197         }
0198 
0199         return new PluginUiDialog(file, *current_plugin, parent);
0200     }
0201 };
0202 
0203 ScriptConsole::ScriptConsole(QWidget* parent)
0204     : QWidget(parent), d(std::make_unique<Private>())
0205 {
0206     d->ui.setupUi(this);
0207     d->parent = this;
0208 
0209     d->ui.console_input->setHistoryItems(app::settings::get<QStringList>("scripting", "history"));
0210     d->ui.console_input->completionObject()->setCompletionMode(KCompletion::CompletionPopup);
0211     d->ui.console_input->completionObject()->setOrder(KCompletion::Sorted);
0212     connect(d->ui.console_input, &KHistoryComboBox::completion, this, [this](const QString& text){ d->set_completions(text); });
0213 
0214     for ( const auto& engine : app::scripting::ScriptEngineFactory::instance().engines() )
0215     {
0216         d->ui.console_language->addItem(engine->label());
0217         if ( engine->slug() == "python" )
0218             d->ui.console_language->setCurrentIndex(d->ui.console_language->count()-1);
0219     }
0220 
0221     connect(d->ui.btn_reload, &QAbstractButton::clicked, this, &ScriptConsole::clear_contexts);
0222 }
0223 
0224 ScriptConsole::~ScriptConsole() = default;
0225 
0226 void ScriptConsole::changeEvent ( QEvent* e )
0227 {
0228     QWidget::changeEvent(e);
0229 
0230     if ( e->type() == QEvent::LanguageChange)
0231     {
0232         d->ui.retranslateUi(this);
0233     }
0234 }
0235 
0236 void ScriptConsole::console_clear()
0237 {
0238     d->ui.console_output->clear();
0239 }
0240 
0241 void ScriptConsole::console_commit(const QString& command)
0242 {
0243     d->console_commit(command);
0244 }
0245 
0246 bool ScriptConsole::execute(const plugin::Plugin& plugin, const plugin::PluginScript& script, const QVariantList& args)
0247 {
0248     return d->execute_script(plugin, script, args);
0249 }
0250 
0251 QVariant ScriptConsole::get_global(const QString& name)
0252 {
0253     auto it = d->globals.find(name);
0254     if ( it != d->globals.end() )
0255         return it->second;
0256     return {};
0257 }
0258 
0259 void ScriptConsole::set_global(const QString& name, const QVariant& value)
0260 {
0261     d->globals[name] = value;
0262 }
0263 
0264 void ScriptConsole::clear_contexts()
0265 {
0266     d->script_contexts.clear();
0267 }
0268 
0269 void ScriptConsole::clear_output()
0270 {
0271     if ( !d->ui.check_persist->isChecked() )
0272         console_clear();
0273 }
0274 
0275 PluginUiDialog* ScriptConsole::create_dialog(const QString& ui_file) const
0276 {
0277     return d->create_dialog(ui_file);
0278 }
0279 
0280 void ScriptConsole::save_settings()
0281 {
0282     QStringList history = d->ui.console_input->historyItems();
0283     int max_history = app::settings::get<int>("scripting", "max_history");
0284     if ( history.size() > max_history )
0285         history.erase(history.begin(), history.end() - max_history);
0286     app::settings::set("scripting", "history", history);
0287 }
0288 
0289 void ScriptConsole::run_snippet(const QString& source)
0290 {
0291     d->run_snippet(source, false);
0292 }